diff --git a/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java b/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java
index a919fcbbd889b..3779915d270f7 100644
--- a/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java
+++ b/src/java.desktop/macosx/classes/sun/font/CCharToGlyphMapper.java
@@ -47,12 +47,12 @@ public int getNumGlyphs() {
     }
 
     public boolean canDisplay(char ch) {
-        int glyph = charToGlyph(ch);
+        int glyph = charToGlyph(ch, false);
         return glyph != missingGlyph;
     }
 
     public boolean canDisplay(int cp) {
-        int glyph = charToGlyph(cp);
+        int glyph = charToGlyph(cp, false);
         return glyph != missingGlyph;
     }
 
@@ -89,17 +89,17 @@ public synchronized boolean charsToGlyphsNS(int count,
     }
 
     public synchronized int charToGlyph(char unicode) {
-        int glyph = cache.get(unicode);
+        return charToGlyph(unicode, false);
+    }
+
+    private int charToGlyph(char unicode, boolean raw) {
+        int glyph = cache.get(unicode, raw);
         if (glyph != 0) return glyph;
 
-        if (FontUtilities.isDefaultIgnorable(unicode)) {
-            glyph = INVISIBLE_GLYPH_ID;
-        } else {
-            final char[] unicodeArray = new char[] { unicode };
-            final int[] glyphArray = new int[1];
-            nativeCharsToGlyphs(fFont.getNativeFontPtr(), 1, unicodeArray, glyphArray);
-            glyph = glyphArray[0];
-        }
+        final char[] unicodeArray = new char[] { unicode };
+        final int[] glyphArray = new int[1];
+        nativeCharsToGlyphs(fFont.getNativeFontPtr(), 1, unicodeArray, glyphArray);
+        glyph = glyphArray[0];
 
         cache.put(unicode, glyph);
 
@@ -107,26 +107,34 @@ public synchronized int charToGlyph(char unicode) {
     }
 
     public synchronized int charToGlyph(int unicode) {
+        return charToGlyph(unicode, false);
+    }
+
+    public synchronized int charToGlyphRaw(int unicode) {
+        return charToGlyph(unicode, true);
+    }
+
+    private int charToGlyph(int unicode, boolean raw) {
         if (unicode >= 0x10000) {
             int[] glyphs = new int[2];
             char[] surrogates = new char[2];
             int base = unicode - 0x10000;
             surrogates[0] = (char)((base >>> 10) + HI_SURROGATE_START);
             surrogates[1] = (char)((base % 0x400) + LO_SURROGATE_START);
-            charsToGlyphs(2, surrogates, glyphs);
+            cache.get(2, surrogates, glyphs, raw);
             return glyphs[0];
          } else {
-             return charToGlyph((char)unicode);
+             return charToGlyph((char) unicode, raw);
          }
     }
 
     public synchronized void charsToGlyphs(int count, char[] unicodes, int[] glyphs) {
-        cache.get(count, unicodes, glyphs);
+        cache.get(count, unicodes, glyphs, false);
     }
 
     public synchronized void charsToGlyphs(int count, int[] unicodes, int[] glyphs) {
         for (int i = 0; i < count; i++) {
-            glyphs[i] = charToGlyph(unicodes[i]);
+            glyphs[i] = charToGlyph(unicodes[i], false);
         }
     }
 
@@ -153,7 +161,11 @@ private class Cache {
             firstLayerCache[1] = 1;
         }
 
-        public synchronized int get(final int index) {
+        public synchronized int get(final int index, final boolean raw) {
+            if (!raw && FontUtilities.isDefaultIgnorable(index)) {
+                return INVISIBLE_GLYPH_ID;
+            }
+
             if (index < FIRST_LAYER_SIZE) {
                 // catch common glyphcodes
                 return firstLayerCache[index];
@@ -224,7 +236,7 @@ public void put(final int index, final int value) {
             }
         }
 
-        public synchronized void get(int count, char[] indices, int[] values)
+        public synchronized void get(int count, char[] indices, int[] values, boolean raw)
         {
             // "missed" is the count of 'char' that are not mapped.
             // Surrogates count for 2.
@@ -246,16 +258,13 @@ public synchronized void get(int count, char[] indices, int[] values)
                     }
                 }
 
-                final int value = get(code);
+                final int value = get(code, raw);
                 if (value != 0 && value != -1) {
                     values[i] = value;
                     if (code >= 0x10000) {
                         values[i+1] = INVISIBLE_GLYPH_ID;
                         i++;
                     }
-                } else if (FontUtilities.isDefaultIgnorable(code)) {
-                    values[i] = INVISIBLE_GLYPH_ID;
-                    put(code, INVISIBLE_GLYPH_ID);
                 } else {
                     values[i] = 0;
                     put(code, -1);
diff --git a/src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java b/src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java
index bac385d81db23..3b50b0e887600 100644
--- a/src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java
+++ b/src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2006, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -86,6 +86,13 @@ public int charToVariationGlyph(int unicode, int variationSelector) {
         return charToGlyph(unicode);
     }
 
+    public int charToVariationGlyphRaw(int unicode, int variationSelector) {
+        // Override this if variation selector is supported.
+        return charToGlyphRaw(unicode);
+    }
+
+    public abstract int charToGlyphRaw(int unicode);
+
     public abstract int getNumGlyphs();
 
     public abstract void charsToGlyphs(int count,
diff --git a/src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java b/src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java
index cd53d96d0f4e0..54627997a2ec4 100644
--- a/src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java
+++ b/src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -51,7 +51,6 @@ public class CompositeGlyphMapper extends CharToGlyphMapper {
     public static final int BLOCKSZ = 256;
     public static final int MAXUNICODE = NBLOCKS*BLOCKSZ;
 
-
     CompositeFont font;
     CharToGlyphMapper[] slotMappers;
     int[][] glyphMaps;
@@ -96,7 +95,7 @@ private int getCachedGlyphCode(int unicode) {
 
     private void setCachedGlyphCode(int unicode, int glyphCode) {
         if (unicode >= MAXUNICODE) {
-            return;     // don't cache surrogates
+            return; // don't cache surrogates
         }
         int index0 = unicode >> 8;
         if (glyphMaps[index0] == null) {
@@ -117,12 +116,18 @@ private CharToGlyphMapper getSlotMapper(int slot) {
         return mapper;
     }
 
-    private int convertToGlyph(int unicode) {
-
+    private int getGlyph(int unicode, boolean raw) {
+        if (!raw && FontUtilities.isDefaultIgnorable(unicode)) {
+            return INVISIBLE_GLYPH_ID;
+        }
+        int glyphCode = getCachedGlyphCode(unicode);
+        if (glyphCode != UNINITIALIZED_GLYPH) {
+            return glyphCode;
+        }
         for (int slot = 0; slot < font.numSlots; slot++) {
             if (!hasExcludes || !font.isExcludedChar(slot, unicode)) {
                 CharToGlyphMapper mapper = getSlotMapper(slot);
-                int glyphCode = mapper.charToGlyph(unicode);
+                glyphCode = mapper.charToGlyphRaw(unicode);
                 if (glyphCode != mapper.getMissingGlyphCode()) {
                     glyphCode = compositeGlyphCode(slot, glyphCode);
                     setCachedGlyphCode(unicode, glyphCode);
@@ -155,12 +160,13 @@ public int getNumGlyphs() {
         return numGlyphs;
     }
 
-    public int charToGlyph(int unicode) {
+    public int charToGlyphRaw(int unicode) {
+        int glyphCode = getGlyph(unicode, true);
+        return glyphCode;
+    }
 
-        int glyphCode = getCachedGlyphCode(unicode);
-        if (glyphCode == UNINITIALIZED_GLYPH) {
-            glyphCode = convertToGlyph(unicode);
-        }
+    public int charToGlyph(int unicode) {
+        int glyphCode = getGlyph(unicode, false);
         return glyphCode;
     }
 
@@ -176,11 +182,7 @@ public int charToGlyph(int unicode, int prefSlot) {
     }
 
     public int charToGlyph(char unicode) {
-
-        int glyphCode  = getCachedGlyphCode(unicode);
-        if (glyphCode == UNINITIALIZED_GLYPH) {
-            glyphCode = convertToGlyph(unicode);
-        }
+        int glyphCode = getGlyph(unicode, false);
         return glyphCode;
     }
 
@@ -206,10 +208,7 @@ public boolean charsToGlyphsNS(int count, char[] unicodes, int[] glyphs) {
                 }
             }
 
-            int gc = glyphs[i] = getCachedGlyphCode(code);
-            if (gc == UNINITIALIZED_GLYPH) {
-                glyphs[i] = convertToGlyph(code);
-            }
+            glyphs[i] = getGlyph(code, false);
 
             if (code < FontUtilities.MIN_LAYOUT_CHARCODE) {
                 continue;
@@ -243,31 +242,21 @@ public void charsToGlyphs(int count, char[] unicodes, int[] glyphs) {
                     code = (code - HI_SURROGATE_START) *
                         0x400 + low - LO_SURROGATE_START + 0x10000;
 
-                    int gc = glyphs[i] = getCachedGlyphCode(code);
-                    if (gc == UNINITIALIZED_GLYPH) {
-                        glyphs[i] = convertToGlyph(code);
-                    }
+                    glyphs[i] = getGlyph(code, false);
                     i += 1; // Empty glyph slot after surrogate
                     glyphs[i] = INVISIBLE_GLYPH_ID;
                     continue;
                 }
             }
 
-            int gc = glyphs[i] = getCachedGlyphCode(code);
-            if (gc == UNINITIALIZED_GLYPH) {
-                glyphs[i] = convertToGlyph(code);
-            }
+            glyphs[i] = getGlyph(code, false);
         }
     }
 
     public void charsToGlyphs(int count, int[] unicodes, int[] glyphs) {
         for (int i=0; i<count; i++) {
             int code = unicodes[i];
-
-            glyphs[i] = getCachedGlyphCode(code);
-            if (glyphs[i] == UNINITIALIZED_GLYPH) {
-                glyphs[i] = convertToGlyph(code);
-            }
+            glyphs[i] = getGlyph(code, false);
         }
     }
 
diff --git a/src/java.desktop/share/classes/sun/font/Font2D.java b/src/java.desktop/share/classes/sun/font/Font2D.java
index 64968086a3b46..dd0f92ded2f89 100644
--- a/src/java.desktop/share/classes/sun/font/Font2D.java
+++ b/src/java.desktop/share/classes/sun/font/Font2D.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -535,6 +535,14 @@ public int charToVariationGlyph(int wchar, int variationSelector) {
         return getMapper().charToVariationGlyph(wchar, variationSelector);
     }
 
+    public int charToGlyphRaw(int wchar) {
+        return getMapper().charToGlyphRaw(wchar);
+    }
+
+    public int charToVariationGlyphRaw(int wchar, int variationSelector) {
+        return getMapper().charToVariationGlyphRaw(wchar, variationSelector);
+    }
+
     public int getMissingGlyphCode() {
         return getMapper().getMissingGlyphCode();
     }
diff --git a/src/java.desktop/share/classes/sun/font/HBShaper.java b/src/java.desktop/share/classes/sun/font/HBShaper.java
index 16bfd1ba971d2..7d3f58fb88f4d 100644
--- a/src/java.desktop/share/classes/sun/font/HBShaper.java
+++ b/src/java.desktop/share/classes/sun/font/HBShaper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -338,7 +338,7 @@ private static int get_nominal_glyph(
     ) {
 
         Font2D font2D = scopedVars.get().font();
-        int glyphID = font2D.charToGlyph(unicode);
+        int glyphID = font2D.charToGlyphRaw(unicode);
         @SuppressWarnings("restricted")
         MemorySegment glyphIDPtr = glyph.reinterpret(4);
         glyphIDPtr.setAtIndex(JAVA_INT, 0, glyphID);
@@ -354,7 +354,7 @@ private static int get_variation_glyph(
         MemorySegment user_data   /* Not used */
     ) {
         Font2D font2D = scopedVars.get().font();
-        int glyphID = font2D.charToVariationGlyph(unicode, variation_selector);
+        int glyphID = font2D.charToVariationGlyphRaw(unicode, variation_selector);
         @SuppressWarnings("restricted")
         MemorySegment glyphIDPtr = glyph.reinterpret(4);
         glyphIDPtr.setAtIndex(JAVA_INT, 0, glyphID);
diff --git a/src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java b/src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java
index fe784ac4b971c..cdada7b341d9d 100644
--- a/src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java
+++ b/src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java
@@ -57,8 +57,8 @@ public int getNumGlyphs() {
         return numGlyphs;
     }
 
-    private char getGlyphFromCMAP(int charCode) {
-        if (FontUtilities.isDefaultIgnorable(charCode)) {
+    private char getGlyphFromCMAP(int charCode, boolean raw) {
+        if (!raw && FontUtilities.isDefaultIgnorable(charCode)) {
             return INVISIBLE_GLYPH_ID;
         }
         try {
@@ -80,11 +80,11 @@ private char getGlyphFromCMAP(int charCode) {
         }
     }
 
-    private char getGlyphFromCMAP(int charCode, int variationSelector) {
+    private char getGlyphFromCMAP(int charCode, int variationSelector, boolean raw) {
         if (variationSelector == 0) {
-            return getGlyphFromCMAP(charCode);
+            return getGlyphFromCMAP(charCode, raw);
         }
-        if (FontUtilities.isDefaultIgnorable(charCode)) {
+        if (!raw && FontUtilities.isDefaultIgnorable(charCode)) {
             return INVISIBLE_GLYPH_ID;
         }
         try {
@@ -122,25 +122,36 @@ private void handleBadCMAP() {
         cmap = CMap.theNullCmap;
     }
 
+    public int charToGlyphRaw(int unicode) {
+        int glyph = getGlyphFromCMAP(unicode, true);
+        return glyph;
+    }
+
+    @Override
+    public int charToVariationGlyphRaw(int unicode, int variationSelector) {
+        int glyph = getGlyphFromCMAP(unicode, variationSelector, true);
+        return glyph;
+    }
+
     public int charToGlyph(char unicode) {
-        int glyph = getGlyphFromCMAP(unicode);
+        int glyph = getGlyphFromCMAP(unicode, false);
         return glyph;
     }
 
     public int charToGlyph(int unicode) {
-        int glyph = getGlyphFromCMAP(unicode);
+        int glyph = getGlyphFromCMAP(unicode, false);
         return glyph;
     }
 
     @Override
     public int charToVariationGlyph(int unicode, int variationSelector) {
-        int glyph = getGlyphFromCMAP(unicode, variationSelector);
+        int glyph = getGlyphFromCMAP(unicode, variationSelector, false);
         return glyph;
     }
 
     public void charsToGlyphs(int count, int[] unicodes, int[] glyphs) {
         for (int i=0;i<count;i++) {
-            glyphs[i] = getGlyphFromCMAP(unicodes[i]);
+            glyphs[i] = getGlyphFromCMAP(unicodes[i], false);
         }
     }
 
@@ -158,13 +169,13 @@ public void charsToGlyphs(int count, char[] unicodes, int[] glyphs) {
                     code = (code - HI_SURROGATE_START) *
                         0x400 + low - LO_SURROGATE_START + 0x10000;
 
-                    glyphs[i] = getGlyphFromCMAP(code);
+                    glyphs[i] = getGlyphFromCMAP(code, false);
                     i += 1; // Empty glyph slot after surrogate
                     glyphs[i] = INVISIBLE_GLYPH_ID;
                     continue;
                 }
             }
-            glyphs[i] = getGlyphFromCMAP(code);
+            glyphs[i] = getGlyphFromCMAP(code, false);
 
         }
     }
@@ -191,7 +202,7 @@ public boolean charsToGlyphsNS(int count, char[] unicodes, int[] glyphs) {
                 }
             }
 
-            glyphs[i] = getGlyphFromCMAP(code);
+            glyphs[i] = getGlyphFromCMAP(code, false);
 
             if (code < FontUtilities.MIN_LAYOUT_CHARCODE) {
                 continue;
diff --git a/src/java.desktop/share/classes/sun/font/Type1GlyphMapper.java b/src/java.desktop/share/classes/sun/font/Type1GlyphMapper.java
index 8715e300c7d93..6c00fb2c24122 100644
--- a/src/java.desktop/share/classes/sun/font/Type1GlyphMapper.java
+++ b/src/java.desktop/share/classes/sun/font/Type1GlyphMapper.java
@@ -90,10 +90,20 @@ public int charToGlyph(char ch) {
     }
 
     public int charToGlyph(int ch) {
+        int glyph = charToGlyph(ch, false);
+        return glyph;
+    }
+
+    public int charToGlyphRaw(int ch) {
+        int glyph = charToGlyph(ch, true);
+        return glyph;
+    }
+
+    private int charToGlyph(int ch, boolean raw) {
         if (ch < 0 || ch > 0xffff) {
             return missingGlyph;
         } else {
-            if (FontUtilities.isDefaultIgnorable(ch)) {
+            if (!raw && FontUtilities.isDefaultIgnorable(ch)) {
                 return INVISIBLE_GLYPH_ID;
             }
             try {
diff --git a/src/java.desktop/share/native/libfontmanager/sunFont.c b/src/java.desktop/share/native/libfontmanager/sunFont.c
index e6082b69416b6..1fe4cc7dfd752 100644
--- a/src/java.desktop/share/native/libfontmanager/sunFont.c
+++ b/src/java.desktop/share/native/libfontmanager/sunFont.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2007, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -143,9 +143,9 @@ static void initFontIDs(JNIEnv *env) {
 
      CHECK_NULL(tmpClass = (*env)->FindClass(env, "sun/font/Font2D"));
      CHECK_NULL(sunFontIDs.f2dCharToGlyphMID =
-         (*env)->GetMethodID(env, tmpClass, "charToGlyph", "(I)I"));
+         (*env)->GetMethodID(env, tmpClass, "charToGlyphRaw", "(I)I"));
      CHECK_NULL(sunFontIDs.f2dCharToVariationGlyphMID =
-         (*env)->GetMethodID(env, tmpClass, "charToVariationGlyph", "(II)I"));
+         (*env)->GetMethodID(env, tmpClass, "charToVariationGlyphRaw", "(II)I"));
      CHECK_NULL(sunFontIDs.getMapperMID =
          (*env)->GetMethodID(env, tmpClass, "getMapper",
                              "()Lsun/font/CharToGlyphMapper;"));
diff --git a/src/java.desktop/unix/classes/sun/font/NativeGlyphMapper.java b/src/java.desktop/unix/classes/sun/font/NativeGlyphMapper.java
index aecb2494f2493..cec455e2e8313 100644
--- a/src/java.desktop/unix/classes/sun/font/NativeGlyphMapper.java
+++ b/src/java.desktop/unix/classes/sun/font/NativeGlyphMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2003, 2005, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -75,6 +75,10 @@ public int charToGlyph(int unicode) {
         }
     }
 
+    public int charToGlyphRaw(int unicode) {
+        return charToGlyph(unicode);
+    }
+
     public void charsToGlyphs(int count, char[] unicodes, int[] glyphs) {
         for (int i=0; i<count; i++) {
             char code = unicodes[i];
diff --git a/test/jdk/java/awt/font/GlyphVector/GlyphVectorGsubTest.java b/test/jdk/java/awt/font/GlyphVector/GlyphVectorGsubTest.java
new file mode 100644
index 0000000000000..1211f13757a6f
--- /dev/null
+++ b/test/jdk/java/awt/font/GlyphVector/GlyphVectorGsubTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * @test
+ * @bug 8353230
+ * @summary Regression test for TrueType font GSUB substitutions.
+ */
+
+import java.awt.Font;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.awt.font.TextAttribute;
+import java.io.ByteArrayInputStream;
+import java.util.Base64;
+import java.util.Map;
+
+public class GlyphVectorGsubTest {
+
+    /**
+     * <p>Font created for this test which contains two GSUB substitutions: a
+     * "liga" ligature for "a" + "b" which requires that the ligature support
+     * be enabled, and a "ccmp" ligature for an emoji sequence which does not
+     * require that ligatures be explicitly enabled.
+     *
+     * <p>The following FontForge Python script was used to generate this font:
+     *
+     * <pre>
+     * import fontforge
+     * import base64
+     *
+     * def draw(glyph, width, height):
+     *   pen = glyph.glyphPen()
+     *   pen.moveTo((100, 100))
+     *   pen.lineTo((100, 100 + height))
+     *   pen.lineTo((100 + width, 100 + height))
+     *   pen.lineTo((100 + width, 100))
+     *   pen.closePath()
+     *   glyph.draw(pen)
+     *   pen = None
+     *
+     * font = fontforge.font()
+     * font.encoding = 'UnicodeFull'
+     * font.design_size = 16
+     * font.em = 2048
+     * font.ascent = 1638
+     * font.descent = 410
+     * font.familyname = 'Test'
+     * font.fontname = 'Test'
+     * font.fullname = 'Test'
+     * font.copyright = ''
+     * font.autoWidth(0, 0, 2048)
+     *
+     * font.addLookup('ligatures', 'gsub_ligature', (), (('liga',(('latn',('dflt')),)),))
+     * font.addLookupSubtable('ligatures', 'sub1')
+     *
+     * font.addLookup('sequences', 'gsub_ligature', (), (('ccmp',(('latn',('dflt')),)),))
+     * font.addLookupSubtable('sequences', 'sub2')
+     *
+     * space = font.createChar(0x20)
+     * space.width = 600
+     *
+     * # create glyphs: a, b, ab
+     *
+     * for char in list('ab'):
+     *   glyph = font.createChar(ord(char))
+     *   draw(glyph, 400, 100)
+     *   glyph.width = 600
+     *
+     * ab = font.createChar(-1, 'ab')
+     * ab.addPosSub('sub1', ('a', 'b'))
+     * draw(ab, 400, 400)
+     * ab.width = 600
+     *
+     * # create glyphs for "woman" emoji sequence
+     *
+     * components = []
+     * woman = '\U0001F471\U0001F3FD\u200D\u2640\uFE0F'
+     * for char in list(woman):
+     *   glyph = font.createChar(ord(char))
+     *   draw(glyph, 400, 800)
+     *   glyph.width = 600
+     *   components.append(glyph.glyphname)
+     *
+     * del components[-1] # remove last
+     * seq = font.createChar(-1, 'seq')
+     * seq.addPosSub('sub2', components)
+     * draw(seq, 400, 1200)
+     * seq.width = 600
+     *
+     * # save font to file
+     *
+     * ttf = 'test.ttf'     # TrueType
+     * t64 = 'test.ttf.txt' # TrueType Base64
+     *
+     * font.generate(ttf)
+     *
+     * with open(ttf, 'rb') as f1:
+     *   encoded = base64.b64encode(f1.read())
+     *   with open(t64, 'wb') as f2:
+     *     f2.write(encoded)
+     * </pre>
+     */
+    private static final String TTF_BYTES = "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";
+
+    public static void main(String[] args) throws Exception {
+
+        byte[] ttfBytes = Base64.getDecoder().decode(TTF_BYTES);
+        ByteArrayInputStream ttfStream = new ByteArrayInputStream(ttfBytes);
+        Font f1 = Font.createFont(Font.TRUETYPE_FONT, ttfStream).deriveFont(80f);
+
+        // Test emoji sequence, using "ccmp" feature and ZWJ (zero-width joiner):
+        // - person with blonde hair
+        // - emoji modifier fitzpatrick type 4
+        // - zero-width joiner
+        // - female sign
+        // - variation selector 16
+        // Does not require the use of the TextAttribute.LIGATURES_ON attribute.
+        char[] text1 = "\ud83d\udc71\ud83c\udffd\u200d\u2640\ufe0f".toCharArray();
+        FontRenderContext frc = new FontRenderContext(null, true, true);
+        GlyphVector gv1 = f1.layoutGlyphVector(frc, text1, 0, text1.length, 0);
+        checkOneGlyph(gv1, text1, 12);
+
+        // Test regular ligature, using "liga" feature: "ab" -> replacement
+        // Requires the use of the TextAttribute.LIGATURES_ON attribute.
+        char[] text2 = "ab".toCharArray();
+        Font f2 = f1.deriveFont(Map.of(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON));
+        GlyphVector gv2 = f2.layoutGlyphVector(frc, text2, 0, text2.length, 0);
+        checkOneGlyph(gv2, text2, 11);
+    }
+
+    private static void checkOneGlyph(GlyphVector gv, char[] text, int expectedCode) {
+        int glyphs = gv.getNumGlyphs();
+        if (glyphs != 1) {
+            throw new RuntimeException("Unexpected number of glyphs for text " +
+                new String(text) + ": " + glyphs);
+        }
+        int code = gv.getGlyphCode(0);
+        if (code != expectedCode) {
+            throw new RuntimeException("Unexpected glyph code for text " +
+                new String(text) + ": " + expectedCode + " != " + code);
+        }
+    }
+}