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); + } + } +}