8187100: Support Unicode Variation Selectors
Summary: Support Unicode Variation Selectors
Reviewed-by: prr, srl
Contributed-by: Toshio Nakamura <toshiona@jp.ibm.com>
--- a/src/java.desktop/share/classes/sun/font/CMap.java Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/classes/sun/font/CMap.java Mon Jun 25 11:40:46 2018 -0700
@@ -140,6 +140,7 @@
* Using this saves running character coverters repeatedly.
*/
char[] xlat;
+ UVS uvs = null;
static CMap initialize(TrueTypeFont font) {
@@ -149,6 +150,7 @@
int three0=0, three1=0, three2=0, three3=0, three4=0, three5=0,
three6=0, three10=0;
+ int zero5 = 0; // for Unicode Variation Sequences
boolean threeStar = false;
ByteBuffer cmapBuffer = font.getTableBuffer(TrueTypeFont.cmapTag);
@@ -173,6 +175,12 @@
case 6: three6 = offset; break; // Johab
case 10: three10 = offset; break; // MS Unicode surrogates
}
+ } else if (platformID == 0) {
+ encodingID = cmapBuffer.getShort();
+ offset = cmapBuffer.getInt();
+ if (encodingID == 5) {
+ zero5 = offset;
+ }
}
}
@@ -262,6 +270,10 @@
*/
cmap = createCMap(cmapBuffer, cmapBuffer.getInt(8), null);
}
+ // For Unicode Variation Sequences
+ if (cmap != null && zero5 != 0) {
+ cmap.createUVS(cmapBuffer, zero5);
+ }
return cmap;
}
@@ -424,6 +436,25 @@
}
}
+ private void createUVS(ByteBuffer buffer, int offset) {
+ int subtableFormat = buffer.getChar(offset);
+ if (subtableFormat == 14) {
+ long subtableLength = buffer.getInt(offset + 2) & INTMASK;
+ if (offset + subtableLength > buffer.capacity()) {
+ if (FontUtilities.isLogging()) {
+ FontUtilities.getLogger()
+ .warning("Cmap UVS subtable overflows buffer.");
+ }
+ }
+ try {
+ this.uvs = new UVS(buffer, offset);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ return;
+ }
+
/*
final char charVal(byte[] cmap, int index) {
return (char)(((0xff & cmap[index]) << 8)+(0xff & cmap[index+1]));
@@ -1059,4 +1090,87 @@
}
return -1;
}
+
+ static class UVS {
+ int numSelectors;
+ int[] selector;
+
+ //for Non-Default UVS Table
+ int[] numUVSMapping;
+ int[][] unicodeValue;
+ char[][] glyphID;
+
+ UVS(ByteBuffer buffer, int offset) {
+ numSelectors = buffer.getInt(offset+6);
+ selector = new int[numSelectors];
+ numUVSMapping = new int[numSelectors];
+ unicodeValue = new int[numSelectors][];
+ glyphID = new char[numSelectors][];
+
+ for (int i = 0; i < numSelectors; i++) {
+ buffer.position(offset + 10 + i * 11);
+ selector[i] = (buffer.get() & 0xff) << 16; //UINT24
+ selector[i] += (buffer.get() & 0xff) << 8;
+ selector[i] += buffer.get() & 0xff;
+
+ //skip Default UVS Table
+
+ //for Non-Default UVS Table
+ int tableOffset = buffer.getInt(offset + 10 + i * 11 + 7);
+ if (tableOffset == 0) {
+ numUVSMapping[i] = 0;
+ } else if (tableOffset > 0) {
+ buffer.position(offset+tableOffset);
+ numUVSMapping[i] = buffer.getInt() & INTMASK;
+ unicodeValue[i] = new int[numUVSMapping[i]];
+ glyphID[i] = new char[numUVSMapping[i]];
+
+ for (int j = 0; j < numUVSMapping[i]; j++) {
+ int temp = (buffer.get() & 0xff) << 16; //UINT24
+ temp += (buffer.get() & 0xff) << 8;
+ temp += buffer.get() & 0xff;
+ unicodeValue[i][j] = temp;
+ glyphID[i][j] = buffer.getChar();
+ }
+ }
+ }
+ }
+
+ static final int VS_NOGLYPH = 0;
+ private int getGlyph(int charCode, int variationSelector) {
+ int targetSelector = -1;
+ for (int i = 0; i < numSelectors; i++) {
+ if (selector[i] == variationSelector) {
+ targetSelector = i;
+ break;
+ }
+ }
+ if (targetSelector == -1) {
+ return VS_NOGLYPH;
+ }
+ if (numUVSMapping[targetSelector] > 0) {
+ int index = java.util.Arrays.binarySearch(
+ unicodeValue[targetSelector], charCode);
+ if (index >= 0) {
+ return glyphID[targetSelector][index];
+ }
+ }
+ return VS_NOGLYPH;
+ }
+ }
+
+ char getVariationGlyph(int charCode, int variationSelector) {
+ char glyph = 0;
+ if (uvs == null) {
+ glyph = getGlyph(charCode);
+ } else {
+ int result = uvs.getGlyph(charCode, variationSelector);
+ if (result > 0) {
+ glyph = (char)(result & 0xFFFF);
+ } else {
+ glyph = getGlyph(charCode);
+ }
+ }
+ return glyph;
+ }
}
--- a/src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java Mon Jun 25 11:40:46 2018 -0700
@@ -36,6 +36,10 @@
public static final int HI_SURROGATE_END = 0xDBFF;
public static final int LO_SURROGATE_START = 0xDC00;
public static final int LO_SURROGATE_END = 0xDFFF;
+ public static final int VS_START = 0xFE00;
+ public static final int VS_END = 0xFE0F;
+ public static final int VSS_START = 0xE0100;
+ public static final int VSS_END = 0xE01FF;
public static final int UNINITIALIZED_GLYPH = -1;
public static final int INVISIBLE_GLYPH_ID = 0xffff;
@@ -77,6 +81,11 @@
return glyphs[0];
}
+ public int charToVariationGlyph(int unicode, int variationSelector) {
+ // Override this if variation selector is supported.
+ return charToGlyph(unicode);
+ }
+
public abstract int getNumGlyphs();
public abstract void charsToGlyphs(int count,
@@ -88,4 +97,9 @@
public abstract void charsToGlyphs(int count,
int[] unicodes, int[] glyphs);
+ public static boolean isVariationSelector(int charCode) {
+ return ((charCode >= VSS_START && charCode <= VSS_END) ||
+ (charCode >= VS_START && charCode <= VS_END));
+ }
+
}
--- a/src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java Mon Jun 25 11:40:46 2018 -0700
@@ -214,7 +214,8 @@
if (code < FontUtilities.MIN_LAYOUT_CHARCODE) {
continue;
}
- else if (FontUtilities.isComplexCharCode(code)) {
+ else if (FontUtilities.isComplexCharCode(code) ||
+ CharToGlyphMapper.isVariationSelector(code)) {
return true;
}
else if (code >= 0x10000) {
--- a/src/java.desktop/share/classes/sun/font/Font2D.java Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/classes/sun/font/Font2D.java Mon Jun 25 11:40:46 2018 -0700
@@ -524,6 +524,10 @@
return getMapper().charToGlyph(wchar);
}
+ public int charToVariationGlyph(int wchar, int variationSelector) {
+ return getMapper().charToVariationGlyph(wchar, variationSelector);
+ }
+
public int getMissingGlyphCode() {
return getMapper().getMissingGlyphCode();
}
--- a/src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java Mon Jun 25 11:40:46 2018 -0700
@@ -93,6 +93,32 @@
}
}
+ private char getGlyphFromCMAP(int charCode, int variationSelector) {
+ if (variationSelector == 0) {
+ return getGlyphFromCMAP(charCode);
+ }
+ try {
+ char glyphCode = cmap.getVariationGlyph(charCode,
+ variationSelector);
+ if (glyphCode < numGlyphs ||
+ glyphCode >= FileFontStrike.INVISIBLE_GLYPHS) {
+ return glyphCode;
+ } else {
+ if (FontUtilities.isLogging()) {
+ FontUtilities.getLogger().warning
+ (font + " out of range glyph id=" +
+ Integer.toHexString((int)glyphCode) +
+ " for char " + Integer.toHexString(charCode) +
+ " for vs " + Integer.toHexString(variationSelector));
+ }
+ return (char)missingGlyph;
+ }
+ } catch (Exception e) {
+ handleBadCMAP();
+ return (char) missingGlyph;
+ }
+ }
+
private void handleBadCMAP() {
if (FontUtilities.isLogging()) {
FontUtilities.getLogger().severe("Null Cmap for " + font +
@@ -136,6 +162,18 @@
return glyph;
}
+ @Override
+ public int charToVariationGlyph(int unicode, int variationSelector) {
+ if (needsJAremapping) {
+ unicode = remapJAIntChar(unicode);
+ }
+ int glyph = getGlyphFromCMAP(unicode, variationSelector);
+ if (font.checkUseNatives() && glyph < font.glyphToCharMap.length) {
+ font.glyphToCharMap[glyph] = (char)unicode;
+ }
+ return glyph;
+ }
+
public void charsToGlyphs(int count, int[] unicodes, int[] glyphs) {
for (int i=0;i<count;i++) {
if (needsJAremapping) {
@@ -221,7 +259,8 @@
if (code < FontUtilities.MIN_LAYOUT_CHARCODE) {
continue;
}
- else if (FontUtilities.isComplexCharCode(code)) {
+ else if (FontUtilities.isComplexCharCode(code) ||
+ CharToGlyphMapper.isVariationSelector(code)) {
return true;
}
else if (code >= 0x10000) {
--- a/src/java.desktop/share/native/common/font/sunfontids.h Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/native/common/font/sunfontids.h Mon Jun 25 11:40:46 2018 -0700
@@ -39,6 +39,7 @@
jmethodID getTableBytesMID;
jmethodID canDisplayMID;
jmethodID f2dCharToGlyphMID;
+ jmethodID f2dCharToVariationGlyphMID;
/* sun/font/CharToGlyphMapper methods */
jmethodID charToGlyphMID;
--- a/src/java.desktop/share/native/libfontmanager/hb-jdk-font.cc Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/native/libfontmanager/hb-jdk-font.cc Mon Jun 25 11:40:46 2018 -0700
@@ -48,10 +48,18 @@
JDKFontInfo *jdkFontInfo = (JDKFontInfo*)font_data;
JNIEnv* env = jdkFontInfo->env;
jobject font2D = jdkFontInfo->font2D;
- hb_codepoint_t u = (variation_selector==0) ? unicode : variation_selector;
-
- *glyph = (hb_codepoint_t)
- env->CallIntMethod(font2D, sunFontIDs.f2dCharToGlyphMID, u);
+ if (variation_selector == 0) {
+ *glyph = (hb_codepoint_t)env->CallIntMethod(
+ font2D, sunFontIDs.f2dCharToGlyphMID, unicode);
+ } else {
+ *glyph = (hb_codepoint_t)env->CallIntMethod(
+ font2D, sunFontIDs.f2dCharToVariationGlyphMID,
+ unicode, variation_selector);
+ }
+ if (env->ExceptionOccurred())
+ {
+ env->ExceptionClear();
+ }
if ((int)*glyph < 0) {
*glyph = 0;
}
--- a/src/java.desktop/share/native/libfontmanager/sunFont.c Mon Jun 25 14:32:46 2018 +0530
+++ b/src/java.desktop/share/native/libfontmanager/sunFont.c Mon Jun 25 11:40:46 2018 -0700
@@ -144,6 +144,8 @@
CHECK_NULL(tmpClass = (*env)->FindClass(env, "sun/font/Font2D"));
CHECK_NULL(sunFontIDs.f2dCharToGlyphMID =
(*env)->GetMethodID(env, tmpClass, "charToGlyph", "(I)I"));
+ CHECK_NULL(sunFontIDs.f2dCharToVariationGlyphMID =
+ (*env)->GetMethodID(env, tmpClass, "charToVariationGlyph", "(II)I"));
CHECK_NULL(sunFontIDs.getMapperMID =
(*env)->GetMethodID(env, tmpClass, "getMapper",
"()Lsun/font/CharToGlyphMapper;"));
Binary file test/jdk/java/awt/font/TextLayout/TestVS-expect.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/awt/font/TextLayout/TestVS.java Mon Jun 25 11:40:46 2018 -0700
@@ -0,0 +1,92 @@
+/*
+ * 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
+ * @summary Verify Variation Selector matches an expected image
+ * @bug 8187100
+ * @ignore Requires a special font installed.
+ */
+
+import javax.swing.SwingUtilities;
+import javax.swing.border.LineBorder;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JFrame;
+import javax.swing.JTextArea;
+import javax.swing.ImageIcon;
+import java.awt.Font;
+import java.awt.Color;
+
+public class TestVS {
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ new TestVS().run();
+ }
+ });
+ }
+
+ private void run() {
+ Font ourFont = null;
+ final String fontName = "ipaexm.ttf";
+ // download from https://ipafont.ipa.go.jp/node26#en
+ // and place in {user.home}/fonts/
+ try {
+ ourFont = Font.createFont(Font.TRUETYPE_FONT,
+ new java.io.File(new java.io.File(
+ System.getProperty("user.home"),
+ "fonts"), fontName));
+ ourFont = ourFont.deriveFont((float)48.0);
+ final String actualFontName = ourFont.getFontName();
+ if (!actualFontName.equals("IPAexMincho")) {
+ System.err.println("*** Warning: missing font IPAexMincho.");
+ System.err.println("*** Using font: " + actualFontName);
+ }
+ } catch(Throwable t) {
+ t.printStackTrace();
+ System.err.println("Fail: " + t);
+ return;
+ }
+ JFrame frame = new JFrame(System.getProperty("java.version"));
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ JPanel panel = new JPanel();
+ final JTextArea label = new JTextArea("empty");
+ label.setSize(400, 300);
+ label.setBorder(new LineBorder(Color.black));
+ label.setFont(ourFont);
+
+ final String str = "\u845b\udb40\udd00\u845b\udb40\udd01\n";
+
+ label.setText(str);
+
+ panel.add(label);
+ panel.add(new JLabel(ourFont.getFamily()));
+
+ // Show the expected result.
+ panel.add(new JLabel(new ImageIcon("TestVS-expect.png")));
+
+ frame.getContentPane().add(panel);
+ frame.pack();
+ frame.setVisible(true);
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/awt/font/TextLayout/VariationSelectorTest.java Mon Jun 25 11:40:46 2018 -0700
@@ -0,0 +1,65 @@
+/* @test
+ * @summary Verify two identical 'a's are rendered
+ * @bug 8187100
+ * @ignore Requires a special font installed.
+ */
+import javax.swing.JFrame;
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+import javax.swing.WindowConstants;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+
+public class VariationSelectorTest {
+ // A font supporting Unicode variation selectors is required
+ // At least DejaVu 2.20 from 2007
+ private static final Font FONT = new Font("DejaVu Sans", Font.PLAIN, 12);
+
+ public static void main(String[] args) {
+ final String fontName = FONT.getFontName();
+ if (!fontName.equals("DejaVuSans")) {
+ System.err.println("*** Warning: Font DejaVuSans not installed.");
+ System.err.println("*** Using font: " + fontName);
+ }
+ SwingUtilities.invokeLater(() -> {
+ JFrame frame = new JFrame();
+ frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ frame.add(new MyComponent());
+ frame.setSize(200, 200);
+ frame.setVisible(true);
+ frame.setLocationRelativeTo(null);
+ });
+ }
+
+ private static class MyComponent extends JComponent {
+ @Override
+ protected void paintComponent(Graphics g) {
+ Graphics2D g2d = (Graphics2D) g;
+ FontRenderContext frc = g2d.getFontRenderContext();
+ String text = "a";
+ GlyphVector gv = FONT.layoutGlyphVector(
+ frc, text.toCharArray(), 0, text.length(),
+ Font.LAYOUT_LEFT_TO_RIGHT);
+ System.out.println("'a'=" + gv.getNumGlyphs());
+ g2d.drawString("=" + gv.getNumGlyphs() + " ('a')", 100, 50);
+ g2d.drawGlyphVector(gv, 80, 50);
+ String text2 = "a\ufe00";
+ GlyphVector gv2 = FONT.layoutGlyphVector(
+ frc, text2.toCharArray(), 0, text2.length(),
+ Font.LAYOUT_LEFT_TO_RIGHT);
+ g2d.drawGlyphVector(gv2, 80, 100);
+ System.out.println("'a'+VS=" + gv2.getNumGlyphs());
+ g2d.drawString("=" + gv2.getNumGlyphs() + " ('a'+VS)", 100, 100);
+ if ((gv.getNumGlyphs() == 1) && (gv2.getNumGlyphs() == 1)) {
+ System.out.println("PASS");
+ g2d.drawString("PASS", 10, 15);
+ } else {
+ System.err.println("FAIL");
+ g2d.drawString("FAIL", 10, 15);
+ }
+ }
+ }
+}