Merge branch 'upstream' into subtree
[nikiroo-utils.git] / backend / GlyphMaker.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.backend;
30
31 import java.awt.Font;
32 import java.awt.FontFormatException;
33 import java.awt.FontMetrics;
34 import java.awt.Graphics2D;
35 import java.awt.geom.Rectangle2D;
36 import java.awt.image.BufferedImage;
37 import java.io.InputStream;
38 import java.io.IOException;
39 import java.util.HashMap;
40
41 import jexer.bits.Cell;
42 import jexer.bits.StringUtils;
43
44 /**
45 * GlyphMakerFont creates glyphs as bitmaps from a font.
46 */
47 class GlyphMakerFont {
48
49 // ------------------------------------------------------------------------
50 // Constants --------------------------------------------------------------
51 // ------------------------------------------------------------------------
52
53 // ------------------------------------------------------------------------
54 // Variables --------------------------------------------------------------
55 // ------------------------------------------------------------------------
56
57 /**
58 * If true, enable debug messages.
59 */
60 private static boolean DEBUG = false;
61
62 /**
63 * If true, we were successful at getting the font dimensions.
64 */
65 private boolean gotFontDimensions = false;
66
67 /**
68 * The currently selected font.
69 */
70 private Font font = null;
71
72 /**
73 * Width of a character cell in pixels.
74 */
75 private int textWidth = 1;
76
77 /**
78 * Height of a character cell in pixels.
79 */
80 private int textHeight = 1;
81
82 /**
83 * Width of a character cell in pixels, as reported by font.
84 */
85 private int fontTextWidth = 1;
86
87 /**
88 * Height of a character cell in pixels, as reported by font.
89 */
90 private int fontTextHeight = 1;
91
92 /**
93 * Descent of a character cell in pixels.
94 */
95 private int maxDescent = 0;
96
97 /**
98 * System-dependent Y adjustment for text in the character cell.
99 */
100 private int textAdjustY = 0;
101
102 /**
103 * System-dependent X adjustment for text in the character cell.
104 */
105 private int textAdjustX = 0;
106
107 /**
108 * System-dependent height adjustment for text in the character cell.
109 */
110 private int textAdjustHeight = 0;
111
112 /**
113 * System-dependent width adjustment for text in the character cell.
114 */
115 private int textAdjustWidth = 0;
116
117 /**
118 * A cache of previously-rendered glyphs for blinking text, when it is
119 * not visible.
120 */
121 private HashMap<Cell, BufferedImage> glyphCacheBlink;
122
123 /**
124 * A cache of previously-rendered glyphs for non-blinking, or
125 * blinking-and-visible, text.
126 */
127 private HashMap<Cell, BufferedImage> glyphCache;
128
129 // ------------------------------------------------------------------------
130 // Constructors -----------------------------------------------------------
131 // ------------------------------------------------------------------------
132
133 /**
134 * Public constructor.
135 *
136 * @param filename the resource filename of the font to use
137 * @param fontSize the size of font to use
138 */
139 public GlyphMakerFont(final String filename, final int fontSize) {
140
141 if (filename.length() == 0) {
142 // Fallback font
143 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
144 return;
145 }
146
147 Font fontRoot = null;
148 try {
149 ClassLoader loader = Thread.currentThread().getContextClassLoader();
150 InputStream in = loader.getResourceAsStream(filename);
151 fontRoot = Font.createFont(Font.TRUETYPE_FONT, in);
152 font = fontRoot.deriveFont(Font.PLAIN, fontSize - 2);
153 } catch (FontFormatException e) {
154 // Ideally we would report an error here, either via System.err
155 // or TExceptionDialog. However, I do not want GlyphMaker to
156 // know about available backends, so we quietly fallback to
157 // whatever is available as MONO.
158 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
159 } catch (IOException e) {
160 // See comment above.
161 font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
162 }
163 }
164
165 // ------------------------------------------------------------------------
166 // GlyphMakerFont ---------------------------------------------------------
167 // ------------------------------------------------------------------------
168
169 /**
170 * Get a glyph image.
171 *
172 * @param cell the character to draw
173 * @param cellWidth the width of the text cell to draw into
174 * @param cellHeight the height of the text cell to draw into
175 * @return the glyph as an image
176 */
177 public BufferedImage getImage(final Cell cell, final int cellWidth,
178 final int cellHeight) {
179
180 return getImage(cell, cellWidth, cellHeight, true);
181 }
182
183 /**
184 * Get a glyph image.
185 *
186 * @param cell the character to draw
187 * @param cellWidth the width of the text cell to draw into
188 * @param cellHeight the height of the text cell to draw into
189 * @param blinkVisible if true, the cell is visible if it is blinking
190 * @return the glyph as an image
191 */
192 public BufferedImage getImage(final Cell cell, final int cellWidth,
193 final int cellHeight, final boolean blinkVisible) {
194
195 if (gotFontDimensions == false) {
196 // Lazy-load the text width/height and adjustments.
197 getFontDimensions();
198 }
199
200 if (DEBUG && !font.canDisplay(cell.getChar())) {
201 System.err.println("font " + font + " has no glyph for " +
202 String.format("0x%x", cell.getChar()));
203 }
204
205 BufferedImage image = null;
206 if (cell.isBlink() && !blinkVisible) {
207 image = glyphCacheBlink.get(cell);
208 } else {
209 image = glyphCache.get(cell);
210 }
211 if (image != null) {
212 return image;
213 }
214
215 // Generate glyph and draw it.
216 image = new BufferedImage(cellWidth, cellHeight,
217 BufferedImage.TYPE_INT_ARGB);
218 Graphics2D gr2 = image.createGraphics();
219 gr2.setFont(font);
220
221 Cell cellColor = new Cell(cell);
222
223 // Check for reverse
224 if (cell.isReverse()) {
225 cellColor.setForeColor(cell.getBackColor());
226 cellColor.setBackColor(cell.getForeColor());
227 }
228
229 // Draw the background rectangle, then the foreground character.
230 gr2.setColor(SwingTerminal.attrToBackgroundColor(cellColor));
231 gr2.fillRect(0, 0, cellWidth, cellHeight);
232
233 // Handle blink and underline
234 if (!cell.isBlink()
235 || (cell.isBlink() && blinkVisible)
236 ) {
237 gr2.setColor(SwingTerminal.attrToForegroundColor(cellColor));
238 char [] chars = Character.toChars(cell.getChar());
239 gr2.drawChars(chars, 0, chars.length, textAdjustX,
240 cellHeight - maxDescent + textAdjustY);
241
242 if (cell.isUnderline()) {
243 gr2.fillRect(0, cellHeight - 2, cellWidth, 2);
244 }
245 }
246 gr2.dispose();
247
248 // We need a new key that will not be mutated by invertCell().
249 Cell key = new Cell(cell);
250 if (cell.isBlink() && !blinkVisible) {
251 glyphCacheBlink.put(key, image);
252 } else {
253 glyphCache.put(key, image);
254 }
255
256 /*
257 System.err.println("cellWidth " + cellWidth +
258 " cellHeight " + cellHeight + " image " + image);
259 */
260
261 return image;
262 }
263
264 /**
265 * Figure out my font dimensions.
266 */
267 private void getFontDimensions() {
268 glyphCacheBlink = new HashMap<Cell, BufferedImage>();
269 glyphCache = new HashMap<Cell, BufferedImage>();
270
271 BufferedImage image = new BufferedImage(font.getSize() * 2,
272 font.getSize() * 2, BufferedImage.TYPE_INT_ARGB);
273 Graphics2D gr = image.createGraphics();
274 gr.setFont(font);
275 FontMetrics fm = gr.getFontMetrics();
276 maxDescent = fm.getMaxDescent();
277 Rectangle2D bounds = fm.getMaxCharBounds(gr);
278 int leading = fm.getLeading();
279 fontTextWidth = (int)Math.round(bounds.getWidth());
280 // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
281
282 // This produces the same number, but works better for ugly
283 // monospace.
284 fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
285 gr.dispose();
286
287 textHeight = fontTextHeight + textAdjustHeight;
288 textWidth = fontTextWidth + textAdjustWidth;
289 /*
290 System.err.println("font " + font);
291 System.err.println("fontTextWidth " + fontTextWidth);
292 System.err.println("fontTextHeight " + fontTextHeight);
293 System.err.println("textWidth " + textWidth);
294 System.err.println("textHeight " + textHeight);
295 */
296
297 gotFontDimensions = true;
298 }
299
300 /**
301 * Checks if this maker's Font has a glyph for the specified character.
302 *
303 * @param codePoint the character (Unicode code point) for which a glyph
304 * is needed.
305 * @return true if this Font has a glyph for the character; false
306 * otherwise.
307 */
308 public boolean canDisplay(final int codePoint) {
309 return font.canDisplay(codePoint);
310 }
311 }
312
313 /**
314 * GlyphMaker presents unified interface to all of its supported fonts to
315 * clients.
316 */
317 public class GlyphMaker {
318
319 // ------------------------------------------------------------------------
320 // Constants --------------------------------------------------------------
321 // ------------------------------------------------------------------------
322
323 /**
324 * The mono font resource filename (terminus).
325 */
326 private static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
327
328 /**
329 * The CJK font resource filename.
330 */
331 private static final String cjkFontFilename = "NotoSansMonoCJKtc-Regular.otf";
332
333 /**
334 * The emoji font resource filename.
335 */
336 private static final String emojiFontFilename = "OpenSansEmoji.ttf";
337
338 /**
339 * The fallback font resource filename.
340 */
341 private static final String fallbackFontFilename = "";
342
343 // ------------------------------------------------------------------------
344 // Variables --------------------------------------------------------------
345 // ------------------------------------------------------------------------
346
347 /**
348 * If true, enable debug messages.
349 */
350 private static boolean DEBUG = false;
351
352 /**
353 * Cache of font bundles by size.
354 */
355 private static HashMap<Integer, GlyphMaker> makers = new HashMap<Integer, GlyphMaker>();
356
357 /**
358 * The instance that has the mono (default) font.
359 */
360 private GlyphMakerFont makerMono;
361
362 /**
363 * The instance that has the CJK font.
364 */
365 private GlyphMakerFont makerCjk;
366
367 /**
368 * The instance that has the emoji font.
369 */
370 private GlyphMakerFont makerEmoji;
371
372 /**
373 * The instance that has the fallback font.
374 */
375 private GlyphMakerFont makerFallback;
376
377 // ------------------------------------------------------------------------
378 // Constructors -----------------------------------------------------------
379 // ------------------------------------------------------------------------
380
381 /**
382 * Create an instance with references to the necessary fonts.
383 *
384 * @param fontSize the size of these fonts in pixels
385 */
386 private GlyphMaker(final int fontSize) {
387 makerMono = new GlyphMakerFont(MONO, fontSize);
388
389 String fontFilename = null;
390 fontFilename = System.getProperty("jexer.cjkFont.filename",
391 cjkFontFilename);
392 makerCjk = new GlyphMakerFont(fontFilename, fontSize);
393 fontFilename = System.getProperty("jexer.emojiFont.filename",
394 emojiFontFilename);
395 makerEmoji = new GlyphMakerFont(fontFilename, fontSize);
396 fontFilename = System.getProperty("jexer.fallbackFont.filename",
397 fallbackFontFilename);
398 makerFallback = new GlyphMakerFont(fontFilename, fontSize);
399 }
400
401 // ------------------------------------------------------------------------
402 // GlyphMaker -------------------------------------------------------------
403 // ------------------------------------------------------------------------
404
405 /**
406 * Obtain the GlyphMaker instance for a particular font size.
407 *
408 * @param fontSize the size of these fonts in pixels
409 * @return the instance
410 */
411 public static GlyphMaker getInstance(final int fontSize) {
412 synchronized (GlyphMaker.class) {
413 GlyphMaker maker = makers.get(fontSize);
414 if (maker == null) {
415 maker = new GlyphMaker(fontSize);
416 makers.put(fontSize, maker);
417 }
418 return maker;
419 }
420 }
421
422 /**
423 * Get a glyph image.
424 *
425 * @param cell the character to draw
426 * @param cellWidth the width of the text cell to draw into
427 * @param cellHeight the height of the text cell to draw into
428 * @return the glyph as an image
429 */
430 public BufferedImage getImage(final Cell cell, final int cellWidth,
431 final int cellHeight) {
432
433 return getImage(cell, cellWidth, cellHeight, true);
434 }
435
436 /**
437 * Get a glyph image.
438 *
439 * @param cell the character to draw
440 * @param cellWidth the width of the text cell to draw into
441 * @param cellHeight the height of the text cell to draw into
442 * @param blinkVisible if true, the cell is visible if it is blinking
443 * @return the glyph as an image
444 */
445 public BufferedImage getImage(final Cell cell, final int cellWidth,
446 final int cellHeight, final boolean blinkVisible) {
447
448 int ch = cell.getChar();
449 if (StringUtils.isCjk(ch)) {
450 if (makerCjk.canDisplay(ch)) {
451 return makerCjk.getImage(cell, cellWidth, cellHeight,
452 blinkVisible);
453 }
454 }
455 if (StringUtils.isEmoji(ch)) {
456 if (makerEmoji.canDisplay(ch)) {
457 // System.err.println("emoji: " + String.format("0x%x", ch));
458 return makerEmoji.getImage(cell, cellWidth, cellHeight,
459 blinkVisible);
460 }
461 }
462
463 // When all else fails, use the default.
464 if (makerMono.canDisplay(ch)) {
465 return makerMono.getImage(cell, cellWidth, cellHeight,
466 blinkVisible);
467 }
468
469 return makerFallback.getImage(cell, cellWidth, cellHeight,
470 blinkVisible);
471 }
472
473 }