From 57ab17734dfd9201e04701f9bb73c7042cbb5996 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Fri, 10 Jun 2022 21:29:51 +0300 Subject: [PATCH] Patch applied, #130 --- Makefile | 2 +- README.adoc | 10 +- pom.xml | 2 +- .../scripts/DefaultScriptProcessor.java | 158 +++++++++ .../scripts/HebrewScriptProcessor.java | 31 ++ .../scripts/ScriptProcessor.java | 300 ++++++++++++++++++ .../scripts/ThaiScriptProcessor.java | 31 ++ 7 files changed, 527 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java create mode 100644 src/main/java/org/apache/fop/complexscripts/scripts/HebrewScriptProcessor.java create mode 100644 src/main/java/org/apache/fop/complexscripts/scripts/ScriptProcessor.java create mode 100644 src/main/java/org/apache/fop/complexscripts/scripts/ThaiScriptProcessor.java diff --git a/Makefile b/Makefile index 3cd2d92f..95e5a79f 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ SHELL ?= /bin/bash endif #JAR_VERSION := $(shell mvn -q -Dexec.executable="echo" -Dexec.args='$${project.version}' --non-recursive exec:exec -DforceStdout) -JAR_VERSION := 1.46 +JAR_VERSION := 1.47 JAR_FILE := mn2pdf-$(JAR_VERSION).jar all: target/$(JAR_FILE) diff --git a/README.adoc b/README.adoc index 1d04d029..51660686 100644 --- a/README.adoc +++ b/README.adoc @@ -17,14 +17,14 @@ You will need the Java Development Kit (JDK) version 8, Update 241 (8u241) or hi [source,sh] ---- -java -Xss5m -Xmx2048m -jar target/mn2pdf-1.46.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] +java -Xss5m -Xmx2048m -jar target/mn2pdf-1.47.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] ---- e.g. [source,sh] ---- -java -Xss5m -Xmx2048m -jar target/mn2pdf-1.46.jar --xml-file tests/G.191.xml --xsl-file tests/itu.recommendation.xsl --pdf-file tests/G.191.pdf +java -Xss5m -Xmx2048m -jar target/mn2pdf-1.47.jar --xml-file tests/G.191.xml --xsl-file tests/itu.recommendation.xsl --pdf-file tests/G.191.pdf ---- === PDF encryption features @@ -100,7 +100,7 @@ Update version in `pom.xml`, e.g.: ---- org.metanorma.fop mn2pdf -1.46 +1.47 Metanorma XML to PDF converter ---- @@ -111,8 +111,8 @@ Tag the same version in Git: [source,xml] ---- -git tag v1.46 -git push origin v1.46 +git tag v1.47 +git push origin v1.47 ---- Then the corresponding GitHub release will be automatically created at: diff --git a/pom.xml b/pom.xml index 9ad7f197..6c8c4a4f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.metanorma.fop mn2pdf - 1.46 + 1.47 Metanorma XML to PDF converter jar https://www.metanorma.org diff --git a/src/main/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java b/src/main/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java new file mode 100644 index 00000000..eb478e41 --- /dev/null +++ b/src/main/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java @@ -0,0 +1,158 @@ +/* + * 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.fop.complexscripts.scripts; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.util.CharAssociation; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.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 { + + /** features to use for substitutions */ + private static final String[] GSUB_FEATURES = + { + "ccmp", // glyph composition/decomposition + "liga", // common ligatures + "locl" // localized forms + }; + + /** features to use for positioning */ + private static final String[] GPOS_FEATURES = + { + "kern", // kerning + "mark", // mark to base or ligature positioning + "mkmk" // mark to mark positioning + }; + + DefaultScriptProcessor(String script) { + super(script); + } + + @Override + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures() { + return GSUB_FEATURES; + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public String[] getPositioningFeatures() { + return GPOS_FEATURES; + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public GlyphSequence reorderCombiningMarks(GlyphDefinitionTable gdef, GlyphSequence gs, int[] unscaledWidths, int[][] gpa, String script, String language) { + 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) && (unscaledWidths[index] != 0); + return gdef.isGlyphClass(glyphs[index], GlyphDefinitionTable.GLYPH_CLASS_MARK); + } + +} diff --git a/src/main/java/org/apache/fop/complexscripts/scripts/HebrewScriptProcessor.java b/src/main/java/org/apache/fop/complexscripts/scripts/HebrewScriptProcessor.java new file mode 100644 index 00000000..d772fe33 --- /dev/null +++ b/src/main/java/org/apache/fop/complexscripts/scripts/HebrewScriptProcessor.java @@ -0,0 +1,31 @@ +/* + * 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.fop.complexscripts.scripts; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; + +public class HebrewScriptProcessor extends DefaultScriptProcessor { + HebrewScriptProcessor(String script) { + super(script); + } + + protected boolean isReorderedMark(GlyphDefinitionTable gdef, int[] glyphs, int[] unscaledWidths, int index) { + return gdef.isGlyphClass(glyphs[index], GlyphDefinitionTable.GLYPH_CLASS_MARK) && (unscaledWidths[index] != 0); + } +} diff --git a/src/main/java/org/apache/fop/complexscripts/scripts/ScriptProcessor.java b/src/main/java/org/apache/fop/complexscripts/scripts/ScriptProcessor.java new file mode 100644 index 00000000..7e761fbb --- /dev/null +++ b/src/main/java/org/apache/fop/complexscripts/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.fop.complexscripts.scripts; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; +import org.apache.fop.complexscripts.fonts.GlyphTable; +import org.apache.fop.complexscripts.util.CharScript; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; +import org.apache.fop.fonts.MultiByteFont; + +// 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; + + /** + * 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(); + + /** + * 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, + Map> lookups) { + return substitute(gs, script, language, assembleLookups(gsub, getSubstitutionFeatures(), 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, GlyphTable.UseSpec[] usa, ScriptContextTester sct) { + assert usa != null; + for (GlyphTable.UseSpec us : usa) { + 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) { + return gs; + } + + /** + * Obtain script specific required positioning features. + * @return array of suppported positioning features or null + */ + public abstract String[] getPositioningFeatures(); + + /** + * 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, int fontSize, + Map> lookups, int[] widths, int[][] adjustments) { + return position(gs, script, language, fontSize, assembleLookups(gpos, getPositioningFeatures(), 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, GlyphTable.UseSpec[] usa, int[] widths, int[][] adjustments, ScriptContextTester sct) { + assert usa != null; + boolean adjusted = false; + for (GlyphTable.UseSpec us : usa) { + 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 GlyphTable.UseSpec[] assembleLookups(GlyphTable table, String[] features, + Map> lookups) { + AssembledLookupsKey key = new AssembledLookupsKey(table, features, lookups); + GlyphTable.UseSpec[] usa; + if ((usa = assembledLookupsGet(key)) != null) { + return usa; + } else { + return assembledLookupsPut(key, table.assembleLookups(features, lookups)); + } + } + + private GlyphTable.UseSpec[] assembledLookupsGet(AssembledLookupsKey key) { + return assembledLookups.get(key); + } + + private GlyphTable.UseSpec[] assembledLookupsPut(AssembledLookupsKey key, GlyphTable.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, Map processors) { + 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 if (sc == CharScript.SCRIPT_THAI) { + sp = new ThaiScriptProcessor(script); + } else if (sc == CharScript.SCRIPT_HEBREW) { + sp = new HebrewScriptProcessor(script); + } else { + sp = new DefaultScriptProcessor(script); + } + return sp; + } + + private static class AssembledLookupsKey { + + private final GlyphTable table; + private final String[] features; + private final Map> lookups; + + AssembledLookupsKey(GlyphTable table, String[] features, Map> lookups) { + this.table = table; + this.features = features; + this.lookups = lookups; + } + + /** {@inheritDoc} */ + 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} */ + 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; + } + } + + } + + public CharSequence preProcess(CharSequence charSequence, MultiByteFont font, List associations) { + return charSequence; + } +} diff --git a/src/main/java/org/apache/fop/complexscripts/scripts/ThaiScriptProcessor.java b/src/main/java/org/apache/fop/complexscripts/scripts/ThaiScriptProcessor.java new file mode 100644 index 00000000..c681861e --- /dev/null +++ b/src/main/java/org/apache/fop/complexscripts/scripts/ThaiScriptProcessor.java @@ -0,0 +1,31 @@ +/* + * 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.fop.complexscripts.scripts; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; + +public class ThaiScriptProcessor extends DefaultScriptProcessor { + ThaiScriptProcessor(String script) { + super(script); + } + + protected boolean isReorderedMark(GlyphDefinitionTable gdef, int[] glyphs, int[] unscaledWidths, int index) { + return gdef.isGlyphClass(glyphs[index], GlyphDefinitionTable.GLYPH_CLASS_MARK); + } +}