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