2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
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
;
40 import jexer
.bits
.Cell
;
41 import jexer
.bits
.StringUtils
;
44 * GlyphMakerFont creates glyphs as bitmaps from a font.
46 class GlyphMakerFont
{
48 // ------------------------------------------------------------------------
49 // Constants --------------------------------------------------------------
50 // ------------------------------------------------------------------------
52 // ------------------------------------------------------------------------
53 // Variables --------------------------------------------------------------
54 // ------------------------------------------------------------------------
57 * If true, enable debug messages.
59 private static boolean DEBUG
= false;
62 * If true, we were successful at getting the font dimensions.
64 private boolean gotFontDimensions
= false;
67 * The currently selected font.
69 private Font font
= null;
72 * Width of a character cell in pixels.
74 private int textWidth
= 1;
77 * Height of a character cell in pixels.
79 private int textHeight
= 1;
82 * Width of a character cell in pixels, as reported by font.
84 private int fontTextWidth
= 1;
87 * Height of a character cell in pixels, as reported by font.
89 private int fontTextHeight
= 1;
92 * Descent of a character cell in pixels.
94 private int maxDescent
= 0;
97 * System-dependent Y adjustment for text in the character cell.
99 private int textAdjustY
= 0;
102 * System-dependent X adjustment for text in the character cell.
104 private int textAdjustX
= 0;
107 * System-dependent height adjustment for text in the character cell.
109 private int textAdjustHeight
= 0;
112 * System-dependent width adjustment for text in the character cell.
114 private int textAdjustWidth
= 0;
117 * A cache of previously-rendered glyphs for blinking text, when it is
120 private HashMap
<Cell
, BufferedImage
> glyphCacheBlink
;
123 * A cache of previously-rendered glyphs for non-blinking, or
124 * blinking-and-visible, text.
126 private HashMap
<Cell
, BufferedImage
> glyphCache
;
128 // ------------------------------------------------------------------------
129 // Constructors -----------------------------------------------------------
130 // ------------------------------------------------------------------------
133 * Public constructor.
135 * @param filename the resource filename of the font to use
136 * @param fontSize the size of font to use
138 public GlyphMakerFont(final String filename
, final int fontSize
) {
140 if (filename
.length() == 0) {
142 font
= new Font(Font
.MONOSPACED
, Font
.PLAIN
, fontSize
);
146 Font fontRoot
= null;
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
);
164 // ------------------------------------------------------------------------
165 // GlyphMakerFont ---------------------------------------------------------
166 // ------------------------------------------------------------------------
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
176 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
177 final int cellHeight
) {
179 return getImage(cell
, cellWidth
, cellHeight
, true);
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
191 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
192 final int cellHeight
, final boolean blinkVisible
) {
194 if (gotFontDimensions
== false) {
195 // Lazy-load the text width/height and adjustments.
199 if (DEBUG
&& !font
.canDisplay(cell
.getChar())) {
200 System
.err
.println("font " + font
+ " has no glyph for " +
201 String
.format("0x%x", cell
.getChar()));
204 BufferedImage image
= null;
205 if (cell
.isBlink() && !blinkVisible
) {
206 image
= glyphCacheBlink
.get(cell
);
208 image
= glyphCache
.get(cell
);
214 // Generate glyph and draw it.
215 image
= new BufferedImage(cellWidth
, cellHeight
,
216 BufferedImage
.TYPE_INT_ARGB
);
217 Graphics2D gr2
= image
.createGraphics();
220 Cell cellColor
= new Cell(cell
);
223 if (cell
.isReverse()) {
224 cellColor
.setForeColor(cell
.getBackColor());
225 cellColor
.setBackColor(cell
.getForeColor());
228 // Draw the background rectangle, then the foreground character.
229 gr2
.setColor(SwingTerminal
.attrToBackgroundColor(cellColor
));
230 gr2
.fillRect(0, 0, cellWidth
, cellHeight
);
232 // Handle blink and underline
234 || (cell
.isBlink() && blinkVisible
)
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
);
241 if (cell
.isUnderline()) {
242 gr2
.fillRect(0, cellHeight
- 2, cellWidth
, 2);
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
);
252 glyphCache
.put(key
, image
);
256 System.err.println("cellWidth " + cellWidth +
257 " cellHeight " + cellHeight + " image " + image);
264 * Figure out my font dimensions.
266 private void getFontDimensions() {
267 glyphCacheBlink
= new HashMap
<Cell
, BufferedImage
>();
268 glyphCache
= new HashMap
<Cell
, BufferedImage
>();
270 BufferedImage image
= new BufferedImage(font
.getSize() * 2,
271 font
.getSize() * 2, BufferedImage
.TYPE_INT_ARGB
);
272 Graphics2D gr
= image
.createGraphics();
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;
281 // This produces the same number, but works better for ugly
283 fontTextHeight
= fm
.getMaxAscent() + maxDescent
- leading
;
286 textHeight
= fontTextHeight
+ textAdjustHeight
;
287 textWidth
= fontTextWidth
+ textAdjustWidth
;
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);
296 gotFontDimensions
= true;
300 * Checks if this maker's Font has a glyph for the specified character.
302 * @param codePoint the character (Unicode code point) for which a glyph
304 * @return true if this Font has a glyph for the character; false
307 public boolean canDisplay(final int codePoint
) {
308 return font
.canDisplay(codePoint
);
313 * GlyphMaker presents unified interface to all of its supported fonts to
316 public class GlyphMaker
{
318 // ------------------------------------------------------------------------
319 // Constants --------------------------------------------------------------
320 // ------------------------------------------------------------------------
323 * The mono font resource filename (terminus).
325 private static final String MONO
= "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
328 * The CJK font resource filename.
330 private static final String cjkFontFilename
= "NotoSansMonoCJKtc-Regular.otf";
333 * The emoji font resource filename.
335 private static final String emojiFontFilename
= "OpenSansEmoji.ttf";
338 * The fallback font resource filename.
340 private static final String fallbackFontFilename
= "";
342 // ------------------------------------------------------------------------
343 // Variables --------------------------------------------------------------
344 // ------------------------------------------------------------------------
347 * If true, enable debug messages.
349 private static boolean DEBUG
= false;
352 * Cache of font bundles by size.
354 private static HashMap
<Integer
, GlyphMaker
> makers
= new HashMap
<Integer
, GlyphMaker
>();
357 * The instance that has the mono (default) font.
359 private GlyphMakerFont makerMono
;
362 * The instance that has the CJK font.
364 private GlyphMakerFont makerCjk
;
367 * The instance that has the emoji font.
369 private GlyphMakerFont makerEmoji
;
372 * The instance that has the fallback font.
374 private GlyphMakerFont makerFallback
;
376 // ------------------------------------------------------------------------
377 // Constructors -----------------------------------------------------------
378 // ------------------------------------------------------------------------
381 * Create an instance with references to the necessary fonts.
383 * @param fontSize the size of these fonts in pixels
385 private GlyphMaker(final int fontSize
) {
386 makerMono
= new GlyphMakerFont(MONO
, fontSize
);
388 String fontFilename
= null;
389 fontFilename
= System
.getProperty("jexer.cjkFont.filename",
391 makerCjk
= new GlyphMakerFont(fontFilename
, fontSize
);
392 fontFilename
= System
.getProperty("jexer.emojiFont.filename",
394 makerEmoji
= new GlyphMakerFont(fontFilename
, fontSize
);
395 fontFilename
= System
.getProperty("jexer.fallbackFont.filename",
396 fallbackFontFilename
);
397 makerFallback
= new GlyphMakerFont(fontFilename
, fontSize
);
400 // ------------------------------------------------------------------------
401 // GlyphMaker -------------------------------------------------------------
402 // ------------------------------------------------------------------------
405 * Obtain the GlyphMaker instance for a particular font size.
407 * @param fontSize the size of these fonts in pixels
408 * @return the instance
410 public static GlyphMaker
getInstance(final int fontSize
) {
411 synchronized (GlyphMaker
.class) {
412 GlyphMaker maker
= makers
.get(fontSize
);
414 maker
= new GlyphMaker(fontSize
);
415 makers
.put(fontSize
, maker
);
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
429 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
430 final int cellHeight
) {
432 return getImage(cell
, cellWidth
, cellHeight
, true);
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
444 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
445 final int cellHeight
, final boolean blinkVisible
) {
447 int ch
= cell
.getChar();
448 if (StringUtils
.isCjk(ch
)) {
449 if (makerCjk
.canDisplay(ch
)) {
450 return makerCjk
.getImage(cell
, cellWidth
, cellHeight
,
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
,
462 // When all else fails, use the default.
463 if (makerMono
.canDisplay(ch
)) {
464 return makerMono
.getImage(cell
, cellWidth
, cellHeight
,
468 return makerFallback
.getImage(cell
, cellWidth
, cellHeight
,