You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@xmlgraphics.apache.org by ss...@apache.org on 2026/09/25 09:28:43 UTC
(xmlgraphics-fop) branch main updated: FOP-3337: Add AFP truetype complex script support
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/xmlgraphics-fop.git
The following commit(s) were added to refs/heads/main by this push:
new feb2323ca FOP-3337: Add AFP truetype complex script support
feb2323ca is described below
commit feb2323ca7ab8ef67f696f3204721624c480b731
Author: Simon Steiner <ss...@apache.org>
AuthorDate: Fri Sep 25 10:28:28 2026 +0100
FOP-3337: Add AFP truetype complex script support
---
.../org/apache/fop/afp/AFPResourceManager.java | 68 ++-----
.../java/org/apache/fop/afp/AFPTrailerFont.java | 149 ++++++++++++++
.../java/org/apache/fop/fonts/MultiByteFont.java | 4 +
.../org/apache/fop/fonts/truetype/CmapWriter.java | 223 +++++++++++++++++++++
.../apache/fop/render/afp/AFPDocumentHandler.java | 2 +-
.../org/apache/fop/render/afp/AFPFontConfig.java | 41 +++-
.../org/apache/fop/afp/AFPTrailerFontTestCase.java | 104 ++++++++++
.../fop/fonts/truetype/CmapWriterTestCase.java | 81 ++++++++
.../apache/fop/render/afp/AFPTrueTypeTestCase.java | 53 ++++-
9 files changed, 667 insertions(+), 58 deletions(-)
diff --git a/fop-core/src/main/java/org/apache/fop/afp/AFPResourceManager.java b/fop-core/src/main/java/org/apache/fop/afp/AFPResourceManager.java
index f48e7223f..cc638dc14 100644
--- a/fop-core/src/main/java/org/apache/fop/afp/AFPResourceManager.java
+++ b/fop-core/src/main/java/org/apache/fop/afp/AFPResourceManager.java
@@ -39,19 +39,16 @@ import org.apache.fop.afp.fonts.AFPFont;
import org.apache.fop.afp.fonts.CharacterSet;
import org.apache.fop.afp.modca.AbstractNamedAFPObject;
import org.apache.fop.afp.modca.AbstractPageObject;
-import org.apache.fop.afp.modca.ActiveEnvironmentGroup;
import org.apache.fop.afp.modca.IncludeObject;
import org.apache.fop.afp.modca.IncludedResourceObject;
-import org.apache.fop.afp.modca.ObjectContainer;
import org.apache.fop.afp.modca.PageSegment;
import org.apache.fop.afp.modca.Registry;
import org.apache.fop.afp.modca.ResourceGroup;
import org.apache.fop.afp.modca.ResourceObject;
-import org.apache.fop.afp.modca.triplets.EncodingTriplet;
-import org.apache.fop.afp.modca.triplets.FullyQualifiedNameTriplet;
import org.apache.fop.afp.util.AFPResourceAccessor;
import org.apache.fop.afp.util.AFPResourceUtil;
import org.apache.fop.apps.io.InternalResourceResolver;
+import org.apache.fop.fonts.CMapSegment;
import org.apache.fop.fonts.FontType;
import org.apache.fop.render.afp.AFPFontConfig;
@@ -84,6 +81,8 @@ public class AFPResourceManager {
protected boolean includeCached = true;
+ private Map<AFPResourceInfo, AFPTrailerFont> trailerFonts = new HashMap<>();
+
/**
* Main constructor
*
@@ -125,6 +124,9 @@ public class AFPResourceManager {
* @throws IOException thrown if an I/O exception of some sort has occurred.
*/
public void writeToStream() throws IOException {
+ for (AFPTrailerFont font : trailerFonts.values()) {
+ font.build();
+ }
streamer.close();
}
@@ -348,11 +350,12 @@ public class AFPResourceManager {
if (charSet.getResourceAccessor() != null) {
AFPResourceAccessor accessor = charSet.getResourceAccessor();
if (afpFont.getFontType() == FontType.TRUETYPE) {
-
+ AFPFontConfig.AFPTrueTypeFont ttf = (AFPFontConfig.AFPTrueTypeFont) afpFont;
+ CMapSegment[] cmap = ttf.hasPrivateUseSubstitutions() ? ttf.getCMap() : null;
createIncludedResource(afpFont.getFontName(),
- ((AFPFontConfig.AFPTrueTypeFont) afpFont).getUri(), accessor,
+ ttf.getUri(), accessor,
ResourceObject.TYPE_OBJECT_CONTAINER, true,
- ((AFPFontConfig.AFPTrueTypeFont) afpFont).getTTC());
+ ttf.getTTC(), cmap);
} else {
createIncludedResource(
charSet.getName(), accessor,
@@ -391,7 +394,7 @@ public class AFPResourceManager {
+ " (" + e.getMessage() + ")");
}
- createIncludedResource(resourceName, uri, accessor, resourceObjectType, false, null);
+ createIncludedResource(resourceName, uri, accessor, resourceObjectType, false, null, null);
}
/**
@@ -403,7 +406,8 @@ public class AFPResourceManager {
* @throws IOException if an I/O error occurs while loading the resource
*/
public void createIncludedResource(String resourceName, URI uri, AFPResourceAccessor accessor,
- byte resourceObjectType, boolean truetype, String ttc) throws IOException {
+ byte resourceObjectType, boolean truetype, String ttc,
+ CMapSegment[] cmap) throws IOException {
AFPResourceLevel resourceLevel = new AFPResourceLevel(ResourceType.PRINT_FILE);
AFPResourceInfo resourceInfo = new AFPResourceInfo();
@@ -420,29 +424,8 @@ public class AFPResourceManager {
ResourceGroup resourceGroup = streamer.getResourceGroup(resourceLevel);
if (truetype) {
- ResourceObject res = factory.createResource();
- res.setType(ResourceObject.TYPE_OBJECT_CONTAINER);
-
- ActiveEnvironmentGroup.setupTruetypeMDR(res, false);
-
- ObjectContainer oc = factory.createObjectContainer();
- InputStream is = accessor.createInputStream(uri);
-
- if (ttc != null) {
- oc.setData(extractTTC(ttc, is));
- } else {
- oc.setData(IOUtils.toByteArray(is));
- }
-
- ActiveEnvironmentGroup.setupTruetypeMDR(oc, true);
-
- res.addTriplet(new EncodingTriplet(1200));
-
- res.setFullyQualifiedName(FullyQualifiedNameTriplet.TYPE_REPLACE_FIRST_GID_NAME,
- FullyQualifiedNameTriplet.FORMAT_CHARSTR, resourceName, true);
-
- res.setDataObject(oc);
- resourceGroup.addObject(res);
+ trailerFonts.put(resourceInfo,
+ new AFPTrailerFont(accessor, uri, ttc, cmap, resourceName, resourceGroup, factory));
} else {
ResourceObject resourceObject = factory.createResource(resourceName);
IncludedResourceObject resourceContent = new IncludedResourceObject(
@@ -457,28 +440,11 @@ public class AFPResourceManager {
// record mapping of resource info to data object resource name
addToCache(resourceInfo, newcachedObject);
- } else {
- //skip, already created
+ } else if (truetype) {
+ trailerFonts.get(resourceInfo).cmap = cmap;
}
}
- private byte[] extractTTC(String ttc, InputStream is) throws IOException {
-// TrueTypeCollection trueTypeCollection = new TrueTypeCollection(is);
-// for (TrueTypeFont ttf : trueTypeCollection.getFonts()) {
-// String name = ttf.getNaming().getFontFamily();
-// if (name.equals(ttc)) {
-// ByteArrayOutputStream bos = new ByteArrayOutputStream();
-// TTFSubsetter s = new TTFSubsetter(ttf, null);
-// for (int i = 0; i < 256 * 256; i++) {
-// s.addCharCode(i);
-// }
-// s.writeToStream(bos);
-// return bos.toByteArray();
-// }
-// }
- throw new IOException(ttc + " not supported");
- }
-
/**
* Creates an included resource extracting the named resource from an external source.
* @param resourceName the name of the resource
diff --git a/fop-core/src/main/java/org/apache/fop/afp/AFPTrailerFont.java b/fop-core/src/main/java/org/apache/fop/afp/AFPTrailerFont.java
new file mode 100644
index 000000000..751da5f38
--- /dev/null
+++ b/fop-core/src/main/java/org/apache/fop/afp/AFPTrailerFont.java
@@ -0,0 +1,149 @@
+/*
+ * 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.afp;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.fontbox.ttf.TTFTable;
+import org.apache.fontbox.ttf.TrueTypeCollection;
+import org.apache.fontbox.ttf.TrueTypeFont;
+
+import org.apache.fop.afp.modca.ActiveEnvironmentGroup;
+import org.apache.fop.afp.modca.ObjectContainer;
+import org.apache.fop.afp.modca.ResourceGroup;
+import org.apache.fop.afp.modca.ResourceObject;
+import org.apache.fop.afp.modca.triplets.EncodingTriplet;
+import org.apache.fop.afp.modca.triplets.FullyQualifiedNameTriplet;
+import org.apache.fop.afp.util.AFPResourceAccessor;
+import org.apache.fop.fonts.CMapSegment;
+import org.apache.fop.fonts.truetype.CmapWriter;
+
+/**
+ * Builds an AFP object container resource holding an embedded TrueType font, written to the
+ * document trailer's resource group. The full font is read from the given URI (extracted from a
+ * TrueType Collection when a {@code ttc} name is supplied), optionally has a cmap appended, and is
+ * wrapped in the MO:DCA structures required to reference it as a TrueType font resource.
+ */
+public class AFPTrailerFont {
+ private AFPResourceAccessor accessor;
+ private URI uri;
+ private String ttc;
+ private String resourceName;
+ private ResourceGroup resourceGroup;
+ private Factory factory;
+ protected CMapSegment[] cmap;
+
+ AFPTrailerFont(AFPResourceAccessor accessor, URI uri, String ttc, CMapSegment[] cmap, String resourceName,
+ ResourceGroup resourceGroup, Factory factory) {
+ this.accessor = accessor;
+ this.uri = uri;
+ this.ttc = ttc;
+ this.cmap = cmap;
+ this.resourceName = resourceName;
+ this.resourceGroup = resourceGroup;
+ this.factory = factory;
+ }
+
+ public void build() throws IOException {
+ ResourceObject res = factory.createResource();
+ res.setType(ResourceObject.TYPE_OBJECT_CONTAINER);
+
+ ActiveEnvironmentGroup.setupTruetypeMDR(res, false);
+
+ ObjectContainer objectContainer = factory.createObjectContainer();
+ InputStream is = accessor.createInputStream(uri);
+
+ byte[] fontData;
+ if (ttc != null) {
+ fontData = extractTTC(ttc, is);
+ } else {
+ fontData = IOUtils.toByteArray(is);
+ }
+ if (cmap != null) {
+ fontData = CmapWriter.appendCmap(fontData, cmap);
+ }
+ objectContainer.setData(fontData);
+
+ ActiveEnvironmentGroup.setupTruetypeMDR(objectContainer, true);
+
+ res.addTriplet(new EncodingTriplet(1200));
+
+ res.setFullyQualifiedName(FullyQualifiedNameTriplet.TYPE_REPLACE_FIRST_GID_NAME,
+ FullyQualifiedNameTriplet.FORMAT_CHARSTR, resourceName, true);
+
+ res.setDataObject(objectContainer);
+ resourceGroup.addObject(res);
+ }
+
+ private byte[] extractTTC(String ttc, InputStream is) throws IOException {
+ TrueTypeCollection trueTypeCollection = new TrueTypeCollection(is);
+ TrueTypeFont ttf = trueTypeCollection.getFontByName(ttc);
+ return extractFullFont(ttf);
+ }
+
+ protected static byte[] extractFullFont(TrueTypeFont ttf) throws IOException {
+ byte[] source = IOUtils.toByteArray(ttf.getOriginalData());
+ List<TTFTable> tables = new ArrayList<>(ttf.getTables());
+ Collections.sort(tables, new Comparator<TTFTable>() {
+ public int compare(TTFTable table1, TTFTable table2) {
+ return table1.getTag().compareTo(table2.getTag());
+ }
+ });
+ int numTables = tables.size();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bos);
+ out.writeInt(0x00010000); // sfnt version (TrueType outlines)
+ out.writeShort(numTables);
+ int entrySelector = 0;
+ while ((1 << (entrySelector + 1)) <= numTables) {
+ entrySelector++;
+ }
+ int searchRange = (1 << entrySelector) * 16;
+ out.writeShort(searchRange);
+ out.writeShort(entrySelector);
+ out.writeShort(numTables * 16 - searchRange);
+ int offset = 12 + numTables * 16;
+ for (TTFTable table : tables) {
+ out.writeBytes(table.getTag());
+ out.writeInt((int) table.getCheckSum());
+ out.writeInt(offset);
+ out.writeInt((int) table.getLength());
+ offset += (int) ((table.getLength() + 3) & ~3L);
+ }
+ for (TTFTable table : tables) {
+ int length = (int) table.getLength();
+ out.write(source, (int) table.getOffset(), length);
+ while (length % 4 != 0) {
+ out.write(0);
+ length++;
+ }
+ }
+ out.flush();
+ return bos.toByteArray();
+ }
+}
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java b/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
index cbaace8a2..959c4c10d 100644
--- a/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
+++ b/fop-core/src/main/java/org/apache/fop/fonts/MultiByteFont.java
@@ -303,6 +303,10 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
}
}
+ public boolean hasPrivateUseSubstitutions() {
+ return numMapped > 0;
+ }
+
/**
* Returns the Unicode scalar value that corresponds to the glyph index. If more than
* one correspondence exists, then the first one is returned (ordered by bfentries[]).
diff --git a/fop-core/src/main/java/org/apache/fop/fonts/truetype/CmapWriter.java b/fop-core/src/main/java/org/apache/fop/fonts/truetype/CmapWriter.java
new file mode 100644
index 000000000..55956ec0f
--- /dev/null
+++ b/fop-core/src/main/java/org/apache/fop/fonts/truetype/CmapWriter.java
@@ -0,0 +1,223 @@
+/*
+ * 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.fonts.truetype;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.fop.fonts.CMapSegment;
+
+/**
+ * Rewrites the {@code cmap} table of an sfnt (TrueType) font so that it contains the character to
+ * glyph-index mappings held by FOP for that font, including the Private Use Area code points that
+ * {@code MultiByteFont.createPrivateUseMapping} allocates for complex-script (GSUB) output.
+ *
+ * <p>The AFP renderer writes text as UTF-16 code points and the AFP consumer resolves each code
+ * point to a glyph through the embedded font's {@code cmap}. Substituted glyphs are exposed by FOP
+ * as PUA code points which are, by construction, absent from the original font's {@code cmap};
+ * without this rewrite they would not resolve.</p>
+ *
+ * <p>The rewrite is deliberately minimal: the original font bytes are preserved, a fresh
+ * {@code cmap} table (a single Windows BMP, format 4 subtable) is appended on a 4-byte
+ * boundary, and only the {@code cmap} entry in the sfnt table directory is repointed to it. The
+ * original {@code cmap} bytes remain in the file as unreferenced dead space. The font's glyph
+ * numbering is left untouched, so the {@link CMapSegment#getGlyphStartIndex() glyph indices} line up
+ * with the embedded glyphs.</p>
+ */
+public final class CmapWriter {
+
+ private static final int SFNT_HEADER_SIZE = 12;
+ private static final int DIR_ENTRY_SIZE = 16;
+ private static final int BMP_MAX = 0xFFFF;
+ private static final long CHECKSUM_MAGIC = 0xB1B0AFBAL;
+
+ private CmapWriter() {
+ }
+
+ /**
+ * Returns a copy of the given TrueType font whose {@code cmap} table maps the supplied segments.
+ *
+ * @param font the original TrueType font bytes (must keep its original glyph numbering)
+ * @param segments the character-to-glyph mappings to encode (original mappings plus PUA additions)
+ * @return the rewritten font bytes
+ * @throws IOException if the font has no {@code cmap} or {@code head} table
+ */
+ public static byte[] appendCmap(byte[] font, CMapSegment[] segments) throws IOException {
+ int numTables = readUShort(font, 4);
+ int cmapRecordOffset = -1;
+ int headTableOffset = -1;
+ for (int i = 0; i < numTables; i++) {
+ int recordOffset = SFNT_HEADER_SIZE + i * DIR_ENTRY_SIZE;
+ String tag = readTag(font, recordOffset);
+ if ("cmap".equals(tag)) {
+ cmapRecordOffset = recordOffset;
+ } else if ("head".equals(tag)) {
+ headTableOffset = readULong(font, recordOffset + 8);
+ }
+ }
+ if (cmapRecordOffset < 0) {
+ throw new IOException("Font has no cmap table to rewrite");
+ }
+ if (headTableOffset < 0) {
+ throw new IOException("Font has no head table");
+ }
+
+ byte[] cmapTable = buildCmapTable(segments);
+
+ int appendOffset = align4(font.length);
+ int newLength = align4(appendOffset + cmapTable.length);
+ byte[] out = new byte[newLength];
+ System.arraycopy(font, 0, out, 0, font.length);
+ System.arraycopy(cmapTable, 0, out, appendOffset, cmapTable.length);
+
+ // Repoint the cmap directory entry at the appended table.
+ writeULong(out, cmapRecordOffset + 4, TTFSubSetFile.getCheckSum(out, appendOffset, cmapTable.length));
+ writeULong(out, cmapRecordOffset + 8, appendOffset);
+ writeULong(out, cmapRecordOffset + 12, cmapTable.length);
+
+ // Recompute head.checkSumAdjustment over the whole file (with the field zeroed first).
+ writeULong(out, headTableOffset + 8, 0);
+ int checkSum = TTFSubSetFile.getCheckSum(out, 0, newLength);
+ writeULong(out, headTableOffset + 8, (int) (CHECKSUM_MAGIC - (checkSum & 0xFFFFFFFFL)));
+
+ return out;
+ }
+
+ /** Builds a complete cmap table: table header + one (3,1) encoding record + a format-4 subtable. */
+ private static byte[] buildCmapTable(CMapSegment[] segments) throws IOException {
+ byte[] subtable = buildFormat4Subtable(segments);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bos);
+ out.writeShort(0); // table version
+ out.writeShort(1); // number of encoding records
+ out.writeShort(3); // platformID: Windows
+ out.writeShort(1); // encodingID: Unicode BMP
+ out.writeInt(12); // offset to subtable (4 header + 8 encoding record)
+ out.write(subtable);
+ out.flush();
+ return bos.toByteArray();
+ }
+
+ /** Builds a format-4 (segment mapping to delta values) subtable for BMP code points. */
+ private static byte[] buildFormat4Subtable(CMapSegment[] segments) throws IOException {
+ List<int[]> ranges = buildRanges(segments);
+ // Mandatory terminating segment 0xFFFF -> 0xFFFF.
+ ranges.add(new int[] {BMP_MAX, BMP_MAX, 1});
+
+ int segCount = ranges.size();
+ int segCountX2 = segCount * 2;
+ int entrySelector = 0;
+ while ((1 << (entrySelector + 1)) <= segCount) {
+ entrySelector++;
+ }
+ int searchRange = (1 << entrySelector) * 2;
+ int rangeShift = segCountX2 - searchRange;
+
+ int length = 16 + 8 * segCount; // header (14) + reservedPad (2) + 4 arrays of segCount USHORTs
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bos);
+ out.writeShort(4); // format
+ out.writeShort(length);
+ out.writeShort(0); // language
+ out.writeShort(segCountX2);
+ out.writeShort(searchRange);
+ out.writeShort(entrySelector);
+ out.writeShort(rangeShift);
+ for (int[] range : ranges) { // endCode[]
+ out.writeShort(range[1]);
+ }
+ out.writeShort(0); // reservedPad
+ for (int[] range : ranges) { // startCode[]
+ out.writeShort(range[0]);
+ }
+ for (int[] range : ranges) { // idDelta[]
+ out.writeShort(range[2]);
+ }
+ for (int i = 0; i < segCount; i++) { // idRangeOffset[] (all zero)
+ out.writeShort(0);
+ }
+ out.flush();
+ return bos.toByteArray();
+ }
+
+ /**
+ * Converts the FOP cmap segments into sorted, non-overlapping BMP ranges of
+ * {startCode, endCode, idDelta}. Supplementary-plane segments are dropped and ranges are clamped
+ * to the BMP; the terminating 0xFFFF segment is added by the caller.
+ */
+ private static List<int[]> buildRanges(CMapSegment[] segments) {
+ CMapSegment[] sorted = segments.clone();
+ Arrays.sort(sorted, new Comparator<CMapSegment>() {
+ public int compare(CMapSegment a, CMapSegment b) {
+ return Integer.compare(a.getUnicodeStart(), b.getUnicodeStart());
+ }
+ });
+ List<int[]> ranges = new ArrayList<>();
+ int lastEnd = -1;
+ for (CMapSegment segment : sorted) {
+ int start = segment.getUnicodeStart();
+ int end = segment.getUnicodeEnd();
+ if (start >= BMP_MAX || start <= lastEnd) {
+ continue; // cannot be represented in a format-4 subtable
+ }
+ if (end >= BMP_MAX) {
+ end = BMP_MAX - 1;
+ }
+ int idDelta = (segment.getGlyphStartIndex() - start) & 0xFFFF;
+ ranges.add(new int[] {start, end, idDelta});
+ lastEnd = end;
+ }
+ return ranges;
+ }
+
+ private static int align4(int value) {
+ int pad = 4 - (value % 4);
+ return pad < 4 ? value + pad : value;
+ }
+
+ private static String readTag(byte[] data, int pos) {
+ return new String(new char[] {
+ (char) (data[pos] & 0xff), (char) (data[pos + 1] & 0xff),
+ (char) (data[pos + 2] & 0xff), (char) (data[pos + 3] & 0xff),
+ });
+ }
+
+ private static int readUShort(byte[] data, int pos) {
+ return ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff);
+ }
+
+ private static int readULong(byte[] data, int pos) {
+ return ((data[pos] & 0xff) << 24) | ((data[pos + 1] & 0xff) << 16)
+ | ((data[pos + 2] & 0xff) << 8) | (data[pos + 3] & 0xff);
+ }
+
+ private static void writeULong(byte[] data, int pos, int value) {
+ data[pos] = (byte) ((value >> 24) & 0xff);
+ data[pos + 1] = (byte) ((value >> 16) & 0xff);
+ data[pos + 2] = (byte) ((value >> 8) & 0xff);
+ data[pos + 3] = (byte) (value & 0xff);
+ }
+}
diff --git a/fop-core/src/main/java/org/apache/fop/render/afp/AFPDocumentHandler.java b/fop-core/src/main/java/org/apache/fop/render/afp/AFPDocumentHandler.java
index 0e13d8c8a..bcd213218 100644
--- a/fop-core/src/main/java/org/apache/fop/render/afp/AFPDocumentHandler.java
+++ b/fop-core/src/main/java/org/apache/fop/render/afp/AFPDocumentHandler.java
@@ -403,7 +403,7 @@ public class AFPDocumentHandler extends AbstractBinaryWritingIFDocumentHandler
try {
getResourceManager().createIncludedResource(formMap.getName(),
formMap.getSrc(), accessor,
- ResourceObject.TYPE_FORMDEF, false, null);
+ ResourceObject.TYPE_FORMDEF, false, null, null);
} catch (IOException ioe) {
throw new IFException(
"I/O error while embedding form map resource: " + formMap.getName(), ioe);
diff --git a/fop-core/src/main/java/org/apache/fop/render/afp/AFPFontConfig.java b/fop-core/src/main/java/org/apache/fop/render/afp/AFPFontConfig.java
index 5230a5c0b..311ba5230 100644
--- a/fop-core/src/main/java/org/apache/fop/render/afp/AFPFontConfig.java
+++ b/fop-core/src/main/java/org/apache/fop/render/afp/AFPFontConfig.java
@@ -42,9 +42,12 @@ import org.apache.fop.afp.util.AFPResourceAccessor;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.io.InternalResourceResolver;
+import org.apache.fop.complexscripts.fonts.Substitutable;
import org.apache.fop.configuration.Configuration;
import org.apache.fop.configuration.ConfigurationException;
import org.apache.fop.events.EventProducer;
+import org.apache.fop.fonts.CMapSegment;
+import org.apache.fop.fonts.CustomFont;
import org.apache.fop.fonts.EmbedFontInfo;
import org.apache.fop.fonts.EmbeddingMode;
import org.apache.fop.fonts.EncodingMode;
@@ -57,6 +60,7 @@ import org.apache.fop.fonts.FontType;
import org.apache.fop.fonts.FontUris;
import org.apache.fop.fonts.FontUtil;
import org.apache.fop.fonts.LazyFont;
+import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.fonts.Typeface;
/**
@@ -388,12 +392,14 @@ public final class AFPFontConfig implements FontConfig {
FontUris fontUris = new FontUris(new URI(fontUri), null);
EmbedFontInfo embedFontInfo = new EmbedFontInfo(fontUris, false, true, null, subfont, EncodingMode.AUTO,
EmbeddingMode.FULL, false, false, true, false);
- Typeface tf = new LazyFont(embedFontInfo, resourceResolver, false).getRealFont();
+ LazyFont lazyFont = new LazyFont(embedFontInfo, resourceResolver,
+ userAgent.isComplexScriptFeaturesEnabled());
+ Typeface typeface = lazyFont.getRealFont();
AFPResourceAccessor accessor = getAccessor(resourceResolver);
CharacterSet characterSet = userAgent.getDoubleByteCharacterSetBuilder().build(characterset,
- super.codePage, super.encoding, tf, accessor, eventProducer);
+ super.codePage, super.encoding, typeface, accessor, eventProducer);
OutlineFont font = new AFPTrueTypeFont(super.name, super.embeddable, characterSet,
- eventProducer, subfont, new URI(fontUri), positionByChar);
+ eventProducer, subfont, new URI(fontUri), positionByChar, lazyFont);
return getFontInfo(font, this);
} catch (URISyntaxException e) {
throw new IOException(e);
@@ -401,16 +407,18 @@ public final class AFPFontConfig implements FontConfig {
}
}
- public static class AFPTrueTypeFont extends OutlineFont {
+ public static class AFPTrueTypeFont extends OutlineFont implements Substitutable {
private String ttc;
private URI uri;
private boolean positionByChar;
+ private LazyFont lazyFont;
public AFPTrueTypeFont(String name, boolean embeddable, CharacterSet charSet, AFPEventProducer eventProducer,
- String ttc, URI uri, boolean positionByChar) {
+ String ttc, URI uri, boolean positionByChar, LazyFont lazyFont) {
super(name, embeddable, charSet, eventProducer);
this.ttc = ttc;
this.uri = uri;
this.positionByChar = positionByChar;
+ this.lazyFont = lazyFont;
}
public FontType getFontType() {
@@ -428,6 +436,29 @@ public final class AFPFontConfig implements FontConfig {
public boolean isPositionByChar() {
return positionByChar;
}
+
+ public CMapSegment[] getCMap() {
+ return ((CustomFont) lazyFont.getRealFont()).getCMap();
+ }
+
+ public boolean hasPrivateUseSubstitutions() {
+ Typeface realFont = lazyFont.getRealFont();
+ return realFont instanceof MultiByteFont && ((MultiByteFont) realFont).hasPrivateUseSubstitutions();
+ }
+
+ public boolean performsSubstitution() {
+ return lazyFont.performsSubstitution();
+ }
+
+ public CharSequence performSubstitution(CharSequence cs, String script, String language, List associations,
+ boolean retainControls) {
+ return lazyFont.performSubstitution(cs, script, language, associations, retainControls);
+ }
+
+ public CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa, String script, String language,
+ List associations) {
+ return lazyFont.reorderCombiningMarks(cs, gpa, script, language, associations);
+ }
}
static final class OutlineFontConfig extends AFPFontConfigData {
diff --git a/fop-core/src/test/java/org/apache/fop/afp/AFPTrailerFontTestCase.java b/fop-core/src/test/java/org/apache/fop/afp/AFPTrailerFontTestCase.java
new file mode 100644
index 000000000..29f925615
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/afp/AFPTrailerFontTestCase.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.
+ */
+
+/* $Id$ */
+
+package org.apache.fop.afp;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.fontbox.ttf.CmapSubtable;
+import org.apache.fontbox.ttf.TTFParser;
+import org.apache.fontbox.ttf.TrueTypeCollection;
+import org.apache.fontbox.ttf.TrueTypeFont;
+import org.apache.pdfbox.io.RandomAccessReadBuffer;
+
+import org.apache.fop.fonts.CMapSegment;
+import org.apache.fop.fonts.truetype.CmapWriter;
+
+public class AFPTrailerFontTestCase {
+ private static final String FONT = "test/resources/fonts/ttf/DejaVuLGCSerif.ttf";
+
+ @Test
+ public void testExtractFullFontPreservesGlyphs() throws Exception {
+ TrueTypeFont original = new TTFParser().parse(new RandomAccessReadBuffer(new FileInputStream(FONT)));
+ TrueTypeFont extracted = extractFromTTC(wrapInTTC(new File(FONT)));
+ Assert.assertEquals(original.getNumberOfGlyphs(), extracted.getNumberOfGlyphs());
+ Assert.assertEquals(original.getTables().size(), extracted.getTables().size());
+ Assert.assertEquals(original.getPath("A").getBounds(), extracted.getPath("A").getBounds());
+ }
+
+ @Test
+ public void testAppendedCmapResolvesPuaToOriginalGlyph() throws Exception {
+ int gidOfA = new TTFParser().parse(new RandomAccessReadBuffer(new FileInputStream(FONT)))
+ .getCmap().getSubtable(3, 1).getGlyphId('A');
+ byte[] fontData = AFPTrailerFont.extractFullFont(extractFromTTC(wrapInTTC(new File(FONT))));
+ fontData = CmapWriter.appendCmap(fontData, new CMapSegment[] {new CMapSegment(0xE000, 0xE000, gidOfA)});
+ TrueTypeFont withPua = new TTFParser().parse(new RandomAccessReadBuffer(new ByteArrayInputStream(fontData)));
+ CmapSubtable subtable = withPua.getCmap().getSubtable(3, 1);
+ Assert.assertEquals(gidOfA, subtable.getGlyphId(0xE000));
+ }
+
+ private TrueTypeFont extractFromTTC(byte[] ttc) throws Exception {
+ TrueTypeCollection collection = new TrueTypeCollection(new ByteArrayInputStream(ttc));
+ TrueTypeFont font = collection.getFontByName("DejaVuLGCSerif");
+ return new TTFParser().parse(new RandomAccessReadBuffer(new ByteArrayInputStream(
+ AFPTrailerFont.extractFullFont(font))));
+ }
+
+ private byte[] wrapInTTC(File ttf) throws Exception {
+ byte[] sfnt = IOUtils.toByteArray(ttf.toURI().toURL());
+ int header = 16;
+ byte[] ttc = new byte[header + sfnt.length];
+ ttc[0] = 't';
+ ttc[1] = 't';
+ ttc[2] = 'c';
+ ttc[3] = 'f';
+ writeUInt32(ttc, 4, 0x00010000); // version 1.0
+ writeUInt32(ttc, 8, 1); // numFonts
+ writeUInt32(ttc, 12, header); // offset to font 0 directory
+ System.arraycopy(sfnt, 0, ttc, header, sfnt.length);
+ int numTables = readUInt16(ttc, header + 4);
+ for (int i = 0; i < numTables; i++) {
+ int offsetField = header + 12 + i * 16 + 8;
+ writeUInt32(ttc, offsetField, readUInt32(ttc, offsetField) + header);
+ }
+ return ttc;
+ }
+
+ private static int readUInt16(byte[] data, int pos) {
+ return ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff);
+ }
+
+ private static int readUInt32(byte[] data, int pos) {
+ return ((data[pos] & 0xff) << 24) | ((data[pos + 1] & 0xff) << 16)
+ | ((data[pos + 2] & 0xff) << 8) | (data[pos + 3] & 0xff);
+ }
+
+ private static void writeUInt32(byte[] data, int pos, int value) {
+ data[pos] = (byte) ((value >> 24) & 0xff);
+ data[pos + 1] = (byte) ((value >> 16) & 0xff);
+ data[pos + 2] = (byte) ((value >> 8) & 0xff);
+ data[pos + 3] = (byte) (value & 0xff);
+ }
+}
diff --git a/fop-core/src/test/java/org/apache/fop/fonts/truetype/CmapWriterTestCase.java b/fop-core/src/test/java/org/apache/fop/fonts/truetype/CmapWriterTestCase.java
new file mode 100644
index 000000000..8d6d54316
--- /dev/null
+++ b/fop-core/src/test/java/org/apache/fop/fonts/truetype/CmapWriterTestCase.java
@@ -0,0 +1,81 @@
+/*
+ * 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.fonts.truetype;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.commons.io.IOUtils;
+
+import org.apache.fop.fonts.CMapSegment;
+
+public class CmapWriterTestCase {
+
+ private static final String FONT = "test/resources/fonts/ttf/DejaVuLGCSerif.ttf";
+
+ @Test
+ public void testAppendCmapResolvesPrivateUseMappings() throws IOException {
+ byte[] original = readFont();
+ List<CMapSegment> originalCmap = loadCmap(original);
+ int gidForA = lookup(originalCmap, 'A');
+ int gidForB = lookup(originalCmap, 'B');
+ assertTrue("test font must map 'A'", gidForA > 0);
+ assertTrue("test font must map 'B'", gidForB > 0);
+ List<CMapSegment> augmented = new ArrayList<>(originalCmap);
+ augmented.add(new CMapSegment(0xE000, 0xE000, gidForA));
+ augmented.add(new CMapSegment(0xE001, 0xE001, gidForB));
+ byte[] rewritten = CmapWriter.appendCmap(original, augmented.toArray(new CMapSegment[0]));
+ List<CMapSegment> rewrittenCmap = loadCmap(rewritten);
+ assertEquals(gidForA, lookup(rewrittenCmap, 0xE000));
+ assertEquals(gidForB, lookup(rewrittenCmap, 0xE001));
+ assertEquals(gidForA, lookup(rewrittenCmap, 'A'));
+ assertEquals(gidForB, lookup(rewrittenCmap, 'B'));
+ }
+
+ private byte[] readFont() throws IOException {
+ try (InputStream is = new FileInputStream(FONT)) {
+ return IOUtils.toByteArray(is);
+ }
+ }
+
+ private List<CMapSegment> loadCmap(byte[] font) throws IOException {
+ FontFileReader reader = new FontFileReader(new ByteArrayInputStream(font));
+ TTFFile ttf = new TTFFile();
+ ttf.readFont(reader, OFFontLoader.readHeader(reader));
+ return ttf.getCMaps();
+ }
+
+ private int lookup(List<CMapSegment> cmap, int codePoint) {
+ for (CMapSegment segment : cmap) {
+ if (codePoint >= segment.getUnicodeStart() && codePoint <= segment.getUnicodeEnd()) {
+ return segment.getGlyphStartIndex() + (codePoint - segment.getUnicodeStart());
+ }
+ }
+ return 0;
+ }
+}
diff --git a/fop-core/src/test/java/org/apache/fop/render/afp/AFPTrueTypeTestCase.java b/fop-core/src/test/java/org/apache/fop/render/afp/AFPTrueTypeTestCase.java
index 1575f03b3..b7c16099d 100644
--- a/fop-core/src/test/java/org/apache/fop/render/afp/AFPTrueTypeTestCase.java
+++ b/fop-core/src/test/java/org/apache/fop/render/afp/AFPTrueTypeTestCase.java
@@ -27,6 +27,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
@@ -37,6 +39,10 @@ import javax.xml.transform.stream.StreamSource;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -57,10 +63,13 @@ import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopConfParser;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.FopFactoryBuilder;
+import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.apps.io.ResourceResolverFactory;
+import org.apache.fop.complexscripts.fonts.ttx.TTXFile;
import org.apache.fop.fonts.EmbeddingMode;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.FontTriplet;
+import org.apache.fop.fonts.LazyFont;
import org.apache.fop.fonts.MultiByteFont;
import org.apache.fop.render.intermediate.IFException;
@@ -264,6 +273,48 @@ public class AFPTrueTypeTestCase {
Assert.assertEquals(bis.readShort(), 5);
}
+ @Test
+ public void testArabicComplexScriptToAFP() {
+ TTXFile ttx = TTXFile.getFromCache("test/resources/complexscripts/arab/ttx/arab-002.ttx");
+ final MyMultiByteFont multiByteFont = new MyMultiByteFont(null, EmbeddingMode.AUTO);
+ multiByteFont.setGSUB(ttx.getGSUB());
+ for (int cp = 0x20; cp <= 0x06FF; cp++) {
+ int gi = ttx.mapCharToGlyph(cp);
+ if (gi > 0) {
+ multiByteFont.callAddPrivateUseMapping(cp, gi);
+ }
+ }
+ multiByteFont.setWidthArray(ttx.getWidths());
+ LazyFont lazyFont = mock(LazyFont.class);
+ when(lazyFont.performsSubstitution()).thenReturn(multiByteFont.performsSubstitution());
+ when(lazyFont.performSubstitution(any(CharSequence.class), any(String.class), any(String.class),
+ any(List.class), anyBoolean())).thenAnswer(
+ new Answer<CharSequence>() {
+ public CharSequence answer(InvocationOnMock inv) {
+ Object[] a = inv.getArguments();
+ return multiByteFont.performSubstitution((CharSequence) a[0], (String) a[1],
+ (String) a[2], (List) a[3], (Boolean) a[4]);
+ }
+ });
+ FopCharacterSet characterSet = new FopCharacterSet("", "UTF-16BE", "", multiByteFont, null, null);
+ AFPFontConfig.AFPTrueTypeFont afpFont = new AFPFontConfig.AFPTrueTypeFont("", true,
+ characterSet, null, null, null, false, lazyFont);
+ String arabic = "\u0645\u0643\u062A\u0628";
+ CharSequence shaped = afpFont.performSubstitution(arabic, "arab", "dflt", new ArrayList(), false);
+ Assert.assertTrue(multiByteFont.hasPrivateUseSubstitutions());
+ Assert.assertEquals("\uE000\uE001\uE002\uE003", shaped.toString());
+ }
+
+ static class MyMultiByteFont extends MultiByteFont {
+ MyMultiByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) {
+ super(resourceResolver, embeddingMode);
+ }
+
+ void callAddPrivateUseMapping(int pu, int gi) {
+ addPrivateUseMapping(pu, gi);
+ }
+ }
+
private String getAFP(String fo) throws Exception {
ByteArrayOutputStream bos = getAFPBytes(fo);
StringBuilder sb = new StringBuilder();
@@ -410,7 +461,7 @@ public class AFPTrueTypeTestCase {
}
font.setWidthArray(widths);
f.addMetrics("any", new AFPFontConfig.AFPTrueTypeFont("", true,
- new FopCharacterSet("", "UTF-16BE", "", font, null, null), null, null, null, positionByChar));
+ new FopCharacterSet("", "UTF-16BE", "", font, null, null), null, null, null, positionByChar, null));
return f;
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@xmlgraphics.apache.org
For additional commands, e-mail: commits-help@xmlgraphics.apache.org