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