diff --git a/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AdvancedTextLayout.java b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AdvancedTextLayout.java new file mode 100644 index 00000000000..84fc65f7875 --- /dev/null +++ b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AdvancedTextLayout.java @@ -0,0 +1,115 @@ +/* + * 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.pdfbox.examples.pdmodel; + +import java.io.IOException; + +import org.apache.fontbox.ttf.OTFParser; +import org.apache.fontbox.ttf.OpenTypeFont; +import org.apache.fontbox.ttf.advanced.GlyphVectorAdvanced; +import org.apache.fontbox.ttf.advanced.api.AdvancedOTFParser; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; +import org.apache.fontbox.ttf.advanced.api.GlyphVector; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.util.Matrix; + +/** + * An example of using an embedded OpenType font with advanced glyph layout. + * + * @author Daniel Fickling + */ +public final class AdvancedTextLayout +{ + private AdvancedTextLayout() + { + } + + private static float getAdvance(float startX, GlyphVector vector, float fontSize) { + return (((vector.getWidth() / 1000f) * fontSize) + startX); + } + + private static Matrix createMatrix(float translateX, float translateY) { + return Matrix.getTranslateInstance(translateX, translateY); + } + + public static void main(String[] args) throws IOException + { + try (PDDocument document = new PDDocument()) + { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + //String dir = "C:\\Users\\daniel\\Desktop\\fonts\\"; + //String fontFile = "./pdfbox/src/main/resources/org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf"; + //String fontFile = dir + "IndieFlower-Regular.ttf"; + //String fontFile = dir + "SourceSansPro-Regular.ttf"; + String dir = "/home/volker/work/"; + String fontFile = dir + "NotoSans-Regular.ttf"; + + + AdvancedOTFParser fontParser = new AdvancedOTFParser(); + AdvancedOpenTypeFont otFont = fontParser.parse(fontFile); + PDFont font = PDType0Font.load(document, otFont, true); + + GlyphVector vector = null; + float x = 10; + + try (PDPageContentStream stream = new PDPageContentStream(document, page)) + { + float fontSize = 20; + stream.beginText(); + stream.setFont(font, fontSize); + + vector = otFont.createGlyphVector("PDFBox's Unicode with Embedded OpenType Font", (int)fontSize); + stream.setTextMatrix(createMatrix(x, 200)); + stream.showGlyphVector(vector); + x = getAdvance(10, vector, fontSize); + + vector = otFont.createGlyphVector("|AFTER (SIMPLE)", (int)fontSize); + stream.setTextMatrix(createMatrix(x, 200)); + stream.showGlyphVector(vector); + x = 10; + + String complex = "A̋L̦ a̋ N̂N̦B ўў 1/2"; + + vector = otFont.createGlyphVector(complex, (int)fontSize); + stream.setTextMatrix(createMatrix(x, 100)); + stream.showGlyphVector(vector); + x = getAdvance(10, vector, fontSize); + + vector = otFont.createGlyphVector("|AFTER (COMPLEX)", (int)fontSize); + stream.setTextMatrix(createMatrix(x, 100)); + stream.showGlyphVector(vector); + + stream.setTextMatrix(Matrix.getTranslateInstance(10, 160)); + stream.showText(complex); + stream.setTextMatrix(Matrix.getTranslateInstance(10 + (font.getStringWidth(complex) / 1000f * fontSize), 160)); + stream.showText("|AFTER (BAD)"); + + stream.endText(); + } + + document.save("advanced-text.pdf"); + } + } +} diff --git a/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AdvancedTextLayoutSequencesDin91379.java b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AdvancedTextLayoutSequencesDin91379.java new file mode 100644 index 00000000000..266bc979c2f --- /dev/null +++ b/examples/src/main/java/org/apache/pdfbox/examples/pdmodel/AdvancedTextLayoutSequencesDin91379.java @@ -0,0 +1,321 @@ +/* + * 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. + */ + +/* +Comparing Java AWT layout vector and AdvancedLayout: factor 50 = 1000/(20=fontSize), y different sign +Small differences, awt.Glyphlayout has data type float, AdvancedLayout integer + +hb-shape, see HarfBUzz documentation: + hb_position_t x_offset; how much the glyph moves on the X-axis before drawing it, this should not affect how much the line advances. + hb_position_t y_offset; how much the glyph moves on the Y-axis before drawing it, this should not affect how much the line advances. + hb_position_t x_advance; how much the line advances after drawing this glyph when setting text in horizontal direction. + hb_position_t y_advance; how much the line advances after drawing this glyph when setting text in vertical direction. + +java.awt.font.GlyphVector + Positioning is possible using glyph positions (relative to origin of text) using MOVE_TEXT (newLineAtOffset()). + Point2D p = awtGlyphVector.getGlyphPosition(i); + AdvanceX is needed for positioning using SHOW_TEXT_ADJUSTED (showTextWithPositioning()) + float ax = awtGlyphVector.getGlyphMetrics(i).getAdvanceX(); + AdvanceY is not needed for horizontal scripts. + +GlyphVectorAdvanced + ??? + +2022-05-28 + Error positioning double accents, e.g. "C̨̆" GlyphVectorAdvanced does not contain positioning information for the second accent. +*/ +package org.apache.pdfbox.examples.pdmodel; + +import org.apache.fontbox.ttf.advanced.api.GlyphVector; +import org.apache.fontbox.ttf.advanced.GlyphVectorAdvanced; +import org.apache.fontbox.ttf.advanced.api.AdvancedOTFParser; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.util.Matrix; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.io.File; +import java.io.IOException; +import java.text.AttributedString; +import java.text.Bidi; + +/** + * An example of using an embedded OpenType font with advanced glyph layout for + * the sequences of DIN91379 + * + * + * @author Volker Kunert + * @author Daniel Fickling + * @see AdvancedTextLayout + */ +public final class AdvancedTextLayoutSequencesDin91379 +{ + public static String sequencesDin91379 = /*"C̨̆x"; */ + "A̋ C̀ C̄ C̆ C̈ C̕ C̣ C̦ C̨̆ D̂ F̀ F̄ G̀ H̄ H̦ H̱ J́ J̌ K̀ K̂ K̄ K̇ K̕ K̛ K̦ K͟H \n" + + "K͟h L̂ L̥ L̥̄ L̦ M̀ M̂ M̆ M̐ N̂ N̄ N̆ N̦ P̀ P̄ P̕ P̣ R̆ R̥ R̥̄ S̀ S̄ S̛̄ S̱ T̀ T̄ \n" + + "T̈ T̕ T̛ U̇ Z̀ Z̄ Z̆ Z̈ Z̧ a̋ c̀ c̄ c̆ c̈ c̕ c̣ c̦ c̨̆ d̂ f̀ f̄ g̀ h̄ h̦ j́ k̀ \n" + + "k̂ k̄ k̇ k̕ k̛ k̦ k͟h l̂ l̥ l̥̄ l̦ m̀ m̂ m̆ m̐ n̂ n̄ n̆ n̦ p̀ p̄ p̕ p̣ r̆ r̥ r̥̄ \n" + + "s̀ s̄ s̛̄ s̱ t̀ t̄ t̕ t̛ u̇ z̀ z̄ z̆ z̈ z̧ Ç̆ Û̄ ç̆ û̄ ÿ́ Č̕ Č̣ č̕ č̣ Ī́ ī́ Ž̦ \n" + + "Ž̧ ž̦ ž̧ Ḳ̄ ḳ̄ Ṣ̄ ṣ̄ Ṭ̄ ṭ̄ Ạ̈ ạ̈ Ọ̈ ọ̈ Ụ̄ Ụ̈ ụ̄ ụ̈ \n"; +/**/ + + private AdvancedTextLayoutSequencesDin91379() + { + } + + + public static void main(String[] args) { + if (args.length < 1) { + throw new RuntimeException("Usage AdvancedTextLayoutSequencesDin91379 directory"); + } + String dir = args[0]; + String[] fontFileNames = new String[] { + "NotoSans-Regular.ttf", + /* "LiberationSans-Regular.ttf", + "DejaVuSans.ttf", + "IBMPlexSans-Regular.ttf", + "SourceSans3-Regular.ttf", + + */ + }; + float[] fontSizes = new float[]{20f}; + + for (float fontSize: fontSizes) { + for (String fontFileName : fontFileNames) { + try { + System.out.printf("--font:%s%n", fontFileName); + testJava2D(dir, fontFileName, fontSize, sequencesDin91379); + testAdvancedLayout(dir, fontFileName, fontSize, sequencesDin91379); + } catch (Exception ee) { + ee.printStackTrace(); + } + } + } + } + + public static void testJava2D(String dir, String fontFileName, float fontSize, String s) { + + try { + PDDocument pdDocument = new PDDocument(); + PDPage page = new PDPage(); + pdDocument.addPage(page); + PDPageContentStream cs = new PDPageContentStream(pdDocument, pdDocument.getPage(0), + PDPageContentStream.AppendMode.APPEND, true); + + File fontFile = new File(dir + "/" + fontFileName ); + PDType0Font font = PDType0Font.load(pdDocument, fontFile); + Font awtFont = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(fontSize); + float x = page.getBBox().getLowerLeftX(); + float y = page.getBBox().getUpperRightY() - awtFont.getSize2D(); + System.out.printf("testJava2D ur=%f h=%f %n", page.getBBox().getUpperRightY(), awtFont.getSize2D()); + System.out.printf("testJava2D (x,y)=(%f, %f)%n", x, y); + + + testSequences2D(cs, font, fontSize, awtFont, x, y, s); + cs.close(); + pdDocument.save(String.format("%s/TestDin91379Java2D-%s-%s.pdf", dir, fontFileName, fontSize)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void testSequences2D(PDPageContentStream cs, PDType0Font font, + float fontSize, Font awtFont, float x, float y, String s) throws Exception { + + s = s.replaceAll("[ \t]", " "); + String[] lines = s.split("\n"); + + for (String line : lines) { + if (line.length() > 0) { + testSequencesLine2D(cs, font, fontSize, awtFont, line, x, y); + } + y -= awtFont.getSize2D()*1.2f; + } + } + + public static void printAdjustments2D(String line, java.awt.font.GlyphVector awtGlyphVector, float fontSize) { + int[] gids = awtGlyphVector.getGlyphCodes(0, awtGlyphVector.getNumGlyphs(), null); + System.out.println("--Java2D--"); + System.out.println(line); + float lastX = 0f; + float lastY = 0f; + float lastAx = 0f; + float lastAy = 0f; + for (int i=0; i 0) { + vector = otFont.createGlyphVector(line, (int)fontSize); + System.out.printf("fontbox vector=%s%n",vector.toString()); + printAdjustmentsAdvancedLayout(line, vector); + stream.showGlyphVector(vector); + } + stream.newLineAtOffset(0, -24); + System.out.printf("(x,y)=(%f, %f) l=%s%n", x, y, line); + } + stream.endText(); + } + document.save(String.format("%s/TestDin91379AdvancedLayout-%s-%s.pdf", dir, fontFileName, fontSize)); + } + } +} diff --git a/fontbox/pom.xml b/fontbox/pom.xml index 65459162a80..2dbd09bf823 100644 --- a/fontbox/pom.xml +++ b/fontbox/pom.xml @@ -143,6 +143,16 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.0 + + + --allow-script-in-comments + + + diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/CFFTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/CFFTable.java index 136b09fe316..3d68e56afbb 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/CFFTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/CFFTable.java @@ -33,7 +33,10 @@ public class CFFTable extends TTFTable private CFFFont cffFont; - CFFTable(TrueTypeFont font) + /** + * Constructs and reads the CFF table. + */ + public CFFTable(TrueTypeFont font) { super(font); } @@ -46,7 +49,7 @@ public class CFFTable extends TTFTable * @throws java.io.IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { byte[] bytes = data.read((int)getLength()); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java index 12ee5d4dc39..e5ebff873fa 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/CmapSubtable.java @@ -642,6 +642,22 @@ private int getCharCode(int gid) return glyphIdToCharacterCode[gid]; } + /** + * Returns the character code for the given GID, or null if there is none. + * + * @param gid glyph id + * @return character code + */ + public Integer getCharacterCode(int gid) + { + int code = getCharCode(gid); + if (code == -1) + { + return null; + } + return Integer.valueOf(code); + } + /** * Returns all possible character codes for the given gid, or null if there is none. * diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java index 04d4501660f..4b47f02aecb 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/CmapTable.java @@ -69,7 +69,7 @@ public class CmapTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { @SuppressWarnings({"unused", "squid:S1854", "squid:S1481"}) int version = data.readUnsignedShort(); 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 d65961af452..ef8ea5b8044 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphSubstitutionTable.java @@ -81,7 +81,7 @@ public class GlyphSubstitutionTable extends TTFTable @Override @SuppressWarnings({"squid:S1854"}) - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { long start = data.getCurrentPosition(); @SuppressWarnings({"unused"}) diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java index 5e9d03a4525..40d7b170cd0 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphTable.java @@ -62,7 +62,7 @@ public class GlyphTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { loca = ttf.getIndexToLocation(); numGlyphs = ttf.getNumberOfGlyphs(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/HeaderTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/HeaderTable.java index 90a827e9052..9bc7669f757 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/HeaderTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/HeaderTable.java @@ -72,7 +72,7 @@ public class HeaderTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.read32Fixed(); fontRevision = data.read32Fixed(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalHeaderTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalHeaderTable.java index d9d3e1805b4..9da9479ee09 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalHeaderTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalHeaderTable.java @@ -61,7 +61,7 @@ public class HorizontalHeaderTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.read32Fixed(); ascender = data.readSignedShort(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalMetricsTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalMetricsTable.java index 5aa81bb39b2..9c4938308fe 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalMetricsTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/HorizontalMetricsTable.java @@ -48,7 +48,7 @@ public class HorizontalMetricsTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { HorizontalHeaderTable hHeader = ttf.getHorizontalHeader(); if (hHeader == null) @@ -118,6 +118,15 @@ public int getAdvanceWidth(int gid) } } + /** + * Returns array of advance widths. + * @return array of advance widths + */ + public int[] getAdvanceWidths() + { + return advanceWidth; + } + /** * Returns the left side bearing for the given GID. * diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/IndexToLocationTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/IndexToLocationTable.java index 5ba11ca5aed..64428197f81 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/IndexToLocationTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/IndexToLocationTable.java @@ -48,7 +48,7 @@ public class IndexToLocationTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { HeaderTable head = ttf.getHeader(); if (head == null) diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/KerningTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/KerningTable.java index 5070fff3e73..68a30f52758 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/KerningTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/KerningTable.java @@ -51,7 +51,7 @@ public class KerningTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { int version = data.readUnsignedShort(); if (version != 0) diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/MaximumProfileTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/MaximumProfileTable.java index abd902edc77..2f29033667e 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/MaximumProfileTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/MaximumProfileTable.java @@ -270,7 +270,7 @@ public void setVersion(float versionValue) * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.read32Fixed(); numGlyphs = data.readUnsignedShort(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/NamingTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/NamingTable.java index 1b98a3ea5e6..51ea87f51b1 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/NamingTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/NamingTable.java @@ -57,7 +57,7 @@ public class NamingTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { int formatSelector = data.readUnsignedShort(); int numberOfNameRecords = data.readUnsignedShort(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java index 8900f2f855b..7fc2ab0f542 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OS2WindowsMetricsTable.java @@ -794,7 +794,7 @@ public int getMaxContext() * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.readUnsignedShort(); averageCharWidth = data.readSignedShort(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java index 00ace1698cb..652c59fcf40 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OTFParser.java @@ -24,14 +24,14 @@ /** * OpenType font file parser. */ -public final class OTFParser extends TTFParser +public class OTFParser extends TTFParser { /** * Constructor. */ public OTFParser() { - super(); + this(false); } /** @@ -62,13 +62,13 @@ public OpenTypeFont parse(RandomAccessRead randomAccessRead) throws IOException } @Override - OpenTypeFont parse(TTFDataStream raf) throws IOException + protected OpenTypeFont parse(TTFDataStream raf) throws IOException { return (OpenTypeFont)super.parse(raf); } @Override - OpenTypeFont newFont(TTFDataStream raf) + protected OpenTypeFont newFont(TTFDataStream raf) { return new OpenTypeFont(raf); } @@ -77,9 +77,11 @@ OpenTypeFont newFont(TTFDataStream raf) protected TTFTable readTable(TrueTypeFont font, String tag) { // todo: this is a stub, a full implementation is needed + assert font instanceof OpenTypeFont; switch (tag) { case "BASE": + case "JSTF": case "GDEF": case "GPOS": case GlyphSubstitutionTable.TAG: diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OTLTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OTLTable.java index b115e643c25..8835ddcd2dd 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OTLTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OTLTable.java @@ -25,8 +25,11 @@ public class OTLTable extends TTFTable public static final String TAG = "JSTF"; // todo: this is a stub, a full implementation is needed - - OTLTable(TrueTypeFont font) + + /** + * Creates and reads the OTL table. + */ + public OTLTable(TrueTypeFont font) { super(font); } diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeFont.java b/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeFont.java index b52cc3375ab..f536c6daa86 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeFont.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/OpenTypeFont.java @@ -32,7 +32,7 @@ public class OpenTypeFont extends TrueTypeFont * * @param fontData The font data. */ - OpenTypeFont(TTFDataStream fontData) + protected OpenTypeFont(TTFDataStream fontData) { super(fontData); } diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/PostScriptTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/PostScriptTable.java index 00447ae0218..e9cafedc195 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/PostScriptTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/PostScriptTable.java @@ -57,7 +57,7 @@ public class PostScriptTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { formatType = data.read32Fixed(); italicAngle = data.read32Fixed(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java index ce877a970e8..c27269ebc45 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFDataStream.java @@ -30,7 +30,7 @@ * * @author Ben Litchfield */ -abstract class TTFDataStream implements Closeable +public abstract class TTFDataStream implements Closeable { TTFDataStream() { @@ -82,6 +82,16 @@ public String readString(int length, Charset charset) throws IOException */ public abstract int read() throws IOException; + /** + * Skip bytes. + * + * @throws IOException If there is an error reading the data. + */ + public void skip(int count) throws IOException { + while (count-- > 0) + read(); + } + /** * Read an unsigned byte. * diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java index 4d45e024b26..ed644649574 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java @@ -62,7 +62,7 @@ public TTFParser(boolean isEmbedded) public TTFParser(boolean isEmbedded, boolean parseOnDemand) { this.isEmbedded = isEmbedded; - parseOnDemandOnly = parseOnDemand; + this.parseOnDemandOnly = parseOnDemand; } /** @@ -234,10 +234,31 @@ protected boolean allowCFF() return false; } - private TTFTable readTableDirectory(TrueTypeFont font, TTFDataStream raf) throws IOException + protected TTFTable readTableDirectory(TrueTypeFont font, TTFDataStream raf) throws IOException { - TTFTable table; String tag = raf.readString(4); + + TTFTable table = createTable(font, tag); + + if (table == null) + table = readTable(font, tag); + table.setTag(tag); + table.setCheckSum(raf.readUnsignedInt()); + table.setOffset(raf.readUnsignedInt()); + table.setLength(raf.readUnsignedInt()); + + // skip tables with zero length (except glyf) + if (table.getLength() == 0 && !tag.equals(GlyphTable.TAG)) + { + return null; + } + + return table; + } + + protected TTFTable createTable(TrueTypeFont font, String tag) + { + TTFTable table = null; switch (tag) { case CmapTable.TAG: @@ -289,20 +310,8 @@ private TTFTable readTableDirectory(TrueTypeFont font, TTFDataStream raf) throws table = new GlyphSubstitutionTable(font); break; default: - table = readTable(font, tag); break; } - table.setTag(tag); - table.setCheckSum(raf.readUnsignedInt()); - table.setOffset(raf.readUnsignedInt()); - table.setLength(raf.readUnsignedInt()); - - // skip tables with zero length (except glyf) - if (table.getLength() == 0 && !tag.equals(GlyphTable.TAG)) - { - return null; - } - return table; } diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFTable.java index 9d3ae6dfce0..b9aaa656202 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFTable.java @@ -45,7 +45,7 @@ public class TTFTable * * @param font The font which contains this table. */ - TTFTable(TrueTypeFont font) + protected TTFTable(TrueTypeFont font) { this.font = font; } @@ -61,7 +61,7 @@ public long getCheckSum() /** * @param checkSumValue The checkSum to set. */ - void setCheckSum(long checkSumValue) + public void setCheckSum(long checkSumValue) { this.checkSum = checkSumValue; } @@ -77,7 +77,7 @@ public long getLength() /** * @param lengthValue The length to set. */ - void setLength(long lengthValue) + public void setLength(long lengthValue) { this.length = lengthValue; } @@ -93,7 +93,7 @@ public long getOffset() /** * @param offsetValue The offset to set. */ - void setOffset(long offsetValue) + public void setOffset(long offsetValue) { this.offset = offsetValue; } @@ -109,7 +109,7 @@ public String getTag() /** * @param tagValue The tag to set. */ - void setTag(String tagValue) + public void setTag(String tagValue) { this.tag = tagValue; } @@ -131,7 +131,7 @@ public boolean getInitialized() * @param data The stream to read the data from. * @throws IOException If there is an error reading the data. */ - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) 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 518106ce247..67ec940de83 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java @@ -31,6 +31,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fontbox.FontBoxFont; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; import org.apache.fontbox.ttf.model.GsubData; import org.apache.fontbox.util.BoundingBox; @@ -62,7 +63,7 @@ public class TrueTypeFont implements FontBoxFont, Closeable */ TrueTypeFont(TTFDataStream fontData) { - data = fontData; + this.data = fontData; } @Override @@ -364,13 +365,13 @@ public long getOriginalDataSize() } /** - * Read the given table if necessary. Package-private, used by TTFParser only. + * Read the given table if necessary. Protected, used by TTFParser and subclasses only. * * @param table the table to be initialized * * @throws IOException if there was an error reading the table. */ - void readTable(TTFTable table) throws IOException + protected void readTable(TTFTable table) throws IOException { // PDFBOX-4219: synchronize on data because it is accessed by several threads // when PDFBox is accessing a standard 14 font for the first time @@ -433,6 +434,26 @@ public int getUnitsPerEm() throws IOException return unitsPerEm; } + /** + * Convert from truetype units to pdf units based on the + * unitsPerEm field in the "head" table + * @param ttfUnits truetype units + * @return pdf units + */ + public int convertTTFUnit2PDFUnit(int ttfUnits) throws IOException { + int pdfUnits; + int upem = getUnitsPerEm(); + if (ttfUnits < 0) { + long rest1 = ttfUnits % upem; + long storrest = 1000 * rest1; + long ledd2 = (storrest != 0 ? rest1 / storrest : 0); + pdfUnits = -((-1000 * ttfUnits) / upem - (int) ledd2); + } else { + pdfUnits = (ttfUnits / upem) * 1000 + ((ttfUnits % upem) * 1000) / upem; + } + return pdfUnits; + } + /** * Returns the width for the given GID. * @@ -454,6 +475,16 @@ public int getAdvanceWidth(int gid) throws IOException } } + /** + * Returns array of advance widths. + * @return array of advance widths + */ + public int[] getAdvanceWidths() throws IOException + { + HorizontalMetricsTable hmtx = getHorizontalMetrics(); + return (hmtx != null) ? hmtx.getAdvanceWidths() : null; + } + /** * Returns the height for the given GID. * @@ -475,6 +506,16 @@ public int getAdvanceHeight(int gid) throws IOException } } + /** + * Returns array of advance heights. + * @return array of advance heights + */ + public int[] getAdvanceHeights() throws IOException + { + VerticalMetricsTable vmtx = getVerticalMetrics(); + return (vmtx != null) ? vmtx.getAdvanceHeights() : null; + } + @Override public String getName() throws IOException { @@ -520,6 +561,73 @@ private void readPostScriptNames() throws IOException } } + /** + * Returns the best Unicode from the font (the most general). The PDF spec says that "The means + * by which this is accomplished are implementation-dependent." + * + * @throws IOException if the font could not be read + */ + public CmapSubtable getUnicodeCmap() throws IOException + { + return getUnicodeCmap(true); + } + + /** + * Returns the best Unicode from the font (the most general). The PDF spec says that "The means + * by which this is accomplished are implementation-dependent." + * + * @param isStrict False if we allow falling back to any cmap, even if it's not Unicode. + * @throws IOException if the font could not be read, or there is no Unicode cmap + */ + public CmapSubtable getUnicodeCmap(boolean isStrict) throws IOException + { + CmapTable cmapTable = getCmap(); + if (cmapTable == null) + { + if (isStrict) + { + throw new IOException("The TrueType font does not contain a 'cmap' table"); + } + else + { + return null; + } + } + + CmapSubtable cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, + CmapTable.ENCODING_UNICODE_2_0_FULL); + if (cmap == null) + { + cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, + CmapTable.ENCODING_UNICODE_2_0_BMP); + } + if (cmap == null) + { + cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, + CmapTable.ENCODING_WIN_UNICODE_BMP); + } + if (cmap == null) + { + // Microsoft's "Recommendations for OpenType Fonts" says that "Symbol" encoding + // actually means "Unicode, non-standard character set" + cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, + CmapTable.ENCODING_WIN_SYMBOL); + } + if (cmap == null) + { + if (isStrict) + { + throw new IOException("The TrueType font does not contain a Unicode cmap"); + } + else + { + // fallback to the first cmap (may not be Unicode, so may produce poor results) + cmap = cmapTable.getCmaps()[0]; + } + } + return cmap; + } + /** * Returns the best Unicode from the font (the most general). The PDF spec says that "The means * by which this is accomplished are implementation-dependent." diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalHeaderTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalHeaderTable.java index 7849940cbcf..c112c4dc857 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalHeaderTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalHeaderTable.java @@ -71,7 +71,7 @@ public class VerticalHeaderTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.read32Fixed(); ascender = data.readSignedShort(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalMetricsTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalMetricsTable.java index 3cb404d6793..4965f105ae6 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalMetricsTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalMetricsTable.java @@ -54,7 +54,7 @@ public class VerticalMetricsTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { VerticalHeaderTable vHeader = ttf.getVerticalHeader(); if (vHeader == null) @@ -133,4 +133,13 @@ public int getAdvanceHeight(int gid) return advanceHeight[advanceHeight.length -1]; } } + + /** + * Returns array of advance heights. + * @return array of advance heights + */ + public int[] getAdvanceHeights() + { + return advanceHeight; + } } diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalOriginTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalOriginTable.java index f1e80fef736..004fd4c742e 100644 --- a/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalOriginTable.java +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/VerticalOriginTable.java @@ -59,7 +59,7 @@ public class VerticalOriginTable extends TTFTable * @throws IOException If there is an error reading the data. */ @Override - void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.read32Fixed(); defaultVertOriginY = data.readSignedShort(); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTable.java new file mode 100644 index 00000000000..dae384ed45f --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTable.java @@ -0,0 +1,1330 @@ +/* + * 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.advanced; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.fontbox.ttf.TTFTable; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

Base class for all advanced typographic glyph tables.

+ * + *

Adapted from the Apache FOP Project.

+ * + * @author Glenn Adams + */ +public class AdvancedTypographicTable extends TTFTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(AdvancedTypographicTable.class); + + /** substitution glyph table type */ + public static final int GLYPH_TABLE_TYPE_SUBSTITUTION = 1; + /** positioning glyph table type */ + public static final int GLYPH_TABLE_TYPE_POSITIONING = 2; + /** justification glyph table type */ + public static final int GLYPH_TABLE_TYPE_JUSTIFICATION = 3; + /** baseline glyph table type */ + public static final int GLYPH_TABLE_TYPE_BASELINE = 4; + /** definition glyph table type */ + public static final int GLYPH_TABLE_TYPE_DEFINITION = 5; + + // (optional) glyph definition table in table types other than glyph definition table + private AdvancedTypographicTable gdef; + + // map from lookup specs to lists of strings, each of which identifies a lookup table (consisting of one or more subtables) + private Map> lookups; + + // map from lookup identifiers to lookup tables + private Map lookupTables; + + // cache for lookups matching + private Map>> matchedLookups; + + // if true, then prevent further subtable addition + private boolean frozen; + + /** + * Instantiate glyph table with specified lookups. + * @param gdef glyph definition table that applies + * @param lookups map from lookup specs to lookup tables + */ + public AdvancedTypographicTable(TrueTypeFont ttf, AdvancedTypographicTable gdef, Map> lookups) { + super(ttf); + if ((gdef != null) && !(gdef instanceof GlyphDefinitionTable)) { + throw new AdvancedTypographicTableFormatException("bad glyph definition table"); + } else if (lookups == null) { + throw new AdvancedTypographicTableFormatException("lookups must be non-null map"); + } else { + this.gdef = gdef; + this.lookups = lookups; + this.lookupTables = new LinkedHashMap(); + this.matchedLookups = new HashMap>>(); + } + } + + protected void initialize(Map> lookups) { + this.lookups = lookups; + } + + /** + * Obtain glyph definition table. + * @return (possibly null) glyph definition table + */ + public GlyphDefinitionTable getGlyphDefinitions() { + return (GlyphDefinitionTable) gdef; + } + + /** + * Set glyph definition table + * @param gdef + */ + public void setGdef(GlyphDefinitionTable gdef) { + this.gdef = gdef; + } + + + /** + * Obtain list of all lookup specifications. + * @return (possibly empty) list of all lookup specifications + */ + public List getLookups() { + return matchLookupSpecs("*", "*", "*"); + } + + /** + * Obtain ordered list of all lookup tables, where order is by lookup identifier, which + * lexicographic ordering follows the lookup list order. + * @return (possibly empty) ordered list of all lookup tables + */ + public List getLookupTables() { + TreeSet lids = new TreeSet<>(lookupTables.keySet()); + List ltl = new ArrayList(lids.size()); + lids.forEach(lid -> ltl.add(lookupTables.get(lid))); + return ltl; + } + + /** + * Obtain lookup table by lookup id. This method is used by test code, and provides + * access to embedded lookups not normally accessed by {script, language, feature} lookup spec. + * @param lid lookup id + * @return table associated with lookup id or null if none + */ + public LookupTable getLookupTable(String lid) { + return lookupTables.get(lid); + } + + /** + * Add a subtable. + * @param subtable a (non-null) glyph subtable + */ + protected void addSubtable(GlyphSubtable subtable) { + // ensure table is not frozen + if (frozen) { + throw new IllegalStateException("glyph table is frozen, subtable addition prohibited"); + } + // set subtable's table reference to this table + subtable.setTable(this); + // add subtable to this table's subtable collection + String lid = subtable.getLookupId(); + if (lookupTables.containsKey(lid)) { + LookupTable lt = lookupTables.get(lid); + lt.addSubtable(subtable); + } else { + LookupTable lt = new LookupTable(lid, subtable); + lookupTables.put(lid, lt); + } + } + + /** + * Freeze subtables, i.e., do not allow further subtable addition, and + * create resulting cached state. + */ + protected void freezeSubtables() { + if (!frozen) { + lookupTables.values().forEach(lt -> lt.freezeSubtables(lookupTables)); + frozen = true; + } + } + + /** + * Match lookup specifications according to <script,language,feature> tuple, where + * '*' is a wildcard for a tuple component. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @return a (possibly empty) array of matching lookup specifications + */ + public List matchLookupSpecs(String script, String language, String feature) { + Set keys = lookups.keySet(); + List matches = new ArrayList<>(); + for (LookupSpec ls : keys) { + if (!"*".equals(script)) { + if (!ls.getScript().equals(script)) { + continue; + } + } + if (!"*".equals(language)) { + if (!ls.getLanguage().equals(language)) { + continue; + } + } + if (!"*".equals(feature)) { + if (!ls.getFeature().equals(feature)) { + continue; + } + } + matches.add(ls); + } + return matches; + } + + /** + * Match lookup specifications according to <script,language,feature> tuple, where + * '*' is a wildcard for a tuple component. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @return a (possibly empty) map from matching lookup specifications to lists of corresponding lookup tables + */ + public Map> matchLookups(String script, String language, String feature) { + LookupSpec lsm = new LookupSpec(script, language, feature, true, true); + Map> lm = matchedLookups.get(lsm); + if (lm == null) { + lm = new LinkedHashMap<>(); + List lsl = matchLookupSpecs(script, language, feature); + for (LookupSpec ls : lsl) { + lm.put(ls, findLookupTables(ls)); + } + matchedLookups.put(lsm, lm); + } + if (lm.isEmpty() && !OTFScript.isDefault(script) && !OTFScript.isWildCard(script)) { + return matchLookups(OTFScript.DEFAULT, OTFLanguage.DEFAULT, feature); + } else { + return lm; + } + } + + /** + * Obtain ordered list of glyph lookup tables that match a specific lookup specification. + * @param ls a (non-null) lookup specification + * @return a (possibly empty) ordered list of lookup tables whose corresponding lookup specifications match the specified lookup spec + */ + public List findLookupTables(LookupSpec ls) { + TreeSet lts = new TreeSet<>(); + List ids = lookups.get(ls); + transformConsume(ids, lookupTables::get, lts::add); + return new ArrayList<>(lts); + } + + /** + * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups, + * where the order of the array is in accordance to the order of the applicable lookup list. + * @param features array of feature identifiers to apply + * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features + * @return ordered array of assembled lookup table use specifications + */ + public UseSpec[] assembleLookups(String[] features, Map> lookups) { + TreeSet uss = new TreeSet<>(); + for (int i = 0, n = features.length; i < n; i++) { + String feature = features[i]; + for (Map.Entry> e : lookups.entrySet()) { + LookupSpec ls = e.getKey(); + if (ls.getFeature().equals(feature)) { + List ltl = e.getValue(); + if (ltl != null) { + ltl.forEach(lt -> uss.add(new UseSpec(lt, feature))); + } + } + } + } + return uss.toArray(new UseSpec [ uss.size() ]); + } + + /** + * Determine if table supports specific feature, i.e., supports at least one lookup. + * + * @param script to qualify feature lookup + * @param language to qualify feature lookup + * @param feature to test + * @return true if feature supported (has at least one lookup) + */ + public boolean hasFeature(String script, String language, String feature) { + UseSpec[] usa = assembleLookups(new String[] { feature }, matchLookups(script, language, feature)); + return usa.length > 0; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append("{"); + sb.append("lookups={"); + sb.append(lookups.toString()); + sb.append("},lookupTables={"); + sb.append(lookupTables.toString()); + sb.append("}}"); + return sb.toString(); + } + + /** + * Obtain glyph table type from name. + * @param name of table type to map to type value + * @return glyph table type (as an integer constant) or -1 + */ + public static int getTableTypeFromName(String name) { + int t; + String s = name.toLowerCase(Locale.ROOT); + if ("gsub".equals(s)) { + t = GLYPH_TABLE_TYPE_SUBSTITUTION; + } else if ("gpos".equals(s)) { + t = GLYPH_TABLE_TYPE_POSITIONING; + } else if ("jstf".equals(s)) { + t = GLYPH_TABLE_TYPE_JUSTIFICATION; + } else if ("base".equals(s)) { + t = GLYPH_TABLE_TYPE_BASELINE; + } else if ("gdef".equals(s)) { + t = GLYPH_TABLE_TYPE_DEFINITION; + } else { + t = -1; + } + return t; + } + + /** + * Resolve references to lookup tables in a collection of rules sets. + * @param rsa array of rule sets + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public static void resolveLookupReferences(RuleSet[] rsa, Map lookupTables) { + if ((rsa != null) && (lookupTables != null)) { + for (int i = 0, n = rsa.length; i < n; i++) { + RuleSet rs = rsa [ i ]; + if (rs != null) { + rs.resolveLookupReferences(lookupTables); + } + } + } + } + + /** + * A structure class encapsulating a lookup specification as a <script,language,feature> tuple. + */ + public static class LookupSpec implements Comparable{ + + private final String script; + private final String language; + private final String feature; + + /** + * Instantiate lookup spec. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + */ + public LookupSpec(String script, String language, String feature) { + this (script, language, feature, false, false); + } + + /** + * Instantiate lookup spec. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @param permitEmpty if true then permit empty script, language, or feature + * @param permitWildcard if true then permit wildcard script, language, or feature + */ + LookupSpec(String script, String language, String feature, boolean permitEmpty, boolean permitWildcard) { + if ((script == null) || (!permitEmpty && (script.length() == 0))) { + throw new AdvancedTypographicTableFormatException("script must be non-empty string"); + } else if ((language == null) || (!permitEmpty && (language.length() == 0))) { + throw new AdvancedTypographicTableFormatException("language must be non-empty string"); + } else if ((feature == null) || (!permitEmpty && (feature.length() == 0))) { + throw new AdvancedTypographicTableFormatException("feature must be non-empty string"); + } else if (!permitWildcard && script.equals("*")) { + throw new AdvancedTypographicTableFormatException("script must not be wildcard"); + } else if (!permitWildcard && language.equals("*")) { + throw new AdvancedTypographicTableFormatException("language must not be wildcard"); + } else if (!permitWildcard && feature.equals("*")) { + throw new AdvancedTypographicTableFormatException("feature must not be wildcard"); + } + this.script = script.trim(); + this.language = language.trim(); + this.feature = feature.trim(); + } + + /** @return script identifier */ + public String getScript() { + return script; + } + + /** @return language identifier */ + public String getLanguage() { + return language; + } + + /** @return feature identifier */ + public String getFeature() { + return feature; + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(script, language, feature); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (o instanceof LookupSpec) { + LookupSpec l = (LookupSpec) o; + if (!l.script.equals(script)) { + return false; + } else if (!l.language.equals(language)) { + return false; + } else { + return l.feature.equals(feature); + } + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int compareTo(LookupSpec ls) { + int d; + if ((d = script.compareTo(ls.script)) == 0) { + if ((d = language.compareTo(ls.language)) == 0) { + if ((d = feature.compareTo(ls.feature)) == 0) { + d = 0; + } + } + } + return d; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append("{"); + sb.append("<'" + script + "'"); + sb.append(",'" + language + "'"); + sb.append(",'" + feature + "'"); + sb.append(">}"); + return sb.toString(); + } + + } + + /** + * The LookupTable class comprising an identifier and an ordered list + * of glyph subtables, each of which employ the same lookup identifier. + */ + public static class LookupTable implements Comparable { + + private final String id; // lookup identifier + private final int idOrdinal; // parsed lookup identifier ordinal + private final List subtables; // list of subtables + private boolean doesSub; // performs substitutions + private boolean doesPos; // performs positioning + private boolean frozen; // if true, then don't permit further subtable additions + // frozen state + private GlyphSubtable[] subtablesArray; + private static final GlyphSubtable[] EMPTY_SUBTABLES_ARRAY = new GlyphSubtable[0]; + + /** + * Instantiate a LookupTable. + * @param id the lookup table's identifier + * @param subtable an initial subtable (or null) + */ + public LookupTable(String id, GlyphSubtable subtable) { + this (id, makeSingleton(subtable)); + } + + /** + * Instantiate a LookupTable. + * @param id the lookup table's identifier + * @param subtables a pre-poplated list of subtables or null + */ + public LookupTable(String id, List subtables) { + assert id != null; + assert id.length() != 0; + assert id.startsWith("lu"); + this.id = id; + this.idOrdinal = Integer.parseInt(id.substring(2)); + this.subtables = new ArrayList(); + if (subtables != null) { + subtables.forEach(this::addSubtable); + } + } + + /** @return the subtables as an array */ + public GlyphSubtable[] getSubtables() { + if (frozen) { + return (subtablesArray != null) ? subtablesArray : EMPTY_SUBTABLES_ARRAY; + } else { + if (doesSub) { + return subtables.toArray(new GlyphSubstitutionSubtable [ subtables.size() ]); + } else if (doesPos) { + return subtables.toArray(new GlyphPositioningSubtable [ subtables.size() ]); + } else { + return null; + } + } + } + + /** + * Add a subtable into this lookup table's collecion of subtables according to its + * natural order. + * @param subtable to add + * @return true if subtable was not already present, otherwise false + */ + public boolean addSubtable(GlyphSubtable subtable) { + boolean added; + // ensure table is not frozen + if (frozen) { + throw new IllegalStateException("glyph table is frozen, subtable addition prohibited"); + } + // validate subtable to ensure consistency with current subtables + validateSubtable(subtable); + + // insert subtable into ordered list + int insertIdx = -1; + for (int i = 0; i < subtables.size(); i++) { + GlyphSubtable st = subtables.get(i); + int compareResult = subtable.compareTo(st); + if (compareResult < 0) { + // insert before i + insertIdx = i; + break; + } else if (compareResult == 0) { + // duplicate entry is ignored + insertIdx = -2; + break; + } + } + + if (insertIdx >= 0) { + subtables.add(insertIdx, subtable); + added = true; + } else if (insertIdx == -1) { + // append at end of list + subtables.add(subtable); + added = true; + } else { + // duplicate + added = false; + } + + return added; + } + + private void validateSubtable(GlyphSubtable subtable) { + if (subtable == null) { + throw new AdvancedTypographicTableFormatException("subtable must be non-null"); + } + if (subtable instanceof GlyphSubstitutionSubtable) { + if (doesPos) { + throw new AdvancedTypographicTableFormatException("subtable must be positioning subtable, but is: " + subtable); + } else { + doesSub = true; + } + } + if (subtable instanceof GlyphPositioningSubtable) { + if (doesSub) { + throw new AdvancedTypographicTableFormatException("subtable must be substitution subtable, but is: " + subtable); + } else { + doesPos = true; + } + } + if (!subtables.isEmpty()) { + GlyphSubtable st = subtables.get(0); + if (!st.isCompatible(subtable)) { + if (log.isDebugEnabled()) { + log.debug("Adding " + st.getClass().getSimpleName() + " to existing: " + toClassString(subtables, "[", "]")); + } + // FIXME + //throw new AdvancedTypographicTableFormatException("subtable " + subtable + " is not compatible with subtable " + st); + } + } + } + + /** + * Freeze subtables, i.e., do not allow further subtable addition, and + * create resulting cached state. In addition, resolve any references to + * lookup tables that appear in this lookup table's subtables. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void freezeSubtables(Map lookupTables) { + if (!frozen) { + GlyphSubtable[] sta = getSubtables(); + resolveLookupReferences(sta, lookupTables); + this.subtablesArray = sta; + this.frozen = true; + } + } + + private void resolveLookupReferences(GlyphSubtable[] subtables, Map lookupTables) { + if (subtables != null) { + for (int i = 0, n = subtables.length; i < n; i++) { + GlyphSubtable st = subtables [ i ]; + if (st != null) { + st.resolveLookupReferences(lookupTables); + } + } + } + } + + /** + * Determine if this glyph table performs substitution. + * @return true if it performs substitution + */ + public boolean performsSubstitution() { + return doesSub; + } + + /** + * Perform substitution processing using this lookup table's subtables. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @param sct a script specific context tester (or null) + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) { + if (performsSubstitution()) { + return GlyphSubstitutionSubtable.substitute(gs, script, language, feature, (GlyphSubstitutionSubtable[]) subtablesArray, sct); + } else { + return gs; + } + } + + /** + * Perform substitution processing on an existing glyph substitution state object using this lookup table's subtables. + * @param ss a glyph substitution state object + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute(GlyphSubstitutionState ss, int sequenceIndex) { + if (performsSubstitution()) { + return GlyphSubstitutionSubtable.substitute(ss, (GlyphSubstitutionSubtable[]) subtablesArray, sequenceIndex); + } else { + return ss.getInput(); + } + } + + /** + * Determine if this glyph table performs positioning. + * @return true if it performs positioning + */ + public boolean performsPositioning() { + return doesPos; + } + + /** + * Perform positioning processing using this lookup table's subtables. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @param fontSize size in device units + * @param widths array of default advancements for each glyph in font + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @param sct a script specific context tester (or null) + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) { + if (performsPositioning()) { + return GlyphPositioningSubtable.position(gs, script, language, feature, fontSize, (GlyphPositioningSubtable[]) subtablesArray, widths, adjustments, sct); + } else { + return false; + } + } + + /** + * Perform positioning processing on an existing glyph positioning state object using this lookup table's subtables. + * @param ps a glyph positioning state object + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position(GlyphPositioningState ps, int sequenceIndex) { + if (performsPositioning()) { + return GlyphPositioningSubtable.position(ps, (GlyphPositioningSubtable[]) subtablesArray, sequenceIndex); + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return idOrdinal; + } + + /** + * {@inheritDoc} + * @return true if identifier of the specified lookup table is the same + * as the identifier of this lookup table + */ + @Override + public boolean equals(Object o) { + if (o instanceof LookupTable) { + LookupTable lt = (LookupTable) o; + return idOrdinal == lt.idOrdinal; + } else { + return false; + } + } + + /** + * {@inheritDoc} + * @return the result of comparing the identifier of the specified lookup table with + * the identifier of this lookup table; lookup table identifiers take the form + * "lu(DIGIT)+", with comparison based on numerical ordering of numbers expressed by + * (DIGIT)+. + */ + @Override + public int compareTo(LookupTable lt) { + int i = idOrdinal; + int j = lt.idOrdinal; + if (i < j) { + return -1; + } else if (i > j) { + return 1; + } else { + return 0; + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("id = " + id); + sb.append(", subtables = " + subtables); + sb.append(" }"); + return sb.toString(); + } + + private static List makeSingleton(GlyphSubtable subtable) { + if (subtable == null) { + return null; + } else { + return mutableSingleton(subtable); + } + } + + } + + /** + * The UseSpec class comprises a lookup table reference + * and the feature that selected the lookup table. + */ + public static class UseSpec implements Comparable { + + /** lookup table to apply */ + private final LookupTable lookupTable; + /** feature that caused selection of the lookup table */ + private final String feature; + + /** + * Construct a glyph lookup table use specification. + * @param lookupTable a glyph lookup table + * @param feature a feature that caused lookup table selection + */ + public UseSpec(LookupTable lookupTable, String feature) { + this.lookupTable = lookupTable; + this.feature = feature; + } + + /** @return the lookup table */ + public LookupTable getLookupTable() { + return lookupTable; + } + + /** @return the feature that selected this lookup table */ + public String getFeature() { + return feature; + } + + /** + * Perform substitution processing using this use specification's lookup table. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param sct a script specific context tester (or null) + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute(GlyphSequence gs, String script, String language, ScriptContextTester sct) { + return lookupTable.substitute(gs, script, language, feature, sct); + } + + /** + * Perform positioning processing using this use specification's lookup table. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param widths array of default advancements for each glyph in font + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @param sct a script specific context tester (or null) + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position(GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) { + return lookupTable.position(gs, script, language, feature, fontSize, widths, adjustments, sct); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return lookupTable.hashCode(); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (o instanceof UseSpec) { + UseSpec u = (UseSpec) o; + return lookupTable.equals(u.lookupTable); + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int compareTo(UseSpec u) { + return lookupTable.compareTo(u.lookupTable); + } + + } + + /** + * The RuleLookup class implements a rule lookup record, comprising + * a glyph sequence index and a lookup table index (in an applicable lookup list). + */ + public static class RuleLookup { + + private final int sequenceIndex; // index into input glyph sequence + private final int lookupIndex; // lookup list index + private LookupTable lookup; // resolved lookup table + + /** + * Instantiate a RuleLookup. + * @param sequenceIndex the index into the input sequence + * @param lookupIndex the lookup table index + */ + public RuleLookup(int sequenceIndex, int lookupIndex) { + this.sequenceIndex = sequenceIndex; + this.lookupIndex = lookupIndex; + this.lookup = null; + } + + /** @return the sequence index */ + public int getSequenceIndex() { + return sequenceIndex; + } + + /** @return the lookup index */ + public int getLookupIndex() { + return lookupIndex; + } + + /** @return the lookup table */ + public LookupTable getLookup() { + return lookup; + } + + /** + * Resolve references to lookup tables. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences(Map lookupTables) { + if (lookupTables != null) { + String lid = "lu" + Integer.toString(lookupIndex); + LookupTable lt = lookupTables.get(lid); + if (lt != null) { + this.lookup = lt; + } else { + log.warn("unable to resolve glyph lookup table reference '" + lid + "' amongst lookup tables: " + lookupTables.values()); + } + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "{ sequenceIndex = " + sequenceIndex + ", lookupIndex = " + lookupIndex + " }"; + } + + } + + /** + * The Rule class implements an array of rule lookup records. + */ + public abstract static class Rule { + + private final RuleLookup[] lookups; // rule lookups + private final int inputSequenceLength; // input sequence length + + /** + * Instantiate a Rule. + * @param lookups the rule's lookups + * @param inputSequenceLength the number of glyphs in the input sequence for this rule + */ + protected Rule(RuleLookup[] lookups, int inputSequenceLength) { + assert lookups != null; + this.lookups = lookups; + this.inputSequenceLength = inputSequenceLength; + } + + /** @return the lookups */ + public RuleLookup[] getLookups() { + return lookups; + } + + /** @return the input sequence length */ + public int getInputSequenceLength() { + return inputSequenceLength; + } + + /** + * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences(Map lookupTables) { + if (lookups != null) { + for (int i = 0; i < lookups.length; i++) { + if (lookups[i] != null) { + lookups[i].resolveLookupReferences(lookupTables); + } + } + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "{ lookups = " + Arrays.toString(lookups) + ", inputSequenceLength = " + inputSequenceLength + " }"; + } + + } + + /** + * The GlyphSequenceRule class implements a subclass of Rule + * that supports matching on a specific glyph sequence. + */ + public static class GlyphSequenceRule extends Rule { + + private final int[] glyphs; // glyphs + + /** + * Instantiate a GlyphSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param glyphs the rule's glyph sequence to match, starting with second glyph in sequence + */ + public GlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs) { + super(lookups, inputSequenceLength); + assert glyphs != null; + this.glyphs = glyphs; + } + + /** + * Obtain glyphs. N.B. that this array starts with the second + * glyph of the input sequence. + * @return the glyphs + */ + public int[] getGlyphs() { + return glyphs; + } + + /** + * Obtain glyphs augmented by specified first glyph entry. + * @param firstGlyph to fill in first glyph entry + * @return the glyphs augmented by first glyph + */ + public int[] getGlyphs(int firstGlyph) { + int[] ga = new int [ glyphs.length + 1 ]; + ga [ 0 ] = firstGlyph; + System.arraycopy(glyphs, 0, ga, 1, glyphs.length); + return ga; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("lookups = " + Arrays.toString(getLookups())); + sb.append(", glyphs = " + Arrays.toString(glyphs)); + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The ClassSequenceRule class implements a subclass of Rule + * that supports matching on a specific glyph class sequence. + */ + public static class ClassSequenceRule extends Rule { + + private final int[] classes; // glyph classes + + /** + * Instantiate a ClassSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param classes the rule's glyph class sequence to match, starting with second glyph in sequence + */ + public ClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes) { + super(lookups, inputSequenceLength); + assert classes != null; + this.classes = classes; + } + + /** + * Obtain glyph classes. N.B. that this array starts with the class of the second + * glyph of the input sequence. + * @return the classes + */ + public int[] getClasses() { + return classes; + } + + /** + * Obtain glyph classes augmented by specified first class entry. + * @param firstClass to fill in first class entry + * @return the classes augmented by first class + */ + public int[] getClasses(int firstClass) { + int[] ca = new int [ classes.length + 1 ]; + ca [ 0 ] = firstClass; + System.arraycopy(classes, 0, ca, 1, classes.length); + return ca; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("lookups = " + Arrays.toString(getLookups())); + sb.append(", classes = " + Arrays.toString(classes)); + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The CoverageSequenceRule class implements a subclass of Rule + * that supports matching on a specific glyph coverage sequence. + */ + public static class CoverageSequenceRule extends Rule { + + private final GlyphCoverageTable[] coverages; // glyph coverages + + /** + * Instantiate a ClassSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param coverages the rule's glyph coverage sequence to match, starting with first glyph in sequence + */ + public CoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages) { + super(lookups, inputSequenceLength); + assert coverages != null; + this.coverages = coverages; + } + + /** @return the coverages */ + public GlyphCoverageTable[] getCoverages() { + return coverages; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("lookups = " + Arrays.toString(getLookups())); + sb.append(", coverages = " + Arrays.toString(coverages)); + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The ChainedGlyphSequenceRule class implements a subclass of GlyphSequenceRule + * that supports matching on a specific glyph sequence in a specific chained contextual. + */ + public static class ChainedGlyphSequenceRule extends GlyphSequenceRule { + + private final int[] backtrackGlyphs; // backtrack glyphs + private final int[] lookaheadGlyphs; // lookahead glyphs + + /** + * Instantiate a ChainedGlyphSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param glyphs the rule's input glyph sequence to match, starting with second glyph in sequence + * @param backtrackGlyphs the rule's backtrack glyph sequence to match, starting with first glyph in sequence + * @param lookaheadGlyphs the rule's lookahead glyph sequence to match, starting with first glyph in sequence + */ + public ChainedGlyphSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs, int[] lookaheadGlyphs) { + super(lookups, inputSequenceLength, glyphs); + assert backtrackGlyphs != null; + assert lookaheadGlyphs != null; + this.backtrackGlyphs = backtrackGlyphs; + this.lookaheadGlyphs = lookaheadGlyphs; + } + + /** @return the backtrack glyphs */ + public int[] getBacktrackGlyphs() { + return backtrackGlyphs; + } + + /** @return the lookahead glyphs */ + public int[] getLookaheadGlyphs() { + return lookaheadGlyphs; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("lookups = " + Arrays.toString(getLookups())); + sb.append(", glyphs = " + Arrays.toString(getGlyphs())); + sb.append(", backtrackGlyphs = " + Arrays.toString(backtrackGlyphs)); + sb.append(", lookaheadGlyphs = " + Arrays.toString(lookaheadGlyphs)); + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The ChainedClassSequenceRule class implements a subclass of ClassSequenceRule + * that supports matching on a specific glyph class sequence in a specific chained contextual. + */ + public static class ChainedClassSequenceRule extends ClassSequenceRule { + + private final int[] backtrackClasses; // backtrack classes + private final int[] lookaheadClasses; // lookahead classes + + /** + * Instantiate a ChainedClassSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param classes the rule's input glyph class sequence to match, starting with second glyph in sequence + * @param backtrackClasses the rule's backtrack glyph class sequence to match, starting with first glyph in sequence + * @param lookaheadClasses the rule's lookahead glyph class sequence to match, starting with first glyph in sequence + */ + public ChainedClassSequenceRule(RuleLookup[] lookups, int inputSequenceLength, int[] classes, int[] backtrackClasses, int[] lookaheadClasses) { + super(lookups, inputSequenceLength, classes); + assert backtrackClasses != null; + assert lookaheadClasses != null; + this.backtrackClasses = backtrackClasses; + this.lookaheadClasses = lookaheadClasses; + } + + /** @return the backtrack classes */ + public int[] getBacktrackClasses() { + return backtrackClasses; + } + + /** @return the lookahead classes */ + public int[] getLookaheadClasses() { + return lookaheadClasses; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("lookups = " + Arrays.toString(getLookups())); + sb.append(", classes = " + Arrays.toString(getClasses())); + sb.append(", backtrackClasses = " + Arrays.toString(backtrackClasses)); + sb.append(", lookaheadClasses = " + Arrays.toString(lookaheadClasses)); + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The ChainedCoverageSequenceRule class implements a subclass of CoverageSequenceRule + * that supports matching on a specific glyph class sequence in a specific chained contextual. + */ + public static class ChainedCoverageSequenceRule extends CoverageSequenceRule { + + private final GlyphCoverageTable[] backtrackCoverages; // backtrack coverages + private final GlyphCoverageTable[] lookaheadCoverages; // lookahead coverages + + /** + * Instantiate a ChainedCoverageSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param coverages the rule's input glyph class sequence to match, starting with first glyph in sequence + * @param backtrackCoverages the rule's backtrack glyph class sequence to match, starting with first glyph in sequence + * @param lookaheadCoverages the rule's lookahead glyph class sequence to match, starting with first glyph in sequence + */ + public ChainedCoverageSequenceRule(RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages, GlyphCoverageTable[] backtrackCoverages, GlyphCoverageTable[] lookaheadCoverages) { + super(lookups, inputSequenceLength, coverages); + assert backtrackCoverages != null; + assert lookaheadCoverages != null; + this.backtrackCoverages = backtrackCoverages; + this.lookaheadCoverages = lookaheadCoverages; + } + + /** @return the backtrack coverages */ + public GlyphCoverageTable[] getBacktrackCoverages() { + return backtrackCoverages; + } + + /** @return the lookahead coverages */ + public GlyphCoverageTable[] getLookaheadCoverages() { + return lookaheadCoverages; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ "); + sb.append("lookups = " + Arrays.toString(getLookups())); + sb.append(", coverages = " + Arrays.toString(getCoverages())); + sb.append(", backtrackCoverages = " + Arrays.toString(backtrackCoverages)); + sb.append(", lookaheadCoverages = " + Arrays.toString(lookaheadCoverages)); + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The RuleSet class implements a collection of rules, which + * may or may not be the same rule type. + */ + public static class RuleSet { + + private final Rule[] rules; // set of rules + + /** + * Instantiate a Rule Set. + * @param rules the rules + * @throws AdvancedTypographicTableFormatException if rules or some element of rules is null + */ + public RuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException { + // enforce rules array instance + if (rules == null) { + throw new AdvancedTypographicTableFormatException("rules[] is null"); + } + this.rules = rules; + } + + /** @return the rules */ + public Rule[] getRules() { + return rules; + } + + /** + * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences(Map lookupTables) { + if (rules != null) { + for (int i = 0; i < rules.length; i++) { + if (rules[i] != null) { + rules[i].resolveLookupReferences(lookupTables); + } + } + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "{ rules = " + Arrays.toString(rules) + " }"; + } + + } + + /** + * The HomogenousRuleSet class implements a collection of rules, which + * must be the same rule type (i.e., same concrete rule class) or null. + */ + public static class HomogeneousRuleSet extends RuleSet { + + /** + * Instantiate a Homogeneous Rule Set. + * @param rules the rules + * @throws AdvancedTypographicTableFormatException if some rule[i] is not an instance of rule[0] + */ + public HomogeneousRuleSet(Rule[] rules) throws AdvancedTypographicTableFormatException { + super(rules); + // find first non-null rule + Rule r0 = null; + for (int i = 1, n = rules.length; (r0 == null) && (i < n); i++) { + if (rules[i] != null) { + r0 = rules[i]; + } + } + // enforce rule instance homogeneity + if (r0 != null) { + Class c = r0.getClass(); + for (int i = 1, n = rules.length; i < n; i++) { + Rule r = rules[i]; + if ((r != null) && !c.isInstance(r)) { + throw new AdvancedTypographicTableFormatException("rules[" + i + "] is not an instance of " + c.getName()); + } + } + } + + } + + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTableFormatException.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTableFormatException.java new file mode 100644 index 00000000000..6d7c05ddde4 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTableFormatException.java @@ -0,0 +1,48 @@ +/* + * 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.advanced; + +/** + *

Exception thrown when attempting to decode a truetype font file and a format + * constraint is violated.

+ * + * @author Glenn Adams + */ +public class AdvancedTypographicTableFormatException extends RuntimeException { + /** + * Instantiate ATT format exception. + */ + public AdvancedTypographicTableFormatException() { + super(); + } + /** + * Instantiate ATT format exception. + * @param message a message string + */ + public AdvancedTypographicTableFormatException(String message) { + super(message); + } + /** + * Instantiate ATT format exception. + * @param message a message string + * @param cause a Throwable that caused this exception + */ + public AdvancedTypographicTableFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTableReader.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTableReader.java new file mode 100644 index 00000000000..8b416b942d8 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/AdvancedTypographicTableReader.java @@ -0,0 +1,3770 @@ +/* + * 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.advanced; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.TTFDataStream; +import org.apache.fontbox.ttf.TTFTable; +import org.apache.fontbox.ttf.advanced.AdvancedTypographicTable.LookupSpec; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.*; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

OpenType Font (OTF) advanced typographic table reader.

+ * + * @author Glenn Adams + */ +public final class AdvancedTypographicTableReader { + + // logging state + private static Log log = LogFactory.getLog(AdvancedTypographicTableReader.class); + // instance state + private AdvancedOpenTypeFont otf; // enclosing font instance + private TTFTable table; // table being constructed + private TTFDataStream data; // data stream + // transient parsing state + private transient Map seScripts; // script-tag => Object[3] : { default-language-tag, List(language-tag), seLanguages } + private transient Map seLanguages; // language-tag => Object[2] : { "f", List("f") + private transient Map seFeatures; // "f" => Object[2] : { feature-tag, List("lu") } + private transient GlyphMappingTable seMapping; // subtable entry mappings + private transient List seEntries; // subtable entry entries + private transient List seSubtables; // subtable entry subtables + + private static class SubtableEntryScript { + final String defaultLanguageTag; + final List languageTags; + final Map languages; + + SubtableEntryScript(String defaultLanguageTag, List languageTags, Map languages) { + this.defaultLanguageTag = defaultLanguageTag; + this.languageTags = languageTags; + this.languages = languages; + } + } + + private static class SubtableEntryLanguage { + final String requiredFeatureId; + final List featureIds; + + SubtableEntryLanguage(String rfi, List fl) { + this.requiredFeatureId = rfi; + this.featureIds = fl; + } + } + + private static class SubtableEntryFeature { + final String featureTag; + final List lookupIndexes; + + SubtableEntryFeature(String featureTag, List lookupIndexes) { + this.featureTag = featureTag; + this.lookupIndexes = lookupIndexes; + } + } + + private static class SubtableState { + final int tableType; + final int lookupType; + final int lookupFlags; + final int lookupSequence; + final int subtableSequence; + final int subtableFormat; + final List entries; + final GlyphMappingTable map; + + SubtableState( + int tableType, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, int subtableFormat, GlyphMappingTable map, List entries) { + this.tableType = tableType; + this.lookupType = lookupType; + this.lookupFlags = lookupFlags; + this.lookupSequence = lookupSequence; + this.subtableSequence = subtableSequence; + this.subtableFormat = subtableFormat; + this.entries = entries; + this.map = map; + } + } + + /** + * Construct an AdvancedTypographicTableReader instance. + * @param otf enclosing font file (must be non-null) + * @param table table instance being constructed, will be one of Glyph{Definition,Substitution,Positioning}Table + * @param data font file reader (must be non-null) + */ + public AdvancedTypographicTableReader(AdvancedOpenTypeFont otf, TTFTable table, TTFDataStream data) { + assert otf != null; + assert table != null; + assert data != null; + this.otf = otf; + this.table = table; + this.data = data; + } + + /** + * Read advanced typographic table. + * @throws AdvancedTypographicTableFormatException if ATT table has invalid format + */ + public void read() throws AdvancedTypographicTableFormatException { + try { + if (table instanceof GlyphDefinitionTable) + readGDEF(); + else if (table instanceof GlyphSubstitutionTable) + readGSUB(); + else if (table instanceof GlyphPositioningTable) + readGPOS(); + } catch (AdvancedTypographicTableFormatException e) { + resetATStateAll(); + throw e; + } catch (IOException e) { + resetATStateAll(); + throw new AdvancedTypographicTableFormatException(e.getMessage(), e); + } finally { + resetATState(); + } + } + + private void readLangSysTable(String tableTag, long langSysTable, String langSysTag) + throws IOException { + data.seek(langSysTable); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys table: " + langSysTag); + } + // read lookup order (reorder) table offset + int lo = data.readUnsignedShort(); + // read required feature index + int rf = data.readUnsignedShort(); + String rfi; + if (rf != 65535) { + rfi = "f" + rf; + } else { + rfi = null; + } + // read (non-required) feature count + int nf = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys table reorder table: " + lo); + log.debug(tableTag + " lang sys table required feature index: " + rf); + log.debug(tableTag + " lang sys table non-required feature count: " + nf); + } + // read (non-required) feature indices + int[] fia = new int[nf]; + List fl = new java.util.ArrayList<>(); + for (int i = 0; i < nf; i++) { + int fi = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys table non-required feature index: " + fi); + } + fia[i] = fi; + fl.add("f" + fi); + } + if (seLanguages == null) { + seLanguages = new java.util.LinkedHashMap<>(); + } + seLanguages.put(langSysTag, new SubtableEntryLanguage(rfi, fl)); + } + + private static final String defaultTag = "dflt"; + + private void readScriptTable(String tableTag, long scriptTable, String scriptTag) throws IOException { + data.seek(scriptTable); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script table: " + scriptTag); + } + // read default language system table offset + int dl = data.readUnsignedShort(); + String dt = defaultTag; + if (dl > 0) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " default lang sys tag: " + dt); + log.debug(tableTag + " default lang sys table offset: " + dl); + } + } + // read language system record count + int nl = data.readUnsignedShort(); + List ll = new java.util.ArrayList<>(); + if (nl > 0) { + String[] lta = new String[nl]; + int[] loa = new int[nl]; + // read language system records + for (int i = 0, n = nl; i < n; i++) { + String lt = data.readTag(); + int lo = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys tag: " + lt); + log.debug(tableTag + " lang sys table offset: " + lo); + } + lta[i] = lt; + loa[i] = lo; + if (dl == lo) { + dl = 0; + dt = lt; + } + ll.add(lt); + } + // read non-default language system tables + for (int i = 0, n = nl; i < n; i++) { + readLangSysTable(tableTag, scriptTable + loa [ i ], lta [ i ]); + } + } + // read default language system table (if specified) + if (dl > 0) { + readLangSysTable(tableTag, scriptTable + dl, dt); + } else if (dt != null) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys default: " + dt); + } + } + seScripts.put(scriptTag, new SubtableEntryScript(dt, ll, seLanguages)); + seLanguages = null; + } + + private void readScriptList(String tableTag, long scriptList) throws IOException { + data.seek(scriptList); + // read script record count + int ns = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script list record count: " + ns); + } + if (ns > 0) { + String[] sta = new String[ns]; + int[] soa = new int[ns]; + // read script records + for (int i = 0, n = ns; i < n; i++) { + String st = data.readTag(); + int so = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script tag: " + st); + log.debug(tableTag + " script table offset: " + so); + } + sta[i] = st; + soa[i] = so; + } + // read script tables + for (int i = 0, n = ns; i < n; i++) { + seLanguages = null; + readScriptTable(tableTag, scriptList + soa [ i ], sta [ i ]); + } + } + } + + private void readFeatureTable(String tableTag, long featureTable, String featureTag, int featureIndex) throws IOException { + data.seek(featureTable); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature table: " + featureTag); + } + // read feature params offset + int po = data.readUnsignedShort(); + // read lookup list indices count + int nl = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature table parameters offset: " + po); + log.debug(tableTag + " feature table lookup list index count: " + nl); + } + // read lookup table indices + int[] lia = new int[nl]; + List lul = new java.util.ArrayList<>(); + for (int i = 0; i < nl; i++) { + int li = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature table lookup index: " + li); + } + lia[i] = li; + lul.add("lu" + li); + } + + seFeatures.put("f" + featureIndex, new SubtableEntryFeature(featureTag, lul)); + } + + private void readFeatureList(String tableTag, long featureList) throws IOException { + data.seek(featureList); + // read feature record count + int nf = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature list record count: " + nf); + } + if (nf > 0) { + String[] fta = new String[nf]; + int[] foa = new int[nf]; + // read feature records + for (int i = 0, n = nf; i < n; i++) { + String ft = data.readTag(); + int fo = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature tag: " + ft); + log.debug(tableTag + " feature table offset: " + fo); + } + fta[i] = ft; + foa[i] = fo; + } + // read feature tables + for (int i = 0, n = nf; i < n; i++) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature index: " + i); + } + readFeatureTable(tableTag, featureList + foa [ i ], fta [ i ], i); + } + } + } + + static final class GDEFLookupType { + static final int GLYPH_CLASS = 1; + static final int ATTACHMENT_POINT = 2; + static final int LIGATURE_CARET = 3; + static final int MARK_ATTACHMENT = 4; + private GDEFLookupType() { + } + public static int getSubtableType(int lt) { + int st; + switch (lt) { + case GDEFLookupType.GLYPH_CLASS: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_GLYPH_CLASS; + break; + case GDEFLookupType.ATTACHMENT_POINT: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + break; + case GDEFLookupType.LIGATURE_CARET: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_LIGATURE_CARET; + break; + case GDEFLookupType.MARK_ATTACHMENT: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + break; + default: + st = -1; + break; + } + return st; + } + public static String toString(int type) { + String s; + switch (type) { + case GLYPH_CLASS: + s = "GlyphClass"; + break; + case ATTACHMENT_POINT: + s = "AttachmentPoint"; + break; + case LIGATURE_CARET: + s = "LigatureCaret"; + break; + case MARK_ATTACHMENT: + s = "MarkAttachment"; + break; + default: + s = "?"; + break; + } + return s; + } + } + + static final class GSUBLookupType { + static final int SINGLE = 1; + static final int MULTIPLE = 2; + static final int ALTERNATE = 3; + static final int LIGATURE = 4; + static final int CONTEXTUAL = 5; + static final int CHAINED_CONTEXTUAL = 6; + static final int EXTENSION = 7; + static final int REVERSE_CHAINED_SINGLE = 8; + private GSUBLookupType() { + } + public static int getSubtableType(int lt) { + int st; + switch (lt) { + case GSUBLookupType.SINGLE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE; + break; + case GSUBLookupType.MULTIPLE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE; + break; + case GSUBLookupType.ALTERNATE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE; + break; + case GSUBLookupType.LIGATURE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE; + break; + case GSUBLookupType.CONTEXTUAL: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXTUAL; + break; + case GSUBLookupType.CHAINED_CONTEXTUAL: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + break; + case GSUBLookupType.EXTENSION: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION; + break; + case GSUBLookupType.REVERSE_CHAINED_SINGLE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; + break; + default: + st = -1; + break; + } + return st; + } + public static String toString(int type) { + String s; + switch (type) { + case SINGLE: + s = "Single"; + break; + case MULTIPLE: + s = "Multiple"; + break; + case ALTERNATE: + s = "Alternate"; + break; + case LIGATURE: + s = "Ligature"; + break; + case CONTEXTUAL: + s = "Contextual"; + break; + case CHAINED_CONTEXTUAL: + s = "ChainedContextual"; + break; + case EXTENSION: + s = "Extension"; + break; + case REVERSE_CHAINED_SINGLE: + s = "ReverseChainedSingle"; + break; + default: + s = "?"; + break; + } + return s; + } + } + + static final class GPOSLookupType { + static final int SINGLE = 1; + static final int PAIR = 2; + static final int CURSIVE = 3; + static final int MARK_TO_BASE = 4; + static final int MARK_TO_LIGATURE = 5; + static final int MARK_TO_MARK = 6; + static final int CONTEXTUAL = 7; + static final int CHAINED_CONTEXTUAL = 8; + static final int EXTENSION = 9; + private GPOSLookupType() { + } + public static String toString(int type) { + String s; + switch (type) { + case SINGLE: + s = "Single"; + break; + case PAIR: + s = "Pair"; + break; + case CURSIVE: + s = "Cursive"; + break; + case MARK_TO_BASE: + s = "MarkToBase"; + break; + case MARK_TO_LIGATURE: + s = "MarkToLigature"; + break; + case MARK_TO_MARK: + s = "MarkToMark"; + break; + case CONTEXTUAL: + s = "Contextual"; + break; + case CHAINED_CONTEXTUAL: + s = "ChainedContextual"; + break; + case EXTENSION: + s = "Extension"; + break; + default: + s = "?"; + break; + } + return s; + } + } + + static final class LookupFlag { + static final int RIGHT_TO_LEFT = 0x0001; + static final int IGNORE_BASE_GLYPHS = 0x0002; + static final int IGNORE_LIGATURE = 0x0004; + static final int IGNORE_MARKS = 0x0008; + static final int USE_MARK_FILTERING_SET = 0x0010; + static final int MARK_ATTACHMENT_TYPE = 0xFF00; + private LookupFlag() { + } + public static String toString(int flags) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if ((flags & RIGHT_TO_LEFT) != 0) { + if (first) { + first = false; + } else { + sb.append('|'); + } + sb.append("RightToLeft"); + } + if ((flags & IGNORE_BASE_GLYPHS) != 0) { + if (first) { + first = false; + } else { + sb.append('|'); + } + sb.append("IgnoreBaseGlyphs"); + } + if ((flags & IGNORE_LIGATURE) != 0) { + if (first) { + first = false; + } else { + sb.append('|'); + } + sb.append("IgnoreLigature"); + } + if ((flags & IGNORE_MARKS) != 0) { + if (first) { + first = false; + } else { + sb.append('|'); + } + sb.append("IgnoreMarks"); + } + if ((flags & USE_MARK_FILTERING_SET) != 0) { + if (first) { + first = false; + } else { + sb.append('|'); + } + sb.append("UseMarkFilteringSet"); + } + if (sb.length() == 0) { + sb.append('-'); + } + return sb.toString(); + } + } + + private GlyphCoverageTable readCoverageTableFormat1(String label, long tableOffset, int coverageFormat) throws IOException { + List entries = new ArrayList<>(); + data.seek(tableOffset); + // skip over format (already known) + data.skip(2); + // read glyph count + int ng = data.readUnsignedShort(); + int[] ga = new int[ng]; + for (int i = 0, n = ng; i < n; i++) { + int g = data.readUnsignedShort(); + ga[i] = g; + entries.add(SEInteger.valueOf(g)); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " glyphs: " + toString(ga)); + } + return GlyphCoverageTable.createCoverageTable(entries); + } + + private GlyphCoverageTable readCoverageTableFormat2(String label, long tableOffset, int coverageFormat) throws IOException { + List entries = new ArrayList<>(); + data.seek(tableOffset); + // skip over format (already known) + data.skip(2); + // read range record count + int nr = data.readUnsignedShort(); + for (int i = 0, n = nr; i < n; i++) { + // read range start + int s = data.readUnsignedShort(); + // read range end + int e = data.readUnsignedShort(); + // read range coverage (mapping) index + int m = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m); + } + entries.add(new SEMappingRange(new GlyphCoverageTable.MappingRange(s, e, m))); + } + return GlyphCoverageTable.createCoverageTable(entries); + } + + private GlyphCoverageTable readCoverageTable(String label, long tableOffset) throws IOException { + GlyphCoverageTable gct; + long cp = data.getCurrentPosition(); + data.seek(tableOffset); + // read coverage table format + int cf = data.readUnsignedShort(); + if (cf == 1) { + gct = readCoverageTableFormat1(label, tableOffset, cf); + } else if (cf == 2) { + gct = readCoverageTableFormat2(label, tableOffset, cf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported coverage table format: " + cf); + } + data.seek(cp); + return gct; + } + + private GlyphClassTable readClassDefTableFormat1(String label, long tableOffset, int classFormat) throws IOException { + List entries = new ArrayList<>(); + data.seek(tableOffset); + // skip over format (already known) + data.skip(2); + // read start glyph + int sg = data.readUnsignedShort(); + entries.add(SEInteger.valueOf(sg)); + // read glyph count + int ng = data.readUnsignedShort(); + // read glyph classes + int[] ca = new int[ng]; + for (int i = 0, n = ng; i < n; i++) { + int gc = data.readUnsignedShort(); + ca[i] = gc; + entries.add(SEInteger.valueOf(gc)); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " glyph classes: " + toString(ca)); + } + return GlyphClassTable.createClassTable(entries); + } + + private GlyphClassTable readClassDefTableFormat2(String label, long tableOffset, int classFormat) throws IOException { + List entries = new ArrayList<>(); + data.seek(tableOffset); + // skip over format (already known) + data.skip(2); + // read range record count + int nr = data.readUnsignedShort(); + for (int i = 0, n = nr; i < n; i++) { + // read range start + int s = data.readUnsignedShort(); + // read range end + int e = data.readUnsignedShort(); + // read range glyph class (mapping) index + int m = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m); + } + entries.add(new SEMappingRange(new GlyphClassTable.MappingRange(s, e, m))); + } + return GlyphClassTable.createClassTable(entries); + } + + private GlyphClassTable readClassDefTable(String label, long tableOffset) throws IOException { + GlyphClassTable gct; + long cp = data.getCurrentPosition(); + data.seek(tableOffset); + // read class table format + int cf = data.readUnsignedShort(); + if (cf == 1) { + gct = readClassDefTableFormat1(label, tableOffset, cf); + } else if (cf == 2) { + gct = readClassDefTableFormat2(label, tableOffset, cf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported class definition table format: " + cf); + } + data.seek(cp); + return gct; + } + + private void readSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read delta glyph + int dg = data.readSignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single substitution subtable format: " + subtableFormat + " (delta)"); + log.debug(tableTag + " single substitution coverage table offset: " + co); + log.debug(tableTag + " single substitution delta: " + dg); + } + // read coverage table + seMapping = readCoverageTable(tableTag + " single substitution coverage", subtableOffset + co); + seEntries.add(SEInteger.valueOf(dg)); + } + + private void readSingleSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read glyph count + int ng = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single substitution subtable format: " + subtableFormat + " (mapped)"); + log.debug(tableTag + " single substitution coverage table offset: " + co); + log.debug(tableTag + " single substitution glyph count: " + ng); + } + // read coverage table + seMapping = readCoverageTable(tableTag + " single substitution coverage", subtableOffset + co); + // read glyph substitutions + int[] gsa = new int[ng]; + for (int i = 0, n = ng; i < n; i++) { + int gs = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " single substitution glyph[" + i + "]: " + gs); + } + gsa[i] = gs; + seEntries.add(SEInteger.valueOf(gs)); + } + } + + private int readSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readSingleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readSingleSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported single substitution subtable format: " + sf); + } + return sf; + } + + private void readMultipleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read sequence count + int ns = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " multiple substitution subtable format: " + subtableFormat + " (mapped)"); + log.debug(tableTag + " multiple substitution coverage table offset: " + co); + log.debug(tableTag + " multiple substitution sequence count: " + ns); + } + // read coverage table + seMapping = readCoverageTable(tableTag + " multiple substitution coverage", subtableOffset + co); + // read sequence table offsets + int[] soa = new int[ns]; + for (int i = 0, n = ns; i < n; i++) { + soa[i] = data.readUnsignedShort(); + } + // read sequence tables + int[][] gsa = new int [ ns ] []; + for (int i = 0, n = ns; i < n; i++) { + int so = soa[i]; + int[] ga; + if (so > 0) { + data.seek(subtableOffset + so); + // read glyph count + int ng = data.readUnsignedShort(); + ga = new int[ng]; + for (int j = 0; j < ng; j++) { + ga[j] = data.readUnsignedShort(); + } + } else { + ga = null; + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " multiple substitution sequence[" + i + "]: " + toString(ga)); + } + gsa [ i ] = ga; + } + seEntries.add(new SESequenceList(gsa)); + } + + private int readMultipleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readMultipleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported multiple substitution subtable format: " + sf); + } + return sf; + } + + private void readAlternateSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read alternate set count + int ns = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " alternate substitution subtable format: " + subtableFormat + " (mapped)"); + log.debug(tableTag + " alternate substitution coverage table offset: " + co); + log.debug(tableTag + " alternate substitution alternate set count: " + ns); + } + // read coverage table + seMapping = readCoverageTable(tableTag + " alternate substitution coverage", subtableOffset + co); + // read alternate set table offsets + int[] soa = new int[ns]; + for (int i = 0, n = ns; i < n; i++) { + soa[i] = data.readUnsignedShort(); + } + // read alternate set tables + for (int i = 0, n = ns; i < n; i++) { + int so = soa[i]; + data.seek(subtableOffset + so); + // read glyph count + int ng = data.readUnsignedShort(); + int[] ga = new int[ng]; + for (int j = 0; j < ng; j++) { + int gs = data.readUnsignedShort(); + ga[j] = gs; + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " alternate substitution alternate set[" + i + "]: " + toString(ga)); + } + seEntries.add(new SEIntList(ga)); + } + } + + private int readAlternateSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readAlternateSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported alternate substitution subtable format: " + sf); + } + return sf; + } + + private void readLigatureSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read ligature set count + int ns = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " ligature substitution subtable format: " + subtableFormat + " (mapped)"); + log.debug(tableTag + " ligature substitution coverage table offset: " + co); + log.debug(tableTag + " ligature substitution ligature set count: " + ns); + } + // read coverage table + seMapping = readCoverageTable(tableTag + " ligature substitution coverage", subtableOffset + co); + // read ligature set table offsets + int[] soa = new int[ns]; + for (int i = 0, n = ns; i < n; i++) { + soa[i] = data.readUnsignedShort(); + } + // read ligature set tables + for (int i = 0, n = ns; i < n; i++) { + int so = soa[i]; + data.seek(subtableOffset + so); + // read ligature table count + int nl = data.readUnsignedShort(); + int[] loa = new int[nl]; + for (int j = 0; j < nl; j++) { + loa[j] = data.readUnsignedShort(); + } + List ligs = new java.util.ArrayList<>(); + for (int j = 0; j < nl; j++) { + int lo = loa[j]; + data.seek(subtableOffset + so + lo); + // read ligature glyph id + int lg = data.readUnsignedShort(); + // read ligature (input) component count + int nc = data.readUnsignedShort(); + int[] ca = new int [ nc - 1 ]; + // read ligature (input) component glyph ids + for (int k = 0; k < nc - 1; k++) { + ca[k] = data.readUnsignedShort(); + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " ligature substitution ligature set[" + i + "]: ligature(" + lg + "), components: " + toString(ca)); + } + ligs.add(new GlyphSubstitutionTable.Ligature(lg, ca)); + } + seEntries.add(new SELigatureSet(new GlyphSubstitutionTable.LigatureSet(ligs))); + } + } + + private int readLigatureSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readLigatureSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported ligature substitution subtable format: " + sf); + } + return sf; + } + + private AdvancedTypographicTable.RuleLookup[] readRuleLookups(int numLookups, String header) throws IOException { + AdvancedTypographicTable.RuleLookup[] la = new AdvancedTypographicTable.RuleLookup [ numLookups ]; + for (int i = 0, n = numLookups; i < n; i++) { + int sequenceIndex = data.readUnsignedShort(); + int lookupIndex = data.readUnsignedShort(); + la [ i ] = new AdvancedTypographicTable.RuleLookup(sequenceIndex, lookupIndex); + // dump info if debugging and header is non-null + if (log.isDebugEnabled() && (header != null)) { + log.debug(header + "lookup[" + i + "]: " + la[i]); + } + } + return la; + } + + private void readContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read rule set count + int nrs = data.readUnsignedShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for (int i = 0; i < nrs; i++) { + rsoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyphs)"); + log.debug(tableTag + " contextual substitution coverage table offset: " + co); + log.debug(tableTag + " contextual substitution rule set count: " + nrs); + for (int i = 0; i < nrs; i++) { + log.debug(tableTag + " contextual substitution rule set offset[" + i + "]: " + rsoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " contextual substitution coverage", subtableOffset + co); + } else { + ct = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ nrs ]; + String header = null; + for (int i = 0; i < nrs; i++) { + AdvancedTypographicTable.RuleSet rs; + int rso = rsoa [ i ]; + if (rso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + rso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + AdvancedTypographicTable.GlyphSequenceRule r; + int ro = roa [ j ]; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + rso + ro); + // read glyph count + int ng = data.readUnsignedShort(); + // read rule lookup count + int nl = data.readUnsignedShort(); + // read glyphs + int[] glyphs = new int [ ng - 1 ]; + for (int k = 0, nk = glyphs.length; k < nk; k++) { + glyphs [ k ] = data.readUnsignedShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.GlyphSequenceRule(lookups, ng, glyphs); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SERuleSetList(rsa)); + } + + private void readContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read class def table offset + int cdo = data.readUnsignedShort(); + // read class rule set count + int ngc = data.readUnsignedShort(); + // read class rule set offsets + int[] csoa = new int [ ngc ]; + for (int i = 0; i < ngc; i++) { + csoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyph classes)"); + log.debug(tableTag + " contextual substitution coverage table offset: " + co); + log.debug(tableTag + " contextual substitution class set count: " + ngc); + for (int i = 0; i < ngc; i++) { + log.debug(tableTag + " contextual substitution class set offset[" + i + "]: " + csoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " contextual substitution coverage", subtableOffset + co); + } else { + ct = null; + } + // read class definition table + GlyphClassTable cdt; + if (cdo > 0) { + cdt = readClassDefTable(tableTag + " contextual substitution class definition", subtableOffset + cdo); + } else { + cdt = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ ngc ]; + String header = null; + for (int i = 0; i < ngc; i++) { + int cso = csoa [ i ]; + AdvancedTypographicTable.RuleSet rs; + if (cso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + cso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + int ro = roa [ j ]; + AdvancedTypographicTable.ClassSequenceRule r; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + cso + ro); + // read glyph count + int ng = data.readUnsignedShort(); + // read rule lookup count + int nl = data.readUnsignedShort(); + // read classes + int[] classes = new int [ ng - 1 ]; + for (int k = 0, nk = classes.length; k < nk; k++) { + classes [ k ] = data.readUnsignedShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.ClassSequenceRule(lookups, ng, classes); + } else { + assert ro > 0 : "unexpected null subclass rule offset"; + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SEGlyphClassTable(cdt)); + seEntries.add(SEInteger.valueOf(ngc)); + seEntries.add(new SERuleSetList(rsa)); + } + + private void readContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read glyph (input sequence length) count + int ng = data.readUnsignedShort(); + // read substitution lookup count + int nl = data.readUnsignedShort(); + // read glyph coverage offsets, one per glyph input sequence length count + int[] gcoa = new int [ ng ]; + for (int i = 0; i < ng; i++) { + gcoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyph sets)"); + log.debug(tableTag + " contextual substitution glyph input sequence length count: " + ng); + log.debug(tableTag + " contextual substitution lookup count: " + nl); + for (int i = 0; i < ng; i++) { + log.debug(tableTag + " contextual substitution coverage table offset[" + i + "]: " + gcoa[i]); + } + } + // read coverage tables + GlyphCoverageTable[] gca = new GlyphCoverageTable [ ng ]; + for (int i = 0; i < ng; i++) { + int gco = gcoa [ i ]; + GlyphCoverageTable gct; + if (gco > 0) { + gct = readCoverageTable(tableTag + " contextual substitution coverage[" + i + "]", subtableOffset + gco); + } else { + gct = null; + } + gca [ i ] = gct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + // construct rule, rule set, and rule set array + AdvancedTypographicTable.Rule r = new AdvancedTypographicTable.CoverageSequenceRule(lookups, ng, gca); + AdvancedTypographicTable.RuleSet rs = new AdvancedTypographicTable.HomogeneousRuleSet(new AdvancedTypographicTable.Rule[] {r}); + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet[] {rs}; + // store results + assert (gca != null) && (gca.length > 0); + seMapping = gca[0]; + seEntries.add(new SERuleSetList(rsa)); + } + + private int readContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readContextualSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readContextualSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 3) { + readContextualSubTableFormat3(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported contextual substitution subtable format: " + sf); + } + return sf; + } + + private void readChainedContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read rule set count + int nrs = data.readUnsignedShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for (int i = 0; i < nrs; i++) { + rsoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyphs)"); + log.debug(tableTag + " chained contextual substitution coverage table offset: " + co); + log.debug(tableTag + " chained contextual substitution rule set count: " + nrs); + for (int i = 0; i < nrs; i++) { + log.debug(tableTag + " chained contextual substitution rule set offset[" + i + "]: " + rsoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " chained contextual substitution coverage", subtableOffset + co); + } else { + ct = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ nrs ]; + String header = null; + for (int i = 0; i < nrs; i++) { + AdvancedTypographicTable.RuleSet rs; + int rso = rsoa [ i ]; + if (rso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + rso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + AdvancedTypographicTable.ChainedGlyphSequenceRule r; + int ro = roa [ j ]; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + rso + ro); + // read backtrack glyph count + int nbg = data.readUnsignedShort(); + // read backtrack glyphs + int[] backtrackGlyphs = new int [ nbg ]; + for (int k = 0, nk = backtrackGlyphs.length; k < nk; k++) { + backtrackGlyphs [ k ] = data.readUnsignedShort(); + } + // read input glyph count + int nig = data.readUnsignedShort(); + // read glyphs + int[] glyphs = new int [ nig - 1 ]; + for (int k = 0, nk = glyphs.length; k < nk; k++) { + glyphs [ k ] = data.readUnsignedShort(); + } + // read lookahead glyph count + int nlg = data.readUnsignedShort(); + // read lookahead glyphs + int[] lookaheadGlyphs = new int [ nlg ]; + for (int k = 0, nk = lookaheadGlyphs.length; k < nk; k++) { + lookaheadGlyphs [ k ] = data.readUnsignedShort(); + } + // read rule lookup count + int nl = data.readUnsignedShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.ChainedGlyphSequenceRule(lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SERuleSetList(rsa)); + } + + private void readChainedContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read backtrack class def table offset + int bcdo = data.readUnsignedShort(); + // read input class def table offset + int icdo = data.readUnsignedShort(); + // read lookahead class def table offset + int lcdo = data.readUnsignedShort(); + // read class set count + int ngc = data.readUnsignedShort(); + // read class set offsets + int[] csoa = new int [ ngc ]; + for (int i = 0; i < ngc; i++) { + csoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyph classes)"); + log.debug(tableTag + " chained contextual substitution coverage table offset: " + co); + log.debug(tableTag + " chained contextual substitution class set count: " + ngc); + for (int i = 0; i < ngc; i++) { + log.debug(tableTag + " chained contextual substitution class set offset[" + i + "]: " + csoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " chained contextual substitution coverage", subtableOffset + co); + } else { + ct = null; + } + // read backtrack class definition table + GlyphClassTable bcdt; + if (bcdo > 0) { + bcdt = readClassDefTable(tableTag + " contextual substitution backtrack class definition", subtableOffset + bcdo); + } else { + bcdt = null; + } + // read input class definition table + GlyphClassTable icdt; + if (icdo > 0) { + icdt = readClassDefTable(tableTag + " contextual substitution input class definition", subtableOffset + icdo); + } else { + icdt = null; + } + // read lookahead class definition table + GlyphClassTable lcdt; + if (lcdo > 0) { + lcdt = readClassDefTable(tableTag + " contextual substitution lookahead class definition", subtableOffset + lcdo); + } else { + lcdt = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ ngc ]; + String header = null; + for (int i = 0; i < ngc; i++) { + int cso = csoa [ i ]; + AdvancedTypographicTable.RuleSet rs; + if (cso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + cso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + int ro = roa [ j ]; + AdvancedTypographicTable.ChainedClassSequenceRule r; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + cso + ro); + // read backtrack glyph class count + int nbc = data.readUnsignedShort(); + // read backtrack glyph classes + int[] backtrackClasses = new int [ nbc ]; + for (int k = 0, nk = backtrackClasses.length; k < nk; k++) { + backtrackClasses [ k ] = data.readUnsignedShort(); + } + // read input glyph class count + int nic = data.readUnsignedShort(); + // read input glyph classes + int[] classes = new int [ nic - 1 ]; + for (int k = 0, nk = classes.length; k < nk; k++) { + classes [ k ] = data.readUnsignedShort(); + } + // read lookahead glyph class count + int nlc = data.readUnsignedShort(); + // read lookahead glyph classes + int[] lookaheadClasses = new int [ nlc ]; + for (int k = 0, nk = lookaheadClasses.length; k < nk; k++) { + lookaheadClasses [ k ] = data.readUnsignedShort(); + } + // read rule lookup count + int nl = data.readUnsignedShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.ChainedClassSequenceRule(lookups, nic, classes, backtrackClasses, lookaheadClasses); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SEGlyphClassTable(icdt)); + seEntries.add(new SEGlyphClassTable(bcdt)); + seEntries.add(new SEGlyphClassTable(lcdt)); + seEntries.add(SEInteger.valueOf(ngc)); + seEntries.add(new SERuleSetList(rsa)); + } + + private void readChainedContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read backtrack glyph count + int nbg = data.readUnsignedShort(); + // read backtrack glyph coverage offsets + int[] bgcoa = new int [ nbg ]; + for (int i = 0; i < nbg; i++) { + bgcoa [ i ] = data.readUnsignedShort(); + } + // read input glyph count + int nig = data.readUnsignedShort(); + // read input glyph coverage offsets + int[] igcoa = new int [ nig ]; + for (int i = 0; i < nig; i++) { + igcoa [ i ] = data.readUnsignedShort(); + } + // read lookahead glyph count + int nlg = data.readUnsignedShort(); + // read lookahead glyph coverage offsets + int[] lgcoa = new int [ nlg ]; + for (int i = 0; i < nlg; i++) { + lgcoa [ i ] = data.readUnsignedShort(); + } + // read substitution lookup count + int nl = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyph sets)"); + log.debug(tableTag + " chained contextual substitution backtrack glyph count: " + nbg); + for (int i = 0; i < nbg; i++) { + log.debug(tableTag + " chained contextual substitution backtrack coverage table offset[" + i + "]: " + bgcoa[i]); + } + log.debug(tableTag + " chained contextual substitution input glyph count: " + nig); + for (int i = 0; i < nig; i++) { + log.debug(tableTag + " chained contextual substitution input coverage table offset[" + i + "]: " + igcoa[i]); + } + log.debug(tableTag + " chained contextual substitution lookahead glyph count: " + nlg); + for (int i = 0; i < nlg; i++) { + log.debug(tableTag + " chained contextual substitution lookahead coverage table offset[" + i + "]: " + lgcoa[i]); + } + log.debug(tableTag + " chained contextual substitution lookup count: " + nl); + } + // read backtrack coverage tables + GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg]; + for (int i = 0; i < nbg; i++) { + int bgco = bgcoa [ i ]; + GlyphCoverageTable bgct; + if (bgco > 0) { + bgct = readCoverageTable(tableTag + " chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco); + } else { + bgct = null; + } + bgca[i] = bgct; + } + // read input coverage tables + GlyphCoverageTable[] igca = new GlyphCoverageTable[nig]; + for (int i = 0; i < nig; i++) { + int igco = igcoa [ i ]; + GlyphCoverageTable igct; + if (igco > 0) { + igct = readCoverageTable(tableTag + " chained contextual substitution input coverage[" + i + "]", subtableOffset + igco); + } else { + igct = null; + } + igca[i] = igct; + } + // read lookahead coverage tables + GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg]; + for (int i = 0; i < nlg; i++) { + int lgco = lgcoa [ i ]; + GlyphCoverageTable lgct; + if (lgco > 0) { + lgct = readCoverageTable(tableTag + " chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco); + } else { + lgct = null; + } + lgca[i] = lgct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " chained contextual substitution lookups: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + // construct rule, rule set, and rule set array + AdvancedTypographicTable.Rule r = new AdvancedTypographicTable.ChainedCoverageSequenceRule(lookups, nig, igca, bgca, lgca); + AdvancedTypographicTable.RuleSet rs = new AdvancedTypographicTable.HomogeneousRuleSet(new AdvancedTypographicTable.Rule[] {r}); + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet[] {rs}; + // store results + assert (igca != null) && (igca.length > 0); + seMapping = igca[0]; + seEntries.add(new SERuleSetList(rsa)); + } + + private int readChainedContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readChainedContextualSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readChainedContextualSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 3) { + readChainedContextualSubTableFormat3(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported chained contextual substitution subtable format: " + sf); + } + return sf; + } + + private void readExtensionSubTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read extension lookup type + int lt = data.readUnsignedShort(); + // read extension offset + long eo = data.readUnsignedInt(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " extension substitution subtable format: " + subtableFormat); + log.debug(tableTag + " extension substitution lookup type: " + lt); + log.debug(tableTag + " extension substitution lookup table offset: " + eo); + } + // read referenced subtable from extended offset + readGSUBSubtable(lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo); + } + + private int readExtensionSubTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readExtensionSubTableFormat1(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported extension substitution subtable format: " + sf); + } + return sf; + } + + private void readReverseChainedSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read backtrack glyph count + int nbg = data.readUnsignedShort(); + // read backtrack glyph coverage offsets + int[] bgcoa = new int [ nbg ]; + for (int i = 0; i < nbg; i++) { + bgcoa [ i ] = data.readUnsignedShort(); + } + // read lookahead glyph count + int nlg = data.readUnsignedShort(); + // read backtrack glyph coverage offsets + int[] lgcoa = new int [ nlg ]; + for (int i = 0; i < nlg; i++) { + lgcoa [ i ] = data.readUnsignedShort(); + } + // read substitution (output) glyph count + int ng = data.readUnsignedShort(); + // read substitution (output) glyphs + int[] glyphs = new int [ ng ]; + for (int i = 0, n = ng; i < n; i++) { + glyphs [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " reverse chained contextual substitution format: " + subtableFormat); + log.debug(tableTag + " reverse chained contextual substitution coverage table offset: " + co); + log.debug(tableTag + " reverse chained contextual substitution backtrack glyph count: " + nbg); + for (int i = 0; i < nbg; i++) { + log.debug(tableTag + " reverse chained contextual substitution backtrack coverage table offset[" + i + "]: " + bgcoa[i]); + } + log.debug(tableTag + " reverse chained contextual substitution lookahead glyph count: " + nlg); + for (int i = 0; i < nlg; i++) { + log.debug(tableTag + " reverse chained contextual substitution lookahead coverage table offset[" + i + "]: " + lgcoa[i]); + } + log.debug(tableTag + " reverse chained contextual substitution glyphs: " + toString(glyphs)); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " reverse chained contextual substitution coverage", subtableOffset + co); + // read backtrack coverage tables + GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg]; + for (int i = 0; i < nbg; i++) { + int bgco = bgcoa[i]; + GlyphCoverageTable bgct; + if (bgco > 0) { + bgct = readCoverageTable(tableTag + " reverse chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco); + } else { + bgct = null; + } + bgca[i] = bgct; + } + // read lookahead coverage tables + GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg]; + for (int i = 0; i < nlg; i++) { + int lgco = lgcoa[i]; + GlyphCoverageTable lgct; + if (lgco > 0) { + lgct = readCoverageTable(tableTag + " reverse chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco); + } else { + lgct = null; + } + lgca[i] = lgct; + } + // store results + seMapping = ct; + seEntries.add(new SEGlyphCoverageTableList(bgca)); + seEntries.add(new SEGlyphCoverageTableList(lgca)); + seEntries.add(new SEIntList(glyphs)); + } + + private int readReverseChainedSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read substitution subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readReverseChainedSingleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported reverse chained single substitution subtable format: " + sf); + } + return sf; + } + + private void readGSUBSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + initATSubState(); + int subtableFormat = -1; + switch (lookupType) { + case GSUBLookupType.SINGLE: + subtableFormat = readSingleSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.MULTIPLE: + subtableFormat = readMultipleSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.ALTERNATE: + subtableFormat = readAlternateSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.LIGATURE: + subtableFormat = readLigatureSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.CONTEXTUAL: + subtableFormat = readContextualSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.CHAINED_CONTEXTUAL: + subtableFormat = readChainedContextualSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.REVERSE_CHAINED_SINGLE: + subtableFormat = readReverseChainedSingleSubTable(lookupType, lookupFlags, subtableOffset); + break; + case GSUBLookupType.EXTENSION: + subtableFormat = readExtensionSubTable(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset); + break; + default: + break; + } + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_SUBSTITUTION, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableFormat); + resetATSubState(); + } + + private GlyphPositioningTable.DeviceTable readPosDeviceTable(long subtableOffset, long deviceTableOffset) throws IOException { + long cp = data.getCurrentPosition(); + data.seek(subtableOffset + deviceTableOffset); + // read start size + int ss = data.readUnsignedShort(); + // read end size + int es = data.readUnsignedShort(); + // read delta format + int df = data.readUnsignedShort(); + int s1; + int m1; + int dm; + int dd; + int s2; + if (df == 1) { + s1 = 14; + m1 = 0x3; + dm = 1; + dd = 4; + s2 = 2; + } else if (df == 2) { + s1 = 12; + m1 = 0xF; + dm = 7; + dd = 16; + s2 = 4; + } else if (df == 3) { + s1 = 8; + m1 = 0xFF; + dm = 127; + dd = 256; + s2 = 8; + } else { + log.debug("unsupported device table delta format: " + df + ", ignoring device table"); + return null; + } + // read deltas + int n = (es - ss) + 1; + if (n < 0) { + log.debug("invalid device table delta count: " + n + ", ignoring device table"); + return null; + } + int[] da = new int [ n ]; + for (int i = 0; (i < n) && (s2 > 0);) { + int p = data.readUnsignedShort(); + for (int j = 0, k = 16 / s2; j < k; j++) { + int d = (p >> s1) & m1; + if (d > dm) { + d -= dd; + } + if (i < n) { + da [ i++ ] = d; + } else { + break; + } + p <<= s2; + } + } + data.seek(cp); + return new GlyphPositioningTable.DeviceTable(ss, es, da); + } + + private GlyphPositioningTable.Value readPosValue(long subtableOffset, int valueFormat) throws IOException { + // XPlacement + int xp; + if ((valueFormat & GlyphPositioningTable.Value.X_PLACEMENT) != 0) { + xp = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + } else { + xp = 0; + } + // YPlacement + int yp; + if ((valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT) != 0) { + yp = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + } else { + yp = 0; + } + // XAdvance + int xa; + if ((valueFormat & GlyphPositioningTable.Value.X_ADVANCE) != 0) { + xa = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + } else { + xa = 0; + } + // YAdvance + int ya; + if ((valueFormat & GlyphPositioningTable.Value.Y_ADVANCE) != 0) { + ya = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + } else { + ya = 0; + } + // XPlaDevice + GlyphPositioningTable.DeviceTable xpd; + if ((valueFormat & GlyphPositioningTable.Value.X_PLACEMENT_DEVICE) != 0) { + int xpdo = data.readUnsignedShort(); + xpd = readPosDeviceTable(subtableOffset, xpdo); + } else { + xpd = null; + } + // YPlaDevice + GlyphPositioningTable.DeviceTable ypd; + if ((valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT_DEVICE) != 0) { + int ypdo = data.readUnsignedShort(); + ypd = readPosDeviceTable(subtableOffset, ypdo); + } else { + ypd = null; + } + // XAdvDevice + GlyphPositioningTable.DeviceTable xad; + if ((valueFormat & GlyphPositioningTable.Value.X_ADVANCE_DEVICE) != 0) { + int xado = data.readUnsignedShort(); + xad = readPosDeviceTable(subtableOffset, xado); + } else { + xad = null; + } + // YAdvDevice + GlyphPositioningTable.DeviceTable yad; + if ((valueFormat & GlyphPositioningTable.Value.Y_ADVANCE_DEVICE) != 0) { + int yado = data.readUnsignedShort(); + yad = readPosDeviceTable(subtableOffset, yado); + } else { + yad = null; + } + return new GlyphPositioningTable.Value(xp, yp, xa, ya, xpd, ypd, xad, yad); + } + + private void readSinglePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read value format + int vf = data.readUnsignedShort(); + // read value + GlyphPositioningTable.Value v = readPosValue(subtableOffset, vf); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single positioning subtable format: " + subtableFormat + " (delta)"); + log.debug(tableTag + " single positioning coverage table offset: " + co); + log.debug(tableTag + " single positioning value: " + v); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " single positioning coverage", subtableOffset + co); + // store results + seMapping = ct; + seEntries.add(new SEValue(v)); + } + + private void readSinglePosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read value format + int vf = data.readUnsignedShort(); + // read value count + int nv = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single positioning subtable format: " + subtableFormat + " (mapped)"); + log.debug(tableTag + " single positioning coverage table offset: " + co); + log.debug(tableTag + " single positioning value count: " + nv); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " single positioning coverage", subtableOffset + co); + // read positioning values + GlyphPositioningTable.Value[] pva = new GlyphPositioningTable.Value[nv]; + for (int i = 0, n = nv; i < n; i++) { + GlyphPositioningTable.Value pv = readPosValue(subtableOffset, vf); + if (log.isDebugEnabled()) { + log.debug(tableTag + " single positioning value[" + i + "]: " + pv); + } + pva[i] = pv; + } + // store results + seMapping = ct; + seEntries.add(new SEValueList(pva)); + } + + private int readSinglePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positionining subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readSinglePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readSinglePosTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported single positioning subtable format: " + sf); + } + return sf; + } + + private GlyphPositioningTable.PairValues readPosPairValues(long subtableOffset, boolean hasGlyph, int vf1, int vf2) throws IOException { + // read glyph (if present) + int glyph; + if (hasGlyph) { + glyph = data.readUnsignedShort(); + } else { + glyph = 0; + } + // read first value (if present) + GlyphPositioningTable.Value v1; + if (vf1 != 0) { + v1 = readPosValue(subtableOffset, vf1); + } else { + v1 = null; + } + // read second value (if present) + GlyphPositioningTable.Value v2; + if (vf2 != 0) { + v2 = readPosValue(subtableOffset, vf2); + } else { + v2 = null; + } + return new GlyphPositioningTable.PairValues(glyph, v1, v2); + } + + private GlyphPositioningTable.PairValues[] readPosPairSetTable(long subtableOffset, int pairSetTableOffset, int vf1, int vf2) throws IOException { + String tableTag = "GPOS"; + long cp = data.getCurrentPosition(); + data.seek(subtableOffset + pairSetTableOffset); + // read pair values count + int npv = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair set table offset: " + pairSetTableOffset); + log.debug(tableTag + " pair set table values count: " + npv); + } + // read pair values + GlyphPositioningTable.PairValues[] pva = new GlyphPositioningTable.PairValues [ npv ]; + for (int i = 0, n = npv; i < n; i++) { + GlyphPositioningTable.PairValues pv = readPosPairValues(subtableOffset, true, vf1, vf2); + pva [ i ] = pv; + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair set table value[" + i + "]: " + pv); + } + } + data.seek(cp); + return pva; + } + + private void readPairPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read value format for first glyph + int vf1 = data.readUnsignedShort(); + // read value format for second glyph + int vf2 = data.readUnsignedShort(); + // read number (count) of pair sets + int nps = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair positioning subtable format: " + subtableFormat + " (glyphs)"); + log.debug(tableTag + " pair positioning coverage table offset: " + co); + log.debug(tableTag + " pair positioning value format #1: " + vf1); + log.debug(tableTag + " pair positioning value format #2: " + vf2); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " pair positioning coverage", subtableOffset + co); + // read pair value matrix + GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues [ nps ][]; + for (int i = 0, n = nps; i < n; i++) { + // read pair set offset + int pso = data.readUnsignedShort(); + // read pair set table at offset + pvm [ i ] = readPosPairSetTable(subtableOffset, pso, vf1, vf2); + } + // store results + seMapping = ct; + seEntries.add(new SEPairValueMatrix(pvm)); + } + + private void readPairPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read value format for first glyph + int vf1 = data.readUnsignedShort(); + // read value format for second glyph + int vf2 = data.readUnsignedShort(); + // read class def 1 offset + int cd1o = data.readUnsignedShort(); + // read class def 2 offset + int cd2o = data.readUnsignedShort(); + // read number (count) of classes in class def 1 table + int nc1 = data.readUnsignedShort(); + // read number (count) of classes in class def 2 table + int nc2 = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair positioning subtable format: " + subtableFormat + " (glyph classes)"); + log.debug(tableTag + " pair positioning coverage table offset: " + co); + log.debug(tableTag + " pair positioning value format #1: " + vf1); + log.debug(tableTag + " pair positioning value format #2: " + vf2); + log.debug(tableTag + " pair positioning class def table #1 offset: " + cd1o); + log.debug(tableTag + " pair positioning class def table #2 offset: " + cd2o); + log.debug(tableTag + " pair positioning class #1 count: " + nc1); + log.debug(tableTag + " pair positioning class #2 count: " + nc2); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " pair positioning coverage", subtableOffset + co); + // read class definition table #1 + GlyphClassTable cdt1 = readClassDefTable(tableTag + " pair positioning class definition #1", subtableOffset + cd1o); + // read class definition table #2 + GlyphClassTable cdt2 = readClassDefTable(tableTag + " pair positioning class definition #2", subtableOffset + cd2o); + // read pair value matrix + GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues [ nc1 ] [ nc2 ]; + for (int i = 0; i < nc1; i++) { + for (int j = 0; j < nc2; j++) { + GlyphPositioningTable.PairValues pv = readPosPairValues(subtableOffset, false, vf1, vf2); + pvm [ i ] [ j ] = pv; + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair set table value[" + i + "][" + j + "]: " + pv); + } + } + } + // store results + seMapping = ct; + seEntries.add(new SEGlyphClassTable(cdt1)); + seEntries.add(new SEGlyphClassTable(cdt2)); + seEntries.add(SEInteger.valueOf(nc1)); + seEntries.add(SEInteger.valueOf(nc2)); + seEntries.add(new SEPairValueMatrix(pvm)); + } + + private int readPairPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readPairPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readPairPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported pair positioning subtable format: " + sf); + } + return sf; + } + + private GlyphPositioningTable.Anchor readPosAnchor(long anchorTableOffset) throws IOException { + GlyphPositioningTable.Anchor a; + long cp = data.getCurrentPosition(); + data.seek(anchorTableOffset); + // read anchor table format + int af = data.readUnsignedShort(); + if (af == 1) { + // read x coordinate + int x = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + // read y coordinate + int y = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + a = new GlyphPositioningTable.Anchor(x, y); + } else if (af == 2) { + // read x coordinate + int x = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + // read y coordinate + int y = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + // read anchor point index + int ap = data.readUnsignedShort(); + a = new GlyphPositioningTable.Anchor(x, y, ap); + } else if (af == 3) { + // read x coordinate + int x = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + // read y coordinate + int y = otf.convertTTFUnit2PDFUnit(data.readSignedShort()); + // read x device table offset + int xdo = data.readUnsignedShort(); + // read y device table offset + int ydo = data.readUnsignedShort(); + // read x device table (if present) + GlyphPositioningTable.DeviceTable xd; + if (xdo != 0) { + xd = readPosDeviceTable(cp, xdo); + } else { + xd = null; + } + // read y device table (if present) + GlyphPositioningTable.DeviceTable yd; + if (ydo != 0) { + yd = readPosDeviceTable(cp, ydo); + } else { + yd = null; + } + a = new GlyphPositioningTable.Anchor(x, y, xd, yd); + } else { + throw new AdvancedTypographicTableFormatException("unsupported positioning anchor format: " + af); + } + data.seek(cp); + return a; + } + + private void readCursivePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read entry/exit count + int ec = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " cursive positioning subtable format: " + subtableFormat); + log.debug(tableTag + " cursive positioning coverage table offset: " + co); + log.debug(tableTag + " cursive positioning entry/exit count: " + ec); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " cursive positioning coverage", subtableOffset + co); + // read entry/exit records + GlyphPositioningTable.Anchor[] aa = new GlyphPositioningTable.Anchor [ ec * 2 ]; + for (int i = 0, n = ec; i < n; i++) { + // read entry anchor offset + int eno = data.readUnsignedShort(); + // read exit anchor offset + int exo = data.readUnsignedShort(); + // read entry anchor + GlyphPositioningTable.Anchor ena; + if (eno > 0) { + ena = readPosAnchor(subtableOffset + eno); + } else { + ena = null; + } + // read exit anchor + GlyphPositioningTable.Anchor exa; + if (exo > 0) { + exa = readPosAnchor(subtableOffset + exo); + } else { + exa = null; + } + aa [ (i * 2) + 0 ] = ena; + aa [ (i * 2) + 1 ] = exa; + if (log.isDebugEnabled()) { + if (ena != null) { + log.debug(tableTag + " cursive entry anchor [" + i + "]: " + ena); + } + if (exa != null) { + log.debug(tableTag + " cursive exit anchor [" + i + "]: " + exa); + } + } + } + // store results + seMapping = ct; + seEntries.add(new SEAnchorList(aa)); + } + + private int readCursivePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readCursivePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported cursive positioning subtable format: " + sf); + } + return sf; + } + + private void readMarkToBasePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read mark coverage offset + int mco = data.readUnsignedShort(); + // read base coverage offset + int bco = data.readUnsignedShort(); + // read mark class count + int nmc = data.readUnsignedShort(); + // read mark array offset + int mao = data.readUnsignedShort(); + // read base array offset + int bao = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning subtable format: " + subtableFormat); + log.debug(tableTag + " mark-to-base positioning mark coverage table offset: " + mco); + log.debug(tableTag + " mark-to-base positioning base coverage table offset: " + bco); + log.debug(tableTag + " mark-to-base positioning mark class count: " + nmc); + log.debug(tableTag + " mark-to-base positioning mark array offset: " + mao); + log.debug(tableTag + " mark-to-base positioning base array offset: " + bao); + } + // read mark coverage table + GlyphCoverageTable mct = readCoverageTable(tableTag + " mark-to-base positioning mark coverage", subtableOffset + mco); + // read base coverage table + GlyphCoverageTable bct = readCoverageTable(tableTag + " mark-to-base positioning base coverage", subtableOffset + bco); + // read mark anchor array + // seek to mark array + data.seek(subtableOffset + mao); + // read mark count + int nm = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning mark count: " + nm); + } + // read mark anchor array, where i:{0...markCount} + GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm ]; + for (int i = 0; i < nm; i++) { + // read mark class + int mc = data.readUnsignedShort(); + // read mark anchor offset + int ao = data.readUnsignedShort(); + GlyphPositioningTable.Anchor a; + if (ao > 0) { + a = readPosAnchor(subtableOffset + mao + ao); + } else { + a = null; + } + GlyphPositioningTable.MarkAnchor ma; + if (a != null) { + ma = new GlyphPositioningTable.MarkAnchor(mc, a); + } else { + ma = null; + } + maa [ i ] = ma; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning mark anchor[" + i + "]: " + ma); + } + + } + // read base anchor matrix + // seek to base array + data.seek(subtableOffset + bao); + // read base count + int nb = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning base count: " + nb); + } + // read anchor matrix, where i:{0...baseCount - 1}, j:{0...markClassCount - 1} + GlyphPositioningTable.Anchor[][] bam = new GlyphPositioningTable.Anchor [ nb ] [ nmc ]; + for (int i = 0; i < nb; i++) { + for (int j = 0; j < nmc; j++) { + // read base anchor offset + int ao = data.readUnsignedShort(); + GlyphPositioningTable.Anchor a; + if (ao > 0) { + a = readPosAnchor(subtableOffset + bao + ao); + } else { + a = null; + } + bam [ i ] [ j ] = a; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning base anchor[" + i + "][" + j + "]: " + a); + } + } + } + // store results + seMapping = mct; + seEntries.add(new SEGlyphCoverageTable(bct)); + seEntries.add(SEInteger.valueOf(nmc)); + seEntries.add(new SEMarkAnchorList(maa)); + seEntries.add(new SEAnchorMatrix(bam)); + } + + private int readMarkToBasePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readMarkToBasePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported mark-to-base positioning subtable format: " + sf); + } + return sf; + } + + private void readMarkToLigaturePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read mark coverage offset + int mco = data.readUnsignedShort(); + // read ligature coverage offset + int lco = data.readUnsignedShort(); + // read mark class count + int nmc = data.readUnsignedShort(); + // read mark array offset + int mao = data.readUnsignedShort(); + // read ligature array offset + int lao = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning subtable format: " + subtableFormat); + log.debug(tableTag + " mark-to-ligature positioning mark coverage table offset: " + mco); + log.debug(tableTag + " mark-to-ligature positioning ligature coverage table offset: " + lco); + log.debug(tableTag + " mark-to-ligature positioning mark class count: " + nmc); + log.debug(tableTag + " mark-to-ligature positioning mark array offset: " + mao); + log.debug(tableTag + " mark-to-ligature positioning ligature array offset: " + lao); + } + // read mark coverage table + GlyphCoverageTable mct = readCoverageTable(tableTag + " mark-to-ligature positioning mark coverage", subtableOffset + mco); + // read ligature coverage table + GlyphCoverageTable lct = readCoverageTable(tableTag + " mark-to-ligature positioning ligature coverage", subtableOffset + lco); + // read mark anchor array + // seek to mark array + data.seek(subtableOffset + mao); + // read mark count + int nm = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning mark count: " + nm); + } + // read mark anchor array, where i:{0...markCount} + GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm ]; + for (int i = 0; i < nm; i++) { + // read mark class + int mc = data.readUnsignedShort(); + // read mark anchor offset + int ao = data.readUnsignedShort(); + GlyphPositioningTable.Anchor a; + if (ao > 0) { + a = readPosAnchor(subtableOffset + mao + ao); + } else { + a = null; + } + GlyphPositioningTable.MarkAnchor ma; + if (a != null) { + ma = new GlyphPositioningTable.MarkAnchor(mc, a); + } else { + ma = null; + } + maa [ i ] = ma; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning mark anchor[" + i + "]: " + ma); + } + } + // read ligature anchor matrix + // seek to ligature array + data.seek(subtableOffset + lao); + // read ligature count + int nl = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning ligature count: " + nl); + } + // read ligature attach table offsets + int[] laoa = new int [ nl ]; + for (int i = 0; i < nl; i++) { + laoa [ i ] = data.readUnsignedShort(); + } + // iterate over ligature attach tables, recording maximum component count + int mxc = 0; + for (int i = 0; i < nl; i++) { + int lato = laoa [ i ]; + data.seek(subtableOffset + lao + lato); + // read component count + int cc = data.readUnsignedShort(); + if (cc > mxc) { + mxc = cc; + } + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning maximum component count: " + mxc); + } + // read anchor matrix, where i:{0...ligatureCount - 1}, j:{0...maxComponentCount - 1}, k:{0...markClassCount - 1} + GlyphPositioningTable.Anchor[][][] lam = new GlyphPositioningTable.Anchor [ nl ][][]; + for (int i = 0; i < nl; i++) { + int lato = laoa [ i ]; + // seek to ligature attach table for ligature[i] + data.seek(subtableOffset + lao + lato); + // read component count + int cc = data.readUnsignedShort(); + GlyphPositioningTable.Anchor[][] lcm = new GlyphPositioningTable.Anchor [ cc ] [ nmc ]; + for (int j = 0; j < cc; j++) { + for (int k = 0; k < nmc; k++) { + // read ligature anchor offset + int ao = data.readUnsignedShort(); + GlyphPositioningTable.Anchor a; + if (ao > 0) { + a = readPosAnchor(subtableOffset + lao + lato + ao); + } else { + a = null; + } + lcm [ j ] [ k ] = a; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning ligature anchor[" + i + "][" + j + "][" + k + "]: " + a); + } + } + } + lam [ i ] = lcm; + } + // store results + seMapping = mct; + seEntries.add(new SEGlyphCoverageTable(lct)); + seEntries.add(SEInteger.valueOf(nmc)); + seEntries.add(SEInteger.valueOf(mxc)); + seEntries.add(new SEMarkAnchorList(maa)); + seEntries.add(new SEAnchorMultiMatrix(lam)); + } + + private int readMarkToLigaturePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readMarkToLigaturePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported mark-to-ligature positioning subtable format: " + sf); + } + return sf; + } + + private void readMarkToMarkPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read mark #1 coverage offset + int m1co = data.readUnsignedShort(); + // read mark #2 coverage offset + int m2co = data.readUnsignedShort(); + // read mark class count + int nmc = data.readUnsignedShort(); + // read mark #1 array offset + int m1ao = data.readUnsignedShort(); + // read mark #2 array offset + int m2ao = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning subtable format: " + subtableFormat); + log.debug(tableTag + " mark-to-mark positioning mark #1 coverage table offset: " + m1co); + log.debug(tableTag + " mark-to-mark positioning mark #2 coverage table offset: " + m2co); + log.debug(tableTag + " mark-to-mark positioning mark class count: " + nmc); + log.debug(tableTag + " mark-to-mark positioning mark #1 array offset: " + m1ao); + log.debug(tableTag + " mark-to-mark positioning mark #2 array offset: " + m2ao); + } + // read mark #1 coverage table + GlyphCoverageTable mct1 = readCoverageTable(tableTag + " mark-to-mark positioning mark #1 coverage", subtableOffset + m1co); + // read mark #2 coverage table + GlyphCoverageTable mct2 = readCoverageTable(tableTag + " mark-to-mark positioning mark #2 coverage", subtableOffset + m2co); + // read mark #1 anchor array + // seek to mark array + data.seek(subtableOffset + m1ao); + // read mark count + int nm1 = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #1 count: " + nm1); + } + // read mark anchor array, where i:{0...mark1Count} + GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm1 ]; + for (int i = 0; i < nm1; i++) { + // read mark class + int mc = data.readUnsignedShort(); + // read mark anchor offset + int ao = data.readUnsignedShort(); + GlyphPositioningTable.Anchor a; + if (ao > 0) { + a = readPosAnchor(subtableOffset + m1ao + ao); + } else { + a = null; + } + GlyphPositioningTable.MarkAnchor ma; + if (a != null) { + ma = new GlyphPositioningTable.MarkAnchor(mc, a); + } else { + ma = null; + } + maa [ i ] = ma; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #1 anchor[" + i + "]: " + ma); + } + } + // read mark #2 anchor matrix + // seek to mark #2 array + data.seek(subtableOffset + m2ao); + // read mark #2 count + int nm2 = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #2 count: " + nm2); + } + // read anchor matrix, where i:{0...mark2Count - 1}, j:{0...markClassCount - 1} + GlyphPositioningTable.Anchor[][] mam = new GlyphPositioningTable.Anchor [ nm2 ] [ nmc ]; + for (int i = 0; i < nm2; i++) { + for (int j = 0; j < nmc; j++) { + // read mark anchor offset + int ao = data.readUnsignedShort(); + GlyphPositioningTable.Anchor a; + if (ao > 0) { + a = readPosAnchor(subtableOffset + m2ao + ao); + } else { + a = null; + } + mam [ i ] [ j ] = a; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #2 anchor[" + i + "][" + j + "]: " + a); + } + } + } + // store results + seMapping = mct1; + seEntries.add(new SEGlyphCoverageTable(mct2)); + seEntries.add(SEInteger.valueOf(nmc)); + seEntries.add(new SEMarkAnchorList(maa)); + seEntries.add(new SEAnchorMatrix(mam)); + } + + private int readMarkToMarkPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readMarkToMarkPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported mark-to-mark positioning subtable format: " + sf); + } + return sf; + } + + private void readContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read rule set count + int nrs = data.readUnsignedShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for (int i = 0; i < nrs; i++) { + rsoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyphs)"); + log.debug(tableTag + " contextual positioning coverage table offset: " + co); + log.debug(tableTag + " contextual positioning rule set count: " + nrs); + for (int i = 0; i < nrs; i++) { + log.debug(tableTag + " contextual positioning rule set offset[" + i + "]: " + rsoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " contextual positioning coverage", subtableOffset + co); + } else { + ct = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ nrs ]; + String header = null; + for (int i = 0; i < nrs; i++) { + AdvancedTypographicTable.RuleSet rs; + int rso = rsoa [ i ]; + if (rso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + rso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + AdvancedTypographicTable.GlyphSequenceRule r; + int ro = roa [ j ]; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + rso + ro); + // read glyph count + int ng = data.readUnsignedShort(); + // read rule lookup count + int nl = data.readUnsignedShort(); + // read glyphs + int[] glyphs = new int [ ng - 1 ]; + for (int k = 0, nk = glyphs.length; k < nk; k++) { + glyphs [ k ] = data.readUnsignedShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.GlyphSequenceRule(lookups, ng, glyphs); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SERuleSetList(rsa)); + } + + private void readContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read class def table offset + int cdo = data.readUnsignedShort(); + // read class rule set count + int ngc = data.readUnsignedShort(); + // read class rule set offsets + int[] csoa = new int [ ngc ]; + for (int i = 0; i < ngc; i++) { + csoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyph classes)"); + log.debug(tableTag + " contextual positioning coverage table offset: " + co); + log.debug(tableTag + " contextual positioning class set count: " + ngc); + for (int i = 0; i < ngc; i++) { + log.debug(tableTag + " contextual positioning class set offset[" + i + "]: " + csoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " contextual positioning coverage", subtableOffset + co); + } else { + ct = null; + } + // read class definition table + GlyphClassTable cdt; + if (cdo > 0) { + cdt = readClassDefTable(tableTag + " contextual positioning class definition", subtableOffset + cdo); + } else { + cdt = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ ngc ]; + String header = null; + for (int i = 0; i < ngc; i++) { + int cso = csoa [ i ]; + AdvancedTypographicTable.RuleSet rs; + if (cso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + cso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + int ro = roa [ j ]; + AdvancedTypographicTable.ClassSequenceRule r; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + cso + ro); + // read glyph count + int ng = data.readUnsignedShort(); + // read rule lookup count + int nl = data.readUnsignedShort(); + // read classes + int[] classes = new int [ ng - 1 ]; + for (int k = 0, nk = classes.length; k < nk; k++) { + classes [ k ] = data.readUnsignedShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.ClassSequenceRule(lookups, ng, classes); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SEGlyphClassTable(cdt)); + seEntries.add(SEInteger.valueOf(ngc)); + seEntries.add(new SERuleSetList(rsa)); + } + + private void readContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read glyph (input sequence length) count + int ng = data.readUnsignedShort(); + // read positioning lookup count + int nl = data.readUnsignedShort(); + // read glyph coverage offsets, one per glyph input sequence length count + int[] gcoa = new int [ ng ]; + for (int i = 0; i < ng; i++) { + gcoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyph sets)"); + log.debug(tableTag + " contextual positioning glyph input sequence length count: " + ng); + log.debug(tableTag + " contextual positioning lookup count: " + nl); + for (int i = 0; i < ng; i++) { + log.debug(tableTag + " contextual positioning coverage table offset[" + i + "]: " + gcoa[i]); + } + } + // read coverage tables + GlyphCoverageTable[] gca = new GlyphCoverageTable [ ng ]; + for (int i = 0; i < ng; i++) { + int gco = gcoa [ i ]; + GlyphCoverageTable gct; + if (gco > 0) { + gct = readCoverageTable(tableTag + " contextual positioning coverage[" + i + "]", subtableOffset + gcoa[i]); + } else { + gct = null; + } + gca [ i ] = gct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + // construct rule, rule set, and rule set array + AdvancedTypographicTable.Rule r = new AdvancedTypographicTable.CoverageSequenceRule(lookups, ng, gca); + AdvancedTypographicTable.RuleSet rs = new AdvancedTypographicTable.HomogeneousRuleSet(new AdvancedTypographicTable.Rule[] {r}); + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet[] {rs}; + // store results + assert (gca != null) && (gca.length > 0); + seMapping = gca[0]; + seEntries.add(new SERuleSetList(rsa)); + } + + private int readContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readContextualPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readContextualPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 3) { + readContextualPosTableFormat3(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported contextual positioning subtable format: " + sf); + } + return sf; + } + + private void readChainedContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read rule set count + int nrs = data.readUnsignedShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for (int i = 0; i < nrs; i++) { + rsoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyphs)"); + log.debug(tableTag + " chained contextual positioning coverage table offset: " + co); + log.debug(tableTag + " chained contextual positioning rule set count: " + nrs); + for (int i = 0; i < nrs; i++) { + log.debug(tableTag + " chained contextual positioning rule set offset[" + i + "]: " + rsoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " chained contextual positioning coverage", subtableOffset + co); + } else { + ct = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ nrs ]; + String header = null; + for (int i = 0; i < nrs; i++) { + AdvancedTypographicTable.RuleSet rs; + int rso = rsoa [ i ]; + if (rso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + rso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + AdvancedTypographicTable.ChainedGlyphSequenceRule r; + int ro = roa [ j ]; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + rso + ro); + // read backtrack glyph count + int nbg = data.readUnsignedShort(); + // read backtrack glyphs + int[] backtrackGlyphs = new int [ nbg ]; + for (int k = 0, nk = backtrackGlyphs.length; k < nk; k++) { + backtrackGlyphs [ k ] = data.readUnsignedShort(); + } + // read input glyph count + int nig = data.readUnsignedShort(); + // read glyphs + int[] glyphs = new int [ nig - 1 ]; + for (int k = 0, nk = glyphs.length; k < nk; k++) { + glyphs [ k ] = data.readUnsignedShort(); + } + // read lookahead glyph count + int nlg = data.readUnsignedShort(); + // read lookahead glyphs + int[] lookaheadGlyphs = new int [ nlg ]; + for (int k = 0, nk = lookaheadGlyphs.length; k < nk; k++) { + lookaheadGlyphs [ k ] = data.readUnsignedShort(); + } + // read rule lookup count + int nl = data.readUnsignedShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.ChainedGlyphSequenceRule(lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SERuleSetList(rsa)); + } + + private void readChainedContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read coverage offset + int co = data.readUnsignedShort(); + // read backtrack class def table offset + int bcdo = data.readUnsignedShort(); + // read input class def table offset + int icdo = data.readUnsignedShort(); + // read lookahead class def table offset + int lcdo = data.readUnsignedShort(); + // read class set count + int ngc = data.readUnsignedShort(); + // read class set offsets + int[] csoa = new int [ ngc ]; + for (int i = 0; i < ngc; i++) { + csoa [ i ] = data.readUnsignedShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyph classes)"); + log.debug(tableTag + " chained contextual positioning coverage table offset: " + co); + log.debug(tableTag + " chained contextual positioning class set count: " + ngc); + for (int i = 0; i < ngc; i++) { + log.debug(tableTag + " chained contextual positioning class set offset[" + i + "]: " + csoa[i]); + } + } + // read coverage table + GlyphCoverageTable ct; + if (co > 0) { + ct = readCoverageTable(tableTag + " chained contextual positioning coverage", subtableOffset + co); + } else { + ct = null; + } + // read backtrack class definition table + GlyphClassTable bcdt; + if (bcdo > 0) { + bcdt = readClassDefTable(tableTag + " contextual positioning backtrack class definition", subtableOffset + bcdo); + } else { + bcdt = null; + } + // read input class definition table + GlyphClassTable icdt; + if (icdo > 0) { + icdt = readClassDefTable(tableTag + " contextual positioning input class definition", subtableOffset + icdo); + } else { + icdt = null; + } + // read lookahead class definition table + GlyphClassTable lcdt; + if (lcdo > 0) { + lcdt = readClassDefTable(tableTag + " contextual positioning lookahead class definition", subtableOffset + lcdo); + } else { + lcdt = null; + } + // read rule sets + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet [ ngc ]; + String header = null; + for (int i = 0; i < ngc; i++) { + int cso = csoa [ i ]; + AdvancedTypographicTable.RuleSet rs; + if (cso > 0) { + // seek to rule set [ i ] + data.seek(subtableOffset + cso); + // read rule count + int nr = data.readUnsignedShort(); + // read rule offsets + int[] roa = new int [ nr ]; + AdvancedTypographicTable.Rule[] ra = new AdvancedTypographicTable.Rule [ nr ]; + for (int j = 0; j < nr; j++) { + roa [ j ] = data.readUnsignedShort(); + } + // read glyph sequence rules + for (int j = 0; j < nr; j++) { + AdvancedTypographicTable.ChainedClassSequenceRule r; + int ro = roa [ j ]; + if (ro > 0) { + // seek to rule [ j ] + data.seek(subtableOffset + cso + ro); + // read backtrack glyph class count + int nbc = data.readUnsignedShort(); + // read backtrack glyph classes + int[] backtrackClasses = new int [ nbc ]; + for (int k = 0, nk = backtrackClasses.length; k < nk; k++) { + backtrackClasses [ k ] = data.readUnsignedShort(); + } + // read input glyph class count + int nic = data.readUnsignedShort(); + // read input glyph classes + int[] classes = new int [ nic - 1 ]; + for (int k = 0, nk = classes.length; k < nk; k++) { + classes [ k ] = data.readUnsignedShort(); + } + // read lookahead glyph class count + int nlc = data.readUnsignedShort(); + // read lookahead glyph classes + int[] lookaheadClasses = new int [ nlc ]; + for (int k = 0, nk = lookaheadClasses.length; k < nk; k++) { + lookaheadClasses [ k ] = data.readUnsignedShort(); + } + // read rule lookup count + int nl = data.readUnsignedShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + r = new AdvancedTypographicTable.ChainedClassSequenceRule(lookups, nic, classes, backtrackClasses, lookaheadClasses); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new AdvancedTypographicTable.HomogeneousRuleSet(ra); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add(new SEGlyphClassTable(icdt)); + seEntries.add(new SEGlyphClassTable(bcdt)); + seEntries.add(new SEGlyphClassTable(lcdt)); + seEntries.add(SEInteger.valueOf(ngc)); + seEntries.add(new SERuleSetList(rsa)); + } + + private void readChainedContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read backtrack glyph count + int nbg = data.readUnsignedShort(); + // read backtrack glyph coverage offsets + int[] bgcoa = new int [ nbg ]; + for (int i = 0; i < nbg; i++) { + bgcoa [ i ] = data.readUnsignedShort(); + } + // read input glyph count + int nig = data.readUnsignedShort(); + // read backtrack glyph coverage offsets + int[] igcoa = new int [ nig ]; + for (int i = 0; i < nig; i++) { + igcoa [ i ] = data.readUnsignedShort(); + } + // read lookahead glyph count + int nlg = data.readUnsignedShort(); + // read backtrack glyph coverage offsets + int[] lgcoa = new int [ nlg ]; + for (int i = 0; i < nlg; i++) { + lgcoa [ i ] = data.readUnsignedShort(); + } + // read positioning lookup count + int nl = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyph sets)"); + log.debug(tableTag + " chained contextual positioning backtrack glyph count: " + nbg); + for (int i = 0; i < nbg; i++) { + log.debug(tableTag + " chained contextual positioning backtrack coverage table offset[" + i + "]: " + bgcoa[i]); + } + log.debug(tableTag + " chained contextual positioning input glyph count: " + nig); + for (int i = 0; i < nig; i++) { + log.debug(tableTag + " chained contextual positioning input coverage table offset[" + i + "]: " + igcoa[i]); + } + log.debug(tableTag + " chained contextual positioning lookahead glyph count: " + nlg); + for (int i = 0; i < nlg; i++) { + log.debug(tableTag + " chained contextual positioning lookahead coverage table offset[" + i + "]: " + lgcoa[i]); + } + log.debug(tableTag + " chained contextual positioning lookup count: " + nl); + } + // read backtrack coverage tables + GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg]; + for (int i = 0; i < nbg; i++) { + int bgco = bgcoa [ i ]; + GlyphCoverageTable bgct; + if (bgco > 0) { + bgct = readCoverageTable(tableTag + " chained contextual positioning backtrack coverage[" + i + "]", subtableOffset + bgco); + } else { + bgct = null; + } + bgca[i] = bgct; + } + // read input coverage tables + GlyphCoverageTable[] igca = new GlyphCoverageTable[nig]; + for (int i = 0; i < nig; i++) { + int igco = igcoa [ i ]; + GlyphCoverageTable igct; + if (igco > 0) { + igct = readCoverageTable(tableTag + " chained contextual positioning input coverage[" + i + "]", subtableOffset + igco); + } else { + igct = null; + } + igca[i] = igct; + } + // read lookahead coverage tables + GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg]; + for (int i = 0; i < nlg; i++) { + int lgco = lgcoa [ i ]; + GlyphCoverageTable lgct; + if (lgco > 0) { + lgct = readCoverageTable(tableTag + " chained contextual positioning lookahead coverage[" + i + "]", subtableOffset + lgco); + } else { + lgct = null; + } + lgca[i] = lgct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " chained contextual positioning lookups: "; + } + AdvancedTypographicTable.RuleLookup[] lookups = readRuleLookups(nl, header); + // construct rule, rule set, and rule set array + AdvancedTypographicTable.Rule r = new AdvancedTypographicTable.ChainedCoverageSequenceRule(lookups, nig, igca, bgca, lgca); + AdvancedTypographicTable.RuleSet rs = new AdvancedTypographicTable.HomogeneousRuleSet(new AdvancedTypographicTable.Rule[] {r}); + AdvancedTypographicTable.RuleSet[] rsa = new AdvancedTypographicTable.RuleSet[] {rs}; + // store results + assert (igca != null) && (igca.length > 0); + seMapping = igca[0]; + seEntries.add(new SERuleSetList(rsa)); + } + + private int readChainedContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readChainedContextualPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 2) { + readChainedContextualPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf); + } else if (sf == 3) { + readChainedContextualPosTableFormat3(lookupType, lookupFlags, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported chained contextual positioning subtable format: " + sf); + } + return sf; + } + + private void readExtensionPosTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read extension lookup type + int lt = data.readUnsignedShort(); + // read extension offset + long eo = data.readUnsignedInt(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " extension positioning subtable format: " + subtableFormat); + log.debug(tableTag + " extension positioning lookup type: " + lt); + log.debug(tableTag + " extension positioning lookup table offset: " + eo); + } + // read referenced subtable from extended offset + readGPOSSubtable(lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo); + } + + private int readExtensionPosTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read positioning subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readExtensionPosTableFormat1(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported extension positioning subtable format: " + sf); + } + return sf; + } + + private void readGPOSSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + initATSubState(); + int subtableFormat = -1; + switch (lookupType) { + case GPOSLookupType.SINGLE: + subtableFormat = readSinglePosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.PAIR: + subtableFormat = readPairPosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.CURSIVE: + subtableFormat = readCursivePosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.MARK_TO_BASE: + subtableFormat = readMarkToBasePosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.MARK_TO_LIGATURE: + subtableFormat = readMarkToLigaturePosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.MARK_TO_MARK: + subtableFormat = readMarkToMarkPosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.CONTEXTUAL: + subtableFormat = readContextualPosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.CHAINED_CONTEXTUAL: + subtableFormat = readChainedContextualPosTable(lookupType, lookupFlags, subtableOffset); + break; + case GPOSLookupType.EXTENSION: + subtableFormat = readExtensionPosTable(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset); + break; + default: + break; + } + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_POSITIONING, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableFormat); + resetATSubState(); + } + + private void readLookupTable(String tableTag, int lookupSequence, long lookupTable) throws IOException { + boolean isGSUB = tableTag.equals(GlyphSubstitutionTable.TAG); + boolean isGPOS = tableTag.equals(GlyphPositioningTable.TAG); + data.seek(lookupTable); + // read lookup type + int lt = data.readUnsignedShort(); + // read lookup flags + int lf = data.readUnsignedShort(); + // read sub-table count + int ns = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + String lts; + if (isGSUB) { + lts = GSUBLookupType.toString(lt); + } else if (isGPOS) { + lts = GPOSLookupType.toString(lt); + } else { + lts = "?"; + } + log.debug(tableTag + " lookup table type: " + lt + " (" + lts + ")"); + log.debug(tableTag + " lookup table flags: " + lf + " (" + LookupFlag.toString(lf) + ")"); + log.debug(tableTag + " lookup table subtable count: " + ns); + } + // read subtable offsets + int[] soa = new int[ns]; + for (int i = 0; i < ns; i++) { + int so = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup table subtable offset: " + so); + } + soa[i] = so; + } + // read mark filtering set + if ((lf & LookupFlag.USE_MARK_FILTERING_SET) != 0) { + // read mark filtering set + int fs = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup table mark filter set: " + fs); + } + } + // read subtables + for (int i = 0; i < ns; i++) { + int so = soa[i]; + if (isGSUB) { + readGSUBSubtable(lt, lf, lookupSequence, i, lookupTable + so); + } else if (isGPOS) { + readGPOSSubtable(lt, lf, lookupSequence, i, lookupTable + so); + } + } + } + + private void readLookupList(String tableTag, long lookupList) throws IOException { + data.seek(lookupList); + // read lookup record count + int nl = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup list record count: " + nl); + } + if (nl > 0) { + int[] loa = new int[nl]; + // read lookup records + for (int i = 0, n = nl; i < n; i++) { + int lo = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup table offset: " + lo); + } + loa[i] = lo; + } + // read lookup tables + for (int i = 0, n = nl; i < n; i++) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup index: " + i); + } + readLookupTable(tableTag, i, lookupList + loa [ i ]); + } + } + } + + /** + * Read the common layout tables (used by GSUB and GPOS). + * @param tableTag tag of table being read + * @param scriptList offset to script list from beginning of font file + * @param featureList offset to feature list from beginning of font file + * @param lookupList offset to lookup list from beginning of font file + * @throws IOException In case of a I/O problem + */ + private void readCommonLayoutTables(String tableTag, long scriptList, long featureList, long lookupList) throws IOException { + if (scriptList > 0) { + readScriptList(tableTag, scriptList); + } + if (featureList > 0) { + readFeatureList(tableTag, featureList); + } + if (lookupList > 0) { + readLookupList(tableTag, lookupList); + } + } + + private void readGDEFClassDefTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + data.seek(subtableOffset); + // subtable is a bare class definition table + GlyphClassTable ct = readClassDefTable(tableTag + " glyph class definition table", subtableOffset); + // store results + seMapping = ct; + // extract subtable + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.GLYPH_CLASS, 0, lookupSequence, 0, 1); + resetATSubState(); + } + + private void readGDEFAttachmentTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + data.seek(subtableOffset); + // read coverage offset + int co = data.readUnsignedShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " attachment point coverage table offset: " + co); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " attachment point coverage", subtableOffset + co); + // store results + seMapping = ct; + // extract subtable + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.ATTACHMENT_POINT, 0, lookupSequence, 0, 1); + resetATSubState(); + } + + private void readGDEFLigatureCaretTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + data.seek(subtableOffset); + // read coverage offset + int co = data.readUnsignedShort(); + // read ligature glyph count + int nl = data.readUnsignedShort(); + // read ligature glyph table offsets + int[] lgto = new int [ nl ]; + for (int i = 0; i < nl; i++) { + lgto [ i ] = data.readUnsignedShort(); + } + + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " ligature caret coverage table offset: " + co); + log.debug(tableTag + " ligature caret ligature glyph count: " + nl); + for (int i = 0; i < nl; i++) { + log.debug(tableTag + " ligature glyph table offset[" + i + "]: " + lgto[i]); + } + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable(tableTag + " ligature caret coverage", subtableOffset + co); + // store results + seMapping = ct; + // extract subtable + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.LIGATURE_CARET, 0, lookupSequence, 0, 1); + resetATSubState(); + } + + private void readGDEFMarkAttachmentTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + data.seek(subtableOffset); + // subtable is a bare class definition table + GlyphClassTable ct = readClassDefTable(tableTag + " glyph class definition table", subtableOffset); + // store results + seMapping = ct; + // extract subtable + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1); + resetATSubState(); + } + + private void readGDEFMarkGlyphsTableFormat1(String tableTag, int lookupSequence, long subtableOffset, int subtableFormat) throws IOException { + initATSubState(); + data.seek(subtableOffset); + // skip over format (already known) + data.skip(2); + // read mark set class count + int nmc = data.readUnsignedShort(); + long[] mso = new long [ nmc ]; + // read mark set coverage offsets + for (int i = 0; i < nmc; i++) { + mso [ i ] = data.readUnsignedInt(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark set subtable format: " + subtableFormat + " (glyph sets)"); + log.debug(tableTag + " mark set class count: " + nmc); + for (int i = 0; i < nmc; i++) { + log.debug(tableTag + " mark set coverage table offset[" + i + "]: " + mso[i]); + } + } + // read mark set coverage tables, one per class + GlyphCoverageTable[] msca = new GlyphCoverageTable[nmc]; + for (int i = 0; i < nmc; i++) { + msca[i] = readCoverageTable(tableTag + " mark set coverage[" + i + "]", subtableOffset + mso[i]); + } + // create combined class table from per-class coverage tables + List coverageTableList = arrayMap(msca, table -> new SEGlyphCoverageTable(table)); + GlyphClassTable ct = GlyphClassTable.createClassTable(coverageTableList); + // store results + seMapping = ct; + // extract subtable + extractSESubState(AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1); + resetATSubState(); + } + + private void readGDEFMarkGlyphsTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + data.seek(subtableOffset); + // read mark set subtable format + int sf = data.readUnsignedShort(); + if (sf == 1) { + readGDEFMarkGlyphsTableFormat1(tableTag, lookupSequence, subtableOffset, sf); + } else { + throw new AdvancedTypographicTableFormatException("unsupported mark glyph sets subtable format: " + sf); + } + } + + /** + * Read the GDEF table. + * @throws IOException In case of a I/O problem + */ + private void readGDEF() throws IOException { + String tableTag = GlyphDefinitionTable.TAG; + // Initialize temporary state + initATState(); + // Read glyph definition (GDEF) table + long version = data.readUnsignedInt(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " version: " + (version / 65536) + "." + (version % 65536)); + } + // glyph class definition table offset (may be null) + int cdo = data.readUnsignedShort(); + // attach point list offset (may be null) + int apo = data.readUnsignedShort(); + // ligature caret list offset (may be null) + int lco = data.readUnsignedShort(); + // mark attach class definition table offset (may be null) + int mao = data.readUnsignedShort(); + // mark glyph sets definition table offset (may be null) + int mgo; + if (version >= 0x00010002) { + mgo = data.readUnsignedShort(); + } else { + mgo = 0; + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " glyph class definition table offset: " + cdo); + log.debug(tableTag + " attachment point list offset: " + apo); + log.debug(tableTag + " ligature caret list offset: " + lco); + log.debug(tableTag + " mark attachment class definition table offset: " + mao); + log.debug(tableTag + " mark glyph set definitions table offset: " + mgo); + } + // initialize subtable sequence number + int seqno = 0; + // obtain offset to start of gdef table + long to = table.getOffset(); + // (optionally) read glyph class definition subtable + if (cdo != 0) { + readGDEFClassDefTable(tableTag, seqno++, to + cdo); + } + // (optionally) read glyph attachment point subtable + if (apo != 0) { + readGDEFAttachmentTable(tableTag, seqno++, to + apo); + } + // (optionally) read ligature caret subtable + if (lco != 0) { + readGDEFLigatureCaretTable(tableTag, seqno++, to + lco); + } + // (optionally) read mark attachment class subtable + if (mao != 0) { + readGDEFMarkAttachmentTable(tableTag, seqno++, to + mao); + } + // (optionally) read mark glyph sets subtable + if (mgo != 0) { + readGDEFMarkGlyphsTable(tableTag, seqno++, to + mgo); + } + initializeGDEF(); + } + + /** + * Read the GSUB table. + * @throws IOException In case of a I/O problem + */ + private void readGSUB() throws IOException { + String tableTag = GlyphSubstitutionTable.TAG; + // Initialize temporary state + initATState(); + // Read glyph substitution (GSUB) table + long version = data.readUnsignedInt(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " version: " + (version / 65536) + "." + (version % 65536)); + } + int slo = data.readUnsignedShort(); + int flo = data.readUnsignedShort(); + int llo = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script list offset: " + slo); + log.debug(tableTag + " feature list offset: " + flo); + log.debug(tableTag + " lookup list offset: " + llo); + } + long to = table.getOffset(); + readCommonLayoutTables(tableTag, to + slo, to + flo, to + llo); + initializeGSUB(); + } + + /** + * Read the GPOS table. + * @throws IOException In case of a I/O problem + */ + private void readGPOS() throws IOException { + String tableTag = GlyphPositioningTable.TAG; + // Initialize temporary state + initATState(); + // Read glyph positioning (GPOS) table + long version = data.readUnsignedInt(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " version: " + (version / 65536) + "." + (version % 65536)); + } + int slo = data.readUnsignedShort(); + int flo = data.readUnsignedShort(); + int llo = data.readUnsignedShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script list offset: " + slo); + log.debug(tableTag + " feature list offset: " + flo); + log.debug(tableTag + " lookup list offset: " + llo); + } + long to = table.getOffset(); + readCommonLayoutTables(tableTag, to + slo, to + flo, to + llo); + initializeGPOS(); + } + + /** + * Initialize the (internal representation of the) GDEF table based on previously + * parsed state. + * @returns glyph definition table or null if insufficient or invalid state + */ + private void initializeGDEF() { + List subtables; + if ((subtables = constructGDEFSubtables()) != null) { + if (subtables.size() > 0) { + if (table instanceof GlyphDefinitionTable) + ((GlyphDefinitionTable) table).initialize(subtables); + } + } + resetATState(); + } + + /** + * Initialize the (internal representation of the) GSUB table based on previously + * parsed state. + * @returns glyph substitution table or null if insufficient or invalid state + */ + private void initializeGSUB() throws IOException { + Map> lookups; + if ((lookups = constructLookups()) != null) { + List subtables; + if ((subtables = constructGSUBSubtables()) != null) { + if ((lookups.size() > 0) && (subtables.size() > 0)) { + if (table instanceof GlyphSubstitutionTable) + ((GlyphSubstitutionTable) table).initialize(otf.getGDEF(), lookups, subtables); + } + } + } + resetATState(); + } + + /** + * Initialize the (internal representation of the) GPOS table based on previously + * parsed state. + * @returns glyph positioning table or null if insufficient or invalid state + */ + private void initializeGPOS() throws IOException { + Map> lookups; + if ((lookups = constructLookups()) != null) { + List subtables; + if ((subtables = constructGPOSSubtables()) != null) { + if ((lookups.size() > 0) && (subtables.size() > 0)) { + if (table instanceof GlyphPositioningTable) + ((GlyphPositioningTable) table).initialize(otf.getGDEF(), lookups, subtables); + } + } + } + resetATState(); + } + + private void constructLookupsFeature(Map> lookups, String st, String lt, String fid) { + SubtableEntryFeature fp = seFeatures.get(fid); + if (fp != null) { + String ft = fp.featureTag; // feature tag + List lul = fp.lookupIndexes; // list of lookup table ids + if ((ft != null) && (lul != null) && (lul.size() > 0)) { + AdvancedTypographicTable.LookupSpec ls = new AdvancedTypographicTable.LookupSpec(st, lt, ft); + lookups.put(ls, lul); + } + } + } + + private void constructLookupsFeatures(Map> lookups, String st, String lt, List fids) { + fids.forEach(fid -> constructLookupsFeature(lookups, st, lt, fid)); + } + + private void constructLookupsLanguage(Map> lookups, String st, String lt, Map languages) { + SubtableEntryLanguage lp = languages.get(lt); + if (lp != null) { + if (lp.requiredFeatureId != null) { // required feature id + constructLookupsFeature(lookups, st, lt, lp.requiredFeatureId); + } + if (lp.featureIds != null) { // non-required features ids + constructLookupsFeatures(lookups, st, lt, lp.featureIds); + } + } + } + + private void constructLookupsLanguages(Map> lookups, String st, List ll, Map languages) { + ll.forEach(lt -> constructLookupsLanguage(lookups, st, lt, languages)); + } + + private Map> constructLookups() { + Map> lookups = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : seScripts.entrySet()) { + String st = entry.getKey(); + SubtableEntryScript sp = entry.getValue(); + if (sp != null) { + if (sp.defaultLanguageTag != null) { // default language + constructLookupsLanguage(lookups, st, sp.defaultLanguageTag, sp.languages); + } + if (sp.languageTags != null) { // non-default languages + constructLookupsLanguages(lookups, st, sp.languageTags, sp.languages); + } + } + } + return lookups; + } + + private List constructGDEFSubtables() { + List subtables = new java.util.ArrayList<>(); + if (seSubtables != null) { + for (SubtableState stp : seSubtables) { + GlyphSubtable st; + if ((st = constructGDEFSubtable(stp)) != null) { + subtables.add(st); + } + } + } + return subtables; + } + + private GlyphSubtable constructGDEFSubtable(SubtableState stp) { + GlyphSubtable st = null; + assert (stp != null); + Integer tt = (Integer) stp.tableType; // table type + Integer lt = (Integer) stp.lookupType; // lookup type + Integer ln = (Integer) stp.lookupSequence; // lookup sequence number + Integer lf = (Integer) stp.lookupFlags; // lookup flags + Integer sn = (Integer) stp.subtableSequence; // subtable sequence number + Integer sf = (Integer) stp.subtableFormat; // subtable format + GlyphMappingTable mapping = (GlyphMappingTable) stp.map; + List entries = stp.entries; + if (tt.intValue() == AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION) { + int type = GDEFLookupType.getSubtableType(lt.intValue()); + String lid = "lu" + ln.intValue(); + int sequence = sn.intValue(); + int flags = lf.intValue(); + int format = sf.intValue(); + st = GlyphDefinitionTable.createSubtable(type, lid, sequence, flags, format, mapping, entries); + } + return st; + } + + private List constructGSUBSubtables() { + List subtables = new java.util.ArrayList<>(); + if (seSubtables != null) { + for (SubtableState stp : seSubtables) { + GlyphSubtable st; + if ((st = constructGSUBSubtable(stp)) != null) { + subtables.add(st); + } + } + } + return subtables; + } + + private GlyphSubtable constructGSUBSubtable(SubtableState stp) { + GlyphSubtable st = null; + assert (stp != null); + Integer tt = (Integer) stp.tableType; // table type + Integer lt = (Integer) stp.lookupType; // lookup type + Integer ln = (Integer) stp.lookupSequence; // lookup sequence number + Integer lf = (Integer) stp.lookupFlags; // lookup flags + Integer sn = (Integer) stp.subtableSequence; // subtable sequence number + Integer sf = (Integer) stp.subtableFormat; // subtable format + GlyphCoverageTable coverage = (GlyphCoverageTable) stp.map; + List entries = stp.entries; + if (tt.intValue() == AdvancedTypographicTable.GLYPH_TABLE_TYPE_SUBSTITUTION) { + int type = GSUBLookupType.getSubtableType(lt.intValue()); + String lid = "lu" + ln.intValue(); + int sequence = sn.intValue(); + int flags = lf.intValue(); + int format = sf.intValue(); + st = GlyphSubstitutionTable.createSubtable(type, lid, sequence, flags, format, coverage, entries); + } + return st; + } + + private List constructGPOSSubtables() { + List subtables = new java.util.ArrayList<>(); + if (seSubtables != null) { + for (SubtableState stp : seSubtables) { + GlyphSubtable st; + if ((st = constructGPOSSubtable(stp)) != null) { + subtables.add(st); + } + } + } + return subtables; + } + + private GlyphSubtable constructGPOSSubtable(SubtableState stp) { + GlyphSubtable st = null; + assert (stp != null); + int tt = stp.tableType; // table type + int lt = stp.lookupType; // lookup type + int ln = stp.lookupSequence; // lookup sequence number + int lf = stp.lookupFlags; // lookup flags + int sn = stp.lookupSequence; // subtable sequence number + int sf = stp.subtableFormat; // subtable format + GlyphCoverageTable coverage = (GlyphCoverageTable) stp.map; + List entries = stp.entries; + if (tt == AdvancedTypographicTable.GLYPH_TABLE_TYPE_POSITIONING) { + int type = GSUBLookupType.getSubtableType(lt); + String lid = "lu" + ln; + int sequence = sn; + int flags = lf; + int format = sf; + st = GlyphPositioningTable.createSubtable(type, lid, sequence, flags, format, coverage, entries); + } + return st; + } + + private void initATState() { + seScripts = new java.util.LinkedHashMap<>(); + seLanguages = new java.util.LinkedHashMap<>(); + seFeatures = new java.util.LinkedHashMap<>(); + seSubtables = new java.util.ArrayList<>(); + resetATSubState(); + } + + private void resetATState() { + seScripts = null; + seLanguages = null; + seFeatures = null; + seSubtables = null; + resetATSubState(); + } + + private void initATSubState() { + seMapping = null; + seEntries = new java.util.ArrayList<>(); + } + + private void extractSESubState(int tableType, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, int subtableFormat) { + if (seEntries != null) { + if ((tableType == AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION) || (seEntries.size() > 0)) { + if (seSubtables != null) { + Integer tt = Integer.valueOf(tableType); + Integer lt = Integer.valueOf(lookupType); + Integer ln = Integer.valueOf(lookupSequence); + Integer lf = Integer.valueOf(lookupFlags); + Integer sn = Integer.valueOf(subtableSequence); + Integer sf = Integer.valueOf(subtableFormat); + seSubtables.add(new SubtableState(tt, lt, lf, ln, sn, sf, seMapping, seEntries)); + } + } + } + } + + private void resetATSubState() { + seMapping = null; + seEntries = null; + } + + private void resetATStateAll() { + resetATState(); + } + + /** helper method for formatting an integer array for output */ + private String toString(int[] ia) { + if ((ia == null) || (ia.length == 0)) { + return "-"; + } else { + return Arrays + .stream(ia) + .mapToObj(Integer::toString) + .collect(Collectors.joining(" ")); + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphClassMapping.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphClassMapping.java new file mode 100644 index 00000000000..fd16131ccd2 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphClassMapping.java @@ -0,0 +1,45 @@ +/* + * 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.advanced; + +/** + *

The GlyphClassMapping interface provides glyph identifier to class + * index mapping support.

+ * + * @author Glenn Adams + */ +public interface GlyphClassMapping { + + /** + * Obtain size of class table, i.e., ciMax + 1, where ciMax is the maximum + * class index. + * @param set for coverage set based class mappings, indicates set index, otherwise ignored + * @return size of class table + */ + int getClassSize(int set); + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the class table. + * @param gid glyph identifier (code) + * @param set for coverage set based class mappings, indicates set index, otherwise ignored + * @return non-negative glyph class index or -1 if glyph identifiers is not mapped by table + */ + int getClassIndex(int gid, int set); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphClassTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphClassTable.java new file mode 100644 index 00000000000..4c4a9818c57 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphClassTable.java @@ -0,0 +1,290 @@ +/* + * 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.advanced; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SEGlyphCoverageTable; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SEInteger; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SEMappingRange; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SubtableEntry; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

Base class implementation of glyph class table.

+ * + * @author Glenn Adams + */ +public final class GlyphClassTable extends GlyphMappingTable implements GlyphClassMapping { + + /** empty mapping table */ + public static final int GLYPH_CLASS_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY; + + /** mapped mapping table */ + public static final int GLYPH_CLASS_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED; + + /** range based mapping table */ + public static final int GLYPH_CLASS_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE; + + /** empty mapping table */ + public static final int GLYPH_CLASS_TYPE_COVERAGE_SET = 3; + + private GlyphClassMapping cm; + + private GlyphClassTable(GlyphClassMapping cm) { + assert cm != null; + assert cm instanceof GlyphMappingTable; + this.cm = cm; + } + + /** {@inheritDoc} */ + public int getType() { + return ((GlyphMappingTable) cm) .getType(); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return ((GlyphMappingTable) cm) .getEntries(); + } + + /** {@inheritDoc} */ + @Override + public int getClassSize(int set) { + return cm.getClassSize(set); + } + + /** {@inheritDoc} */ + @Override + public int getClassIndex(int gid, int set) { + return cm.getClassIndex(gid, set); + } + + /** + * Create glyph class table. + * @param entries list of mapped or ranged class entries, or null or empty list + * @return a new covera table instance + */ + public static GlyphClassTable createClassTable(List entries) { + GlyphClassMapping cm; + if ((entries == null) || (entries.size() == 0)) { + cm = new EmptyClassTable(entries); + } else if (isMappedClass(entries)) { + cm = new MappedClassTable(entries); + } else if (isRangeClass(entries)) { + cm = new RangeClassTable(entries); + } else if (isCoverageSetClass(entries)) { + cm = new CoverageSetClassTable(entries); + } else { + cm = null; + } + assert cm != null : "unknown class type"; + return new GlyphClassTable(cm); + } + + private static boolean isMappedClass(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + return allOfType(entries, SEInteger.class); + } + } + + private static boolean isRangeClass(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + return allOfType(entries, SEMappingRange.class); + } + } + + private static boolean isCoverageSetClass(List entries) { + if ((entries == null) || (entries.size() == 0)) { + return false; + } else { + return allOfType(entries, SEGlyphCoverageTable.class); + } + } + + private static class EmptyClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping { + public EmptyClassTable(List entries) { + super(entries); + } + + /** {@inheritDoc} */ + @Override + public int getClassSize(int set) { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getClassIndex(int gid, int set) { + return -1; + } + } + + private static class MappedClassTable extends GlyphMappingTable.MappedMappingTable implements GlyphClassMapping { + private int firstGlyph; + private int[] gca; + private int gcMax = -1; + public MappedClassTable(List entries) { + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + List entries = new java.util.ArrayList<>(); + entries.add(SEInteger.valueOf(firstGlyph)); + if (gca != null) { + for (int i = 0, n = gca.length; i < n; i++) { + entries.add(SEInteger.valueOf(gca [ i ])); + } + } + return entries; + } + + /** {@inheritDoc} */ + @Override + public int getMappingSize() { + return gcMax + 1; + } + + /** {@inheritDoc} */ + @Override + public int getMappedIndex(int gid) { + int i = gid - firstGlyph; + if ((i >= 0) && (i < gca.length)) { + return gca [ i ]; + } else { + return -1; + } + } + + /** {@inheritDoc} */ + @Override + public int getClassSize(int set) { + return getMappingSize(); + } + + /** {@inheritDoc} */ + @Override + public int getClassIndex(int gid, int set) { + return getMappedIndex(gid); + } + + private void populate(List entries) { + if (entries == null || entries.isEmpty()) { + throw new AdvancedTypographicTableFormatException("Mapped class table must contain at least one glyph"); + } + + int i = 0; + int n = entries.size() - 1; + int gcMax = -1; + int[] gca = new int [ n ]; + int firstGlyph = 0; + + for (int idx = 0; idx < entries.size(); idx++) { + if (idx == 0) { + firstGlyph = checkGet(entries, 0, SEInteger.class).get(); + } else { + // extract glyph class array + int gc = checkGet(entries, idx, SEInteger.class).get(); + gca [ i++ ] = gc; + if (gc > gcMax) { + gcMax = gc; + } + } + } + + assert i == n; + assert this.gca == null; + this.firstGlyph = firstGlyph; + this.gca = gca; + this.gcMax = gcMax; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + String prefix = "{ firstGlyph = " + firstGlyph + ", classes = {"; + return Arrays.stream(gca) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", prefix, "} }")); + } + } + + private static class RangeClassTable extends GlyphMappingTable.RangeMappingTable implements GlyphClassMapping { + public RangeClassTable(List entries) { + super(entries); + } + + /** {@inheritDoc} */ + @Override + public int getMappedIndex(int gid, int s, int m) { + return m; + } + + /** {@inheritDoc} */ + @Override + public int getClassSize(int set) { + return getMappingSize(); + } + + /** {@inheritDoc} */ + @Override + public int getClassIndex(int gid, int set) { + return getMappedIndex(gid); + } + } + + private static class CoverageSetClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping { + private static final Log LOG = LogFactory.getLog(CoverageSetClassTable.class); + public CoverageSetClassTable(List entries) { + // See FOP-2704 + // throw new UnsupportedOperationException("coverage set class table not yet supported"); + LOG.warn("coverage set class table not yet supported"); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GLYPH_CLASS_TYPE_COVERAGE_SET; + } + + /** {@inheritDoc} */ + @Override + public int getClassSize(int set) { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getClassIndex(int gid, int set) { + return -1; + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphCoverageMapping.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphCoverageMapping.java new file mode 100644 index 00000000000..b2133131d49 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphCoverageMapping.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.advanced; + +/** + *

The GlyphCoverageMapping interface provides glyph identifier to coverage + * index mapping support.

+ * + * @author Glenn Adams + */ +public interface GlyphCoverageMapping { + + /** + * Obtain size of coverage table, i.e., ciMax + 1, where ciMax is the maximum + * coverage index. + * @return size of coverage table + */ + int getCoverageSize(); + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the coverage table. + * @param gid glyph identifier (code) + * @return non-negative glyph coverage index or -1 if glyph identifiers is not mapped by table + */ + int getCoverageIndex(int gid); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphCoverageTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphCoverageTable.java new file mode 100644 index 00000000000..220e87ab46b --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphCoverageTable.java @@ -0,0 +1,235 @@ +/* + * 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.advanced; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SEInteger; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SEMappingRange; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SubtableEntry; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

.Base class implementation of glyph coverage table.

+ * + * @author Glenn Adams + */ +public final class GlyphCoverageTable extends GlyphMappingTable implements GlyphCoverageMapping { + + /* logging instance */ + private static final Log log = LogFactory.getLog(GlyphCoverageTable.class); + + /** empty mapping table */ + public static final int GLYPH_COVERAGE_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY; + + /** mapped mapping table */ + public static final int GLYPH_COVERAGE_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED; + + /** range based mapping table */ + public static final int GLYPH_COVERAGE_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE; + + private GlyphCoverageMapping cm; + + private GlyphCoverageTable(GlyphCoverageMapping cm) { + assert cm != null; + assert cm instanceof GlyphMappingTable; + this.cm = cm; + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return ((GlyphMappingTable) cm) .getType(); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return ((GlyphMappingTable) cm) .getEntries(); + } + + /** {@inheritDoc} */ + @Override + public int getCoverageSize() { + return cm.getCoverageSize(); + } + + /** {@inheritDoc} */ + @Override + public int getCoverageIndex(int gid) { + return cm.getCoverageIndex(gid); + } + + /** + * Create glyph coverage table. + * @param entries list of mapped or ranged coverage entries, or null or empty list + * @return a new covera table instance + */ + public static GlyphCoverageTable createCoverageTable(List entries) { + GlyphCoverageMapping cm; + if ((entries == null) || (entries.size() == 0)) { + cm = new EmptyCoverageTable(entries); + } else if (isMappedCoverage(entries)) { + cm = new MappedCoverageTable(entries); + } else if (isRangeCoverage(entries)) { + cm = new RangeCoverageTable(entries); + } else { + cm = null; + } + assert cm != null : "unknown coverage type"; + return new GlyphCoverageTable(cm); + } + + private static boolean isMappedCoverage(List entries) { + if ((entries == null) || (entries.isEmpty())) { + return false; + } else { + return allOfType(entries, SEInteger.class); + } + } + + private static boolean isRangeCoverage(List entries) { + if ((entries == null) || (entries.isEmpty())) { + return false; + } else { + return allOfType(entries, SEMappingRange.class); + } + } + + private static class EmptyCoverageTable extends GlyphMappingTable.EmptyMappingTable implements GlyphCoverageMapping { + public EmptyCoverageTable(List entries) { + super(entries); + } + + /** {@inheritDoc} */ + @Override + public int getCoverageSize() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getCoverageIndex(int gid) { + return -1; + } + } + + private static class MappedCoverageTable extends GlyphMappingTable.MappedMappingTable implements GlyphCoverageMapping { + private int[] map; + public MappedCoverageTable(List entries) { + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return arrayMap(map, SEInteger::valueOf); + } + + /** {@inheritDoc} */ + @Override + public int getMappingSize() { + return (map != null) ? map.length : 0; + } + + /** {@inheritDoc} */ + @Override + public int getMappedIndex(int gid) { + int i; + if ((i = Arrays.binarySearch(map, gid)) >= 0) { + return i; + } else { + return -1; + } + } + + /** {@inheritDoc} */ + @Override + public int getCoverageSize() { + return getMappingSize(); + } + + /** {@inheritDoc} */ + @Override + public int getCoverageIndex(int gid) { + return getMappedIndex(gid); + } + + private void populate(List entries) { + int i = 0; + int skipped = 0; + int n = entries.size(); + int gidMax = -1; + int[] map = new int [ n ]; + + for (int idx = 0; idx < n; idx++) { + int gid = checkGet(entries, idx, SEInteger.class).get(); + checkGidRange(gid, () -> "illegal glyph index: " + gid); + + if (gid > gidMax) { + map [ i++ ] = gidMax = gid; + } else { + log.info("ignoring out of order or duplicate glyph index: " + gid); + skipped++; + } + } + + assert (i + skipped) == n; + assert this.map == null; + this.map = map; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return Arrays.stream(map) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + } + } + + private static class RangeCoverageTable extends GlyphMappingTable.RangeMappingTable implements GlyphCoverageMapping { + public RangeCoverageTable(List entries) { + super(entries); + } + + /** {@inheritDoc} */ + @Override + public int getMappedIndex(int gid, int s, int m) { + return m + gid - s; + } + + /** {@inheritDoc} */ + @Override + public int getCoverageSize() { + return getMappingSize(); + } + + /** {@inheritDoc} */ + @Override + public int getCoverageIndex(int gid) { + return getMappedIndex(gid); + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinition.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinition.java new file mode 100644 index 00000000000..19e863e22d0 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinition.java @@ -0,0 +1,35 @@ +/* + * 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.advanced; + +/** + *

The GlyphDefinition interface is a marker interface implemented by a glyph definition + * subtable.

+ * + * @author Glenn Adams + */ +public interface GlyphDefinition { + + /** + * Determine if some definition is available for a specific glyph. + * @param gi a glyph index + * @return true if some (unspecified) definition is available for the specified glyph + */ + boolean hasDefinition(int gi); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinitionSubtable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinitionSubtable.java new file mode 100644 index 00000000000..c039d38c7da --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinitionSubtable.java @@ -0,0 +1,72 @@ +/* + * 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.advanced; + +/** + *

The GlyphDefinitionSubtable implements an abstract base of a glyph definition subtable, + * providing a default implementation of the GlyphDefinition interface.

+ * + * @author Glenn Adams + */ +public abstract class GlyphDefinitionSubtable extends GlyphSubtable implements GlyphDefinition { + + /** + * Instantiate a GlyphDefinitionSubtable. + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param mapping subtable coverage table + */ + protected GlyphDefinitionSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping) { + super(id, sequence, flags, format, mapping); + } + + /** {@inheritDoc} */ + public int getTableType() { + return AdvancedTypographicTable.GLYPH_TABLE_TYPE_DEFINITION; + } + + /** {@inheritDoc} */ + public String getTypeName() { + return GlyphDefinitionTable.getLookupTypeName(getType()); + } + + /** {@inheritDoc} */ + public boolean usesReverseScan() { + return false; + } + + /** {@inheritDoc} */ + public boolean hasDefinition(int gi) { + GlyphCoverageMapping cvm; + if ((cvm = getCoverage()) != null) { + if (cvm.getCoverageIndex(gi) >= 0) { + return true; + } + } + GlyphClassMapping clm; + if ((clm = getClasses()) != null) { + if (clm.getClassIndex(gi, 0) >= 0) { + return true; + } + } + return false; + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinitionTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinitionTable.java new file mode 100644 index 00000000000..58e03844062 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphDefinitionTable.java @@ -0,0 +1,506 @@ +/* + * 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.advanced; + +import java.io.IOException; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.OpenTypeFont; +import org.apache.fontbox.ttf.TTFDataStream; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SubtableEntry; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; +import org.apache.fontbox.ttf.advanced.scripts.ScriptProcessor; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; + +/** + *

The GlyphDefinitionTable class is a glyph table that implements + * glyph definition functionality according to the OpenType GDEF table.

+ * + *

Adapted from the Apache FOP Project.

+ * + * @author Glenn Adams + */ +public class GlyphDefinitionTable extends AdvancedTypographicTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphDefinitionTable.class); + + /** tag that identifies this table type */ + public static final String TAG = "GDEF"; + + /** glyph class subtable type */ + public static final int GDEF_LOOKUP_TYPE_GLYPH_CLASS = 1; + /** attachment point subtable type */ + public static final int GDEF_LOOKUP_TYPE_ATTACHMENT_POINT = 2; + /** ligature caret subtable type */ + public static final int GDEF_LOOKUP_TYPE_LIGATURE_CARET = 3; + /** mark attachment subtable type */ + public static final int GDEF_LOOKUP_TYPE_MARK_ATTACHMENT = 4; + + /** pre-defined glyph class - base glyph */ + public static final int GLYPH_CLASS_BASE = 1; + /** pre-defined glyph class - ligature glyph */ + public static final int GLYPH_CLASS_LIGATURE = 2; + /** pre-defined glyph class - mark glyph */ + public static final int GLYPH_CLASS_MARK = 3; + /** pre-defined glyph class - component glyph */ + public static final int GLYPH_CLASS_COMPONENT = 4; + + /** singleton glyph class table */ + private GlyphClassSubtable gct; + /** singleton attachment point table */ + // private AttachmentPointSubtable apt; // NOT YET USED + /** singleton ligature caret table */ + // private LigatureCaretSubtable lct; // NOT YET USED + /** singleton mark attachment table */ + private MarkAttachmentSubtable mat; + + public GlyphDefinitionTable(OpenTypeFont otf) { + super(otf, null, new java.util.HashMap<>(0)); + } + + /** + * Initialize a GlyphDefinitionTable object using the specified subtables. + * @param subtables a list of identified subtables + */ + public GlyphDefinitionTable initialize(List subtables) { + if ((subtables == null) || (subtables.isEmpty())) { + throw new AdvancedTypographicTableFormatException("subtables must be non-empty"); + } else { + for (GlyphSubtable o : subtables) { + if (o instanceof GlyphDefinitionSubtable) { + addSubtable(o); + } else { + throw new AdvancedTypographicTableFormatException("subtable must be a glyph definition subtable"); + } + } + + freezeSubtables(); + return this; + } + } + + @Override + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + if (ttf instanceof AdvancedOpenTypeFont) { + new AdvancedTypographicTableReader((AdvancedOpenTypeFont) ttf, this, data).read(); + this.initialized = true; + } + } + + /** + * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base glyph. + * @param gs an input glyph sequence + * @param widths associated advance widths (also reordered) + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @return the reordered (output) glyph sequence + */ + public GlyphSequence reorderCombiningMarks(GlyphSequence gs, int[] widths, int[][] gpa, String script, String language, Object[][] features) { + ScriptProcessor sp = ScriptProcessor.getInstance(script); + return sp.reorderCombiningMarks(this, gs, widths, gpa, script, language, features); + } + + /** {@inheritDoc} */ + @Override + protected void addSubtable(GlyphSubtable subtable) { + if (subtable instanceof GlyphClassSubtable) { + this.gct = (GlyphClassSubtable) subtable; + } else if (subtable instanceof AttachmentPointSubtable) { + // TODO - not yet used + // this.apt = (AttachmentPointSubtable) subtable; + } else if (subtable instanceof LigatureCaretSubtable) { + // TODO - not yet used + // this.lct = (LigatureCaretSubtable) subtable; + } else if (subtable instanceof MarkAttachmentSubtable) { + this.mat = (MarkAttachmentSubtable) subtable; + } else { + throw new UnsupportedOperationException("unsupported glyph definition subtable type: " + subtable); + } + } + + /** + * Determine if glyph belongs to pre-defined glyph class. + * @param gid a glyph identifier (index) + * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + * @return true if glyph belongs to specified glyph class + */ + public boolean isGlyphClass(int gid, int gc) { + if (gct != null) { + return gct.isGlyphClass(gid, gc); + } else { + return false; + } + } + + /** + * Determine glyph class. + * @param gid a glyph identifier (index) + * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + */ + public int getGlyphClass(int gid) { + if (gct != null) { + return gct.getGlyphClass(gid); + } else { + return -1; + } + } + + /** + * Determine if glyph belongs to (font specific) mark attachment class. + * @param gid a glyph identifier (index) + * @param mac a (font specific) mark attachment class + * @return true if glyph belongs to specified mark attachment class + */ + public boolean isMarkAttachClass(int gid, int mac) { + if (mat != null) { + return mat.isMarkAttachClass(gid, mac); + } else { + return false; + } + } + + /** + * Determine mark attachment class. + * @param gid a glyph identifier (index) + * @return a non-negative mark attachment class, or -1 if no class defined + */ + public int getMarkAttachClass(int gid) { + if (mat != null) { + return mat.getMarkAttachClass(gid); + } else { + return -1; + } + } + + /** + * Map a lookup type name to its constant (integer) value. + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName(String name) { + int t; + String s = name.toLowerCase(); + if ("glyphclass".equals(s)) { + t = GDEF_LOOKUP_TYPE_GLYPH_CLASS; + } else if ("attachmentpoint".equals(s)) { + t = GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + } else if ("ligaturecaret".equals(s)) { + t = GDEF_LOOKUP_TYPE_LIGATURE_CARET; + } else if ("markattachment".equals(s)) { + t = GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName(int type) { + String tn = null; + switch (type) { + case GDEF_LOOKUP_TYPE_GLYPH_CLASS: + tn = "glyphclass"; + break; + case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT: + tn = "attachmentpoint"; + break; + case GDEF_LOOKUP_TYPE_LIGATURE_CARET: + tn = "ligaturecaret"; + break; + case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT: + tn = "markattachment"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a definition subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags (must be zero) + * @param format subtable format + * @param mapping subtable mapping table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + GlyphSubtable st = null; + switch (type) { + case GDEF_LOOKUP_TYPE_GLYPH_CLASS: + st = GlyphClassSubtable.create(id, sequence, flags, format, mapping, entries); + break; + case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT: + st = AttachmentPointSubtable.create(id, sequence, flags, format, mapping, entries); + break; + case GDEF_LOOKUP_TYPE_LIGATURE_CARET: + st = LigatureCaretSubtable.create(id, sequence, flags, format, mapping, entries); + break; + case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT: + st = MarkAttachmentSubtable.create(id, sequence, flags, format, mapping, entries); + break; + default: + break; + } + return st; + } + + private abstract static class GlyphClassSubtable extends GlyphDefinitionSubtable { + GlyphClassSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GDEF_LOOKUP_TYPE_GLYPH_CLASS; + } + + /** + * Determine if glyph belongs to pre-defined glyph class. + * @param gid a glyph identifier (index) + * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + * @return true if glyph belongs to specified glyph class + */ + public abstract boolean isGlyphClass(int gid, int gc); + + /** + * Determine glyph class. + * @param gid a glyph identifier (index) + * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + */ + public abstract int getGlyphClass(int gid); + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + if (format == 1) { + return new GlyphClassSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class GlyphClassSubtableFormat1 extends GlyphClassSubtable { + GlyphClassSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof GlyphClassSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean isGlyphClass(int gid, int gc) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0) == gc; + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int getGlyphClass(int gid) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0); + } else { + return -1; + } + } + } + + private abstract static class AttachmentPointSubtable extends GlyphDefinitionSubtable { + AttachmentPointSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + } + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + if (format == 1) { + return new AttachmentPointSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class AttachmentPointSubtableFormat1 extends AttachmentPointSubtable { + AttachmentPointSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof AttachmentPointSubtable; + } + } + + private abstract static class LigatureCaretSubtable extends GlyphDefinitionSubtable { + LigatureCaretSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GDEF_LOOKUP_TYPE_LIGATURE_CARET; + } + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + if (format == 1) { + return new LigatureCaretSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class LigatureCaretSubtableFormat1 extends LigatureCaretSubtable { + LigatureCaretSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof LigatureCaretSubtable; + } + } + + private abstract static class MarkAttachmentSubtable extends GlyphDefinitionSubtable { + MarkAttachmentSubtable(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + } + + /** + * Determine if glyph belongs to (font specific) mark attachment class. + * @param gid a glyph identifier (index) + * @param mac a (font specific) mark attachment class + * @return true if glyph belongs to specified mark attachment class + */ + public abstract boolean isMarkAttachClass(int gid, int mac); + /** + * Determine mark attachment class. + * @param gid a glyph identifier (index) + * @return a non-negative mark attachment class, or -1 if no class defined + */ + public abstract int getMarkAttachClass(int gid); + + static GlyphDefinitionSubtable create(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + if (format == 1) { + return new MarkAttachmentSubtableFormat1(id, sequence, flags, format, mapping, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkAttachmentSubtableFormat1 extends MarkAttachmentSubtable { + MarkAttachmentSubtableFormat1(String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries) { + super(id, sequence, flags, format, mapping, entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof MarkAttachmentSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean isMarkAttachClass(int gid, int mac) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0) == mac; + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int getMarkAttachClass(int gid) { + GlyphClassMapping cm = getClasses(); + if (cm != null) { + return cm.getClassIndex(gid, 0); + } else { + return -1; + } + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphMappingTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphMappingTable.java new file mode 100644 index 00000000000..d9190337527 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphMappingTable.java @@ -0,0 +1,328 @@ +/* + * 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.advanced; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SEMappingRange; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SubtableEntry; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

Base class implementation of glyph mapping table. This base + * class maps glyph indices to arbitrary integers (mappping indices), and + * is used to implement both glyph coverage and glyph class maps.

+ * + * @author Glenn Adams + */ +public class GlyphMappingTable { + + /** empty mapping table */ + public static final int GLYPH_MAPPING_TYPE_EMPTY = 0; + + /** mapped mapping table */ + public static final int GLYPH_MAPPING_TYPE_MAPPED = 1; + + /** range based mapping table */ + public static final int GLYPH_MAPPING_TYPE_RANGE = 2; + + /** + * Obtain mapping type. + * @return mapping format type + */ + public int getType() { + return -1; + } + + /** + * Obtain mapping entries. + * @return list of mapping entries + */ + public List getEntries() { + return null; + } + + /** + * Obtain size of mapping table, i.e., ciMax + 1, where ciMax is the maximum + * mapping index. + * @return size of mapping table + */ + public int getMappingSize() { + return 0; + } + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the mapping table. + * @param gid glyph identifier (code) + * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table + */ + public int getMappedIndex(int gid) { + return -1; + } + + /** empty mapping table base class */ + protected static class EmptyMappingTable extends GlyphMappingTable { + /** + * Construct empty mapping table. + */ + public EmptyMappingTable() { + this(null); + } + + /** + * Construct empty mapping table with entries (ignored). + * @param entries list of entries (ignored) + */ + public EmptyMappingTable(List entries) { + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GLYPH_MAPPING_TYPE_EMPTY; + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return new java.util.ArrayList<>(); + } + + /** {@inheritDoc} */ + @Override + public int getMappingSize() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getMappedIndex(int gid) { + return -1; + } + } + + /** mapped mapping table base class */ + protected static class MappedMappingTable extends GlyphMappingTable { + /** + * Construct mapped mapping table. + */ + public MappedMappingTable() { + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GLYPH_MAPPING_TYPE_MAPPED; + } + } + + /** range mapping table base class */ + protected abstract static class RangeMappingTable extends GlyphMappingTable { + private int[] sa; // array of range (inclusive) starts + private int[] ea; // array of range (inclusive) ends + private int[] ma; // array of range mapped values + private int miMax = -1; + /** + * Construct range mapping table. + * @param entries of mapping ranges + */ + public RangeMappingTable(List entries) { + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GLYPH_MAPPING_TYPE_RANGE; + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (sa != null) { + return rangeMap(sa.length, (i) -> new SEMappingRange(new MappingRange(sa [ i ], ea [ i ], ma [ i ]))); + } + return new ArrayList<>(); + } + + /** {@inheritDoc} */ + @Override + public int getMappingSize() { + return miMax + 1; + } + + /** {@inheritDoc} */ + @Override + public int getMappedIndex(int gid) { + int i; + int mi; + if ((i = Arrays.binarySearch(sa, gid)) >= 0) { + mi = getMappedIndex(gid, sa [ i ], ma [ i ]); // matches start of (some) range + } else if ((i = -(i + 1)) == 0) { + mi = -1; // precedes first range + } else if (gid > ea [ --i ]) { + mi = -1; // follows preceding (or last) range + } else { + mi = getMappedIndex(gid, sa [ i ], ma [ i ]); // intersects (some) range + } + return mi; + } + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the mapping table. + * @param gid glyph identifier (code) + * @param s start of range + * @param m mapping value + * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table + */ + public abstract int getMappedIndex(int gid, int s, int m); + + private void populate(List entries) { + int n = entries.size(); + int gidMax = -1; + int miMax = -1; + int[] sa = new int [ n ]; + int[] ea = new int [ n ]; + int[] ma = new int [ n ]; + + for (int idx = 0; idx < n; idx++) { + MappingRange r = checkGet(entries, idx, SEMappingRange.class).get(); + + int gs = r.getStart(); + int ge = r.getEnd(); + int mi = r.getIndex(); + + checkGidRange(gs, () -> "illegal glyph range: [" + gs + "," + ge + "]: bad start index"); + checkGidRange(ge, () -> "illegal glyph range: [" + gs + "," + ge + "]: bad end index"); + checkCondition(gs, notGt(ge), () -> "illegal glyph range: [" + gs + "," + ge + "]: start index exceeds end index"); + checkCondition(gs, notLt(gidMax), () -> "out of order glyph range: [" + gs + "," + ge + "]"); + checkCondition(mi, notLt(0), () -> "illegal mapping index: " + mi); + + int miLast; + sa [ idx ] = gs; + ea [ idx ] = gidMax = ge; + ma [ idx ] = mi; + + if ((miLast = mi + (ge - gs)) > miMax) { + miMax = miLast; + } + } + + assert this.sa == null; + assert this.ea == null; + assert this.ma == null; + this.sa = sa; + this.ea = ea; + this.ma = ma; + this.miMax = miMax; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return IntStream.range(0, sa.length) + .mapToObj(i -> '[' + sa [ i ] + ea [ i ] + "]:" + ma [ i ]) + .collect(Collectors.joining(",", "{", "}")); + } + } + + /** + * The MappingRange class encapsulates a glyph [start,end] range and + * a mapping index. + */ + public static class MappingRange { + + private final int gidStart; // first glyph in range (inclusive) + private final int gidEnd; // last glyph in range (inclusive) + private final int index; // mapping index; + + /** + * Instantiate a mapping range. + */ + public MappingRange() { + this (0, 0, 0); + } + + /** + * Instantiate a specific mapping range. + * @param gidStart start of range + * @param gidEnd end of range + * @param index mapping index + */ + public MappingRange(int gidStart, int gidEnd, int index) { + if ((gidStart < 0) || (gidEnd < 0) || (index < 0)) { + throw new AdvancedTypographicTableFormatException(); + } else if (gidStart > gidEnd) { + throw new AdvancedTypographicTableFormatException(); + } else { + this.gidStart = gidStart; + this.gidEnd = gidEnd; + this.index = index; + } + } + + /** @return start of range */ + public int getStart() { + return gidStart; + } + + /** @return end of range */ + public int getEnd() { + return gidEnd; + } + + /** @return mapping index */ + public int getIndex() { + return index; + } + + /** @return interval as a pair of integers */ + public int[] getInterval() { + return new int[] { gidStart, gidEnd }; + } + + /** + * Obtain interval, filled into first two elements of specified array. + * @param interval an array of length two + * @return interval as a pair of integers, filled into specified array + */ + public int[] getInterval(int[] interval) { + if ((interval == null) || (interval.length != 2)) { + throw new IllegalArgumentException(); + } else { + interval[0] = gidStart; + interval[1] = gidEnd; + } + return interval; + } + + /** @return length of interval */ + public int getLength() { + return gidStart - gidEnd; + } + + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioning.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioning.java new file mode 100644 index 00000000000..c279ce5875c --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioning.java @@ -0,0 +1,40 @@ +/* + * 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.advanced; + +/** + *

The GlyphPositioning interface is implemented by a glyph positioning subtable + * that supports the determination of glyph positioning information based on script and + * language of the corresponding character content.

+ * + * @author Glenn Adams + */ +public interface GlyphPositioning { + + /** + * Perform glyph positioning at the current index, mutating the positioning state object as required. + * Only the context associated with the current index is processed. + * @param ps glyph positioning state object + * @return true if the glyph subtable applies, meaning that the current context matches the + * associated input context glyph coverage table; note that returning true does not mean any position + * adjustment occurred; it only means that no further glyph subtables for the current lookup table + * should be applied. + */ + boolean position(GlyphPositioningState ps); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningState.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningState.java new file mode 100644 index 00000000000..364eee262c4 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningState.java @@ -0,0 +1,230 @@ +/* + * 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.advanced; + +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +/** + *

The GlyphPositioningState implements an state object used during glyph positioning + * processing.

+ * + * @author Glenn Adams + */ + +public class GlyphPositioningState extends GlyphProcessingState { + + /** font size */ + private int fontSize; + /** default advancements */ + private int[] widths; + /** current adjustments */ + private int[][] adjustments; + /** if true, then some adjustment was applied */ + private boolean adjusted; + + /** + * Construct default (reset) glyph positioning state. + */ + public GlyphPositioningState() { + } + + /** + * Construct glyph positioning state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param fontSize font size (in micropoints) + * @param widths array of design advancements (in glyph index order) + * @param adjustments positioning adjustments to which positioning is applied + * @param sct script context tester (or null) + */ + public GlyphPositioningState(GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) { + super(gs, script, language, feature, sct); + this.fontSize = fontSize; + this.widths = widths; + this.adjustments = adjustments; + } + + /** + * Construct glyph positioning state using an existing state object using shallow copy + * except as follows: input glyph sequence is copied deep except for its characters array. + * @param ps existing positioning state to copy from + */ + public GlyphPositioningState(GlyphPositioningState ps) { + super(ps); + this.fontSize = ps.fontSize; + this.widths = ps.widths; + this.adjustments = ps.adjustments; + } + + /** + * Reset glyph positioning state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param fontSize font size (in micropoints) + * @param widths array of design advancements (in glyph index order) + * @param adjustments positioning adjustments to which positioning is applied + * @param sct script context tester (or null) + */ + public GlyphPositioningState reset(GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct) { + super.reset(gs, script, language, feature, sct); + this.fontSize = fontSize; + this.widths = widths; + this.adjustments = adjustments; + this.adjusted = false; + return this; + } + + /** + * Obtain design advancement (width) of glyph at specified index. + * @param gi glyph index + * @return design advancement, or zero if glyph index is not present + */ + public int getWidth(int gi) { + if ((widths != null) && (gi < widths.length)) { + return widths [ gi ]; + } else { + return 0; + } + } + + /** + * Perform adjustments at current position index. + * @param v value containing adjustments + * @return true if a non-zero adjustment was made + */ + public boolean adjust(GlyphPositioningTable.Value v) { + return adjust(v, 0); + } + + /** + * Perform adjustments at specified offset from current position index. + * @param v value containing adjustments + * @param offset from current position index + * @return true if a non-zero adjustment was made + */ + public boolean adjust(GlyphPositioningTable.Value v, int offset) { + assert v != null; + if ((index + offset) < indexLast) { + return v.adjust(adjustments [ index + offset ], fontSize); + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Obtain current adjustments at current position index. + * @return array of adjustments (int[4]) at current position + */ + public int[] getAdjustment() { + return getAdjustment(0); + } + + /** + * Obtain current adjustments at specified offset from current position index. + * @param offset from current position index + * @return array of adjustments (int[4]) at specified offset + * @throws IndexOutOfBoundsException if offset is invalid + */ + public int[] getAdjustment(int offset) throws IndexOutOfBoundsException { + if ((index + offset) < indexLast) { + return adjustments [ index + offset ]; + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Apply positioning subtable to current state at current position (only), + * resulting in the consumption of zero or more input glyphs. + * @param st the glyph positioning subtable to apply + * @return true if subtable applied, or false if it did not (e.g., its + * input coverage table did not match current input context) + */ + public boolean apply(GlyphPositioningSubtable st) { + assert st != null; + updateSubtableState(st); + boolean applied = st.position(this); + return applied; + } + + /** + * Apply a sequence of matched rule lookups to the nig input glyphs + * starting at the current position. If lookups are non-null and non-empty, then + * all input glyphs specified by nig are consumed irregardless of + * whether any specified lookup applied. + * @param lookups array of matched lookups (or null) + * @param nig number of glyphs in input sequence, starting at current position, to which + * the lookups are to apply, and to be consumed once the application has finished + * @return true if lookups are non-null and non-empty; otherwise, false + */ + public boolean apply(AdvancedTypographicTable.RuleLookup[] lookups, int nig) { + if ((lookups != null) && (lookups.length > 0)) { + // apply each rule lookup to extracted input glyph array + for (int i = 0, n = lookups.length; i < n; i++) { + AdvancedTypographicTable.RuleLookup l = lookups [ i ]; + if (l != null) { + AdvancedTypographicTable.LookupTable lt = l.getLookup(); + if (lt != null) { + // perform positioning on a copy of previous state + GlyphPositioningState ps = new GlyphPositioningState(this); + // apply lookup table positioning + if (lt.position(ps, l.getSequenceIndex())) { + setAdjusted(true); + } + } + } + } + consume(nig); + return true; + } else { + return false; + } + } + + /** + * Apply default application semantices; namely, consume one input glyph. + */ + @Override + public void applyDefault() { + super.applyDefault(); + } + + /** + * Set adjusted state, used to record effect of non-zero adjustment. + * @param adjusted true if to set adjusted state, otherwise false to + * clear adjusted state + */ + public void setAdjusted(boolean adjusted) { + this.adjusted = adjusted; + } + + /** + * Get adjusted state. + * @return adjusted true if some non-zero adjustment occurred and + * was recorded by {@link #setAdjusted}; otherwise, false. + */ + public boolean getAdjusted() { + return adjusted; + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningSubtable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningSubtable.java new file mode 100644 index 00000000000..ec6774c8656 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningSubtable.java @@ -0,0 +1,131 @@ +/* + * 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.advanced; + +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +/** + *

The GlyphPositioningSubtable implements an abstract base of a glyph subtable, + * providing a default implementation of the GlyphPositioning interface.

+ * + * @author Glenn Adams + */ +public abstract class GlyphPositioningSubtable extends GlyphSubtable implements GlyphPositioning { + + private static final GlyphPositioningState STATE = new GlyphPositioningState(); + + /** + * Instantiate a GlyphPositioningSubtable. + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + */ + protected GlyphPositioningSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getTableType() { + return AdvancedTypographicTable.GLYPH_TABLE_TYPE_POSITIONING; + } + + /** {@inheritDoc} */ + @Override + public String getTypeName() { + return GlyphPositioningTable.getLookupTypeName(getType()); + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof GlyphPositioningSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean usesReverseScan() { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + return false; + } + + /** + * Apply positioning using specified state and subtable array. For each position in input sequence, + * apply subtables in order until some subtable applies or none remain. If no subtable applied or no + * input was consumed for a given position, then apply default action (no adjustments and advance). + * If sequenceIndex is non-negative, then apply subtables only when current position + * matches sequenceIndex in relation to the starting position. Furthermore, upon + * successful application at sequenceIndex, then discontinue processing the remaining + * @param ps positioning state + * @param sta array of subtables to apply + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return true if a non-zero adjustment occurred + */ + public static final boolean position(GlyphPositioningState ps, GlyphPositioningSubtable[] sta, int sequenceIndex) { + int sequenceStart = ps.getPosition(); + boolean appliedOneShot = false; + while (ps.hasNext()) { + boolean applied = false; + if (!appliedOneShot && ps.maybeApplicable()) { + for (int i = 0, n = sta.length; !applied && (i < n); i++) { + if (sequenceIndex < 0) { + applied = ps.apply(sta [ i ]); + } else if (ps.getPosition() == (sequenceStart + sequenceIndex)) { + applied = ps.apply(sta [ i ]); + if (applied) { + appliedOneShot = true; + } + } + } + } + if (!applied || !ps.didConsume()) { + ps.applyDefault(); + } + ps.next(); + } + return ps.getAdjusted(); + } + + /** + * Apply positioning. + * @param gs input glyph sequence + * @param script tag + * @param language tag + * @param feature tag + * @param fontSize the font size + * @param sta subtable array + * @param widths array + * @param adjustments array (receives output adjustments) + * @param sct script context tester + * @return true if a non-zero adjustment occurred + */ + public static final boolean position(GlyphSequence gs, String script, String language, String feature, int fontSize, GlyphPositioningSubtable[] sta, int[] widths, int[][] adjustments, ScriptContextTester sct) { + synchronized (STATE) { + return position(STATE.reset(gs, script, language, feature, fontSize, widths, adjustments, sct), sta, -1); + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningTable.java new file mode 100644 index 00000000000..57f6dbc69df --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphPositioningTable.java @@ -0,0 +1,2247 @@ +/* + * 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.advanced; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.OpenTypeFont; +import org.apache.fontbox.ttf.TTFDataStream; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.*; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; +import org.apache.fontbox.ttf.advanced.scripts.ScriptProcessor; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.GlyphTester; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

The GlyphPositioningTable class is a glyph table that implements + * GlyphPositioning functionality.

+ * + *

Adapted from the Apache FOP Project.

+ * + * @author Glenn Adams + */ +public class GlyphPositioningTable extends AdvancedTypographicTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphPositioningTable.class); + + /** tag that identifies this table type */ + public static final String TAG = "GPOS"; + + /** single positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_SINGLE = 1; + /** multiple positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_PAIR = 2; + /** cursive positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_CURSIVE = 3; + /** mark to base positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_MARK_TO_BASE = 4; + /** mark to ligature positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE = 5; + /** mark to mark positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_MARK_TO_MARK = 6; + /** contextual positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_CONTEXTUAL = 7; + /** chained contextual positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 8; + /** extension positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING = 9; + + public GlyphPositioningTable(OpenTypeFont otf) { + super(otf, null, new java.util.HashMap<>(0)); + } + + /** + * Initialize this GlyphPositioningTable object using the specified lookups + * and subtables. + * @param gdef glyph definition table that applies + * @param lookups a map of lookup specifications to subtable identifier strings + * @param subtables a list of identified subtables + */ + public GlyphPositioningTable initialize(GlyphDefinitionTable gdef, Map> lookups, List subtables) { + setGdef(gdef); + initialize(lookups); + if ((subtables == null) || (subtables.size() == 0)) { + throw new AdvancedTypographicTableFormatException("subtables must be non-empty"); + } else { + for (GlyphSubtable o : subtables) { + if (o instanceof GlyphPositioningSubtable) { + addSubtable(o); + } else { + throw new AdvancedTypographicTableFormatException("subtable must be a glyph positioning subtable"); + } + } + freezeSubtables(); + return this; + } + } + + @Override + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + if (ttf instanceof AdvancedOpenTypeFont) { + new AdvancedTypographicTableReader((AdvancedOpenTypeFont) ttf, this, data).read(); + this.initialized = true; + } + } + + /** + * Map a lookup type name to its constant (integer) value. + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName(String name) { + int t; + String s = name.toLowerCase(); + if ("single".equals(s)) { + t = GPOS_LOOKUP_TYPE_SINGLE; + } else if ("pair".equals(s)) { + t = GPOS_LOOKUP_TYPE_PAIR; + } else if ("cursive".equals(s)) { + t = GPOS_LOOKUP_TYPE_CURSIVE; + } else if ("marktobase".equals(s)) { + t = GPOS_LOOKUP_TYPE_MARK_TO_BASE; + } else if ("marktoligature".equals(s)) { + t = GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE; + } else if ("marktomark".equals(s)) { + t = GPOS_LOOKUP_TYPE_MARK_TO_MARK; + } else if ("contextual".equals(s)) { + t = GPOS_LOOKUP_TYPE_CONTEXTUAL; + } else if ("chainedcontextual".equals(s)) { + t = GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } else if ("extensionpositioning".equals(s)) { + t = GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName(int type) { + String tn; + switch (type) { + case GPOS_LOOKUP_TYPE_SINGLE: + tn = "single"; + break; + case GPOS_LOOKUP_TYPE_PAIR: + tn = "pair"; + break; + case GPOS_LOOKUP_TYPE_CURSIVE: + tn = "cursive"; + break; + case GPOS_LOOKUP_TYPE_MARK_TO_BASE: + tn = "marktobase"; + break; + case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE: + tn = "marktoligature"; + break; + case GPOS_LOOKUP_TYPE_MARK_TO_MARK: + tn = "marktomark"; + break; + case GPOS_LOOKUP_TYPE_CONTEXTUAL: + tn = "contextual"; + break; + case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + tn = "chainedcontextual"; + break; + case GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING: + tn = "extensionpositioning"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a positioning subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + GlyphSubtable st = null; + switch (type) { + case GPOS_LOOKUP_TYPE_SINGLE: + st = SingleSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_PAIR: + st = PairSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_CURSIVE: + st = CursiveSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_MARK_TO_BASE: + st = MarkToBaseSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE: + st = MarkToLigatureSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_MARK_TO_MARK: + st = MarkToMarkSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_CONTEXTUAL: + st = ContextualSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + st = ChainedContextualSubtable.create(id, sequence, flags, format, coverage, entries); + break; + default: + break; + } + return st; + } + + /** + * Create a positioning subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage list of coverage table entries + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, List coverage, List entries) { + return createSubtable(type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable(coverage), entries); + } + + /** + * Perform positioning processing using all matching lookups. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param widths array of default advancements for each glyph + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position(GlyphSequence gs, String script, String language, Object[][] features, int fontSize, int[] widths, int[][] adjustments) { + Map> lookups = matchLookups(script, language, "*"); + if ((lookups != null) && (lookups.size() > 0)) { + ScriptProcessor sp = ScriptProcessor.getInstance(script); + return sp.position(this, gs, script, language, features, fontSize, lookups, widths, adjustments); + } else { + return false; + } + } + + private abstract static class SingleSubtable extends GlyphPositioningSubtable { + SingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_SINGLE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof SingleSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + int gi = ps.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + Value v = getValue(ci, gi); + if (v != null) { + if (ps.adjust(v)) { + ps.setAdjusted(true); + } + ps.consume(1); + } + return true; + } + } + + /** + * Obtain positioning value for coverage index. + * @param ci coverage index + * @param gi input glyph index + * @return positioning value or null if none applies + */ + public abstract Value getValue(int ci, int gi); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new SingleSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new SingleSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class SingleSubtableFormat1 extends SingleSubtable { + private Value value; + private int ciMax; + + SingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (value != null) { + List entries = new ArrayList<>(1); + entries.add(new SEValue(value)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public Value getValue(int ci, int gi) { + if ((value != null) && (ci <= ciMax)) { + return value; + } else { + return null; + } + } + + private void populate(List entries) { + checkSize(entries, 1); + this.value = checkGet(entries, 0, SEValue.class).get(); + this.ciMax = getCoverageSize() - 1; + } + } + + private static class SingleSubtableFormat2 extends SingleSubtable { + private Value[] values; + SingleSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (values != null) { + return arrayMap(values, val -> new SEValue(val)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public Value getValue(int ci, int gi) { + if ((values != null) && (ci < values.length)) { + return values [ ci ]; + } else { + return null; + } + } + + private void populate(List entries) { + checkSize(entries, 1); + Value[] va = checkGet(entries, 0, SEValueList.class).get(); + if (va.length != getCoverageSize()) { + throw new AdvancedTypographicTableFormatException("illegal values array, " + entries.size() + " values present, but requires " + getCoverageSize() + " values"); + } else { + assert this.values == null; + this.values = va; + } + } + } + + private abstract static class PairSubtable extends GlyphPositioningSubtable { + PairSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_PAIR; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof PairSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int gi = ps.getGlyph(0); + int ci; + if ((ci = getCoverageIndex(gi)) >= 0) { + int[] counts = ps.getGlyphsAvailable(0); + int nga = counts[0]; + if (nga > 1) { + int[] iga = ps.getGlyphs(0, 2, null, counts); + if ((iga != null) && (iga.length == 2)) { + PairValues pv = getPairValues(ci, iga[0], iga[1]); + if (pv != null) { + int offset = 0; + int offsetLast = counts[0] + counts[1]; + // skip any ignored glyphs prior to first non-ignored glyph + for ( ; offset < offsetLast; ++offset) { + if (!ps.isIgnoredGlyph(offset)) { + break; + } else { + ps.consume(1); + } + } + // adjust first non-ignored glyph if first value isn't null + Value v1 = pv.getValue1(); + if (v1 != null) { + if (ps.adjust(v1, offset)) { + ps.setAdjusted(true); + } + ps.consume(1); // consume first non-ignored glyph + ++offset; + } + // skip any ignored glyphs prior to second non-ignored glyph + for ( ; offset < offsetLast; ++offset) { + if (!ps.isIgnoredGlyph(offset)) { + break; + } else { + ps.consume(1); + } + } + // adjust second non-ignored glyph if second value isn't null + Value v2 = pv.getValue2(); + if (v2 != null) { + if (ps.adjust(v2, offset)) { + ps.setAdjusted(true); + } + ps.consume(1); // consume second non-ignored glyph + ++offset; + } + applied = true; + } + } + } + } + return applied; + } + + /** + * Obtain associated pair values. + * @param ci coverage index + * @param gi1 first input glyph index + * @param gi2 second input glyph index + * @return pair values or null if none applies + */ + public abstract PairValues getPairValues(int ci, int gi1, int gi2); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new PairSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new PairSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class PairSubtableFormat1 extends PairSubtable { + private PairValues[][] pvm; // pair values matrix + PairSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (pvm != null) { + return mutableSingleton(new SEPairValueMatrix(pvm)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public PairValues getPairValues(int ci, int gi1, int gi2) { + if ((pvm != null) && (ci < pvm.length)) { + PairValues[] pvt = pvm [ ci ]; + for (int i = 0, n = pvt.length; i < n; i++) { + PairValues pv = pvt [ i ]; + if (pv != null) { + int g = pv.getGlyph(); + if (g < gi2) { + continue; + } else if (g == gi2) { + return pv; + } else { + break; + } + } + } + } + return null; + } + + private void populate(List entries) { + checkSize(entries, 1); + pvm = checkGet(entries, 0, SEPairValueMatrix.class).get(); + } + } + + private static class PairSubtableFormat2 extends PairSubtable { + private GlyphClassTable cdt1; // class def table 1 + private GlyphClassTable cdt2; // class def table 2 + private int nc1; // class 1 count + private int nc2; // class 2 count + private PairValues[][] pvm; // pair values matrix + PairSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (pvm != null) { + List entries = new ArrayList<>(5); + entries.add(new SEGlyphClassTable(cdt1)); + entries.add(new SEGlyphClassTable(cdt2)); + entries.add(SEInteger.valueOf(nc1)); + entries.add(SEInteger.valueOf(nc2)); + entries.add(new SEPairValueMatrix(pvm)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public PairValues getPairValues(int ci, int gi1, int gi2) { + if (pvm != null) { + int c1 = cdt1.getClassIndex(gi1, 0); + if ((c1 >= 0) && (c1 < nc1) && (c1 < pvm.length)) { + PairValues[] pvt = pvm [ c1 ]; + if (pvt != null) { + int c2 = cdt2.getClassIndex(gi2, 0); + if ((c2 >= 0) && (c2 < nc2) && (c2 < pvt.length)) { + return pvt [ c2 ]; + } + } + } + } + return null; + } + + private void populate(List entries) { + checkSize(entries, 5); + cdt1 = checkGet(entries, 0, SEGlyphClassTable.class).get(); + cdt2 = checkGet(entries, 1, SEGlyphClassTable.class).get(); + nc1 = checkGet(entries, 2, SEInteger.class).get(); + nc2 = checkGet(entries, 3, SEInteger.class).get(); + pvm = checkGet(entries, 4, SEPairValueMatrix.class).get(); + } + } + + private abstract static class CursiveSubtable extends GlyphPositioningSubtable { + CursiveSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_CURSIVE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof CursiveSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int gi = ps.getGlyph(0); + int ci; + if ((ci = getCoverageIndex(gi)) >= 0) { + int[] counts = ps.getGlyphsAvailable(0); + int nga = counts[0]; + if (nga > 1) { + int[] iga = ps.getGlyphs(0, 2, null, counts); + if ((iga != null) && (iga.length == 2)) { + // int gi1 = gi; + int ci1 = ci; + int gi2 = iga [ 1 ]; + int ci2 = getCoverageIndex(gi2); + Anchor[] aa = getExitEntryAnchors(ci1, ci2); + if (aa != null) { + Anchor exa = aa [ 0 ]; + Anchor ena = aa [ 1 ]; + // int exw = ps.getWidth ( gi1 ); + int enw = ps.getWidth(gi2); + if ((exa != null) && (ena != null)) { + Value v = ena.getAlignmentAdjustment(exa); + v.adjust(-enw, 0, 0, 0); + if (ps.adjust(v)) { + ps.setAdjusted(true); + } + } + // consume only first glyph of exit/entry glyph pair + ps.consume(1); + applied = true; + } + } + } + } + return applied; + } + + /** + * Obtain exit anchor for first glyph with coverage index ci1 and entry anchor for second + * glyph with coverage index ci2. + * @param ci1 coverage index of first glyph (may be negative) + * @param ci2 coverage index of second glyph (may be negative) + * @return array of two anchors or null if either coverage index is negative or corresponding anchor is + * missing, where the first entry is the exit anchor of the first glyph and the second entry is the + * entry anchor of the second glyph + */ + public abstract Anchor[] getExitEntryAnchors(int ci1, int ci2); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new CursiveSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class CursiveSubtableFormat1 extends CursiveSubtable { + private Anchor[] aa; // anchor array, where even entries are entry anchors, and odd entries are exit anchors + CursiveSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (aa != null) { + return mutableSingleton(new SEAnchorList(aa)); + } else { + return null; + } + } + /** {@inheritDoc} */ + @Override + public Anchor[] getExitEntryAnchors(int ci1, int ci2) { + if ((ci1 >= 0) && (ci2 >= 0)) { + int ai1 = (ci1 * 2) + 1; // ci1 denotes glyph with exit anchor + int ai2 = (ci2 * 2) + 0; // ci2 denotes glyph with entry anchor + if ((aa != null) && (ai1 < aa.length) && (ai2 < aa.length)) { + Anchor exa = aa [ ai1 ]; + Anchor ena = aa [ ai2 ]; + if ((exa != null) && (ena != null)) { + return new Anchor[] { exa, ena }; + } + } + } + return null; + } + + private void populate(List entries) { + checkSize(entries, 1); + Anchor[] o = checkGet(entries, 0, SEAnchorList.class).get(); + + if ((o.length % 2) != 0) { + throw new AdvancedTypographicTableFormatException("illegal entries, Anchor[] array must have an even number of entries, but has: " + o.length); + } else { + aa = o; + } + } + } + + private abstract static class MarkToBaseSubtable extends GlyphPositioningSubtable { + MarkToBaseSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_MARK_TO_BASE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof MarkToBaseSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int giMark = ps.getGlyph(); + int ciMark; + if ((ciMark = getCoverageIndex(giMark)) >= 0) { + MarkAnchor ma = getMarkAnchor(ciMark, giMark); + if (ma != null) { + for (int i = 0, n = ps.getPosition(); i < n; i++) { + int gi = ps.getGlyph(-(i + 1)); + if (ps.isMark(gi)) { + continue; + } else { + Anchor a = getBaseAnchor(gi, ma.getMarkClass()); + if (a != null) { + Value v = a.getAlignmentAdjustment(ma); + // start experimental fix for END OF AYAH in Lateef/Scheherazade + int[] aa = ps.getAdjustment(); + if (aa[2] == 0) { + v.adjust(0, 0, -ps.getWidth(giMark), 0); + } + // end experimental fix for END OF AYAH in Lateef/Scheherazade + if (ps.adjust(v)) { + ps.setAdjusted(true); + } + } + ps.consume(1); + applied = true; + break; + } + } + } + } + return applied; + } + + /** + * Obtain mark anchor associated with mark coverage index. + * @param ciMark coverage index + * @param giMark input glyph index of mark glyph + * @return mark anchor or null if none applies + */ + public abstract MarkAnchor getMarkAnchor(int ciMark, int giMark); + + /** + * Obtain anchor associated with base glyph index and mark class. + * @param giBase input glyph index of base glyph + * @param markClass class number of mark glyph + * @return anchor or null if none applies + */ + public abstract Anchor getBaseAnchor(int giBase, int markClass); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new MarkToBaseSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkToBaseSubtableFormat1 extends MarkToBaseSubtable { + private GlyphCoverageTable bct; // base coverage table + private int nmc; // mark class count + private MarkAnchor[] maa; // mark anchor array, ordered by mark coverage index + private Anchor[][] bam; // base anchor matrix, ordered by base coverage index, then by mark class + MarkToBaseSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if ((bct != null) && (maa != null) && (nmc > 0) && (bam != null)) { + return new ArrayList<>(Arrays.asList( + new SEGlyphCoverageTable(bct), + SEInteger.valueOf(nmc), + new SEMarkAnchorList(maa), + new SEAnchorMatrix(bam))); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public MarkAnchor getMarkAnchor(int ciMark, int giMark) { + if ((maa != null) && (ciMark < maa.length)) { + return maa [ ciMark ]; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public Anchor getBaseAnchor(int giBase, int markClass) { + int ciBase; + if ((bct != null) && ((ciBase = bct.getCoverageIndex(giBase)) >= 0)) { + if ((bam != null) && (ciBase < bam.length)) { + Anchor[] ba = bam [ ciBase ]; + if ((ba != null) && (markClass < ba.length)) { + return ba [ markClass ]; + } + } + } + return null; + } + + private void populate(List entries) { + checkSize(entries, 4); + bct = checkGet(entries, 0, SEGlyphCoverageTable.class).get(); + nmc = checkGet(entries, 1, SEInteger.class).get(); + maa = checkGet(entries, 2, SEMarkAnchorList.class).get(); + bam = checkGet(entries, 3, SEAnchorMatrix.class).get(); + } + } + + private abstract static class MarkToLigatureSubtable extends GlyphPositioningSubtable { + MarkToLigatureSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof MarkToLigatureSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int giMark = ps.getGlyph(); + int ciMark; + if ((ciMark = getCoverageIndex(giMark)) >= 0) { + MarkAnchor ma = getMarkAnchor(ciMark, giMark); + int mxc = getMaxComponentCount(); + if (ma != null) { + for (int i = 0, n = ps.getPosition(); i < n; i++) { + int gi = ps.getGlyph(-(i + 1)); + if (ps.isMark(gi)) { + continue; + } else { + Anchor a = getLigatureAnchor(gi, mxc, i, ma.getMarkClass()); + if (a != null) { + if (ps.adjust(a.getAlignmentAdjustment(ma))) { + ps.setAdjusted(true); + } + } + ps.consume(1); + applied = true; + break; + } + } + } + } + return applied; + } + + /** + * Obtain mark anchor associated with mark coverage index. + * @param ciMark coverage index + * @param giMark input glyph index of mark glyph + * @return mark anchor or null if none applies + */ + public abstract MarkAnchor getMarkAnchor(int ciMark, int giMark); + + /** + * Obtain maximum component count. + * @return maximum component count (>=0) + */ + public abstract int getMaxComponentCount(); + + /** + * Obtain anchor associated with ligature glyph index and mark class. + * @param giLig input glyph index of ligature glyph + * @param maxComponents maximum component count + * @param component component number (0...maxComponents-1) + * @param markClass class number of mark glyph + * @return anchor or null if none applies + */ + public abstract Anchor getLigatureAnchor(int giLig, int maxComponents, int component, int markClass); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new MarkToLigatureSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkToLigatureSubtableFormat1 extends MarkToLigatureSubtable { + private GlyphCoverageTable lct; // ligature coverage table + private int nmc; // mark class count + private int mxc; // maximum ligature component count + private MarkAnchor[] maa; // mark anchor array, ordered by mark coverage index + private Anchor[][][] lam; // ligature anchor matrix, ordered by ligature coverage index, then ligature component, then mark class + MarkToLigatureSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (lam != null) { + List entries = new ArrayList<>(5); + entries.add(new SEGlyphCoverageTable(lct)); + entries.add(SEInteger.valueOf(nmc)); + entries.add(SEInteger.valueOf(mxc)); + entries.add(new SEMarkAnchorList(maa)); + entries.add(new SEAnchorMultiMatrix(lam)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public MarkAnchor getMarkAnchor(int ciMark, int giMark) { + if ((maa != null) && (ciMark < maa.length)) { + return maa [ ciMark ]; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public int getMaxComponentCount() { + return mxc; + } + + /** {@inheritDoc} */ + @Override + public Anchor getLigatureAnchor(int giLig, int maxComponents, int component, int markClass) { + int ciLig; + if ((lct != null) && ((ciLig = lct.getCoverageIndex(giLig)) >= 0)) { + if ((lam != null) && (ciLig < lam.length)) { + Anchor[][] lcm = lam [ ciLig ]; + if (component < maxComponents) { + Anchor[] la = lcm [ component ]; + if ((la != null) && (markClass < la.length)) { + return la [ markClass ]; + } + } + } + } + return null; + } + + private void populate(List entries) { + checkSize(entries, 5); + lct = checkGet(entries, 0, SEGlyphCoverageTable.class).get(); + nmc = checkGet(entries, 1, SEInteger.class).get(); + mxc = checkGet(entries, 2, SEInteger.class).get(); + maa = checkGet(entries, 3, SEMarkAnchorList.class).get(); + lam = checkGet(entries, 4, SEAnchorMultiMatrix.class).get(); + } + } + + private abstract static class MarkToMarkSubtable extends GlyphPositioningSubtable { + MarkToMarkSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_MARK_TO_MARK; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof MarkToMarkSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int giMark1 = ps.getGlyph(); + int ciMark1; + if ((ciMark1 = getCoverageIndex(giMark1)) >= 0) { + MarkAnchor ma = getMark1Anchor(ciMark1, giMark1); + if (ma != null) { + if (ps.hasPrev()) { + Anchor a = getMark2Anchor(ps.getGlyph(-1), ma.getMarkClass()); + if (a != null) { + if (ps.adjust(a.getAlignmentAdjustment(ma))) { + ps.setAdjusted(true); + } + } + ps.consume(1); + applied = true; + } + } + } + return applied; + } + + /** + * Obtain mark 1 anchor associated with mark 1 coverage index. + * @param ciMark1 mark 1 coverage index + * @param giMark1 input glyph index of mark 1 glyph + * @return mark 1 anchor or null if none applies + */ + public abstract MarkAnchor getMark1Anchor(int ciMark1, int giMark1); + + /** + * Obtain anchor associated with mark 2 glyph index and mark 1 class. + * @param giMark2 input glyph index of mark 2 glyph + * @param markClass class number of mark 1 glyph + * @return anchor or null if none applies + */ + public abstract Anchor getMark2Anchor(int giBase, int markClass); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new MarkToMarkSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkToMarkSubtableFormat1 extends MarkToMarkSubtable { + private GlyphCoverageTable mct2; // mark 2 coverage table + private int nmc; // mark class count + private MarkAnchor[] maa; // mark1 anchor array, ordered by mark1 coverage index + private Anchor[][] mam; // mark2 anchor matrix, ordered by mark2 coverage index, then by mark1 class + MarkToMarkSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if ((mct2 != null) && (maa != null) && (nmc > 0) && (mam != null)) { + List entries = new ArrayList<>(4); + entries.add(new SEGlyphCoverageTable(mct2)); + entries.add(SEInteger.valueOf(nmc)); + entries.add(new SEMarkAnchorList(maa)); + entries.add(new SEAnchorMatrix(mam)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public MarkAnchor getMark1Anchor(int ciMark1, int giMark1) { + if ((maa != null) && (ciMark1 < maa.length)) { + return maa [ ciMark1 ]; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public Anchor getMark2Anchor(int giMark2, int markClass) { + int ciMark2; + if ((mct2 != null) && ((ciMark2 = mct2.getCoverageIndex(giMark2)) >= 0)) { + if ((mam != null) && (ciMark2 < mam.length)) { + Anchor[] ma = mam [ ciMark2 ]; + if ((ma != null) && (markClass < ma.length)) { + return ma [ markClass ]; + } + } + } + return null; + } + + private void populate(List entries) { + checkSize(entries, 4); + mct2 = checkGet(entries, 0, SEGlyphCoverageTable.class).get(); + nmc = checkGet(entries, 1, SEInteger.class).get(); + maa = checkGet(entries, 2, SEMarkAnchorList.class).get(); + mam = checkGet(entries, 3, SEAnchorMatrix.class).get(); + } + } + + private abstract static class ContextualSubtable extends GlyphPositioningSubtable { + ContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_CONTEXTUAL; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof ContextualSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int gi = ps.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) >= 0) { + int[] rv = new int[1]; + RuleLookup[] la = getLookups(ci, gi, ps, rv); + if (la != null) { + ps.apply(la, rv[0]); + applied = true; + } + } + return applied; + } + + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ps glyph positioning state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater, + * where the first entry is used to return the input sequence length of the matched rule + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new ContextualSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new ContextualSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else if (format == 3) { + return new ContextualSubtableFormat3(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ContextualSubtableFormat1 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) { + assert ps != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs(gi); + if (matches(ps, iga, 0, rv)) { + return r.getLookups(); + } + } + } + } + } + return null; + } + + static boolean matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv) { + if ((glyphs == null) || (glyphs.length == 0)) { + return true; // match null or empty glyph sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ps.getIgnoreDefault(); + int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores); + int nga = counts[0]; + int ngm = glyphs.length; + if (nga < ngm) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts); + for (int k = 0; k < ngm; k++) { + if (ga [ k ] != glyphs [ k ]) { + return false; // match fails at ga [ k ] + } + } + if (rv != null) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private static class ContextualSubtableFormat2 extends ContextualSubtable { + private GlyphClassTable cdt; // class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + List entries = new ArrayList<>(3); + entries.add(new SEGlyphClassTable(cdt)); + entries.add(SEInteger.valueOf(ngc)); + entries.add(new SERuleSetList(rsa)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) { + assert ps != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedClassSequenceRule)) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ca = cr.getClasses(cdt.getClassIndex(gi, ps.getClassMatchSet(gi))); + if (matches(ps, cdt, ca, 0, rv)) { + return r.getLookups(); + } + } + } + } + } + return null; + } + + static boolean matches(GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv) { + if ((cdt == null) || (classes == null) || (classes.length == 0)) { + return true; // match null class definitions, null or empty class sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ps.getIgnoreDefault(); + int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores); + int nga = counts[0]; + int ngm = classes.length; + if (nga < ngm) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts); + for (int k = 0; k < ngm; k++) { + int gi = ga [ k ]; + int ms = ps.getClassMatchSet(gi); + int gc = cdt.getClassIndex(gi, ms); + if ((gc < 0) || (gc >= cdt.getClassSize(ms))) { + return false; // none or invalid class fails mat ch + } else if (gc != classes [ k ]) { + return false; // match fails at ga [ k ] + } + } + if (rv != null) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + + private void populate(List entries) { + checkSize(entries, 3); + cdt = checkGet(entries, 0, SEGlyphClassTable.class).get(); + ngc = checkGet(entries, 1, SEInteger.class).get(); + rsa = checkGet(entries, 2, SERuleSetList.class).get(); + } + } + + private static class ContextualSubtableFormat3 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) { + assert ps != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] gca = cr.getCoverages(); + if (matches(ps, gca, 0, rv)) { + return r.getLookups(); + } + } + } + } + } + return null; + } + + static boolean matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv) { + if ((gca == null) || (gca.length == 0)) { + return true; // match null or empty coverage array + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ps.getIgnoreDefault(); + int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores); + int nga = counts[0]; + int ngm = gca.length; + if (nga < ngm) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts); + for (int k = 0; k < ngm; k++) { + GlyphCoverageTable ct = gca [ k ]; + if (ct != null) { + if (ct.getCoverageIndex(ga [ k ]) < 0) { + return false; // match fails at ga [ k ] + } + } + } + if (rv != null) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private abstract static class ChainedContextualSubtable extends GlyphPositioningSubtable { + ChainedContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof ChainedContextualSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphPositioningState ps) { + boolean applied = false; + int gi = ps.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) >= 0) { + int[] rv = new int[1]; + RuleLookup[] la = getLookups(ci, gi, ps, rv); + if (la != null) { + ps.apply(la, rv[0]); + applied = true; + } + } + return applied; + } + + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ps glyph positioning state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater, + * where the first entry is used to return the input sequence length of the matched rule + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv); + + static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new ChainedContextualSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new ChainedContextualSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else if (format == 3) { + return new ChainedContextualSubtableFormat3(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ChainedContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) { + assert ps != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs(gi); + if (matches(ps, iga, 0, rv)) { + int[] bga = cr.getBacktrackGlyphs(); + if (matches(ps, bga, -1, null)) { + int[] lga = cr.getLookaheadGlyphs(); + if (matches(ps, lga, rv[0], null)) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + + private boolean matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv) { + return ContextualSubtableFormat1.matches(ps, glyphs, offset, rv); + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable { + private GlyphClassTable icdt; // input class def table + private GlyphClassTable bcdt; // backtrack class def table + private GlyphClassTable lcdt; // lookahead class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ChainedContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + List entries = new ArrayList<>(5); + entries.add(new SEGlyphClassTable(icdt)); + entries.add(new SEGlyphClassTable(bcdt)); + entries.add(new SEGlyphClassTable(lcdt)); + entries.add(SEInteger.valueOf(ngc)); + entries.add(new SERuleSetList(rsa)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) { + assert ps != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedClassSequenceRule)) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ica = cr.getClasses(icdt.getClassIndex(gi, ps.getClassMatchSet(gi))); + if (matches(ps, icdt, ica, 0, rv)) { + int[] bca = cr.getBacktrackClasses(); + if (matches(ps, bcdt, bca, -1, null)) { + int[] lca = cr.getLookaheadClasses(); + if (matches(ps, lcdt, lca, rv[0], null)) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + + private boolean matches(GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv) { + return ContextualSubtableFormat2.matches(ps, cdt, classes, offset, rv); + } + + private void populate(List entries) { + checkSize(entries, 5); + icdt = checkGet(entries, 0, SEGlyphClassTable.class).get(); + bcdt = checkGet(entries, 1, SEGlyphClassTable.class).get(); + lcdt = checkGet(entries, 2, SEGlyphClassTable.class).get(); + ngc = checkGet(entries, 3, SEInteger.class).get(); + rsa = checkGet(entries, 4, SERuleSetList.class).get(); + if (rsa.length != ngc) { + throw new AdvancedTypographicTableFormatException("illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes"); + } + } + } + + private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ChainedContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) { + assert ps != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] igca = cr.getCoverages(); + if (matches(ps, igca, 0, rv)) { + GlyphCoverageTable[] bgca = cr.getBacktrackCoverages(); + if (matches(ps, bgca, -1, null)) { + GlyphCoverageTable[] lgca = cr.getLookaheadCoverages(); + if (matches(ps, lgca, rv[0], null)) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + + private boolean matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv) { + return ContextualSubtableFormat3.matches(ps, gca, offset, rv); + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + /** + * The DeviceTable class implements a positioning device table record, comprising + * adjustments to be made to scaled design units according to the scaled size. + */ + public static class DeviceTable { + + private final int startSize; + private final int endSize; + private final int[] deltas; + + /** + * Instantiate a DeviceTable. + * @param startSize the + * @param endSize the ending (scaled) size + * @param deltas adjustments for each scaled size + */ + public DeviceTable(int startSize, int endSize, int[] deltas) { + assert startSize >= 0; + assert startSize <= endSize; + assert deltas != null; + assert deltas.length == (endSize - startSize) + 1; + this.startSize = startSize; + this.endSize = endSize; + this.deltas = deltas; + } + + /** @return the start size */ + public int getStartSize() { + return startSize; + } + + /** @return the end size */ + public int getEndSize() { + return endSize; + } + + /** @return the deltas */ + public int[] getDeltas() { + return deltas; + } + + /** + * Find device adjustment. + * @param fontSize the font size to search for + * @return an adjustment if font size matches an entry + */ + public int findAdjustment(int fontSize) { + // [TODO] at present, assumes that 1 device unit equals one point + int fs = fontSize / 1000; + if (fs < startSize) { + return 0; + } else if (fs <= endSize) { + return deltas [ fs - startSize ] * 1000; + } else { + return 0; + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "{ start = " + startSize + ", end = " + endSize + ", deltas = " + Arrays.toString(deltas) + "}"; + } + + } + + /** + * The Value class implements a positioning value record, comprising placement + * and advancement information in X and Y axes, and optionally including device data used to + * perform device (grid-fitted) specific fine grain adjustments. + */ + public static class Value { + + /** X_PLACEMENT value format flag */ + public static final int X_PLACEMENT = 0x0001; + /** Y_PLACEMENT value format flag */ + public static final int Y_PLACEMENT = 0x0002; + /** X_ADVANCE value format flag */ + public static final int X_ADVANCE = 0x0004; + /** Y_ADVANCE value format flag */ + public static final int Y_ADVANCE = 0x0008; + /** X_PLACEMENT_DEVICE value format flag */ + public static final int X_PLACEMENT_DEVICE = 0x0010; + /** Y_PLACEMENT_DEVICE value format flag */ + public static final int Y_PLACEMENT_DEVICE = 0x0020; + /** X_ADVANCE_DEVICE value format flag */ + public static final int X_ADVANCE_DEVICE = 0x0040; + /** Y_ADVANCE_DEVICE value format flag */ + public static final int Y_ADVANCE_DEVICE = 0x0080; + + /** X_PLACEMENT value index (within adjustments arrays) */ + public static final int IDX_X_PLACEMENT = 0; + /** Y_PLACEMENT value index (within adjustments arrays) */ + public static final int IDX_Y_PLACEMENT = 1; + /** X_ADVANCE value index (within adjustments arrays) */ + public static final int IDX_X_ADVANCE = 2; + /** Y_ADVANCE value index (within adjustments arrays) */ + public static final int IDX_Y_ADVANCE = 3; + + private int xPlacement; // x placement + private int yPlacement; // y placement + private int xAdvance; // x advance + private int yAdvance; // y advance + private final DeviceTable xPlaDevice; // x placement device table + private final DeviceTable yPlaDevice; // y placement device table + private final DeviceTable xAdvDevice; // x advance device table + private final DeviceTable yAdvDevice; // x advance device table + + /** + * Instantiate a Value. + * @param xPlacement the x placement or zero + * @param yPlacement the y placement or zero + * @param xAdvance the x advance or zero + * @param yAdvance the y advance or zero + * @param xPlaDevice the x placement device table or null + * @param yPlaDevice the y placement device table or null + * @param xAdvDevice the x advance device table or null + * @param yAdvDevice the y advance device table or null + */ + public Value(int xPlacement, int yPlacement, int xAdvance, int yAdvance, DeviceTable xPlaDevice, DeviceTable yPlaDevice, DeviceTable xAdvDevice, DeviceTable yAdvDevice) { + this.xPlacement = xPlacement; + this.yPlacement = yPlacement; + this.xAdvance = xAdvance; + this.yAdvance = yAdvance; + this.xPlaDevice = xPlaDevice; + this.yPlaDevice = yPlaDevice; + this.xAdvDevice = xAdvDevice; + this.yAdvDevice = yAdvDevice; + } + + /** @return the x placement */ + public int getXPlacement() { + return xPlacement; + } + + /** @return the y placement */ + public int getYPlacement() { + return yPlacement; + } + + /** @return the x advance */ + public int getXAdvance() { + return xAdvance; + } + + /** @return the y advance */ + public int getYAdvance() { + return yAdvance; + } + + /** @return the x placement device table */ + public DeviceTable getXPlaDevice() { + return xPlaDevice; + } + + /** @return the y placement device table */ + public DeviceTable getYPlaDevice() { + return yPlaDevice; + } + + /** @return the x advance device table */ + public DeviceTable getXAdvDevice() { + return xAdvDevice; + } + + /** @return the y advance device table */ + public DeviceTable getYAdvDevice() { + return yAdvDevice; + } + + /** + * Apply value to specific adjustments to without use of device table adjustments. + * @param xPlacement the x placement or zero + * @param yPlacement the y placement or zero + * @param xAdvance the x advance or zero + * @param yAdvance the y advance or zero + */ + public void adjust(int xPlacement, int yPlacement, int xAdvance, int yAdvance) { + this.xPlacement += xPlacement; + this.yPlacement += yPlacement; + this.xAdvance += xAdvance; + this.yAdvance += yAdvance; + } + + /** + * Apply value to adjustments using font size for device table adjustments. + * @param adjustments array of four integers containing X,Y placement and X,Y advance adjustments + * @param fontSize font size for device table adjustments + * @return true if some adjustment was made + */ + public boolean adjust(int[] adjustments, int fontSize) { + boolean adjust = false; + int dv; + if ((dv = xPlacement) != 0) { + adjustments [ IDX_X_PLACEMENT ] += dv; + adjust = true; + } + if ((dv = yPlacement) != 0) { + adjustments [ IDX_Y_PLACEMENT ] += dv; + adjust = true; + } + if ((dv = xAdvance) != 0) { + adjustments [ IDX_X_ADVANCE ] += dv; + adjust = true; + } + if ((dv = yAdvance) != 0) { + adjustments [ IDX_Y_ADVANCE ] += dv; + adjust = true; + } + if (fontSize != 0) { + DeviceTable dt; + if ((dt = xPlaDevice) != null) { + if ((dv = dt.findAdjustment(fontSize)) != 0) { + adjustments [ IDX_X_PLACEMENT ] += dv; + adjust = true; + } + } + if ((dt = yPlaDevice) != null) { + if ((dv = dt.findAdjustment(fontSize)) != 0) { + adjustments [ IDX_Y_PLACEMENT ] += dv; + adjust = true; + } + } + if ((dt = xAdvDevice) != null) { + if ((dv = dt.findAdjustment(fontSize)) != 0) { + adjustments [ IDX_X_ADVANCE ] += dv; + adjust = true; + } + } + if ((dt = yAdvDevice) != null) { + if ((dv = dt.findAdjustment(fontSize)) != 0) { + adjustments [ IDX_Y_ADVANCE ] += dv; + adjust = true; + } + } + } + return adjust; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + sb.append("{ "); + if (xPlacement != 0) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("xPlacement = " + xPlacement); + } + if (yPlacement != 0) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("yPlacement = " + yPlacement); + } + if (xAdvance != 0) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("xAdvance = " + xAdvance); + } + if (yAdvance != 0) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("yAdvance = " + yAdvance); + } + if (xPlaDevice != null) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("xPlaDevice = " + xPlaDevice); + } + if (yPlaDevice != null) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("xPlaDevice = " + yPlaDevice); + } + if (xAdvDevice != null) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("xAdvDevice = " + xAdvDevice); + } + if (yAdvDevice != null) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("xAdvDevice = " + yAdvDevice); + } + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The PairValues class implements a pair value record, comprising a glyph id (or zero) + * and two optional positioning values. + */ + public static class PairValues { + + private final int glyph; // glyph id (or 0) + private final Value value1; // value for first glyph in pair (or null) + private final Value value2; // value for second glyph in pair (or null) + + /** + * Instantiate a PairValues. + * @param glyph the glyph id (or zero) + * @param value1 the value of the first glyph in pair (or null) + * @param value2 the value of the second glyph in pair (or null) + */ + public PairValues(int glyph, Value value1, Value value2) { + assert glyph >= 0; + this.glyph = glyph; + this.value1 = value1; + this.value2 = value2; + } + + /** @return the glyph id */ + public int getGlyph() { + return glyph; + } + + /** @return the first value */ + public Value getValue1() { + return value1; + } + + /** @return the second value */ + public Value getValue2() { + return value2; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + sb.append("{ "); + if (glyph != 0) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("glyph = " + glyph); + } + if (value1 != null) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("value1 = " + value1); + } + if (value2 != null) { + if (!first) { + sb.append(", "); + } else { + first = false; + } + sb.append("value2 = " + value2); + } + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The Anchor class implements a anchor record, comprising an X,Y coordinate pair, + * an optional anchor point index (or -1), and optional X or Y device tables (or null if absent). + */ + public static class Anchor { + + private final int x; // xCoordinate (in design units) + private final int y; // yCoordinate (in design units) + private final int anchorPoint; // anchor point index (or -1) + private final DeviceTable xDevice; // x device table + private final DeviceTable yDevice; // y device table + + /** + * Instantiate an Anchor (format 1). + * @param x the x coordinate + * @param y the y coordinate + */ + public Anchor(int x, int y) { + this (x, y, -1, null, null); + } + + /** + * Instantiate an Anchor (format 2). + * @param x the x coordinate + * @param y the y coordinate + * @param anchorPoint anchor index (or -1) + */ + public Anchor(int x, int y, int anchorPoint) { + this (x, y, anchorPoint, null, null); + } + + /** + * Instantiate an Anchor (format 3). + * @param x the x coordinate + * @param y the y coordinate + * @param xDevice the x device table (or null if not present) + * @param yDevice the y device table (or null if not present) + */ + public Anchor(int x, int y, DeviceTable xDevice, DeviceTable yDevice) { + this (x, y, -1, xDevice, yDevice); + } + + /** + * Instantiate an Anchor based on an existing anchor. + * @param a the existing anchor + */ + protected Anchor(Anchor a) { + this (a.x, a.y, a.anchorPoint, a.xDevice, a.yDevice); + } + + private Anchor(int x, int y, int anchorPoint, DeviceTable xDevice, DeviceTable yDevice) { + assert (anchorPoint >= 0) || (anchorPoint == -1); + this.x = x; + this.y = y; + this.anchorPoint = anchorPoint; + this.xDevice = xDevice; + this.yDevice = yDevice; + } + + /** @return the x coordinate */ + public int getX() { + return x; + } + + /** @return the y coordinate */ + public int getY() { + return y; + } + + /** @return the anchor point index (or -1 if not specified) */ + public int getAnchorPoint() { + return anchorPoint; + } + + /** @return the x device table (or null if not specified) */ + public DeviceTable getXDevice() { + return xDevice; + } + + /** @return the y device table (or null if not specified) */ + public DeviceTable getYDevice() { + return yDevice; + } + + /** + * Obtain adjustment value required to align the specified anchor + * with this anchor. + * @param a the anchor to align + * @return the adjustment value needed to effect alignment + */ + public Value getAlignmentAdjustment(Anchor a) { + assert a != null; + // TODO - handle anchor point + // TODO - handle device tables + return new Value(x - a.x, y - a.y, 0, 0, null, null, null, null); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ [" + x + "," + y + "]"); + if (anchorPoint != -1) { + sb.append(", anchorPoint = " + anchorPoint); + } + if (xDevice != null) { + sb.append(", xDevice = " + xDevice); + } + if (yDevice != null) { + sb.append(", yDevice = " + yDevice); + } + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The MarkAnchor class is a subclass of the Anchor class, adding a mark + * class designation. + */ + public static class MarkAnchor extends Anchor { + + private final int markClass; // mark class + + /** + * Instantiate a MarkAnchor + * @param markClass the mark class + * @param a the underlying anchor (whose fields are copied) + */ + public MarkAnchor(int markClass, Anchor a) { + super(a); + this.markClass = markClass; + } + + /** @return the mark class */ + public int getMarkClass() { + return markClass; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "{ markClass = " + markClass + ", anchor = " + super.toString() + " }"; + } + + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphProcessingState.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphProcessingState.java new file mode 100644 index 00000000000..5bda861b557 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphProcessingState.java @@ -0,0 +1,1193 @@ +/* + * 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.advanced; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphContextTester; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.GlyphTester; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +/** + *

The GlyphProcessingState implements a common, base state object used during glyph substitution + * and positioning processing.

+ * + * @author Glenn Adams + */ +public class GlyphProcessingState { + + /** governing glyph definition table */ + protected GlyphDefinitionTable gdef; + /** governing script */ + protected String script; + /** governing language */ + protected String language; + /** governing feature */ + protected String feature; + /** current input glyph sequence */ + protected GlyphSequence igs; + /** current index in input sequence */ + protected int index; + /** last (maximum) index of input sequence (exclusive) */ + protected int indexLast; + /** consumed, updated after each successful subtable application */ + protected int consumed; + /** lookup flags */ + protected int lookupFlags; + /** class match set */ + protected int classMatchSet; + /** script specific context tester or null */ + protected ScriptContextTester sct; + /** glyph context tester or null */ + protected GlyphContextTester gct; + /** ignore base glyph tester */ + protected GlyphTester ignoreBase; + /** ignore ligature glyph tester */ + protected GlyphTester ignoreLigature; + /** ignore mark glyph tester */ + protected GlyphTester ignoreMark; + /** default ignore glyph tester */ + protected GlyphTester ignoreDefault; + /** current subtable */ + private GlyphSubtable subtable; + + /** + * Construct default (reset) glyph processing state. + */ + public GlyphProcessingState() { + } + + /** + * Construct glyph processing state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param sct script context tester (or null) + */ + protected GlyphProcessingState(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) { + this.script = script; + this.language = language; + this.feature = feature; + this.igs = gs; + this.indexLast = gs.getGlyphCount(); + this.sct = sct; + this.gct = (sct != null) ? sct.getTester(feature) : null; + this.ignoreBase = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredBase(gi, flags); } }; + this.ignoreLigature = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredLigature(gi, flags); } }; + this.ignoreMark = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredMark(gi, flags); } }; + } + + /** + * Construct glyph processing state using an existing state object using shallow copy + * except as follows: input glyph sequence is copied deep except for its characters array. + * @param s existing processing state to copy from + */ + protected GlyphProcessingState(GlyphProcessingState s) { + this (new GlyphSequence(s.igs), s.script, s.language, s.feature, s.sct); + setPosition(s.index); + } + + /** + * Reset glyph processing state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param sct script context tester (or null) + */ + protected GlyphProcessingState reset(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) { + this.gdef = null; + this.script = script; + this.language = language; + this.feature = feature; + this.igs = gs; + this.index = 0; + this.indexLast = gs.getGlyphCount(); + this.consumed = 0; + this.lookupFlags = 0; + this.classMatchSet = 0; + this.sct = sct; + this.gct = (sct != null) ? sct.getTester(feature) : null; + this.ignoreBase = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredBase(gi, flags); } }; + this.ignoreLigature = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredLigature(gi, flags); } }; + this.ignoreMark = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredMark(gi, flags); } }; + this.ignoreDefault = null; + this.subtable = null; + return this; + } + + /** + * Set governing glyph definition table. + * @param gdef glyph definition table (or null, to unset) + */ + public void setGDEF(GlyphDefinitionTable gdef) { + if (this.gdef == null) { + this.gdef = gdef; + } else if (gdef == null) { + this.gdef = null; + } + } + + /** + * Obtain governing glyph definition table. + * @return glyph definition table (or null, to not set) + */ + public GlyphDefinitionTable getGDEF() { + return gdef; + } + + /** + * Set governing lookup flags + * @param flags lookup flags (or zero, to unset) + */ + public void setLookupFlags(int flags) { + if (this.lookupFlags == 0) { + this.lookupFlags = flags; + } else if (flags == 0) { + this.lookupFlags = 0; + } + } + + /** + * Obtain governing lookup flags. + * @return lookup flags (zero may indicate unset or no flags) + */ + public int getLookupFlags() { + return lookupFlags; + } + + /** + * Obtain governing class match set. + * @param gi glyph index that may be used to determine which match set applies + * @return class match set (zero may indicate unset or no set) + */ + public int getClassMatchSet(int gi) { + return 0; + } + + /** + * Set default ignore tester. + * @param ignoreDefault glyph tester (or null, to unset) + */ + public void setIgnoreDefault(GlyphTester ignoreDefault) { + if (this.ignoreDefault == null) { + this.ignoreDefault = ignoreDefault; + } else if (ignoreDefault == null) { + this.ignoreDefault = null; + } + } + + /** + * Obtain governing default ignores tester. + * @return default ignores tester + */ + public GlyphTester getIgnoreDefault() { + return ignoreDefault; + } + + /** + * Update glyph subtable specific state. Each time a + * different glyph subtable is to be applied, it is used + * to update this state prior to application, after which + * this state is to be reset. + * @param st glyph subtable to use for update + */ + public void updateSubtableState(GlyphSubtable st) { + if (this.subtable != st) { + setGDEF(st.getGDEF()); + setLookupFlags(st.getFlags()); + setIgnoreDefault(getIgnoreTester(getLookupFlags())); + this.subtable = st; + } + } + + /** + * Obtain current position index in input glyph sequence. + * @return current index + */ + public int getPosition() { + return index; + } + + /** + * Set (seek to) position index in input glyph sequence. + * @param index to seek to + * @throws IndexOutOfBoundsException if index is less than zero + * or exceeds last valid position + */ + public void setPosition(int index) throws IndexOutOfBoundsException { + if ((index >= 0) && (index <= indexLast)) { + this.index = index; + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Obtain last valid position index in input glyph sequence. + * @return current last index + */ + public int getLastPosition() { + return indexLast; + } + + /** + * Determine if at least one glyph remains in + * input sequence. + * @return true if one or more glyph remains + */ + public boolean hasNext() { + return hasNext(1); + } + + /** + * Determine if at least count glyphs remain in + * input sequence. + * @param count of glyphs to test + * @return true if at least count glyphs are available + */ + public boolean hasNext(int count) { + return (index + count) <= indexLast; + } + + /** + * Update the current position index based upon previously consumed + * glyphs, i.e., add the consuemd count to the current position index. + * If no glyphs were previously consumed, then forces exactly one + * glyph to be consumed. + * @return the new (updated) position index + */ + public int next() { + if (index < indexLast) { + // force consumption of at least one input glyph + if (consumed == 0) { + consumed = 1; + } + index += consumed; + consumed = 0; + if (index > indexLast) { + index = indexLast; + } + } + return index; + } + + /** + * Determine if at least one backtrack (previous) glyph is present + * in input sequence. + * @return true if one or more glyph remains + */ + public boolean hasPrev() { + return hasPrev(1); + } + + /** + * Determine if at least count backtrack (previous) glyphs + * are present in input sequence. + * @param count of glyphs to test + * @return true if at least count glyphs are available + */ + public boolean hasPrev(int count) { + return (index - count) >= 0; + } + + /** + * Update the current position index based upon previously consumed + * glyphs, i.e., subtract the consuemd count from the current position index. + * If no glyphs were previously consumed, then forces exactly one + * glyph to be consumed. This method is used to traverse an input + * glyph sequence in reverse order. + * @return the new (updated) position index + */ + public int prev() { + if (index > 0) { + // force consumption of at least one input glyph + if (consumed == 0) { + consumed = 1; + } + index -= consumed; + consumed = 0; + if (index < 0) { + index = 0; + } + } + return index; + } + + /** + * Record the consumption of count glyphs such that + * this consumption never exceeds the number of glyphs in the input glyph + * sequence. + * @param count of glyphs to consume + * @return newly adjusted consumption count + * @throws IndexOutOfBoundsException if count would cause consumption + * to exceed count of glyphs in input glyph sequence + */ + public int consume(int count) throws IndexOutOfBoundsException { + if ((consumed + count) <= indexLast) { + consumed += count; + return consumed; + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Determine if any consumption has occurred. + * @return true if consumption count is greater than zero + */ + public boolean didConsume() { + return consumed > 0; + } + + /** + * Obtain reference to input glyph sequence, which must not be modified. + * @return input glyph sequence + */ + public GlyphSequence getInput() { + return igs; + } + + /** + * Obtain glyph at specified offset from current position. + * @param offset from current position + * @return glyph at specified offset from current position + * @throws IndexOutOfBoundsException if no glyph available at offset + */ + public int getGlyph(int offset) throws IndexOutOfBoundsException { + int i = index + offset; + if ((i >= 0) && (i < indexLast)) { + return igs.getGlyph(i); + } else { + throw new IndexOutOfBoundsException("attempting index at " + i); + } + } + + /** + * Obtain glyph at current position. + * @return glyph at current position + * @throws IndexOutOfBoundsException if no glyph available + */ + public int getGlyph() throws IndexOutOfBoundsException { + return getGlyph(0); + } + + /** + * Set (replace) glyph at specified offset from current position. + * @param offset from current position + * @param glyph to set at specified offset from current position + * @throws IndexOutOfBoundsException if specified offset is not valid position + */ + public void setGlyph(int offset, int glyph) throws IndexOutOfBoundsException { + int i = index + offset; + if ((i >= 0) && (i < indexLast)) { + igs.setGlyph(i, glyph); + } else { + throw new IndexOutOfBoundsException("attempting index at " + i); + } + } + + /** + * Obtain character association of glyph at specified offset from current position. + * @param offset from current position + * @return character association of glyph at current position + * @throws IndexOutOfBoundsException if offset results in an invalid index into input glyph sequence + */ + public CharAssociation getAssociation(int offset) throws IndexOutOfBoundsException { + int i = index + offset; + if ((i >= 0) && (i < indexLast)) { + return igs.getAssociation(i); + } else { + throw new IndexOutOfBoundsException("attempting index at " + i); + } + } + + /** + * Obtain character association of glyph at current position. + * @return character association of glyph at current position + * @throws IndexOutOfBoundsException if no glyph available + */ + public CharAssociation getAssociation() throws IndexOutOfBoundsException { + return getAssociation(0); + } + + /** + * Obtain count glyphs starting at specified offset from current position. If + * reverseOrder is true, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of glyphs to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param glyphs array to use to fetch glyphs + * @param counts int[2] array to receive fetched glyph counts, where counts[0] will + * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs + * ignored + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphs(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs, int[] counts) throws IndexOutOfBoundsException { + if (count < 0) { + count = getGlyphsAvailable(offset, reverseOrder, ignoreTester) [ 0 ]; + } + int start = index + offset; + if (start < 0) { + throw new IndexOutOfBoundsException("will attempt index at " + start); + } else if (!reverseOrder && ((start + count) > indexLast)) { + throw new IndexOutOfBoundsException("will attempt index at " + (start + count)); + } else if (reverseOrder && ((start + 1) < count)) { + throw new IndexOutOfBoundsException("will attempt index at " + (start - count)); + } + if (glyphs == null) { + glyphs = new int [ count ]; + } else if (glyphs.length != count) { + throw new IllegalArgumentException("glyphs array is non-null, but its length (" + glyphs.length + "), is not equal to count (" + count + ")"); + } + if (!reverseOrder) { + return getGlyphsForward(start, count, ignoreTester, glyphs, counts); + } else { + return getGlyphsReverse(start, count, ignoreTester, glyphs, counts); + } + } + + private int[] getGlyphsForward(int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for (int i = start, n = indexLast; (i < n) && (counted < count); i++) { + int gi = getGlyph(i - index); + if (gi == 65535) { + ignored++; + } else { + if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) { + glyphs [ counted++ ] = gi; + } else { + ignored++; + } + } + } + if ((counts != null) && (counts.length > 1)) { + counts[0] = counted; + counts[1] = ignored; + } + return glyphs; + } + + private int[] getGlyphsReverse(int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for (int i = start; (i >= 0) && (counted < count); i--) { + int gi = getGlyph(i - index); + if (gi == 65535) { + ignored++; + } else { + if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) { + glyphs [ counted++ ] = gi; + } else { + ignored++; + } + } + } + if ((counts != null) && (counts.length > 1)) { + counts[0] = counted; + counts[1] = ignored; + } + return glyphs; + } + + /** + * Obtain count glyphs starting at specified offset from current position. If + * offset is negative, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of glyphs to obtain + * @param glyphs array to use to fetch glyphs + * @param counts int[2] array to receive fetched glyph counts, where counts[0] will + * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs + * ignored + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphs(int offset, int count, int[] glyphs, int[] counts) throws IndexOutOfBoundsException { + return getGlyphs(offset, count, offset < 0, ignoreDefault, glyphs, counts); + } + + /** + * Obtain all glyphs starting from current position to end of input glyph sequence. + * @return array of available glyphs + * @throws IndexOutOfBoundsException if no glyph available + */ + public int[] getGlyphs() throws IndexOutOfBoundsException { + return getGlyphs(0, indexLast - index, false, null, null, null); + } + + /** + * Obtain count ignored glyphs starting at specified offset from current position. If + * reverseOrder is true, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of glyphs to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param glyphs array to use to fetch glyphs + * @param counts int[2] array to receive fetched glyph counts, where counts[0] will + * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs + * ignored + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getIgnoredGlyphs(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs, int[] counts) throws IndexOutOfBoundsException { + return getGlyphs(offset, count, reverseOrder, new NotGlyphTester(ignoreTester), glyphs, counts); + } + + /** + * Obtain count ignored glyphs starting at specified offset from current position. If offset is + * negative, then fetch in reverse order. + * @param offset from current position + * @param count number of glyphs to obtain + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getIgnoredGlyphs(int offset, int count) throws IndexOutOfBoundsException { + return getIgnoredGlyphs(offset, count, offset < 0, ignoreDefault, null, null); + } + + /** + * Determine if glyph at specified offset from current position is ignored. If offset is + * negative, then test in reverse order. + * @param offset from current position + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @return true if glyph is ignored + * @throws IndexOutOfBoundsException if offset results in an + * invalid index into input glyph sequence + */ + public boolean isIgnoredGlyph(int offset, GlyphTester ignoreTester) throws IndexOutOfBoundsException { + return (ignoreTester != null) && ignoreTester.test(getGlyph(offset), getLookupFlags()); + } + + /** + * Determine if glyph at specified offset from current position is ignored. If offset is + * negative, then test in reverse order. + * @param offset from current position + * @return true if glyph is ignored + * @throws IndexOutOfBoundsException if offset results in an + * invalid index into input glyph sequence + */ + public boolean isIgnoredGlyph(int offset) throws IndexOutOfBoundsException { + return isIgnoredGlyph(offset, ignoreDefault); + } + + /** + * Determine if glyph at current position is ignored. + * @return true if glyph is ignored + * @throws IndexOutOfBoundsException if offset results in an + * invalid index into input glyph sequence + */ + public boolean isIgnoredGlyph() throws IndexOutOfBoundsException { + return isIgnoredGlyph(getPosition()); + } + + /** + * Determine number of glyphs available starting at specified offset from current position. If + * reverseOrder is true, then search backwards in input glyph sequence. + * @param offset from current position + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs to count (or null, in which case none are ignored) + * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphsAvailable(int offset, boolean reverseOrder, GlyphTester ignoreTester) throws IndexOutOfBoundsException { + int start = index + offset; + if ((start < 0) || (start > indexLast)) { + return new int[] { 0, 0 }; + } else if (!reverseOrder) { + return getGlyphsAvailableForward(start, ignoreTester); + } else { + return getGlyphsAvailableReverse(start, ignoreTester); + } + } + + private int[] getGlyphsAvailableForward(int start, GlyphTester ignoreTester) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + if (ignoreTester == null) { + counted = indexLast - start; + } else { + for (int i = start, n = indexLast; i < n; i++) { + int gi = getGlyph(i - index); + if (gi == 65535) { + ignored++; + } else { + if (ignoreTester.test(gi, getLookupFlags())) { + ignored++; + } else { + counted++; + } + } + } + } + return new int[] { counted, ignored }; + } + + private int[] getGlyphsAvailableReverse(int start, GlyphTester ignoreTester) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + if (ignoreTester == null) { + counted = start + 1; + } else { + for (int i = start; i >= 0; i--) { + int gi = getGlyph(i - index); + if (gi == 65535) { + ignored++; + } else { + if (ignoreTester.test(gi, getLookupFlags())) { + ignored++; + } else { + counted++; + } + } + } + } + return new int[] { counted, ignored }; + } + + /** + * Determine number of glyphs available starting at specified offset from current position. If + * reverseOrder is true, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @param reverseOrder true if to obtain in reverse order + * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphsAvailable(int offset, boolean reverseOrder) throws IndexOutOfBoundsException { + return getGlyphsAvailable(offset, reverseOrder, ignoreDefault); + } + + /** + * Determine number of glyphs available starting at specified offset from current position. If + * offset is negative, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphsAvailable(int offset) throws IndexOutOfBoundsException { + return getGlyphsAvailable(offset, offset < 0); + } + + /** + * Obtain count character associations of glyphs starting at specified offset from current position. If + * reverseOrder is true, then associations are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of associations to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param associations array to use to fetch associations + * @param counts int[2] array to receive fetched association counts, where counts[0] will + * receive the number of associations obtained, and counts[1] will receive the number of glyphs whose + * associations were ignored + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public CharAssociation[] getAssociations(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, CharAssociation[] associations, int[] counts) + throws IndexOutOfBoundsException { + if (count < 0) { + count = getGlyphsAvailable(offset, reverseOrder, ignoreTester) [ 0 ]; + } + int start = index + offset; + if (start < 0) { + throw new IndexOutOfBoundsException("will attempt index at " + start); + } else if (!reverseOrder && ((start + count) > indexLast)) { + throw new IndexOutOfBoundsException("will attempt index at " + (start + count)); + } else if (reverseOrder && ((start + 1) < count)) { + throw new IndexOutOfBoundsException("will attempt index at " + (start - count)); + } + if (associations == null) { + associations = new CharAssociation [ count ]; + } else if (associations.length != count) { + throw new IllegalArgumentException("associations array is non-null, but its length (" + associations.length + "), is not equal to count (" + count + ")"); + } + if (!reverseOrder) { + return getAssociationsForward(start, count, ignoreTester, associations, counts); + } else { + return getAssociationsReverse(start, count, ignoreTester, associations, counts); + } + } + + private CharAssociation[] getAssociationsForward(int start, int count, GlyphTester ignoreTester, CharAssociation[] associations, int[] counts) + throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for (int i = start, n = indexLast, k = 0; i < n; i++) { + int gi = getGlyph(i - index); + if (gi == 65535) { + ignored++; + } else { + if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) { + if (k < count) { + associations [ k++ ] = getAssociation(i - index); + counted++; + } else { + break; + } + } else { + ignored++; + } + } + } + if ((counts != null) && (counts.length > 1)) { + counts[0] = counted; + counts[1] = ignored; + } + return associations; + } + + private CharAssociation[] getAssociationsReverse(int start, int count, GlyphTester ignoreTester, CharAssociation[] associations, int[] counts) + throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for (int i = start, k = 0; i >= 0; i--) { + int gi = getGlyph(i - index); + if (gi == 65535) { + ignored++; + } else { + if ((ignoreTester == null) || !ignoreTester.test(gi, getLookupFlags())) { + if (k < count) { + associations [ k++ ] = getAssociation(i - index); + counted++; + } else { + break; + } + } else { + ignored++; + } + } + } + if ((counts != null) && (counts.length > 1)) { + counts[0] = counted; + counts[1] = ignored; + } + return associations; + } + + /** + * Obtain count character associations of glyphs starting at specified offset from current position. If + * offset is negative, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @param count number of associations to obtain + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public CharAssociation[] getAssociations(int offset, int count) throws IndexOutOfBoundsException { + return getAssociations(offset, count, offset < 0, ignoreDefault, null, null); + } + + /** + * Obtain count character associations of ignored glyphs starting at specified offset from current position. If + * reverseOrder is true, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of character associations to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param associations array to use to fetch associations + * @param counts int[2] array to receive fetched association counts, where counts[0] will + * receive the number of associations obtained, and counts[1] will receive the number of glyphs whose + * associations were ignored + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public CharAssociation[] getIgnoredAssociations(int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, CharAssociation[] associations, int[] counts) + throws IndexOutOfBoundsException { + return getAssociations(offset, count, reverseOrder, new NotGlyphTester(ignoreTester), associations, counts); + } + + /** + * Obtain count character associations of ignored glyphs starting at specified offset from current position. If + * offset is negative, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @param count number of character associations to obtain + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public CharAssociation[] getIgnoredAssociations(int offset, int count) throws IndexOutOfBoundsException { + return getIgnoredAssociations(offset, count, offset < 0, ignoreDefault, null, null); + } + + /** + * Replace subsequence of input glyph sequence starting at specified offset from current position and of + * length count glyphs with a subsequence of the sequence gs starting from the specified + * offset gsOffset of length gsCount glyphs. + * @param offset from current position + * @param count number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence + * @param gs glyph sequence from which to obtain replacement glyphs + * @param gsOffset offset of first glyph in replacement sequence + * @param gsCount count of glyphs in replacement sequence starting at gsOffset + * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public boolean replaceInput(int offset, int count, GlyphSequence gs, int gsOffset, int gsCount) throws IndexOutOfBoundsException { + int nig = (igs != null) ? igs.getGlyphCount() : 0; + int position = getPosition() + offset; + if (position < 0) { + position = 0; + } else if (position > nig) { + position = nig; + } + if ((count < 0) || ((position + count) > nig)) { + count = nig - position; + } + int nrg = (gs != null) ? gs.getGlyphCount() : 0; + if (gsOffset < 0) { + gsOffset = 0; + } else if (gsOffset > nrg) { + gsOffset = nrg; + } + if ((gsCount < 0) || ((gsOffset + gsCount) > nrg)) { + gsCount = nrg - gsOffset; + } + int ng = nig + gsCount - count; + IntBuffer gb = IntBuffer.allocate(ng); + List al = new ArrayList<>(ng); + for (int i = 0, n = position; i < n; i++) { + gb.put(igs.getGlyph(i)); + al.add(igs.getAssociation(i)); + } + for (int i = gsOffset, n = gsOffset + gsCount; i < n; i++) { + gb.put(gs.getGlyph(i)); + al.add(gs.getAssociation(i)); + } + for (int i = position + count, n = nig; i < n; i++) { + gb.put(igs.getGlyph(i)); + al.add(igs.getAssociation(i)); + } + gb.flip(); + if (igs.compareGlyphs(gb) != 0) { + this.igs = new GlyphSequence(igs.getCharacters(), gb, al); + this.indexLast = gb.limit(); + return true; + } else { + return false; + } + } + + /** + * Replace subsequence of input glyph sequence starting at specified offset from current position and of + * length count glyphs with all glyphs in the replacement sequence gs. + * @param offset from current position + * @param count number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence + * @param gs glyph sequence from which to obtain replacement glyphs + * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public boolean replaceInput(int offset, int count, GlyphSequence gs) throws IndexOutOfBoundsException { + return replaceInput(offset, count, gs, 0, gs.getGlyphCount()); + } + + /** + * Erase glyphs in input glyph sequence starting at specified offset from current position, where each glyph + * in the specified glyphs array is matched, one at a time, and when a (forward searching) match is found + * in the input glyph sequence, the matching glyph is replaced with the glyph index 65535. + * @param offset from current position + * @param glyphs array of glyphs to erase + * @return the number of glyphs erased, which may be less than the number of specified glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int erase(int offset, int[] glyphs) throws IndexOutOfBoundsException { + int start = index + offset; + if ((start < 0) || (start > indexLast)) { + throw new IndexOutOfBoundsException("will attempt index at " + start); + } else { + int erased = 0; + for (int i = start - index, n = indexLast - start; i < n; i++) { + int gi = getGlyph(i); + if (gi == glyphs [ erased ]) { + setGlyph(i, 65535); + erased++; + } + } + return erased; + } + } + + /** + * Determine if is possible that the current input sequence satisfies a script specific + * context testing predicate. If no predicate applies, then application is always possible. + * @return true if no script specific context tester applies or if a specified tester returns + * true for the current input sequence context + */ + public boolean maybeApplicable() { + if (gct == null) { + return true; + } else { + return gct.test(script, language, feature, igs, index, getLookupFlags()); + } + } + + /** + * Apply default application semantices; namely, consume one glyph. + */ + public void applyDefault() { + consumed += 1; + } + + /** + * Determine if specified glyph is a base glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a base glyph; otherwise, false + */ + public boolean isBase(int gi) { + if (gdef != null) { + return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_BASE); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored base glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a base glyph; otherwise, false + */ + public boolean isIgnoredBase(int gi, int flags) { + return ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) && isBase(gi); + } + + /** + * Determine if specified glyph is an ligature glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isLigature(int gi) { + if (gdef != null) { + return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_LIGATURE); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored ligature glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isIgnoredLigature(int gi, int flags) { + return ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) && isLigature(gi); + } + + /** + * Determine if specified glyph is a mark glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a mark glyph; otherwise, false + */ + public boolean isMark(int gi) { + if (gdef != null) { + return gdef.isGlyphClass(gi, GlyphDefinitionTable.GLYPH_CLASS_MARK); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored ligature glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isIgnoredMark(int gi, int flags) { + if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) { + return isMark(gi); + } else if ((flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE) != 0) { + int lac = (flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE) >> 8; + int gac = gdef.getMarkAttachClass(gi); + return (gac != lac); + } else { + return false; + } + } + + /** + * Obtain an ignored glyph tester that corresponds to the specified lookup flags. + * @param flags lookup flags + * @return a glyph tester + */ + public GlyphTester getIgnoreTester(int flags) { + if ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) { + if ((flags & (GlyphSubtable.LF_IGNORE_LIGATURE | GlyphSubtable.LF_IGNORE_MARK)) == 0) { + return ignoreBase; + } else { + return getCombinedIgnoreTester(flags); + } + } + if ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) { + if ((flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_MARK)) == 0) { + return ignoreLigature; + } else { + return getCombinedIgnoreTester(flags); + } + } + if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) { + if ((flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_LIGATURE)) == 0) { + return ignoreMark; + } else { + return getCombinedIgnoreTester(flags); + } + } + return null; + } + + /** + * Obtain an ignored glyph tester that corresponds to the specified multiple (combined) lookup flags. + * @param flags lookup flags + * @return a glyph tester + */ + public GlyphTester getCombinedIgnoreTester(int flags) { + GlyphTester[] gta = new GlyphTester [ 3 ]; + int ngt = 0; + if ((flags & GlyphSubtable.LF_IGNORE_BASE) != 0) { + gta [ ngt++ ] = ignoreBase; + } + if ((flags & GlyphSubtable.LF_IGNORE_LIGATURE) != 0) { + gta [ ngt++ ] = ignoreLigature; + } + if ((flags & GlyphSubtable.LF_IGNORE_MARK) != 0) { + gta [ ngt++ ] = ignoreMark; + } + return getCombinedOrTester(gta, ngt); + } + + /** + * Obtain an combined OR glyph tester. + * @param gta an array of glyph testers + * @param ngt number of glyph testers present in specified array + * @return a combined OR glyph tester + */ + public GlyphTester getCombinedOrTester(GlyphTester[] gta, int ngt) { + if (ngt > 0) { + return new CombinedOrGlyphTester(gta, ngt); + } else { + return null; + } + } + + /** + * Obtain an combined AND glyph tester. + * @param gta an array of glyph testers + * @param ngt number of glyph testers present in specified array + * @return a combined AND glyph tester + */ + public GlyphTester getCombinedAndTester(GlyphTester[] gta, int ngt) { + if (ngt > 0) { + return new CombinedAndGlyphTester(gta, ngt); + } else { + return null; + } + } + + /** combined OR glyph tester */ + private static class CombinedOrGlyphTester implements GlyphTester { + private GlyphTester[] gta; + private int ngt; + CombinedOrGlyphTester(GlyphTester[] gta, int ngt) { + this.gta = gta; + this.ngt = ngt; + } + /** {@inheritDoc} */ + public boolean test(int gi, int flags) { + for (int i = 0, n = ngt; i < n; i++) { + GlyphTester gt = gta [ i ]; + if (gt != null) { + if (gt.test(gi, flags)) { + return true; + } + } + } + return false; + } + } + + /** combined AND glyph tester */ + private static class CombinedAndGlyphTester implements GlyphTester { + private GlyphTester[] gta; + private int ngt; + CombinedAndGlyphTester(GlyphTester[] gta, int ngt) { + this.gta = gta; + this.ngt = ngt; + } + /** {@inheritDoc} */ + public boolean test(int gi, int flags) { + for (int i = 0, n = ngt; i < n; i++) { + GlyphTester gt = gta [ i ]; + if (gt != null) { + if (!gt.test(gi, flags)) { + return false; + } + } + } + return true; + } + } + + /** NOT glyph tester */ + private static class NotGlyphTester implements GlyphTester { + private GlyphTester gt; + NotGlyphTester(GlyphTester gt) { + this.gt = gt; + } + /** {@inheritDoc} */ + public boolean test(int gi, int flags) { + if (gt != null) { + if (gt.test(gi, flags)) { + return false; + } + } + return true; + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitution.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitution.java new file mode 100644 index 00000000000..1137efe5e90 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitution.java @@ -0,0 +1,38 @@ +/* + * 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.advanced; + +/** + *

The GlyphSubstitution interface is implemented by a glyph substitution subtable + * that supports the determination of glyph substitution information based on script and + * language of the corresponding character content.

+ * + * @author Glenn Adams + */ +public interface GlyphSubstitution { + + /** + * Perform glyph substitution at the current index, mutating the substitution state object as required. + * Only the context associated with the current index is processed. + * @param ss glyph substitution state object + * @return true if the glyph subtable was applied, meaning that the current context matches the + * associated input context glyph coverage table + */ + boolean substitute(GlyphSubstitutionState ss); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionState.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionState.java new file mode 100644 index 00000000000..ab0426dfbca --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionState.java @@ -0,0 +1,248 @@ +/* + * 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.advanced; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +/** + *

The GlyphSubstitutionState implements an state object used during glyph substitution + * processing.

+ * + * @author Glenn Adams + */ +public class GlyphSubstitutionState extends GlyphProcessingState { + + /** alternates index */ + private int[] alternatesIndex; + /** current output glyph sequence */ + private IntBuffer ogb; + /** current output glyph to character associations */ + private List oal; + /** character association predications */ + private boolean predications; + + /** + * Construct default (reset) glyph substitution state. + */ + public GlyphSubstitutionState() { + } + + /** + * Construct glyph substitution state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param sct script context tester (or null) + */ + public GlyphSubstitutionState(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) { + super(gs, script, language, feature, sct); + this.ogb = IntBuffer.allocate(gs.getGlyphCount()); + this.oal = new ArrayList<>(gs.getGlyphCount()); + this.predications = gs.getPredications(); + } + + /** + * Construct glyph substitution state using an existing state object using shallow copy + * except as follows: input glyph sequence is copied deep except for its characters array. + * @param ss existing positioning state to copy from + */ + public GlyphSubstitutionState(GlyphSubstitutionState ss) { + super(ss); + this.ogb = IntBuffer.allocate(indexLast); + this.oal = new ArrayList<>(indexLast); + } + + /** + * Reset glyph substitution state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param sct script context tester (or null) + */ + public GlyphSubstitutionState reset(GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct) { + super.reset(gs, script, language, feature, sct); + this.alternatesIndex = null; + this.ogb = IntBuffer.allocate(gs.getGlyphCount()); + this.oal = new ArrayList<>(gs.getGlyphCount()); + this.predications = gs.getPredications(); + return this; + } + + /** + * Set alternates indices. + * @param alternates array of alternates indices ordered by coverage index + */ + public void setAlternates(int[] alternates) { + this.alternatesIndex = alternates; + } + + /** + * Obtain alternates index associated with specified coverage index. An alternates + * index is used to select among stylistic alternates of a glyph at a particular + * coverage index. This information must be provided by the document itself (in the + * form of an extension attribute value), since a font has no way to determine which + * alternate the user desires. + * @param ci coverage index + * @return an alternates index + */ + public int getAlternatesIndex(int ci) { + if (alternatesIndex == null) { + return 0; + } else if ((ci < 0) || (ci > alternatesIndex.length)) { + return 0; + } else { + return alternatesIndex [ ci ]; + } + } + + /** + * Put (write) glyph into glyph output buffer. + * @param glyph to write + * @param a character association that applies to glyph + * @param predication a predication value to add to association A if predications enabled + */ + public void putGlyph(int glyph, CharAssociation a, Object predication) { + if (!ogb.hasRemaining()) { + ogb = growBuffer(ogb); + } + ogb.put(glyph); + if (predications && (predication != null)) { + a.setPredication(feature, predication); + } + oal.add(a); + } + + /** + * Put (write) array of glyphs into glyph output buffer. + * @param glyphs to write + * @param associations array of character associations that apply to glyphs + * @param predication optional predicaion object to be associated with glyphs' associations + */ + public void putGlyphs(int[] glyphs, CharAssociation[] associations, Object predication) { + assert glyphs != null; + assert associations != null; + assert associations.length >= glyphs.length; + for (int i = 0, n = glyphs.length; i < n; i++) { + putGlyph(glyphs [ i ], associations [ i ], predication); + } + } + + /** + * Obtain output glyph sequence. + * @return newly constructed glyph sequence comprised of original + * characters, output glyphs, and output associations + */ + public GlyphSequence getOutput() { + int position = ogb.position(); + if (position > 0) { + ogb.limit(position); + ogb.rewind(); + return new GlyphSequence(igs.getCharacters(), ogb, oal); + } else { + return igs; + } + } + + /** + * Apply substitution subtable to current state at current position (only), + * resulting in the consumption of zero or more input glyphs, and possibly + * replacing the current input glyphs starting at the current position, in + * which case it is possible that indexLast is altered to be either less than + * or greater than its value prior to this application. + * @param st the glyph substitution subtable to apply + * @return true if subtable applied, or false if it did not (e.g., its + * input coverage table did not match current input context) + */ + public boolean apply(GlyphSubstitutionSubtable st) { + assert st != null; + updateSubtableState(st); + boolean applied = st.substitute(this); + return applied; + } + + /** + * Apply a sequence of matched rule lookups to the nig input glyphs + * starting at the current position. If lookups are non-null and non-empty, then + * all input glyphs specified by nig are consumed irregardless of + * whether any specified lookup applied. + * @param lookups array of matched lookups (or null) + * @param nig number of glyphs in input sequence, starting at current position, to which + * the lookups are to apply, and to be consumed once the application has finished + * @return true if lookups are non-null and non-empty; otherwise, false + */ + public boolean apply(AdvancedTypographicTable.RuleLookup[] lookups, int nig) { + // int nbg = index; + int nlg = indexLast - (index + nig); + int nog = 0; + if ((lookups != null) && (lookups.length > 0)) { + // apply each rule lookup to extracted input glyph array + for (int i = 0, n = lookups.length; i < n; i++) { + AdvancedTypographicTable.RuleLookup l = lookups [ i ]; + if (l != null) { + AdvancedTypographicTable.LookupTable lt = l.getLookup(); + if (lt != null) { + // perform substitution on a copy of previous state + GlyphSubstitutionState ss = new GlyphSubstitutionState(this); + // apply lookup table substitutions + GlyphSequence gs = lt.substitute(ss, l.getSequenceIndex()); + // replace current input sequence starting at current position with result + if (replaceInput(0, -1, gs)) { + nog = gs.getGlyphCount() - nlg; + } + } + } + } + // output glyphs and associations + putGlyphs(getGlyphs(0, nog, false, null, null, null), getAssociations(0, nog, false, null, null, null), null); + // consume replaced input glyphs + consume(nog); + return true; + } else { + return false; + } + } + + /** + * Apply default application semantices; namely, consume one input glyph, + * writing that glyph (and its association) to the output glyphs (and associations). + */ + public void applyDefault() { + super.applyDefault(); + int gi = getGlyph(); + if (gi != 65535) { + putGlyph(gi, getAssociation(), null); + } + } + + private static IntBuffer growBuffer(IntBuffer ib) { + int capacity = ib.capacity(); + int capacityNew = capacity * 2; + IntBuffer ibNew = IntBuffer.allocate(capacityNew); + ib.rewind(); + return ibNew.put(ib); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionSubtable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionSubtable.java new file mode 100644 index 00000000000..2d88306f0af --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionSubtable.java @@ -0,0 +1,129 @@ +/* + * 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.advanced; + +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +/** + *

The GlyphSubstitutionSubtable implements an abstract base of a glyph substitution subtable, + * providing a default implementation of the GlyphSubstitution interface.

+ * + * @author Glenn Adams + */ +public abstract class GlyphSubstitutionSubtable extends GlyphSubtable implements GlyphSubstitution { + + private static final GlyphSubstitutionState STATE = new GlyphSubstitutionState(); + + /** + * Instantiate a GlyphSubstitutionSubtable. + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + */ + protected GlyphSubstitutionSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getTableType() { + return AdvancedTypographicTable.GLYPH_TABLE_TYPE_SUBSTITUTION; + } + + /** {@inheritDoc} */ + @Override + public String getTypeName() { + return GlyphSubstitutionTable.getLookupTypeName(getType()); + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof GlyphSubstitutionSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean usesReverseScan() { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + return false; + } + + /** + * Apply substitutions using specified state and subtable array. For each position in input sequence, + * apply subtables in order until some subtable applies or none remain. If no subtable applied or no + * input was consumed for a given position, then apply default action (copy input glyph and advance). + * If sequenceIndex is non-negative, then apply subtables only when current position + * matches sequenceIndex in relation to the starting position. Furthermore, upon + * successful application at sequenceIndex, then apply default action for all remaining + * glyphs in input sequence. + * @param ss substitution state + * @param sta array of subtables to apply + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return output glyph sequence + */ + public static final GlyphSequence substitute(GlyphSubstitutionState ss, GlyphSubstitutionSubtable[] sta, int sequenceIndex) { + int sequenceStart = ss.getPosition(); + boolean appliedOneShot = false; + while (ss.hasNext()) { + boolean applied = false; + if (!appliedOneShot && ss.maybeApplicable()) { + for (int i = 0, n = sta.length; !applied && (i < n); i++) { + if (sequenceIndex < 0) { + applied = ss.apply(sta [ i ]); + } else if (ss.getPosition() == (sequenceStart + sequenceIndex)) { + applied = ss.apply(sta [ i ]); + if (applied) { + appliedOneShot = true; + } + } + } + } + if (!applied || !ss.didConsume()) { + ss.applyDefault(); + } + ss.next(); + } + return ss.getOutput(); + } + + /** + * Apply substitutions. + * @param gs input glyph sequence + * @param script tag + * @param language tag + * @param feature tag + * @param sta subtable array + * @param sct script context tester + * @return output glyph sequence + */ + public static final GlyphSequence substitute(GlyphSequence gs, String script, String language, String feature, GlyphSubstitutionSubtable[] sta, ScriptContextTester sct) { + synchronized (STATE) { + return substitute(STATE.reset(gs, script, language, feature, sct), sta, -1); + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionTable.java new file mode 100644 index 00000000000..924272955c4 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubstitutionTable.java @@ -0,0 +1,1494 @@ +/* + * 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.advanced; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.OpenTypeFont; +import org.apache.fontbox.ttf.TTFDataStream; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.*; +import org.apache.fontbox.ttf.advanced.api.AdvancedOpenTypeFont; +import org.apache.fontbox.ttf.advanced.scripts.ScriptProcessor; +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.GlyphTester; + +import static org.apache.fontbox.ttf.advanced.util.AdvancedChecker.*; + +/** + *

The GlyphSubstitutionTable class is a glyph table that implements + * GlyphSubstitution functionality.

+ * + *

Adapted from the Apache FOP Project.

+ * + * @author Glenn Adams + */ +public class GlyphSubstitutionTable extends AdvancedTypographicTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphSubstitutionTable.class); + + /** tag that identifies this table type */ + public static final String TAG = "GSUB"; + + /** single substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_SINGLE = 1; + /** multiple substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_MULTIPLE = 2; + /** alternate substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_ALTERNATE = 3; + /** ligature substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_LIGATURE = 4; + /** contextual substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_CONTEXTUAL = 5; + /** chained contextual substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 6; + /** extension substitution substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION = 7; + /** reverse chained contextual single substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE = 8; + + public GlyphSubstitutionTable(OpenTypeFont otf) { + super(otf, null, new java.util.HashMap<>(0)); + } + + /** + * Initialize this GlyphSubstitutionTable object using the specified lookups + * and subtables. + * @param gdef glyph definition table that applies + * @param lookups a map of lookup specifications to subtable identifier strings + * @param subtables a list of identified subtables + */ + public GlyphSubstitutionTable initialize(GlyphDefinitionTable gdef, Map> lookups, List subtables) { + initialize(lookups); + if ((subtables == null) || (subtables.isEmpty())) { + throw new AdvancedTypographicTableFormatException("subtables must be non-empty"); + } else { + for (GlyphSubtable o : subtables) { + if (o instanceof GlyphSubstitutionSubtable) { + addSubtable(o); + } else { + throw new AdvancedTypographicTableFormatException("subtable must be a glyph substitution subtable"); + } + } + freezeSubtables(); + return this; + } + } + + @Override + protected void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + if (ttf instanceof AdvancedOpenTypeFont) { + new AdvancedTypographicTableReader((AdvancedOpenTypeFont) ttf, this, data).read(); + this.initialized = true; + } + } + + /** + * Perform substitution processing using all matching lookups. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param features parameterized features + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute(GlyphSequence gs, String script, String language, Object[][] features) { + GlyphSequence ogs; + Map> lookups = matchLookups(script, language, "*"); + if ((lookups != null) && (lookups.size() > 0)) { + ScriptProcessor sp = ScriptProcessor.getInstance(script); + ogs = sp.substitute(this, gs, script, language, features, lookups); + } else { + ogs = gs; + } + return ogs; + } + + /** + * Map a lookup type name to its constant (integer) value. + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName(String name) { + int t; + String s = name.toLowerCase(); + if ("single".equals(s)) { + t = GSUB_LOOKUP_TYPE_SINGLE; + } else if ("multiple".equals(s)) { + t = GSUB_LOOKUP_TYPE_MULTIPLE; + } else if ("alternate".equals(s)) { + t = GSUB_LOOKUP_TYPE_ALTERNATE; + } else if ("ligature".equals(s)) { + t = GSUB_LOOKUP_TYPE_LIGATURE; + } else if ("contextual".equals(s)) { + t = GSUB_LOOKUP_TYPE_CONTEXTUAL; + } else if ("chainedcontextual".equals(s)) { + t = GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } else if ("extensionsubstitution".equals(s)) { + t = GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION; + } else if ("reversechainiingcontextualsingle".equals(s)) { + t = GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName(int type) { + String tn = null; + switch (type) { + case GSUB_LOOKUP_TYPE_SINGLE: + tn = "single"; + break; + case GSUB_LOOKUP_TYPE_MULTIPLE: + tn = "multiple"; + break; + case GSUB_LOOKUP_TYPE_ALTERNATE: + tn = "alternate"; + break; + case GSUB_LOOKUP_TYPE_LIGATURE: + tn = "ligature"; + break; + case GSUB_LOOKUP_TYPE_CONTEXTUAL: + tn = "contextual"; + break; + case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + tn = "chainedcontextual"; + break; + case GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION: + tn = "extensionsubstitution"; + break; + case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE: + tn = "reversechainiingcontextualsingle"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a substitution subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + GlyphSubtable st = null; + switch (type) { + case GSUB_LOOKUP_TYPE_SINGLE: + st = SingleSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GSUB_LOOKUP_TYPE_MULTIPLE: + st = MultipleSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GSUB_LOOKUP_TYPE_ALTERNATE: + st = AlternateSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GSUB_LOOKUP_TYPE_LIGATURE: + st = LigatureSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GSUB_LOOKUP_TYPE_CONTEXTUAL: + st = ContextualSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + st = ChainedContextualSubtable.create(id, sequence, flags, format, coverage, entries); + break; + case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE: + st = ReverseChainedSingleSubtable.create(id, sequence, flags, format, coverage, entries); + break; + default: + break; + } + return st; + } + + /** + * Create a substitution subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage list of coverage table entries + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, List coverage, List entries) { + return createSubtable(type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable(coverage), entries); + } + + private abstract static class SingleSubtable extends GlyphSubstitutionSubtable { + SingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_SINGLE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof SingleSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + int gi = ss.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + int go = getGlyphForCoverageIndex(ci, gi); + if ((go < 0) || (go > 65535)) { + go = 65535; + } + ss.putGlyph(go, ss.getAssociation(), Boolean.TRUE); + ss.consume(1); + return true; + } + } + + /** + * Obtain glyph for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return substituted glyph value + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract int getGlyphForCoverageIndex(int ci, int gi) throws IllegalArgumentException; + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new SingleSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new SingleSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class SingleSubtableFormat1 extends SingleSubtable { + private int delta; + private int ciMax; + SingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return mutableSingleton(SEInteger.valueOf(delta)); + } + + /** {@inheritDoc} */ + @Override + public int getGlyphForCoverageIndex(int ci, int gi) throws IllegalArgumentException { + if (ci <= ciMax) { + return gi + delta; + } else { + throw new IllegalArgumentException("coverage index " + ci + " out of range, maximum coverage index is " + ciMax); + } + } + + private void populate(List entries) { + checkSize(entries, 1); + this.delta = checkGet(entries, 0, SEInteger.class).get(); + this.ciMax = getCoverageSize() - 1; + } + } + + private static class SingleSubtableFormat2 extends SingleSubtable { + private int[] glyphs; + SingleSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return arrayMap(glyphs, SEInteger::valueOf); + } + + /** {@inheritDoc} */ + @Override + public int getGlyphForCoverageIndex(int ci, int gi) throws IllegalArgumentException { + if (glyphs == null) { + return -1; + } else if (ci >= glyphs.length) { + throw new IllegalArgumentException("coverage index " + ci + " out of range, maximum coverage index is " + glyphs.length); + } else { + return glyphs [ ci ]; + } + } + + private void populate(List entries) { + int i = 0; + int n = entries.size(); + int[] glyphs = new int [ n ]; + + for (int idx = 0; idx < n; idx++) { + int gid = checkGet(entries, idx, SEInteger.class).get(); + checkGidRange(gid, () -> "illegal glyph index: " + gid); + glyphs [ i++ ] = gid; + } + + assert i == n; + assert this.glyphs == null; + this.glyphs = glyphs; + } + } + + private abstract static class MultipleSubtable extends GlyphSubstitutionSubtable { + public MultipleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_MULTIPLE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof MultipleSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + int gi = ss.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + int[] ga = getGlyphsForCoverageIndex(ci, gi); + if (ga != null) { + ss.putGlyphs(ga, CharAssociation.replicate(ss.getAssociation(), ga.length), Boolean.TRUE); + ss.consume(1); + } + return true; + } + } + + /** + * Obtain glyph sequence for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return sequence of glyphs to substitute for input glyph + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract int[] getGlyphsForCoverageIndex(int ci, int gi) throws IllegalArgumentException; + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new MultipleSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MultipleSubtableFormat1 extends MultipleSubtable { + private int[][] gsa; // glyph sequence array, ordered by coverage index + MultipleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (gsa != null) { + return mutableSingleton(new SESequenceList(gsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public int[] getGlyphsForCoverageIndex(int ci, int gi) throws IllegalArgumentException { + if (gsa == null) { + return null; + } else if (ci >= gsa.length) { + throw new IllegalArgumentException("coverage index " + ci + " out of range, maximum coverage index is " + gsa.length); + } else { + return gsa [ ci ]; + } + } + + private void populate(List entries) { + checkSize(entries, 1); + gsa = checkGet(entries, 0, SESequenceList.class).get(); + } + } + + private abstract static class AlternateSubtable extends GlyphSubstitutionSubtable { + public AlternateSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_ALTERNATE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof AlternateSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + int gi = ss.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + int[] ga = getAlternatesForCoverageIndex(ci, gi); + int ai = ss.getAlternatesIndex(ci); + int go; + if ((ai < 0) || (ai >= ga.length)) { + go = gi; + } else { + go = ga [ ai ]; + } + if ((go < 0) || (go > 65535)) { + go = 65535; + } + ss.putGlyph(go, ss.getAssociation(), Boolean.TRUE); + ss.consume(1); + return true; + } + } + + /** + * Obtain glyph alternates for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return sequence of glyphs to substitute for input glyph + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract int[] getAlternatesForCoverageIndex(int ci, int gi) throws IllegalArgumentException; + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new AlternateSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class AlternateSubtableFormat1 extends AlternateSubtable { + private int[][] gaa; // glyph alternates array, ordered by coverage index + AlternateSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return arrayMap(gaa, SEIntList::new); + } + + /** {@inheritDoc} */ + @Override + public int[] getAlternatesForCoverageIndex(int ci, int gi) throws IllegalArgumentException { + if (gaa == null) { + return null; + } else if (ci >= gaa.length) { + throw new IllegalArgumentException("coverage index " + ci + " out of range, maximum coverage index is " + gaa.length); + } else { + return gaa [ ci ]; + } + } + private void populate(List entries) { + int i = 0; + int n = entries.size(); + int[][] gaa = new int [ n ][]; + for (int idx = 0; idx < n; idx++) { + gaa[i++] = checkGet(entries, idx, SEIntList.class).get(); + } + assert i == n; + assert this.gaa == null; + this.gaa = gaa; + } + } + + private abstract static class LigatureSubtable extends GlyphSubstitutionSubtable { + public LigatureSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_LIGATURE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof LigatureSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + int gi = ss.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + LigatureSet ls = getLigatureSetForCoverageIndex(ci, gi); + if (ls != null) { + boolean reverse = false; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable(0, reverse, ignores); + int nga = counts[0]; + int ngi; + if (nga > 1) { + int[] iga = ss.getGlyphs(0, nga, reverse, ignores, null, counts); + Ligature l = findLigature(ls, iga); + if (l != null) { + int go = l.getLigature(); + if ((go < 0) || (go > 65535)) { + go = 65535; + } + int nmg = 1 + l.getNumComponents(); + // fetch matched number of component glyphs to determine matched and ignored count + ss.getGlyphs(0, nmg, reverse, ignores, null, counts); + nga = counts[0]; + ngi = counts[1]; + // fetch associations of matched component glyphs + CharAssociation[] laa = ss.getAssociations(0, nga); + // output ligature glyph and its association + ss.putGlyph(go, CharAssociation.join(laa), Boolean.TRUE); + // fetch and output ignored glyphs (if necessary) + if (ngi > 0) { + ss.putGlyphs(ss.getIgnoredGlyphs(0, ngi), ss.getIgnoredAssociations(0, ngi), null); + } + ss.consume(nga + ngi); + } + } + } + return true; + } + } + + private Ligature findLigature(LigatureSet ls, int[] glyphs) { + Ligature[] la = ls.getLigatures(); + int k = -1; + int maxComponents = -1; + for (int i = 0, n = la.length; i < n; i++) { + Ligature l = la [ i ]; + if (l.matchesComponents(glyphs)) { + int nc = l.getNumComponents(); + if (nc > maxComponents) { + maxComponents = nc; + k = i; + } + } + } + if (k >= 0) { + return la [ k ]; + } else { + return null; + } + } + + /** + * Obtain ligature set for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return ligature set (or null if none defined) + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract LigatureSet getLigatureSetForCoverageIndex(int ci, int gi) throws IllegalArgumentException; + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new LigatureSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class LigatureSubtableFormat1 extends LigatureSubtable { + private LigatureSet[] ligatureSets; + public LigatureSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return arrayMap(ligatureSets, SELigatureSet::new); + } + + /** {@inheritDoc} */ + @Override + public LigatureSet getLigatureSetForCoverageIndex(int ci, int gi) throws IllegalArgumentException { + if (ligatureSets == null) { + return null; + } else if (ci >= ligatureSets.length) { + throw new IllegalArgumentException("coverage index " + ci + " out of range, maximum coverage index is " + ligatureSets.length); + } else { + return ligatureSets [ ci ]; + } + } + + private void populate(List entries) { + int i = 0; + int n = entries.size(); + LigatureSet[] ligatureSets = new LigatureSet [ n ]; + for (int idx = 0; idx < n; idx++) { + ligatureSets[i++] = checkGet(entries, idx, SELigatureSet.class).get(); + } + assert i == n; + assert this.ligatureSets == null; + this.ligatureSets = ligatureSets; + } + } + + private abstract static class ContextualSubtable extends GlyphSubstitutionSubtable { + public ContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_CONTEXTUAL; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof ContextualSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + int gi = ss.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + int[] rv = new int[1]; + RuleLookup[] la = getLookups(ci, gi, ss, rv); + if (la != null) { + ss.apply(la, rv[0]); + } + return true; + } + } + + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ss glyph substitution state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater, + * where the first entry is used to return the input sequence length of the matched rule + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv); + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new ContextualSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new ContextualSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else if (format == 3) { + return new ContextualSubtableFormat3(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ContextualSubtableFormat1 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) { + assert ss != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs(gi); + if (matches(ss, iga, 0, rv)) { + return r.getLookups(); + } + } + } + } + } + return null; + } + + static boolean matches(GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv) { + if ((glyphs == null) || (glyphs.length == 0)) { + return true; // match null or empty glyph sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable(offset, reverse, ignores); + int nga = counts[0]; + int ngm = glyphs.length; + if (nga < ngm) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ss.getGlyphs(offset, ngm, reverse, ignores, null, counts); + for (int k = 0; k < ngm; k++) { + if (ga [ k ] != glyphs [ k ]) { + return false; // match fails at ga [ k ] + } + } + if (rv != null) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private static class ContextualSubtableFormat2 extends ContextualSubtable { + private GlyphClassTable cdt; // class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + List entries = new ArrayList<>(3); + entries.add(new SEGlyphClassTable(cdt)); + entries.add(SEInteger.valueOf(ngc)); + entries.add(new SERuleSetList(rsa)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) { + assert ss != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedClassSequenceRule)) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ca = cr.getClasses(cdt.getClassIndex(gi, ss.getClassMatchSet(gi))); + if (matches(ss, cdt, ca, 0, rv)) { + return r.getLookups(); + } + } + } + } + } + return null; + } + + static boolean matches(GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv) { + if ((cdt == null) || (classes == null) || (classes.length == 0)) { + return true; // match null class definitions, null or empty class sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable(offset, reverse, ignores); + int nga = counts[0]; + int ngm = classes.length; + if (nga < ngm) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ss.getGlyphs(offset, ngm, reverse, ignores, null, counts); + for (int k = 0; k < ngm; k++) { + int gi = ga [ k ]; + int ms = ss.getClassMatchSet(gi); + int gc = cdt.getClassIndex(gi, ms); + if ((gc < 0) || (gc >= cdt.getClassSize(ms))) { + return false; // none or invalid class fails mat ch + } else if (gc != classes [ k ]) { + return false; // match fails at ga [ k ] + } + } + if (rv != null) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + + private void populate(List entries) { + checkSize(entries, 3); + cdt = checkGet(entries, 0, SEGlyphClassTable.class).get(); + ngc = checkGet(entries, 1, SEInteger.class).get(); + rsa = checkGet(entries, 2, SERuleSetList.class).get(); + if (rsa.length != ngc) { + throw new AdvancedTypographicTableFormatException("illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes"); + } + } + } + + private static class ContextualSubtableFormat3 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) { + assert ss != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] gca = cr.getCoverages(); + if (matches(ss, gca, 0, rv)) { + return r.getLookups(); + } + } + } + } + } + return null; + } + + static boolean matches(GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv) { + if ((gca == null) || (gca.length == 0)) { + return true; // match null or empty coverage array + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable(offset, reverse, ignores); + int nga = counts[0]; + int ngm = gca.length; + if (nga < ngm) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ss.getGlyphs(offset, ngm, reverse, ignores, null, counts); + for (int k = 0; k < ngm; k++) { + GlyphCoverageTable ct = gca [ k ]; + if (ct != null) { + if (ct.getCoverageIndex(ga [ k ]) < 0) { + return false; // match fails at ga [ k ] + } + } + } + if (rv != null) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private abstract static class ChainedContextualSubtable extends GlyphSubstitutionSubtable { + public ChainedContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof ChainedContextualSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean substitute(GlyphSubstitutionState ss) { + int gi = ss.getGlyph(); + int ci; + if ((ci = getCoverageIndex(gi)) < 0) { + return false; + } else { + int[] rv = new int[1]; + RuleLookup[] la = getLookups(ci, gi, ss, rv); + if (la != null) { + ss.apply(la, rv[0]); + return true; + } else { + return false; + } + } + } + + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ss glyph substitution state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv); + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new ChainedContextualSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else if (format == 2) { + return new ChainedContextualSubtableFormat2(id, sequence, flags, format, coverage, entries); + } else if (format == 3) { + return new ChainedContextualSubtableFormat3(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ChainedContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) { + assert ss != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs(gi); + if (matches(ss, iga, 0, rv)) { + int[] bga = cr.getBacktrackGlyphs(); + if (matches(ss, bga, -1, null)) { + int[] lga = cr.getLookaheadGlyphs(); + if (matches(ss, lga, rv[0], null)) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + + private boolean matches(GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv) { + return ContextualSubtableFormat1.matches(ss, glyphs, offset, rv); + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable { + private GlyphClassTable icdt; // input class def table + private GlyphClassTable bcdt; // backtrack class def table + private GlyphClassTable lcdt; // lookahead class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ChainedContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + List entries = new ArrayList<>(5); + entries.add(new SEGlyphClassTable(icdt)); + entries.add(new SEGlyphClassTable(bcdt)); + entries.add(new SEGlyphClassTable(lcdt)); + entries.add(SEInteger.valueOf(ngc)); + entries.add(new SERuleSetList(rsa)); + return entries; + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) { + assert ss != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + for (RuleSet rs : rsa) { + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra[i]; + if ((r != null) && (r instanceof ChainedClassSequenceRule)) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ica = cr.getClasses(icdt.getClassIndex(gi, ss.getClassMatchSet(gi))); + if (matches(ss, icdt, ica, 0, rv)) { + int[] bca = cr.getBacktrackClasses(); + if (matches(ss, bcdt, bca, -1, null)) { + int[] lca = cr.getLookaheadClasses(); + if (matches(ss, lcdt, lca, rv[0], null)) { + return r.getLookups(); + } + } + } + } + } + } + } + } + return null; + } + + private boolean matches(GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv) { + return ContextualSubtableFormat2.matches(ss, cdt, classes, offset, rv); + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + private void populate(List entries) { + checkSize(entries, 5); + icdt = checkGet(entries, 0, SEGlyphClassTable.class).get(); + bcdt = checkGet(entries, 1, SEGlyphClassTable.class).get(); + lcdt = checkGet(entries, 2, SEGlyphClassTable.class).get(); + ngc = checkGet(entries, 3, SEInteger.class).get(); + rsa = checkGet(entries, 4, SERuleSetList.class).get(); + if (rsa.length != ngc) { + throw new AdvancedTypographicTableFormatException("illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes"); + } + } + } + + private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ChainedContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + if (rsa != null) { + return mutableSingleton(new SERuleSetList(rsa)); + } else { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void resolveLookupReferences(Map lookupTables) { + AdvancedTypographicTable.resolveLookupReferences(rsa, lookupTables); + } + + /** {@inheritDoc} */ + @Override + public RuleLookup[] getLookups(int ci, int gi, GlyphSubstitutionState ss, int[] rv) { + assert ss != null; + assert (rv != null) && (rv.length > 0); + assert rsa != null; + if (rsa.length > 0) { + RuleSet rs = rsa [ 0 ]; + if (rs != null) { + Rule[] ra = rs.getRules(); + for (int i = 0, n = ra.length; i < n; i++) { + Rule r = ra [ i ]; + if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] igca = cr.getCoverages(); + if (matches(ss, igca, 0, rv)) { + GlyphCoverageTable[] bgca = cr.getBacktrackCoverages(); + if (matches(ss, bgca, -1, null)) { + GlyphCoverageTable[] lgca = cr.getLookaheadCoverages(); + if (matches(ss, lgca, rv[0], null)) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + + private boolean matches(GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv) { + return ContextualSubtableFormat3.matches(ss, gca, offset, rv); + } + + private void populate(List entries) { + checkSize(entries, 1); + rsa = checkGet(entries, 0, SERuleSetList.class).get(); + } + } + + private abstract static class ReverseChainedSingleSubtable extends GlyphSubstitutionSubtable { + public ReverseChainedSingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage); + } + + /** {@inheritDoc} */ + @Override + public int getType() { + return GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; + } + + /** {@inheritDoc} */ + @Override + public boolean isCompatible(GlyphSubtable subtable) { + return subtable instanceof ReverseChainedSingleSubtable; + } + + /** {@inheritDoc} */ + @Override + public boolean usesReverseScan() { + return true; + } + + static GlyphSubstitutionSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + if (format == 1) { + return new ReverseChainedSingleSubtableFormat1(id, sequence, flags, format, coverage, entries); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ReverseChainedSingleSubtableFormat1 extends ReverseChainedSingleSubtable { + ReverseChainedSingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) { + super(id, sequence, flags, format, coverage, entries); + populate(entries); + } + + /** {@inheritDoc} */ + @Override + public List getEntries() { + return null; + } + + private void populate(List entries) { + } + } + + /** + * The Ligature class implements a ligature lookup result in terms of + * a ligature glyph (code) and the N+1... components that comprise the ligature, + * where the Nth component was consumed in the coverage table lookup mapping to + * this ligature instance. + */ + public static class Ligature { + + private final int ligature; // (resulting) ligature glyph + private final int[] components; // component glyph codes (note that first component is implied) + + /** + * Instantiate a ligature. + * @param ligature glyph id + * @param components sequence of N+1... component glyph (or character) identifiers + */ + public Ligature(int ligature, int[] components) { + if ((ligature < 0) || (ligature > 65535)) { + throw new AdvancedTypographicTableFormatException("invalid ligature glyph index: " + ligature); + } else if (components == null) { + throw new AdvancedTypographicTableFormatException("invalid ligature components, must be non-null array"); + } else { + for (int i = 0, n = components.length; i < n; i++) { + int gc = components [ i ]; + if ((gc < 0) || (gc > 65535)) { + throw new AdvancedTypographicTableFormatException("invalid component glyph index: " + gc); + } + } + this.ligature = ligature; + this.components = components; + } + } + + /** @return ligature glyph id */ + public int getLigature() { + return ligature; + } + + /** @return array of N+1... components */ + public int[] getComponents() { + return components; + } + + /** @return components count */ + public int getNumComponents() { + return components.length; + } + + /** + * Determine if input sequence at offset matches ligature's components. + * @param glyphs array of glyph components to match (including first, implied glyph) + * @return true if matches + */ + public boolean matchesComponents(int[] glyphs) { + if (glyphs.length < (components.length + 1)) { + return false; + } else { + for (int i = 0, n = components.length; i < n; i++) { + if (glyphs [ i + 1 ] != components [ i ]) { + return false; + } + } + return true; + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{components={"); + for (int i = 0, n = components.length; i < n; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(Integer.toString(components[i])); + } + sb.append("},ligature="); + sb.append(Integer.toString(ligature)); + sb.append("}"); + return sb.toString(); + } + + } + + /** + * The LigatureSet class implements a set of ligatures. + */ + public static class LigatureSet { + + private final Ligature[] ligatures; // set of ligatures all of which share the first (implied) component + private final int maxComponents; // maximum number of components (including first) + + /** + * Instantiate a set of ligatures. + * @param ligatures collection of ligatures + */ + public LigatureSet(List ligatures) { + this(ligatures.toArray(new Ligature [ ligatures.size() ])); + } + + /** + * Instantiate a set of ligatures. + * @param ligatures array of ligatures + */ + public LigatureSet(Ligature[] ligatures) { + if (ligatures == null) { + throw new AdvancedTypographicTableFormatException("invalid ligatures, must be non-null array"); + } else { + this.ligatures = ligatures; + int ncMax = -1; + for (int i = 0, n = ligatures.length; i < n; i++) { + Ligature l = ligatures [ i ]; + int nc = l.getNumComponents() + 1; + if (nc > ncMax) { + ncMax = nc; + } + } + maxComponents = ncMax; + } + } + + /** @return array of ligatures in this ligature set */ + public Ligature[] getLigatures() { + return ligatures; + } + + /** @return count of ligatures in this ligature set */ + public int getNumLigatures() { + return ligatures.length; + } + + /** @return maximum number of components in one ligature (including first component) */ + public int getMaxComponents() { + return maxComponents; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ligs={"); + for (int i = 0, n = ligatures.length; i < n; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(ligatures[i]); + } + sb.append("}}"); + return sb.toString(); + } + + } + +} + diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubtable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubtable.java new file mode 100644 index 00000000000..dbb462d3d74 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphSubtable.java @@ -0,0 +1,315 @@ +/* + * 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.advanced; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Map; + +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SubtableEntry; + +/** + *

The GlyphSubtable implements an abstract glyph subtable that + * encapsulates identification, type, format, and coverage information.

+ * + * @author Glenn Adams + */ +public abstract class GlyphSubtable implements Comparable { + + /** lookup flag - right to left */ + public static final int LF_RIGHT_TO_LEFT = 0x0001; + /** lookup flag - ignore base glyphs */ + public static final int LF_IGNORE_BASE = 0x0002; + /** lookup flag - ignore ligatures */ + public static final int LF_IGNORE_LIGATURE = 0x0004; + /** lookup flag - ignore marks */ + public static final int LF_IGNORE_MARK = 0x0008; + /** lookup flag - use mark filtering set */ + public static final int LF_USE_MARK_FILTERING_SET = 0x0010; + /** lookup flag - reserved */ + public static final int LF_RESERVED = 0x0E00; + /** lookup flag - mark attachment type */ + public static final int LF_MARK_ATTACHMENT_TYPE = 0xFF00; + /** internal flag - use reverse scan */ + public static final int LF_INTERNAL_USE_REVERSE_SCAN = 0x10000; + + /** lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list; shared by multiple subtables in a single lookup */ + private String lookupId; + /** subtable sequence (index) number in lookup, zero based */ + private int sequence; + /** subtable flags */ + private int flags; + /** subtable format */ + private int format; + /** subtable mapping table */ + private GlyphMappingTable mapping; + /** weak reference to parent (gsub or gpos) table */ + private WeakReference table; + + /** + * Instantiate this glyph subtable. + * @param lookupId lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list + * @param sequence subtable sequence (within lookup), starting with zero + * @param flags subtable flags + * @param format subtable format + * @param mapping subtable mapping table + */ + protected GlyphSubtable(String lookupId, int sequence, int flags, int format, GlyphMappingTable mapping) + { + if ((lookupId == null) || (lookupId.length() == 0)) { + throw new AdvancedTypographicTableFormatException("invalid lookup identifier, must be non-empty string"); + } else if (mapping == null) { + throw new AdvancedTypographicTableFormatException("invalid mapping table, must not be null"); + } else { + this.lookupId = lookupId; + this.sequence = sequence; + this.flags = flags; + this.format = format; + this.mapping = mapping; + } + } + + /** @return this subtable's lookup identifer */ + public String getLookupId() { + return lookupId; + } + + /** @return this subtable's table type */ + public abstract int getTableType(); + + /** @return this subtable's type */ + public abstract int getType(); + + /** @return this subtable's type name */ + public abstract String getTypeName(); + + /** + * Determine if a glyph subtable is compatible with this glyph subtable. Two glyph subtables are + * compatible if the both may appear in a single lookup table. + * @param subtable a glyph subtable to determine compatibility + * @return true if specified subtable is compatible with this glyph subtable, where by compatible + * is meant that they share the same lookup type + */ + public abstract boolean isCompatible(GlyphSubtable subtable); + + /** @return true if subtable uses reverse scanning of glyph sequence, meaning from the last glyph + * in a glyph sequence to the first glyph + */ + public abstract boolean usesReverseScan(); + + /** @return this subtable's sequence (index) within lookup */ + public int getSequence() { + return sequence; + } + + /** @return this subtable's flags */ + public int getFlags() { + return flags; + } + + /** @return this subtable's format */ + public int getFormat() { + return format; + } + + /** @return this subtable's governing glyph definition table or null if none available */ + public GlyphDefinitionTable getGDEF() { + AdvancedTypographicTable gt = getTable(); + if (gt != null) { + return gt.getGlyphDefinitions(); + } else { + return null; + } + } + + /** @return this subtable's coverage mapping or null if mapping is not a coverage mapping */ + public GlyphCoverageMapping getCoverage() { + if (mapping instanceof GlyphCoverageMapping) { + return (GlyphCoverageMapping) mapping; + } else { + return null; + } + } + + /** @return this subtable's class mapping or null if mapping is not a class mapping */ + public GlyphClassMapping getClasses() { + if (mapping instanceof GlyphClassMapping) { + return (GlyphClassMapping) mapping; + } else { + return null; + } + } + + /** @return this subtable's lookup entries */ + public abstract List getEntries(); + + /** @return this subtable's parent table (or null if undefined) */ + public synchronized AdvancedTypographicTable getTable() { + WeakReference r = this.table; + return (r != null) ? (AdvancedTypographicTable) r.get() : null; + } + + /** + * Establish a weak reference from this subtable to its parent + * table. If table parameter is specified as null, then + * clear and remove weak reference. + * @param table the table or null + * @throws IllegalStateException if table is already set to non-null + */ + public synchronized void setTable(AdvancedTypographicTable table) throws IllegalStateException { + WeakReference r = this.table; + if (table == null) { + this.table = null; + if (r != null) { + r.clear(); + } + } else if (r == null) { + this.table = new WeakReference<>(table); + } else { + throw new IllegalStateException("table already set"); + } + } + + /** + * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences(Map lookupTables) { + } + + /** + * Map glyph id to coverage index. + * @param gid glyph id + * @return the corresponding coverage index of the specified glyph id + */ + public int getCoverageIndex(int gid) { + if (mapping instanceof GlyphCoverageMapping) { + return ((GlyphCoverageMapping) mapping) .getCoverageIndex(gid); + } else { + return -1; + } + } + + /** + * Map glyph id to coverage index. + * @return the corresponding coverage index of the specified glyph id + */ + public int getCoverageSize() { + if (mapping instanceof GlyphCoverageMapping) { + return ((GlyphCoverageMapping) mapping) .getCoverageSize(); + } else { + return 0; + } + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + int hc = sequence; + hc = (hc * 3) + (lookupId.hashCode() ^ hc); + return hc; + } + + /** + * {@inheritDoc} + * @return true if the lookup identifier and the sequence number of the specified subtable is the same + * as the lookup identifier and sequence number of this subtable + */ + @Override + public boolean equals(Object o) { + if (o instanceof GlyphSubtable) { + GlyphSubtable st = (GlyphSubtable) o; + return lookupId.equals(st.lookupId) && (sequence == st.sequence); + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return '{' + getSequence() + "=" + getClass().getSimpleName() + '}'; + } + + /** + * {@inheritDoc} + * @return the result of comparing the lookup identifier and the sequence number of the specified subtable with + * the lookup identifier and sequence number of this subtable + */ + @Override + public int compareTo(GlyphSubtable st) { + int d; + if ((d = lookupId.compareTo(st.lookupId)) == 0) { + if (sequence < st.sequence) { + d = -1; + } else if (sequence > st.sequence) { + d = 1; + } + } + return d; + } + + /** + * Determine if any of the specified subtables uses reverse scanning. + * @param subtables array of glyph subtables + * @return true if any of the specified subtables uses reverse scanning. + */ + public static boolean usesReverseScan(GlyphSubtable[] subtables) { + if ((subtables == null) || (subtables.length == 0)) { + return false; + } else { + for (int i = 0, n = subtables.length; i < n; i++) { + if (subtables[i].usesReverseScan()) { + return true; + } + } + return false; + } + } + + /** + * Determine consistent flags for a set of subtables. + * @param subtables array of glyph subtables + * @return consistent flags + * @throws IllegalStateException if inconsistent flags + */ + public static int getFlags(GlyphSubtable[] subtables) throws IllegalStateException { + if ((subtables == null) || (subtables.length == 0)) { + return 0; + } else { + int flags = 0; + // obtain first non-zero value of flags in array of subtables + for (int i = 0, n = subtables.length; i < n; i++) { + int f = subtables[i].getFlags(); + if (flags == 0) { + flags = f; + break; + } + } + // enforce flag consistency + for (int i = 0, n = subtables.length; i < n; i++) { + int f = subtables[i].getFlags(); + if (f != flags) { + throw new IllegalStateException("inconsistent lookup flags " + f + ", expected " + flags); + } + } + return flags | (usesReverseScan(subtables) ? LF_INTERNAL_USE_REVERSE_SCAN : 0); + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphVectorAdvanced.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphVectorAdvanced.java new file mode 100644 index 00000000000..465a709d5a8 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphVectorAdvanced.java @@ -0,0 +1,70 @@ +package org.apache.fontbox.ttf.advanced; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.fontbox.ttf.advanced.api.GlyphVector; + +/** + * TODO + */ +public class GlyphVectorAdvanced implements GlyphVector { + private final int[] glyphs; + private final float width; + private final int[][] adjustments; + + /** TODO */ + public GlyphVectorAdvanced(int[] glyphs, float width, int[][] adjustments) + { + this.adjustments = adjustments; + this.width = width; + this.glyphs = glyphs; + } + + /** {@inheritDoc} */ + @Override + public float getWidth() + { + return width; + } + + /** TODO */ + public Set getGlyphs() + { + Set gset = new LinkedHashSet(glyphs.length); + for (int i = 0; i < glyphs.length; i++) { + gset.add(glyphs[i]); + } + return gset; + } + + /** + * TODO + */ + public int[] getGlyphArray() { + return glyphs; + } + + /** TODO */ + public int[][] getAdjustments() + { + return adjustments; + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("{ width: ") + .append(width) + .append(", gids: [") + .append(glyphs) + .append("], adjustments: [") + .append(Arrays.deepToString(adjustments)) + .append("] }"); + return sb.toString(); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphVectorSimple.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphVectorSimple.java new file mode 100644 index 00000000000..0044f95ed93 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/GlyphVectorSimple.java @@ -0,0 +1,23 @@ +package org.apache.fontbox.ttf.advanced; + +import org.apache.fontbox.ttf.advanced.api.GlyphVector; + +/** + * TODO + */ +public class GlyphVectorSimple implements GlyphVector { + + public GlyphVectorSimple(Object object) + { + // TODO + } + + /** {@inheritDoc} */ + @Override + public float getWidth() + { + // TODO Auto-generated method stub + return 0; + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/IncompatibleSubtableException.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/IncompatibleSubtableException.java new file mode 100644 index 00000000000..08f21beba1e --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/IncompatibleSubtableException.java @@ -0,0 +1,40 @@ +/* + * 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.advanced; + +/** + *

Exception thrown during when attempting to map glyphs to associated characters + * in the case that the associated characters do not represent a compact interval.

+ * + * @author Glenn Adams + */ +public class IncompatibleSubtableException extends RuntimeException { + /** + * Instantiate incompatible subtable exception + */ + public IncompatibleSubtableException() { + super(); + } + /** + * Instantiate incompatible subtable exception + * @param message a message string + */ + public IncompatibleSubtableException(String message) { + super(message); + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/OTFLanguage.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/OTFLanguage.java new file mode 100644 index 00000000000..ffd9924fe60 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/OTFLanguage.java @@ -0,0 +1,432 @@ +/* + * 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.advanced; + +/** + *

Language system tags defined by OTF specification. Note that this set and their + * values do not correspond with ISO639* or any other language registry.

+ * + * @author Glenn Adams + */ +public final class OTFLanguage { + public static final String ABAZA = "ABA"; + public static final String ABKHAZIAN = "ABK"; + public static final String ADYGHE = "ADY"; + public static final String AFRIKAANS = "AFK"; + public static final String AFAR = "AFR"; + public static final String AGAW = "AGW"; + public static final String ALSATIAN = "ALS"; + public static final String ALTAI = "ALT"; + public static final String AMHARIC = "AMH"; + public static final String PHONETIC_AMERICANIST = "APPH"; + public static final String ARABIC = "ARA"; + public static final String AARI = "ARI"; + public static final String ARAKANESE = "ARK"; + public static final String ASSAMESE = "ASM"; + public static final String ATHAPASKAN = "ATH"; + public static final String AVAR = "AVR"; + public static final String AWADHI = "AWA"; + public static final String AYMARA = "AYM"; + public static final String AZERI = "AZE"; + public static final String BADAGA = "BAD"; + public static final String BAGHELKHANDI = "BAG"; + public static final String BALKAR = "BAL"; + public static final String BAULE = "BAU"; + public static final String BERBER = "BBR"; + public static final String BENCH = "BCH"; + public static final String BIBLE_CREE = "BCR"; + public static final String BELARUSSIAN = "BEL"; + public static final String BEMBA = "BEM"; + public static final String BENGALI = "BEN"; + public static final String BULGARIAN = "BGR"; + public static final String BHILI = "BHI"; + public static final String BHOJPURI = "BHO"; + public static final String BIKOL = "BIK"; + public static final String BILEN = "BIL"; + public static final String BLACKFOOT = "BKF"; + public static final String BALOCHI = "BLI"; + public static final String BALANTE = "BLN"; + public static final String BALTI = "BLT"; + public static final String BAMBARA = "BMB"; + public static final String BAMILEKE = "BML"; + public static final String BOSNIAN = "BOS"; + public static final String BRETON = "BRE"; + public static final String BRAHUI = "BRH"; + public static final String BRAJ_BHASHA = "BRI"; + public static final String BURMESE = "BRM"; + public static final String BASHKIR = "BSH"; + public static final String BETI = "BTI"; + public static final String CATALAN = "CAT"; + public static final String CEBUANO = "CEB"; + public static final String CHECHEN = "CHE"; + public static final String CHAHA_GURAGE = "CHG"; + public static final String CHATTISGARHI = "CHH"; + public static final String CHICHEWA = "CHI"; + public static final String CHUKCHI = "CHK"; + public static final String CHIPEWYAN = "CHP"; + public static final String CHEROKEE = "CHR"; + public static final String CHUVASH = "CHU"; + public static final String COMORIAN = "CMR"; + public static final String COPTIC = "COP"; + public static final String CORSICAN = "COS"; + public static final String CREE = "CRE"; + public static final String CARRIER = "CRR"; + public static final String CRIMEAN_TATAR = "CRT"; + public static final String CHURCH_SLAVONIC = "CSL"; + public static final String CZECH = "CSY"; + public static final String DANISH = "DAN"; + public static final String DARGWA = "DAR"; + public static final String WOODS_CREE = "DCR"; + public static final String GERMAN = "DEU"; + public static final String DEFAULT = "dflt"; + public static final String DOGRI = "DGR"; + public static final String DHIVEHI_DEPRECATED = "DHV"; + public static final String DHIVEHI = "DIV"; + public static final String DJERMA = "DJR"; + public static final String DANGME = "DNG"; + public static final String DINKA = "DNK"; + public static final String DARI = "DRI"; + public static final String DUNGAN = "DUN"; + public static final String DZONGKHA = "DZN"; + public static final String EBIRA = "EBI"; + public static final String EASTERN_CREE = "ECR"; + public static final String EDO = "EDO"; + public static final String EFIK = "EFI"; + public static final String GREEK = "ELL"; + public static final String ENGLISH = "ENG"; + public static final String ERZYA = "ERZ"; + public static final String SPANISH = "ESP"; + public static final String ESTONIAN = "ETI"; + public static final String BASQUE = "EUQ"; + public static final String EVENKI = "EVK"; + public static final String EVEN = "EVN"; + public static final String EWE = "EWE"; + public static final String FRENCH_ANTILLEAN = "FAN"; + public static final String FARSI = "FAR"; + public static final String FINNISH = "FIN"; + public static final String FIJIAN = "FJI"; + public static final String FLEMISH = "FLE"; + public static final String FOREST_NENETS = "FNE"; + public static final String FON = "FON"; + public static final String FAROESE = "FOS"; + public static final String FRENCH = "FRA"; + public static final String FRISIAN = "FRI"; + public static final String FRIULIAN = "FRL"; + public static final String FUTA = "FTA"; + public static final String FULANI = "FUL"; + public static final String GA = "GAD"; + public static final String GAELIC = "GAE"; + public static final String GAGAUZ = "GAG"; + public static final String GALICIAN = "GAL"; + public static final String GARSHUNI = "GAR"; + public static final String GARHWALI = "GAW"; + public static final String GEEZ = "GEZ"; + public static final String GILYAK = "GIL"; + public static final String GUMUZ = "GMZ"; + public static final String GONDI = "GON"; + public static final String GREENLANDIC = "GRN"; + public static final String GARO = "GRO"; + public static final String GUARANI = "GUA"; + public static final String GUJARATI = "GUJ"; + public static final String HAITIAN = "HAI"; + public static final String HALAM = "HAL"; + public static final String HARAUTI = "HAR"; + public static final String HAUSA = "HAU"; + public static final String HAWAIIN = "HAW"; + public static final String HAMMER_BANNA = "HBN"; + public static final String HILIGAYNON = "HIL"; + public static final String HINDI = "HIN"; + public static final String HIGH_MARI = "HMA"; + public static final String HINDKO = "HND"; + public static final String HO = "HO"; + public static final String HARARI = "HRI"; + public static final String CROATIAN = "HRV"; + public static final String HUNGARIAN = "HUN"; + public static final String ARMENIAN = "HYE"; + public static final String IGBO = "IBO"; + public static final String IJO = "IJO"; + public static final String ILOKANO = "ILO"; + public static final String INDONESIAN = "IND"; + public static final String INGUSH = "ING"; + public static final String INUKTITUT = "INU"; + public static final String PHONETIC_IPA = "IPPH"; + public static final String IRISH = "IRI"; + public static final String IRISH_TRADITIONAL = "IRT"; + public static final String ICELANDIC = "ISL"; + public static final String INARI_SAMI = "ISM"; + public static final String ITALIAN = "ITA"; + public static final String HEBREW = "IWR"; + public static final String JAVANESE = "JAV"; + public static final String YIDDISH = "JII"; + public static final String JAPANESE = "JAN"; + public static final String JUDEZMO = "JUD"; + public static final String JULA = "JUL"; + public static final String KABARDIAN = "KAB"; + public static final String KACHCHI = "KAC"; + public static final String KALENJIN = "KAL"; + public static final String KANNADA = "KAN"; + public static final String KARACHAY = "KAR"; + public static final String GEORGIAN = "KAT"; + public static final String KAZAKH = "KAZ"; + public static final String KEBENA = "KEB"; + public static final String KHUTSURI_GEORGIAN = "KGE"; + public static final String KHAKASS = "KHA"; + public static final String KHANTY_KAZIM = "KHK"; + public static final String KHMER = "KHM"; + public static final String KHANTY_SHURISHKAR = "KHS"; + public static final String KHANTY_VAKHI = "KHV"; + public static final String KHOWAR = "KHW"; + public static final String KIKUYU = "KIK"; + public static final String KIRGHIZ = "KIR"; + public static final String KISII = "KIS"; + public static final String KOKNI = "KKN"; + public static final String KALMYK = "KLM"; + public static final String KAMBA = "KMB"; + public static final String KUMAONI = "KMN"; + public static final String KOMO = "KMO"; + public static final String KOMSO = "KMS"; + public static final String KANURI = "KNR"; + public static final String KODAGU = "KOD"; + public static final String KOREAN_OLD_HANGUL = "KOH"; + public static final String KONKANI = "KOK"; + public static final String KIKONGO = "KON"; + public static final String KOMI_PERMYAK = "KOP"; + public static final String KOREAN = "KOR"; + public static final String KOMI_ZYRIAN = "KOZ"; + public static final String KPELLE = "KPL"; + public static final String KRIO = "KRI"; + public static final String KARAKALPAK = "KRK"; + public static final String KARELIAN = "KRL"; + public static final String KARAIM = "KRM"; + public static final String KAREN = "KRN"; + public static final String KOORETE = "KRT"; + public static final String KASHMIRI = "KSH"; + public static final String KHASI = "KSI"; + public static final String KILDIN_SAMI = "KSM"; + public static final String KUI = "KUI"; + public static final String KULVI = "KUL"; + public static final String KUMYK = "KUM"; + public static final String KURDISH = "KUR"; + public static final String KURUKH = "KUU"; + public static final String KUY = "KUY"; + public static final String KORYAK = "KYK"; + public static final String LADIN = "LAD"; + public static final String LAHULI = "LAH"; + public static final String LAK = "LAK"; + public static final String LAMBANI = "LAM"; + public static final String LAO = "LAO"; + public static final String LATIN = "LAT"; + public static final String LAZ = "LAZ"; + public static final String L_CREE = "LCR"; + public static final String LADAKHI = "LDK"; + public static final String LEZGI = "LEZ"; + public static final String LINGALA = "LIN"; + public static final String LOW_MARI = "LMA"; + public static final String LIMBU = "LMB"; + public static final String LOMWE = "LMW"; + public static final String LOWER_SORBIAN = "LSB"; + public static final String LULE_SAMI = "LSM"; + public static final String LITHUANIAN = "LTH"; + public static final String LUXEMBOURGISH = "LTZ"; + public static final String LUBA = "LUB"; + public static final String LUGANDA = "LUG"; + public static final String LUHYA = "LUH"; + public static final String LUO = "LUO"; + public static final String LATVIAN = "LVI"; + public static final String MAJANG = "MAJ"; + public static final String MAKUA = "MAK"; + public static final String MALAYALAM_TRADITIONAL = "MAL"; + public static final String MANSI = "MAN"; + public static final String MAPUDUNGUN = "MAP"; + public static final String MARATHI = "MAR"; + public static final String MARWARI = "MAW"; + public static final String MBUNDU = "MBN"; + public static final String MANCHU = "MCH"; + public static final String MOOSE_CREE = "MCR"; + public static final String MENDE = "MDE"; + public static final String MEEN = "MEN"; + public static final String MIZO = "MIZ"; + public static final String MACEDONIAN = "MKD"; + public static final String MALE = "MLE"; + public static final String MALAGASY = "MLG"; + public static final String MALINKE = "MLN"; + public static final String MALAYALAM_REFORMED = "MLR"; + public static final String MALAY = "MLY"; + public static final String MANDINKA = "MND"; + public static final String MONGOLIAN = "MNG"; + public static final String MANIPURI = "MNI"; + public static final String MANINKA = "MNK"; + public static final String MANX_GAELIC = "MNX"; + public static final String MOHAWK = "MOH"; + public static final String MOKSHA = "MOK"; + public static final String MOLDAVIAN = "MOL"; + public static final String MON = "MON"; + public static final String MOROCCAN = "MOR"; + public static final String MAORI = "MRI"; + public static final String MAITHILI = "MTH"; + public static final String MALTESE = "MTS"; + public static final String MUNDARI = "MUN"; + public static final String NAGA_ASSAMESE = "NAG"; + public static final String NANAI = "NAN"; + public static final String NASKAPI = "NAS"; + public static final String N_CREE = "NCR"; + public static final String NDEBELE = "NDB"; + public static final String NDONGA = "NDG"; + public static final String NEPALI = "NEP"; + public static final String NEWARI = "NEW"; + public static final String NAGARI = "NGR"; + public static final String NORWAY_HOUSE_CREE = "NHC"; + public static final String NISI = "NIS"; + public static final String NIUEAN = "NIU"; + public static final String NKOLE = "NKL"; + public static final String NKO = "NKO"; + public static final String DUTCH = "NLD"; + public static final String NOGAI = "NOG"; + public static final String NORWEGIAN = "NOR"; + public static final String NORTHERN_SAMI = "NSM"; + public static final String NORTHERN_TAI = "NTA"; + public static final String ESPERANTO = "NTO"; + public static final String NYNORSK = "NYN"; + public static final String OCCITAN = "OCI"; + public static final String OJI_CREE = "OCR"; + public static final String OJIBWAY = "OJB"; + public static final String ORIYA = "ORI"; + public static final String OROMO = "ORO"; + public static final String OSSETIAN = "OSS"; + public static final String PALESTINIAN_ARAMAIC = "PAA"; + public static final String PALI = "PAL"; + public static final String PUNJABI = "PAN"; + public static final String PALPA = "PAP"; + public static final String PASHTO = "PAS"; + public static final String POLYTONIC_GREEK = "PGR"; + public static final String FILIPINO = "PIL"; + public static final String PALAUNG = "PLG"; + public static final String POLISH = "PLK"; + public static final String PROVENCAL = "PRO"; + public static final String PORTUGUESE = "PTG"; + public static final String CHIN = "QIN"; + public static final String RAJASTHANI = "RAJ"; + public static final String R_CREE = "RCR"; + public static final String RUSSIAN_BURIAT = "RBU"; + public static final String RIANG = "RIA"; + public static final String RHAETO_ROMANIC = "RMS"; + public static final String ROMANIAN = "ROM"; + public static final String ROMANY = "ROY"; + public static final String RUSYN = "RSY"; + public static final String RUANDA = "RUA"; + public static final String RUSSIAN = "RUS"; + public static final String SADRI = "SAD"; + public static final String SANSKRIT = "SAN"; + public static final String SANTALI = "SAT"; + public static final String SAYISI = "SAY"; + public static final String SEKOTA = "SEK"; + public static final String SELKUP = "SEL"; + public static final String SANGO = "SGO"; + public static final String SHAN = "SHN"; + public static final String SIBE = "SIB"; + public static final String SIDAMO = "SID"; + public static final String SILTE_GURAGE = "SIG"; + public static final String SKOLT_SAMI = "SKS"; + public static final String SLOVAK = "SKY"; + public static final String SLAVEY = "SLA"; + public static final String SLOVENIAN = "SLV"; + public static final String SOMALI = "SML"; + public static final String SAMOAN = "SMO"; + public static final String SENA = "SNA"; + public static final String SINDHI = "SND"; + public static final String SINHALESE = "SNH"; + public static final String SONINKE = "SNK"; + public static final String SODO_GURAGE = "SOG"; + public static final String SOTHO = "SOT"; + public static final String ALBANIAN = "SQI"; + public static final String SERBIAN = "SRB"; + public static final String SARAIKI = "SRK"; + public static final String SERER = "SRR"; + public static final String SOUTH_SLAVEY = "SSL"; + public static final String SOUTHERN_SAMI = "SSM"; + public static final String SURI = "SUR"; + public static final String SVAN = "SVA"; + public static final String SWEDISH = "SVE"; + public static final String SWADAYA_ARAMAIC = "SWA"; + public static final String SWAHILI = "SWK"; + public static final String SWAZI = "SWZ"; + public static final String SUTU = "SXT"; + public static final String SYRIAC = "SYR"; + public static final String TABASARAN = "TAB"; + public static final String TAJIKI = "TAJ"; + public static final String TAMIL = "TAM"; + public static final String TATAR = "TAT"; + public static final String TH_CREE = "TCR"; + public static final String TELUGU = "TEL"; + public static final String TONGAN = "TGN"; + public static final String TIGRE = "TGR"; + public static final String TIGRINYA = "TGY"; + public static final String THAI = "THA"; + public static final String TAHITIAN = "THT"; + public static final String TIBETAN = "TIB"; + public static final String TURKMEN = "TKM"; + public static final String TEMNE = "TMN"; + public static final String TSWANA = "TNA"; + public static final String TUNDRA_NENETS = "TNE"; + public static final String TONGA = "TNG"; + public static final String TODO = "TOD"; + public static final String TURKISH = "TRK"; + public static final String TSONGA = "TSG"; + public static final String TUROYO_ARAMAIC = "TUA"; + public static final String TULU = "TUL"; + public static final String TUVIN = "TUV"; + public static final String TWI = "TWI"; + public static final String UDMURT = "UDM"; + public static final String UKRAINIAN = "UKR"; + public static final String URDU = "URD"; + public static final String UPPER_SORBIAN = "USB"; + public static final String UYGHUR = "UYG"; + public static final String UZBEK = "UZB"; + public static final String VENDA = "VEN"; + public static final String VIETNAMESE = "VIT"; + public static final String WA = "WA"; + public static final String WAGDI = "WAG"; + public static final String WEST_CREE = "WCR"; + public static final String WELSH = "WEL"; + public static final String WILDCARD = "*"; + public static final String WOLOF = "WLF"; + public static final String TAI_LUE = "XBD"; + public static final String XHOSA = "XHS"; + public static final String SAKHA = "YAK"; + public static final String YORUBA = "YBA"; + public static final String Y_CREE = "YCR"; + public static final String YI_CLASSIC = "YIC"; + public static final String YI_MODERN = "YIM"; + public static final String CHINESE_HONG_KONG_SAR = "ZHH"; + public static final String CHINESE_PHONETIC = "ZHP"; + public static final String CHINESE_SIMPLIFIED = "ZHS"; + public static final String CHINESE_TRADITIONAL = "ZHT"; + public static final String ZANDE = "ZND"; + public static final String ZULU = "ZUL"; + + public static boolean isDefault(String language) { + return (language != null) && language.equals(DEFAULT); + } + + public static boolean isWildCard(String language) { + return (language != null) && language.equals(WILDCARD); + } + + private OTFLanguage() { + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/OTFScript.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/OTFScript.java new file mode 100644 index 00000000000..7a373dc2831 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/OTFScript.java @@ -0,0 +1,152 @@ +/* + * 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.advanced; + +/** + *

Script tags defined by OTF specification. Note that this set and their + * values do not correspond with ISO 15924 or Unicode Script names.

+ * + * @author Glenn Adams + */ +public final class OTFScript { + public static final String ARABIC = "arab"; + public static final String ARMENIAN = "armn"; + public static final String AVESTAN = "avst"; + public static final String BALINESE = "bali"; + public static final String BAMUM = "bamu"; + public static final String BATAK = "batk"; + public static final String BENGALI = "beng"; + public static final String BENGALI_V2 = "bng2"; + public static final String BOPOMOFO = "bopo"; + public static final String BRAILLE = "brai"; + public static final String BRAHMI = "brah"; + public static final String BUGINESE = "bugi"; + public static final String BUHID = "buhd"; + public static final String BYZANTINE_MUSIC = "byzm"; + public static final String CANADIAN_SYLLABICS = "cans"; + public static final String CARIAN = "cari"; + public static final String CHAKMA = "cakm"; + public static final String CHAM = "cham"; + public static final String CHEROKEE = "cher"; + public static final String CJK_IDEOGRAPHIC = "hani"; + public static final String COPTIC = "copt"; + public static final String CYPRIOT_SYLLABARY = "cprt"; + public static final String CYRILLIC = "cyrl"; + public static final String DEFAULT = "DFLT"; + public static final String DESERET = "dsrt"; + public static final String DEVANAGARI = "deva"; + public static final String DEVANAGARI_V2 = "dev2"; + public static final String EGYPTIAN_HEIROGLYPHS = "egyp"; + public static final String ETHIOPIC = "ethi"; + public static final String GEORGIAN = "geor"; + public static final String GLAGOLITIC = "glag"; + public static final String GOTHIC = "goth"; + public static final String GREEK = "grek"; + public static final String GUJARATI = "gujr"; + public static final String GUJARATI_V2 = "gjr2"; + public static final String GURMUKHI = "guru"; + public static final String GURMUKHI_V2 = "gur2"; + public static final String HANGUL = "hang"; + public static final String HANGUL_JAMO = "jamo"; + public static final String HANUNOO = "hano"; + public static final String HEBREW = "hebr"; + public static final String HIRAGANA = "kana"; + public static final String IMPERIAL_ARAMAIC = "armi"; + public static final String INSCRIPTIONAL_PAHLAVI = "phli"; + public static final String INSCRIPTIONAL_PARTHIAN = "prti"; + public static final String JAVANESE = "java"; + public static final String KAITHI = "kthi"; + public static final String KANNADA = "knda"; + public static final String KANNADA_V2 = "knd2"; + public static final String KATAKANA = "kana"; + public static final String KAYAH_LI = "kali"; + public static final String KHAROSTHI = "khar"; + public static final String KHMER = "khmr"; + public static final String LAO = "lao"; + public static final String LATIN = "latn"; + public static final String LEPCHA = "lepc"; + public static final String LIMBU = "limb"; + public static final String LINEAR_B = "linb"; + public static final String LISU = "lisu"; + public static final String LYCIAN = "lyci"; + public static final String LYDIAN = "lydi"; + public static final String MALAYALAM = "mlym"; + public static final String MALAYALAM_V2 = "mlm2"; + public static final String MANDAIC = "mand"; + public static final String MATHEMATICAL_ALPHANUMERIC_SYMBOLS = "math"; + public static final String MEITEI = "mtei"; + public static final String MEROITIC_CURSIVE = "merc"; + public static final String MEROITIC_HIEROGLYPHS = "mero"; + public static final String MONGOLIAN = "mong"; + public static final String MUSICAL_SYMBOLS = "musc"; + public static final String MYANMAR = "mymr"; + public static final String NEW_TAI_LUE = "talu"; + public static final String NKO = "nko"; + public static final String OGHAM = "ogam"; + public static final String OL_CHIKI = "olck"; + public static final String OLD_ITALIC = "ital"; + public static final String OLD_PERSIAN_CUNEIFORM = "xpeo"; + public static final String OLD_SOUTH_ARABIAN = "sarb"; + public static final String OLD_TURKIC = "orkh"; + public static final String ORIYA = "orya"; + public static final String ORIYA_V2 = "ory2"; + public static final String OSMANYA = "osma"; + public static final String PHAGS_PA = "phag"; + public static final String PHOENICIAN = "phnx"; + public static final String REJANG = "rjng"; + public static final String RUNIC = "runr"; + public static final String SAMARITAN = "samr"; + public static final String SAURASHTRA = "saur"; + public static final String SHARADA = "shrd"; + public static final String SHAVIAN = "shaw"; + public static final String SINHALA = "sinh"; + public static final String SORA_SOMPENG = "sora"; + public static final String SUMERO_AKKADIAN_CUNEIFORM = "xsux"; + public static final String SUNDANESE = "sund"; + public static final String SYLOTI_NAGRI = "sylo"; + public static final String SYRIAC = "syrc"; + public static final String TAGALOG = "tglg"; + public static final String TAGBANWA = "tagb"; + public static final String TAI_LE = "tale"; + public static final String TAI_THAM = "lana"; + public static final String TAI_VIET = "tavt"; + public static final String TAKRI = "takr"; + public static final String TAMIL = "taml"; + public static final String TAMIL_V2 = "tml2"; + public static final String TELUGU = "telu"; + public static final String TELUGU_V2 = "tel2"; + public static final String THAANA = "thaa"; + public static final String THAI = "thai"; + public static final String TIBETAN = "tibt"; + public static final String TIFINAGH = "tfng"; + public static final String UGARITIC_CUNEIFORM = "ugar"; + public static final String VAI = "vai"; + public static final String WILDCARD = "*"; + public static final String YI = "yi"; + + public static boolean isDefault(String script) { + return (script != null) && script.equals(DEFAULT); + } + + public static boolean isWildCard(String script) { + return (script != null) && script.equals(DEFAULT); + } + + private OTFScript() { + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/Positionable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/Positionable.java new file mode 100644 index 00000000000..8ecc09d6c6b --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/Positionable.java @@ -0,0 +1,55 @@ +/* + * 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.advanced; + +/** + *

Optional interface which indicates that glyph positioning is supported and, if supported, + * can perform positioning.

+ * + * @author Glenn Adams + */ +public interface Positionable { + + /** + * Determines if font performs glyph positioning. + * @return true if performs positioning + */ + boolean performsPositioning(); + + /** + * Perform glyph positioning. + * @param cs character sequence to map to position offsets (advancement adjustments) + * @param script a script identifier + * @param language a language identifier + * @param fontSize font size + * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies + */ + int[][] performPositioning(CharSequence cs, String script, String language, int fontSize); + + /** + * Perform glyph positioning using an implied font size. + * @param cs character sequence to map to position offsets (advancement adjustments) + * @param script a script identifier + * @param language a language identifier + * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies + */ + int[][] performPositioning(CharSequence cs, String script, String language); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/Substitutable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/Substitutable.java new file mode 100644 index 00000000000..aada275da34 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/Substitutable.java @@ -0,0 +1,67 @@ +/* + * 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.advanced; + +import java.util.List; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; + +/** + *

Optional interface which indicates that glyph substitution is supported and, if supported, + * can perform substitution.

+ * + * @author Glenn Adams + */ +public interface Substitutable { + + /** + * Determines if font performs glyph substitution. + * @return true if performs substitution. + */ + boolean performsSubstitution(); + + /** + * Perform substitutions on characters to effect glyph substitution. If some substitution is performed, it + * entails mapping from one or more input characters denoting textual character information to one or more + * output character codes denoting glyphs in this font, where the output character codes may make use of + * private character code values that have significance only for this font. + * @param cs character sequence to map to output font encoding character sequence + * @param script a script identifier + * @param language a language identifier + * @param associations optional list to receive list of character associations + * @param retainControls if true, then retain control characters and their glyph mappings, otherwise remove + * @return output sequence (represented as a character sequence, where each character in the returned sequence + * denotes "font characters", i.e., character codes that map directly (1-1) to their associated glyphs + */ + CharSequence performSubstitution(CharSequence cs, String script, String language, List associations, boolean retainControls); + + /** + * Reorder combining marks in character sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base character. + * @param cs character sequence within which combining marks to be reordered + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @param associations optional list of associations to be reordered + * @return output sequence containing reordered "font characters" + */ + CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa, String script, String language, List associations); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/SubtableEntryHolder.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/SubtableEntryHolder.java new file mode 100644 index 00000000000..c59343ac4d5 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/SubtableEntryHolder.java @@ -0,0 +1,111 @@ +/* + * 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.advanced; + +import org.apache.fontbox.ttf.advanced.GlyphMappingTable.MappingRange; +import org.apache.fontbox.ttf.advanced.GlyphPositioningTable.Anchor; +import org.apache.fontbox.ttf.advanced.GlyphPositioningTable.MarkAnchor; +import org.apache.fontbox.ttf.advanced.GlyphPositioningTable.PairValues; + +public class SubtableEntryHolder { + private SubtableEntryHolder() { } + + public interface SubtableEntry { } + + public static class SEInteger implements SubtableEntry { + public final int value; + SEInteger(int value) { + this.value = value; + } + static SEInteger valueOf(int value) { + return new SEInteger(value); + } + public int get() { + return this.value; + } + } + + private static class SEObject implements SubtableEntry { + final T value; + SEObject(T value) { + this.value = value; + } + T get() { + return this.value; + } + } + + public static class SEMappingRange extends SEObject { + SEMappingRange(MappingRange value) { super(value); } + } + + public static class SERuleSetList extends SEObject { + SERuleSetList(AdvancedTypographicTable.RuleSet[] value) { super(value); } + } + + public static class SESequenceList extends SEObject { + SESequenceList(int[][] value) { super(value); } + } + + public static class SEIntList extends SEObject { + SEIntList(int[] value) { super(value); } + } + + public static class SELigatureSet extends SEObject { + SELigatureSet(GlyphSubstitutionTable.LigatureSet value) { super(value); } + } + + public static class SEGlyphClassTable extends SEObject { + SEGlyphClassTable(GlyphClassTable value) { super(value); } + } + + public static class SEGlyphCoverageTable extends SEObject { + SEGlyphCoverageTable(GlyphCoverageTable value) { super(value); } + } + + public static class SEGlyphCoverageTableList extends SEObject { + SEGlyphCoverageTableList(GlyphCoverageTable[] value) { super(value); } + } + + public static class SEValue extends SEObject { + SEValue(GlyphPositioningTable.Value value) { super(value); } + } + + public static class SEValueList extends SEObject { + SEValueList(GlyphPositioningTable.Value[] value) { super(value); } + } + + public static class SEPairValueMatrix extends SEObject { + SEPairValueMatrix(PairValues[][] value) { super(value); } + } + + public static class SEAnchorList extends SEObject { + SEAnchorList(Anchor[] value) { super(value); } + } + + public static class SEAnchorMatrix extends SEObject { + SEAnchorMatrix(Anchor[][] value) { super(value); } + } + + public static class SEAnchorMultiMatrix extends SEObject { + SEAnchorMultiMatrix(Anchor[][][] value) { super(value); } + } + + public static class SEMarkAnchorList extends SEObject { + SEMarkAnchorList(MarkAnchor[] value) { super(value); } + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/AdvancedOTFParser.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/AdvancedOTFParser.java new file mode 100644 index 00000000000..9667a519cd7 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/AdvancedOTFParser.java @@ -0,0 +1,129 @@ +/* + * 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.advanced.api; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.fontbox.ttf.CFFTable; +import org.apache.fontbox.ttf.GlyphSubstitutionTable; +import org.apache.fontbox.ttf.GlyphTable; +import org.apache.fontbox.ttf.OTLTable; +import org.apache.fontbox.ttf.TTFDataStream; +import org.apache.fontbox.ttf.TTFTable; +import org.apache.fontbox.ttf.TrueTypeFont; + +/** + * OpenType font file parser. + */ +public final class AdvancedOTFParser extends org.apache.fontbox.ttf.OTFParser +{ + /** + * Constructor. + */ + public AdvancedOTFParser() + { + this(false); + } + + /** + * Constructor. + * + * @param isEmbedded true if the font is embedded in PDF + */ + public AdvancedOTFParser(boolean isEmbedded) + { + this(isEmbedded, false); + } + + /** + * Constructor. + * + * @param isEmbedded true if the font is embedded in PDF + * @param parseOnDemand true if the tables of the font should be parsed on demand + */ + public AdvancedOTFParser(boolean isEmbedded, boolean parseOnDemand) + { + super(isEmbedded, parseOnDemand); + } + + @Override + public AdvancedOpenTypeFont parse(String file) throws IOException + { + return (AdvancedOpenTypeFont) super.parse(file); + } + + @Override + public AdvancedOpenTypeFont parse(File file) throws IOException + { + return (AdvancedOpenTypeFont) super.parse(file); + } + + @Override + public AdvancedOpenTypeFont parse(InputStream data) throws IOException + { + return (AdvancedOpenTypeFont) super.parse(data); + } + + @Override + protected AdvancedOpenTypeFont parse(TTFDataStream raf) throws IOException + { + return (AdvancedOpenTypeFont) super.parse(raf); + } + + @Override + protected AdvancedOpenTypeFont newFont(TTFDataStream raf) + { + return new AdvancedOpenTypeFont(raf); + } + + @Override + protected TTFTable readTable(TrueTypeFont font, String tag) + { + assert font instanceof AdvancedOpenTypeFont; + switch (tag) + { + case "BASE": + case "JSTF": + return new OTLTable(font); + case "GDEF": + return new org.apache.fontbox.ttf.advanced.GlyphDefinitionTable((AdvancedOpenTypeFont) font); + case "GPOS": + return new org.apache.fontbox.ttf.advanced.GlyphPositioningTable((AdvancedOpenTypeFont) font); + case "GSUB": + return new org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable((AdvancedOpenTypeFont) font); + case "CFF ": + return new CFFTable(font); + default: + return super.readTable(font, tag); + } + } + + @Override + protected TTFTable createTable(TrueTypeFont font, String tag) + { + if (tag.equals(GlyphSubstitutionTable.TAG)) { + // we need to special case GSUB table as TTFParser tries to handle it + return null; + } + + return super.createTable(font, tag); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/AdvancedOpenTypeFont.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/AdvancedOpenTypeFont.java new file mode 100644 index 00000000000..f05511afe50 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/AdvancedOpenTypeFont.java @@ -0,0 +1,207 @@ +package org.apache.fontbox.ttf.advanced.api; + +import java.io.IOException; +import java.nio.IntBuffer; + +import org.apache.fontbox.ttf.CFFTable; +import org.apache.fontbox.ttf.CmapLookup; +import org.apache.fontbox.ttf.OTLTable; +import org.apache.fontbox.ttf.OpenTypeFont; +import org.apache.fontbox.ttf.TTFDataStream; +import org.apache.fontbox.ttf.TTFTable; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.fontbox.ttf.advanced.GlyphDefinitionTable; +import org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable; +import org.apache.fontbox.ttf.advanced.GlyphVectorAdvanced; +import org.apache.fontbox.ttf.advanced.GlyphVectorSimple; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; + +public class AdvancedOpenTypeFont extends OpenTypeFont { + + AdvancedOpenTypeFont(TTFDataStream fontData) { + super(fontData); + } + + /** + * Get the "GSUB" table for this OTF. + * + * @return The "GSUB" table. + */ + public org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable getGSUB() throws IOException + { + return (org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable) getTable(org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable.TAG); + } + + /** + * Get the "GDEF" table for this OTF. + * + * @return The "GDEF" table. + */ + public org.apache.fontbox.ttf.advanced.GlyphDefinitionTable getGDEF() throws IOException + { + return (org.apache.fontbox.ttf.advanced.GlyphDefinitionTable) getTable(org.apache.fontbox.ttf.advanced.GlyphDefinitionTable.TAG); + } + + /** + * Get the "GPOS" table for this OTF. + * + * @return The "GPOS" table. + */ + public org.apache.fontbox.ttf.advanced.GlyphPositioningTable getGPOS() throws IOException + { + return (org.apache.fontbox.ttf.advanced.GlyphPositioningTable) getTable(org.apache.fontbox.ttf.advanced.GlyphPositioningTable.TAG); + } + + @Override + public org.apache.fontbox.ttf.GlyphSubstitutionTable getGsub() throws IOException { + return null; + //TODO return (org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable) getTable(org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable.TAG); + } + + /** + * TODO + */ + private float getNormalizedWidth(float width) throws IOException { + float unitsPerEM = getUnitsPerEm(); + if (Float.compare(unitsPerEM, 1000) != 0) + { + width *= 1000f / unitsPerEM; + } + return width; + } + + /** + * TODO + */ + public GlyphVector createGlyphVector(String text, int fontSize) throws IOException + { + if (text.isEmpty()) { + return new GlyphVectorSimple(null); + } + + int[] codePoints = text.codePoints().toArray(); + int[] originalGlyphs = new int[codePoints.length]; + + // TODO: What if cmap is null? + // TODO: What if glyph for character does not exist? + CmapLookup cmapLookup = getUnicodeCmapLookup(); + + for (int i = 0; i < codePoints.length; i++) { + originalGlyphs[i] = cmapLookup.getGlyphId(codePoints[i]); + } +//System.out.println("original glyphs = " + Arrays.toString(originalGlyphs)); + + IntBuffer characters = IntBuffer.wrap(codePoints); + IntBuffer glyphs = IntBuffer.wrap(originalGlyphs); + + GlyphSequence sequence = new GlyphSequence(characters, glyphs, null); + + org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable substitutionTable = + (org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable) getGSUB(); + + org.apache.fontbox.ttf.advanced.GlyphPositioningTable positioningTable = + (org.apache.fontbox.ttf.advanced.GlyphPositioningTable) getGPOS(); + + org.apache.fontbox.ttf.advanced.GlyphDefinitionTable gdefTable = + (org.apache.fontbox.ttf.advanced.GlyphDefinitionTable) getGDEF(); + + + + Object[][] extraFeatures = new Object[][] { + //new Object[] { "smcp", Boolean.FALSE }, + //new Object[] { "frac", Boolean.TRUE } + }; + //extraFeatures = null; + + // TODO: Correct script and language + Object[][] features = {{ "kern", true}, {"mark", true}, {"mkmk", true}}; + String script = "latn"; + String language = "dflt"; + + GlyphSequence substituted = substitutionTable != null ? + substitutionTable.substitute(sequence, script, language, extraFeatures) : sequence; + + int[][] adjustments = null; + int[] widths = null; + boolean positioned = false; + + if (positioningTable != null) { + int size = substituted.getGlyphCount(); + adjustments = new int[size][4]; + widths = new int[size]; + int[] workingGlyphs = substituted.getGlyphArray(false); + + for (int i = 0; i < size; i++) { + widths[i] = getAdvanceWidth(workingGlyphs[i]); + } + + // TODO: Correct script, language and font size + // TODO: widths? + positioned = positioningTable != null ? positioningTable.position( + substituted, script, language, features, fontSize*1000, getAdvanceWidths(), adjustments) : false; + } + + System.out.printf("createGlyphVector1 i dx dy dax day w -- positioned %n"); + for (int i = 0; i < substituted.getGlyphCount(); i++) { + System.out.printf("createGlyphVector1 %d %h %d %d %d %d %n", i, substituted.getGlyph(i), adjustments[i][0], adjustments[i][1], adjustments[i][2], adjustments[i][3], widths[i]); + } + + + GlyphSequence reordered = gdefTable != null ? + gdefTable.reorderCombiningMarks(substituted, widths, adjustments, script, language, features) : substituted; + + // For positioning an array dx, dy, advance_x, advance_y is needed + // Compare output of HarfBuzz hb-shape + + System.out.printf("createGlyphVector1 2 dx dy dax day w -- reordered %n"); + for (int i = 0; i < reordered.getGlyphCount(); i++) { + System.out.printf("createGlyphVector2 %d %h %d %d %d %d %n", i, reordered.getGlyph(i), adjustments[i][0], adjustments[i][1], adjustments[i][2], adjustments[i][3], widths[i]); + } + +//System.out.println("widths = " + Arrays.toString(widths)); + int[] outGlyphs = reordered.getGlyphArray(false); +//System.out.println("outGlyphs = " + Arrays.toString(outGlyphs)); + int outSize = reordered.getGlyphCount(); + + float width = 0f; + for (int i = 0; i < outSize; i++) { + int xAdjust = 0; + + if (positioned) { + int[] glyphAdjust = adjustments[i]; +//System.out.println("xAdjust = " + Arrays.toString(glyphAdjust)); + + int placementX = glyphAdjust[0]; + int placementY = glyphAdjust[1]; + int advanceX = glyphAdjust[2]; + int advanceY = glyphAdjust[3]; + + if (placementX != 0 || advanceX != 0) { + xAdjust = advanceX; // + placementX; + } + } + +//System.out.println("advance = " + getAdvanceWidth(outGlyphs[i])); + + // TODO: Something with yAdjust + // TODO: Debug width + width += getAdvanceWidth(outGlyphs[i]) + xAdjust; + } + + //System.out.println("w = " + width); + return new GlyphVectorAdvanced(outGlyphs, getNormalizedWidth(width), positioned ? adjustments : null); + } + + /** + * Returns true if this font uses OpenType Layout (Advanced Typographic) tables. + */ + public boolean hasLayoutTables() + { + return tables.containsKey("BASE") || + tables.containsKey("GDEF") || + tables.containsKey("GPOS") || + tables.containsKey("GSUB") || + tables.containsKey("JSTF"); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/GlyphVector.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/GlyphVector.java new file mode 100644 index 00000000000..30673c24fcc --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/api/GlyphVector.java @@ -0,0 +1,13 @@ +package org.apache.fontbox.ttf.advanced.api; + +/** + * TODO + */ +public interface GlyphVector { + + /** + * Gets the total advance of this glyph vector. + * @return The width of the vector in 1/1000 units of text space. + */ + float getWidth(); +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/bidi/BidiClass.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/bidi/BidiClass.java new file mode 100644 index 00000000000..affd88c5a09 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/bidi/BidiClass.java @@ -0,0 +1,267 @@ +/* + * 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.advanced.bidi; + +import java.util.Arrays; + +// CSOFF: LineLengthCheck + +/* + * !!! THIS IS A GENERATED FILE !!! + * If updates to the source are needed, then: + * - apply the necessary modifications to + * 'src/codegen/unicode/java/org/apache/fop/complexscripts/bidi/GenerateBidiClass.java' + * - run 'ant codegen-unicode', which will generate a new BidiClass.java + * in 'src/java/org/apache/fop/complexscripts/bidi' + * - commit BOTH changed files + */ + +/** Bidirectional class utilities. */ +public final class BidiClass { + +private BidiClass() { +} + +private static byte[] bcL1 = { +15,15,15,15,15,15,15,15,15,17,16,17,18,16,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,17,18,19,19,11,11,11,19,19,19, +19,19,10,13,10,13,13,9,9,9,9,9,9,9,9,9,9,13,19,19,19,19,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,19,19, +19,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,19,19,19,15,15,15,15,15,15,16,15,15,15,15,15,15,15,15,15,15, +15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,13,19,11,11,11,11,19,19,19,19,1,19,19,15,19,19,11,11,9,9,19,1,19,19,19,9,1, +19,19,19,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,19,1,1,1,1,1,1,1,1 +}; + +private static byte[] bcR1 = { +4,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14, +14,14,14,14,14,4,14,4,14,14,4,14,14,4,14,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, +4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,12,12,12,12,5,5,19,19,5,11,11,5,13,5,19,19,14,14,14,14,14,14,14,14,14,14,14,5,5,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,14,14,14,14,14,14,14,14,14,14,14,14,14,14, +14,14,14,14,14,14,14,12,12,12,12,12,12,12,12,12,12,11,12,12,5,5,5,14,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,14,14,14,14,14,14,14,12,19,14,14,14,14,14,14,5,5,14,14,19,14,14,14,14,5,5,9,9,9,9,9,9,9,9,9,9,5, +5,5,5,5,5 +}; + +private static int[] bcS1 = { +256,443,444,448,452,660,661,688,697,699,706,710,720,722,736,741,748,749,750,751,768,880,884,885,886,890,891,894,900,902,903, +904,908,910,931,1014,1015,1154,1155,1160,1162,1329,1369,1370,1377,1417,1418,1792,1806,1807,1808,1809,1810,1840,1867,1869,1958, +1969,1970,1984,1994,2027,2036,2038,2039,2042,2043,2048,2070,2074,2075,2084,2085,2088,2089,2094,2096,2111,2112,2137,2140,2142, +2143,2304,2307,2308,2362,2363,2364,2365,2366,2369,2377,2381,2382,2384,2385,2392,2402,2404,2406,2416,2417,2418,2425,2433,2434, +2437,2447,2451,2474,2482,2486,2492,2493,2494,2497,2503,2507,2509,2510,2519,2524,2527,2530,2534,2544,2546,2548,2554,2555,2561, +2563,2565,2575,2579,2602,2610,2613,2616,2620,2622,2625,2631,2635,2641,2649,2654,2662,2672,2674,2677,2689,2691,2693,2703,2707, +2730,2738,2741,2748,2749,2750,2753,2759,2761,2763,2765,2768,2784,2786,2790,2801,2817,2818,2821,2831,2835,2858,2866,2869,2876, +2877,2878,2879,2880,2881,2887,2891,2893,2902,2903,2908,2911,2914,2918,2928,2929,2930,2946,2947,2949,2958,2962,2969,2972,2974, +2979,2984,2990,3006,3008,3009,3014,3018,3021,3024,3031,3046,3056,3059,3065,3066,3073,3077,3086,3090,3114,3125,3133,3134,3137, +3142,3146,3157,3160,3168,3170,3174,3192,3199,3202,3205,3214,3218,3242,3253,3260,3261,3262,3263,3264,3270,3271,3274,3276,3285, +3294,3296,3298,3302,3313,3330,3333,3342,3346,3389,3390,3393,3398,3402,3405,3406,3415,3424,3426,3430,3440,3449,3450,3458,3461, +3482,3507,3517,3520,3530,3535,3538,3542,3544,3570,3572,3585,3633,3634,3636,3647,3648,3654,3655,3663,3664,3674,3713,3716,3719, +3722,3725,3732,3737,3745,3749,3751,3754,3757,3761,3762,3764,3771,3773,3776,3782,3784,3792,3804,3840,3841,3844,3859,3864,3866, +3872,3882,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3904,3913,3953,3967,3968,3973,3974,3976,3981,3993,4030,4038, +4039,4046,4048,4053,4057,4096,4139,4141,4145,4146,4152,4153,4155,4157,4159,4160,4170,4176,4182,4184,4186,4190,4193,4194,4197, +4199,4206,4209,4213,4226,4227,4229,4231,4237,4238,4239,4240,4250,4253,4254,4256,4304,4347,4348,4352,4682,4688,4696,4698,4704, +4746,4752,4786,4792,4800,4802,4808,4824,4882,4888,4957,4960,4961,4969,4992,5008,5024,5120,5121,5741,5743,5760,5761,5787,5788, +5792,5867,5870,5888,5902,5906,5920,5938,5941,5952,5970,5984,5998,6002,6016,6068,6070,6071,6078,6086,6087,6089,6100,6103,6104, +6107,6108,6109,6112,6128,6144,6150,6151,6155,6158,6160,6176,6211,6212,6272,6313,6314,6320,6400,6432,6435,6439,6441,6448,6450, +6451,6457,6464,6468,6470,6480,6512,6528,6576,6593,6600,6608,6618,6622,6656,6679,6681,6686,6688,6741,6742,6743,6744,6752,6753, +6754,6755,6757,6765,6771,6783,6784,6800,6816,6823,6824,6912,6916,6917,6964,6965,6966,6971,6972,6973,6978,6979,6981,6992,7002, +7009,7019,7028,7040,7042,7043,7073,7074,7078,7080,7082,7086,7088,7104,7142,7143,7144,7146,7149,7150,7151,7154,7164,7168,7204, +7212,7220,7222,7227,7232,7245,7248,7258,7288,7294,7376,7379,7380,7393,7394,7401,7405,7406,7410,7424,7468,7522,7544,7545,7579, +7616,7676,7680,7960,7968,8008,8016,8025,8027,8029,8031,8064,8118,8125,8126,8127,8130,8134,8141,8144,8150,8157,8160,8173,8178, +8182,8189,8192,8203,8206,8207,8208,8214,8216,8217,8218,8219,8221,8222,8223,8224,8232,8233,8234,8235,8236,8237,8238,8239,8240, +8245,8249,8250,8251,8255,8257,8260,8261,8262,8263,8274,8275,8276,8277,8287,8288,8293,8298,8304,8305,8308,8314,8316,8317,8318, +8319,8320,8330,8332,8333,8334,8336,8352,8400,8413,8417,8418,8421,8448,8450,8451,8455,8456,8458,8468,8469,8470,8472,8473,8478, +8484,8485,8486,8487,8488,8489,8490,8494,8495,8501,8505,8506,8508,8512,8517,8522,8523,8524,8526,8527,8528,8544,8579,8581,8585, +8592,8597,8602,8604,8608,8609,8611,8612,8614,8615,8622,8623,8654,8656,8658,8659,8660,8661,8692,8722,8723,8724,8960,8968,8972, +8992,8994,9001,9002,9003,9014,9083,9084,9085,9109,9110,9115,9140,9180,9186,9216,9280,9312,9352,9372,9450,9472,9655,9656,9665, +9666,9720,9728,9839,9840,9900,9901,9985,10088,10089,10090,10091,10092,10093,10094,10095,10096,10097,10098,10099,10100,10101, +10102,10132,10176,10181,10182,10183,10188,10190,10214,10215,10216,10217,10218,10219,10220,10221,10222,10223,10224,10240,10496, +10627,10628,10629,10630,10631,10632,10633,10634,10635,10636,10637,10638,10639,10640,10641,10642,10643,10644,10645,10646,10647, +10648,10649,10712,10713,10714,10715,10716,10748,10749,10750,11008,11056,11077,11079,11088,11264,11312,11360,11389,11390,11493, +11499,11503,11513,11517,11518,11520,11568,11631,11632,11647,11648,11680,11688,11696,11704,11712,11720,11728,11736,11744,11776, +11778,11779,11780,11781,11782,11785,11786,11787,11788,11789,11790,11799,11800,11802,11803,11804,11805,11806,11808,11809,11810, +11811,11812,11813,11814,11815,11816,11817,11818,11823,11824,11904,11931,12032,12272,12288,12289,12292,12293,12294,12295,12296, +12297,12298,12299,12300,12301,12302,12303,12304,12305,12306,12308,12309,12310,12311,12312,12313,12314,12315,12316,12317,12318, +12320,12321,12330,12336,12337,12342,12344,12347,12348,12349,12350,12353,12441,12443,12445,12447,12448,12449,12539,12540,12543, +12549,12593,12688,12690,12694,12704,12736,12784,12800,12829,12832,12842,12880,12881,12896,12924,12927,12928,12938,12977,12992, +13004,13008,13056,13175,13179,13278,13280,13311,13312,19904,19968,40960,40981,40982,42128,42192,42232,42238,42240,42508,42509, +42512,42528,42538,42560,42606,42607,42608,42611,42620,42622,42623,42624,42656,42726,42736,42738,42752,42775,42784,42786,42864, +42865,42888,42889,42891,42896,42912,43002,43003,43010,43011,43014,43015,43019,43020,43043,43045,43047,43048,43056,43062,43064, +43065,43072,43124,43136,43138,43188,43204,43214,43216,43232,43250,43256,43259,43264,43274,43302,43310,43312,43335,43346,43359, +43360,43392,43395,43396,43443,43444,43446,43450,43452,43453,43457,43471,43472,43486,43520,43561,43567,43569,43571,43573,43584, +43587,43588,43596,43597,43600,43612,43616,43632,43633,43639,43642,43643,43648,43696,43697,43698,43701,43703,43705,43710,43712, +43713,43714,43739,43741,43742,43777,43785,43793,43808,43816,43968,44003,44005,44006,44008,44009,44011,44012,44013,44016,44032, +55216,55243,57344,63744,64048,64112,64256,64275,64285,64286,64287,64297,64298,64311,64312,64317,64318,64319,64320,64322,64323, +64325,64326,64336,64434,64450,64467,64830,64831,64832,64848,64912,64914,64968,64976,65008,65020,65021,65022,65024,65040,65047, +65048,65049,65056,65072,65073,65075,65077,65078,65079,65080,65081,65082,65083,65084,65085,65086,65087,65088,65089,65090,65091, +65092,65093,65095,65096,65097,65101,65104,65105,65106,65108,65109,65110,65112,65113,65114,65115,65116,65117,65118,65119,65120, +65122,65123,65124,65128,65129,65130,65131,65136,65141,65142,65277,65279,65281,65283,65284,65285,65286,65288,65289,65290,65291, +65292,65293,65294,65296,65306,65307,65308,65311,65313,65339,65340,65341,65342,65343,65344,65345,65371,65372,65373,65374,65375, +65376,65377,65378,65379,65380,65382,65392,65393,65438,65440,65474,65482,65490,65498,65504,65506,65507,65508,65509,65512,65513, +65517,65520,65529,65532,65534,65536,65549,65576,65596,65599,65616,65664,65792,65793,65794,65799,65847,65856,65909,65913,65930, +65936,66000,66045,66176,66208,66304,66336,66352,66369,66370,66378,66432,66463,66464,66504,66512,66513,66560,66640,66720,67584, +67590,67592,67593,67594,67638,67639,67641,67644,67645,67647,67670,67671,67672,67680,67840,67862,67868,67871,67872,67898,67903, +67904,68096,68097,68100,68101,68103,68108,68112,68116,68117,68120,68121,68148,68152,68155,68159,68160,68168,68176,68185,68192, +68221,68223,68224,68352,68406,68409,68416,68438,68440,68448,68467,68472,68480,68608,68681,69216,69247,69632,69633,69634,69635, +69688,69703,69714,69734,69760,69762,69763,69808,69811,69815,69817,69819,69821,69822,73728,74752,74864,77824,92160,110592,118784, +119040,119081,119141,119143,119146,119149,119155,119163,119171,119173,119180,119210,119214,119296,119362,119365,119552,119648, +119808,119894,119966,119970,119973,119977,119982,119995,119997,120005,120071,120077,120086,120094,120123,120128,120134,120138, +120146,120488,120513,120514,120539,120540,120571,120572,120597,120598,120629,120630,120655,120656,120687,120688,120713,120714, +120745,120746,120771,120772,120782,124928,126976,127024,127136,127153,127169,127185,127232,127248,127280,127344,127462,127504, +127552,127568,127744,127792,127799,127872,127904,127942,127968,128000,128064,128066,128140,128141,128249,128256,128292,128293, +128336,128507,128513,128530,128534,128536,128538,128540,128544,128552,128557,128560,128565,128581,128640,128768,131070,131072, +173824,177984,194560,196606,262142,327678,393214,458750,524286,589822,655358,720894,786430,851966,917502,917505,917506,917536, +917632,917760,918000,983038,983040,1048574,1048576,1114110 +}; + +private static int[] bcE1 = { +442,443,447,451,659,660,687,696,698,705,709,719,721,735,740,747,748,749,750,767,879,883,884,885,887,890,893,894,901,902,903, +906,908,929,1013,1014,1153,1154,1159,1161,1319,1366,1369,1375,1415,1417,1418,1805,1806,1807,1808,1809,1839,1866,1868,1957, +1968,1969,1983,1993,2026,2035,2037,2038,2041,2042,2047,2069,2073,2074,2083,2084,2087,2088,2093,2095,2110,2111,2136,2139,2141, +2142,2303,2306,2307,2361,2362,2363,2364,2365,2368,2376,2380,2381,2383,2384,2391,2401,2403,2405,2415,2416,2417,2423,2431,2433, +2435,2444,2448,2472,2480,2482,2489,2492,2493,2496,2500,2504,2508,2509,2510,2519,2525,2529,2531,2543,2545,2547,2553,2554,2555, +2562,2563,2570,2576,2600,2608,2611,2614,2617,2620,2624,2626,2632,2637,2641,2652,2654,2671,2673,2676,2677,2690,2691,2701,2705, +2728,2736,2739,2745,2748,2749,2752,2757,2760,2761,2764,2765,2768,2785,2787,2799,2801,2817,2819,2828,2832,2856,2864,2867,2873, +2876,2877,2878,2879,2880,2884,2888,2892,2893,2902,2903,2909,2913,2915,2927,2928,2929,2935,2946,2947,2954,2960,2965,2970,2972, +2975,2980,2986,3001,3007,3008,3010,3016,3020,3021,3024,3031,3055,3058,3064,3065,3066,3075,3084,3088,3112,3123,3129,3133,3136, +3140,3144,3149,3158,3161,3169,3171,3183,3198,3199,3203,3212,3216,3240,3251,3257,3260,3261,3262,3263,3268,3270,3272,3275,3277, +3286,3294,3297,3299,3311,3314,3331,3340,3344,3386,3389,3392,3396,3400,3404,3405,3406,3415,3425,3427,3439,3445,3449,3455,3459, +3478,3505,3515,3517,3526,3530,3537,3540,3542,3551,3571,3572,3632,3633,3635,3642,3647,3653,3654,3662,3663,3673,3675,3714,3716, +3720,3722,3725,3735,3743,3747,3749,3751,3755,3760,3761,3763,3769,3772,3773,3780,3782,3789,3801,3805,3840,3843,3858,3863,3865, +3871,3881,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3903,3911,3948,3966,3967,3972,3973,3975,3980,3991,4028,4037, +4038,4044,4047,4052,4056,4058,4138,4140,4144,4145,4151,4152,4154,4156,4158,4159,4169,4175,4181,4183,4185,4189,4192,4193,4196, +4198,4205,4208,4212,4225,4226,4228,4230,4236,4237,4238,4239,4249,4252,4253,4255,4293,4346,4347,4348,4680,4685,4694,4696,4701, +4744,4749,4784,4789,4798,4800,4805,4822,4880,4885,4954,4959,4960,4968,4988,5007,5017,5108,5120,5740,5742,5759,5760,5786,5787, +5788,5866,5869,5872,5900,5905,5908,5937,5940,5942,5969,5971,5996,6000,6003,6067,6069,6070,6077,6085,6086,6088,6099,6102,6103, +6106,6107,6108,6109,6121,6137,6149,6150,6154,6157,6158,6169,6210,6211,6263,6312,6313,6314,6389,6428,6434,6438,6440,6443,6449, +6450,6456,6459,6464,6469,6479,6509,6516,6571,6592,6599,6601,6617,6618,6655,6678,6680,6683,6687,6740,6741,6742,6743,6750,6752, +6753,6754,6756,6764,6770,6780,6783,6793,6809,6822,6823,6829,6915,6916,6963,6964,6965,6970,6971,6972,6977,6978,6980,6987,7001, +7008,7018,7027,7036,7041,7042,7072,7073,7077,7079,7081,7082,7087,7097,7141,7142,7143,7145,7148,7149,7150,7153,7155,7167,7203, +7211,7219,7221,7223,7231,7241,7247,7257,7287,7293,7295,7378,7379,7392,7393,7400,7404,7405,7409,7410,7467,7521,7543,7544,7578, +7615,7654,7679,7957,7965,8005,8013,8023,8025,8027,8029,8061,8116,8124,8125,8126,8129,8132,8140,8143,8147,8155,8159,8172,8175, +8180,8188,8190,8202,8205,8206,8207,8213,8215,8216,8217,8218,8220,8221,8222,8223,8231,8232,8233,8234,8235,8236,8237,8238,8239, +8244,8248,8249,8250,8254,8256,8259,8260,8261,8262,8273,8274,8275,8276,8286,8287,8292,8297,8303,8304,8305,8313,8315,8316,8317, +8318,8319,8329,8331,8332,8333,8334,8348,8377,8412,8416,8417,8420,8432,8449,8450,8454,8455,8457,8467,8468,8469,8471,8472,8477, +8483,8484,8485,8486,8487,8488,8489,8493,8494,8500,8504,8505,8507,8511,8516,8521,8522,8523,8525,8526,8527,8543,8578,8580,8584, +8585,8596,8601,8603,8607,8608,8610,8611,8613,8614,8621,8622,8653,8655,8657,8658,8659,8660,8691,8721,8722,8723,8959,8967,8971, +8991,8993,9000,9001,9002,9013,9082,9083,9084,9108,9109,9114,9139,9179,9185,9203,9254,9290,9351,9371,9449,9471,9654,9655,9664, +9665,9719,9727,9838,9839,9899,9900,9983,10087,10088,10089,10090,10091,10092,10093,10094,10095,10096,10097,10098,10099,10100, +10101,10131,10175,10180,10181,10182,10186,10188,10213,10214,10215,10216,10217,10218,10219,10220,10221,10222,10223,10239,10495, +10626,10627,10628,10629,10630,10631,10632,10633,10634,10635,10636,10637,10638,10639,10640,10641,10642,10643,10644,10645,10646, +10647,10648,10711,10712,10713,10714,10715,10747,10748,10749,11007,11055,11076,11078,11084,11097,11310,11358,11388,11389,11492, +11498,11502,11505,11516,11517,11519,11557,11621,11631,11632,11647,11670,11686,11694,11702,11710,11718,11726,11734,11742,11775, +11777,11778,11779,11780,11781,11784,11785,11786,11787,11788,11789,11798,11799,11801,11802,11803,11804,11805,11807,11808,11809, +11810,11811,11812,11813,11814,11815,11816,11817,11822,11823,11825,11929,12019,12245,12283,12288,12291,12292,12293,12294,12295, +12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,12307,12308,12309,12310,12311,12312,12313,12314,12315,12316,12317, +12319,12320,12329,12335,12336,12341,12343,12346,12347,12348,12349,12351,12438,12442,12444,12446,12447,12448,12538,12539,12542, +12543,12589,12686,12689,12693,12703,12730,12771,12799,12828,12830,12841,12879,12880,12895,12923,12926,12927,12937,12976,12991, +13003,13007,13054,13174,13178,13277,13279,13310,13311,19893,19967,40907,40980,40981,42124,42182,42231,42237,42239,42507,42508, +42511,42527,42537,42539,42605,42606,42607,42610,42611,42621,42622,42623,42647,42725,42735,42737,42743,42774,42783,42785,42863, +42864,42887,42888,42890,42894,42897,42921,43002,43009,43010,43013,43014,43018,43019,43042,43044,43046,43047,43051,43061,43063, +43064,43065,43123,43127,43137,43187,43203,43204,43215,43225,43249,43255,43258,43259,43273,43301,43309,43311,43334,43345,43347, +43359,43388,43394,43395,43442,43443,43445,43449,43451,43452,43456,43469,43471,43481,43487,43560,43566,43568,43570,43572,43574, +43586,43587,43595,43596,43597,43609,43615,43631,43632,43638,43641,43642,43643,43695,43696,43697,43700,43702,43704,43709,43711, +43712,43713,43714,43740,43741,43743,43782,43790,43798,43814,43822,44002,44004,44005,44007,44008,44010,44011,44012,44013,44025, +55203,55238,55291,63743,64045,64109,64217,64262,64279,64285,64286,64296,64297,64310,64311,64316,64317,64318,64319,64321,64322, +64324,64325,64335,64433,64449,64466,64829,64830,64831,64847,64911,64913,64967,64975,65007,65019,65020,65021,65023,65039,65046, +65047,65048,65049,65062,65072,65074,65076,65077,65078,65079,65080,65081,65082,65083,65084,65085,65086,65087,65088,65089,65090, +65091,65092,65094,65095,65096,65100,65103,65104,65105,65106,65108,65109,65111,65112,65113,65114,65115,65116,65117,65118,65119, +65121,65122,65123,65126,65128,65129,65130,65131,65140,65141,65276,65278,65279,65282,65283,65284,65285,65287,65288,65289,65290, +65291,65292,65293,65295,65305,65306,65307,65310,65312,65338,65339,65340,65341,65342,65343,65344,65370,65371,65372,65373,65374, +65375,65376,65377,65378,65379,65381,65391,65392,65437,65439,65470,65479,65487,65495,65500,65505,65506,65507,65508,65510,65512, +65516,65518,65528,65531,65533,65535,65547,65574,65594,65597,65613,65629,65786,65792,65793,65794,65843,65855,65908,65912,65929, +65930,65947,66044,66045,66204,66256,66334,66339,66368,66369,66377,66378,66461,66463,66499,66511,66512,66517,66639,66717,66729, +67589,67591,67592,67593,67637,67638,67640,67643,67644,67646,67669,67670,67671,67679,67839,67861,67867,67870,67871,67897,67902, +67903,68095,68096,68099,68100,68102,68107,68111,68115,68116,68119,68120,68147,68151,68154,68158,68159,68167,68175,68184,68191, +68220,68222,68223,68351,68405,68408,68415,68437,68439,68447,68466,68471,68479,68607,68680,69215,69246,69631,69632,69633,69634, +69687,69702,69709,69733,69743,69761,69762,69807,69810,69814,69816,69818,69820,69821,69825,74606,74850,74867,78894,92728,110593, +119029,119078,119140,119142,119145,119148,119154,119162,119170,119172,119179,119209,119213,119261,119361,119364,119365,119638, +119665,119892,119964,119967,119970,119974,119980,119993,119995,120003,120069,120074,120084,120092,120121,120126,120132,120134, +120144,120485,120512,120513,120538,120539,120570,120571,120596,120597,120628,120629,120654,120655,120686,120687,120712,120713, +120744,120745,120770,120771,120779,120831,126975,127019,127123,127150,127166,127183,127199,127242,127278,127337,127386,127490, +127546,127560,127569,127776,127797,127868,127891,127940,127946,127984,128062,128064,128139,128140,128247,128252,128291,128292, +128317,128359,128511,128528,128532,128534,128536,128538,128542,128549,128555,128557,128563,128576,128591,128709,128883,131071, +173782,177972,178205,195101,196607,262143,327679,393215,458751,524287,589823,655359,720895,786431,851967,917504,917505,917535, +917631,917759,917999,921599,983039,1048573,1048575,1114109,1114111 +}; + +private static byte[] bcC1 = { +1,1,1,1,1,1,1,1,19,1,19,19,1,19,1,19,19,19,1,19,14,1,19,19,1,1,1,19,19,1,19,1,1,1,1,19,1,1,14,14,1,1,1,1,1,1,19,5,5,12,5,14, +5,14,5,5,14,5,5,4,4,14,4,19,19,4,4,4,14,4,14,4,14,4,14,4,4,4,4,14,4,4,4,14,1,1,14,1,14,1,1,14,1,14,1,1,14,1,14,1,1,1,1,1,1, +14,1,1,1,1,1,1,1,14,1,1,14,1,1,14,1,1,1,1,14,1,1,11,1,1,11,14,1,1,1,1,1,1,1,1,14,1,14,14,14,14,1,1,1,14,1,14,14,1,1,1,1,1, +1,1,14,1,1,14,14,1,1,14,1,1,14,1,11,14,1,1,1,1,1,1,1,14,1,1,14,1,14,1,1,14,14,1,1,1,14,1,1,1,1,14,1,1,1,1,1,1,1,1,1,1,1,14, +1,1,1,14,1,1,1,1,19,11,19,1,1,1,1,1,1,1,14,1,14,14,14,1,1,14,1,19,1,1,1,1,1,1,1,14,1,1,1,1,1,1,1,14,1,1,1,14,1,1,1,1,1,1,1, +1,14,1,1,14,1,1,1,14,1,1,1,1,1,1,1,1,1,1,14,1,14,14,1,1,1,1,14,1,14,11,1,1,14,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,1,14,14,1,1, +1,14,1,1,1,1,1,1,14,1,1,1,1,14,1,14,1,14,19,19,19,19,1,1,1,14,1,14,1,14,1,14,14,1,14,1,1,1,1,1,1,1,14,1,14,1,14,1,14,1,1,1, +1,1,14,1,14,1,1,1,1,1,14,1,14,1,14,1,14,1,1,1,1,14,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,1,1,1,1,19,1,19,1,1,1,18,1, +19,19,1,1,1,1,1,14,1,14,1,1,14,1,1,14,1,1,1,14,1,14,1,14,1,1,1,11,1,14,1,19,19,19,19,14,18,1,1,1,1,1,14,1,1,1,14,1,14,1,1, +14,1,14,19,19,1,1,1,1,1,1,1,1,1,19,1,14,1,1,1,1,14,1,14,14,1,14,1,14,1,14,14,1,1,1,1,1,14,1,1,14,1,14,1,14,1,14,1,1,1,1,1, +14,1,14,1,1,1,14,1,14,1,1,1,1,14,1,14,1,14,1,14,1,1,1,1,14,1,14,1,1,1,1,1,1,1,14,1,14,1,14,1,14,1,1,1,1,1,1,1,1,14,14,1,1, +1,1,1,1,1,1,1,1,1,19,1,19,1,1,19,1,1,19,1,19,1,1,19,18,15,1,4,19,19,19,19,19,19,19,19,19,19,18,16,2,6,8,3,7,13,11,19,19,19, +19,19,19,13,19,19,19,19,19,19,19,18,15,15,15,9,1,9,10,19,19,19,1,9,10,19,19,19,1,11,14,14,14,14,14,19,1,19,1,19,1,19,1,19, +19,1,19,1,19,1,19,1,19,1,11,1,1,1,19,1,19,1,19,19,19,1,1,19,1,1,1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19, +19,10,11,19,19,19,19,19,19,19,19,19,1,19,19,19,1,19,19,19,19,19,19,19,19,9,1,19,19,19,19,19,19,19,19,19,19,1,19,19,19,19,19, +19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,1,19,19,19,19,19,19,19,19,19,19, +19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,1,1,1,1,1,19,1,14,19,19,19,1,1,1,1,14,1,1, +1,1,1,1,1,1,1,14,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19, +19,18,19,19,1,1,1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,1,14,19,1,19,1,1,1,19,19,1,14,19,1, +1,19,1,19,1,1,1,1,1,1,1,1,19,1,1,19,1,1,19,19,1,19,1,1,1,19,1,19,1,1,19,1,19,1,19,1,19,1,1,1,1,19,1,1,1,1,1,19,1,1,1,1,1,14, +14,19,14,19,19,1,1,1,14,1,19,19,19,1,1,1,19,1,1,1,1,1,1,14,1,14,1,14,1,1,14,1,19,1,1,11,11,1,19,1,1,1,14,1,1,14,1,1,1,1,1, +14,1,1,14,1,1,1,14,1,1,14,1,14,1,14,1,1,1,1,1,1,14,1,14,1,14,1,14,1,14,1,1,1,1,1,1,1,1,1,1,14,1,14,1,14,1,14,1,14,1,1,1,1, +1,1,1,1,1,1,1,14,1,14,1,1,1,14,1,1,1,1,1,1,1,1,1,1,4,14,4,10,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,19,19,5,5,5,5,5,15,5,5,19,5,14, +19,19,19,19,14,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,13,19,13,19,13,19,19,19,19,19,19,19, +19,11,19,10,10,19,19,11,11,19,5,5,5,5,15,19,11,11,11,19,19,19,19,10,13,10,13,9,13,19,19,19,1,19,19,19,19,19,19,1,19,19,19, +19,19,19,19,19,19,19,1,1,1,1,1,1,1,1,1,11,19,19,19,11,19,19,19,15,19,19,15,1,1,1,1,1,1,1,1,19,1,1,1,19,19,19,19,19,1,14,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,19,4,4,4,4,4,14,4,14,4,14,4,4,4,4,4,4,14,4,14,4,4,4,4, +4,4,4,4,4,4,19,4,4,4,4,4,4,4,4,4,12,4,1,14,1,1,14,1,19,1,14,1,1,1,14,1,14,1,1,1,1,1,1,1,1,1,1,1,1,1,14,1,1,15,14,1,14,1,14, +1,19,14,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,1,1,1,19,1,1,1,19,1,1,1,19,1,1,1,19,1,9,4,19,19,19,19,19,19, +9,1,1,1,1,1,1,1,19,19,19,19,19,19,19,19,19,19,1,19,19,19,1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,15,1,1,1,1,15, +15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,14,15,15,1,15,1,15 +}; + +/** + * Lookup bidi class for character expressed as unicode scalar value. + * @param ch a unicode scalar value + * @return bidi class + */ +public static int getBidiClass(int ch) { + if (ch <= 0x00FF) { + return bcL1 [ ch - 0x0000 ]; + } else if ((ch >= 0x0590) && (ch <= 0x06FF)) { + return bcR1 [ ch - 0x0590 ]; + } else { + return getBidiClass(ch, bcS1, bcE1, bcC1); + } +} + +private static int getBidiClass(int ch, int[] sa, int[] ea, byte[] ca) { + int k = Arrays.binarySearch(sa, ch); + if (k >= 0) { + return ca [ k ]; + } else { + k = - (k + 1); + if (k == 0) { + return BidiConstants.L; + } else if (ch <= ea [ k - 1 ]) { + return ca [ k - 1 ]; + } else { + return BidiConstants.L; + } + } +} + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/bidi/BidiConstants.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/bidi/BidiConstants.java new file mode 100644 index 00000000000..845d9a70be6 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/bidi/BidiConstants.java @@ -0,0 +1,94 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.bidi; + + +/** + *

Constants used for bidirectional processing.

+ * + *

Adapted from the Apache FOP Project.

+ * + * @author Glenn Adams + */ +public interface BidiConstants { + + // bidi character class + + /** first external (official) category */ + int FIRST = 1; + + // strong category + /** left-to-right class */ + int L = 1; + /** left-to-right embedding class */ + int LRE = 2; + /** left-to-right override class */ + int LRO = 3; + /** right-to-left class */ + int R = 4; + /** right-to-left arabic class */ + int AL = 5; + /** right-to-left embedding class */ + int RLE = 6; + /** right-to-left override class */ + int RLO = 7; + + // weak category + /** pop directional formatting class */ + int PDF = 8; + /** european number class */ + int EN = 9; + /** european number separator class */ + int ES = 10; + /** european number terminator class */ + int ET = 11; + /** arabic number class */ + int AN = 12; + /** common number separator class */ + int CS = 13; + /** non-spacing mark class */ + int NSM = 14; + /** boundary neutral class */ + int BN = 15; + + // neutral category + /** paragraph separator class */ + int B = 16; + /** segment separator class */ + int S = 17; + /** whitespace class */ + int WS = 18; + /** other neutrals class */ + int ON = 19; + + /** last external (official) category */ + int LAST = 19; + + // implementation specific categories + /** placeholder for low surrogate */ + int SURROGATE = 20; + + // other constants + /** last + /** maximum bidirectional levels */ + int MAX_LEVELS = 61; + /** override flag */ + int OVERRIDE = 128; +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/ArabicScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/ArabicScriptProcessor.java new file mode 100644 index 00000000000..fbce83b1167 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/ArabicScriptProcessor.java @@ -0,0 +1,640 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.advanced.GlyphDefinitionTable; +import org.apache.fontbox.ttf.advanced.bidi.BidiClass; +import org.apache.fontbox.ttf.advanced.bidi.BidiConstants; +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphContextTester; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +// CSOFF: LineLengthCheck + +/** + *

The ArabicScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Arabic script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class ArabicScriptProcessor extends DefaultScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(ArabicScriptProcessor.class); + + /** features to use for substitutions */ + private static final String[] GSUB_FEATURES = + { + "calt", // contextual alternates + "ccmp", // glyph composition/decomposition + "fina", // final (terminal) forms + "init", // initial forms + "isol", // isolated formas + "liga", // standard ligatures + "medi", // medial forms + "rlig" // required ligatures + }; + + /** features to use for positioning */ + private static final String[] GPOS_FEATURES = + { + "curs", // cursive positioning + "kern", // kerning + "mark", // mark to base or ligature positioning + "mkmk" // mark to mark positioning + }; + + private static class SubstitutionScriptContextTester implements ScriptContextTester { + private static Map testerMap = new HashMap<>(); + static { + testerMap.put("fina", new GlyphContextTester() { + public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + return inFinalContext(script, language, feature, gs, index, flags); + } + }); + testerMap.put("init", new GlyphContextTester() { + public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + return inInitialContext(script, language, feature, gs, index, flags); + } + }); + testerMap.put("isol", new GlyphContextTester() { + public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + return inIsolateContext(script, language, feature, gs, index, flags); + } + }); + testerMap.put("liga", new GlyphContextTester() { + public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + return inLigatureContext(script, language, feature, gs, index, flags); + } + }); + testerMap.put("medi", new GlyphContextTester() { + public boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + return inMedialContext(script, language, feature, gs, index, flags); + } + }); + } + public GlyphContextTester getTester(String feature) { + return (GlyphContextTester) testerMap.get(feature); + } + } + + private static class PositioningScriptContextTester implements ScriptContextTester { + private static Map testerMap = new HashMap<>(); + public GlyphContextTester getTester(String feature) { + return (GlyphContextTester) testerMap.get(feature); + } + } + + private final ScriptContextTester subContextTester; + private final ScriptContextTester posContextTester; + + ArabicScriptProcessor(String script) { + super(script); + this.subContextTester = new SubstitutionScriptContextTester(); + this.posContextTester = new PositioningScriptContextTester(); + } + + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures(Object[][] features) { + return GSUB_FEATURES; + } + + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return subContextTester; + } + + /** {@inheritDoc} */ + public String[] getPositioningFeatures(Object[][] features) { + return GPOS_FEATURES; + } + + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return posContextTester; + } + + /** {@inheritDoc} */ + @Override + public GlyphSequence + reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] widths, int[][] gpa, String script, String language, Object[][] features) { + // a side effect of BIDI reordering is to order combining marks before their base, so we need to override the default here to + // prevent double reordering + return gs; + } + + private static boolean inFinalContext(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasFinalPrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasFinalThisContext(ca, nc, s, e)) { + return false; + } else if (forceFinalThisContext(ca, nc, s, e)) { + return true; + } else if (!hasFinalSucceedingContext(ca, nc, s, e)) { + return false; + } else { + return true; + } + } + } + + private static boolean inInitialContext(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasInitialPrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasInitialThisContext(ca, nc, s, e)) { + return false; + } else if (!hasInitialSucceedingContext(ca, nc, s, e)) { + return false; + } else { + return true; + } + } + } + + private static boolean inIsolateContext(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasIsolatePrecedingContext(ca, nc, s, e)) + return false; + else if (!hasIsolateSucceedingContext(ca, nc, s, e)) + return false; + else + return true; + } + } + + private static boolean inLigatureContext(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasLigaturePrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasLigatureSucceedingContext(ca, nc, s, e)) { + return false; + } else { + return true; + } + } + } + + private static boolean inMedialContext(String script, String language, String feature, GlyphSequence gs, int index, int flags) { + CharAssociation a = gs.getAssociation(index); + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if (!hasMedialPrecedingContext(ca, nc, s, e)) { + return false; + } else if (!hasMedialThisContext(ca, nc, s, e)) { + return false; + } else if (!hasMedialSucceedingContext(ca, nc, s, e)) { + return false; + } else { + return true; + } + } + } + + private static boolean hasFinalPrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return isZWJ(chp); + } else if (hasIsolateInitial(chp)) { + return false; + } else { + return true; + } + } + + private static boolean hasFinalThisContext(int[] ca, int nc, int s, int e) { + int chl = 0; // last non-{NSM,ZWJ} char in [s,e) + int cll = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = n - i - 1; + int j = s + k; + if ((j >= 0) && (j < nc)) { + chl = ca [ j ]; + cll = BidiClass.getBidiClass(chl); + if ((cll != BidiConstants.NSM) && !isZWJ(chl)) { + break; + } + } + } + if (cll != BidiConstants.AL) { + return false; + } else if (hasIsolateFinal(chl)) { + return false; + } else { + return true; + } + } + + private static boolean forceFinalThisContext(int[] ca, int nc, int s, int e) { + int chl = 0; // last non-{NSM,ZWJ} char in [s,e) + int cll = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = n - i - 1; + int j = s + k; + if ((j >= 0) && (j < nc)) { + chl = ca [ j ]; + cll = BidiClass.getBidiClass(chl); + if ((cll != BidiConstants.NSM) && !isZWJ(chl)) { + break; + } + } + } + if (cll != BidiConstants.AL) { + return false; + } else if (hasIsolateInitial(chl)) { + return true; + } else { + return false; + } + } + + private static boolean hasFinalSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca [ i ]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return !isZWJ(chs); + } else if (hasIsolateFinal(chs)) { + return true; + } else { + return false; + } + } + + private static boolean hasInitialPrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return !isZWJ(chp); + } else if (hasIsolateInitial(chp)) { + return true; + } else { + return false; + } + } + + private static boolean hasInitialThisContext(int[] ca, int nc, int s, int e) { + int chf = 0; // first non-{NSM,ZWJ} char in [s,e) + int clf = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = s + i; + if ((k >= 0) && (k < nc)) { + chf = ca [ s + i ]; + clf = BidiClass.getBidiClass(chf); + if ((clf != BidiConstants.NSM) && !isZWJ(chf)) { + break; + } + } + } + if (clf != BidiConstants.AL) { + return false; + } else if (hasIsolateInitial(chf)) { + return false; + } else { + return true; + } + } + + private static boolean hasInitialSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca [ i ]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return isZWJ(chs); + } else if (hasIsolateFinal(chs)) { + return false; + } else { + return true; + } + } + + private static boolean hasMedialPrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return isZWJ(chp); + } else if (hasIsolateInitial(chp)) { + return false; + } else { + return true; + } + } + + private static boolean hasMedialThisContext(int[] ca, int nc, int s, int e) { + int chf = 0; // first non-{NSM,ZWJ} char in [s,e) + int clf = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = s + i; + if ((k >= 0) && (k < nc)) { + chf = ca [ s + i ]; + clf = BidiClass.getBidiClass(chf); + if ((clf != BidiConstants.NSM) && !isZWJ(chf)) { + break; + } + } + } + if (clf != BidiConstants.AL) { + return false; + } + int chl = 0; // last non-{NSM,ZWJ} char in [s,e) + int cll = 0; + for (int i = 0, n = e - s; i < n; i++) { + int k = n - i - 1; + int j = s + k; + if ((j >= 0) && (j < nc)) { + chl = ca [ j ]; + cll = BidiClass.getBidiClass(chl); + if ((cll != BidiConstants.NSM) && !isZWJ(chl)) { + break; + } + } + } + if (cll != BidiConstants.AL) { + return false; + } else if (hasIsolateFinal(chf)) { + return false; + } else if (hasIsolateInitial(chl)) { + return false; + } else { + return true; + } + } + + private static boolean hasMedialSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca [ i ]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return isZWJ(chs); + } else if (hasIsolateFinal(chs)) { + return false; + } else { + return true; + } + } + + private static boolean hasIsolatePrecedingContext(int[] ca, int nc, int s, int e) { + int chp = 0; // preceding non-NSM char in [0,s) searching back from s + int clp = 0; + for (int i = s; i > 0; i--) { + int k = i - 1; + if ((k >= 0) && (k < nc)) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass(chp); + if (clp != BidiConstants.NSM) { + break; + } + } + } + if (clp != BidiConstants.AL) { + return true; + } else if (hasIsolateInitial(chp)) { + return true; + } else { + return false; + } + } + + private static boolean hasIsolateSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca [ i ]; + cls = BidiClass.getBidiClass(chs); + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls != BidiConstants.AL) { + return true; + } else if (hasIsolateFinal(chs)) { + return true; + } else { + return false; + } + } + + private static boolean hasLigaturePrecedingContext(int[] ca, int nc, int s, int e) { + return true; + } + + private static boolean hasLigatureSucceedingContext(int[] ca, int nc, int s, int e) { + int chs = 0; // succeeding non-NSM char in [e,nc) searching forward from e + int cls = 0; + for (int i = e, n = nc; i < n; i++) { + chs = ca [ i ]; + cls = BidiClass.getBidiClass(chs); + // TBD - does ZWJ have impact here? + if (cls != BidiConstants.NSM) { + break; + } + } + if (cls == BidiConstants.AL) { + return true; + } else { + return false; + } + } + + /** + * Ordered array of Unicode scalars designating those characters in the AL class + * which exhibit an isolated form in word initial position. + */ + private static final int[] ISOLATED_INITIALS = { + 0x0608, // RAY + 0x060B, // AFGHANI SIGN + 0x060D, // DATE SEPARATOR + 0x061B, // SEMICOLON + 0x061C, // LETTER MARK + 0x061F, // QUESTION MARK + 0x0621, // HAMZA + 0x0622, // ALEF WITH MADDA ABOVE + 0x0623, // ALEF WITH HAMZA ABOVE + 0x0624, // WAW WITH HAMZA ABOVE + 0x0625, // ALEF WITH HAMZA BELOWW + 0x0627, // ALEF + 0x062F, // DAL + 0x0630, // THAL + 0x0631, // REH + 0x0632, // ZAIN + 0x0648, // WAW + 0x066D, // FIVE POINTED STAR + 0x0671, // ALEF WASLA + 0x0672, // ALEF WITH WAVY HAMZA ABOVE + 0x0673, // ALEF WITH WAVY HAMZA BELOW + 0x0675, // HIGH HAMZA ALEF + 0x0676, // HIGH HAMZA WAW + 0x0677, // U WITH HAMZA ABOVE + 0x0688, // DDAL + 0x0689, // DAL WITH RING + 0x068A, // DAL WITH DOT BELOW + 0x068B, // DAL WITH DOT BELOW AND SMALL TAH + 0x068C, // DAHAL + 0x068D, // DDAHAL + 0x068E, // DUL + 0x068F, // DUL WITH THREE DOTS ABOVE DOWNWARDS + 0x0690, // DUL WITH FOUR DOTS ABOVE + 0x0691, // RREH + 0x0692, // REH WITH SMALL V + 0x0693, // REH WITH RING + 0x0694, // REH WITH DOT BELOW + 0x0695, // REH WITH SMALL V BELOW + 0x0696, // REH WITH DOT BELOW AND DOT ABOVE + 0x0697, // REH WITH TWO DOTS ABOVE + 0x0698, // JEH + 0x0699, // REH WITH FOUR DOTS ABOVE + 0x06C4, // WAW WITH RING + 0x06C5, // KIRGHIZ OE + 0x06C6, // OE + 0x06C7, // U + 0x06C8, // YU + 0x06C9, // KIRGHIZ YU + 0x06CA, // WAW WITH TWO DOTS ABOVE + 0x06CB, // VE + 0x06CF, // WAW WITH DOT ABOVE + 0x06D4, // FULL STOP + 0x06EE, // DAL WITH INVERTED V + 0x06EF, // REH WITH INVERTED V + 0x06FD, // SINDHI AMPERSAND + 0x06FE // SINDHI POSTPOSITION MEN + }; + + private static boolean hasIsolateInitial(int ch) { + return Arrays.binarySearch(ISOLATED_INITIALS, ch) >= 0; + } + + /** + * Ordered array of Unicode scalars designating those characters in the AL class + * which exhibit an isolated form in word final position. + */ + private static final int[] ISOLATED_FINALS = { + 0x0608, // RAY + 0x060B, // AFGHANI SIGN + 0x060D, // DATE SEPARATOR + 0x061B, // SEMICOLON + 0x061C, // LETTER MARK + 0x061F, // QUESTION MARK + 0x0621, // HAMZA + 0x066D, // FIVE POINTED STAR + 0x06D4, // FULL STOP + 0x06FD, // SINDHI AMPERSAND + 0x06FE // SINDHI POSTPOSITION MEN + }; + + private static boolean hasIsolateFinal(int ch) { + return Arrays.binarySearch(ISOLATED_FINALS, ch) >= 0; + } + + private static boolean isZWJ(int ch) { + return ch == '\u200D'; + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/DefaultScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/DefaultScriptProcessor.java new file mode 100644 index 00000000000..d69652d3dd9 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/DefaultScriptProcessor.java @@ -0,0 +1,207 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import java.util.List; + +import org.apache.fontbox.ttf.advanced.GlyphDefinitionTable; +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +// CSOFF: LineLengthCheck + +/** + *

Default script processor, which enables default glyph composition/decomposition, common ligatures, localized forms + * and kerning.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class DefaultScriptProcessor extends ScriptProcessor { + + private static final String ccmpFeatureName = "ccmp"; + private static final String kernFeatureName = "kern"; + private static final String ligaFeatureName = "liga"; + private static final String loclFeatureName = "locl"; + private static final String markFeatureName = "mark"; + private static final String mkmkFeatureName = "mkmk"; + + /** features to use for substitutions */ + private static final String[] GSUB_FEATURES = + { + ccmpFeatureName, // glyph composition/decomposition + ligaFeatureName, // common ligatures + loclFeatureName // localized forms + }; + + /** features to use for positioning */ + private static final String[] GPOS_FEATURES = + { + kernFeatureName, // kerning + markFeatureName, // mark to base or ligature positioning + mkmkFeatureName // mark to mark positioning + }; + + DefaultScriptProcessor(String script) { + super(script); + } + + @Override + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures(Object[][] features) { + if ((features == null) || (features.length == 0)) + return GSUB_FEATURES; + else + return augmentFeatures(GSUB_FEATURES, features); + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public String[] getPositioningFeatures(Object[][] features) { + if ((features == null) || (features.length == 0)) + return GPOS_FEATURES; + else + return augmentFeatures(GPOS_FEATURES, features); + } + + private String[] augmentFeatures(String[] features, Object[][] moreFeatures) { + assert features != null; + assert moreFeatures != null; + List augmentedFeatures = new java.util.ArrayList(); + for (String f : features) + augmentedFeatures.add(f); + for (int i = 0, n = moreFeatures.length; i < n; ++i) { + Object[] mf = moreFeatures[i]; + if (mf != null) { + assert mf.length > 0; + if (mf[0] instanceof String) { + String mfn = (String) mf[0]; + if (mfn.equals(kernFeatureName)) { + if (mf.length > 1) { + if (mf[1] instanceof Boolean) { + boolean kerningEnabled = (Boolean) mf[1]; + if (augmentedFeatures.contains(kernFeatureName)) { + if (!kerningEnabled) + augmentedFeatures.remove(kernFeatureName); + } else { + if (kerningEnabled) + augmentedFeatures.add(kernFeatureName); + } + } + } + } else if (!augmentedFeatures.contains(mfn)) { + augmentedFeatures.add(mfn); + } + } + } + } + return augmentedFeatures.toArray(new String[augmentedFeatures.size()]); + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public GlyphSequence + reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] unscaledWidths, int[][] gpa, String script, String language, Object[][] features) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray(false); + int nm = 0; + // count combining marks + for (int i = 0; i < ng; i++) { + int gid = ga [ i ]; + int gw = unscaledWidths [ i ]; + if (isReorderedMark(gdef, ga, unscaledWidths, i)) { + nm++; + } + } + // only reorder if there is at least one mark and at least one non-mark glyph + if ((nm > 0) && ((ng - nm) > 0)) { + CharAssociation[] aa = gs.getAssociations(0, -1); + int[] nga = new int [ ng ]; + int[][] npa = (gpa != null) ? new int [ ng ][] : null; + CharAssociation[] naa = new CharAssociation [ ng ]; + int k = 0; + CharAssociation ba = null; + int bg = -1; + int[] bpa = null; + for (int i = 0; i < ng; i++) { + int gid = ga [ i ]; + int[] pa = (gpa != null) ? gpa [ i ] : null; + CharAssociation ca = aa [ i ]; + if (isReorderedMark(gdef, ga, unscaledWidths, i)) { + nga [ k ] = gid; + naa [ k ] = ca; + if (npa != null) { + npa [ k ] = pa; + } + k++; + } else { + if (bg != -1) { + nga [ k ] = bg; + naa [ k ] = ba; + if (npa != null) { + npa [ k ] = bpa; + } + k++; + bg = -1; + ba = null; + bpa = null; + } + if (bg == -1) { + bg = gid; + ba = ca; + bpa = pa; + } + } + } + if (bg != -1) { + nga [ k ] = bg; + naa [ k ] = ba; + if (npa != null) { + npa [ k ] = bpa; + } + k++; + } + assert k == ng; + if (npa != null) { + System.arraycopy(npa, 0, gpa, 0, ng); + } + return new GlyphSequence(gs, null, nga, null, null, naa, null); + } else { + return gs; + } + } + + protected boolean isReorderedMark(GlyphDefinitionTable gdef, int[] glyphs, int[] unscaledWidths, int index) { + return gdef.isGlyphClass(glyphs[index], GlyphDefinitionTable.GLYPH_CLASS_MARK); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/DevanagariScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/DevanagariScriptProcessor.java new file mode 100644 index 00000000000..716118de23c --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/DevanagariScriptProcessor.java @@ -0,0 +1,540 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; + +// CSOFF: LineLengthCheck + +/** + *

The DevanagariScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Devanagari script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class DevanagariScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(DevanagariScriptProcessor.class); + + DevanagariScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return DevanagariSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k) .getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k) .getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca [ i ])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class DevanagariSyllabizer extends DefaultSyllabizer { + DevanagariSyllabizer(String script, String language) { + super(script, language); + } + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca [ s ]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + 1 ]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + i ]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca [ s + i ]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // devanagari character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // devanagari block range + static final int CCA_START = 0x0900; // first code point mapped by cca + static final int CCA_END = 0x0980; // last code point + 1 mapped by cca + // devanagari character type lookups + static final short[] CCA = { + C_O, // 0x0900 // INVERTED CANDRABINDU + C_O, // 0x0901 // CANDRABINDU + C_O, // 0x0902 // ANUSVARA + C_O, // 0x0903 // VISARGA + C_V, // 0x0904 // SHORT A + C_V, // 0x0905 // A + C_V, // 0x0906 // AA + C_V, // 0x0907 // I + C_V, // 0x0908 // II + C_V, // 0x0909 // U + C_V, // 0x090A // UU + C_V, // 0x090B // VOCALIC R + C_V, // 0x090C // VOCALIC L + C_V, // 0x090D // CANDRA E + C_V, // 0x090E // SHORT E + C_V, // 0x090F // E + C_V, // 0x0910 // AI + C_V, // 0x0911 // CANDRA O + C_V, // 0x0912 // SHORT O + C_V, // 0x0913 // O + C_V, // 0x0914 // AU + C_C, // 0x0915 // KA + C_C, // 0x0916 // KHA + C_C, // 0x0917 // GA + C_C, // 0x0918 // GHA + C_C, // 0x0919 // NGA + C_C, // 0x091A // CA + C_C, // 0x091B // CHA + C_C, // 0x091C // JA + C_C, // 0x091D // JHA + C_C, // 0x091E // NYA + C_C, // 0x091F // TTA + C_C, // 0x0920 // TTHA + C_C, // 0x0921 // DDA + C_C, // 0x0922 // DDHA + C_C, // 0x0923 // NNA + C_C, // 0x0924 // TA + C_C, // 0x0925 // THA + C_C, // 0x0926 // DA + C_C, // 0x0927 // DHA + C_C, // 0x0928 // NA + C_C, // 0x0929 // NNNA + C_C, // 0x092A // PA + C_C, // 0x092B // PHA + C_C, // 0x092C // BA + C_C, // 0x092D // BHA + C_C, // 0x092E // MA + C_C, // 0x092F // YA + C_C | C_R, // 0x0930 // RA + C_C | C_R | C_N, // 0x0931 // RRA = 0930+093C + C_C, // 0x0932 // LA + C_C, // 0x0933 // LLA + C_C, // 0x0934 // LLLA + C_C, // 0x0935 // VA + C_C, // 0x0936 // SHA + C_C, // 0x0937 // SSA + C_C, // 0x0938 // SA + C_C, // 0x0939 // HA + C_M, // 0x093A // OE (KASHMIRI) + C_M, // 0x093B // OOE (KASHMIRI) + C_N, // 0x093C // NUKTA + C_S, // 0x093D // AVAGRAHA + C_M, // 0x093E // AA + C_M | C_PRE, // 0x093F // I + C_M, // 0x0940 // II + C_M, // 0x0941 // U + C_M, // 0x0942 // UU + C_M, // 0x0943 // VOCALIC R + C_M, // 0x0944 // VOCALIC RR + C_M, // 0x0945 // CANDRA E + C_M, // 0x0946 // SHORT E + C_M, // 0x0947 // E + C_M, // 0x0948 // AI + C_M, // 0x0949 // CANDRA O + C_M, // 0x094A // SHORT O + C_M, // 0x094B // O + C_M, // 0x094C // AU + C_H, // 0x094D // VIRAMA (HALANT) + C_M, // 0x094E // PRISHTHAMATRA E + C_M, // 0x094F // AW + C_S, // 0x0950 // OM + C_T, // 0x0951 // UDATTA + C_T, // 0x0952 // ANUDATTA + C_A, // 0x0953 // GRAVE + C_A, // 0x0954 // ACUTE + C_M, // 0x0955 // CANDRA LONG E + C_M, // 0x0956 // UE + C_M, // 0x0957 // UUE + C_C | C_N, // 0x0958 // QA + C_C | C_N, // 0x0959 // KHHA + C_C | C_N, // 0x095A // GHHA + C_C | C_N, // 0x095B // ZA + C_C | C_N, // 0x095C // DDDHA + C_C | C_N, // 0x095D // RHA + C_C | C_N, // 0x095E // FA + C_C | C_N, // 0x095F // YYA + C_V, // 0x0960 // VOCALIC RR + C_V, // 0x0961 // VOCALIC LL + C_M, // 0x0962 // VOCALIC RR + C_M, // 0x0963 // VOCALIC LL + C_P, // 0x0964 // DANDA + C_P, // 0x0965 // DOUBLE DANDA + C_D, // 0x0966 // ZERO + C_D, // 0x0967 // ONE + C_D, // 0x0968 // TWO + C_D, // 0x0969 // THREE + C_D, // 0x096A // FOUR + C_D, // 0x096B // FIVE + C_D, // 0x096C // SIX + C_D, // 0x096D // SEVEN + C_D, // 0x096E // EIGHT + C_D, // 0x096F // NINE + C_S, // 0x0970 // ABBREVIATION SIGN + C_S, // 0x0971 // HIGH SPACING DOT + C_V, // 0x0972 // CANDRA A (MARATHI) + C_V, // 0x0973 // OE (KASHMIRI) + C_V, // 0x0974 // OOE (KASHMIRI) + C_V, // 0x0975 // AW (KASHMIRI) + C_V, // 0x0976 // UE (KASHMIRI) + C_V, // 0x0977 // UUE (KASHMIRI) + C_U, // 0x0978 // UNASSIGNED + C_C, // 0x0979 // ZHA + C_C, // 0x097A // HEAVY YA + C_C, // 0x097B // GGAA (SINDHI) + C_C, // 0x097C // JJA (SINDHI) + C_C, // 0x097D // GLOTTAL STOP (LIMBU) + C_C, // 0x097E // DDDA (SINDHI) + C_C // 0x097F // BBA (SINDHI) + }; + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA [ c - CCA_START ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA [ c - CCA_START ] & f) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c, C_C); + } + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c, C_V); + } + static boolean isN(int c) { + return c == 0x093C; + } + static boolean isH(int c) { + return c == 0x094D; + } + static boolean isM(int c) { + return isType(c, C_M); + } + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/GujaratiScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/GujaratiScriptProcessor.java new file mode 100644 index 00000000000..b0ba82f0063 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/GujaratiScriptProcessor.java @@ -0,0 +1,540 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; + +// CSOFF: LineLengthCheck + +/** + *

The GujaratiScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Gujarati script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class GujaratiScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GujaratiScriptProcessor.class); + + GujaratiScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return GujaratiSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k) .getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k) .getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca [ i ])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class GujaratiSyllabizer extends DefaultSyllabizer { + GujaratiSyllabizer(String script, String language) { + super(script, language); + } + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca [ s ]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + 1 ]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + i ]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca [ s + i ]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // gujarati character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // gujarati block range + static final int CCA_START = 0x0A80; // first code point mapped by cca + static final int CCA_END = 0x0B00; // last code point + 1 mapped by cca + // gujarati character type lookups + static final short[] CCA = { + C_U, // 0x0A80 // UNASSIGNED + C_O, // 0x0A81 // CANDRABINDU + C_O, // 0x0A82 // ANUSVARA + C_O, // 0x0A83 // VISARGA + C_U, // 0x0A84 // UNASSIGNED + C_V, // 0x0A85 // A + C_V, // 0x0A86 // AA + C_V, // 0x0A87 // I + C_V, // 0x0A88 // II + C_V, // 0x0A89 // U + C_V, // 0x0A8A // UU + C_V, // 0x0A8B // VOCALIC R + C_V, // 0x0A8C // VOCALIC L + C_V, // 0x0A8D // CANDRA E + C_U, // 0x0A8E // UNASSIGNED + C_V, // 0x0A8F // E + C_V, // 0x0A90 // AI + C_V, // 0x0A91 // CANDRA O + C_U, // 0x0A92 // UNASSIGNED + C_V, // 0x0A93 // O + C_V, // 0x0A94 // AU + C_C, // 0x0A95 // KA + C_C, // 0x0A96 // KHA + C_C, // 0x0A97 // GA + C_C, // 0x0A98 // GHA + C_C, // 0x0A99 // NGA + C_C, // 0x0A9A // CA + C_C, // 0x0A9B // CHA + C_C, // 0x0A9C // JA + C_C, // 0x0A9D // JHA + C_C, // 0x0A9E // NYA + C_C, // 0x0A9F // TTA + C_C, // 0x0AA0 // TTHA + C_C, // 0x0AA1 // DDA + C_C, // 0x0AA2 // DDHA + C_C, // 0x0AA3 // NNA + C_C, // 0x0AA4 // TA + C_C, // 0x0AA5 // THA + C_C, // 0x0AA6 // DA + C_C, // 0x0AA7 // DHA + C_C, // 0x0AA8 // NA + C_U, // 0x0AA9 // UNASSIGNED + C_C, // 0x0AAA // PA + C_C, // 0x0AAB // PHA + C_C, // 0x0AAC // BA + C_C, // 0x0AAD // BHA + C_C, // 0x0AAE // MA + C_C, // 0x0AAF // YA + C_C | C_R, // 0x0AB0 // RA + C_U, // 0x0AB1 // UNASSIGNED + C_C, // 0x0AB2 // LA + C_C, // 0x0AB3 // LLA + C_U, // 0x0AB4 // UNASSIGNED + C_C, // 0x0AB5 // VA + C_C, // 0x0AB6 // SHA + C_C, // 0x0AB7 // SSA + C_C, // 0x0AB8 // SA + C_C, // 0x0AB9 // HA + C_U, // 0x0ABA // UNASSIGNED + C_U, // 0x0ABB // UNASSIGNED + C_N, // 0x0ABC // NUKTA + C_S, // 0x0ABD // AVAGRAHA + C_M, // 0x0ABE // AA + C_M | C_PRE, // 0x0ABF // I + C_M, // 0x0AC0 // II + C_M, // 0x0AC1 // U + C_M, // 0x0AC2 // UU + C_M, // 0x0AC3 // VOCALIC R + C_M, // 0x0AC4 // VOCALIC RR + C_M, // 0x0AC5 // CANDRA E + C_U, // 0x0AC6 // UNASSIGNED + C_M, // 0x0AC7 // E + C_M, // 0x0AC8 // AI + C_M, // 0x0AC9 // CANDRA O + C_U, // 0x0ACA // UNASSIGNED + C_M, // 0x0ACB // O + C_M, // 0x0ACC // AU + C_H, // 0x0ACD // VIRAMA (HALANT) + C_U, // 0x0ACE // UNASSIGNED + C_U, // 0x0ACF // UNASSIGNED + C_S, // 0x0AD0 // OM + C_U, // 0x0AD1 // UNASSIGNED + C_U, // 0x0AD2 // UNASSIGNED + C_U, // 0x0AD3 // UNASSIGNED + C_U, // 0x0AD4 // UNASSIGNED + C_U, // 0x0AD5 // UNASSIGNED + C_U, // 0x0AD6 // UNASSIGNED + C_U, // 0x0AD7 // UNASSIGNED + C_U, // 0x0AD8 // UNASSIGNED + C_U, // 0x0AD9 // UNASSIGNED + C_U, // 0x0ADA // UNASSIGNED + C_U, // 0x0ADB // UNASSIGNED + C_U, // 0x0ADC // UNASSIGNED + C_U, // 0x0ADD // UNASSIGNED + C_U, // 0x0ADE // UNASSIGNED + C_U, // 0x0ADF // UNASSIGNED + C_V, // 0x0AE0 // VOCALIC RR + C_V, // 0x0AE1 // VOCALIC LL + C_M, // 0x0AE2 // VOCALIC L + C_M, // 0x0AE3 // VOCALIC LL + C_U, // 0x0AE4 // UNASSIGNED + C_U, // 0x0AE5 // UNASSIGNED + C_D, // 0x0AE6 // ZERO + C_D, // 0x0AE7 // ONE + C_D, // 0x0AE8 // TWO + C_D, // 0x0AE9 // THREE + C_D, // 0x0AEA // FOUR + C_D, // 0x0AEB // FIVE + C_D, // 0x0AEC // SIX + C_D, // 0x0AED // SEVEN + C_D, // 0x0AEE // EIGHT + C_D, // 0x0AEF // NINE + C_U, // 0x0AF0 // UNASSIGNED + C_S, // 0x0AF1 // RUPEE SIGN + C_U, // 0x0AF2 // UNASSIGNED + C_U, // 0x0AF3 // UNASSIGNED + C_U, // 0x0AF4 // UNASSIGNED + C_U, // 0x0AF5 // UNASSIGNED + C_U, // 0x0AF6 // UNASSIGNED + C_U, // 0x0AF7 // UNASSIGNED + C_U, // 0x0AF8 // UNASSIGNED + C_U, // 0x0AF9 // UNASSIGNED + C_U, // 0x0AFA // UNASSIGNED + C_U, // 0x0AFB // UNASSIGNED + C_U, // 0x0AFC // UNASSIGNED + C_U, // 0x0AFD // UNASSIGNED + C_U, // 0x0AFE // UNASSIGNED + C_U // 0x0AFF // UNASSIGNED + }; + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA [ c - CCA_START ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA [ c - CCA_START ] & f) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c, C_C); + } + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c, C_V); + } + static boolean isN(int c) { + return c == 0x0ABC; + } + static boolean isH(int c) { + return c == 0x0ACD; + } + static boolean isM(int c) { + return isType(c, C_M); + } + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/GurmukhiScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/GurmukhiScriptProcessor.java new file mode 100644 index 00000000000..baa45281889 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/GurmukhiScriptProcessor.java @@ -0,0 +1,540 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; + +// CSOFF: LineLengthCheck + +/** + *

The GurmukhiScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Gurmukhi script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class GurmukhiScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GurmukhiScriptProcessor.class); + + GurmukhiScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return GurmukhiSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k) .getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k) .getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca [ i ])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class GurmukhiSyllabizer extends DefaultSyllabizer { + GurmukhiSyllabizer(String script, String language) { + super(script, language); + } + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca [ s ]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + 1 ]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + i ]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca [ s + i ]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // gurmukhi character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // gurmukhi block range + static final int CCA_START = 0x0A00; // first code point mapped by cca + static final int CCA_END = 0x0A80; // last code point + 1 mapped by cca + // gurmukhi character type lookups + static final short[] CCA = { + C_U, // 0x0A00 // UNASSIGNED + C_O, // 0x0A01 // ADAK BINDI + C_O, // 0x0A02 // BINDI + C_O, // 0x0A03 // VISARGA + C_U, // 0x0A04 // UNASSIGNED + C_V, // 0x0A05 // A + C_V, // 0x0A06 // AA + C_V, // 0x0A07 // I + C_V, // 0x0A08 // II + C_V, // 0x0A09 // U + C_V, // 0x0A0A // UU + C_U, // 0x0A0B // UNASSIGNED + C_U, // 0x0A0C // UNASSIGNED + C_U, // 0x0A0D // UNASSIGNED + C_U, // 0x0A0E // UNASSIGNED + C_V, // 0x0A0F // E + C_V, // 0x0A10 // AI + C_U, // 0x0A11 // UNASSIGNED + C_U, // 0x0A12 // UNASSIGNED + C_V, // 0x0A13 // O + C_V, // 0x0A14 // AU + C_C, // 0x0A15 // KA + C_C, // 0x0A16 // KHA + C_C, // 0x0A17 // GA + C_C, // 0x0A18 // GHA + C_C, // 0x0A19 // NGA + C_C, // 0x0A1A // CA + C_C, // 0x0A1B // CHA + C_C, // 0x0A1C // JA + C_C, // 0x0A1D // JHA + C_C, // 0x0A1E // NYA + C_C, // 0x0A1F // TTA + C_C, // 0x0A20 // TTHA + C_C, // 0x0A21 // DDA + C_C, // 0x0A22 // DDHA + C_C, // 0x0A23 // NNA + C_C, // 0x0A24 // TA + C_C, // 0x0A25 // THA + C_C, // 0x0A26 // DA + C_C, // 0x0A27 // DHA + C_C, // 0x0A28 // NA + C_U, // 0x0A29 // UNASSIGNED + C_C, // 0x0A2A // PA + C_C, // 0x0A2B // PHA + C_C, // 0x0A2C // BA + C_C, // 0x0A2D // BHA + C_C, // 0x0A2E // MA + C_C, // 0x0A2F // YA + C_C | C_R, // 0x0A30 // RA + C_U, // 0x0A31 // UNASSIGNED + C_C, // 0x0A32 // LA + C_C, // 0x0A33 // LLA + C_U, // 0x0A34 // UNASSIGNED + C_C, // 0x0A35 // VA + C_C, // 0x0A36 // SHA + C_U, // 0x0A37 // UNASSIGNED + C_C, // 0x0A38 // SA + C_C, // 0x0A39 // HA + C_U, // 0x0A3A // UNASSIGNED + C_U, // 0x0A3B // UNASSIGNED + C_N, // 0x0A3C // NUKTA + C_U, // 0x0A3D // UNASSIGNED + C_M, // 0x0A3E // AA + C_M | C_PRE, // 0x0A3F // I + C_M, // 0x0A40 // II + C_M, // 0x0A41 // U + C_M, // 0x0A42 // UU + C_U, // 0x0A43 // UNASSIGNED + C_U, // 0x0A44 // UNASSIGNED + C_U, // 0x0A45 // UNASSIGNED + C_U, // 0x0A46 // UNASSIGNED + C_M, // 0x0A47 // EE + C_M, // 0x0A48 // AI + C_U, // 0x0A49 // UNASSIGNED + C_U, // 0x0A4A // UNASSIGNED + C_M, // 0x0A4B // OO + C_M, // 0x0A4C // AU + C_H, // 0x0A4D // VIRAMA (HALANT) + C_U, // 0x0A4E // UNASSIGNED + C_U, // 0x0A4F // UNASSIGNED + C_U, // 0x0A50 // UNASSIGNED + C_T, // 0x0A51 // UDATTA + C_U, // 0x0A52 // UNASSIGNED + C_U, // 0x0A53 // UNASSIGNED + C_U, // 0x0A54 // UNASSIGNED + C_U, // 0x0A55 // UNASSIGNED + C_U, // 0x0A56 // UNASSIGNED + C_U, // 0x0A57 // UNASSIGNED + C_U, // 0x0A58 // UNASSIGNED + C_C | C_N, // 0x0A59 // KHHA + C_C | C_N, // 0x0A5A // GHHA + C_C | C_N, // 0x0A5B // ZA + C_C | C_N, // 0x0A5C // RRA + C_U, // 0x0A5D // UNASSIGNED + C_C | C_N, // 0x0A5E // FA + C_U, // 0x0A5F // UNASSIGNED + C_U, // 0x0A60 // UNASSIGNED + C_U, // 0x0A61 // UNASSIGNED + C_U, // 0x0A62 // UNASSIGNED + C_U, // 0x0A63 // UNASSIGNED + C_U, // 0x0A64 // UNASSIGNED + C_U, // 0x0A65 // UNASSIGNED + C_D, // 0x0A66 // ZERO + C_D, // 0x0A67 // ONE + C_D, // 0x0A68 // TWO + C_D, // 0x0A69 // THREE + C_D, // 0x0A6A // FOUR + C_D, // 0x0A6B // FIVE + C_D, // 0x0A6C // SIX + C_D, // 0x0A6D // SEVEN + C_D, // 0x0A6E // EIGHT + C_D, // 0x0A6F // NINE + C_O, // 0x0A70 // TIPPI + C_O, // 0x0A71 // ADDAK + C_V, // 0x0A72 // IRI + C_V, // 0x0A73 // URA + C_S, // 0x0A74 // EK ONKAR + C_O, // 0x0A75 // YAKASH + C_U, // 0x0A76 // UNASSIGNED + C_U, // 0x0A77 // UNASSIGNED + C_U, // 0x0A78 // UNASSIGNED + C_U, // 0x0A79 // UNASSIGNED + C_U, // 0x0A7A // UNASSIGNED + C_U, // 0x0A7B // UNASSIGNED + C_U, // 0x0A7C // UNASSIGNED + C_U, // 0x0A7D // UNASSIGNED + C_U, // 0x0A7E // UNASSIGNED + C_U // 0x0A7F // UNASSIGNED + }; + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA [ c - CCA_START ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA [ c - CCA_START ] & f) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c, C_C); + } + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c, C_V); + } + static boolean isN(int c) { + return c == 0x0A3C; + } + static boolean isH(int c) { + return c == 0x0A4D; + } + static boolean isM(int c) { + return isType(c, C_M); + } + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/IndicScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/IndicScriptProcessor.java new file mode 100644 index 00000000000..195d7c3d26c --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/IndicScriptProcessor.java @@ -0,0 +1,602 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.Vector; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.advanced.AdvancedTypographicTable; +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.CharScript; +import org.apache.fontbox.ttf.advanced.util.GlyphContextTester; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +// CSOFF: LineLengthCheck + +/** + *

The IndicScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Indic script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class IndicScriptProcessor extends DefaultScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(IndicScriptProcessor.class); + + /** required features to use for substitutions */ + private static final String[] GSUB_REQ_FEATURES = + { + "abvf", // above base forms + "abvs", // above base substitutions + "akhn", // akhand + "blwf", // below base forms + "blws", // below base substitutions + "ccmp", // glyph composition/decomposition + "cjct", // conjunct forms + "clig", // contextual ligatures + "half", // half forms + "haln", // halant forms + "locl", // localized forms + "nukt", // nukta forms + "pref", // pre-base forms + "pres", // pre-base substitutions + "pstf", // post-base forms + "psts", // post-base substitutions + "rkrf", // rakar forms + "rphf", // reph form + "vatu" // vattu variants + }; + + /** optional features to use for substitutions */ + private static final String[] GSUB_OPT_FEATURES = + { + "afrc", // alternative fractions + "calt", // contextual alternatives + "dlig" // discretionary ligatures + }; + + /** required features to use for positioning */ + private static final String[] GPOS_REQ_FEATURES = + { + "abvm", // above base marks + "blwm", // below base marks + "dist", // distance (adjustment) + "kern" // kerning + }; + + /** required features to use for positioning */ + private static final String[] GPOS_OPT_FEATURES = + { + }; + + private static class SubstitutionScriptContextTester implements ScriptContextTester { + private static Map testerMap = new HashMap<>(); + public GlyphContextTester getTester(String feature) { + return testerMap.get(feature); + } + } + + private static class PositioningScriptContextTester implements ScriptContextTester { + private static Map testerMap = new HashMap<>(); + public GlyphContextTester getTester(String feature) { + return testerMap.get(feature); + } + } + + /** + * Make script specific flavor of Indic script processor. + * @param script tag + * @return script processor instance + */ + public static ScriptProcessor makeProcessor(String script) { + switch (CharScript.scriptCodeFromTag(script)) { + case CharScript.SCRIPT_DEVANAGARI: + case CharScript.SCRIPT_DEVANAGARI_2: + return new DevanagariScriptProcessor(script); + case CharScript.SCRIPT_GUJARATI: + case CharScript.SCRIPT_GUJARATI_2: + return new GujaratiScriptProcessor(script); + case CharScript.SCRIPT_GURMUKHI: + case CharScript.SCRIPT_GURMUKHI_2: + return new GurmukhiScriptProcessor(script); + case CharScript.SCRIPT_TAMIL: + case CharScript.SCRIPT_TAMIL_2: + return new TamilScriptProcessor(script); + // [TBD] implement other script processors + default: + return new IndicScriptProcessor(script); + } + } + + private final ScriptContextTester subContextTester; + private final ScriptContextTester posContextTester; + + IndicScriptProcessor(String script) { + super(script); + this.subContextTester = new SubstitutionScriptContextTester(); + this.posContextTester = new PositioningScriptContextTester(); + } + + /** {@inheritDoc} */ + @Override + public String[] getSubstitutionFeatures(Object[][] features) { + return GSUB_REQ_FEATURES; + } + + /** {@inheritDoc} */ + @Override + public String[] getOptionalSubstitutionFeatures() { + return GSUB_OPT_FEATURES; + } + + /** {@inheritDoc} */ + @Override + public ScriptContextTester getSubstitutionContextTester() { + return subContextTester; + } + + /** {@inheritDoc} */ + @Override + public String[] getPositioningFeatures(Object[][] features) { + return GPOS_REQ_FEATURES; + } + + /** {@inheritDoc} */ + @Override + public String[] getOptionalPositioningFeatures() { + return GPOS_OPT_FEATURES; + } + + /** {@inheritDoc} */ + @Override + public ScriptContextTester getPositioningContextTester() { + return posContextTester; + } + + /** {@inheritDoc} */ + @Override + public GlyphSequence substitute(GlyphSequence gs, String script, String language, AdvancedTypographicTable.UseSpec[] usa, ScriptContextTester sct) { + assert usa != null; + // 1. syllabize + GlyphSequence[] sa = syllabize(gs, script, language); + // 2. process each syllable + for (int i = 0, n = sa.length; i < n; i++) { + GlyphSequence s = sa [ i ]; + // apply basic shaping subs + for (int j = 0, m = usa.length; j < m; j++) { + AdvancedTypographicTable.UseSpec us = usa [ j ]; + if (isBasicShapingUse(us)) { + s.setPredications(true); + s = us.substitute(s, script, language, sct); + } + } + // reorder pre-base matra + s = reorderPreBaseMatra(s); + // reorder reph + s = reorderReph(s); + // apply presentation subs + for (int j = 0, m = usa.length; j < m; j++) { + AdvancedTypographicTable.UseSpec us = usa [ j ]; + if (isPresentationUse(us)) { + s.setPredications(true); + s = us.substitute(s, script, language, sct); + } + } + // record result + sa [ i ] = s; + } + // 3. return reassembled substituted syllables + return unsyllabize(gs, sa); + } + + /** + * Get script specific syllabizer class. + * @return a syllabizer class object or null + */ + protected Class getSyllabizerClass() { + return null; + } + + private GlyphSequence[] syllabize(GlyphSequence gs, String script, String language) { + return Syllabizer.getSyllabizer(script, language, getSyllabizerClass()).syllabize(gs); + } + + private GlyphSequence unsyllabize(GlyphSequence gs, GlyphSequence[] sa) { + return GlyphSequence.join(gs, sa); + } + + private static Set basicShapingFeatures; + private static final String[] BASIC_SHAPING_FEATURE_STRINGS = { + "abvf", + "akhn", + "blwf", + "cjct", + "half", + "locl", + "nukt", + "pref", + "pstf", + "rkrf", + "rphf", + "vatu", + }; + static { + basicShapingFeatures = new HashSet(); + for (String s : BASIC_SHAPING_FEATURE_STRINGS) { + basicShapingFeatures.add(s); + } + } + private boolean isBasicShapingUse(AdvancedTypographicTable.UseSpec us) { + assert us != null; + if (basicShapingFeatures != null) { + return basicShapingFeatures.contains(us.getFeature()); + } else { + return false; + } + } + + private static Set presentationFeatures; + private static final String[] PRESENTATION_FEATURE_STRINGS = { + "abvs", + "blws", + "calt", + "haln", + "pres", + "psts", + }; + static { + presentationFeatures = new HashSet(); + for (String s : PRESENTATION_FEATURE_STRINGS) { + presentationFeatures.add(s); + } + } + private boolean isPresentationUse(AdvancedTypographicTable.UseSpec us) { + assert us != null; + if (presentationFeatures != null) { + return presentationFeatures.contains(us.getFeature()); + } else { + return false; + } + } + + private GlyphSequence reorderPreBaseMatra(GlyphSequence gs) { + int source; + if ((source = findPreBaseMatra(gs)) >= 0) { + int target; + if ((target = findPreBaseMatraTarget(gs, source)) >= 0) { + if (target != source) { + gs = reorder(gs, source, target); + } + } + } + return gs; + } + + /** + * Find pre-base matra in sequence. + * @param gs input sequence + * @return index of pre-base matra or -1 if not found + */ + protected int findPreBaseMatra(GlyphSequence gs) { + return -1; + } + + /** + * Find pre-base matra target in sequence. + * @param gs input sequence + * @param source index of pre-base matra + * @return index of pre-base matra target or -1 + */ + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + return -1; + } + + private GlyphSequence reorderReph(GlyphSequence gs) { + int source; + if ((source = findReph(gs)) >= 0) { + int target; + if ((target = findRephTarget(gs, source)) >= 0) { + if (target != source) { + gs = reorder(gs, source, target); + } + } + } + return gs; + } + + /** + * Find reph in sequence. + * @param gs input sequence + * @return index of reph or -1 if not found + */ + protected int findReph(GlyphSequence gs) { + return -1; + } + + /** + * Find reph target in sequence. + * @param gs input sequence + * @param source index of reph + * @return index of reph target or -1 + */ + protected int findRephTarget(GlyphSequence gs, int source) { + return -1; + } + + private GlyphSequence reorder(GlyphSequence gs, int source, int target) { + return GlyphSequence.reorder(gs, source, 1, target); + } + + /** {@inheritDoc} */ + @Override + public boolean position(GlyphSequence gs, String script, String language, int fontSize, AdvancedTypographicTable.UseSpec[] usa, int[] widths, int[][] adjustments, ScriptContextTester sct) { + boolean adjusted = super.position(gs, script, language, fontSize, usa, widths, adjustments, sct); + return adjusted; + } + + /** Abstract syllabizer. */ + protected abstract static class Syllabizer implements Comparable { + private String script; + private String language; + Syllabizer(String script, String language) { + this.script = script; + this.language = language; + } + /** + * Subdivide glyph sequence GS into syllabic segments each represented by a distinct + * output glyph sequence. + * @param gs input glyph sequence + * @return segmented syllabic glyph sequences + */ + abstract GlyphSequence[] syllabize(GlyphSequence gs); + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(script, language); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (o instanceof Syllabizer) { + Syllabizer s = (Syllabizer) o; + if (!s.script.equals(script)) { + return false; + } else { + return s.language.equals(language); + } + } else { + return false; + } + } + + /** {@inheritDoc} */ + @Override + public int compareTo(Syllabizer s) { + int d; + if ((d = script.compareTo(s.script)) == 0) { + d = language.compareTo(s.language); + } + return d; + } + + private static Map syllabizers = new HashMap(); + + static Syllabizer getSyllabizer(String script, String language, Class syllabizerClass) { + String sid = makeSyllabizerId(script, language); + Syllabizer s = syllabizers.get(sid); + if (s == null) { + if ((syllabizerClass == null) || ((s = makeSyllabizer(script, language, syllabizerClass)) == null)) { + log.warn("No syllabizer available for script '" + script + "', language '" + language + "', using default Indic syllabizer."); + s = new DefaultSyllabizer(script, language); + } + syllabizers.put(sid, s); + } + return s; + } + + static String makeSyllabizerId(String script, String language) { + return script + ":" + language; + } + + static Syllabizer makeSyllabizer(String script, String language, Class syllabizerClass) { + Syllabizer s; + try { + Constructor cf = syllabizerClass.getDeclaredConstructor(new Class[] { String.class, String.class }); + s = (Syllabizer) cf.newInstance(script, language); + } catch (NoSuchMethodException e) { + s = null; + } catch (InstantiationException e) { + s = null; + } catch (IllegalAccessException e) { + s = null; + } catch (InvocationTargetException e) { + s = null; + } + return s; + } + } + + /** Default syllabizer. */ + protected static class DefaultSyllabizer extends Syllabizer { + DefaultSyllabizer(String script, String language) { + super(script, language); + } + /** {@inheritDoc} */ + @Override + GlyphSequence[] syllabize(GlyphSequence gs) { + int[] ca = gs.getCharacterArray(false); + int nc = gs.getCharacterCount(); + if (nc == 0) { + return new GlyphSequence[] { gs }; + } else { + return segmentize(gs, segmentize(ca, nc)); + } + } + /** + * Construct array of segements from original character array (associated with original glyph sequence) + * @param ca input character sequence + * @param nc number of characters in sequence + * @return array of syllable segments + */ + protected Segment[] segmentize(int[] ca, int nc) { + Vector sv = new Vector(nc); + for (int s = 0, e = nc; s < e; ) { + int i; + if ((i = findStartOfSyllable(ca, s, e)) < e) { + if (s < i) { + // from s to i is non-syllable segment + sv.add(new Segment(s, i, Segment.OTHER)); + } + s = i; // move s to start of syllable + } else { + if (s < e) { + // from s to e is non-syllable segment + sv.add(new Segment(s, e, Segment.OTHER)); + } + s = e; // move s to end of input sequence + } + if ((i = findEndOfSyllable(ca, s, e)) > s) { + if (s < i) { + // from s to i is syllable segment + sv.add(new Segment(s, i, Segment.SYLLABLE)); + } + s = i; // move s to end of syllable + } else { + if (s < e) { + // from s to e is non-syllable segment + sv.add(new Segment(s, e, Segment.OTHER)); + } + s = e; // move s to end of input sequence + } + } + return sv.toArray(new Segment [ sv.size() ]); + } + /** + * Construct array of glyph sequences from original glyph sequence and segment array. + * @param gs original input glyph sequence + * @param sa segment array + * @return array of glyph sequences each belonging to an (ordered) segment in SA + */ + protected GlyphSequence[] segmentize(GlyphSequence gs, Segment[] sa) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray(false); + CharAssociation[] aa = gs.getAssociations(0, -1); + Vector nsv = new Vector(); + for (int i = 0, ns = sa.length; i < ns; i++) { + Segment s = sa [ i ]; + Vector ngv = new Vector(ng); + Vector nav = new Vector(ng); + for (int j = 0; j < ng; j++) { + CharAssociation ca = aa [ j ]; + if (ca.contained(s.getOffset(), s.getCount())) { + ngv.add(ga [ j ]); + nav.add(ca); + } + } + if (ngv.size() > 0) { + nsv.add(new GlyphSequence(gs, null, toIntArray(ngv), null, null, nav.toArray(new CharAssociation [ nav.size() ]), null)); + } + } + if (nsv.size() > 0) { + return nsv.toArray(new GlyphSequence [ nsv.size() ]); + } else { + return new GlyphSequence[] { gs }; + } + } + /** + * Find start of syllable in character array, starting at S, ending at E. + * @param ca character array + * @param s start index + * @param e end index + * @return index of start or E if no start found + */ + protected int findStartOfSyllable(int[] ca, int s, int e) { + return e; + } + /** + * Find end of syllable in character array, starting at S, ending at E. + * @param ca character array + * @param s start index + * @param e end index + * @return index of start or S if no end found + */ + protected int findEndOfSyllable(int[] ca, int s, int e) { + return s; + } + private static int[] toIntArray(Vector iv) { + int ni = iv.size(); + int[] ia = new int [ iv.size() ]; + for (int i = 0, n = ni; i < n; i++) { + ia [ i ] = (int) iv.get(i); + } + return ia; + } + } + + /** Syllabic segment. */ + protected static class Segment { + + static final int OTHER = 0; // other (non-syllable) characters + static final int SYLLABLE = 1; // (orthographic) syllable + + private int start; + private int end; + private int type; + + Segment(int start, int end, int type) { + this.start = start; + this.end = end; + this.type = type; + } + + int getStart() { + return start; + } + + int getEnd() { + return end; + } + + int getOffset() { + return start; + } + + int getCount() { + return end - start; + } + + int getType() { + return type; + } + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/ScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/ScriptProcessor.java new file mode 100644 index 00000000000..16188cee452 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/ScriptProcessor.java @@ -0,0 +1,300 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fontbox.ttf.advanced.AdvancedTypographicTable; +import org.apache.fontbox.ttf.advanced.GlyphDefinitionTable; +import org.apache.fontbox.ttf.advanced.GlyphPositioningTable; +import org.apache.fontbox.ttf.advanced.GlyphSubstitutionTable; +import org.apache.fontbox.ttf.advanced.AdvancedTypographicTable.LookupSpec; +import org.apache.fontbox.ttf.advanced.AdvancedTypographicTable.LookupTable; +import org.apache.fontbox.ttf.advanced.util.CharScript; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; +import org.apache.fontbox.ttf.advanced.util.ScriptContextTester; + +// CSOFF: LineLengthCheck + +/** + *

Abstract script processor base class for which an implementation of the substitution and positioning methods + * must be supplied.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public abstract class ScriptProcessor { + + private final String script; + + private final Map assembledLookups; + + private static Map processors = new HashMap(); + + /** + * Instantiate a script processor. + * @param script a script identifier + */ + protected ScriptProcessor(String script) { + if ((script == null) || (script.length() == 0)) { + throw new IllegalArgumentException("script must be non-empty string"); + } else { + this.script = script; + this.assembledLookups = new HashMap(); + } + } + + /** @return script identifier */ + public final String getScript() { + return script; + } + + /** + * Obtain script specific required substitution features. + * @return array of suppported substitution features or null + */ + public abstract String[] getSubstitutionFeatures(Object[][] features); + + /** + * Obtain script specific optional substitution features. + * @return array of suppported substitution features or null + */ + public String[] getOptionalSubstitutionFeatures() { + return new String[0]; + } + + /** + * Obtain script specific substitution context tester. + * @return substitution context tester or null + */ + public abstract ScriptContextTester getSubstitutionContextTester(); + + /** + * Perform substitution processing using a specific set of lookup tables. + * @param gsub the glyph substitution table that applies + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param lookups a mapping from lookup specifications to glyph subtables to use for substitution processing + * @return the substituted (output) glyph sequence + */ + public final GlyphSequence + substitute(GlyphSubstitutionTable gsub, GlyphSequence gs, String script, String language, Object[][] features, Map> lookups) { + return substitute(gs, script, language, assembleLookups(gsub, getSubstitutionFeatures(features), lookups), getSubstitutionContextTester()); + } + + /** + * Perform substitution processing using a specific set of ordered glyph table use specifications. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param usa an ordered array of glyph table use specs + * @param sct a script specific context tester (or null) + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute(GlyphSequence gs, String script, String language, AdvancedTypographicTable.UseSpec[] usa, ScriptContextTester sct) { + assert usa != null; + for (int i = 0, n = usa.length; i < n; i++) { + AdvancedTypographicTable.UseSpec us = usa [ i ]; + gs = us.substitute(gs, script, language, sct); + } + return gs; + } + + /** + * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of RTL segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base glyph. + * @param gdef the glyph definition table that applies + * @param gs an input glyph sequence + * @param unscaledWidths associated unscaled advance widths (also reordered) + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @return the reordered (output) glyph sequence + */ + public GlyphSequence + reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] unscaledWidths, int[][] gpa, String script, String language, Object[][] features) { + return gs; + } + + /** + * Obtain script specific required positioning features. + * @param features + * @return array of suppported positioning features or null + */ + public abstract String[] getPositioningFeatures(Object[][] features); + + /** + * Obtain script specific optional positioning features. + * @return array of suppported positioning features or null + */ + public String[] getOptionalPositioningFeatures() { + return new String[0]; + } + + /** + * Obtain script specific positioning context tester. + * @return positioning context tester or null + */ + public abstract ScriptContextTester getPositioningContextTester(); + + /** + * Perform positioning processing using a specific set of lookup tables. + * @param gpos the glyph positioning table that applies + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param lookups a mapping from lookup specifications to glyph subtables to use for positioning processing + * @param widths array of default advancements for each glyph + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @return true if some adjustment is not zero; otherwise, false + */ + public final boolean position(GlyphPositioningTable gpos, GlyphSequence gs, String script, String language, Object[][] features, int fontSize, Map> lookups, int[] widths, int[][] adjustments) { + return position(gs, script, language, fontSize, assembleLookups(gpos, getPositioningFeatures(features), lookups), widths, adjustments, getPositioningContextTester()); + } + + /** + * Perform positioning processing using a specific set of ordered glyph table use specifications. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param usa an ordered array of glyph table use specs + * @param widths array of default advancements for each glyph in font + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @param sct a script specific context tester (or null) + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position(GlyphSequence gs, String script, String language, int fontSize, AdvancedTypographicTable.UseSpec[] usa, int[] widths, int[][] adjustments, ScriptContextTester sct) { + assert usa != null; + boolean adjusted = false; + for (int i = 0, n = usa.length; i < n; i++) { + AdvancedTypographicTable.UseSpec us = usa [ i ]; + if (us.position(gs, script, language, fontSize, widths, adjustments, sct)) { + adjusted = true; + } + } + return adjusted; + } + + /** + * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups, + * where the order of the array is in accordance to the order of the applicable lookup list. + * @param table the governing glyph table + * @param features array of feature identifiers to apply + * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features + * @return ordered array of assembled lookup table use specifications + */ + public final AdvancedTypographicTable.UseSpec[] assembleLookups(AdvancedTypographicTable table, String[] features, Map> lookups) { + AssembledLookupsKey key = new AssembledLookupsKey(table, features, lookups); + AdvancedTypographicTable.UseSpec[] usa; + if ((usa = assembledLookupsGet(key)) != null) { + return usa; + } else { + return assembledLookupsPut(key, table.assembleLookups(features, lookups)); + } + } + + private AdvancedTypographicTable.UseSpec[] assembledLookupsGet(AssembledLookupsKey key) { + return (AdvancedTypographicTable.UseSpec[]) assembledLookups.get(key); + } + + private AdvancedTypographicTable.UseSpec[] assembledLookupsPut(AssembledLookupsKey key, AdvancedTypographicTable.UseSpec[] usa) { + assembledLookups.put(key, usa); + return usa; + } + + /** + * Obtain script processor instance associated with specified script. + * @param script a script identifier + * @return a script processor instance or null if none found + */ + public static synchronized ScriptProcessor getInstance(String script) { + ScriptProcessor sp = null; + assert processors != null; + if ((sp = processors.get(script)) == null) { + processors.put(script, sp = createProcessor(script)); + } + return sp; + } + + // [TBD] - rework to provide more configurable binding between script name and script processor constructor + private static ScriptProcessor createProcessor(String script) { + ScriptProcessor sp = null; + int sc = CharScript.scriptCodeFromTag(script); + if (sc == CharScript.SCRIPT_ARABIC) { + sp = new ArabicScriptProcessor(script); + } else if (CharScript.isIndicScript(sc)) { + sp = IndicScriptProcessor.makeProcessor(script); + } else { + sp = new DefaultScriptProcessor(script); + } + return sp; + } + + private static class AssembledLookupsKey { + + private final AdvancedTypographicTable table; + private final String[] features; + private final Map> lookups; + + AssembledLookupsKey(AdvancedTypographicTable table, String[] features, Map> lookups) { + this.table = table; + this.features = features; + this.lookups = lookups; + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + int hc = 0; + hc = 7 * hc + (hc ^ table.hashCode()); + hc = 11 * hc + (hc ^ Arrays.hashCode(features)); + hc = 17 * hc + (hc ^ lookups.hashCode()); + return hc; + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (o instanceof AssembledLookupsKey) { + AssembledLookupsKey k = (AssembledLookupsKey) o; + if (!table.equals(k.table)) { + return false; + } else if (!Arrays.equals(features, k.features)) { + return false; + } else { + return lookups.equals(k.lookups); + } + } else { + return false; + } + } + + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/TamilScriptProcessor.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/TamilScriptProcessor.java new file mode 100644 index 00000000000..42e224e87ca --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/scripts/TamilScriptProcessor.java @@ -0,0 +1,542 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.ttf.advanced.util.CharAssociation; +import org.apache.fontbox.ttf.advanced.util.GlyphSequence; + +// CSOFF: LineLengthCheck + +/** + *

The TamilScriptProcessor class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Tamil script.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class TamilScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(TamilScriptProcessor.class); + + TamilScriptProcessor(String script) { + super(script); + } + + @Override + protected Class getSyllabizerClass() { + return TamilSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = ng; i > 0; i--) { + int k = i - 1; + if (containsPreBaseMatra(gs, k)) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int lk = -1; + for (int i = (source < ng) ? source : ng; i > 0; i--) { + int k = i - 1; + if (containsConsonant(gs, k)) { + if (containsHalfConsonant(gs, k)) { + lk = k; + } else if (lk == -1) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isPreM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsConsonant(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isC(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant(GlyphSequence gs, int k) { + Boolean half = (Boolean) gs.getAssociation(k).getPredication("half"); + return (half != null) ? half.booleanValue() : false; + } + + @Override + protected int findReph(GlyphSequence gs) { + int ng = gs.getGlyphCount(); + int li = -1; + for (int i = 0; i < ng; i++) { + if (containsReph(gs, i)) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget(GlyphSequence gs, int source) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for (int i = 0; i < ng; i++) { + if ((i != source) && containsConsonant(gs, i)) { + if (!containsHalfConsonant(gs, i)) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for (int i = (c1 >= 0) ? c1 : 0; i < ng; i++) { + if (containsMatra(gs, i) && !containsPreBaseMatra(gs, i)) { + c2 = i + 1; + } else if (containsOtherMark(gs, i)) { + c2 = i; + break; + } + } + if (c2 >= 0) { + return c2; + } else if (c1 >= 0) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph(GlyphSequence gs, int k) { + Boolean rphf = (Boolean) gs.getAssociation(k).getPredication("rphf"); + return (rphf != null) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + if (isM(ca [ i ])) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark(GlyphSequence gs, int k) { + CharAssociation a = gs.getAssociation(k); + int[] ca = gs.getCharacterArray(false); + for (int i = a.getStart(), e = a.getEnd(); i < e; i++) { + switch (typeOf(ca [ i ])) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class TamilSyllabizer extends DefaultSyllabizer { + TamilSyllabizer(String script, String language) { + super(script, language); + } + @Override + // | C ... + protected int findStartOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + while (s < e) { + int c = ca [ s ]; + if (isC(c)) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable(int[] ca, int s, int e) { + if ((s < 0) || (s >= e)) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ((i = isDeadConsonant(ca, s, e)) > s) { + s = i; + nd++; + } + // consume zero or one live consonant + if ((i = isLiveConsonant(ca, s, e)) > s) { + s = i; + nl++; + } + return ((nd > 0) || (nl > 0)) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nh = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + 1 ]; + if (isN(c)) { + i++; + } + } + // H + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + i++; + nh++; + } else { + break; + } + } + } while (false); + return (nc > 0) && (nh > 0) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant(int[] ca, int s, int e) { + if (s < 0) { + return -1; + } else { + int c; + int i = 0; + int nc = 0; + int nv = 0; + int nx = 0; + do { + // C + if ((s + i) < e) { + c = ca [ s + i ]; + if (isC(c)) { + i++; + nc++; + } else if (isV(c)) { + i++; + nv++; + } else { + break; + } + } + // N? + if ((s + i) < e) { + c = ca [ s + i ]; + if (isN(c)) { + i++; + } + } + // X* + while ((s + i) < e) { + c = ca [ s + i ]; + if (isX(c)) { + i++; + nx++; + } else { + break; + } + } + } while (false); + // if no X but has H, then ignore C|I + if (nx == 0) { + if ((s + i) < e) { + c = ca [ s + i ]; + if (isH(c)) { + if (nc > 0) { + nc--; + } else if (nv > 0) { + nv--; + } + } + } + } + return ((nc > 0) || (nv > 0)) ? s + i : -1; + } + } + } + + // tamil character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_POST = 0x1000; // post-base + static final short C_WRAP = C_PRE | C_POST; // wrap (two part) vowel + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // tamil block range + static final int CCA_START = 0x0B80; // first code point mapped by cca + static final int CCA_END = 0x0C00; // last code point + 1 mapped by cca + // tamil character type lookups + static final short[] CCA = { + C_U, // 0x0B80 // + C_U, // 0x0B81 // + C_O, // 0x0B82 // ANUSVARA + C_O, // 0x0B83 // VISARGA + C_U, // 0x0B84 // + C_V, // 0x0B85 // A + C_V, // 0x0B86 // AA + C_V, // 0x0B87 // I + C_V, // 0x0B88 // II + C_V, // 0x0B89 // U + C_V, // 0x0B8A // UU + C_U, // 0x0B8B // + C_U, // 0x0B8C // + C_U, // 0x0B8D // + C_V, // 0x0B8E // E + C_V, // 0x0B8F // EE + C_V, // 0x0B90 // AI + C_U, // 0x0B91 // + C_V, // 0x0B92 // O + C_V, // 0x0B93 // OO + C_V, // 0x0B94 // AU + C_C, // 0x0B95 // KA + C_U, // 0x0B96 // + C_U, // 0x0B97 // + C_U, // 0x0B98 // + C_C, // 0x0B99 // NGA + C_C, // 0x0B9A // CA + C_U, // 0x0B9B // + C_C, // 0x0B9C // JA + C_U, // 0x0B9D // + C_C, // 0x0B9E // NYA + C_C, // 0x0B9F // TTA + C_U, // 0x0BA0 // + C_U, // 0x0BA1 // + C_U, // 0x0BA2 // + C_C, // 0x0BA3 // NNA + C_C, // 0x0BA4 // TA + C_U, // 0x0BA5 // + C_U, // 0x0BA6 // + C_U, // 0x0BA7 // + C_C, // 0x0BA8 // NA + C_C, // 0x0BA9 // NNNA + C_C, // 0x0BAA // PA + C_U, // 0x0BAB // + C_U, // 0x0BAC // + C_U, // 0x0BAD // + C_C, // 0x0BAE // MA + C_C, // 0x0BAF // YA + C_C | C_R, // 0x0BB0 // RA + C_C | C_R, // 0x0BB1 // RRA + C_C, // 0x0BB2 // LA + C_C, // 0x0BB3 // LLA + C_C, // 0x0BB4 // LLLA + C_C, // 0x0BB5 // VA + C_C, // 0x0BB6 // SHA + C_C, // 0x0BB7 // SSA + C_C, // 0x0BB8 // SA + C_C, // 0x0BB9 // HA + C_U, // 0x0BBA // + C_U, // 0x0BBB // + C_U, // 0x0BBC // + C_U, // 0x0BBD // + C_M, // 0x0BBE // AA + C_M, // 0x0BBF // I + C_M, // 0x0BC0 // II + C_M, // 0x0BC1 // U + C_M, // 0x0BC2 // UU + C_U, // 0x0BC3 // + C_U, // 0x0BC4 // + C_U, // 0x0BC5 // + C_M | C_PRE, // 0x0BC6 // E + C_M | C_PRE, // 0x0BC7 // EE + C_M | C_PRE, // 0x0BC8 // AI + C_U, // 0x0BC9 // + C_M | C_WRAP, // 0x0BCA // O + C_M | C_WRAP, // 0x0BCB // OO + C_M | C_WRAP, // 0x0BCC // AU + C_H, // 0x0BCD // VIRAMA (HALANT) + C_U, // 0x0BCE // + C_U, // 0x0BCF // + C_S, // 0x0BD0 // OM + C_U, // 0x0BD1 // + C_U, // 0x0BD2 // + C_U, // 0x0BD3 // + C_U, // 0x0BD4 // + C_U, // 0x0BD5 // + C_U, // 0x0BD6 // + C_M, // 0x0BD7 // AU LENGTH MARK + C_U, // 0x0BD8 // + C_U, // 0x0BD9 // + C_U, // 0x0BDA // + C_U, // 0x0BDB // + C_U, // 0x0BDC // + C_U, // 0x0BDD // + C_U, // 0x0BDE // + C_U, // 0x0BDF // + C_U, // 0x0BE0 // + C_U, // 0x0BE1 // + C_U, // 0x0BE2 // + C_U, // 0x0BE3 // + C_U, // 0x0BE4 // + C_U, // 0x0BE5 // + C_D, // 0x0BE6 // ZERO + C_D, // 0x0BE7 // ONE + C_D, // 0x0BE8 // TWO + C_D, // 0x0BE9 // THREE + C_D, // 0x0BEA // FOUR + C_D, // 0x0BEB // FIVE + C_D, // 0x0BEC // SIX + C_D, // 0x0BED // SEVEN + C_D, // 0x0BEE // EIGHT + C_D, // 0x0BEF // NINE + C_S, // 0x0BF0 // TEN + C_S, // 0x0BF1 // ONE HUNDRED + C_S, // 0x0BF2 // ONE THOUSAND + C_S, // 0x0BF3 // DAY SIGN (naal) + C_S, // 0x0BF4 // MONTH SIGN (maatham) + C_S, // 0x0BF5 // YEAR SIGN (varudam) + C_S, // 0x0BF6 // DEBIT SIGN (patru) + C_S, // 0x0BF7 // CREDIT SIGN (varavu) + C_S, // 0x0BF8 // AS ABOVE SIGN (merpadi) + C_S, // 0x0BF9 // RUPEE SIGN (rupai) + C_S, // 0x0BFA // NUMBER SIGN (enn) + C_U, // 0x0BFB // + C_U, // 0x0BFC // + C_U, // 0x0BFD // + C_U, // 0x0BFE // + C_U // 0x0BFF // + }; + static int typeOf(int c) { + if ((c >= CCA_START) && (c < CCA_END)) { + return CCA [ c - CCA_START ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf(c) == t; + } + static boolean hasFlag(int c, int f) { + if ((c >= CCA_START) && (c < CCA_END)) { + return (CCA [ c - CCA_START ] & f) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c, C_C); + } + static boolean isR(int c) { + return isType(c, C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c, C_V); + } + static boolean isN(int c) { + return c == 0x093C; + } + static boolean isH(int c) { + return c == 0x094D; + } + static boolean isM(int c) { + return isType(c, C_M); + } + static boolean isPreM(int c) { + return isType(c, C_M) && hasFlag(c, C_PRE); + } + static boolean isX(int c) { + switch (typeOf(c)) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c, C_R); + } + static boolean hasN(int c) { + return hasFlag(c, C_N); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/AdvancedChecker.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/AdvancedChecker.java new file mode 100644 index 00000000000..aaa604e228c --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/AdvancedChecker.java @@ -0,0 +1,199 @@ +package org.apache.fontbox.ttf.advanced.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.IntPredicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.fontbox.ttf.advanced.AdvancedTypographicTableFormatException; +import org.apache.fontbox.ttf.advanced.SubtableEntryHolder.SubtableEntry; + +public class AdvancedChecker { + private AdvancedChecker() { } + + /** + * Extracts the {@link SubtableEntry} with the given {@code index} from the {@code list} and checks + * that it is non-null and is of the {@code required} type. If not throws an {@link AdvancedTypographicTableFormatException}. + * @return the {@link SubtableEntry} cast to the {@code required} type + */ + @SuppressWarnings("unchecked") + public static T checkGet(List list, int index, Class required) { + SubtableEntry o = list.get(index); + if (o == null || o.getClass() != required) { + throw new AdvancedTypographicTableFormatException( + String.format(Locale.ROOT, "illegal entries, entry %d must be an %s, but is: %s", + index, required.getSimpleName(), (o != null) ? o.getClass().getSimpleName() : null)); + } + return (T) o; + } + + /** + * Checks that {@code entries} is exactly {@code size} and throws a + * {@link AdvancedTypographicTableFormatException} if not. + */ + public static void checkSize(List entries, int size) { + if (entries == null || entries.size() != size) { + throw new AdvancedTypographicTableFormatException( + String.format(Locale.ROOT, "illegal entries, must be non-null and contain exactly %d entries", size)); + } + } + + /** + * Checks that {@code gid} is in the range {@code 0..65535} inclusive or throws + * an {@link AdvancedTypographicTableFormatException} with the {@code exMsg} supplied. + */ + public static void checkGidRange(int gid, Supplier exMsg) { + if ((gid < 0) || (gid > 65535)) { + throw new AdvancedTypographicTableFormatException(exMsg.get()); + } + } + + + /** + * Checks that {@code gid} complies with the {@code predicate} or throws an + * {@link AdvancedTypographicTableFormatException} with the {@code exMsg} supplied. + */ + public static void checkCondition(int gid, IntPredicate predicate, Supplier exMsg) { + if (!predicate.test(gid)) { + throw new AdvancedTypographicTableFormatException(exMsg.get()); + } + } + + /** + * Returns an {@link IntPredicate} that tests that the subject is not greater than {@code max}. + */ + public static IntPredicate notGt(int max) { + return gid -> !(gid > max); + } + + /** + * Returns an {@link IntPredicate} that tests that the subject is not less than {@code min}. + */ + public static IntPredicate notLt(int min) { + return gid -> !(gid < min); + } + + /** + * If {@code entries} is non-null then iterates over it applying {@code transform} + * to each item and returning the transformed items in a new list. + * If {@code entries} is null, returns a mutable empty list. + */ + public static List arrayMap(T[] entries, Function transform) { + if (entries != null) { + List result = new ArrayList<>(entries.length); + for (int i = 0; i < entries.length; i++) { + result.add(transform.apply(entries[i])); + } + return result; + } + return new ArrayList<>(); + } + + /** + * If {@code entries} is non-null then iterates over it applying {@code transform} + * to each item and returning the transformed items in a new list. + * If {@code entries} is null, returns a mutable empty list. + */ + public static List arrayMap(int[] entries, IntFunction transform) { + if (entries != null) { + List result = new ArrayList<>(entries.length); + for (int i = 0; i < entries.length; i++) { + result.add(transform.apply(entries[i])); + } + return result; + } + return new ArrayList<>(); + } + + /** + * Calls {@code transform} in the range {@code 0..size} exclusive and + * add the result to a list which is returned. If {@code size} is zero + * returns an empty mutable list. + */ + public static List rangeMap(int size, IntFunction transform) { + List result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + result.add(transform.apply(i)); + } + return result; + } + + /** + * Returns true if all {@code entries} can be assigned to {@code type}. + */ + public static boolean allOfType(List entries, Class type) { + for (SubtableEntry entry : entries) { + if (!type.isAssignableFrom(entry.getClass())) { + return false; + } + } + return true; + } + + /** + * Returns a mutable list with the one supplied {@code item}. + */ + public static List mutableSingleton(T item) { + List list = new ArrayList<>(1); + list.add(item); + return list; + } + + /** + * Returns a comma separated string representing the simple class + * names of all items in {@code objects}. + */ + public static String toClassString(List objects, String prefix, String suffix) { + return objects + .stream() + .map(Object::getClass) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ", prefix, suffix)); + } + + /** + * If {@code items} is non-null then transforms every item using {@code transform} and + * if the result is non-null it is passed to {@code consume}. + * @param array type + * @param result type of transform + * @param items a possibly null list of items + * @param transform a transform function called for each item in items + * @param consume a consumer called for each non-null result from transform + */ + public static void transformConsume(T[] items, Function transform, Consumer consume) { + if (items != null) { + for (T item : items) { + U transformed = transform.apply(item); + if (transformed != null) { + consume.accept(transformed); + } + } + } + } + + + /** + * If {@code items} is non-null then transforms every item using {@code transform} and + * if the result is non-null it is passed to {@code consume}. + * @param list item type + * @param result type of transform + * @param items a possibly null list of items + * @param transform a transform function called for each item in items + * @param consume a consumer called for each non-null result from transform + */ + public static void transformConsume(List items, Function transform, Consumer consume) { + if (items != null) { + for (T item : items) { + U transformed = transform.apply(item); + if (transformed != null) { + consume.accept(transformed); + } + } + } + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharAssociation.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharAssociation.java new file mode 100644 index 00000000000..fb3c718bf50 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharAssociation.java @@ -0,0 +1,489 @@ +/* + * 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.advanced.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * A structure class encapsulating an interval of characters expressed as an offset and count of + * Unicode scalar values (in an IntBuffer). A CharAssociation is used to maintain a + * backpointer from a glyph to one or more character intervals from which the glyph was derived. + * + * Each glyph in a glyph sequence is associated with a single CharAssociation instance. + * + * A CharAssociation instance is additionally (and optionally) used to record + * predication information about the glyph, such as whether the glyph was produced by the + * application of a specific substitution table or whether its position was adjusted by a specific + * poisitioning table. + * + * @author Glenn Adams + */ +public class CharAssociation implements Cloneable { + + // instance state + private final int offset; + private final int count; + private final int[] subIntervals; + private Map predications; + + // class state + private static volatile Map predicationMergers; + + interface PredicationMerger { + Object merge(String key, Object v1, Object v2); + } + + /** + * Instantiate a character association. + * @param offset into array of Unicode scalar values (in associated IntBuffer) + * @param count of Unicode scalar values (in associated IntBuffer) + * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even + * members of array are sub-interval starts, and odd members are sub-interval + * ends (exclusive) + */ + public CharAssociation(int offset, int count, int[] subIntervals) { + this.offset = offset; + this.count = count; + this.subIntervals = ((subIntervals != null) && (subIntervals.length > 2)) ? subIntervals : null; + } + + /** + * Instantiate a non-disjoint character association. + * @param offset into array of UTF-16 code elements (in associated CharSequence) + * @param count of UTF-16 character code elements (in associated CharSequence) + */ + public CharAssociation(int offset, int count) { + this (offset, count, null); + } + + /** + * Instantiate a non-disjoint character association. + * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even + * members of array are sub-interval starts, and odd members are sub-interval + * ends (exclusive) + */ + public CharAssociation(int[] subIntervals) { + this (getSubIntervalsStart(subIntervals), getSubIntervalsLength(subIntervals), subIntervals); + } + + /** @return offset (start of association interval) */ + public int getOffset() { + return offset; + } + + /** @return count (number of characer codes in association) */ + public int getCount() { + return count; + } + + /** @return start of association interval */ + public int getStart() { + return getOffset(); + } + + /** @return end of association interval */ + public int getEnd() { + return getOffset() + getCount(); + } + + /** @return true if association is disjoint */ + public boolean isDisjoint() { + return subIntervals != null; + } + + /** @return subintervals of disjoint association */ + public int[] getSubIntervals() { + return subIntervals; + } + + /** @return count of subintervals of disjoint association */ + public int getSubIntervalCount() { + return (subIntervals != null) ? (subIntervals.length / 2) : 0; + } + + /** + * @param offset of interval in sequence + * @param count length of interval + * @return true if this association is contained within [offset,offset+count) + */ + public boolean contained(int offset, int count) { + int s = offset; + int e = offset + count; + if (!isDisjoint()) { + int s0 = getStart(); + int e0 = getEnd(); + return (s0 >= s) && (e0 <= e); + } else { + int ns = getSubIntervalCount(); + for (int i = 0; i < ns; i++) { + int s0 = subIntervals [ 2 * i + 0 ]; + int e0 = subIntervals [ 2 * i + 1 ]; + if ((s0 >= s) && (e0 <= e)) { + return true; + } + } + return false; + } + } + + /** + * Set predication <KEY,VALUE>. + * @param key predication key + * @param value predication value + */ + public void setPredication(String key, Object value) { + if (predications == null) { + predications = new HashMap(); + } + if (predications != null) { + predications.put(key, value); + } + } + + /** + * Get predication KEY. + * @param key predication key + * @return predication KEY at OFFSET or null if none exists + */ + public Object getPredication(String key) { + if (predications != null) { + return predications.get(key); + } else { + return null; + } + } + + /** + * Merge predication <KEY,VALUE>. + * @param key predication key + * @param value predication value + */ + public void mergePredication(String key, Object value) { + if (predications == null) { + predications = new HashMap(); + } + if (predications != null) { + if (predications.containsKey(key)) { + Object v1 = predications.get(key); + Object v2 = value; + predications.put(key, mergePredicationValues(key, v1, v2)); + } else { + predications.put(key, value); + } + } + } + + /** + * Merge predication values V1 and V2 on KEY. Uses registered PredicationMerger + * if one exists, otherwise uses V2 if non-null, otherwise uses V1. + * @param key predication key + * @param v1 first (original) predication value + * @param v2 second (to be merged) predication value + * @return merged value + */ + public static Object mergePredicationValues(String key, Object v1, Object v2) { + PredicationMerger pm = getPredicationMerger(key); + if (pm != null) { + return pm.merge(key, v1, v2); + } else if (v2 != null) { + return v2; + } else { + return v1; + } + } + + /** + * Merge predications from another CA. + * @param ca from which to merge + */ + public void mergePredications(CharAssociation ca) { + if (ca.predications != null) { + for (Map.Entry e : ca.predications.entrySet()) { + mergePredication(e.getKey(), e.getValue()); + } + } + } + + /** {@inheritDoc} */ + @Override + public Object clone() { + try { + CharAssociation ca = (CharAssociation) super.clone(); + if (predications != null) { + ca.predications = new HashMap(predications); + } + return ca; + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** + * Register predication merger PM for KEY. + * @param key for predication merger + * @param pm predication merger + */ + public static void setPredicationMerger(String key, PredicationMerger pm) { + if (predicationMergers == null) { + predicationMergers = new HashMap(); + } + if (predicationMergers != null) { + predicationMergers.put(key, pm); + } + } + + /** + * Obtain predication merger for KEY. + * @param key for predication merger + * @return predication merger or null if none exists + */ + public static PredicationMerger getPredicationMerger(String key) { + if (predicationMergers != null) { + return predicationMergers.get(key); + } else { + return null; + } + } + + /** + * Replicate association to form repeat new associations. + * @param a association to replicate + * @param repeat count + * @return array of replicated associations + */ + public static CharAssociation[] replicate(CharAssociation a, int repeat) { + CharAssociation[] aa = new CharAssociation [ repeat ]; + for (int i = 0, n = aa.length; i < n; i++) { + aa [ i ] = (CharAssociation) a.clone(); + } + return aa; + } + + /** + * Join (merge) multiple associations into a single, potentially disjoint + * association. + * @param aa array of associations to join + * @return (possibly disjoint) association containing joined associations + */ + public static CharAssociation join(CharAssociation[] aa) { + CharAssociation ca; + // extract sorted intervals + int[] ia = extractIntervals(aa); + if ((ia == null) || (ia.length == 0)) { + ca = new CharAssociation(0, 0); + } else if (ia.length == 2) { + int s = ia[0]; + int e = ia[1]; + ca = new CharAssociation(s, e - s); + } else { + ca = new CharAssociation(mergeIntervals(ia)); + } + return mergePredicates(ca, aa); + } + + private static CharAssociation mergePredicates(CharAssociation ca, CharAssociation[] aa) { + for (CharAssociation a : aa) { + ca.mergePredications(a); + } + return ca; + } + + private static int getSubIntervalsStart(int[] ia) { + int us = Integer.MAX_VALUE; + int ue = Integer.MIN_VALUE; + if (ia != null) { + for (int i = 0, n = ia.length; i < n; i += 2) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + if (s < us) { + us = s; + } + if (e > ue) { + ue = e; + } + } + if (ue < 0) { + ue = 0; + } + if (us > ue) { + us = ue; + } + } + return us; + } + + private static int getSubIntervalsLength(int[] ia) { + int us = Integer.MAX_VALUE; + int ue = Integer.MIN_VALUE; + if (ia != null) { + for (int i = 0, n = ia.length; i < n; i += 2) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + if (s < us) { + us = s; + } + if (e > ue) { + ue = e; + } + } + if (ue < 0) { + ue = 0; + } + if (us > ue) { + us = ue; + } + } + return ue - us; + } + + /** + * Extract sorted sub-intervals. + */ + private static int[] extractIntervals(CharAssociation[] aa) { + int ni = 0; + for (int i = 0, n = aa.length; i < n; i++) { + CharAssociation a = aa [ i ]; + if (a.isDisjoint()) { + ni += a.getSubIntervalCount(); + } else { + ni += 1; + } + } + int[] sa = new int [ ni ]; + int[] ea = new int [ ni ]; + for (int i = 0, k = 0; i < aa.length; i++) { + CharAssociation a = aa [ i ]; + if (a.isDisjoint()) { + int[] da = a.getSubIntervals(); + for (int j = 0; j < da.length; j += 2) { + sa [ k ] = da [ j + 0 ]; + ea [ k ] = da [ j + 1 ]; + k++; + } + } else { + sa [ k ] = a.getStart(); + ea [ k ] = a.getEnd(); + k++; + } + } + return sortIntervals(sa, ea); + } + + private static final int[] SORT_INCREMENTS_16 + = { 1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1 }; + + private static final int[] SORT_INCREMENTS_03 + = { 7, 3, 1 }; + + /** + * Sort sub-intervals using modified Shell Sort. + */ + private static int[] sortIntervals(int[] sa, int[] ea) { + assert sa != null; + assert ea != null; + assert sa.length == ea.length; + int ni = sa.length; + int[] incr = (ni < 21) ? SORT_INCREMENTS_03 : SORT_INCREMENTS_16; + for (int k = 0; k < incr.length; k++) { + for (int h = incr [ k ], i = h, n = ni, j; i < n; i++) { + int s1 = sa [ i ]; + int e1 = ea [ i ]; + for (j = i; j >= h; j -= h) { + int s2 = sa [ j - h ]; + int e2 = ea [ j - h ]; + if (s2 > s1) { + sa [ j ] = s2; + ea [ j ] = e2; + } else if ((s2 == s1) && (e2 > e1)) { + sa [ j ] = s2; + ea [ j ] = e2; + } else { + break; + } + } + sa [ j ] = s1; + ea [ j ] = e1; + } + } + int[] ia = new int [ ni * 2 ]; + for (int i = 0; i < ni; i++) { + ia [ (i * 2) + 0 ] = sa [ i ]; + ia [ (i * 2) + 1 ] = ea [ i ]; + } + return ia; + } + + /** + * Merge overlapping and abutting sub-intervals. + */ + private static int[] mergeIntervals(int[] ia) { + int ni = ia.length; + int i; + int n; + int nm; + int is; + int ie; + // count merged sub-intervals + for (i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + if ((ie < 0) || (s > ie)) { + is = s; + ie = e; + nm++; + } else if (s >= is) { + if (e > ie) { + ie = e; + } + } + } + int[] mi = new int [ nm * 2 ]; + // populate merged sub-intervals + for (i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + int k = nm * 2; + if ((ie < 0) || (s > ie)) { + is = s; + ie = e; + mi [ k + 0 ] = is; + mi [ k + 1 ] = ie; + nm++; + } else if (s >= is) { + if (e > ie) { + ie = e; + } + mi [ k - 1 ] = ie; + } + } + return mi; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('['); + sb.append(offset); + sb.append(','); + sb.append(count); + sb.append(']'); + return sb.toString(); + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharMirror.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharMirror.java new file mode 100644 index 00000000000..5d6f6519390 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharMirror.java @@ -0,0 +1,729 @@ +/* + * 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.advanced.util; + +import java.util.Arrays; + +/** + *

Mirror related utilities.

+ * + * @author Glenn Adams + */ +public final class CharMirror { + + private CharMirror() { + } + + /** + * Mirror characters that are designated as having the bidi mirrorred property. + * @param s a string whose characters are to be mirrored + * @return the resulting string + */ + public static String mirror(String s) { + StringBuffer sb = new StringBuffer(s); + for (int i = 0, n = sb.length(); i < n; ++i) { + sb.setCharAt(i, (char) mirror(sb.charAt(i))); + } + return sb.toString(); + } + + /** + * Determine if string has a mirrorable character. + * @param s a string whose characters are to be tested for mirrorability + * @return true if some character can be mirrored + */ + public static boolean hasMirrorable(String s) { + for (int i = 0, n = s.length(); i < n; ++i) { + char c = s.charAt(i); + if (Arrays.binarySearch(mirroredCharacters, c) >= 0) { + return true; + } + } + return false; + } + + private static int[] mirroredCharacters = { + 0x0028, + 0x0029, + 0x003C, + 0x003E, + 0x005B, + 0x005D, + 0x007B, + 0x007D, + 0x00AB, + 0x00BB, + 0x0F3A, + 0x0F3B, + 0x0F3C, + 0x0F3D, + 0x169B, + 0x169C, + 0x2039, + 0x203A, + 0x2045, + 0x2046, + 0x207D, + 0x207E, + 0x208D, + 0x208E, + 0x2208, + 0x2209, + 0x220A, + 0x220B, + 0x220C, + 0x220D, + 0x2215, + 0x223C, + 0x223D, + 0x2243, + 0x2252, + 0x2253, + 0x2254, + 0x2255, + 0x2264, + 0x2265, + 0x2266, + 0x2267, + 0x2268, + 0x2269, + 0x226A, + 0x226B, + 0x226E, + 0x226F, + 0x2270, + 0x2271, + 0x2272, + 0x2273, + 0x2274, + 0x2275, + 0x2276, + 0x2277, + 0x2278, + 0x2279, + 0x227A, + 0x227B, + 0x227C, + 0x227D, + 0x227E, + 0x227F, + 0x2280, + 0x2281, + 0x2282, + 0x2283, + 0x2284, + 0x2285, + 0x2286, + 0x2287, + 0x2288, + 0x2289, + 0x228A, + 0x228B, + 0x228F, + 0x2290, + 0x2291, + 0x2292, + 0x2298, + 0x22A2, + 0x22A3, + 0x22A6, + 0x22A8, + 0x22A9, + 0x22AB, + 0x22B0, + 0x22B1, + 0x22B2, + 0x22B3, + 0x22B4, + 0x22B5, + 0x22B6, + 0x22B7, + 0x22C9, + 0x22CA, + 0x22CB, + 0x22CC, + 0x22CD, + 0x22D0, + 0x22D1, + 0x22D6, + 0x22D7, + 0x22D8, + 0x22D9, + 0x22DA, + 0x22DB, + 0x22DC, + 0x22DD, + 0x22DE, + 0x22DF, + 0x22E0, + 0x22E1, + 0x22E2, + 0x22E3, + 0x22E4, + 0x22E5, + 0x22E6, + 0x22E7, + 0x22E8, + 0x22E9, + 0x22EA, + 0x22EB, + 0x22EC, + 0x22ED, + 0x22F0, + 0x22F1, + 0x22F2, + 0x22F3, + 0x22F4, + 0x22F6, + 0x22F7, + 0x22FA, + 0x22FB, + 0x22FC, + 0x22FD, + 0x22FE, + 0x2308, + 0x2309, + 0x230A, + 0x230B, + 0x2329, + 0x232A, + 0x2768, + 0x2769, + 0x276A, + 0x276B, + 0x276C, + 0x276D, + 0x276E, + 0x276F, + 0x2770, + 0x2771, + 0x2772, + 0x2773, + 0x2774, + 0x2775, + 0x27C3, + 0x27C4, + 0x27C5, + 0x27C6, + 0x27C8, + 0x27C9, + 0x27D5, + 0x27D6, + 0x27DD, + 0x27DE, + 0x27E2, + 0x27E3, + 0x27E4, + 0x27E5, + 0x27E6, + 0x27E7, + 0x27E8, + 0x27E9, + 0x27EA, + 0x27EB, + 0x27EC, + 0x27ED, + 0x27EE, + 0x27EF, + 0x2983, + 0x2984, + 0x2985, + 0x2986, + 0x2987, + 0x2988, + 0x2989, + 0x298A, + 0x298B, + 0x298C, + 0x298D, + 0x298E, + 0x298F, + 0x2990, + 0x2991, + 0x2992, + 0x2993, + 0x2994, + 0x2995, + 0x2996, + 0x2997, + 0x2998, + 0x29B8, + 0x29C0, + 0x29C1, + 0x29C4, + 0x29C5, + 0x29CF, + 0x29D0, + 0x29D1, + 0x29D2, + 0x29D4, + 0x29D5, + 0x29D8, + 0x29D9, + 0x29DA, + 0x29DB, + 0x29F5, + 0x29F8, + 0x29F9, + 0x29FC, + 0x29FD, + 0x2A2B, + 0x2A2C, + 0x2A2D, + 0x2A2E, + 0x2A34, + 0x2A35, + 0x2A3C, + 0x2A3D, + 0x2A64, + 0x2A65, + 0x2A79, + 0x2A7A, + 0x2A7D, + 0x2A7E, + 0x2A7F, + 0x2A80, + 0x2A81, + 0x2A82, + 0x2A83, + 0x2A84, + 0x2A8B, + 0x2A8C, + 0x2A91, + 0x2A92, + 0x2A93, + 0x2A94, + 0x2A95, + 0x2A96, + 0x2A97, + 0x2A98, + 0x2A99, + 0x2A9A, + 0x2A9B, + 0x2A9C, + 0x2AA1, + 0x2AA2, + 0x2AA6, + 0x2AA7, + 0x2AA8, + 0x2AA9, + 0x2AAA, + 0x2AAB, + 0x2AAC, + 0x2AAD, + 0x2AAF, + 0x2AB0, + 0x2AB3, + 0x2AB4, + 0x2AC3, + 0x2AC4, + 0x2AC5, + 0x2AC6, + 0x2ACD, + 0x2ACE, + 0x2ACF, + 0x2AD0, + 0x2AD1, + 0x2AD2, + 0x2AD3, + 0x2AD4, + 0x2AD5, + 0x2AD6, + 0x2ADE, + 0x2AE3, + 0x2E02, + 0x2E03, + 0x2E04, + 0x2E05, + 0x2E09, + 0x2E0A, + 0x2E0C, + 0x2E0D, + 0x2E1C, + 0x2E1D, + 0x2E20, + 0x2E21, + 0x2E22, + 0x2E23, + 0x2E24, + 0x2E25, + 0x2E26, + 0x300E, + 0x300F, + 0x3010, + 0x3011, + 0x3014, + 0x3015, + 0x3016, + 0x3017, + 0x3018, + 0x3019, + 0x301A, + 0x301B, + 0xFE59, + 0xFE5A, + 0xFF3B, + 0xFF3D, + 0xFF5B, + 0xFF5D, + 0xFF5F, + 0xFF60, + 0xFF62, + 0xFF63 + }; + + private static int[] mirroredCharactersMapping = { + 0x0029, + 0x0028, + 0x003E, + 0x003C, + 0x005D, + 0x005B, + 0x007D, + 0x007B, + 0x00BB, + 0x00AB, + 0x0F3B, + 0x0F3A, + 0x0F3D, + 0x0F3C, + 0x169C, + 0x169B, + 0x203A, + 0x2039, + 0x2046, + 0x2045, + 0x207E, + 0x207D, + 0x208E, + 0x208D, + 0x220B, + 0x220C, + 0x220D, + 0x2208, + 0x2209, + 0x220A, + 0x29F5, + 0x223D, + 0x223C, + 0x22CD, + 0x2253, + 0x2252, + 0x2255, + 0x2254, + 0x2265, + 0x2264, + 0x2267, + 0x2266, + 0x2269, + 0x2268, + 0x226B, + 0x226A, + 0x226F, + 0x226E, + 0x2271, + 0x2270, + 0x2273, + 0x2272, + 0x2275, + 0x2274, + 0x2277, + 0x2276, + 0x2279, + 0x2278, + 0x227B, + 0x227A, + 0x227D, + 0x227C, + 0x227F, + 0x227E, + 0x2281, + 0x2280, + 0x2283, + 0x2282, + 0x2285, + 0x2284, + 0x2287, + 0x2286, + 0x2289, + 0x2288, + 0x228B, + 0x228A, + 0x2290, + 0x228F, + 0x2292, + 0x2291, + 0x29B8, + 0x22A3, + 0x22A2, + 0x2ADE, + 0x2AE4, + 0x2AE3, + 0x2AE5, + 0x22B1, + 0x22B0, + 0x22B3, + 0x22B2, + 0x22B5, + 0x22B4, + 0x22B7, + 0x22B6, + 0x22CA, + 0x22C9, + 0x22CC, + 0x22CB, + 0x2243, + 0x22D1, + 0x22D0, + 0x22D7, + 0x22D6, + 0x22D9, + 0x22D8, + 0x22DB, + 0x22DA, + 0x22DD, + 0x22DC, + 0x22DF, + 0x22DE, + 0x22E1, + 0x22E0, + 0x22E3, + 0x22E2, + 0x22E5, + 0x22E4, + 0x22E7, + 0x22E6, + 0x22E9, + 0x22E8, + 0x22EB, + 0x22EA, + 0x22ED, + 0x22EC, + 0x22F1, + 0x22F0, + 0x22FA, + 0x22FB, + 0x22FC, + 0x22FD, + 0x22FE, + 0x22F2, + 0x22F3, + 0x22F4, + 0x22F6, + 0x22F7, + 0x2309, + 0x2308, + 0x230B, + 0x230A, + 0x232A, + 0x2329, + 0x2769, + 0x2768, + 0x276B, + 0x276A, + 0x276D, + 0x276C, + 0x276F, + 0x276E, + 0x2771, + 0x2770, + 0x2773, + 0x2772, + 0x2775, + 0x2774, + 0x27C4, + 0x27C3, + 0x27C6, + 0x27C5, + 0x27C9, + 0x27C8, + 0x27D6, + 0x27D5, + 0x27DE, + 0x27DD, + 0x27E3, + 0x27E2, + 0x27E5, + 0x27E4, + 0x27E7, + 0x27E6, + 0x27E9, + 0x27E8, + 0x27EB, + 0x27EA, + 0x27ED, + 0x27EC, + 0x27EF, + 0x27EE, + 0x2984, + 0x2983, + 0x2986, + 0x2985, + 0x2988, + 0x2987, + 0x298A, + 0x2989, + 0x298C, + 0x298B, + 0x2990, + 0x298F, + 0x298E, + 0x298D, + 0x2992, + 0x2991, + 0x2994, + 0x2993, + 0x2996, + 0x2995, + 0x2998, + 0x2997, + 0x2298, + 0x29C1, + 0x29C0, + 0x29C5, + 0x29C4, + 0x29D0, + 0x29CF, + 0x29D2, + 0x29D1, + 0x29D5, + 0x29D4, + 0x29D9, + 0x29D8, + 0x29DB, + 0x29DA, + 0x2215, + 0x29F9, + 0x29F8, + 0x29FD, + 0x29FC, + 0x2A2C, + 0x2A2B, + 0x2A2E, + 0x2A2D, + 0x2A35, + 0x2A34, + 0x2A3D, + 0x2A3C, + 0x2A65, + 0x2A64, + 0x2A7A, + 0x2A79, + 0x2A7E, + 0x2A7D, + 0x2A80, + 0x2A7F, + 0x2A82, + 0x2A81, + 0x2A84, + 0x2A83, + 0x2A8C, + 0x2A8B, + 0x2A92, + 0x2A91, + 0x2A94, + 0x2A93, + 0x2A96, + 0x2A95, + 0x2A98, + 0x2A97, + 0x2A9A, + 0x2A99, + 0x2A9C, + 0x2A9B, + 0x2AA2, + 0x2AA1, + 0x2AA7, + 0x2AA6, + 0x2AA9, + 0x2AA8, + 0x2AAB, + 0x2AAA, + 0x2AAD, + 0x2AAC, + 0x2AB0, + 0x2AAF, + 0x2AB4, + 0x2AB3, + 0x2AC4, + 0x2AC3, + 0x2AC6, + 0x2AC5, + 0x2ACE, + 0x2ACD, + 0x2AD0, + 0x2ACF, + 0x2AD2, + 0x2AD1, + 0x2AD4, + 0x2AD3, + 0x2AD6, + 0x2AD5, + 0x22A6, + 0x22A9, + 0x2E03, + 0x2E02, + 0x2E05, + 0x2E04, + 0x2E0A, + 0x2E09, + 0x2E0D, + 0x2E0C, + 0x2E1D, + 0x2E1C, + 0x2E21, + 0x2E20, + 0x2E23, + 0x2E22, + 0x2E25, + 0x2E24, + 0x2E27, + 0x300F, + 0x300E, + 0x3011, + 0x3010, + 0x3015, + 0x3014, + 0x3017, + 0x3016, + 0x3019, + 0x3018, + 0x301B, + 0x301A, + 0xFE5A, + 0xFE59, + 0xFF3D, + 0xFF3B, + 0xFF5D, + 0xFF5B, + 0xFF60, + 0xFF5F, + 0xFF63, + 0xFF62 + }; + + private static int mirror(int c) { + int i = Arrays.binarySearch(mirroredCharacters, c); + if (i < 0) { + return c; + } else { + return mirroredCharactersMapping [ i ]; + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharNormalize.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharNormalize.java new file mode 100644 index 00000000000..30e1833e90f --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharNormalize.java @@ -0,0 +1,104 @@ +/* + * 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.advanced.util; + +import java.util.Arrays; + +/** + *

Normalization related utilities. N.B. This implementation is an experimental + * shortcut, the full version of which would require either using ICU4J or an extraction + * of its normalization function, either being a significant undertaking. At present + * we handle only specialized decomposition of Indic two part matras.

+ * + * @author Glenn Adams + */ +public final class CharNormalize { + + // CSOFF: LineLength + + private CharNormalize() { + } + + private static final int[] DECOMPOSABLES = { + // bengali + 0x09CB, + 0x09CC, + // oriya + 0x0B4B, + 0x0B4C, + // tamil + 0x0BCA, + 0x0BCB, + 0x0BCC, + // malayalam + 0x0D4A, + 0x0D4B, + 0x0D4C, + // sinhala + 0x0DDA, + 0x0DDC, + 0x0DDD, + 0x0DDE, + }; + + private static final int[][] DECOMPOSITIONS = { + // bengali + { 0x09C7, 0x09BE }, // 0x09CB + { 0x09C7, 0x09D7 }, // 0x09CC + // oriya + { 0x0B47, 0x0B4E }, // 0x0B4B + { 0x0B47, 0x0B57 }, // 0x0B4C + // tamil + { 0x0BC6, 0x0BBE }, // 0x0BCA + { 0x0BC7, 0x0BBE }, // 0x0BCB + { 0x0BC6, 0x0BD7 }, // 0x0BCC + // malayalam + { 0x0D46, 0x0D3E }, // 0x0D4A + { 0x0D47, 0x0D3E }, // 0x0D4B + { 0x0D46, 0x0D57 }, // 0x0D4C + // sinhala + { 0x0DD9, 0x0DCA }, // 0x0DDA + { 0x0DD9, 0x0DCF }, // 0x0DDC + { 0x0DD9, 0x0DCF, 0x0DCA }, // 0x0DDD + { 0x0DD9, 0x0DDF }, // 0x0DDE + }; + + private static final int MAX_DECOMPOSITION_LENGTH = 3; + + public static boolean isDecomposable(int c) { + return Arrays.binarySearch(DECOMPOSABLES, c) >= 0; + } + + public static int maximumDecompositionLength() { + return MAX_DECOMPOSITION_LENGTH; + } + + public static int[] decompose(int c, int[] da) { + int di = Arrays.binarySearch(DECOMPOSABLES, c); + if (di >= 0) { + return DECOMPOSITIONS[di]; + } else if ((da != null) && (da.length > 1)) { + da[0] = c; + da[1] = 0; + return da; + } else { + return new int[] { c }; + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharScript.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharScript.java new file mode 100644 index 00000000000..93b06605a7c --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/CharScript.java @@ -0,0 +1,943 @@ +/* + * 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.advanced.util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + *

Script related utilities.

+ * + * @author Glenn Adams + */ +public final class CharScript { + + // CSOFF: LineLength + + // + // The following script codes are based on ISO 15924. Codes less than 1000 are + // official assignments from 15924; those equal to or greater than 1000 are FOP + // implementation specific. + // + /** hebrew script constant */ + public static final int SCRIPT_HEBREW = 125; // 'hebr' + /** mongolian script constant */ + public static final int SCRIPT_MONGOLIAN = 145; // 'mong' + /** arabic script constant */ + public static final int SCRIPT_ARABIC = 160; // 'arab' + /** greek script constant */ + public static final int SCRIPT_GREEK = 200; // 'grek' + /** latin script constant */ + public static final int SCRIPT_LATIN = 215; // 'latn' + /** cyrillic script constant */ + public static final int SCRIPT_CYRILLIC = 220; // 'cyrl' + /** georgian script constant */ + public static final int SCRIPT_GEORGIAN = 240; // 'geor' + /** bopomofo script constant */ + public static final int SCRIPT_BOPOMOFO = 285; // 'bopo' + /** hangul script constant */ + public static final int SCRIPT_HANGUL = 286; // 'hang' + /** gurmukhi script constant */ + public static final int SCRIPT_GURMUKHI = 310; // 'guru' + /** gurmukhi 2 script constant */ + public static final int SCRIPT_GURMUKHI_2 = 1310; // 'gur2' -- MSFT (pseudo) script tag for variant shaping semantics + /** devanagari script constant */ + public static final int SCRIPT_DEVANAGARI = 315; // 'deva' + /** devanagari 2 script constant */ + public static final int SCRIPT_DEVANAGARI_2 = 1315; // 'dev2' -- MSFT (pseudo) script tag for variant shaping semantics + /** gujarati script constant */ + public static final int SCRIPT_GUJARATI = 320; // 'gujr' + /** gujarati 2 script constant */ + public static final int SCRIPT_GUJARATI_2 = 1320; // 'gjr2' -- MSFT (pseudo) script tag for variant shaping semantics + /** bengali script constant */ + public static final int SCRIPT_BENGALI = 326; // 'beng' + /** bengali 2 script constant */ + public static final int SCRIPT_BENGALI_2 = 1326; // 'bng2' -- MSFT (pseudo) script tag for variant shaping semantics + /** oriya script constant */ + public static final int SCRIPT_ORIYA = 327; // 'orya' + /** oriya 2 script constant */ + public static final int SCRIPT_ORIYA_2 = 1327; // 'ory2' -- MSFT (pseudo) script tag for variant shaping semantics + /** tibetan script constant */ + public static final int SCRIPT_TIBETAN = 330; // 'tibt' + /** telugu script constant */ + public static final int SCRIPT_TELUGU = 340; // 'telu' + /** telugu 2 script constant */ + public static final int SCRIPT_TELUGU_2 = 1340; // 'tel2' -- MSFT (pseudo) script tag for variant shaping semantics + /** kannada script constant */ + public static final int SCRIPT_KANNADA = 345; // 'knda' + /** kannada 2 script constant */ + public static final int SCRIPT_KANNADA_2 = 1345; // 'knd2' -- MSFT (pseudo) script tag for variant shaping semantics + /** tamil script constant */ + public static final int SCRIPT_TAMIL = 346; // 'taml' + /** tamil 2 script constant */ + public static final int SCRIPT_TAMIL_2 = 1346; // 'tml2' -- MSFT (pseudo) script tag for variant shaping semantics + /** malayalam script constant */ + public static final int SCRIPT_MALAYALAM = 347; // 'mlym' + /** malayalam 2 script constant */ + public static final int SCRIPT_MALAYALAM_2 = 1347; // 'mlm2' -- MSFT (pseudo) script tag for variant shaping semantics + /** sinhalese script constant */ + public static final int SCRIPT_SINHALESE = 348; // 'sinh' + /** burmese script constant */ + public static final int SCRIPT_BURMESE = 350; // 'mymr' + /** thai script constant */ + public static final int SCRIPT_THAI = 352; // 'thai' + /** khmer script constant */ + public static final int SCRIPT_KHMER = 355; // 'khmr' + /** lao script constant */ + public static final int SCRIPT_LAO = 356; // 'laoo' + /** hiragana script constant */ + public static final int SCRIPT_HIRAGANA = 410; // 'hira' + /** ethiopic script constant */ + public static final int SCRIPT_ETHIOPIC = 430; // 'ethi' + /** han script constant */ + public static final int SCRIPT_HAN = 500; // 'hani' + /** katakana script constant */ + public static final int SCRIPT_KATAKANA = 410; // 'kana' + /** math script constant */ + public static final int SCRIPT_MATH = 995; // 'zmth' + /** symbol script constant */ + public static final int SCRIPT_SYMBOL = 996; // 'zsym' + /** undetermined script constant */ + public static final int SCRIPT_UNDETERMINED = 998; // 'zyyy' + /** uncoded script constant */ + public static final int SCRIPT_UNCODED = 999; // 'zzzz' + + /** + * A static (class) parameter indicating whether V2 indic shaping + * rules apply or not, with default being true. + */ + private static final boolean USE_V2_INDIC = true; + + private CharScript() { + } + + /** + * Determine if character c is punctuation. + * @param c a character represented as a unicode scalar value + * @return true if character is punctuation + */ + public static boolean isPunctuation(int c) { + if ((c >= 0x0021) && (c <= 0x002F)) { // basic latin punctuation + return true; + } else if ((c >= 0x003A) && (c <= 0x0040)) { // basic latin punctuation + return true; + } else if ((c >= 0x005F) && (c <= 0x0060)) { // basic latin punctuation + return true; + } else if ((c >= 0x007E) && (c <= 0x007E)) { // basic latin punctuation + return true; + } else if ((c >= 0x007E) && (c <= 0x007E)) { // basic latin punctuation + return true; + } else if ((c >= 0x00A1) && (c <= 0x00BF)) { // latin supplement punctuation + return true; + } else if ((c >= 0x00D7) && (c <= 0x00D7)) { // latin supplement punctuation + return true; + } else if ((c >= 0x00F7) && (c <= 0x00F7)) { // latin supplement punctuation + return true; + } else if ((c >= 0x2000) && (c <= 0x206F)) { // general punctuation + return true; + } else { // [TBD] - not complete + return false; + } + } + + /** + * Determine if character c is a digit. + * @param c a character represented as a unicode scalar value + * @return true if character is a digit + */ + public static boolean isDigit(int c) { + if ((c >= 0x0030) && (c <= 0x0039)) { // basic latin digits + return true; + } else { // [TBD] - not complete + return false; + } + } + + /** + * Determine if character c belong to the hebrew script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to hebrew script + */ + public static boolean isHebrew(int c) { + if ((c >= 0x0590) && (c <= 0x05FF)) { // hebrew block + return true; + } else if ((c >= 0xFB00) && (c <= 0xFB4F)) { // hebrew presentation forms block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the mongolian script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to mongolian script + */ + public static boolean isMongolian(int c) { + if ((c >= 0x1800) && (c <= 0x18AF)) { // mongolian block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the arabic script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to arabic script + */ + public static boolean isArabic(int c) { + if ((c >= 0x0600) && (c <= 0x06FF)) { // arabic block + return true; + } else if ((c >= 0x0750) && (c <= 0x077F)) { // arabic supplement block + return true; + } else if ((c >= 0xFB50) && (c <= 0xFDFF)) { // arabic presentation forms a block + return true; + } else if ((c >= 0xFE70) && (c <= 0xFEFF)) { // arabic presentation forms b block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the greek script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to greek script + */ + public static boolean isGreek(int c) { + if ((c >= 0x0370) && (c <= 0x03FF)) { // greek (and coptic) block + return true; + } else if ((c >= 0x1F00) && (c <= 0x1FFF)) { // greek extended block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the latin script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to latin script + */ + public static boolean isLatin(int c) { + if ((c >= 0x0041) && (c <= 0x005A)) { // basic latin upper case + return true; + } else if ((c >= 0x0061) && (c <= 0x007A)) { // basic latin lower case + return true; + } else if ((c >= 0x00C0) && (c <= 0x00D6)) { // latin supplement upper case + return true; + } else if ((c >= 0x00D8) && (c <= 0x00DF)) { // latin supplement upper case + return true; + } else if ((c >= 0x00E0) && (c <= 0x00F6)) { // latin supplement lower case + return true; + } else if ((c >= 0x00F8) && (c <= 0x00FF)) { // latin supplement lower case + return true; + } else if ((c >= 0x0100) && (c <= 0x017F)) { // latin extended a + return true; + } else if ((c >= 0x0180) && (c <= 0x024F)) { // latin extended b + return true; + } else if ((c >= 0x1E00) && (c <= 0x1EFF)) { // latin extended additional + return true; + } else if ((c >= 0x2C60) && (c <= 0x2C7F)) { // latin extended c + return true; + } else if ((c >= 0xA720) && (c <= 0xA7FF)) { // latin extended d + return true; + } else if ((c >= 0xFB00) && (c <= 0xFB0F)) { // latin ligatures + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the cyrillic script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to cyrillic script + */ + public static boolean isCyrillic(int c) { + if ((c >= 0x0400) && (c <= 0x04FF)) { // cyrillic block + return true; + } else if ((c >= 0x0500) && (c <= 0x052F)) { // cyrillic supplement block + return true; + } else if ((c >= 0x2DE0) && (c <= 0x2DFF)) { // cyrillic extended-a block + return true; + } else if ((c >= 0xA640) && (c <= 0xA69F)) { // cyrillic extended-b block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the georgian script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to georgian script + */ + public static boolean isGeorgian(int c) { + if ((c >= 0x10A0) && (c <= 0x10FF)) { // georgian block + return true; + } else if ((c >= 0x2D00) && (c <= 0x2D2F)) { // georgian supplement block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the hangul script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to hangul script + */ + public static boolean isHangul(int c) { + if ((c >= 0x1100) && (c <= 0x11FF)) { // hangul jamo + return true; + } else if ((c >= 0x3130) && (c <= 0x318F)) { // hangul compatibility jamo + return true; + } else if ((c >= 0xA960) && (c <= 0xA97F)) { // hangul jamo extended a + return true; + } else if ((c >= 0xAC00) && (c <= 0xD7A3)) { // hangul syllables + return true; + } else if ((c >= 0xD7B0) && (c <= 0xD7FF)) { // hangul jamo extended a + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the gurmukhi script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to gurmukhi script + */ + public static boolean isGurmukhi(int c) { + if ((c >= 0x0A00) && (c <= 0x0A7F)) { // gurmukhi block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the devanagari script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to devanagari script + */ + public static boolean isDevanagari(int c) { + if ((c >= 0x0900) && (c <= 0x097F)) { // devangari block + return true; + } else if ((c >= 0xA8E0) && (c <= 0xA8FF)) { // devangari extended block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the gujarati script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to gujarati script + */ + public static boolean isGujarati(int c) { + if ((c >= 0x0A80) && (c <= 0x0AFF)) { // gujarati block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the bengali script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to bengali script + */ + public static boolean isBengali(int c) { + if ((c >= 0x0980) && (c <= 0x09FF)) { // bengali block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the oriya script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to oriya script + */ + public static boolean isOriya(int c) { + if ((c >= 0x0B00) && (c <= 0x0B7F)) { // oriya block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the tibetan script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to tibetan script + */ + public static boolean isTibetan(int c) { + if ((c >= 0x0F00) && (c <= 0x0FFF)) { // tibetan block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the telugu script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to telugu script + */ + public static boolean isTelugu(int c) { + if ((c >= 0x0C00) && (c <= 0x0C7F)) { // telugu block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the kannada script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to kannada script + */ + public static boolean isKannada(int c) { + if ((c >= 0x0C00) && (c <= 0x0C7F)) { // kannada block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the tamil script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to tamil script + */ + public static boolean isTamil(int c) { + if ((c >= 0x0B80) && (c <= 0x0BFF)) { // tamil block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the malayalam script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to malayalam script + */ + public static boolean isMalayalam(int c) { + if ((c >= 0x0D00) && (c <= 0x0D7F)) { // malayalam block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the sinhalese script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to sinhalese script + */ + public static boolean isSinhalese(int c) { + if ((c >= 0x0D80) && (c <= 0x0DFF)) { // sinhala block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the burmese script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to burmese script + */ + public static boolean isBurmese(int c) { + if ((c >= 0x1000) && (c <= 0x109F)) { // burmese (myanmar) block + return true; + } else if ((c >= 0xAA60) && (c <= 0xAA7F)) { // burmese (myanmar) extended block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the thai script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to thai script + */ + public static boolean isThai(int c) { + if ((c >= 0x0E00) && (c <= 0x0E7F)) { // thai block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the khmer script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to khmer script + */ + public static boolean isKhmer(int c) { + if ((c >= 0x1780) && (c <= 0x17FF)) { // khmer block + return true; + } else if ((c >= 0x19E0) && (c <= 0x19FF)) { // khmer symbols block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the lao script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to lao script + */ + public static boolean isLao(int c) { + if ((c >= 0x0E80) && (c <= 0x0EFF)) { // lao block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the ethiopic (amharic) script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to ethiopic (amharic) script + */ + public static boolean isEthiopic(int c) { + if ((c >= 0x1200) && (c <= 0x137F)) { // ethiopic block + return true; + } else if ((c >= 0x1380) && (c <= 0x139F)) { // ethoipic supplement block + return true; + } else if ((c >= 0x2D80) && (c <= 0x2DDF)) { // ethoipic extended block + return true; + } else if ((c >= 0xAB00) && (c <= 0xAB2F)) { // ethoipic extended-a block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the han (unified cjk) script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to han (unified cjk) script + */ + public static boolean isHan(int c) { + if ((c >= 0x3400) && (c <= 0x4DBF)) { + return true; // cjk unified ideographs extension a + } else if ((c >= 0x4E00) && (c <= 0x9FFF)) { + return true; // cjk unified ideographs + } else if ((c >= 0xF900) && (c <= 0xFAFF)) { + return true; // cjk compatibility ideographs + } else if ((c >= 0x20000) && (c <= 0x2A6DF)) { + return true; // cjk unified ideographs extension b + } else if ((c >= 0x2A700) && (c <= 0x2B73F)) { + return true; // cjk unified ideographs extension c + } else if ((c >= 0x2F800) && (c <= 0x2FA1F)) { + return true; // cjk compatibility ideographs supplement + } else { + return false; + } + } + + /** + * Determine if character c belong to the bopomofo script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to bopomofo script + */ + public static boolean isBopomofo(int c) { + if ((c >= 0x3100) && (c <= 0x312F)) { + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the hiragana script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to hiragana script + */ + public static boolean isHiragana(int c) { + if ((c >= 0x3040) && (c <= 0x309F)) { + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the katakana script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to katakana script + */ + public static boolean isKatakana(int c) { + if ((c >= 0x30A0) && (c <= 0x30FF)) { + return true; + } else if ((c >= 0x31F0) && (c <= 0x31FF)) { + return true; + } else { + return false; + } + } + + /** + * Obtain ISO15924 numeric script code of character. If script is not or cannot be determined, + * then the script code 998 ('zyyy') is returned. + * @param c the character to obtain script + * @return an ISO15924 script code + */ + public static int scriptOf(int c) { // [TBD] - needs optimization!!! + if (isAnySpace(c)) { + return SCRIPT_UNDETERMINED; + } else if (isPunctuation(c)) { + return SCRIPT_UNDETERMINED; + } else if (isDigit(c)) { + return SCRIPT_UNDETERMINED; + } else if (isLatin(c)) { + return SCRIPT_LATIN; + } else if (isCyrillic(c)) { + return SCRIPT_CYRILLIC; + } else if (isGreek(c)) { + return SCRIPT_GREEK; + } else if (isHan(c)) { + return SCRIPT_HAN; + } else if (isBopomofo(c)) { + return SCRIPT_BOPOMOFO; + } else if (isKatakana(c)) { + return SCRIPT_KATAKANA; + } else if (isHiragana(c)) { + return SCRIPT_HIRAGANA; + } else if (isHangul(c)) { + return SCRIPT_HANGUL; + } else if (isArabic(c)) { + return SCRIPT_ARABIC; + } else if (isHebrew(c)) { + return SCRIPT_HEBREW; + } else if (isMongolian(c)) { + return SCRIPT_MONGOLIAN; + } else if (isGeorgian(c)) { + return SCRIPT_GEORGIAN; + } else if (isGurmukhi(c)) { + return useV2IndicRules(SCRIPT_GURMUKHI); + } else if (isDevanagari(c)) { + return useV2IndicRules(SCRIPT_DEVANAGARI); + } else if (isGujarati(c)) { + return useV2IndicRules(SCRIPT_GUJARATI); + } else if (isBengali(c)) { + return useV2IndicRules(SCRIPT_BENGALI); + } else if (isOriya(c)) { + return useV2IndicRules(SCRIPT_ORIYA); + } else if (isTibetan(c)) { + return SCRIPT_TIBETAN; + } else if (isTelugu(c)) { + return useV2IndicRules(SCRIPT_TELUGU); + } else if (isKannada(c)) { + return useV2IndicRules(SCRIPT_KANNADA); + } else if (isTamil(c)) { + return useV2IndicRules(SCRIPT_TAMIL); + } else if (isMalayalam(c)) { + return useV2IndicRules(SCRIPT_MALAYALAM); + } else if (isSinhalese(c)) { + return SCRIPT_SINHALESE; + } else if (isBurmese(c)) { + return SCRIPT_BURMESE; + } else if (isThai(c)) { + return SCRIPT_THAI; + } else if (isKhmer(c)) { + return SCRIPT_KHMER; + } else if (isLao(c)) { + return SCRIPT_LAO; + } else if (isEthiopic(c)) { + return SCRIPT_ETHIOPIC; + } else { + return SCRIPT_UNDETERMINED; + } + } + + /** + * Obtain the V2 indic script code corresponding to V1 indic script code SC if + * and only iff V2 indic rules apply; otherwise return SC. + * @param sc a V1 indic script code + * @return either SC or the V2 flavor of SC if V2 indic rules apply + */ + public static int useV2IndicRules(int sc) { + if (USE_V2_INDIC) { + return (sc < 1000) ? (sc + 1000) : sc; + } else { + return sc; + } + } + + /** + * Obtain the script codes of each character in a character sequence. If script + * is not or cannot be determined for some character, then the script code 998 + * ('zyyy') is returned. + * @param cs the character sequence + * @return a (possibly empty) array of script codes + */ + public static int[] scriptsOf(CharSequence cs) { + Set s = new HashSet<>(); + for (int i = 0, n = cs.length(); i < n; i++) { + s.add(Integer.valueOf(scriptOf(cs.charAt(i)))); + } + int[] sa = new int [ s.size() ]; + int ns = 0; + for (Iterator it = s.iterator(); it.hasNext();) { + sa [ ns++ ] = it.next().intValue(); + } + Arrays.sort(sa); + return sa; + } + + /** + * Determine the dominant script of a character sequence. + * @param cs the character sequence + * @return the dominant script or SCRIPT_UNDETERMINED + */ + public static int dominantScript(CharSequence cs) { + Map m = new HashMap<>(); + for (int i = 0, n = cs.length(); i < n; i++) { + int c = cs.charAt(i); + int s = scriptOf(c); + Integer k = Integer.valueOf(s); + Integer v = (Integer) m.get(k); + if (v != null) { + m.put(k, Integer.valueOf(v.intValue() + 1)); + } else { + m.put(k, Integer.valueOf(0)); + } + } + int sMax = -1; + int cMax = -1; + for (Iterator> it = m.entrySet().iterator(); it.hasNext();) { + Map.Entry e = it.next(); + Integer k = e.getKey(); + int s = k.intValue(); + switch (s) { + case SCRIPT_UNDETERMINED: + case SCRIPT_UNCODED: + break; + default: + Integer v = e.getValue(); + assert v != null; + int c = v.intValue(); + if (c > cMax) { + cMax = c; + sMax = s; + } + break; + } + } + if (sMax < 0) { + sMax = SCRIPT_UNDETERMINED; + } + return sMax; + } + + /** + * Determine if script tag denotes an 'Indic' script, where a + * script is an 'Indic' script if it is intended to be processed by + * the generic 'Indic' Script Processor. + * @param script a script tag + * @return true if script tag is a designated 'Indic' script + */ + public static boolean isIndicScript(String script) { + return isIndicScript(scriptCodeFromTag(script)); + } + + /** + * Determine if script tag denotes an 'Indic' script, where a + * script is an 'Indic' script if it is intended to be processed by + * the generic 'Indic' Script Processor. + * @param script a script code + * @return true if script code is a designated 'Indic' script + */ + public static boolean isIndicScript(int script) { + switch (script) { + case SCRIPT_BENGALI: + case SCRIPT_BENGALI_2: + case SCRIPT_BURMESE: + case SCRIPT_DEVANAGARI: + case SCRIPT_DEVANAGARI_2: + case SCRIPT_GUJARATI: + case SCRIPT_GUJARATI_2: + case SCRIPT_GURMUKHI: + case SCRIPT_GURMUKHI_2: + case SCRIPT_KANNADA: + case SCRIPT_KANNADA_2: + case SCRIPT_MALAYALAM: + case SCRIPT_MALAYALAM_2: + case SCRIPT_ORIYA: + case SCRIPT_ORIYA_2: + case SCRIPT_TAMIL: + case SCRIPT_TAMIL_2: + case SCRIPT_TELUGU: + case SCRIPT_TELUGU_2: + return true; + default: + return false; + } + } + + /** + * Determine the script tag associated with an internal script code. + * @param code the script code + * @return a script tag + */ + public static String scriptTagFromCode(int code) { + Map m = getScriptTagsMap(); + if (m != null) { + String tag; + if ((tag = m.get(Integer.valueOf(code))) != null) { + return tag; + } else { + return ""; + } + } else { + return ""; + } + } + + /** + * Determine the internal script code associated with a script tag. + * @param tag the script tag + * @return a script code + */ + public static int scriptCodeFromTag(String tag) { + Map m = getScriptCodeMap(); + if (m != null) { + Integer c; + if ((c = m.get(tag)) != null) { + return (int) c; + } else { + return SCRIPT_UNDETERMINED; + } + } else { + return SCRIPT_UNDETERMINED; + } + } + + private static Map scriptTagsMap; + private static Map scriptCodeMap; + + private static void putScriptTag(Map tm, Map cm, int code, String tag) { + assert tag != null; + assert tag.length() != 0; + assert code >= 0; + assert code < 2000; + tm.put(Integer.valueOf(code), tag); + cm.put(tag, Integer.valueOf(code)); + } + + private static void makeScriptMaps() { + HashMap tm = new HashMap(); + HashMap cm = new HashMap(); + putScriptTag(tm, cm, SCRIPT_HEBREW, "hebr"); + putScriptTag(tm, cm, SCRIPT_MONGOLIAN, "mong"); + putScriptTag(tm, cm, SCRIPT_ARABIC, "arab"); + putScriptTag(tm, cm, SCRIPT_GREEK, "grek"); + putScriptTag(tm, cm, SCRIPT_LATIN, "latn"); + putScriptTag(tm, cm, SCRIPT_CYRILLIC, "cyrl"); + putScriptTag(tm, cm, SCRIPT_GEORGIAN, "geor"); + putScriptTag(tm, cm, SCRIPT_BOPOMOFO, "bopo"); + putScriptTag(tm, cm, SCRIPT_HANGUL, "hang"); + putScriptTag(tm, cm, SCRIPT_GURMUKHI, "guru"); + putScriptTag(tm, cm, SCRIPT_GURMUKHI_2, "gur2"); + putScriptTag(tm, cm, SCRIPT_DEVANAGARI, "deva"); + putScriptTag(tm, cm, SCRIPT_DEVANAGARI_2, "dev2"); + putScriptTag(tm, cm, SCRIPT_GUJARATI, "gujr"); + putScriptTag(tm, cm, SCRIPT_GUJARATI_2, "gjr2"); + putScriptTag(tm, cm, SCRIPT_BENGALI, "beng"); + putScriptTag(tm, cm, SCRIPT_BENGALI_2, "bng2"); + putScriptTag(tm, cm, SCRIPT_ORIYA, "orya"); + putScriptTag(tm, cm, SCRIPT_ORIYA_2, "ory2"); + putScriptTag(tm, cm, SCRIPT_TIBETAN, "tibt"); + putScriptTag(tm, cm, SCRIPT_TELUGU, "telu"); + putScriptTag(tm, cm, SCRIPT_TELUGU_2, "tel2"); + putScriptTag(tm, cm, SCRIPT_KANNADA, "knda"); + putScriptTag(tm, cm, SCRIPT_KANNADA_2, "knd2"); + putScriptTag(tm, cm, SCRIPT_TAMIL, "taml"); + putScriptTag(tm, cm, SCRIPT_TAMIL_2, "tml2"); + putScriptTag(tm, cm, SCRIPT_MALAYALAM, "mlym"); + putScriptTag(tm, cm, SCRIPT_MALAYALAM_2, "mlm2"); + putScriptTag(tm, cm, SCRIPT_SINHALESE, "sinh"); + putScriptTag(tm, cm, SCRIPT_BURMESE, "mymr"); + putScriptTag(tm, cm, SCRIPT_THAI, "thai"); + putScriptTag(tm, cm, SCRIPT_KHMER, "khmr"); + putScriptTag(tm, cm, SCRIPT_LAO, "laoo"); + putScriptTag(tm, cm, SCRIPT_HIRAGANA, "hira"); + putScriptTag(tm, cm, SCRIPT_ETHIOPIC, "ethi"); + putScriptTag(tm, cm, SCRIPT_HAN, "hani"); + putScriptTag(tm, cm, SCRIPT_KATAKANA, "kana"); + putScriptTag(tm, cm, SCRIPT_MATH, "zmth"); + putScriptTag(tm, cm, SCRIPT_SYMBOL, "zsym"); + putScriptTag(tm, cm, SCRIPT_UNDETERMINED, "zyyy"); + putScriptTag(tm, cm, SCRIPT_UNCODED, "zzzz"); + scriptTagsMap = tm; + scriptCodeMap = cm; + } + + private static Map getScriptTagsMap() { + if (scriptTagsMap == null) { + makeScriptMaps(); + } + return scriptTagsMap; + } + + private static Map getScriptCodeMap() { + if (scriptCodeMap == null) { + makeScriptMaps(); + } + return scriptCodeMap; + } + + private static boolean isAnySpace(int c) { + return (isBreakableSpace(c) || isNonBreakableSpace(c)); + } + + private static boolean isBreakableSpace(int c) { + return (c == '\u0020' || isFixedWidthSpace(c)); + } + + private static boolean isFixedWidthSpace(int c) { + return (c >= '\u2000' && c <= '\u200B') || c == '\u3000'; + } + + private static boolean isNonBreakableSpace(int c) { + return + (c == '\u00A0' // no-break space + || c == '\u202F' // narrow no-break space + || c == '\u2060' // word joiner + || c == '\u3000' // ideographic space + || c == '\uFEFF'); // zero width no-break space + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/DiscontinuousAssociationException.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/DiscontinuousAssociationException.java new file mode 100644 index 00000000000..40d4e637b34 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/DiscontinuousAssociationException.java @@ -0,0 +1,42 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.util; + +/** + *

Exception thrown during when attempting to map glyphs to associated characters + * in the case that the associated characters do not represent a compact interval.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class DiscontinuousAssociationException extends RuntimeException { + /** + * Instantiate discontinuous association exception + */ + public DiscontinuousAssociationException() { + super(); + } + /** + * Instantiate discontinuous association exception + * @param message a message string + */ + public DiscontinuousAssociationException(String message) { + super(message); + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphContextTester.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphContextTester.java new file mode 100644 index 00000000000..f3e410aafd8 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphContextTester.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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.util; + +// CSOFF: LineLengthCheck + +/** + *

Interface for testing the originating (source) character context of a glyph sequence.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphContextTester { + + /** + * Perform a test on a glyph sequence in a specific (originating) character context. + * @param script governing script + * @param language governing language + * @param feature governing feature + * @param gs glyph sequence to test + * @param index index into glyph sequence to test + * @param flags that apply to lookup in scope + * @return true if test is satisfied + */ + boolean test(String script, String language, String feature, GlyphSequence gs, int index, int flags); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphSequence.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphSequence.java new file mode 100644 index 00000000000..69a7e9f2144 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphSequence.java @@ -0,0 +1,616 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.util; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +// CSOFF: LineLengthCheck + +/** + *

A GlyphSequence encapsulates a sequence of character codes, a sequence of glyph codes, + * and a sequence of character associations, where, for each glyph in the sequence of glyph + * codes, there is a corresponding character association. Character associations server to + * relate the glyph codes in a glyph sequence to the specific characters in an original + * character code sequence with which the glyph codes are associated.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public class GlyphSequence implements Cloneable { + + /** default character buffer capacity in case new character buffer is created */ + private static final int DEFAULT_CHARS_CAPACITY = 8; + + /** character buffer */ + private IntBuffer characters; + /** glyph buffer */ + private IntBuffer glyphs; + /** association list */ + private List associations; + /** predications flag */ + private boolean predications; + + /** + * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced + * character and glyph buffers and associations. If characters is null, then + * an empty character buffer is created. If glyphs is null, then a glyph buffer + * is created whose capacity is that of the character buffer. If associations is + * null, then identity associations are created. + * @param characters a (possibly null) buffer of associated (originating) characters + * @param glyphs a (possibly null) buffer of glyphs + * @param associations a (possibly null) array of glyph to character associations + * @param predications true if predications are enabled + */ + public GlyphSequence(IntBuffer characters, IntBuffer glyphs, List associations, boolean predications) { + if (characters == null) { + characters = IntBuffer.allocate(DEFAULT_CHARS_CAPACITY); + } + if (glyphs == null) { + glyphs = IntBuffer.allocate(characters.capacity()); + } + if (associations == null) { + associations = makeIdentityAssociations(characters.limit(), glyphs.limit()); + } + this.characters = characters; + this.glyphs = glyphs; + this.associations = associations; + this.predications = predications; + } + + /** + * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced + * character and glyph buffers and associations. If characters is null, then + * an empty character buffer is created. If glyphs is null, then a glyph buffer + * is created whose capacity is that of the character buffer. If associations is + * null, then identity associations are created. + * @param characters a (possibly null) buffer of associated (originating) characters + * @param glyphs a (possibly null) buffer of glyphs + * @param associations a (possibly null) array of glyph to character associations + */ + public GlyphSequence(IntBuffer characters, IntBuffer glyphs, List associations) { + this (characters, glyphs, associations, false); + } + + /** + * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares + * the character array of the existing sequence (but not the buffer object), and creates new copies + * of glyphs buffer and association list. + * @param gs an existing glyph sequence + */ + public GlyphSequence(GlyphSequence gs) { + this (gs.characters.duplicate(), copyBuffer(gs.glyphs), copyAssociations(gs.associations), gs.predications); + } + + /** + * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares + * the character array of the existing sequence (but not the buffer object), but uses the specified + * backtrack, input, and lookahead glyph arrays to populate the glyphs, and uses the specified + * of glyphs buffer and association list. + * backtrack, input, and lookahead association arrays to populate the associations. + * @param gs an existing glyph sequence + * @param bga backtrack glyph array + * @param iga input glyph array + * @param lga lookahead glyph array + * @param bal backtrack association list + * @param ial input association list + * @param lal lookahead association list + */ + public GlyphSequence(GlyphSequence gs, int[] bga, int[] iga, int[] lga, CharAssociation[] bal, CharAssociation[] ial, CharAssociation[] lal) { + this (gs.characters.duplicate(), concatGlyphs(bga, iga, lga), concatAssociations(bal, ial, lal), gs.predications); + } + + /** + * Obtain reference to underlying character buffer. + * @return character buffer reference + */ + public IntBuffer getCharacters() { + return characters; + } + + /** + * Obtain array of characters. If copy is true, then + * a newly instantiated array is returned, otherwise a reference to + * the underlying buffer's array is returned. N.B. in case a reference + * to the undelying buffer's array is returned, the length + * of the array is not necessarily the number of characters in array. + * To determine the number of characters, use {@link #getCharacterCount}. + * @param copy true if to return a newly instantiated array of characters + * @return array of characters + */ + public int[] getCharacterArray(boolean copy) { + if (copy) { + return toArray(characters); + } else { + return characters.array(); + } + } + + /** + * Obtain the number of characters in character array, where + * each character constitutes a unicode scalar value. + * @return number of characters available in character array + */ + public int getCharacterCount() { + return characters.limit(); + } + + /** + * Obtain glyph id at specified index. + * @param index to obtain glyph + * @return the glyph identifier of glyph at specified index + * @throws IndexOutOfBoundsException if index is less than zero + * or exceeds last valid position + */ + public int getGlyph(int index) throws IndexOutOfBoundsException { + return glyphs.get(index); + } + + /** + * Set glyph id at specified index. + * @param index to set glyph + * @param gi glyph index + * @throws IndexOutOfBoundsException if index is greater or equal to + * the limit of the underlying glyph buffer + */ + public void setGlyph(int index, int gi) throws IndexOutOfBoundsException { + if (gi > 65535) { + gi = 65535; + } + glyphs.put(index, gi); + } + + /** + * Obtain reference to underlying glyph buffer. + * @return glyph buffer reference + */ + public IntBuffer getGlyphs() { + return glyphs; + } + + /** + * Obtain count glyphs starting at offset. If count is + * negative, then it is treated as if the number of available glyphs + * were specified. + * @param offset into glyph sequence + * @param count of glyphs to obtain starting at offset, or negative, + * indicating all avaialble glyphs starting at offset + * @return glyph array + */ + public int[] getGlyphs(int offset, int count) { + int ng = getGlyphCount(); + if (offset < 0) { + offset = 0; + } else if (offset > ng) { + offset = ng; + } + if (count < 0) { + count = ng - offset; + } + int[] ga = new int [ count ]; + for (int i = offset, n = offset + count, k = 0; i < n; i++) { + if (k < ga.length) { + ga [ k++ ] = glyphs.get(i); + } + } + return ga; + } + + /** + * Obtain array of glyphs. If copy is true, then + * a newly instantiated array is returned, otherwise a reference to + * the underlying buffer's array is returned. N.B. in case a reference + * to the undelying buffer's array is returned, the length + * of the array is not necessarily the number of glyphs in array. + * To determine the number of glyphs, use {@link #getGlyphCount}. + * @param copy true if to return a newly instantiated array of glyphs + * @return array of glyphs + */ + public int[] getGlyphArray(boolean copy) { + if (copy) { + return toArray(glyphs); + } else { + return glyphs.array(); + } + } + + /** + * Obtain the number of glyphs in glyphs array, where + * each glyph constitutes a font specific glyph index. + * @return number of glyphs available in character array + */ + public int getGlyphCount() { + return glyphs.limit(); + } + + /** + * Obtain association at specified index. + * @param index into associations array + * @return glyph to character associations at specified index + * @throws IndexOutOfBoundsException if index is less than zero + * or exceeds last valid position + */ + public CharAssociation getAssociation(int index) throws IndexOutOfBoundsException { + return (CharAssociation) associations.get(index); + } + + /** + * Obtain reference to underlying associations list. + * @return associations list + */ + public List getAssociations() { + return associations; + } + + /** + * Obtain count associations starting at offset. + * @param offset into glyph sequence + * @param count of associations to obtain starting at offset, or negative, + * indicating all avaialble associations starting at offset + * @return associations + */ + public CharAssociation[] getAssociations(int offset, int count) { + int ng = getGlyphCount(); + if (offset < 0) { + offset = 0; + } else if (offset > ng) { + offset = ng; + } + if (count < 0) { + count = ng - offset; + } + CharAssociation[] aa = new CharAssociation [ count ]; + for (int i = offset, n = offset + count, k = 0; i < n; i++) { + if (k < aa.length) { + aa [ k++ ] = (CharAssociation) associations.get(i); + } + } + return aa; + } + + /** + * Enable or disable predications. + * @param enable true if predications are to be enabled; otherwise false to disable + */ + public void setPredications(boolean enable) { + this.predications = enable; + } + + /** + * Obtain predications state. + * @return true if predications are enabled + */ + public boolean getPredications() { + return this.predications; + } + + /** + * Set predication <KEY,VALUE> at glyph sequence OFFSET. + * @param offset offset (index) into glyph sequence + * @param key predication key + * @param value predication value + */ + public void setPredication(int offset, String key, Object value) { + if (predications) { + CharAssociation[] aa = getAssociations(offset, 1); + CharAssociation ca = aa[0]; + ca.setPredication(key, value); + } + } + + /** + * Get predication KEY at glyph sequence OFFSET. + * @param offset offset (index) into glyph sequence + * @param key predication key + * @return predication KEY at OFFSET or null if none exists + */ + public Object getPredication(int offset, String key) { + if (predications) { + CharAssociation[] aa = getAssociations(offset, 1); + CharAssociation ca = aa[0]; + return ca.getPredication(key); + } else { + return null; + } + } + + /** + * Compare glyphs. + * @param gb buffer containing glyph indices with which this glyph sequence's glyphs are to be compared + * @return zero if glyphs are the same, otherwise returns 1 or -1 according to whether this glyph sequence's + * glyphs are lexicographically greater or lesser than the glyphs in the specified string buffer + */ + public int compareGlyphs(IntBuffer gb) { + int ng = getGlyphCount(); + for (int i = 0, n = gb.limit(); i < n; i++) { + if (i < ng) { + int g1 = glyphs.get(i); + int g2 = gb.get(i); + if (g1 > g2) { + return 1; + } else if (g1 < g2) { + return -1; + } + } else { + return -1; // this gb is a proper prefix of specified gb + } + } + return 0; // same lengths with no difference + } + + /** {@inheritDoc} */ + @Override + public Object clone() { + try { + GlyphSequence gs = (GlyphSequence) super.clone(); + gs.characters = copyBuffer(characters); + gs.glyphs = copyBuffer(glyphs); + gs.associations = copyAssociations(associations); + return gs; + } catch (CloneNotSupportedException e) { + return null; + } + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + sb.append("chars = ["); + sb.append(characters); + sb.append("], glyphs = ["); + sb.append(glyphs); + sb.append("], associations = ["); + sb.append(associations); + sb.append("]"); + sb.append('}'); + return sb.toString(); + } + + /** + * Determine if two arrays of glyphs are identical. + * @param ga1 first glyph array + * @param ga2 second glyph array + * @return true if arrays are botth null or both non-null and have identical elements + */ + public static boolean sameGlyphs(int[] ga1, int[] ga2) { + if (ga1 == ga2) { + return true; + } else if ((ga1 == null) || (ga2 == null)) { + return false; + } else if (ga1.length != ga2.length) { + return false; + } else { + for (int i = 0, n = ga1.length; i < n; i++) { + if (ga1[i] != ga2[i]) { + return false; + } + } + return true; + } + } + + /** + * Concatenante glyph arrays. + * @param bga backtrack glyph array + * @param iga input glyph array + * @param lga lookahead glyph array + * @return new integer buffer containing concatenated glyphs + */ + public static IntBuffer concatGlyphs(int[] bga, int[] iga, int[] lga) { + int ng = 0; + if (bga != null) { + ng += bga.length; + } + if (iga != null) { + ng += iga.length; + } + if (lga != null) { + ng += lga.length; + } + IntBuffer gb = IntBuffer.allocate(ng); + if (bga != null) { + gb.put(bga); + } + if (iga != null) { + gb.put(iga); + } + if (lga != null) { + gb.put(lga); + } + gb.flip(); + return gb; + } + + /** + * Concatenante association arrays. + * @param baa backtrack association array + * @param iaa input association array + * @param laa lookahead association array + * @return new list containing concatenated associations + */ + public static List concatAssociations(CharAssociation[] baa, CharAssociation[] iaa, CharAssociation[] laa) { + int na = 0; + if (baa != null) { + na += baa.length; + } + if (iaa != null) { + na += iaa.length; + } + if (laa != null) { + na += laa.length; + } + if (na > 0) { + List gl = new ArrayList<>(na); + if (baa != null) { + for (int i = 0; i < baa.length; i++) { + gl.add(baa[i]); + } + } + if (iaa != null) { + for (int i = 0; i < iaa.length; i++) { + gl.add(iaa[i]); + } + } + if (laa != null) { + for (int i = 0; i < laa.length; i++) { + gl.add(laa[i]); + } + } + return gl; + } else { + return null; + } + } + + /** + * Join (concatenate) glyph sequences. + * @param gs original glyph sequence from which to reuse character array reference + * @param sa array of glyph sequences, whose glyph arrays and association lists are to be concatenated + * @return new glyph sequence referring to character array of GS and concatenated glyphs and associations of SA + */ + public static GlyphSequence join(GlyphSequence gs, GlyphSequence[] sa) { + assert sa != null; + int tg = 0; + int ta = 0; + for (int i = 0, n = sa.length; i < n; i++) { + GlyphSequence s = sa [ i ]; + IntBuffer ga = s.getGlyphs(); + assert ga != null; + int ng = ga.limit(); + List al = s.getAssociations(); + assert al != null; + int na = al.size(); + assert na == ng; + tg += ng; + ta += na; + } + IntBuffer uga = IntBuffer.allocate(tg); + ArrayList ual = new ArrayList<>(ta); + for (int i = 0, n = sa.length; i < n; i++) { + GlyphSequence s = sa [ i ]; + uga.put(s.getGlyphs()); + ual.addAll(s.getAssociations()); + } + return new GlyphSequence(gs.getCharacters(), uga, ual, gs.getPredications()); + } + + /** + * Reorder sequence such that [SOURCE,SOURCE+COUNT) is moved just prior to TARGET. + * @param gs input sequence + * @param source index of sub-sequence to reorder + * @param count length of sub-sequence to reorder + * @param target index to which source sub-sequence is to be moved + * @return reordered sequence (or original if no reordering performed) + */ + public static GlyphSequence reorder(GlyphSequence gs, int source, int count, int target) { + if (source != target) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray(false); + int[] nga = new int [ ng ]; + CharAssociation[] aa = gs.getAssociations(0, ng); + CharAssociation[] naa = new CharAssociation [ ng ]; + if (source < target) { + int t = 0; + for (int s = 0, e = source; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source + count, e = target; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source, e = source + count; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = target, e = ng; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + } else { + int t = 0; + for (int s = 0, e = target; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source, e = source + count; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = target, e = source; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for (int s = source + count, e = ng; s < e; s++, t++) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + } + return new GlyphSequence(gs, null, nga, null, null, naa, null); + } else { + return gs; + } + } + + private static int[] toArray(IntBuffer ib) { + if (ib != null) { + int n = ib.limit(); + int[] ia = new int[n]; + ib.get(ia, 0, n); + return ia; + } else { + return new int[0]; + } + } + + private static List makeIdentityAssociations(int numChars, int numGlyphs) { + int nc = numChars; + int ng = numGlyphs; + List av = new ArrayList<>(ng); + for (int i = 0, n = ng; i < n; i++) { + int k = (i > nc) ? nc : i; + av.add(new CharAssociation(i, (k == nc) ? 0 : 1)); + } + return av; + } + + private static IntBuffer copyBuffer(IntBuffer ib) { + if (ib != null) { + int[] ia = new int [ ib.capacity() ]; + int p = ib.position(); + int l = ib.limit(); + System.arraycopy(ib.array(), 0, ia, 0, ia.length); + return IntBuffer.wrap(ia, p, l - p); + } else { + return null; + } + } + + private static List copyAssociations(List ca) { + if (ca != null) { + return new ArrayList<>(ca); + } else { + return ca; + } + } + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphTester.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphTester.java new file mode 100644 index 00000000000..061a7c86dc7 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/GlyphTester.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.util; + +/** + *

Interface for testing glyph properties according to glyph identifier.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface GlyphTester { + + /** + * Perform a test on a glyph identifier. + * @param gi glyph identififer + * @param flags that apply to lookup in scope + * @return true if test is satisfied + */ + boolean test(int gi, int flags); + +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/ScriptContextTester.java b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/ScriptContextTester.java new file mode 100644 index 00000000000..e51f19f4f44 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/advanced/util/ScriptContextTester.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/* $Id$ */ + +package org.apache.fontbox.ttf.advanced.util; + +/** + *

Interface for providing script specific context testers.

+ * + *

This work was originally authored by Glenn Adams (gadams@apache.org).

+ */ +public interface ScriptContextTester { + + /** + * Obtain a glyph context tester for the specified feature. + * @param feature a feature identifier + * @return a glyph context tester or null if none available for the specified feature + */ + GlyphContextTester getTester(String feature); + +} diff --git a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java index 6763fe2d9c7..d5c9a71cfdc 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java @@ -38,6 +38,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fontbox.ttf.CmapLookup; +import org.apache.fontbox.ttf.advanced.GlyphVectorAdvanced; +import org.apache.fontbox.ttf.advanced.GlyphVectorSimple; +import org.apache.fontbox.ttf.advanced.api.GlyphVector; import org.apache.fontbox.ttf.gsub.CompoundCharacterTokenizer; import org.apache.fontbox.ttf.gsub.GsubWorker; import org.apache.fontbox.ttf.gsub.GsubWorkerFactory; @@ -47,6 +50,7 @@ import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.pdfwriter.COSWriter; import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import org.apache.pdfbox.pdmodel.font.PDFont; @@ -85,6 +89,8 @@ abstract class PDAbstractContentStream implements Closeable protected boolean inTextMode = false; protected final Deque fontStack = new ArrayDeque<>(); + protected final Deque fontSizeStack = new ArrayDeque<>(); + protected final Deque nonStrokingColorSpaceStack = new ArrayDeque<>(); protected final Deque strokingColorSpaceStack = new ArrayDeque<>(); @@ -96,6 +102,8 @@ abstract class PDAbstractContentStream implements Closeable private final Map gsubWorkers = new HashMap<>(); private final GsubWorkerFactory gsubWorkerFactory = new GsubWorkerFactory(); + private final COSString EMPTY_COS_STRING = new COSString(""); + /** * Create a new appearance stream. * @@ -115,7 +123,7 @@ abstract class PDAbstractContentStream implements Closeable /** * Sets the maximum number of digits allowed for fractional numbers. - * + * * @see NumberFormat#setMaximumFractionDigits(int) * @param fractionDigitsNumber */ @@ -157,7 +165,7 @@ public void endText() throws IOException writeOperator(OperatorName.END_TEXT); inTextMode = false; } - + /** * Set the font and font size to draw text with. * @@ -170,11 +178,14 @@ public void setFont(PDFont font, float fontSize) throws IOException if (fontStack.isEmpty()) { fontStack.add(font); + fontSizeStack.add(fontSize); } else { fontStack.pop(); fontStack.push(font); + fontSizeStack.pop(); + fontSizeStack.push(fontSize); } // keep track of fonts which are configured for subsetting @@ -262,11 +273,248 @@ public void showText(String text) throws IOException writeOperator(OperatorName.SHOW_TEXT); } + + /** + * TODO + * @param vector + * @return + * @throws IOException + */ + public void showGlyphVector(GlyphVector vector) throws IOException { + if (vector instanceof GlyphVectorAdvanced) + { + showGlyphVector((GlyphVectorAdvanced) vector); + } + else if (vector instanceof GlyphVectorSimple) + { + showGlyphVector((GlyphVectorSimple) vector); + } else + { + // TODO + throw new UnsupportedOperationException("not implemented"); + } + } + + + /** + * TODO + * @param vector + * @return + * @throws IOException + */ + public void showGlyphVector(GlyphVectorSimple vector) throws IOException { + // TODO + } + + + /** + * TODO + * @param vector + * @return + * @throws IOException + */ + public void showGlyphVector(GlyphVectorAdvanced vector) throws IOException + { + if (!inTextMode) + { + throw new IllegalStateException("Must call beginText() before showText()"); + } + + if (fontStack.isEmpty()) + { + throw new IllegalStateException("Must call setFont() before showText()"); + } + + PDType0Font font = (PDType0Font) fontStack.peek(); + float fontSize = fontSizeStack.peek(); + + int[] gids = vector.getGlyphArray(); + int[][] adjustments = vector.getAdjustments(); + + if (gids.length == 0) { + return; + } + + // TODO: Text extraction of result PDF. + font.addGlyphsToSubset(vector.getGlyphs()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + if (adjustments == null) { + for (int i = 0; i < gids.length; i++) { + os.write(font.encodeGlyphId(gids[i])); + } + COSWriter.writeString(os.toByteArray(), outputStream); + write(" "); + writeOperator(OperatorName.SHOW_TEXT); + } else { + for (int i = 0; i < gids.length; i++) { + int placementX = 0; + int placementY = 0; + int advanceX = 0; + // not used: int advanceY = 0; + + if (adjustments != null) { + placementX = adjustments[i][0]; + placementY = adjustments[i][1]; + advanceX = adjustments[i][2]; + // not used: advanceY = adjustments[i][3];; + } + if (placementY != 0) { + System.out.printf("placementY=%d rise=%f%n", placementY, placementY*fontSize/1000.0f); + setTextRise(placementY*fontSize/1000.0f); + } + write("["); + System.out.printf("TJ pX=%d%n", placementX); + writeOperand(-placementX); + COSWriter.writeString(font.encodeGlyphId(gids[i]), outputStream); + if (placementX+advanceX != 0) { // update current PDF-position + writeOperand(placementX+advanceX); + COSWriter.writeString(EMPTY_COS_STRING, outputStream); + } + write("] "); + writeOperator(OperatorName.SHOW_TEXT_ADJUSTED); + if (placementY!=0) { + setTextRise(0); + } + } + } + } + public void showGlyphVector1(GlyphVector vector, Matrix textMatrix) throws IOException + { + if (!inTextMode) + { + throw new IllegalStateException("Must call beginText() before showText()"); + } + + if (fontStack.isEmpty()) + { + throw new IllegalStateException("Must call setFont() before showText()"); + } + + PDType0Font font = (PDType0Font) fontStack.peek(); + + if (vector instanceof GlyphVectorAdvanced) { + GlyphVectorAdvanced vec = (GlyphVectorAdvanced) vector; + + int[] gids = vec.getGlyphArray(); + int[][] adjustments = vec.getAdjustments(); + + if (gids.length == 0) { + return; + } + + // TODO: Text extraction of result PDF. + font.addGlyphsToSubset(vec.getGlyphs()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + if (adjustments == null) { + setTextMatrix(textMatrix); + + for (int i = 0; i < gids.length; i++) { + os.write(font.encodeGlyphId(gids[i])); + } + + COSWriter.writeString(os.toByteArray(), outputStream); + write(" "); + writeOperator(OperatorName.SHOW_TEXT); + } else { + int adjustedY = 0; + // fixupX records the move back we have to perform after altering the placement of a glyph + int fixupX = 0; + Matrix tm = new Matrix(textMatrix.createAffineTransform()); + + for (int i = 0; i < gids.length; i++) { + int placementX = 0; + int placementY = 0; + int advanceX = 0; + int advanceY = 0; + + if (adjustments != null) { + placementX = adjustments[i][0]; + placementY = adjustments[i][1]; + advanceX = adjustments[i][2]; + advanceY = adjustments[i][3]; + } + + boolean haveXAdjust = placementX != 0 || advanceX != 0; + boolean haveYAdjust = placementY != 0 || advanceY != 0; + + if (i == 0) { + if (haveXAdjust || haveYAdjust) { + // If the first glyph has adjustments adjust the text matrix. + if (haveYAdjust) { + adjustedY = placementY + advanceY; + int adjustedX = placementX + advanceX; + tm.translate(-adjustedX, -adjustedY); + } else { + int adjustedX = placementX + advanceX; + tm.translate(-adjustedX, 0); + fixupX = placementX; + } + } + + haveXAdjust = false; + haveYAdjust = false; + + setTextMatrix(tm); + write("["); + + os.write(font.encodeGlyphId(gids[i])); + } else { + if (!haveXAdjust && !haveYAdjust) { + if (fixupX != 0) { + writeOperand(fixupX); + fixupX = 0; + } + os.write(font.encodeGlyphId(gids[i])); + } else if (haveXAdjust && !haveYAdjust) { + if (os.size() > 0) { + COSWriter.writeString(os.toByteArray(), outputStream); + os.reset(); + writeOperand(placementX + advanceX); + } else if (fixupX != 0) { + writeOperand(placementX + advanceX + fixupX); + fixupX = 0; + } else { + writeOperand(placementX + advanceX); + } + + if (placementX != 0) { + COSWriter.writeString(font.encodeGlyphId(gids[i]), outputStream); + fixupX = -placementX; + } + } else if (!haveXAdjust && haveYAdjust) { + // TODO + } else { + // TODO + } + } + } + + if (os.size() > 0) { + COSWriter.writeString(os.toByteArray(), outputStream); + write("] "); + writeOperator(OperatorName.SHOW_TEXT_ADJUSTED); + } else { + write("] "); + writeOperator(OperatorName.SHOW_TEXT_ADJUSTED); + } + } + } else if (vector instanceof GlyphVectorSimple) { + // TODO + + } else { + throw new IllegalArgumentException("Invalid glyph vector provided: " + vector.getClass().getName()); + } + } + /** * Outputs a string using the correct encoding and subsetting as required. * * @param text The Unicode text to show. - * + * * @throws IOException If an io exception occurs. */ protected void showTextInternal(String text) throws IOException @@ -287,7 +535,6 @@ protected void showTextInternal(String text) throws IOException byte[] encodedText = null; if (font instanceof PDType0Font) { - GsubWorker gsubWorker = gsubWorkers.get(font); if (gsubWorker != null) { @@ -1021,7 +1268,7 @@ public void lineTo(float x, float y) throws IOException /** * Stroke the path. - * + * * @throws IOException If the content stream could not be written * @throws IllegalStateException If the method was called within a text block. */ @@ -1036,7 +1283,7 @@ public void stroke() throws IOException /** * Close and stroke the path. - * + * * @throws IOException If the content stream could not be written * @throws IllegalStateException If the method was called within a text block. */ @@ -1193,7 +1440,7 @@ public void clip() throws IOException throw new IllegalStateException("Error: clip is not allowed within a text block."); } writeOperator(OperatorName.CLIP_NON_ZERO); - + // end path without filling or stroking writeOperator(OperatorName.ENDPATH); } @@ -1211,7 +1458,7 @@ public void clipEvenOdd() throws IOException throw new IllegalStateException("Error: clipEvenOdd is not allowed within a text block."); } writeOperator(OperatorName.CLIP_EVEN_ODD); - + // end path without filling or stroking writeOperator(OperatorName.ENDPATH); } @@ -1343,7 +1590,7 @@ public void endMarkedContent() throws IOException /** * Set an extended graphics state. - * + * * @param state The extended graphics state. * @throws IOException If the content stream could not be written. */ @@ -1450,7 +1697,7 @@ protected void write(byte[] data) throws IOException { outputStream.write(data); } - + /** * Writes a newline to the content stream as ASCII. * @throws java.io.IOException