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
.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
;
41 import jexer
.bits
.Cell
;
42 import jexer
.bits
.StringUtils
;
45 * GlyphMakerFont creates glyphs as bitmaps from a font.
47 class GlyphMakerFont
{
49 // ------------------------------------------------------------------------
50 // Constants --------------------------------------------------------------
51 // ------------------------------------------------------------------------
53 // ------------------------------------------------------------------------
54 // Variables --------------------------------------------------------------
55 // ------------------------------------------------------------------------
58 * If true, enable debug messages.
60 private static boolean DEBUG
= false;
63 * If true, we were successful at getting the font dimensions.
65 private boolean gotFontDimensions
= false;
68 * The currently selected font.
70 private Font font
= null;
73 * Width of a character cell in pixels.
75 private int textWidth
= 1;
78 * Height of a character cell in pixels.
80 private int textHeight
= 1;
83 * Width of a character cell in pixels, as reported by font.
85 private int fontTextWidth
= 1;
88 * Height of a character cell in pixels, as reported by font.
90 private int fontTextHeight
= 1;
93 * Descent of a character cell in pixels.
95 private int maxDescent
= 0;
98 * System-dependent Y adjustment for text in the character cell.
100 private int textAdjustY
= 0;
103 * System-dependent X adjustment for text in the character cell.
105 private int textAdjustX
= 0;
108 * System-dependent height adjustment for text in the character cell.
110 private int textAdjustHeight
= 0;
113 * System-dependent width adjustment for text in the character cell.
115 private int textAdjustWidth
= 0;
118 * A cache of previously-rendered glyphs for blinking text, when it is
121 private HashMap
<Cell
, BufferedImage
> glyphCacheBlink
;
124 * A cache of previously-rendered glyphs for non-blinking, or
125 * blinking-and-visible, text.
127 private HashMap
<Cell
, BufferedImage
> glyphCache
;
129 // ------------------------------------------------------------------------
130 // Constructors -----------------------------------------------------------
131 // ------------------------------------------------------------------------
134 * Public constructor.
136 * @param filename the resource filename of the font to use
137 * @param fontSize the size of font to use
139 public GlyphMakerFont(final String filename
, final int fontSize
) {
141 if (filename
.length() == 0) {
143 font
= new Font(Font
.MONOSPACED
, Font
.PLAIN
, fontSize
- 2);
147 Font fontRoot
= null;
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);
165 // ------------------------------------------------------------------------
166 // GlyphMakerFont ---------------------------------------------------------
167 // ------------------------------------------------------------------------
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
177 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
178 final int cellHeight
) {
180 return getImage(cell
, cellWidth
, cellHeight
, true);
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
192 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
193 final int cellHeight
, final boolean blinkVisible
) {
195 if (gotFontDimensions
== false) {
196 // Lazy-load the text width/height and adjustments.
200 if (DEBUG
&& !font
.canDisplay(cell
.getChar())) {
201 System
.err
.println("font " + font
+ " has no glyph for " +
202 String
.format("0x%x", cell
.getChar()));
205 BufferedImage image
= null;
206 if (cell
.isBlink() && !blinkVisible
) {
207 image
= glyphCacheBlink
.get(cell
);
209 image
= glyphCache
.get(cell
);
215 // Generate glyph and draw it.
216 image
= new BufferedImage(cellWidth
, cellHeight
,
217 BufferedImage
.TYPE_INT_ARGB
);
218 Graphics2D gr2
= image
.createGraphics();
221 Cell cellColor
= new Cell(cell
);
224 if (cell
.isReverse()) {
225 cellColor
.setForeColor(cell
.getBackColor());
226 cellColor
.setBackColor(cell
.getForeColor());
229 // Draw the background rectangle, then the foreground character.
230 gr2
.setColor(SwingTerminal
.attrToBackgroundColor(cellColor
));
231 gr2
.fillRect(0, 0, cellWidth
, cellHeight
);
233 // Handle blink and underline
235 || (cell
.isBlink() && blinkVisible
)
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
);
242 if (cell
.isUnderline()) {
243 gr2
.fillRect(0, cellHeight
- 2, cellWidth
, 2);
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
);
253 glyphCache
.put(key
, image
);
257 System.err.println("cellWidth " + cellWidth +
258 " cellHeight " + cellHeight + " image " + image);
265 * Figure out my font dimensions.
267 private void getFontDimensions() {
268 glyphCacheBlink
= new HashMap
<Cell
, BufferedImage
>();
269 glyphCache
= new HashMap
<Cell
, BufferedImage
>();
271 BufferedImage image
= new BufferedImage(font
.getSize() * 2,
272 font
.getSize() * 2, BufferedImage
.TYPE_INT_ARGB
);
273 Graphics2D gr
= image
.createGraphics();
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;
282 // This produces the same number, but works better for ugly
284 fontTextHeight
= fm
.getMaxAscent() + maxDescent
- leading
;
287 textHeight
= fontTextHeight
+ textAdjustHeight
;
288 textWidth
= fontTextWidth
+ textAdjustWidth
;
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);
297 gotFontDimensions
= true;
301 * Checks if this maker's Font has a glyph for the specified character.
303 * @param codePoint the character (Unicode code point) for which a glyph
305 * @return true if this Font has a glyph for the character; false
308 public boolean canDisplay(final int codePoint
) {
309 return font
.canDisplay(codePoint
);
314 * GlyphMaker presents unified interface to all of its supported fonts to
317 public class GlyphMaker
{
319 // ------------------------------------------------------------------------
320 // Constants --------------------------------------------------------------
321 // ------------------------------------------------------------------------
324 * The mono font resource filename (terminus).
326 private static final String MONO
= "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
329 * The CJK font resource filename.
331 private static final String cjkFontFilename
= "NotoSansMonoCJKtc-Regular.otf";
334 * The emoji font resource filename.
336 private static final String emojiFontFilename
= "OpenSansEmoji.ttf";
339 * The fallback font resource filename.
341 private static final String fallbackFontFilename
= "";
343 // ------------------------------------------------------------------------
344 // Variables --------------------------------------------------------------
345 // ------------------------------------------------------------------------
348 * If true, enable debug messages.
350 private static boolean DEBUG
= false;
353 * Cache of font bundles by size.
355 private static HashMap
<Integer
, GlyphMaker
> makers
= new HashMap
<Integer
, GlyphMaker
>();
358 * The instance that has the mono (default) font.
360 private GlyphMakerFont makerMono
;
363 * The instance that has the CJK font.
365 private GlyphMakerFont makerCjk
;
368 * The instance that has the emoji font.
370 private GlyphMakerFont makerEmoji
;
373 * The instance that has the fallback font.
375 private GlyphMakerFont makerFallback
;
377 // ------------------------------------------------------------------------
378 // Constructors -----------------------------------------------------------
379 // ------------------------------------------------------------------------
382 * Create an instance with references to the necessary fonts.
384 * @param fontSize the size of these fonts in pixels
386 private GlyphMaker(final int fontSize
) {
387 makerMono
= new GlyphMakerFont(MONO
, fontSize
);
389 String fontFilename
= null;
390 fontFilename
= System
.getProperty("jexer.cjkFont.filename",
392 makerCjk
= new GlyphMakerFont(fontFilename
, fontSize
);
393 fontFilename
= System
.getProperty("jexer.emojiFont.filename",
395 makerEmoji
= new GlyphMakerFont(fontFilename
, fontSize
);
396 fontFilename
= System
.getProperty("jexer.fallbackFont.filename",
397 fallbackFontFilename
);
398 makerFallback
= new GlyphMakerFont(fontFilename
, fontSize
);
401 // ------------------------------------------------------------------------
402 // GlyphMaker -------------------------------------------------------------
403 // ------------------------------------------------------------------------
406 * Obtain the GlyphMaker instance for a particular font size.
408 * @param fontSize the size of these fonts in pixels
409 * @return the instance
411 public static GlyphMaker
getInstance(final int fontSize
) {
412 synchronized (GlyphMaker
.class) {
413 GlyphMaker maker
= makers
.get(fontSize
);
415 maker
= new GlyphMaker(fontSize
);
416 makers
.put(fontSize
, maker
);
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
430 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
431 final int cellHeight
) {
433 return getImage(cell
, cellWidth
, cellHeight
, true);
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
445 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
446 final int cellHeight
, final boolean blinkVisible
) {
448 int ch
= cell
.getChar();
449 if (StringUtils
.isCjk(ch
)) {
450 if (makerCjk
.canDisplay(ch
)) {
451 return makerCjk
.getImage(cell
, cellWidth
, cellHeight
,
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
,
463 // When all else fails, use the default.
464 if (makerMono
.canDisplay(ch
)) {
465 return makerMono
.getImage(cell
, cellWidth
, cellHeight
,
469 return makerFallback
.getImage(cell
, cellWidth
, cellHeight
,