#35 CJK font wip
authorKevin Lamonte <kevin.lamonte@gmail.com>
Tue, 6 Aug 2019 04:15:58 +0000 (23:15 -0500)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Tue, 6 Aug 2019 04:15:58 +0000 (23:15 -0500)
src/jexer/TTerminalWindow.java
src/jexer/backend/GlyphMaker.java
src/jexer/bits/Cell.java
src/jexer/bits/StringUtils.java
src/jexer/tterminal/ECMA48.java

index c7456358df7193db8d8be27481ecac6133e863a5..6d06fc88530f4d698c99d6dc8c5244949e0df1cc 100644 (file)
@@ -359,6 +359,12 @@ public class TTerminalWindow extends TScrollableWindow
                 }
                 for (int i = 0; i < widthMax; i++) {
                     Cell ch = line.charAt(i);
+
+                    if (ch.isImage()) {
+                        putCharXY(i + 1, row, ch);
+                        continue;
+                    }
+
                     Cell newCell = new Cell();
                     newCell.setTo(ch);
                     boolean reverse = line.isReverseColor() ^ ch.isReverse();
@@ -1032,7 +1038,7 @@ public class TTerminalWindow extends TScrollableWindow
      * The double-width font will be 2x this value.
      */
     private void setupFont(final int fontSize) {
-        doubleFont = GlyphMaker.getDefault().size(fontSize * 2);
+        doubleFont = GlyphMaker.getInstance(fontSize * 2);
 
         // Special case: the ECMA48 backend needs to have a timer to drive
         // its blink state.
index 0094145dafa811c34242530a94c64404f3639e7a..0c798b1d7e1fdd48eb7d0df0b2f10f45326c5231 100644 (file)
@@ -40,24 +40,14 @@ import java.util.HashMap;
 import jexer.bits.Cell;
 
 /**
- * GlyphMaker creates glyphs as bitmaps from a font.
+ * GlyphMakerFont creates glyphs as bitmaps from a font.
  */
-public class GlyphMaker {
+class GlyphMakerFont {
 
     // ------------------------------------------------------------------------
     // Constants --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
-    /**
-     * The mono font resource filename (terminus).
-     */
-    public static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
-
-    /**
-     * The CJK font resource filename.
-     */
-    public static final String CJK = "NotoSansMonoCJKhk-Regular.otf";
-
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -67,16 +57,6 @@ public class GlyphMaker {
      */
     private static boolean DEBUG = false;
 
-    /**
-     * The instance that has the mono (default) font.
-     */
-    private static GlyphMaker INSTANCE_MONO;
-
-    /**
-     * The instance that has the CJK font.
-     */
-    private static GlyphMaker INSTANCE_CJK;
-
     /**
      * If true, we were successful at getting the font dimensions.
      */
@@ -87,11 +67,6 @@ public class GlyphMaker {
      */
     private Font font = null;
 
-    /**
-     * The currently selected font size in points.
-     */
-    private int fontSize = 16;
-
     /**
      * Width of a character cell in pixels.
      */
@@ -153,133 +128,32 @@ public class GlyphMaker {
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
-    /**
-     * Private constructor used by the static instance methods.
-     *
-     * @param font the font to use
-     */
-    private GlyphMaker(final Font font) {
-        this.font = font;
-        fontSize = font.getSize();
-    }
-
     /**
      * Public constructor.
      *
-     * @param fontName the name of the font to use
+     * @param filename the resource filename of the font to use
      * @param fontSize the size of font to use
      */
-    public GlyphMaker(final String fontName, final int fontSize) {
-        font = new Font(fontName, Font.PLAIN, fontSize);
+    public GlyphMakerFont(final String filename, final int fontSize) {
+        Font fontRoot = null;
+        try {
+            ClassLoader loader = Thread.currentThread().getContextClassLoader();
+            InputStream in = loader.getResourceAsStream(filename);
+            fontRoot = Font.createFont(Font.TRUETYPE_FONT, in);
+            font = fontRoot.deriveFont(Font.PLAIN, fontSize);
+        } catch (java.awt.FontFormatException e) {
+            e.printStackTrace();
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+        } catch (java.io.IOException e) {
+            e.printStackTrace();
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+        }
     }
 
     // ------------------------------------------------------------------------
-    // GlyphMaker -------------------------------------------------------------
+    // GlyphMakerFont ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
-    /**
-     * Obtain the GlyphMaker instance that uses the default monospace font.
-     *
-     * @return the instance
-     */
-    public static GlyphMaker getDefault() {
-
-        synchronized (GlyphMaker.class) {
-            if (INSTANCE_MONO != null) {
-                return INSTANCE_MONO;
-            }
-
-            int fallbackFontSize = 16;
-            Font monoRoot = null;
-            try {
-                ClassLoader loader = Thread.currentThread().getContextClassLoader();
-                InputStream in = loader.getResourceAsStream(MONO);
-                monoRoot = Font.createFont(Font.TRUETYPE_FONT, in);
-            } catch (java.awt.FontFormatException e) {
-                e.printStackTrace();
-                monoRoot = new Font(Font.MONOSPACED, Font.PLAIN,
-                    fallbackFontSize);
-            } catch (java.io.IOException e) {
-                e.printStackTrace();
-                monoRoot = new Font(Font.MONOSPACED, Font.PLAIN,
-                    fallbackFontSize);
-            }
-            INSTANCE_MONO = new GlyphMaker(monoRoot);
-            return INSTANCE_MONO;
-        }
-    }
-
-    /**
-     * Obtain the GlyphMaker instance that uses the CJK font.
-     *
-     * @return the instance
-     */
-    public static GlyphMaker getCJK() {
-
-        synchronized (GlyphMaker.class) {
-            if (INSTANCE_CJK != null) {
-                return INSTANCE_CJK;
-            }
-
-            int fallbackFontSize = 16;
-            Font cjkRoot = null;
-            try {
-                ClassLoader loader = Thread.currentThread().getContextClassLoader();
-                InputStream in = loader.getResourceAsStream(CJK);
-                cjkRoot = Font.createFont(Font.TRUETYPE_FONT, in);
-            } catch (java.awt.FontFormatException e) {
-                e.printStackTrace();
-                cjkRoot = new Font(Font.MONOSPACED, Font.PLAIN,
-                    fallbackFontSize);
-            } catch (java.io.IOException e) {
-                e.printStackTrace();
-                cjkRoot = new Font(Font.MONOSPACED, Font.PLAIN,
-                    fallbackFontSize);
-            }
-            INSTANCE_CJK = new GlyphMaker(cjkRoot);
-            return INSTANCE_CJK;
-        }
-    }
-
-    /**
-     * Obtain the GlyphMaker instance that uses the correct font for this
-     * character.
-     *
-     * @param ch the character
-     * @return the instance
-     */
-    public static GlyphMaker getInstance(final int ch) {
-        if (((ch >= 0x4e00) && (ch <= 0x9fff))
-            || ((ch >= 0x3400) && (ch <= 0x4dbf))
-            || ((ch >= 0x20000) && (ch <= 0x2ebef))
-        ) {
-            return getCJK();
-        }
-        return getDefault();
-    }
-
-    /**
-     * Get a derived font at a specific size.
-     *
-     * @param fontSize the size to use
-     * @return a new instance at that font size
-     */
-    public GlyphMaker size(final int fontSize) {
-        GlyphMaker maker = new GlyphMaker(font.deriveFont(Font.PLAIN,
-                fontSize));
-        return maker;
-    }
-
-    /**
-     * Get a glyph image, using the font's idea of cell width and height.
-     *
-     * @param cell the character to draw
-     * @return the glyph as an image
-     */
-    public BufferedImage getImage(final Cell cell) {
-        return getImage(cell, textWidth, textHeight, true);
-    }
-
     /**
      * Get a glyph image.
      *
@@ -375,8 +249,8 @@ public class GlyphMaker {
         glyphCacheBlink = new HashMap<Cell, BufferedImage>();
         glyphCache = new HashMap<Cell, BufferedImage>();
 
-        BufferedImage image = new BufferedImage(fontSize * 2, fontSize * 2,
-            BufferedImage.TYPE_INT_ARGB);
+        BufferedImage image = new BufferedImage(font.getSize() * 2,
+            font.getSize() * 2, BufferedImage.TYPE_INT_ARGB);
         Graphics2D gr = image.createGraphics();
         gr.setFont(font);
         FontMetrics fm = gr.getFontMetrics();
@@ -398,3 +272,149 @@ public class GlyphMaker {
     }
 
 }
+
+/**
+ * GlyphMaker presents unified interface to all of its supported fonts to
+ * clients.
+ */
+public class GlyphMaker {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The mono font resource filename (terminus).
+     */
+    private static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+
+    /**
+     * The CJKhk font resource filename.
+     */
+    // private static final String CJKhk = "NotoSansMonoCJKhk-Regular.otf";
+
+    /**
+     * The CJKkr font resource filename.
+     */
+    // private static final String CJKkr = "NotoSansMonoCJKkr-Regular.otf";
+
+    /**
+     * The CJKtc font resource filename.
+     */
+    private static final String CJKtc = "NotoSansMonoCJKtc-Regular.otf";
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * If true, enable debug messages.
+     */
+    private static boolean DEBUG = false;
+
+    /**
+     * Cache of font bundles by size.
+     */
+    private static HashMap<Integer, GlyphMaker> makers = new HashMap<Integer, GlyphMaker>();
+
+    /**
+     * The instance that has the mono (default) font.
+     */
+    private GlyphMakerFont makerMono;
+
+    /**
+     * The instance that has the CJKhk font.
+     */
+    // private GlyphMakerFont makerCJKhk;
+
+    /**
+     * The instance that has the CJKkr font.
+     */
+    // private GlyphMakerFont makerCJKkr;
+
+    /**
+     * The instance that has the CJKtc font.
+     */
+    private GlyphMakerFont makerCJKtc;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Create an instance with references to the necessary fonts.
+     *
+     * @param fontSize the size of these fonts in pixels
+     */
+    private GlyphMaker(final int fontSize) {
+        makerMono = new GlyphMakerFont(MONO, fontSize);
+        // makerCJKhk = new GlyphMakerFont(CJKhk, fontSize);
+        // makerCJKkr = new GlyphMakerFont(CJKkr, fontSize);
+        makerCJKtc = new GlyphMakerFont(CJKtc, fontSize);
+    }
+
+    // ------------------------------------------------------------------------
+    // GlyphMaker -------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Obtain the GlyphMaker instance for a particular font size.
+     *
+     * @param fontSize the size of these fonts in pixels
+     * @return the instance
+     */
+    public static GlyphMaker getInstance(final int fontSize) {
+        synchronized (GlyphMaker.class) {
+            GlyphMaker maker = makers.get(fontSize);
+            if (maker == null) {
+                maker = new GlyphMaker(fontSize);
+                makers.put(fontSize, maker);
+            }
+            return maker;
+        }
+    }
+
+    /**
+     * Get a glyph image.
+     *
+     * @param cell the character to draw
+     * @param cellWidth the width of the text cell to draw into
+     * @param cellHeight the height of the text cell to draw into
+     * @return the glyph as an image
+     */
+    public BufferedImage getImage(final Cell cell, final int cellWidth,
+        final int cellHeight) {
+
+        return getImage(cell, cellWidth, cellHeight, true);
+    }
+
+    /**
+     * Get a glyph image.
+     *
+     * @param cell the character to draw
+     * @param cellWidth the width of the text cell to draw into
+     * @param cellHeight the height of the text cell to draw into
+     * @param blinkVisible if true, the cell is visible if it is blinking
+     * @return the glyph as an image
+     */
+    public BufferedImage getImage(final Cell cell, final int cellWidth,
+        final int cellHeight, final boolean blinkVisible) {
+
+        char ch = cell.getChar();
+        /*
+        if ((ch >= 0x4e00) && (ch <= 0x9fff)) {
+            return makerCJKhk.getImage(cell, cellWidth, cellHeight, blinkVisible);
+        }
+        if ((ch >= 0x4e00) && (ch <= 0x9fff)) {
+            return makerCJKkr.getImage(cell, cellWidth, cellHeight, blinkVisible);
+        }
+         */
+        if ((ch >= 0x2e80) && (ch <= 0x9fff)) {
+            return makerCJKtc.getImage(cell, cellWidth, cellHeight, blinkVisible);
+        }
+
+        // When all else fails, use the default.
+        return makerMono.getImage(cell, cellWidth, cellHeight, blinkVisible);
+    }
+
+}
index d4c816f41ee88e202bf9e509af30b47630a97482..5db5f4387bdcfbf00f785834918d16407f488547 100644 (file)
@@ -40,6 +40,26 @@ public final class Cell extends CellAttributes {
     // Constants --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * How this cell needs to be displayed if it is part of a larger glyph.
+     */
+    public enum Width {
+        /**
+         * This cell is an entire glyph on its own.
+         */
+        SINGLE,
+
+        /**
+         * This cell is the left half of a wide glyph.
+         */
+        LEFT,
+
+        /**
+         * This cell is the right half of a wide glyph.
+         */
+        RIGHT,
+    }
+
     /**
      * The special "this cell is unset" (null) value.  This is the Unicode
      * "not a character" value.
@@ -55,6 +75,11 @@ public final class Cell extends CellAttributes {
      */
     private char ch;
 
+    /**
+     * The display width of this cell.
+     */
+    private Width width = Width.SINGLE;
+
     /**
      * The image at this cell.
      */
@@ -113,7 +138,6 @@ public final class Cell extends CellAttributes {
     // Cell -------------------------------------------------------------------
     // ------------------------------------------------------------------------
 
-
     /**
      * Set the image data for this cell.
      *
@@ -122,6 +146,7 @@ public final class Cell extends CellAttributes {
     public void setImage(final BufferedImage image) {
         this.image = image;
         imageHashCode = image.hashCode();
+        width = Width.SINGLE;
     }
 
     /**
@@ -223,6 +248,25 @@ public final class Cell extends CellAttributes {
         this.ch = ch;
     }
 
+    /**
+     * Getter for cell width.
+     *
+     * @return Width.SINGLE, Width.LEFT, or Width.RIGHT
+     */
+    public Width getWidth() {
+        return width;
+    }
+
+    /**
+     * Setter for cell width.
+     *
+     * @param ch new cell width, one of Width.SINGLE, Width.LEFT, or
+     * Width.RIGHT
+     */
+    public void setWidth(final Width width) {
+        this.width = width;
+    }
+
     /**
      * Reset this cell to a blank.
      */
@@ -230,6 +274,7 @@ public final class Cell extends CellAttributes {
     public void reset() {
         super.reset();
         ch = ' ';
+        width = Width.SINGLE;
         image = null;
         imageHashCode = 0;
         invertedImage = null;
@@ -244,6 +289,7 @@ public final class Cell extends CellAttributes {
     public void unset() {
         super.reset();
         ch = UNSET_VALUE;
+        width = Width.SINGLE;
         image = null;
         imageHashCode = 0;
         invertedImage = null;
@@ -271,6 +317,7 @@ public final class Cell extends CellAttributes {
             && !isProtect()
             && !isRGB()
             && !isImage()
+            && (width == Width.SINGLE)
             && (ch == ' ')
         ) {
             return true;
@@ -327,7 +374,7 @@ public final class Cell extends CellAttributes {
         }
 
         // Normal case: character and attributes must match.
-        if (ch == that.ch) {
+        if ((ch == that.ch) && (width == that.width)) {
             return super.equals(rhs);
         }
         return false;
@@ -345,6 +392,7 @@ public final class Cell extends CellAttributes {
         int hash = A;
         hash = (B * hash) + super.hashCode();
         hash = (B * hash) + (int)ch;
+        hash = (B * hash) + width.hashCode();
         if (image != null) {
             /*
             hash = (B * hash) + image.hashCode();
@@ -371,11 +419,13 @@ public final class Cell extends CellAttributes {
         this.image = null;
         this.imageHashCode = 0;
         this.backgroundHashCode = 0;
+        this.width = Width.SINGLE;
         super.setTo(thatAttr);
 
         if (rhs instanceof Cell) {
             Cell that = (Cell) rhs;
             this.ch = that.ch;
+            this.width = that.width;
             this.image = that.image;
             this.invertedImage = that.invertedImage;
             this.background = that.background;
index a98756ed22950a5f3eb359f36f24438448078d0e..d71fd31bf9be15548a68b18886e28a82b07790ad 100644 (file)
@@ -404,4 +404,72 @@ public class StringUtils {
         return result.toString();
     }
 
+    /**
+     * Determine display width of a Unicode code point.
+     *
+     * @param ch the code point, can be char
+     * @return the number of text cell columns required to display this code
+     * point, one of 0, 1, or 2
+     */
+    public static int width(final int ch) {
+        /*
+         * This routine is a modified version of mk_wcwidth() available
+         * at: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+         *
+         * The combining characters list has been omitted from this
+         * implementation.  Hopefully no users will be impacted.
+         */
+
+        // 8-bit control characters: width 0
+        if (ch == 0) {
+            return 0;
+        }
+        if ((ch < 32) || ((ch >= 0x7f) && (ch < 0xa0))) {
+            return 0;
+        }
+
+        // All others: either 1 or 2
+        if ((ch >= 0x1100)
+            && ((ch <= 0x115f)
+                // Hangul Jamo init. consonants
+                || (ch == 0x2329)
+                || (ch == 0x232a)
+                // CJK ... Yi
+                || ((ch >= 0x2e80) && (ch <= 0xa4cf) && (ch != 0x303f))
+                // Hangul Syllables
+                || ((ch >= 0xac00) && (ch <= 0xd7a3))
+                // CJK Compatibility Ideographs
+                || ((ch >= 0xf900) && (ch <= 0xfaff))
+                // Vertical forms
+                || ((ch >= 0xfe10) && (ch <= 0xfe19))
+                // CJK Compatibility Forms
+                || ((ch >= 0xfe30) && (ch <= 0xfe6f))
+                // Fullwidth Forms
+                || ((ch >= 0xff00) && (ch <= 0xff60))
+                || ((ch >= 0xffe0) && (ch <= 0xffe6))
+                || ((ch >= 0x20000) && (ch <= 0x2fffd))
+                || ((ch >= 0x30000) && (ch <= 0x3fffd))
+                // TODO: emoji / twemoji
+            )
+        ) {
+            return 2;
+        }
+        return 1;
+    }
+
+    /**
+     * Determine display width of a string.  This ASSUMES that no characters
+     * are combining.  Hopefully no users will be impacted.
+     *
+     * @param str the string
+     * @return the number of text cell columns required to display this string
+     */
+    public static int width(final String str) {
+        int n = 0;
+        for (int i = 0; i < str.length(); i++) {
+            n += width(str.charAt(i));
+        }
+        return n;
+    }
+
 }
index c94d7d2b8524f60290496f53e4b97cbe0bdec883..f00164ba869b5f32d1a282889693828428c15464 100644 (file)
@@ -47,10 +47,12 @@ import java.util.HashMap;
 import java.util.List;
 
 import jexer.TKeypress;
-import jexer.event.TMouseEvent;
+import jexer.backend.GlyphMaker;
 import jexer.bits.Color;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TMouseEvent;
 import jexer.io.ReadTimeoutException;
 import jexer.io.TimeoutInputStream;
 import static jexer.TKeypress.*;
@@ -477,6 +479,17 @@ public class ECMA48 implements Runnable {
      */
     private int textHeight = 20;
 
+    /**
+     * The last used height of a character cell in pixels, only used for
+     * full-width chars.
+     */
+    private int lastTextHeight = -1;
+
+    /**
+     * The glyph drawer for full-width chars.
+     */
+    GlyphMaker glyphMaker = null;
+
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
      * encapsulates those specific flags/modes.
@@ -1377,6 +1390,35 @@ public class ECMA48 implements Runnable {
     private void printCharacter(final char ch) {
         int rightMargin = this.rightMargin;
 
+        if (StringUtils.width(ch) == 2) {
+            // This is a full-width character.  Save two spaces, and then
+            // draw the character as two image halves.
+            int x0 = currentState.cursorX;
+            int y0 = currentState.cursorY;
+            printCharacter(' ');
+            printCharacter(' ');
+            if ((currentState.cursorX == x0 + 2)
+                && (currentState.cursorY == y0)
+            ) {
+                // We can draw both halves of the character.
+                drawHalves(x0, y0, x0 + 1, y0, ch);
+            } else if ((currentState.cursorX == x0 + 1)
+                && (currentState.cursorY == y0)
+            ) {
+                // VT100 line wrap behavior: we should be at the right
+                // margin.  We can draw both halves of the character.
+                drawHalves(x0, y0, x0 + 1, y0, ch);
+            } else {
+                // The character splits across the line.  Draw the entire
+                // character on the new line, giving one more space for it.
+                x0 = currentState.cursorX - 1;
+                y0 = currentState.cursorY;
+                printCharacter(' ');
+                drawHalves(x0, y0, x0 + 1, y0, ch);
+            }
+            return;
+        }
+
         // Check if we have double-width, and if so chop at 40/66 instead of
         // 80/132
         if (display.get(currentState.cursorY).isDoubleWidth()) {
@@ -1427,19 +1469,22 @@ public class ECMA48 implements Runnable {
         CellAttributes newCellAttributes = (CellAttributes) newCell;
         newCellAttributes.setTo(currentState.attr);
         DisplayLine line = display.get(currentState.cursorY);
-        // Insert mode special case
-        if (insertMode == true) {
-            line.insert(currentState.cursorX, newCell);
-        } else {
-            // Replace an existing character
-            line.replace(currentState.cursorX, newCell);
-        }
 
-        // Increment horizontal
-        if (wrapLineFlag == false) {
-            currentState.cursorX++;
-            if (currentState.cursorX > rightMargin) {
-                currentState.cursorX--;
+        if (StringUtils.width(ch) == 1) {
+            // Insert mode special case
+            if (insertMode == true) {
+                line.insert(currentState.cursorX, newCell);
+            } else {
+                // Replace an existing character
+                line.replace(currentState.cursorX, newCell);
+            }
+
+            // Increment horizontal
+            if (wrapLineFlag == false) {
+                currentState.cursorX++;
+                if (currentState.cursorX > rightMargin) {
+                    currentState.cursorX--;
+                }
             }
         }
     }
@@ -6806,4 +6851,45 @@ public class ECMA48 implements Runnable {
 
     }
 
+    /**
+     * Draw the left and right cells of a two-cell-wide (full-width) glyph.
+     *
+     * @param leftX the x position to draw the left half to
+     * @param leftY the y position to draw the left half to
+     * @param rightX the x position to draw the right half to
+     * @param rightY the y position to draw the right half to
+     * @param ch the character to draw
+     */
+    private void drawHalves(final int leftX, final int leftY,
+        final int rightX, final int rightY, final char ch) {
+
+        // System.err.println("drawHalves(): " + Integer.toHexString(ch));
+
+        if (lastTextHeight != textHeight) {
+            glyphMaker = GlyphMaker.getInstance(textHeight);
+            lastTextHeight = textHeight;
+        }
+
+        Cell cell = new Cell(ch);
+        cell.setAttr(currentState.attr);
+        BufferedImage image = glyphMaker.getImage(cell, textWidth * 2,
+            textHeight);
+        BufferedImage leftImage = image.getSubimage(0, 0, textWidth,
+            textHeight);
+        BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth,
+            textHeight);
+
+        Cell left = new Cell();
+        left.setTo(cell);
+        left.setImage(leftImage);
+        left.setWidth(Cell.Width.LEFT);
+        display.get(leftY).replace(leftX, left);
+
+        Cell right = new Cell();
+        right.setTo(cell);
+        right.setImage(rightImage);
+        right.setWidth(Cell.Width.RIGHT);
+        display.get(rightY).replace(rightX, right);
+    }
+
 }