PDFBOX-5808: add support for GSUB lookup type 3, by Fabrice Calafat

git-svn-id: https://svn.apache.org/repos/asf/pdfbox/trunk@1917433 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java
index 7675b2c..7152c7c 100644
--- a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java
@@ -46,8 +46,10 @@
 import org.apache.fontbox.ttf.table.common.LookupTable;
 import org.apache.fontbox.ttf.table.common.RangeRecord;
 import org.apache.fontbox.ttf.table.common.ScriptTable;
+import org.apache.fontbox.ttf.table.gsub.AlternateSetTable;
 import org.apache.fontbox.ttf.table.gsub.LigatureSetTable;
 import org.apache.fontbox.ttf.table.gsub.LigatureTable;
+import org.apache.fontbox.ttf.table.gsub.LookupTypeAlternateSubstitutionFormat1;
 import org.apache.fontbox.ttf.table.gsub.LookupTypeLigatureSubstitutionSubstFormat1;
 import org.apache.fontbox.ttf.table.gsub.LookupTypeMultipleSubstitutionFormat1;
 import org.apache.fontbox.ttf.table.gsub.LookupTypeSingleSubstFormat1;
@@ -308,6 +310,10 @@
                 // Multiple Substitution Subtable
                 // https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#lookuptype-2-multiple-substitution-subtable
                 return readMultipleSubstitutionSubtable(data, offset);
+            case 3:
+                // Alternate Substitution Subtable
+                // https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#lookuptype-3-alternate-substitution-subtable
+                return readAlternateSubstitutionSubtable(data, offset);
             case 4:
                 // Ligature Substitution Subtable
                 // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#LS
@@ -365,6 +371,7 @@
         {
         case 1:
         case 2:
+        case 3:
         case 4:
             for (int i = 0; i < subTableCount; i++)
             {
@@ -482,6 +489,49 @@
         return new LookupTypeMultipleSubstitutionFormat1(substFormat, coverageTable, sequenceTables);
     }
 
+    private LookupSubTable readAlternateSubstitutionSubtable(TTFDataStream data, long offset) throws IOException
+    {
+        data.seek(offset);
+        int substFormat = data.readUnsignedShort();
+
+        if (substFormat != 1)
+        {
+            throw new IOException(
+                    "The expected SubstFormat for AlternateSubstitutionTable is 1");
+        }
+
+        int coverage = data.readUnsignedShort();
+        int altSetCount = data.readUnsignedShort();
+
+        int[] alternateOffsets = new int[altSetCount];
+
+        for (int i = 0; i < altSetCount; i++)
+        {
+            alternateOffsets[i] = data.readUnsignedShort();
+        }
+
+        CoverageTable coverageTable = readCoverageTable(data, offset + coverage);
+
+        if (altSetCount != coverageTable.getSize())
+        {
+            throw new IOException(
+                    "According to the OpenTypeFont specifications, the coverage count should be equal to the no. of AlternateSetTable");
+        }
+
+        AlternateSetTable[] alternateSetTables = new AlternateSetTable[altSetCount];
+
+        for (int i = 0; i < altSetCount; i++)
+        {
+            data.seek(offset + alternateOffsets[i]);
+            int glyphCount = data.readUnsignedShort();
+            int[] alternateGlyphIDs = data.readUnsignedShortArray(glyphCount);
+            alternateSetTables[i] = new AlternateSetTable(glyphCount, alternateGlyphIDs);
+        }
+
+        return new LookupTypeAlternateSubstitutionFormat1(substFormat, coverageTable,
+                alternateSetTables);
+    }
+
     private LookupSubTable readLigatureSubstitutionSubtable(TTFDataStream data, long offset)
             throws IOException
     {
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
index 16609ff..8de1f24 100644
--- a/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
@@ -47,6 +47,7 @@
     private float version;
     private int numberOfGlyphs = -1;
     private int unitsPerEm = -1;
+    private boolean enableGsub = true;
     protected final Map<String,TTFTable> tables = new HashMap<>();
     private final TTFDataStream data;
     private volatile Map<String, Integer> postScriptNames;
@@ -74,7 +75,7 @@
     /**
      * @return Returns the version.
      */
-    public float getVersion() 
+    public float getVersion()
     {
         return version;
     }
@@ -87,7 +88,24 @@
     {
         version = versionValue;
     }
-    
+
+    /**
+     * @return Returns true if the GSUB table can be used for this font
+     */
+    public boolean isEnableGsub()
+    {
+        return enableGsub;
+    }
+
+    /**
+     * Enable or disable the GSUB table for this font.
+     * GSUB table is enabled by default.
+     */
+    public void setEnableGsub(boolean enableGsub)
+    {
+        this.enableGsub = enableGsub;
+    }
+
     /**
      * Add a table definition. Package-private, used by TTFParser only.
      * 
@@ -638,13 +656,17 @@
     /**
      * Returns the GSubData of the GlyphSubstitutionTable if present.
      * 
-     * @return the GSubData of the GlyphSubstitutionTable or {@link GsubData#NO_DATA_FOUND} if either no GSUB data is
-     * available or its scripts are not supported
-     * 
+     * @return the GSubData of the GlyphSubstitutionTable or {@link GsubData#NO_DATA_FOUND} if no GSUB data is
+     * available, its scripts are not supported or it was disabled for that font
      * @throws IOException if the font data could not be read
      */
     public GsubData getGsubData() throws IOException
     {
+        if (!enableGsub)
+        {
+            return GsubData.NO_DATA_FOUND;
+        }
+
         GlyphSubstitutionTable table = getGsub();
         if (table == null)
         {
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/gsub/GlyphSubstitutionDataExtractor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/gsub/GlyphSubstitutionDataExtractor.java
index 04f4565..2b051f4 100644
--- a/fontbox/src/main/java/org/apache/fontbox/ttf/gsub/GlyphSubstitutionDataExtractor.java
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/gsub/GlyphSubstitutionDataExtractor.java
@@ -34,8 +34,10 @@
 import org.apache.fontbox.ttf.table.common.LookupSubTable;
 import org.apache.fontbox.ttf.table.common.LookupTable;
 import org.apache.fontbox.ttf.table.common.ScriptTable;
+import org.apache.fontbox.ttf.table.gsub.AlternateSetTable;
 import org.apache.fontbox.ttf.table.gsub.LigatureSetTable;
 import org.apache.fontbox.ttf.table.gsub.LigatureTable;
+import org.apache.fontbox.ttf.table.gsub.LookupTypeAlternateSubstitutionFormat1;
 import org.apache.fontbox.ttf.table.gsub.LookupTypeLigatureSubstitutionSubstFormat1;
 import org.apache.fontbox.ttf.table.gsub.LookupTypeMultipleSubstitutionFormat1;
 import org.apache.fontbox.ttf.table.gsub.LookupTypeSingleSubstFormat1;
@@ -172,6 +174,11 @@
                 extractDataFromLigatureSubstitutionSubstFormat1Table(glyphSubstitutionMap,
                         (LookupTypeLigatureSubstitutionSubstFormat1) lookupSubTable);
             }
+            else if (lookupSubTable instanceof LookupTypeAlternateSubstitutionFormat1)
+            {
+                extractDataFromAlternateSubstitutionSubstFormat1Table(glyphSubstitutionMap,
+                        (LookupTypeAlternateSubstitutionFormat1) lookupSubTable);
+            }
             else if (lookupSubTable instanceof LookupTypeSingleSubstFormat1)
             {
                 extractDataFromSingleSubstTableFormat1Table(glyphSubstitutionMap,
@@ -277,6 +284,47 @@
 
     }
 
+    /**
+     * Extracts data from the AlternateSubstitutionFormat1 (lookuptype) 3 table and puts it in the glyphSubstitutionMap
+     * This is added for Zola usage
+     *
+     * @param glyphSubstitutionMap         the map to store the substitution data
+     * @param alternateSubstitutionFormat1 the alternate substitution format 1 table
+     */
+    private void extractDataFromAlternateSubstitutionSubstFormat1Table(
+            Map<List<Integer>, Integer> glyphSubstitutionMap,
+            LookupTypeAlternateSubstitutionFormat1 alternateSubstitutionFormat1)
+    {
+
+        CoverageTable coverageTable = alternateSubstitutionFormat1.getCoverageTable();
+
+        if (coverageTable.getSize() != alternateSubstitutionFormat1.getAlternateSetTables().length)
+        {
+            LOG.warn("The coverage table size (" + coverageTable.getSize() +
+                    ") should be the same as the count of the atlternate set tables (" +
+                    alternateSubstitutionFormat1.getAlternateSetTables().length + ")");
+            return;
+        }
+
+        for (int i = 0; i < coverageTable.getSize(); i++)
+        {
+            int coverageGlyphId = coverageTable.getGlyphId(i);
+            AlternateSetTable sequenceTable = alternateSubstitutionFormat1.getAlternateSetTables()[i];
+
+            // Loop through the substitute glyphs and pick the first one that is not the same as the coverage glyph
+            for (int alternateGlyphId : sequenceTable.getAlternateGlyphIDs())
+            {
+                if (alternateGlyphId != coverageGlyphId)
+                {
+                    putNewSubstitutionEntry(glyphSubstitutionMap, alternateGlyphId,
+                            Collections.singletonList(coverageGlyphId));
+                    break;
+                }
+            }
+        }
+
+    }
+
     private void extractDataFromLigatureTable(Map<List<Integer>, Integer> glyphSubstitutionMap,
             LigatureTable ligatureTable)
     {
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/table/gsub/AlternateSetTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/table/gsub/AlternateSetTable.java
new file mode 100644
index 0000000..0c23684
--- /dev/null
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/table/gsub/AlternateSetTable.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.fontbox.ttf.table.gsub;
+
+import java.util.Arrays;
+
+/**
+ * LookupType 3: Alternate Substitution Subtable
+ * as described in OpenType spec: <a href="https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#31-alternate-substitution-format-1">...</a>
+ */
+public class AlternateSetTable
+{
+    private final int glyphCount;
+    private final int[] alternateGlyphIDs;
+
+    public AlternateSetTable(int glyphCount, int[] alternateGlyphIDs)
+    {
+        this.glyphCount = glyphCount;
+        this.alternateGlyphIDs = alternateGlyphIDs;
+    }
+
+    public int getGlyphCount()
+    {
+        return glyphCount;
+    }
+
+    public int[] getAlternateGlyphIDs()
+    {
+        return alternateGlyphIDs;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "AlternateSetTable{" + "glyphCount=" + glyphCount + ", alternateGlyphIDs=" + Arrays.toString(alternateGlyphIDs) + '}';
+    }
+}
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/table/gsub/LookupTypeAlternateSubstitutionFormat1.java b/fontbox/src/main/java/org/apache/fontbox/ttf/table/gsub/LookupTypeAlternateSubstitutionFormat1.java
new file mode 100644
index 0000000..d6eae17
--- /dev/null
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/table/gsub/LookupTypeAlternateSubstitutionFormat1.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.fontbox.ttf.table.gsub;
+
+import org.apache.fontbox.ttf.table.common.CoverageTable;
+import org.apache.fontbox.ttf.table.common.LookupSubTable;
+
+/**
+ * Lookup Type 3: Alternate Substitution Subtable
+ * as described in OpenType spec: <a href="https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#31-alternate-substitution-format-1">...</a>
+ */
+public class LookupTypeAlternateSubstitutionFormat1 extends LookupSubTable {
+    private final AlternateSetTable[] alternateSetTables;
+
+    public LookupTypeAlternateSubstitutionFormat1(
+            int substFormat, CoverageTable coverageTable, AlternateSetTable[] alternateSetTables) {
+        super(substFormat, coverageTable);
+        this.alternateSetTables = alternateSetTables;
+    }
+
+    public AlternateSetTable[] getAlternateSetTables() {
+        return alternateSetTables;
+    }
+
+    @Override
+    public int doSubstitution(int gid, int coverageIndex) {
+        throw new UnsupportedOperationException("not applicable");
+    }
+}