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 * GlyphMakerFont creates glyphs as bitmaps from a font.
45 class GlyphMakerFont
{
47 // ------------------------------------------------------------------------
48 // Constants --------------------------------------------------------------
49 // ------------------------------------------------------------------------
51 // ------------------------------------------------------------------------
52 // Variables --------------------------------------------------------------
53 // ------------------------------------------------------------------------
56 * If true, enable debug messages.
58 private static boolean DEBUG
= false;
61 * If true, we were successful at getting the font dimensions.
63 private boolean gotFontDimensions
= false;
66 * The currently selected font.
68 private Font font
= null;
71 * Width of a character cell in pixels.
73 private int textWidth
= 1;
76 * Height of a character cell in pixels.
78 private int textHeight
= 1;
81 * Width of a character cell in pixels, as reported by font.
83 private int fontTextWidth
= 1;
86 * Height of a character cell in pixels, as reported by font.
88 private int fontTextHeight
= 1;
91 * Descent of a character cell in pixels.
93 private int maxDescent
= 0;
96 * System-dependent Y adjustment for text in the character cell.
98 private int textAdjustY
= 0;
101 * System-dependent X adjustment for text in the character cell.
103 private int textAdjustX
= 0;
106 * System-dependent height adjustment for text in the character cell.
108 private int textAdjustHeight
= 0;
111 * System-dependent width adjustment for text in the character cell.
113 private int textAdjustWidth
= 0;
116 * A cache of previously-rendered glyphs for blinking text, when it is
119 private HashMap
<Cell
, BufferedImage
> glyphCacheBlink
;
122 * A cache of previously-rendered glyphs for non-blinking, or
123 * blinking-and-visible, text.
125 private HashMap
<Cell
, BufferedImage
> glyphCache
;
127 // ------------------------------------------------------------------------
128 // Constructors -----------------------------------------------------------
129 // ------------------------------------------------------------------------
132 * Public constructor.
134 * @param filename the resource filename of the font to use
135 * @param fontSize the size of font to use
137 public GlyphMakerFont(final String filename
, final int fontSize
) {
138 Font fontRoot
= null;
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
) {
146 font
= new Font(Font
.MONOSPACED
, Font
.PLAIN
, fontSize
);
147 } catch (java
.io
.IOException e
) {
149 font
= new Font(Font
.MONOSPACED
, Font
.PLAIN
, fontSize
);
153 // ------------------------------------------------------------------------
154 // GlyphMakerFont ---------------------------------------------------------
155 // ------------------------------------------------------------------------
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
165 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
166 final int cellHeight
) {
168 return getImage(cell
, cellWidth
, cellHeight
, true);
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
180 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
181 final int cellHeight
, final boolean blinkVisible
) {
183 if (gotFontDimensions
== false) {
184 // Lazy-load the text width/height and adjustments.
188 if (DEBUG
&& !font
.canDisplay(cell
.getChar())) {
189 System
.err
.println("font " + font
+ " has no glyph for " +
190 String
.format("0x%x", cell
.getChar()));
193 BufferedImage image
= null;
194 if (cell
.isBlink() && !blinkVisible
) {
195 image
= glyphCacheBlink
.get(cell
);
197 image
= glyphCache
.get(cell
);
203 // Generate glyph and draw it.
204 image
= new BufferedImage(cellWidth
, cellHeight
,
205 BufferedImage
.TYPE_INT_ARGB
);
206 Graphics2D gr2
= image
.createGraphics();
209 Cell cellColor
= new Cell(cell
);
212 if (cell
.isReverse()) {
213 cellColor
.setForeColor(cell
.getBackColor());
214 cellColor
.setBackColor(cell
.getForeColor());
217 // Draw the background rectangle, then the foreground character.
218 gr2
.setColor(SwingTerminal
.attrToBackgroundColor(cellColor
));
219 gr2
.fillRect(0, 0, cellWidth
, cellHeight
);
221 // Handle blink and underline
223 || (cell
.isBlink() && blinkVisible
)
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
);
230 if (cell
.isUnderline()) {
231 gr2
.fillRect(0, cellHeight
- 2, cellWidth
, 2);
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
);
241 glyphCache
.put(key
, image
);
245 System.err.println("cellWidth " + cellWidth +
246 " cellHeight " + cellHeight + " image " + image);
253 * Figure out my font dimensions.
255 private void getFontDimensions() {
256 glyphCacheBlink
= new HashMap
<Cell
, BufferedImage
>();
257 glyphCache
= new HashMap
<Cell
, BufferedImage
>();
259 BufferedImage image
= new BufferedImage(font
.getSize() * 2,
260 font
.getSize() * 2, BufferedImage
.TYPE_INT_ARGB
);
261 Graphics2D gr
= image
.createGraphics();
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;
270 // This produces the same number, but works better for ugly
272 fontTextHeight
= fm
.getMaxAscent() + maxDescent
- leading
;
275 textHeight
= fontTextHeight
+ textAdjustHeight
;
276 textWidth
= fontTextWidth
+ textAdjustWidth
;
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);
285 gotFontDimensions
= true;
291 * GlyphMaker presents unified interface to all of its supported fonts to
294 public class GlyphMaker
{
296 // ------------------------------------------------------------------------
297 // Constants --------------------------------------------------------------
298 // ------------------------------------------------------------------------
301 * The mono font resource filename (terminus).
303 private static final String MONO
= "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
306 * The CJK font resource filename.
308 private static final String cjkFontFilename
= "NotoSansMonoCJKtc-Regular.otf";
311 * The emoji font resource filename.
313 private static final String emojiFontFilename
= "OpenSansEmoji.ttf";
315 // ------------------------------------------------------------------------
316 // Variables --------------------------------------------------------------
317 // ------------------------------------------------------------------------
320 * If true, enable debug messages.
322 private static boolean DEBUG
= false;
325 * Cache of font bundles by size.
327 private static HashMap
<Integer
, GlyphMaker
> makers
= new HashMap
<Integer
, GlyphMaker
>();
330 * The instance that has the mono (default) font.
332 private GlyphMakerFont makerMono
;
335 * The instance that has the CJK font.
337 private GlyphMakerFont makerCjk
;
340 * The instance that has the emoji font.
342 private GlyphMakerFont makerEmoji
;
344 // ------------------------------------------------------------------------
345 // Constructors -----------------------------------------------------------
346 // ------------------------------------------------------------------------
349 * Create an instance with references to the necessary fonts.
351 * @param fontSize the size of these fonts in pixels
353 private GlyphMaker(final int fontSize
) {
354 makerMono
= new GlyphMakerFont(MONO
, fontSize
);
356 String fontFilename
= null;
357 fontFilename
= System
.getProperty("jexer.cjkFont.filename",
359 makerCjk
= new GlyphMakerFont(fontFilename
, fontSize
);
360 fontFilename
= System
.getProperty("jexer.emojiFont.filename",
362 makerEmoji
= new GlyphMakerFont(fontFilename
, fontSize
);
365 // ------------------------------------------------------------------------
366 // GlyphMaker -------------------------------------------------------------
367 // ------------------------------------------------------------------------
370 * Obtain the GlyphMaker instance for a particular font size.
372 * @param fontSize the size of these fonts in pixels
373 * @return the instance
375 public static GlyphMaker
getInstance(final int fontSize
) {
376 synchronized (GlyphMaker
.class) {
377 GlyphMaker maker
= makers
.get(fontSize
);
379 maker
= new GlyphMaker(fontSize
);
380 makers
.put(fontSize
, maker
);
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
394 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
395 final int cellHeight
) {
397 return getImage(cell
, cellWidth
, cellHeight
, true);
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
409 public BufferedImage
getImage(final Cell cell
, final int cellWidth
,
410 final int cellHeight
, final boolean blinkVisible
) {
412 int ch
= cell
.getChar();
413 if ((ch
>= 0x2e80) && (ch
<= 0x9fff)) {
414 return makerCjk
.getImage(cell
, cellWidth
, cellHeight
, blinkVisible
);
416 if ((ch
>= 0x1f004) && (ch
<= 0x1f9c0)) {
417 return makerEmoji
.getImage(cell
, cellWidth
, cellHeight
, blinkVisible
);
420 // When all else fails, use the default.
421 return makerMono
.getImage(cell
, cellWidth
, cellHeight
, blinkVisible
);