8187100: Support Unicode Variation Selectors
authorsrl
Mon, 25 Jun 2018 11:40:46 -0700
changeset 50840 1db5917dfe1c
parent 50838 732a3b600098
child 50841 1a9ebf66fd9f
8187100: Support Unicode Variation Selectors Summary: Support Unicode Variation Selectors Reviewed-by: prr, srl Contributed-by: Toshio Nakamura <toshiona@jp.ibm.com>
src/java.desktop/share/classes/sun/font/CMap.java
src/java.desktop/share/classes/sun/font/CharToGlyphMapper.java
src/java.desktop/share/classes/sun/font/CompositeGlyphMapper.java
src/java.desktop/share/classes/sun/font/Font2D.java
src/java.desktop/share/classes/sun/font/TrueTypeGlyphMapper.java
src/java.desktop/share/native/common/font/sunfontids.h
src/java.desktop/share/native/libfontmanager/hb-jdk-font.cc
src/java.desktop/share/native/libfontmanager/sunFont.c
test/jdk/java/awt/font/TextLayout/TestVS-expect.png
test/jdk/java/awt/font/TextLayout/TestVS.java
test/jdk/java/awt/font/TextLayout/VariationSelectorTest.java
--- 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);
+            }
+        }
+    }
+}