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