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
;
43 * GlyphMaker creates glyphs as bitmaps from a font.
45 public class GlyphMaker
{
47 // ------------------------------------------------------------------------
48 // Constants --------------------------------------------------------------
49 // ------------------------------------------------------------------------
52 * The mono font resource filename (terminus).
54 public static final String MONO
= "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
57 * The CJK font resource filename.
59 public static final String CJK
= "NotoSansMonoCJKhk-Regular.otf";
61 // ------------------------------------------------------------------------
62 // Variables --------------------------------------------------------------
63 // ------------------------------------------------------------------------
66 * If true, enable debug messages.
68 private static boolean DEBUG
= false;
71 * The instance that has the mono (default) font.
73 private static GlyphMaker INSTANCE_MONO
;
76 * The instance that has the CJK font.
78 private static GlyphMaker INSTANCE_CJK
;
81 * If true, we were successful at getting the font dimensions.
83 private boolean gotFontDimensions
= false;
86 * The currently selected font.
88 private Font font
= null;
91 * The currently selected font size in points.
93 private int fontSize
= 16;
96 * Width of a character cell in pixels.
98 private int textWidth
= 1;
101 * Height of a character cell in pixels.
103 private int textHeight
= 1;
106 * Width of a character cell in pixels, as reported by font.
108 private int fontTextWidth
= 1;
111 * Height of a character cell in pixels, as reported by font.
113 private int fontTextHeight
= 1;
116 * Descent of a character cell in pixels.
118 private int maxDescent
= 0;
121 * System-dependent Y adjustment for text in the character cell.
123 private int textAdjustY
= 0;
126 * System-dependent X adjustment for text in the character cell.
128 private int textAdjustX
= 0;
131 * System-dependent height adjustment for text in the character cell.
133 private int textAdjustHeight
= 0;
136 * System-dependent width adjustment for text in the character cell.
138 private int textAdjustWidth
= 0;
141 * A cache of previously-rendered glyphs for blinking text, when it is
144 private HashMap
<Cell
, BufferedImage
> glyphCacheBlink
;
147 * A cache of previously-rendered glyphs for non-blinking, or
148 * blinking-and-visible, text.
150 private HashMap
<Cell
, BufferedImage
> glyphCache
;
152 // ------------------------------------------------------------------------
153 // Constructors -----------------------------------------------------------
154 // ------------------------------------------------------------------------
157 * Private constructor used by the static instance methods.
159 * @param font the font to use
161 private GlyphMaker(final Font font
) {
163 fontSize
= font
.getSize();
167 * Public constructor.
169 * @param fontName the name of the font to use
170 * @param fontSize the size of font to use
172 public GlyphMaker(final String fontName
, final int fontSize
) {
173 font
= new Font(fontName
, Font
.PLAIN
, fontSize
);
176 // ------------------------------------------------------------------------
177 // GlyphMaker -------------------------------------------------------------
178 // ------------------------------------------------------------------------
181 * Obtain the GlyphMaker instance that uses the default monospace font.
183 * @return the instance
185 public static GlyphMaker
getDefault() {
187 synchronized (GlyphMaker
.class) {
188 if (INSTANCE_MONO
!= null) {
189 return INSTANCE_MONO
;
192 int fallbackFontSize
= 16;
193 Font monoRoot
= null;
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
) {
200 monoRoot
= new Font(Font
.MONOSPACED
, Font
.PLAIN
,
202 } catch (java
.io
.IOException e
) {
204 monoRoot
= new Font(Font
.MONOSPACED
, Font
.PLAIN
,
207 INSTANCE_MONO
= new GlyphMaker(monoRoot
);
208 return INSTANCE_MONO
;
213 * Obtain the GlyphMaker instance that uses the CJK font.
215 * @return the instance
217 public static GlyphMaker
getCJK() {
219 synchronized (GlyphMaker
.class) {
220 if (INSTANCE_CJK
!= null) {
224 int fallbackFontSize
= 16;
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
) {
232 cjkRoot
= new Font(Font
.MONOSPACED
, Font
.PLAIN
,
234 } catch (java
.io
.IOException e
) {
236 cjkRoot
= new Font(Font
.MONOSPACED
, Font
.PLAIN
,
239 INSTANCE_CJK
= new GlyphMaker(cjkRoot
);
245 * Obtain the GlyphMaker instance that uses the correct font for this
248 * @param ch the character
249 * @return the instance
251 public static GlyphMaker
getInstance(final int ch
) {
252 if (((ch
>= 0x4e00) && (ch
<= 0x9fff))
253 || ((ch
>= 0x3400) && (ch
<= 0x4dbf))
254 || ((ch
>= 0x20000) && (ch
<= 0x2ebef))
262 * Get a derived font at a specific size.
264 * @param fontSize the size to use
265 * @return a new instance at that font size
267 public GlyphMaker
size(final int fontSize
) {
268 GlyphMaker maker
= new GlyphMaker(font
.deriveFont(Font
.PLAIN
,
274 * Get a glyph image, using the font's idea of cell width and height.
276 * @param cell the character to draw
277 * @return the glyph as an image
279 public BufferedImage
getImage(final Cell cell
) {
280 return getImage(cell
, textWidth
, textHeight
, true);
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
291 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
292 final int cellHeight
) {
294 return getImage(cell
, cellWidth
, cellHeight
, true);
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
306 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
307 final int cellHeight
, final boolean blinkVisible
) {
309 if (gotFontDimensions
== false) {
310 // Lazy-load the text width/height and adjustments.
314 BufferedImage image
= null;
315 if (cell
.isBlink() && !blinkVisible
) {
316 image
= glyphCacheBlink
.get(cell
);
318 image
= glyphCache
.get(cell
);
324 // Generate glyph and draw it.
325 image
= new BufferedImage(cellWidth
, cellHeight
,
326 BufferedImage
.TYPE_INT_ARGB
);
327 Graphics2D gr2
= image
.createGraphics();
330 Cell cellColor
= new Cell();
331 cellColor
.setTo(cell
);
334 if (cell
.isReverse()) {
335 cellColor
.setForeColor(cell
.getBackColor());
336 cellColor
.setBackColor(cell
.getForeColor());
339 // Draw the background rectangle, then the foreground character.
340 gr2
.setColor(SwingTerminal
.attrToBackgroundColor(cellColor
));
341 gr2
.fillRect(0, 0, cellWidth
, cellHeight
);
343 // Handle blink and underline
345 || (cell
.isBlink() && blinkVisible
)
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
);
353 if (cell
.isUnderline()) {
354 gr2
.fillRect(0, cellHeight
- 2, cellWidth
, 2);
359 // We need a new key that will not be mutated by invertCell().
360 Cell key
= new Cell();
362 if (cell
.isBlink() && !blinkVisible
) {
363 glyphCacheBlink
.put(key
, image
);
365 glyphCache
.put(key
, image
);
372 * Figure out my font dimensions.
374 private void getFontDimensions() {
375 glyphCacheBlink
= new HashMap
<Cell
, BufferedImage
>();
376 glyphCache
= new HashMap
<Cell
, BufferedImage
>();
378 BufferedImage image
= new BufferedImage(fontSize
* 2, fontSize
* 2,
379 BufferedImage
.TYPE_INT_ARGB
);
380 Graphics2D gr
= image
.createGraphics();
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;
389 // This produces the same number, but works better for ugly
391 fontTextHeight
= fm
.getMaxAscent() + maxDescent
- leading
;
394 textHeight
= fontTextHeight
+ textAdjustHeight
;
395 textWidth
= fontTextWidth
+ textAdjustWidth
;
397 gotFontDimensions
= true;