From a4e5e6be6295b2b40232777798e4286b2216cd7c Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 19 Nov 2024 21:58:49 +0300 Subject: [PATCH 1/5] initial update for PDFBox 3.0.3, #311 --- pom.xml | 6 +- .../annotation/PDAnnotationMarkup.java | 629 +----------------- .../PDFileAttachmentAppearanceHandler.java | 26 +- .../metanorma/fop/annotations/Annotation.java | 99 +-- .../annotations/FileAttachmentAnnotation.java | 14 +- .../java/org/metanorma/fop/mn2pdfTests.java | 28 +- 6 files changed, 99 insertions(+), 703 deletions(-) diff --git a/pom.xml b/pom.xml index 9cec266..4b013c1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.metanorma.fop mn2pdf - 2.10 + 2.11 Metanorma XML to PDF converter jar https://www.metanorma.org @@ -160,7 +160,7 @@ org.apache.pdfbox fontbox - 2.0.27 + 3.0.3 @@ -249,7 +249,7 @@ org.apache.pdfbox pdfbox - 2.0.27 + 3.0.3 diff --git a/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java b/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java index 6ff4e34..040af1e 100644 --- a/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java +++ b/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java @@ -18,28 +18,11 @@ import java.io.IOException; import java.util.Calendar; - -import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSFloat; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.color.PDColor; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDCaretAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDFileAttachmentAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDFreeTextAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDInkAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDPolygonAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDPolylineAppearanceHandler; -import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDSoundAppearanceHandler; - -// needed for the javadoc generation -import org.apache.pdfbox.pdmodel.interactive.form.PDVariableText; /** * This class represents the additional fields of a Markup type Annotation. See section 12.5.6 of ISO32000-1:2008 @@ -49,53 +32,6 @@ */ public class PDAnnotationMarkup extends PDAnnotation { - - private PDAppearanceHandler customAppearanceHandler; - - /** - * Constant for a FreeText type of annotation. - */ - public static final String SUB_TYPE_FREETEXT = "FreeText"; - /** - * Constant for an Polygon type of annotation. - */ - public static final String SUB_TYPE_POLYGON = "Polygon"; - /** - * Constant for an PolyLine type of annotation. - */ - public static final String SUB_TYPE_POLYLINE = "PolyLine"; - /** - * Constant for an Caret type of annotation. - */ - public static final String SUB_TYPE_CARET = "Caret"; - /** - * Constant for an Ink type of annotation. - */ - public static final String SUB_TYPE_INK = "Ink"; - /** - * Constant for an Sound type of annotation. - */ - public static final String SUB_TYPE_SOUND = "Sound"; - - /* - * The various values of the free text annotation as defined in the PDF 1.7 reference Table 170 - */ - - /** - * A plain free-text annotation, also known as a text box comment. - */ - public static final String IT_FREE_TEXT = "FreeText"; - - /** - * A callout, associated with an area on the page through the callout line specified. - */ - public static final String IT_FREE_TEXT_CALLOUT = "FreeTextCallout"; - - /** - * The annotation is intended to function as a click-to-type or typewriter object. - */ - public static final String IT_FREE_TEXT_TYPE_WRITER = "FreeTextTypeWriter"; - /* * The various values of the reply type as defined in the PDF 1.7 reference Table 170 */ @@ -156,15 +92,8 @@ public void setTitlePopup(String t) */ public PDAnnotationPopup getPopup() { - COSDictionary popup = (COSDictionary) getCOSObject().getDictionaryObject("Popup"); - if (popup != null) - { - return new PDAnnotationPopup(popup); - } - else - { - return null; - } + COSDictionary popup = getCOSObject().getCOSDictionary(COSName.POPUP); + return popup != null ? new PDAnnotationPopup(popup) : null; } /** @@ -174,7 +103,7 @@ public PDAnnotationPopup getPopup() */ public void setPopup(PDAnnotationPopup popup) { - getCOSObject().setItem("Popup", popup); + getCOSObject().setItem(COSName.POPUP, popup); } /** @@ -233,9 +162,8 @@ public void setRichContents(String rc) * This will retrieve the date and time the annotation was created. * * @return the creation date/time. - * @throws IOException if there is a format problem when converting the date. */ - public Calendar getCreationDate() throws IOException + public Calendar getCreationDate() { return getCOSObject().getDate(COSName.CREATION_DATE); } @@ -259,12 +187,8 @@ public void setCreationDate(Calendar creationDate) */ public PDAnnotation getInReplyTo() throws IOException { - COSBase base = getCOSObject().getDictionaryObject("IRT"); - if (base instanceof COSDictionary) - { - return PDAnnotation.createAnnotation(base); - } - return null; + COSDictionary base = getCOSObject().getCOSDictionary(COSName.IRT); + return base != null ? PDAnnotation.createAnnotation(base) : null; } /** @@ -275,7 +199,7 @@ public PDAnnotation getInReplyTo() throws IOException */ public void setInReplyTo(PDAnnotation irt) { - getCOSObject().setItem("IRT", irt); + getCOSObject().setItem(COSName.IRT, irt); } /** @@ -306,7 +230,7 @@ public void setSubject(String subj) */ public String getReplyType() { - return getCOSObject().getNameAsString("RT", RT_REPLY); + return getCOSObject().getNameAsString(COSName.RT, RT_REPLY); } /** @@ -317,7 +241,7 @@ public String getReplyType() */ public void setReplyType(String rt) { - getCOSObject().setName("RT", rt); + getCOSObject().setName(COSName.RT, rt); } /** @@ -349,12 +273,8 @@ public void setIntent(String it) */ public PDExternalDataDictionary getExternalData() { - COSBase exData = this.getCOSObject().getDictionaryObject("ExData"); - if (exData instanceof COSDictionary) - { - return new PDExternalDataDictionary((COSDictionary) exData); - } - return null; + COSDictionary exData = getCOSObject().getCOSDictionary(COSName.EX_DATA); + return exData != null ? new PDExternalDataDictionary(exData) : null; } /** @@ -364,7 +284,7 @@ public PDExternalDataDictionary getExternalData() */ public void setExternalData(PDExternalDataDictionary externalData) { - this.getCOSObject().setItem("ExData", externalData); + this.getCOSObject().setItem(COSName.EX_DATA, externalData); } /** @@ -385,528 +305,7 @@ public void setBorderStyle(PDBorderStyleDictionary bs) */ public PDBorderStyleDictionary getBorderStyle() { - COSBase bs = getCOSObject().getDictionaryObject(COSName.BS); - if (bs instanceof COSDictionary) - { - return new PDBorderStyleDictionary((COSDictionary) bs); - } - return null; - } - - /** - * This will set the line ending style. - * - * @param style The new style. - */ - public final void setLineEndingStyle(String style) - { - getCOSObject().setName(COSName.LE, style); - } - - /** - * This will retrieve the line ending style. - * - * @return The line ending style, possible values shown in the LE_ constants section, LE_NONE if - * missing, never null. - */ - public String getLineEndingStyle() - { - return getCOSObject().getNameAsString(COSName.LE, PDAnnotationLine.LE_NONE); - } - - // PDF 32000 specification has "the interior color with which to fill the annotation’s line endings" - // but it is the inside of the polygon. - - /** - * This will set interior color. - * - * @param ic color. - */ - public void setInteriorColor(PDColor ic) - { - getCOSObject().setItem(COSName.IC, ic.toCOSArray()); - } - - /** - * This will retrieve the interior color. - * - * @return object representing the color. - */ - public PDColor getInteriorColor() - { - return getColor(COSName.IC); - } - - /** - * This will set the border effect dictionary, specifying effects to be applied when drawing the - * line. This is supported by PDF 1.5 and higher. - * - * @param be The border effect dictionary to set. - * - */ - public void setBorderEffect(PDBorderEffectDictionary be) - { - getCOSObject().setItem(COSName.BE, be); - } - - /** - * This will retrieve the border effect dictionary, specifying effects to be applied used in - * drawing the line. - * - * @return The border effect dictionary - */ - public PDBorderEffectDictionary getBorderEffect() - { - COSDictionary be = (COSDictionary) getCOSObject().getDictionaryObject(COSName.BE); - if (be != null) - { - return new PDBorderEffectDictionary(be); - } - else - { - return null; - } - } - - /** - * Sets the paths that make this annotation. - * - * @param inkList An array of arrays, each representing a stroked path. Each array shall be a - * series of alternating horizontal and vertical coordinates. If the parameter is null the entry - * will be removed. - */ - public void setInkList(float[][] inkList) - { - if (inkList == null) - { - getCOSObject().removeItem(COSName.INKLIST); - return; - } - COSArray array = new COSArray(); - for (float[] path : inkList) - { - COSArray innerArray = new COSArray(); - innerArray.setFloatArray(path); - array.add(innerArray); - } - getCOSObject().setItem(COSName.INKLIST, array); - } - - /** - * Get one or more disjoint paths that make this annotation. - * - * @return An array of arrays, each representing a stroked path. Each array shall be a series of - * alternating horizontal and vertical coordinates. - */ - public float[][] getInkList() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.INKLIST); - if (base instanceof COSArray) - { - COSArray array = (COSArray) base; - float[][] inkList = new float[array.size()][]; - for (int i = 0; i < array.size(); ++i) - { - COSBase base2 = array.getObject(i); - if (base2 instanceof COSArray) - { - inkList[i] = ((COSArray) base2).toFloatArray(); - } - else - { - inkList[i] = new float[0]; - } - } - return inkList; - } - return new float[0][0]; - } - - /** - * Get the default appearance. - * - * @return a string describing the default appearance. - */ - public String getDefaultAppearance() - { - return getCOSObject().getString(COSName.DA); - } - - /** - * Set the default appearance. - * - * @param daValue a string describing the default appearance. - */ - public void setDefaultAppearance(String daValue) - { - getCOSObject().setString(COSName.DA, daValue); - } - - /** - * Get the default style string. - * - * The default style string defines the default style for rich text fields. - * - * @return the DS element of the dictionary object - */ - public String getDefaultStyleString() - { - return getCOSObject().getString(COSName.DS); - } - - /** - * Set the default style string. - * - * Providing null as the value will remove the default style string. - * - * @param defaultStyleString a string describing the default style. - */ - public void setDefaultStyleString(String defaultStyleString) - { - getCOSObject().setString(COSName.DS, defaultStyleString); - } - - /** - * This will get the 'quadding' or justification of the text to be displayed. - *
- * 0 - Left (default)
- * 1 - Centered
- * 2 - Right
- * Please see the QUADDING_CONSTANTS in {@link PDVariableText }. - * - * @return The justification of the text strings. - */ - public int getQ() - { - return getCOSObject().getInt(COSName.Q, 0); - } - - /** - * This will set the quadding/justification of the text. Please see the QUADDING_CONSTANTS - * in {@link PDVariableText }. - * - * @param q The new text justification. - */ - public void setQ(int q) - { - getCOSObject().setInt(COSName.Q, q); - } - - /** - * This will set the rectangle difference rectangle. Giving the difference between the - * annotations rectangle and where the drawing occurs. (To take account of any effects applied - * through the BE entry for example) - * - * @param rd the rectangle difference - * - */ - public void setRectDifference(PDRectangle rd) - { - getCOSObject().setItem(COSName.RD, rd); - } - - /** - * This will get the rectangle difference rectangle. Giving the difference between the - * annotations rectangle and where the drawing occurs. (To take account of any effects applied - * through the BE entry for example) - * - * @return the rectangle difference - */ - public PDRectangle getRectDifference() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.RD); - if (base instanceof COSArray) - { - return new PDRectangle((COSArray) base); - } - return null; - } - - /** - * This will set the difference between the annotations "outer" rectangle defined by - * /Rect and boundaries of the underlying. - * - *

This will set an equal difference for all sides

- * - * @param difference from the annotations /Rect entry - */ - public void setRectDifferences(float difference) { - setRectDifferences(difference, difference, difference, difference); - } - - /** - * This will set the difference between the annotations "outer" rectangle defined by - * /Rect and the border. - * - * @param differenceLeft left difference from the annotations /Rect entry - * @param differenceTop top difference from the annotations /Rect entry - * @param differenceRight right difference from the annotations /Rect entry - * @param differenceBottom bottom difference from the annotations /Rect entry - * - */ - public void setRectDifferences(float differenceLeft, float differenceTop, float differenceRight, float differenceBottom) - { - COSArray margins = new COSArray(); - margins.add(new COSFloat(differenceLeft)); - margins.add(new COSFloat(differenceTop)); - margins.add(new COSFloat(differenceRight)); - margins.add(new COSFloat(differenceBottom)); - getCOSObject().setItem(COSName.RD, margins); - } - - /** - * This will get the margin between the annotations "outer" rectangle defined by - * /Rect and the boundaries of the underlying caret. - * - * @return the differences. If the entry hasn't been set am empty array is returned. - */ - public float[] getRectDifferences() - { - COSBase margin = getCOSObject().getItem(COSName.RD); - if (margin instanceof COSArray) - { - return ((COSArray) margin).toFloatArray(); - } - return new float[]{}; - } - - /** - * This will set the coordinates of the callout line. (PDF 1.6 and higher) Only relevant if the - * intent is FreeTextCallout. - * - * @param callout An array of four or six numbers specifying a callout line attached to the free - * text annotation. Six numbers [ x1 y1 x2 y2 x3 y3 ] represent the starting, knee point, and - * ending coordinates of the line in default user space, four numbers [ x1 y1 x2 y2 ] represent - * the starting and ending coordinates of the line. - */ - public final void setCallout(float[] callout) - { - COSArray newCallout = new COSArray(); - newCallout.setFloatArray(callout); - getCOSObject().setItem(COSName.CL, newCallout); - } - - /** - * This will get the coordinates of the callout line. (PDF 1.6 and higher) Only relevant if the - * intent is FreeTextCallout. - * - * @return An array of four or six numbers specifying a callout line attached to the free text - * annotation. Six numbers [ x1 y1 x2 y2 x3 y3 ] represent the starting, knee point, and ending - * coordinates of the line in default user space, four numbers [ x1 y1 x2 y2 ] represent the - * starting and ending coordinates of the line. - */ - public float[] getCallout() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.CL); - if (base instanceof COSArray) - { - return ((COSArray) base).toFloatArray(); - } - return null; - } - - /** - * This will set the line ending style for the start point, see the LE_ constants for the possible values. - * - * @param style The new style. - */ - public void setStartPointEndingStyle(String style) - { - String actualStyle = style == null ? PDAnnotationLine.LE_NONE : style; - COSBase base = getCOSObject().getDictionaryObject(COSName.LE); - COSArray array; - if (!(base instanceof COSArray) || ((COSArray) base).size() == 0) - { - array = new COSArray(); - array.add(COSName.getPDFName(actualStyle)); - array.add(COSName.getPDFName(PDAnnotationLine.LE_NONE)); - getCOSObject().setItem(COSName.LE, array); - } - else - { - array = (COSArray) base; - array.setName(0, actualStyle); - } - } - - /** - * This will retrieve the line ending style for the start point, possible values shown in the LE_ constants section. - * - * @return The ending style for the start point, LE_NONE if missing, never null. - */ - public String getStartPointEndingStyle() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.LE); - if (base instanceof COSArray && ((COSArray) base).size() >= 2) - { - return ((COSArray) base).getName(0, PDAnnotationLine.LE_NONE); - } - return PDAnnotationLine.LE_NONE; - } - - /** - * This will set the line ending style for the end point, see the LE_ constants for the possible values. - * - * @param style The new style. - */ - public void setEndPointEndingStyle(String style) - { - String actualStyle = style == null ? PDAnnotationLine.LE_NONE : style; - COSBase base = getCOSObject().getDictionaryObject(COSName.LE); - COSArray array; - if (!(base instanceof COSArray) || ((COSArray) base).size() < 2) - { - array = new COSArray(); - array.add(COSName.getPDFName(PDAnnotationLine.LE_NONE)); - array.add(COSName.getPDFName(actualStyle)); - getCOSObject().setItem(COSName.LE, array); - } - else - { - array = (COSArray) base; - array.setName(1, actualStyle); - } - } - - /** - * This will retrieve the line ending style for the end point, possible values shown in the LE_ constants section. - * - * @return The ending style for the end point, LE_NONE if missing, never null. - */ - public String getEndPointEndingStyle() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.LE); - if (base instanceof COSArray && ((COSArray) base).size() >= 2) - { - return ((COSArray) base).getName(1, PDAnnotationLine.LE_NONE); - } - return PDAnnotationLine.LE_NONE; + COSDictionary bs = getCOSObject().getCOSDictionary(COSName.BS); + return bs != null ? new PDBorderStyleDictionary(bs) : null; } - - - /** - * This will retrieve the numbers that shall represent the alternating horizontal and vertical - * coordinates. - * - * @return An array of floats representing the alternating horizontal and vertical coordinates. - */ - public float[] getVertices() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.VERTICES); - if (base instanceof COSArray) - { - return ((COSArray) base).toFloatArray(); - } - return null; - } - - /** - * This will set the numbers that shall represent the alternating horizontal and vertical - * coordinates. - * - * @param points an array with the numbers that shall represent the alternating horizontal and - * vertical coordinates. - */ - public void setVertices(float[] points) - { - COSArray ar = new COSArray(); - ar.setFloatArray(points); - getCOSObject().setItem(COSName.VERTICES, ar); - } - - - /** - * PDF 2.0: This will retrieve the arrays that shall represent the alternating horizontal - * and vertical coordinates for path building. - * - * @return An array of float arrays, each supplying the operands for a path building operator - * (m, l or c). The first array should have 2 elements, the others should have 2 or 6 elements. - */ - public float[][] getPath() - { - COSBase base = getCOSObject().getDictionaryObject(COSName.PATH); - if (base instanceof COSArray) - { - COSArray array = (COSArray) base; - float[][] pathArray = new float[array.size()][]; - for (int i = 0; i < array.size(); ++i) - { - COSBase base2 = array.getObject(i); - if (base2 instanceof COSArray) - { - pathArray[i] = ((COSArray) base2).toFloatArray(); - } - else - { - pathArray[i] = new float[0]; - } - } - return pathArray; - } - return null; - } - - /** - * Set a custom appearance handler for generating the annotations appearance streams. - * - * @param appearanceHandler - */ - public void setCustomAppearanceHandler(PDAppearanceHandler appearanceHandler) - { - customAppearanceHandler = appearanceHandler; - } - - @Override - public void constructAppearances() - { - this.constructAppearances(null); - } - - @Override - public void constructAppearances(PDDocument document) - { - if (customAppearanceHandler == null) - { - PDAppearanceHandler appearanceHandler = null; - if (SUB_TYPE_CARET.equals(getSubtype())) - { - appearanceHandler = new PDCaretAppearanceHandler(this, document); - } - else if (SUB_TYPE_FREETEXT.equals(getSubtype())) - { - appearanceHandler = new PDFreeTextAppearanceHandler(this, document); - } - else if (SUB_TYPE_INK.equals(getSubtype())) - { - appearanceHandler = new PDInkAppearanceHandler(this, document); - } - else if (SUB_TYPE_POLYGON.equals(getSubtype())) - { - appearanceHandler = new PDPolygonAppearanceHandler(this, document); - } - else if (SUB_TYPE_POLYLINE.equals(getSubtype())) - { - appearanceHandler = new PDPolylineAppearanceHandler(this, document); - } - else if (SUB_TYPE_SOUND.equals(getSubtype())) - { - appearanceHandler = new PDSoundAppearanceHandler(this, document); - } - else if (PDAnnotationFileAttachment.SUB_TYPE.equals(getSubtype())) - { - appearanceHandler = new PDFileAttachmentAppearanceHandler(this, document); - } - - if (appearanceHandler != null) - { - appearanceHandler.generateAppearanceStreams(); - } - } - else - { - customAppearanceHandler.generateAppearanceStreams(); - } - } - - } diff --git a/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java b/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java index 63fe189..59e49f5 100644 --- a/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java +++ b/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java @@ -20,7 +20,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDAppearanceContentStream; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.common.PDRectangle; @@ -45,28 +44,21 @@ public PDFileAttachmentAppearanceHandler(PDAnnotation annotation, PDDocument doc super(annotation, document); } - @Override - public void generateAppearanceStreams() - { - generateNormalAppearance(); - generateRolloverAppearance(); - generateDownAppearance(); - } - @Override public void generateNormalAppearance() { PDAnnotationFileAttachment annotation = (PDAnnotationFileAttachment) getAnnotation(); - - PDAppearanceContentStream contentStream = null; - try + PDRectangle rect = getRectangle(); + if (rect == null) + { + return; + } + try (PDAppearanceContentStream contentStream = getNormalAppearanceAsContentStream()) { - contentStream = getNormalAppearanceAsContentStream(); setOpacity(contentStream, annotation.getConstantOpacity()); // minimum code of PDTextAppearanceHandler.adjustRectAndBBox() int size = 18; - PDRectangle rect = getRectangle(); rect.setUpperRightX(rect.getLowerLeftX() + size); rect.setLowerLeftY(rect.getUpperRightY() - size); annotation.setRectangle(rect); @@ -79,15 +71,11 @@ public void generateNormalAppearance() { LOG.error(e); } - finally - { - IOUtils.closeQuietly(contentStream); - } } /** * Draw a paperclip. Shape is from - * Iconscout * (Apache licensed). * * @param contentStream diff --git a/src/main/java/org/metanorma/fop/annotations/Annotation.java b/src/main/java/org/metanorma/fop/annotations/Annotation.java index e9a5b07..c07cbdf 100644 --- a/src/main/java/org/metanorma/fop/annotations/Annotation.java +++ b/src/main/java/org/metanorma/fop/annotations/Annotation.java @@ -28,7 +28,10 @@ import javax.xml.xpath.XPathFactory; import org.apache.fop.pdf.PDFObject; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.*; +import org.apache.pdfbox.io.RandomAccessReadBuffer; +import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; @@ -72,11 +75,9 @@ public class Annotation { private PDStructureTreeRoot structureTreeRoot; public void process(File pdf, String xmlReview) throws IOException { - PDDocument document = null; - - try { - document = PDDocument.load(pdf); - + + try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.getAbsoluteFile()))) { + // iterate for each 'annotation' in xmlReview try { DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); @@ -269,65 +270,69 @@ public void process(File pdf, String xmlReview) throws IOException { } // import XFDF annotation xml - - FDFDocument fdfDoc = FDFDocument.loadXFDF(new ByteArrayInputStream(updatedXMLReview.getBytes(StandardCharsets.UTF_8))); - List fdfAnnots = fdfDoc.getCatalog().getFDF().getAnnotations(); - - // group annotations relate to one page and add them into page - HashMap> mapPDFannots = new HashMap<>(); - for (int i=0; i()); + try (FDFDocument fdfDoc = Loader.loadXFDF(new ByteArrayInputStream(updatedXMLReview.getBytes(StandardCharsets.UTF_8)))) { + + //FDFDocument fdfDoc = FDFDocument.loadXFDF(new ByteArrayInputStream(updatedXMLReview.getBytes(StandardCharsets.UTF_8))); + + List fdfAnnots = fdfDoc.getCatalog().getFDF().getAnnotations(); + + // group annotations relate to one page and add them into page + HashMap> mapPDFannots = new HashMap<>(); + for (int i = 0; i < fdfDoc.getCatalog().getFDF().getAnnotations().size(); i++) { + FDFAnnotation fdfannot = fdfAnnots.get(i); + int page = fdfannot.getPage(); + + PDAnnotation pdfannot = PDAnnotation.createAnnotation(fdfannot.getCOSObject()); + + pdfannot.constructAppearances(); // requires for PDF/A + if (mapPDFannots.get(page) == null) { + mapPDFannots.put(page, new ArrayList()); + } + mapPDFannots.get(page).add(pdfannot); } - mapPDFannots.get(page).add(pdfannot); - } - for (Map.Entry> set: mapPDFannots.entrySet()) { - PDPage page = document.getPage(set.getKey()); - List pageAnotations = page.getAnnotations(); - // merge existing annotations (including hyperlinks) and new annotations - pageAnotations.addAll(set.getValue()); - document.getPage(set.getKey()).setAnnotations(pageAnotations); + for (Map.Entry> set : mapPDFannots.entrySet()) { + PDPage page = document.getPage(set.getKey()); + List pageAnotations = page.getAnnotations(); + // merge existing annotations (including hyperlinks) and new annotations + pageAnotations.addAll(set.getValue()); + document.getPage(set.getKey()).setAnnotations(pageAnotations); + } } - - fdfDoc.close(); + //fdfDoc.close(); + document.save(pdf); } catch (IOException | NumberFormatException | ParserConfigurationException | DOMException | TransformerException | SAXException | XPathException ex) { logger.severe("Can't read annotation data from xml."); ex.printStackTrace(); - } - - // add Annot tag for the text annotation - try { - document = PDDocument.load(pdf); // important - hashMapDocumentAnnotations = getAnnotationIDmap(document); + } + } + // add Annot tag for the text annotation + try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.getAbsoluteFile()))) { + //document = PDDocument.load(pdf); // important + hashMapDocumentAnnotations = getAnnotationIDmap(document); - structureTreeRoot = document.getDocumentCatalog().getStructureTreeRoot(); - COSArray aDocument = (COSArray) structureTreeRoot.getK(); - fixAnnotationTags(aDocument, null, 0); + structureTreeRoot = document.getDocumentCatalog().getStructureTreeRoot(); + COSArray aDocument = (COSArray) structureTreeRoot.getK(); + fixAnnotationTags(aDocument, null, 0); - clearEmptyAnnotations(document); + clearEmptyAnnotations(document); - document.save(pdf); - } catch (IOException ex) { - logger.severe("Can't enclose the annotation into the Annot tag."); - ex.printStackTrace(); - } - // END Annot tag adding + document.save(pdf); + } catch (IOException ex) { + logger.severe("Can't enclose the annotation into the Annot tag."); + ex.printStackTrace(); + } + // END Annot tag adding - } finally { + /*finally { if( document != null ) { document.close(); } - } + }*/ } diff --git a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java index 882d4ef..1eaeb80 100644 --- a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java +++ b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java @@ -1,5 +1,7 @@ package org.metanorma.fop.annotations; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; @@ -24,10 +26,10 @@ public class FileAttachmentAnnotation { private boolean DEBUG = false; public void process(File pdf) throws IOException { - PDDocument document = null; - - try { - document = PDDocument.load(pdf); + //PDDocument document = null; + + try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.getAbsoluteFile()))) { + //document = PDDocument.load(pdf); ArrayList embeddedFileAnnotations = new ArrayList<>(); @@ -77,11 +79,11 @@ public void process(File pdf) throws IOException { } - finally { + /*finally { if( document != null ) { document.close(); } - } + }*/ } diff --git a/src/test/java/org/metanorma/fop/mn2pdfTests.java b/src/test/java/org/metanorma/fop/mn2pdfTests.java index 52a864e..7adf294 100644 --- a/src/test/java/org/metanorma/fop/mn2pdfTests.java +++ b/src/test/java/org/metanorma/fop/mn2pdfTests.java @@ -17,8 +17,10 @@ import org.apache.commons.cli.ParseException; import org.apache.fop.complexscripts.util.JapaneseToNumbers; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; @@ -261,9 +263,9 @@ public void checkResultedPDF() throws ParseException { String PDFsubject = ""; String PDFkeywords = ""; - PDDocument doc; - try { - doc = PDDocument.load(pdf.toFile()); + //PDDocument doc; + try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()))) { + //doc = PDDocument.load(pdf.toFile()); PDPageTree pages = doc.getDocumentCatalog().getPages(); for (int i = 0; i < pages.getCount(); i++) { @@ -325,13 +327,13 @@ public void checkResultedEncryptedPDF() throws ParseException { boolean allowAssembleDocument = true; boolean encryptMetadata = false; - PDDocument doc; - try { - doc = PDDocument.load(pdf.toFile(), "userpass"); + //PDDocument doc; + try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()), "userpass")) { + //doc = PDDocument.load(pdf.toFile(), "userpass"); AccessPermission ap = doc.getCurrentAccessPermission(); allowPrint = ap.canPrint(); - allowPrintHQ = ap.canPrintDegraded(); + allowPrintHQ = ap.canPrintFaithful();// canPrintDegraded() in 2.0.27; allowCopyContent = ap.canExtractContent(); allowEditContent = ap.canModify(); allowEditAnnotations = ap.canModifyAnnotations(); @@ -398,9 +400,9 @@ public void checkSpacesInPDF() throws ParseException { mn2pdf.main(args); String pdftext = ""; - PDDocument doc; - try { - doc = PDDocument.load(pdf.toFile()); + //PDDocument doc; + try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()))) { + //doc = PDDocument.load(pdf.toFile()); pdftext = new PDFTextStripper().getText(doc); } catch (IOException ex) { System.out.println(ex.toString()); @@ -423,11 +425,11 @@ public void checkAttachments() throws ParseException { assertTrue(Files.exists(pdf)); // check two attachments - one is embedded file, one is fileattachment annotation - PDDocument doc; + //PDDocument doc; int countFileAttachmentAnnotation = 0; int countFileAttachmentEmbedded = 0; - try { - doc = PDDocument.load(pdf.toFile()); + try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()))) { + //doc = PDDocument.load(pdf.toFile()); int numberOfPages = doc.getNumberOfPages(); for (int pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { From fb574532ae94a37073568a1dc39c8ff7ef8acf49 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 24 Dec 2024 00:03:06 +0300 Subject: [PATCH 2/5] code updated for PDFBox3, #311 --- .../metanorma/fop/annotations/Annotation.java | 68 +++++++++++++------ .../annotations/FileAttachmentAnnotation.java | 11 ++- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/metanorma/fop/annotations/Annotation.java b/src/main/java/org/metanorma/fop/annotations/Annotation.java index c07cbdf..a81d26f 100644 --- a/src/main/java/org/metanorma/fop/annotations/Annotation.java +++ b/src/main/java/org/metanorma/fop/annotations/Annotation.java @@ -7,6 +7,8 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.*; import java.io.StringReader; import java.io.StringWriter; @@ -30,8 +32,6 @@ import org.apache.fop.pdf.PDFObject; import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.*; -import org.apache.pdfbox.io.RandomAccessReadBuffer; -import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; @@ -75,9 +75,15 @@ public class Annotation { private PDStructureTreeRoot structureTreeRoot; public void process(File pdf, String xmlReview) throws IOException { + //PDDocument document = null; - try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.getAbsoluteFile()))) { + Path pdf_tmp = Paths.get(pdf.getAbsolutePath() + "_annotation_tmp"); + Files.copy(Paths.get(pdf.getAbsolutePath()), pdf_tmp, StandardCopyOption.REPLACE_EXISTING); + try (PDDocument document = Loader.loadPDF(pdf_tmp.toFile())) { + //try { + //document = PDDocument.load(pdf); + // iterate for each 'annotation' in xmlReview try { DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); @@ -270,16 +276,15 @@ public void process(File pdf, String xmlReview) throws IOException { } // import XFDF annotation xml - try (FDFDocument fdfDoc = Loader.loadXFDF(new ByteArrayInputStream(updatedXMLReview.getBytes(StandardCharsets.UTF_8)))) { //FDFDocument fdfDoc = FDFDocument.loadXFDF(new ByteArrayInputStream(updatedXMLReview.getBytes(StandardCharsets.UTF_8))); - List fdfAnnots = fdfDoc.getCatalog().getFDF().getAnnotations(); // group annotations relate to one page and add them into page - HashMap> mapPDFannots = new HashMap<>(); - for (int i = 0; i < fdfDoc.getCatalog().getFDF().getAnnotations().size(); i++) { + HashMap> mapPDFannots = new HashMap<>(); + //for (int i=0; i> set : mapPDFannots.entrySet()) { + for (Map.Entry> set: mapPDFannots.entrySet()) { PDPage page = document.getPage(set.getKey()); List pageAnotations = page.getAnnotations(); // merge existing annotations (including hyperlinks) and new annotations pageAnotations.addAll(set.getValue()); document.getPage(set.getKey()).setAnnotations(pageAnotations); } - } - //fdfDoc.close(); - - document.save(pdf); + Files.deleteIfExists(pdf.toPath()); + document.save(pdf); + } + //fdfDoc.close(); } catch (IOException | NumberFormatException | ParserConfigurationException | DOMException | TransformerException | SAXException | XPathException ex) { logger.severe("Can't read annotation data from xml."); ex.printStackTrace(); } + finally { + Files.deleteIfExists(pdf_tmp); + } } + // add Annot tag for the text annotation - try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.getAbsoluteFile()))) { + + Files.copy(Paths.get(pdf.getAbsolutePath()), pdf_tmp, StandardCopyOption.REPLACE_EXISTING); + + try (PDDocument document = Loader.loadPDF(pdf_tmp.toFile())) { + //try { //document = PDDocument.load(pdf); // important hashMapDocumentAnnotations = getAnnotationIDmap(document); @@ -321,14 +334,19 @@ public void process(File pdf, String xmlReview) throws IOException { clearEmptyAnnotations(document); + Files.deleteIfExists(pdf.toPath()); + document.save(pdf); } catch (IOException ex) { logger.severe("Can't enclose the annotation into the Annot tag."); ex.printStackTrace(); } + finally { + Files.deleteIfExists(pdf_tmp); + } // END Annot tag adding - /*finally { + /*finally { if( document != null ) { document.close(); } @@ -359,8 +377,10 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve if (oArray != null) { for(int i = 0; i < oArray.size(); i++) { COSObject oArrayItem = (COSObject) oArray.get(i); + COSBase oBaseItem = oArrayItem.getObject(); + COSDictionary dArrayItem = (COSDictionary) oBaseItem; + COSName cName = (COSName) dArrayItem.getItem(COSName.S); - COSName cName = (COSName) oArrayItem.getItem(COSName.S); if (cName != null) { String tagName = cName.getName(); @@ -373,7 +393,8 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve } if (tagName.equals("Annot")) { - COSBase cbAlt = oArrayItem.getItem(COSName.ALT); + //COSBase cbAlt = oArrayItem.getItem(COSName.ALT); + COSBase cbAlt = dArrayItem.getItem(COSName.ALT); if (cbAlt != null) { String tagAlt = ((COSString)cbAlt).toString(); String COSSTRING_PREFIX = "COSString{"; @@ -393,8 +414,10 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve // set Parent (P) anDict.setItem(COSName.P, parentObject); //oArrayItem oArray // set Page (PG) - COSArray oArrayK = (COSArray) oArrayItem.getItem(COSName.K); - anDict.setItem(COSName.PG, ((COSObject)oArrayK.get(0)).getItem(COSName.PG)); + //COSArray oArrayK = (COSArray) oArrayItem.getItem(COSName.K); + COSArray oArrayK = (COSArray) dArrayItem.getItem(COSName.K); + //anDict.setItem(COSName.PG, ((COSObject)oArrayK.get(0)).getItem(COSName.PG)); + anDict.setItem(COSName.PG, ((COSDictionary)oArrayK.get(0)).getItem(COSName.PG)); PDObjectReference objRef = new PDObjectReference(); anDict.setItem(COSName.K, objRef); @@ -403,11 +426,13 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve objRef.setReferencedObject(foundAnnotation); if (DEBUG) { - System.out.println(oArrayItem.getItem(COSName.K)); + //System.out.println(oArrayItem.getItem(COSName.K)); + System.out.println(dArrayItem.getItem(COSName.K)); } try { - oArrayItem.setObject(anDict); + //oArrayItem.setObject(anDict); + oArrayItem = new COSObject(anDict); // from https://stackoverflow.com/questions/79083813/how-to-add-the-annotation-tag-in-tagged-pdf-using-pdfbox @@ -437,7 +462,8 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve } } try { - COSArray oA_K = (COSArray) oArrayItem.getItem(COSName.K); + //COSArray oA_K = (COSArray) oArrayItem.getItem(COSName.K); + COSArray oA_K = (COSArray) dArrayItem.getItem(COSName.K); fixAnnotationTags(oA_K, oArrayItem, ++level); } catch (Exception e) { // diff --git a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java index 1eaeb80..f5fbc80 100644 --- a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java +++ b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java @@ -1,7 +1,6 @@ package org.metanorma.fop.annotations; import org.apache.pdfbox.Loader; -import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; @@ -12,6 +11,10 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; import org.metanorma.utils.LoggerHelper; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.logging.Logger; @@ -28,7 +31,10 @@ public class FileAttachmentAnnotation { public void process(File pdf) throws IOException { //PDDocument document = null; - try (PDDocument document = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.getAbsoluteFile()))) { + Path pdf_tmp = Paths.get(pdf.getAbsolutePath() + "_annotation_tmp"); + Files.copy(Paths.get(pdf.getAbsolutePath()), pdf_tmp, StandardCopyOption.REPLACE_EXISTING); + try (PDDocument document = Loader.loadPDF(pdf_tmp.toFile())) { + //try { //document = PDDocument.load(pdf); ArrayList embeddedFileAnnotations = new ArrayList<>(); @@ -71,6 +77,7 @@ public void process(File pdf) throws IOException { namesDictionary.setEmbeddedFiles(efTree); } + Files.deleteIfExists(pdf.toPath()); document.save(pdf); } catch (IOException ex) { From 58be03334acac9ae61286c9cf940099dcb297412 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 24 Dec 2024 18:27:22 +0300 Subject: [PATCH 3/5] updated for v2.12 --- Makefile | 2 +- README.adoc | 10 +++++----- pom.xml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index fed964d..761196e 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 := 2.10 +JAR_VERSION := 2.12 JAR_FILE := mn2pdf-$(JAR_VERSION).jar all: target/$(JAR_FILE) diff --git a/README.adoc b/README.adoc index b4a2031..7dc653f 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-2.10.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] +java -Xss5m -Xmx2048m -jar target/mn2pdf-2.12.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] ---- e.g. [source,sh] ---- -java -Xss5m -Xmx2048m -jar target/mn2pdf-2.10.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-2.12.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 -2.10 +2.12 Metanorma XML to PDF converter ---- @@ -111,8 +111,8 @@ Tag the same version in Git: [source,xml] ---- -git tag v2.10 -git push origin v2.10 +git tag v2.12 +git push origin v2.12 ---- Then the corresponding GitHub release will be automatically created at: diff --git a/pom.xml b/pom.xml index 4b013c1..ff76dd6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.metanorma.fop mn2pdf - 2.11 + 2.12 Metanorma XML to PDF converter jar https://www.metanorma.org From dfc285bda3ee8d74e7cf31d326be9c16cf8ccc2c Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 24 Dec 2024 18:32:11 +0300 Subject: [PATCH 4/5] code updated for support PDFBox3, #311 --- .../metanorma/fop/annotations/Annotation.java | 16 ++++++++++++---- .../annotations/FileAttachmentAnnotation.java | 9 +++++---- src/test/java/org/metanorma/fop/mn2pdfTests.java | 11 +++++------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/metanorma/fop/annotations/Annotation.java b/src/main/java/org/metanorma/fop/annotations/Annotation.java index a81d26f..e4de4ed 100644 --- a/src/main/java/org/metanorma/fop/annotations/Annotation.java +++ b/src/main/java/org/metanorma/fop/annotations/Annotation.java @@ -376,9 +376,17 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve if (oArray != null) { for(int i = 0; i < oArray.size(); i++) { - COSObject oArrayItem = (COSObject) oArray.get(i); - COSBase oBaseItem = oArrayItem.getObject(); - COSDictionary dArrayItem = (COSDictionary) oBaseItem; + COSObject oArrayItem = null; + COSDictionary dArrayItem; + if (oArray.get(i) instanceof COSDictionary) { + //oArrayItem = (COSObject) oArray.get(i).getCOSObject(); + dArrayItem = (COSDictionary) oArray.get(i); + } else { + oArrayItem = (COSObject) oArray.get(i); + COSBase oBaseItem = oArrayItem.getObject(); + dArrayItem = (COSDictionary) oBaseItem; + } + COSName cName = (COSName) dArrayItem.getItem(COSName.S); if (cName != null) { @@ -466,7 +474,7 @@ private void fixAnnotationTags(COSArray oArray, COSObject parentObject, int leve COSArray oA_K = (COSArray) dArrayItem.getItem(COSName.K); fixAnnotationTags(oA_K, oArrayItem, ++level); } catch (Exception e) { - // + //System.out.println(e.toString()); } } } diff --git a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java index f5fbc80..430c24e 100644 --- a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java +++ b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java @@ -86,11 +86,12 @@ public void process(File pdf) throws IOException { } - /*finally { - if( document != null ) { + finally { + /*if( document != null ) { document.close(); - } - }*/ + }*/ + Files.deleteIfExists(pdf_tmp); + } } diff --git a/src/test/java/org/metanorma/fop/mn2pdfTests.java b/src/test/java/org/metanorma/fop/mn2pdfTests.java index 7adf294..256cdaa 100644 --- a/src/test/java/org/metanorma/fop/mn2pdfTests.java +++ b/src/test/java/org/metanorma/fop/mn2pdfTests.java @@ -20,7 +20,6 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; @@ -264,7 +263,7 @@ public void checkResultedPDF() throws ParseException { String PDFkeywords = ""; //PDDocument doc; - try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()))) { + try (PDDocument doc = Loader.loadPDF(pdf.toFile())) { //doc = PDDocument.load(pdf.toFile()); PDPageTree pages = doc.getDocumentCatalog().getPages(); @@ -328,12 +327,12 @@ public void checkResultedEncryptedPDF() throws ParseException { boolean encryptMetadata = false; //PDDocument doc; - try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()), "userpass")) { + try (PDDocument doc = Loader.loadPDF(pdf.toFile(), "userpass")) { //doc = PDDocument.load(pdf.toFile(), "userpass"); AccessPermission ap = doc.getCurrentAccessPermission(); allowPrint = ap.canPrint(); - allowPrintHQ = ap.canPrintFaithful();// canPrintDegraded() in 2.0.27; + allowPrintHQ = ap.canPrintFaithful();// ap.canPrintDegraded(); allowCopyContent = ap.canExtractContent(); allowEditContent = ap.canModify(); allowEditAnnotations = ap.canModifyAnnotations(); @@ -401,7 +400,7 @@ public void checkSpacesInPDF() throws ParseException { String pdftext = ""; //PDDocument doc; - try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()))) { + try (PDDocument doc = Loader.loadPDF(pdf.toFile())) { //doc = PDDocument.load(pdf.toFile()); pdftext = new PDFTextStripper().getText(doc); } catch (IOException ex) { @@ -428,7 +427,7 @@ public void checkAttachments() throws ParseException { //PDDocument doc; int countFileAttachmentAnnotation = 0; int countFileAttachmentEmbedded = 0; - try (PDDocument doc = Loader.loadPDF(new RandomAccessReadBufferedFile(pdf.toFile().getAbsoluteFile()))) { + try (PDDocument doc = Loader.loadPDF(pdf.toFile())) { //doc = PDDocument.load(pdf.toFile()); int numberOfPages = doc.getNumberOfPages(); From eda39224ef4968537639dcea8f558266fa78f435 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 24 Dec 2024 23:43:29 +0300 Subject: [PATCH 5/5] added updated classes from Apache FOP for PDFBOX3 support, metanorma/mn2pdf#311 --- .../org/apache/fop/fonts/CFFToType1Font.java | 101 ++ .../apache/fop/fonts/cff/CFFDataReader.java | 924 ++++++++++++ .../apache/fop/fonts/cff/FOPCFFDataInput.java | 95 ++ .../apache/fop/fonts/truetype/OTFFile.java | 154 ++ .../fop/fonts/truetype/OTFSubSetFile.java | 1263 +++++++++++++++++ 5 files changed, 2537 insertions(+) create mode 100644 src/main/java/org/apache/fop/fonts/CFFToType1Font.java create mode 100644 src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java create mode 100644 src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java create mode 100644 src/main/java/org/apache/fop/fonts/truetype/OTFFile.java create mode 100644 src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java diff --git a/src/main/java/org/apache/fop/fonts/CFFToType1Font.java b/src/main/java/org/apache/fop/fonts/CFFToType1Font.java new file mode 100644 index 0000000..22d23e0 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/CFFToType1Font.java @@ -0,0 +1,101 @@ +/* + * 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; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.apache.fontbox.cff.CFFFont; +import org.apache.fontbox.cff.CFFParser; +import org.apache.pdfbox.io.RandomAccessReadBuffer; + +import org.apache.fop.apps.io.InternalResourceResolver; +import org.apache.fop.fonts.type1.PFBData; +import org.apache.fop.fonts.type1.PFBParser; +import org.apache.fop.fonts.type1.Type1SubsetFile; +import org.apache.fop.render.ps.Type1FontFormatter; + +public class CFFToType1Font extends MultiByteFont { + + public CFFToType1Font(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) { + super(resourceResolver, embeddingMode); + setEmbeddingMode(EmbeddingMode.FULL); + setFontType(FontType.TYPE1); + } + + public InputStream getInputStream() throws IOException { + return null; + } + + public List getInputStreams() throws IOException { + InputStream cff = super.getInputStream(); + return convertOTFToType1(cff); + } + + private List convertOTFToType1(InputStream in) throws IOException { + CFFFont f = new CFFParser().parse(new RandomAccessReadBuffer(IOUtils.toByteArray(in))).get(0); + List fonts = new ArrayList(); + Map glyphs = cidSet.getGlyphs(); + int i = 0; + for (Map x : splitGlyphs(glyphs)) { + String iStr = "." + i; + fonts.add(convertOTFToType1(x, f, iStr)); + i++; + } + return fonts; + } + + private List> splitGlyphs(Map glyphs) { + List> allGlyphs = new ArrayList>(); + for (Map.Entry x : glyphs.entrySet()) { + int k = x.getKey(); + int v = x.getValue(); + int pot = v / 256; + v = v % 256; + while (allGlyphs.size() < pot + 1) { + Map glyphsPerFont = new HashMap(); + glyphsPerFont.put(0, 0); + allGlyphs.add(glyphsPerFont); + } + allGlyphs.get(pot).put(k, v); + } + return allGlyphs; + } + + private InputStream convertOTFToType1(Map glyphs, CFFFont cffFont, String splitGlyphsId) + throws IOException { + byte[] type1Bytes = new Type1FontFormatter(glyphs).format(cffFont, splitGlyphsId); + PFBData pfb = new PFBParser().parsePFB(new ByteArrayInputStream(type1Bytes)); + ByteArrayOutputStream s1 = new ByteArrayOutputStream(); + s1.write(pfb.getHeaderSegment()); + ByteArrayOutputStream s2 = new ByteArrayOutputStream(); + s2.write(pfb.getEncryptedSegment()); + ByteArrayOutputStream s3 = new ByteArrayOutputStream(); + s3.write(pfb.getTrailerSegment()); + byte[] out = new Type1SubsetFile().stitchFont(s1, s2, s3); + return new ByteArrayInputStream(out); + } +} diff --git a/src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java b/src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java new file mode 100644 index 0000000..5f68026 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java @@ -0,0 +1,924 @@ +/* + * 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.cff; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fontbox.cff.CFFOperator; +import org.apache.fontbox.cff.DataInputByteArray; + +import org.apache.fop.fonts.truetype.FontFileReader; +import org.apache.fop.fonts.truetype.OTFFile; + +/** + * A class to read the CFF data from an OTF CFF font file. + */ +public class CFFDataReader { + private DataInputByteArray cffData; + + private byte[] header; + private CFFIndexData nameIndex; + private CFFIndexData topDICTIndex; + private CFFIndexData stringIndex; + private CFFIndexData charStringIndex; + private CFFIndexData globalIndexSubr; + private CFFIndexData localIndexSubr; + private CustomEncoding encoding; + private FDSelect fdSelect; + private List fdFonts; + + private static final int DOUBLE_BYTE_OPERATOR = 12; + private static final int NUM_STANDARD_STRINGS = 391; + + /** Commonly used parsed dictionaries */ + private LinkedHashMap topDict; + + public CFFDataReader() { + + } + + /** + * Constructor for the CFF data reader which accepts the CFF byte data + * as an argument. + * @param cffDataArray A byte array which holds the CFF data + */ + public CFFDataReader(byte[] cffDataArray) throws IOException { + cffData = new FOPCFFDataInput(cffDataArray); + readCFFData(); + } + + /** + * Constructor for the CFF data reader which accepts a FontFileReader object + * which points to the original font file as an argument. + * @param fontFile The font file as represented by a FontFileReader object + */ + public CFFDataReader(FontFileReader fontFile) throws IOException { + cffData = new FOPCFFDataInput(OTFFile.getCFFData(fontFile)); + readCFFData(); + } + + private void readCFFData() throws IOException { + header = readHeader(); + nameIndex = readIndex(); + topDICTIndex = readIndex(); + topDict = parseDictData(topDICTIndex.getData()); + stringIndex = readIndex(); + globalIndexSubr = readIndex(); + charStringIndex = readCharStringIndex(); + encoding = readEncoding(); + fdSelect = readFDSelect(); + localIndexSubr = readLocalIndexSubrs(); + fdFonts = parseCIDData(); + } + + public Map getPrivateDict(DICTEntry privateEntry) throws IOException { + return parseDictData(getPrivateDictBytes(privateEntry)); + } + + public byte[] getPrivateDictBytes(DICTEntry privateEntry) throws IOException { + int privateLength = privateEntry.getOperands().get(0).intValue(); + int privateOffset = privateEntry.getOperands().get(1).intValue(); + return getCFFOffsetBytes(privateOffset, privateLength); + } + + /** + * Retrieves a number of bytes from the CFF data stream + * @param offset The offset of the bytes to retrieve + * @param length The number of bytes to retrieve + * @return Returns a byte array of requested bytes + * @throws IOException Throws an IO Exception if an error occurs + */ + private byte[] getCFFOffsetBytes(int offset, int length) throws IOException { + cffData.setPosition(offset); + return cffData.readBytes(length); + } + + /** + * Parses the dictionary data and returns a map of objects for each entry + * @param dictData The data for the dictionary data + * @return Returns a map of type DICTEntry identified by the operand name + * @throws IOException Throws an IO Exception if an error occurs + */ + public LinkedHashMap parseDictData(byte[] dictData) throws IOException { + LinkedHashMap dictEntries = new LinkedHashMap(); + List operands = new ArrayList(); + List operandLengths = new ArrayList(); + int lastOperandLength = 0; + for (int i = 0; i < dictData.length; i++) { + int readByte = dictData[i] & 0xFF; + if (readByte < 28) { + int[] operator = new int[(readByte == DOUBLE_BYTE_OPERATOR) ? 2 : 1]; + if (readByte == DOUBLE_BYTE_OPERATOR) { + operator[0] = dictData[i]; + operator[1] = dictData[i + 1]; + i++; + } else { + operator[0] = dictData[i]; + } + String operatorName = ""; + if (operator.length > 1) { + operatorName = CFFOperator.getOperator(operator[0], operator[1]); + } else { + operatorName = CFFOperator.getOperator(operator[0]); + } + DICTEntry newEntry = new DICTEntry(); + newEntry.setOperator(operator); + newEntry.setOperands(new ArrayList(operands)); + newEntry.setOperatorName(operatorName); + newEntry.setOffset(i - lastOperandLength); + newEntry.setOperandLength(lastOperandLength); + newEntry.setOperandLengths(new ArrayList(operandLengths)); + byte[] byteData = new byte[lastOperandLength + operator.length]; + System.arraycopy(dictData, i - operator.length - (lastOperandLength - 1), + byteData, 0, operator.length + lastOperandLength); + newEntry.setByteData(byteData); + dictEntries.put(operatorName, newEntry); + operands.clear(); + operandLengths.clear(); + lastOperandLength = 0; + } else { + if (readByte >= 32 && readByte <= 246) { + operands.add(readByte - 139); + lastOperandLength += 1; + operandLengths.add(1); + } else if (readByte >= 247 && readByte <= 250) { + operands.add((readByte - 247) * 256 + (dictData[i + 1] & 0xFF) + 108); + lastOperandLength += 2; + operandLengths.add(2); + i++; + } else if (readByte >= 251 && readByte <= 254) { + operands.add(-(readByte - 251) * 256 - (dictData[i + 1] & 0xFF) - 108); + lastOperandLength += 2; + operandLengths.add(2); + i++; + } else if (readByte == 28) { + operands.add((dictData[i + 1] & 0xFF) << 8 | (dictData[i + 2] & 0xFF)); + lastOperandLength += 3; + operandLengths.add(3); + i += 2; + } else if (readByte == 29) { + operands.add((dictData[i + 1] & 0xFF) << 24 | (dictData[i + 2] & 0xFF) << 16 + | (dictData[i + 3] & 0xFF) << 8 | (dictData[i + 4] & 0xFF)); + lastOperandLength += 5; + operandLengths.add(5); + i += 4; + } else if (readByte == 30) { + boolean terminatorFound = false; + StringBuilder realNumber = new StringBuilder(); + int byteCount = 1; + do { + byte nibblesByte = dictData[++i]; + byteCount++; + terminatorFound = readNibble(realNumber, (nibblesByte >> 4) & 0x0F); + if (!terminatorFound) { + terminatorFound = readNibble(realNumber, nibblesByte & 0x0F); + } + } while (!terminatorFound); + operands.add(Double.valueOf(realNumber.toString())); + lastOperandLength += byteCount; + operandLengths.add(byteCount); + } + } + } + return dictEntries; + } + + private boolean readNibble(StringBuilder realNumber, int nibble) { + if (nibble <= 0x9) { + realNumber.append(nibble); + } else { + switch (nibble) { + case 0xa: realNumber.append("."); break; + case 0xb: realNumber.append("E"); break; + case 0xc: realNumber.append("E-"); break; + case 0xd: break; + case 0xe: realNumber.append("-"); break; + case 0xf: return true; + default: throw new AssertionError("Unexpected nibble value"); + } + } + return false; + } + + /** + * A class containing data for a dictionary entry + */ + public static class DICTEntry { + private int[] operator; + private List operands; + private List operandLengths; + private String operatorName; + private int offset; + private int operandLength; + private byte[] data = new byte[0]; + + public void setOperator(int[] operator) { + this.operator = operator; + } + + public int[] getOperator() { + return this.operator; + } + + public void setOperands(List operands) { + this.operands = operands; + } + + public List getOperands() { + return this.operands; + } + + public void setOperatorName(String operatorName) { + this.operatorName = operatorName; + } + + public String getOperatorName() { + return this.operatorName; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getOffset() { + return this.offset; + } + + public void setOperandLength(int operandLength) { + this.operandLength = operandLength; + } + + public int getOperandLength() { + return this.operandLength; + } + + public void setByteData(byte[] data) { + this.data = data.clone(); + } + + public byte[] getByteData() { + return data.clone(); + } + + public void setOperandLengths(List operandLengths) { + this.operandLengths = operandLengths; + } + + public List getOperandLengths() { + return operandLengths; + } + } + + private byte[] readHeader() throws IOException { + //Read known header + byte[] fixedHeader = cffData.readBytes(4); + int hdrSize = (fixedHeader[2] & 0xFF); + byte[] extra = cffData.readBytes(hdrSize - 4); + byte[] header = new byte[hdrSize]; + for (int i = 0; i < fixedHeader.length; i++) { + header[i] = fixedHeader[i]; + } + for (int i = 4; i < extra.length; i++) { + header[i] = extra[i - 4]; + } + return header; + } + + /** + * Reads a CFF index object are the specified offset position + * @param offset The position of the index object to read + * @return Returns an object representing the index + * @throws IOException Throws an IO Exception if an error occurs + */ + public CFFIndexData readIndex(int offset) throws IOException { + cffData.setPosition(offset); + return readIndex(); + } + + private CFFIndexData readIndex() throws IOException { + return readIndex(cffData); + } + + /** + * Reads an index from the current position of the CFFDataInput object + * @param input The object holding the CFF byte data + * @return Returns an object representing the index + * @throws IOException Throws an IO Exception if an error occurs + */ + public CFFIndexData readIndex(DataInputByteArray input) throws IOException { + CFFIndexData nameIndex = new CFFIndexData(); + if (input != null) { + int origPos = input.getPosition(); + nameIndex.parseIndexHeader(input); + int tableSize = input.getPosition() - origPos; + nameIndex.setByteData(input.getPosition() - tableSize, tableSize); + } + return nameIndex; + } + + /** + * Retrieves the SID for the given GID object + * @param charsetOffset The offset of the charset data + * @param gid The GID for which to retrieve the SID + * @return Returns the SID as an integer + */ + public int getSIDFromGID(int charsetOffset, int gid) throws IOException { + if (gid == 0) { + return 0; + } + cffData.setPosition(charsetOffset); + int charsetFormat = cffData.readUnsignedByte(); + switch (charsetFormat) { + case 0: //Adjust for .notdef character + cffData.setPosition(cffData.getPosition() + (--gid * 2)); + return cffData.readUnsignedShort(); + case 1: return getSIDFromGIDFormat(gid, 1); + case 2: return getSIDFromGIDFormat(gid, 2); + default: return 0; + } + } + + private int getSIDFromGIDFormat(int gid, int format) throws IOException { + int glyphCount = 0; + while (true) { + int oldGlyphCount = glyphCount; + int start = cffData.readUnsignedShort(); + glyphCount += ((format == 1) ? cffData.readUnsignedByte() : cffData.readUnsignedShort()) + 1; + if (gid <= glyphCount) { + return start + (gid - oldGlyphCount) - 1; + } + } + } + + public byte[] getHeader() { + return header.clone(); + } + + public CFFIndexData getNameIndex() { + return nameIndex; + } + + public CFFIndexData getTopDictIndex() { + return topDICTIndex; + } + + public LinkedHashMap getTopDictEntries() { + return topDict; + } + + public CFFIndexData getStringIndex() { + return stringIndex; + } + + public CFFIndexData getGlobalIndexSubr() { + return globalIndexSubr; + } + + public CFFIndexData getLocalIndexSubr() { + return localIndexSubr; + } + + public CFFIndexData getCharStringIndex() { + return charStringIndex; + } + + public DataInputByteArray getCFFData() { + return cffData; + } + + public CustomEncoding getEncoding() { + return encoding; + } + + public FDSelect getFDSelect() { + return fdSelect; + } + + public List getFDFonts() { + return fdFonts; + } + + public DataInputByteArray getLocalSubrsForGlyph(int glyph) throws IOException { + //Subsets are currently written using a Format0 FDSelect + FDSelect fontDictionary = getFDSelect(); + if (fontDictionary instanceof Format0FDSelect) { + Format0FDSelect fdSelect = (Format0FDSelect)fontDictionary; + int found = fdSelect.getFDIndexes()[glyph]; + FontDict font = getFDFonts().get(found); + byte[] localSubrData = font.getLocalSubrData().getByteData(); + if (localSubrData != null) { + return new DataInputByteArray(localSubrData); + } else { + return null; + } + } else if (fontDictionary instanceof Format3FDSelect) { + Format3FDSelect fdSelect = (Format3FDSelect)fontDictionary; + int index = 0; + for (int first : fdSelect.getRanges().keySet()) { + if (first > glyph) { + break; + } + index++; + } + FontDict font = getFDFonts().get(index); + byte[] localSubrsData = font.getLocalSubrData().getByteData(); + if (localSubrsData != null) { + return new DataInputByteArray(localSubrsData); + } else { + return null; + } + } + return null; + } + + /** + * Parses the char string index from the CFF byte data + * @return Returns the char string index object + * @throws IOException Throws an IO Exception if an error occurs + */ + public CFFIndexData readCharStringIndex() throws IOException { + int offset = topDict.get("CharStrings").getOperands().get(0).intValue(); + cffData.setPosition(offset); + return readIndex(); + } + + private CustomEncoding readEncoding() throws IOException { + CustomEncoding foundEncoding = null; + if (topDict.get("Encoding") != null) { + int offset = topDict.get("Encoding").getOperands().get(0).intValue(); + if (offset != 0 && offset != 1) { + //No need to set the offset as we are reading the data sequentially. + int format = cffData.readUnsignedByte(); + int numEntries = cffData.readUnsignedByte(); + switch (format) { + case 0: + foundEncoding = readFormat0Encoding(format, numEntries); + break; + case 1: + foundEncoding = readFormat1Encoding(format, numEntries); + break; + default: break; + } + } + } + return foundEncoding; + } + + private Format0Encoding readFormat0Encoding(int format, int numEntries) + throws IOException { + Format0Encoding newEncoding = new Format0Encoding(); + newEncoding.setFormat(format); + newEncoding.setNumEntries(numEntries); + int[] codes = new int[numEntries]; + for (int i = 0; i < numEntries; i++) { + codes[i] = cffData.readUnsignedByte(); + } + newEncoding.setCodes(codes); + return newEncoding; + } + + private Format1Encoding readFormat1Encoding(int format, int numEntries) + throws IOException { + Format1Encoding newEncoding = new Format1Encoding(); + newEncoding.setFormat(format); + newEncoding.setNumEntries(numEntries); + Map ranges = new LinkedHashMap(); + for (int i = 0; i < numEntries; i++) { + int first = cffData.readUnsignedByte(); + int left = cffData.readUnsignedByte(); + ranges.put(first, left); + } + newEncoding.setRanges(ranges); + return newEncoding; + } + + private FDSelect readFDSelect() throws IOException { + FDSelect fdSelect = null; + DICTEntry fdSelectEntry = topDict.get("FDSelect"); + if (fdSelectEntry != null) { + int fdOffset = fdSelectEntry.getOperands().get(0).intValue(); + cffData.setPosition(fdOffset); + int format = cffData.readUnsignedByte(); + switch (format) { + case 0: + fdSelect = readFormat0FDSelect(); + break; + case 3: + fdSelect = readFormat3FDSelect(); + break; + default: + } + } + return fdSelect; + } + + private Format0FDSelect readFormat0FDSelect() throws IOException { + Format0FDSelect newFDs = new Format0FDSelect(); + newFDs.setFormat(0); + int glyphCount = charStringIndex.getNumObjects(); + int[] fds = new int[glyphCount]; + for (int i = 0; i < glyphCount; i++) { + fds[i] = cffData.readUnsignedByte(); + } + newFDs.setFDIndexes(fds); + return newFDs; + } + + private Format3FDSelect readFormat3FDSelect() throws IOException { + Format3FDSelect newFDs = new Format3FDSelect(); + newFDs.setFormat(3); + int rangeCount = cffData.readUnsignedShort(); + newFDs.setRangeCount(rangeCount); + Map ranges = new LinkedHashMap(); + for (int i = 0; i < rangeCount; i++) { + int first = cffData.readUnsignedShort(); + int fd = cffData.readUnsignedByte(); + ranges.put(first, fd); + } + newFDs.setRanges(ranges); + newFDs.setSentinelGID(cffData.readUnsignedShort()); + return newFDs; + } + + private List parseCIDData() throws IOException { + List fdFonts = new ArrayList(); + if (topDict.get("ROS") != null) { + DICTEntry fdArray = topDict.get("FDArray"); + if (fdArray != null) { + int fdIndex = fdArray.getOperands().get(0).intValue(); + CFFIndexData fontDicts = readIndex(fdIndex); + for (int i = 0; i < fontDicts.getNumObjects(); i++) { + FontDict newFontDict = new FontDict(); + + byte[] fdData = fontDicts.getValue(i); + Map fdEntries = parseDictData(fdData); + newFontDict.setByteData(fontDicts.getValuePosition(i), fontDicts.getValueLength(i)); + DICTEntry fontFDEntry = fdEntries.get("FontName"); + if (fontFDEntry != null) { + newFontDict.setFontName(getString(fontFDEntry.getOperands().get(0).intValue())); + } + DICTEntry privateFDEntry = fdEntries.get("Private"); + if (privateFDEntry != null) { + newFontDict = setFDData(privateFDEntry, newFontDict); + } + + fdFonts.add(newFontDict); + } + } + } + return fdFonts; + } + + private FontDict setFDData(DICTEntry privateFDEntry, FontDict newFontDict) throws IOException { + int privateFDLength = privateFDEntry.getOperands().get(0).intValue(); + int privateFDOffset = privateFDEntry.getOperands().get(1).intValue(); + cffData.setPosition(privateFDOffset); + byte[] privateDict = cffData.readBytes(privateFDLength); + newFontDict.setPrivateDictData(privateFDOffset, privateFDLength); + Map privateEntries = parseDictData(privateDict); + DICTEntry subroutines = privateEntries.get("Subrs"); + if (subroutines != null) { + CFFIndexData localSubrs = readIndex(privateFDOffset + + subroutines.getOperands().get(0).intValue()); + newFontDict.setLocalSubrData(localSubrs); + } else { + newFontDict.setLocalSubrData(new CFFIndexData()); + } + return newFontDict; + } + + private String getString(int sid) throws IOException { + return new String(stringIndex.getValue(sid - NUM_STANDARD_STRINGS)); + } + + private CFFIndexData readLocalIndexSubrs() throws IOException { + CFFIndexData localSubrs = null; + DICTEntry privateEntry = topDict.get("Private"); + if (privateEntry != null) { + int length = privateEntry.getOperands().get(0).intValue(); + int offset = privateEntry.getOperands().get(1).intValue(); + cffData.setPosition(offset); + byte[] privateData = cffData.readBytes(length); + Map privateDict = parseDictData(privateData); + DICTEntry localSubrsEntry = privateDict.get("Subrs"); + if (localSubrsEntry != null) { + int localOffset = offset + localSubrsEntry.getOperands().get(0).intValue(); + cffData.setPosition(localOffset); + localSubrs = readIndex(); + } + } + return localSubrs; + } + + /** + * Parent class which provides the ability to retrieve byte data from + * a sub-table. + */ + public class CFFSubTable { + private DataLocation dataLocation = new DataLocation(); + + public void setByteData(int position, int length) { + dataLocation = new DataLocation(position, length); + } + + public byte[] getByteData() throws IOException { + int oldPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition()); + return cffData.readBytes(dataLocation.getDataLength()); + } finally { + cffData.setPosition(oldPos); + } + } + } + + /** + * An object used to hold index data from the CFF data + */ + public class CFFIndexData extends CFFSubTable { + private int numObjects; + private int offSize; + private int[] offsets = new int[0]; + private DataLocation dataLocation = new DataLocation(); + + public void setNumObjects(int numObjects) { + this.numObjects = numObjects; + } + + public int getNumObjects() { + return this.numObjects; + } + + public void setOffSize(int offSize) { + this.offSize = offSize; + } + + public int getOffSize() { + return this.offSize; + } + + public void setOffsets(int[] offsets) { + this.offsets = offsets.clone(); + } + + public int[] getOffsets() { + return offsets.clone(); + } + + public void setData(int position, int length) { + dataLocation = new DataLocation(position, length); + } + + public byte[] getData() throws IOException { + int origPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition()); + return cffData.readBytes(dataLocation.getDataLength()); + } finally { + cffData.setPosition(origPos); + } + } + + /** + * Parses index data from an index object found within the CFF byte data + * @param cffData A byte array containing the CFF data + * @throws IOException Throws an IO Exception if an error occurs + */ + public void parseIndexHeader(DataInputByteArray cffData) throws IOException { + setNumObjects(cffData.readUnsignedShort()); + setOffSize(cffData.readUnsignedByte()); + int[] offsets = new int[getNumObjects() + 1]; + byte[] bytes; + //Fills the offsets array + for (int i = 0; i <= getNumObjects(); i++) { + switch (getOffSize()) { + case 1: + offsets[i] = cffData.readUnsignedByte(); + break; + case 2: + offsets[i] = cffData.readUnsignedShort(); + break; + case 3: + bytes = cffData.readBytes(3); + offsets[i] = ((bytes[0] & 0xFF) << 16) + ((bytes[1] & 0xFF) << 8) + (bytes[2] & 0xFF); + break; + case 4: + bytes = cffData.readBytes(4); + offsets[i] = ((bytes[0] & 0xFF) << 24) + ((bytes[1] & 0xFF) << 16) + + ((bytes[2] & 0xFF) << 8) + (bytes[3] & 0xFF); + break; + default: continue; + } + } + setOffsets(offsets); + int position = cffData.getPosition(); + int dataSize = offsets[offsets.length - 1] - offsets[0]; + + cffData.setPosition(cffData.getPosition() + dataSize); + setData(position, dataSize); + } + + /** + * Retrieves data from the index data + * @param index The index position of the data to retrieve + * @return Returns the byte data for the given index + * @throws IOException Throws an IO Exception if an error occurs + */ + public byte[] getValue(int index) throws IOException { + int oldPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition() + (offsets[index] - 1)); + return cffData.readBytes(offsets[index + 1] - offsets[index]); + } finally { + cffData.setPosition(oldPos); + } + } + + public int getValuePosition(int index) { + return dataLocation.getDataPosition() + (offsets[index] - 1); + } + + public int getValueLength(int index) { + return offsets[index + 1] - offsets[index]; + } + } + + public abstract class CustomEncoding { + private int format; + private int numEntries; + + public void setFormat(int format) { + this.format = format; + } + + public int getFormat() { + return format; + } + + public void setNumEntries(int numEntries) { + this.numEntries = numEntries; + } + + public int getNumEntries() { + return numEntries; + } + } + + public class Format0Encoding extends CustomEncoding { + private int[] codes = new int[0]; + + public void setCodes(int[] codes) { + this.codes = codes.clone(); + } + + public int[] getCodes() { + return codes.clone(); + } + } + + public class Format1Encoding extends CustomEncoding { + private Map ranges; + + public void setRanges(Map ranges) { + this.ranges = ranges; + } + + public Map getRanges() { + return ranges; + } + } + + public abstract class FDSelect { + private int format; + + public void setFormat(int format) { + this.format = format; + } + + public int getFormat() { + return format; + } + } + + public class Format0FDSelect extends FDSelect { + private int[] fds = new int[0]; + + public void setFDIndexes(int[] fds) { + this.fds = fds.clone(); + } + + public int[] getFDIndexes() { + return fds.clone(); + } + } + + public class Format3FDSelect extends FDSelect { + private int rangeCount; + private Map ranges; + private int sentinelGID; + + public void setRangeCount(int rangeCount) { + this.rangeCount = rangeCount; + } + + public int getRangeCount() { + return rangeCount; + } + + public void setRanges(Map ranges) { + this.ranges = ranges; + } + + public Map getRanges() { + return ranges; + } + + public void setSentinelGID(int sentinelGID) { + this.sentinelGID = sentinelGID; + } + + public int getSentinelGID() { + return sentinelGID; + } + } + + public class FontDict extends CFFSubTable { + private String fontName; + private DataLocation dataLocation = new DataLocation(); + private CFFIndexData localSubrData; + + public void setFontName(String groupName) { + this.fontName = groupName; + } + + public String getFontName() { + return fontName; + } + + public void setPrivateDictData(int position, int length) { + dataLocation = new DataLocation(position, length); + } + + public byte[] getPrivateDictData() throws IOException { + int origPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition()); + return cffData.readBytes(dataLocation.getDataLength()); + } finally { + cffData.setPosition(origPos); + } + } + + public void setLocalSubrData(CFFIndexData localSubrData) { + this.localSubrData = localSubrData; + } + + public CFFIndexData getLocalSubrData() { + return localSubrData; + } + } + + private static class DataLocation { + private int dataPosition; + private int dataLength; + + public DataLocation() { + dataPosition = 0; + dataLength = 0; + } + + public DataLocation(int position, int length) { + this.dataPosition = position; + this.dataLength = length; + } + + public int getDataPosition() { + return dataPosition; + } + + public int getDataLength() { + return dataLength; + } + } +} diff --git a/src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java b/src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java new file mode 100644 index 0000000..5202851 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java @@ -0,0 +1,95 @@ +/* + * 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.cff; + +import java.io.IOException; + +import org.apache.fontbox.cff.DataInputByteArray; + +public class FOPCFFDataInput extends DataInputByteArray { + private final byte[] inputBuffer; + private int bufferPosition; + + public FOPCFFDataInput(byte[] buffer) { + super(buffer); + this.inputBuffer = buffer; + } + + public boolean hasRemaining() throws IOException { + return this.bufferPosition < this.inputBuffer.length; + } + + public int getPosition() { + return this.bufferPosition; + } + + public void setPosition(int position) throws IOException { + if (position < 0) { + throw new IOException("position is negative"); +// } else if (position >= this.inputBuffer.length) { +// throw new IOException("New position is out of range " + position + " >= " + this.inputBuffer.length); + } else { + this.bufferPosition = position; + } + } + + public byte readByte() throws IOException { + if (!this.hasRemaining()) { + throw new IOException("End off buffer reached"); + } else { + return this.inputBuffer[this.bufferPosition++]; + } + } + + public int readUnsignedByte() throws IOException { + if (!this.hasRemaining()) { + throw new IOException("End off buffer reached"); + } else { + return this.inputBuffer[this.bufferPosition++] & 255; + } + } + + public int peekUnsignedByte(int offset) throws IOException { + if (offset < 0) { + throw new IOException("offset is negative"); + } else if (this.bufferPosition + offset >= this.inputBuffer.length) { + throw new IOException("Offset position is out of range " + (this.bufferPosition + offset) + + " >= " + this.inputBuffer.length); + } else { + return this.inputBuffer[this.bufferPosition + offset] & 255; + } + } + + public byte[] readBytes(int length) throws IOException { + if (length < 0) { + throw new IOException("length is negative"); + } else if (this.inputBuffer.length - this.bufferPosition < length) { + throw new IOException("Premature end of buffer reached"); + } else { + byte[] bytes = new byte[length]; + System.arraycopy(this.inputBuffer, this.bufferPosition, bytes, 0, length); + this.bufferPosition += length; + return bytes; + } + } + + public int length() throws IOException { + return this.inputBuffer.length; + } +} diff --git a/src/main/java/org/apache/fop/fonts/truetype/OTFFile.java b/src/main/java/org/apache/fop/fonts/truetype/OTFFile.java new file mode 100644 index 0000000..befbb94 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/truetype/OTFFile.java @@ -0,0 +1,154 @@ +/* + * 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.IOException; +import java.util.List; + +import org.apache.fontbox.cff.CFFFont; +import org.apache.fontbox.cff.CFFParser; +import org.apache.fontbox.cff.CFFType1Font; +import org.apache.fontbox.cff.DataInputByteArray; +import org.apache.pdfbox.io.RandomAccessReadBuffer; + +public class OTFFile extends OpenFont { + + protected CFFFont fileFont; + + public OTFFile() throws IOException { + this(true, false); + } + + public OTFFile(boolean useKerning, boolean useAdvanced) throws IOException { + super(useKerning, useAdvanced); + checkForFontbox(); + } + + private void checkForFontbox() throws IOException { + try { + Class.forName("org.apache.fontbox.cff.CFFFont"); + } catch (ClassNotFoundException ex) { + throw new IOException("The Fontbox jar was not found in the classpath. This is " + + "required for OTF CFF ssupport."); + } + } + + @Override + protected void updateBBoxAndOffset() throws IOException { + Object bbox = fileFont.getTopDict().get("FontBBox"); + if (bbox != null) { + List bboxList = (List) bbox; + int[] bboxInt = new int[4]; + for (int i = 0; i < bboxInt.length; i++) { + bboxInt[i] = (Integer) bboxList.get(i); + } + for (OFMtxEntry o : mtxTab) { + o.setBoundingBox(bboxInt); + } + } + } + + private static class Mapping { + private int sid; + private String name; + private byte[] bytes; + + public void setSID(int sid) { + this.sid = sid; + } + + public int getSID() { + return sid; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + } + + + @Override + protected void initializeFont(FontFileReader in) throws IOException { + fontFile = in; + fontFile.seekSet(0); + CFFParser parser = new CFFParser(); + fileFont = parser.parse(new RandomAccessReadBuffer(in.getAllBytes())).get(0); + embedFontName = fileFont.getName(); + } + + protected void readName() throws IOException { + Object familyName = fileFont.getTopDict().get("FamilyName"); + if (familyName != null && !familyName.equals("")) { + familyNames.add(familyName.toString()); + fullName = familyName.toString(); + } else { + fullName = fileFont.getName(); + familyNames.add(fullName); + } + } + + /** + * Reads the CFFData from a given font file + * @param fontFile The font file being read + * @return The byte data found in the CFF table + */ + public static byte[] getCFFData(FontFileReader fontFile) throws IOException { + byte[] cff = fontFile.getAllBytes(); + DataInputByteArray input = new DataInputByteArray(fontFile.getAllBytes()); + input.readBytes(4); //OTTO + short numTables = input.readShort(); + input.readShort(); //searchRange + input.readShort(); //entrySelector + input.readShort(); //rangeShift + + for (int q = 0; q < numTables; q++) { + String tagName = new String(input.readBytes(4)); + readLong(input); //Checksum + long offset = readLong(input); + long length = readLong(input); + if (tagName.equals("CFF ")) { + cff = new byte[(int)length]; + System.arraycopy(fontFile.getAllBytes(), (int)offset, cff, 0, cff.length); + break; + } + } + return cff; + } + + private static long readLong(DataInputByteArray input) throws IOException { + return (input.readUnsignedShort() << 16) | input.readUnsignedShort(); + } + + public boolean isType1() { + return fileFont instanceof CFFType1Font; + } +} diff --git a/src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java b/src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java new file mode 100644 index 0000000..2ea2ed8 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java @@ -0,0 +1,1263 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fontbox.cff.CFFStandardString; +import org.apache.fontbox.cff.CFFType1Font; +import org.apache.fontbox.cff.CharStringCommand; +import org.apache.fontbox.cff.Type1CharString; +import org.apache.fontbox.cff.Type2CharString; + +import org.apache.fop.fonts.MultiByteFont; +import org.apache.fop.fonts.cff.CFFDataReader; +import org.apache.fop.fonts.cff.CFFDataReader.CFFIndexData; +import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry; +import org.apache.fop.fonts.cff.CFFDataReader.FDSelect; +import org.apache.fop.fonts.cff.CFFDataReader.FontDict; +import org.apache.fop.fonts.cff.CFFDataReader.Format0FDSelect; +import org.apache.fop.fonts.cff.CFFDataReader.Format3FDSelect; +import org.apache.fop.fonts.type1.AdobeStandardEncoding; + +/** + * Reads an OpenType CFF file and generates a subset + * The OpenType specification can be found at the Microsoft + * Typography site: http://www.microsoft.com/typography/otspec/ + */ +public class OTFSubSetFile extends OTFSubSetWriter { + + /** A map containing each glyph to be included in the subset + * with their existing and new GID's **/ + protected Map subsetGlyphs = new LinkedHashMap(); + + /** A map of the new GID to SID used to construct the charset table **/ + protected Map gidToSID; + + protected CFFIndexData localIndexSubr; + protected CFFIndexData globalIndexSubr; + + /** List of subroutines to write to the local / global indexes in the subset font **/ + protected List subsetLocalIndexSubr; + protected List subsetGlobalIndexSubr; + + /** For fonts which have an FDSelect or ROS flag in Top Dict, this is used to store the + * local subroutine indexes for each group as opposed to the above subsetLocalIndexSubr */ + protected List> fdSubrs; + + /** The subset FD Select table used to store the mappings between glyphs and their + * associated FDFont object which point to a private dict and local subroutines. */ + private Map subsetFDSelect; + + /** A list of unique subroutines from the global / local subroutine indexes */ + protected List localUniques; + protected List globalUniques; + + /** A store of the number of subroutines each global / local subroutine will store **/ + protected int subsetLocalSubrCount; + protected int subsetGlobalSubrCount; + + /** A list of char string data for each glyph to be stored in the subset font **/ + protected List subsetCharStringsIndex; + + /** The embedded name to change in the name table **/ + protected String embeddedName; + + /** An array used to hold the string index data for the subset font **/ + protected List stringIndexData = new ArrayList(); + + /** The CFF reader object used to read data and offsets from the original font file */ + protected CFFDataReader cffReader; + + /** The class used to represent this font **/ + private MultiByteFont mbFont; + + /** The number of standard strings in CFF **/ + public static final int NUM_STANDARD_STRINGS = 391; + /** The operator used to identify a local subroutine reference */ + private static final int LOCAL_SUBROUTINE = 10; + /** The operator used to identify a global subroutine reference */ + private static final int GLOBAL_SUBROUTINE = 29; + + private static final String ACCENT_CMD = "SEAC|"; + + /** The parser used to parse type2 charstring */ + private Type2Parser type2Parser; + + public OTFSubSetFile() throws IOException { + super(); + } + + public void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont) throws IOException { + readFont(in, embeddedName, mbFont, mbFont.getUsedGlyphs()); + } + + /** + * Reads and creates a subset of the font. + * + * @param in FontFileReader to read from + * @param embeddedName Name to be checked for in the font file + * @param usedGlyphs Map of glyphs (glyphs has old index as (Integer) key and + * new index as (Integer) value) + * @throws IOException in case of an I/O problem + */ + void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont, + Map usedGlyphs) throws IOException { + this.mbFont = mbFont; + fontFile = in; + this.embeddedName = embeddedName; + initializeFont(in); + cffReader = new CFFDataReader(fontFile); + mapChars(usedGlyphs); + //Sort by the new GID and store in a LinkedHashMap + subsetGlyphs = sortByValue(usedGlyphs); + //Create the CIDFontType0C data + createCFF(); + } + + private void mapChars(Map usedGlyphs) throws IOException { + if (fileFont instanceof CFFType1Font) { + CFFType1Font cffType1Font = (CFFType1Font) fileFont; + subsetGlyphs = sortByValue(usedGlyphs); + for (int gid : subsetGlyphs.keySet()) { + Type2CharString type2CharString = cffType1Font.getType2CharString(gid); + List stack = new ArrayList(); + List type1Sequence = getType1Sequence(type2CharString); + for (Object obj : type1Sequence) { + if (obj instanceof CharStringCommand) { + if (ACCENT_CMD.equals(obj.toString())) { + int first = stack.get(3).intValue(); + int second = stack.get(4).intValue(); + mbFont.mapChar(AdobeStandardEncoding.getUnicodeFromCodePoint(first)); + mbFont.mapChar(AdobeStandardEncoding.getUnicodeFromCodePoint(second)); + } + stack.clear(); + } else { + stack.add((Number) obj); + } + } + } + } + } + + private List getType1Sequence(Type1CharString type1CharString) { + try { + Field f = Type1CharString.class.getDeclaredField("type1Sequence"); + f.setAccessible(true); + return (List) f.get(type1CharString); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + private Map sortByValue(Map map) { + List> list = new ArrayList>(map.entrySet()); + Collections.sort(list, new Comparator>() { + public int compare(Entry o1, Entry o2) { + return ((Comparable) o1.getValue()).compareTo(o2.getValue()); + } + }); + + Map result = new LinkedHashMap(); + for (Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } + + protected void createCFF() throws IOException { + //Header + writeBytes(cffReader.getHeader()); + + //Name Index + writeIndex(Arrays.asList(embedFontName.getBytes("UTF-8"))); + + Offsets offsets = new Offsets(); + + //Top DICT Index and Data + offsets.topDictData = currentPos + writeTopDICT(); + + boolean hasFDSelect = cffReader.getFDSelect() != null; + + //Create the char string index data and related local / global subroutines + if (hasFDSelect) { + createCharStringDataCID(); + } else { + createCharStringData(); + } + + //If it is a CID-Keyed font, store each FD font and add each SID + List fontNameSIDs = null; + List subsetFDFonts = null; + if (hasFDSelect) { + subsetFDFonts = getUsedFDFonts(); + fontNameSIDs = storeFDStrings(subsetFDFonts); + } + + //String index + writeStringIndex(); + + //Global subroutine index + writeIndex(subsetGlobalIndexSubr); + + //Encoding + offsets.encoding = currentPos; + + //Charset table + offsets.charset = currentPos; + writeCharsetTable(hasFDSelect); + + //FDSelect table + offsets.fdSelect = currentPos; + if (hasFDSelect) { + writeFDSelect(); + if (!isCharStringBeforeFD()) { + offsets.fdArray = writeFDArray(subsetFDFonts, fontNameSIDs); + } + } + + //Char Strings Index + offsets.charString = currentPos; + writeIndex(subsetCharStringsIndex); + if (hasFDSelect) { + if (isCharStringBeforeFD()) { + offsets.fdArray = writeFDArray(subsetFDFonts, fontNameSIDs); + } + updateCIDOffsets(offsets); + } else { + //Keep offset to modify later with the local subroutine index offset + offsets.privateDict = currentPos; + writePrivateDict(); + + //Local subroutine index + offsets.localIndex = currentPos; + writeIndex(subsetLocalIndexSubr); + + //Update the offsets + updateOffsets(offsets); + } + } + + static class Offsets { + Integer topDictData; + Integer encoding; + Integer charset; + Integer fdSelect; + Integer charString; + Integer fdArray; + Integer privateDict; + Integer localIndex; + } + + private int writeFDArray(List subsetFDFonts, List fontNameSIDs) throws IOException { + List privateDictOffsets = writeCIDDictsAndSubrs(subsetFDFonts); + return writeFDArray(subsetFDFonts, privateDictOffsets, fontNameSIDs); + } + + private boolean isCharStringBeforeFD() { + LinkedHashMap entries = cffReader.getTopDictEntries(); + int len = entries.get("CharStrings").getOperandLength(); + if (entries.containsKey("FDArray")) { + int len2 = entries.get("FDArray").getOperandLength(); + return len < len2; + } + return true; + } + + protected List storeFDStrings(List uniqueNewRefs) throws IOException { + List fontNameSIDs = new ArrayList(); + List fdFonts = cffReader.getFDFonts(); + for (int uniqueNewRef : uniqueNewRefs) { + FontDict fdFont = fdFonts.get(uniqueNewRef); + byte[] fdFontByteData = fdFont.getByteData(); + Map fdFontDict = cffReader.parseDictData(fdFontByteData); + fontNameSIDs.add(stringIndexData.size() + NUM_STANDARD_STRINGS); + stringIndexData.add(cffReader.getStringIndex().getValue(fdFontDict.get("FontName") + .getOperands().get(0).intValue() - NUM_STANDARD_STRINGS)); + } + return fontNameSIDs; + } + + protected int writeTopDICT() throws IOException { + Map topDICT = cffReader.getTopDictEntries(); + List topDictStringEntries = Arrays.asList("version", "Notice", "Copyright", + "FullName", "FamilyName", "Weight", "PostScript"); + ByteArrayOutputStream dict = new ByteArrayOutputStream(); + int offsetExtra = 0; + for (Map.Entry dictEntry : topDICT.entrySet()) { + String dictKey = dictEntry.getKey(); + DICTEntry entry = dictEntry.getValue(); + //If the value is an SID, update the reference but keep the size the same + entry.setOffset(entry.getOffset() + offsetExtra); + if (dictKey.equals("CharStrings") && entry.getOperandLength() < 5) { + byte[] extra = new byte[5 - entry.getOperandLength()]; + offsetExtra += extra.length; + dict.write(extra); + dict.write(entry.getByteData()); + entry.setOperandLength(5); + } else if (dictKey.equals("ROS")) { + dict.write(writeROSEntry(entry)); + } else if (dictKey.equals("CIDCount")) { + dict.write(writeCIDCount(entry)); + } else if (topDictStringEntries.contains(dictKey)) { + if (entry.getOperandLength() < 2) { + entry.setOperandLength(2); + offsetExtra++; + } + dict.write(writeTopDictStringEntry(entry)); + } else { + dict.write(entry.getByteData()); + } + } + byte[] topDictIndex = cffReader.getTopDictIndex().getByteData(); + int offSize = topDictIndex[2]; + return writeIndex(Arrays.asList(dict.toByteArray()), offSize) - dict.size(); + } + + private byte[] writeROSEntry(DICTEntry dictEntry) throws IOException { + int sidA = dictEntry.getOperands().get(0).intValue(); + if (sidA > 390) { + stringIndexData.add(cffReader.getStringIndex().getValue(sidA - NUM_STANDARD_STRINGS)); + } + int sidAStringIndex = stringIndexData.size() + 390; + int sidB = dictEntry.getOperands().get(1).intValue(); + if (sidB > 390) { + stringIndexData.add("Identity".getBytes("UTF-8")); + } + int sidBStringIndex = stringIndexData.size() + 390; + byte[] cidEntryByteData = dictEntry.getByteData(); + updateOffset(cidEntryByteData, 0, dictEntry.getOperandLengths().get(0), + sidAStringIndex); + updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0), + dictEntry.getOperandLengths().get(1), sidBStringIndex); + updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0) + + dictEntry.getOperandLengths().get(1), dictEntry.getOperandLengths().get(2), 0); + return cidEntryByteData; + } + + protected byte[] writeCIDCount(DICTEntry dictEntry) throws IOException { + byte[] cidCountByteData = dictEntry.getByteData(); + updateOffset(cidCountByteData, 0, dictEntry.getOperandLengths().get(0), + subsetGlyphs.size()); + return cidCountByteData; + } + + private byte[] writeTopDictStringEntry(DICTEntry dictEntry) throws IOException { + int sid = dictEntry.getOperands().get(0).intValue(); + if (sid > 391) { + stringIndexData.add(cffReader.getStringIndex().getValue(sid - 391)); + } + byte[] newDictEntry = createNewRef(stringIndexData.size() + 390, dictEntry.getOperator(), + dictEntry.getOperandLength(), true); + return newDictEntry; + } + + private void writeStringIndex() throws IOException { + Map topDICT = cffReader.getTopDictEntries(); + int charsetOffset = topDICT.get("charset").getOperands().get(0).intValue(); + + gidToSID = new LinkedHashMap(); + + for (Entry subsetGlyph : subsetGlyphs.entrySet()) { + int gid = subsetGlyph.getKey(); + int v = subsetGlyph.getValue(); + int sid = cffReader.getSIDFromGID(charsetOffset, gid); + //Check whether the SID falls into the standard string set + if (sid < NUM_STANDARD_STRINGS) { + gidToSID.put(v, sid); + if (mbFont != null) { + mbFont.mapUsedGlyphName(v, CFFStandardString.getName(sid)); + } + } else { + int index = sid - NUM_STANDARD_STRINGS; + //index is 0 based, should use < not <= + if (index < cffReader.getStringIndex().getNumObjects()) { + byte[] value = cffReader.getStringIndex().getValue(index); + if (mbFont != null) { + mbFont.mapUsedGlyphName(v, new String(value, "UTF-8")); + } + gidToSID.put(v, stringIndexData.size() + 391); + stringIndexData.add(value); + } else { + if (mbFont != null) { + mbFont.mapUsedGlyphName(v, ".notdef"); + } + gidToSID.put(v, index); + } + } + } + //Write the String Index + writeIndex(stringIndexData); + } + + protected void createCharStringDataCID() throws IOException { + CFFIndexData charStringsIndex = cffReader.getCharStringIndex(); + + FDSelect fontDictionary = cffReader.getFDSelect(); + if (fontDictionary instanceof Format0FDSelect) { + throw new UnsupportedOperationException("OTF CFF CID Format0 currently not implemented"); + } else if (fontDictionary instanceof Format3FDSelect) { + Format3FDSelect fdSelect = (Format3FDSelect)fontDictionary; + Map subsetGroups = new HashMap(); + + List uniqueGroups = new ArrayList(); + Map rangeMap = fdSelect.getRanges(); + Integer[] ranges = rangeMap.keySet().toArray(new Integer[rangeMap.size()]); + for (int gid : subsetGlyphs.keySet()) { + int i = 0; + for (Entry entry : rangeMap.entrySet()) { + int nextRange; + if (i < ranges.length - 1) { + nextRange = ranges[i + 1]; + } else { + nextRange = fdSelect.getSentinelGID(); + } + if (gid >= entry.getKey() && gid < nextRange) { + int r = entry.getValue(); + subsetGroups.put(gid, r); + if (!uniqueGroups.contains(r)) { + uniqueGroups.add(r); + } + } + i++; + } + } + + //Prepare resources + globalIndexSubr = cffReader.getGlobalIndexSubr(); + + //Create the new char string index + subsetCharStringsIndex = new ArrayList(); + + globalUniques = new ArrayList(); + + subsetFDSelect = new LinkedHashMap(); + + List> foundLocalUniques = new ArrayList>(); + for (int u : uniqueGroups) { + foundLocalUniques.add(new ArrayList()); + } + Map gidHintMaskLengths = new HashMap(); + for (Entry subsetGlyph : subsetGlyphs.entrySet()) { + int gid = subsetGlyph.getKey(); + int group = subsetGroups.get(gid); + localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData(); + localUniques = foundLocalUniques.get(uniqueGroups.indexOf(group)); + type2Parser = new Type2Parser(); + + FDIndexReference newFDReference = new FDIndexReference(uniqueGroups.indexOf(group), group); + subsetFDSelect.put(subsetGlyph.getValue(), newFDReference); + byte[] data = charStringsIndex.getValue(gid); + preScanForSubsetIndexSize(data); + gidHintMaskLengths.put(gid, type2Parser.getMaskLength()); + } + + //Create the two lists which are to store the local and global subroutines + subsetGlobalIndexSubr = new ArrayList(); + + fdSubrs = new ArrayList>(); + subsetGlobalSubrCount = globalUniques.size(); + globalUniques.clear(); + localUniques = null; + + for (List foundLocalUnique : foundLocalUniques) { + fdSubrs.add(new ArrayList()); + } + List> foundLocalUniquesB = new ArrayList>(); + for (int u : uniqueGroups) { + foundLocalUniquesB.add(new ArrayList()); + } + for (Entry subsetGlyph : subsetGlyphs.entrySet()) { + int gid = subsetGlyph.getKey(); + int value = subsetGlyph.getValue(); + int group = subsetGroups.get(gid); + localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData(); + int newFDIndex = subsetFDSelect.get(value).getNewFDIndex(); + localUniques = foundLocalUniquesB.get(newFDIndex); + byte[] data = charStringsIndex.getValue(gid); + subsetLocalIndexSubr = fdSubrs.get(newFDIndex); + subsetLocalSubrCount = foundLocalUniques.get(newFDIndex).size(); + type2Parser = new Type2Parser(); + type2Parser.setMaskLength(gidHintMaskLengths.get(gid)); + data = readCharStringData(data, subsetLocalSubrCount); + subsetCharStringsIndex.add(data); + } + } + } + + protected void writeFDSelect() { + if (cffReader.getTopDictEntries().get("CharStrings").getOperandLength() == 2) { + Map indexs = getFormat3Index(); + writeByte(3); //Format + writeCard16(indexs.size()); + int count = 0; + for (Entry x : indexs.entrySet()) { + writeCard16(count); + writeByte(x.getKey()); + count += x.getValue(); + } + writeCard16(subsetFDSelect.size()); + } else { + writeByte(0); //Format + for (FDIndexReference e : subsetFDSelect.values()) { + writeByte(e.getNewFDIndex()); + } + } + } + + private Map getFormat3Index() { + Map indexs = new LinkedHashMap(); + int last = -1; + int count = 0; + for (FDIndexReference e : subsetFDSelect.values()) { + int i = e.getNewFDIndex(); + count++; + if (i != last) { + indexs.put(i, count); + count = 1; + } + last = i; + } + indexs.put(last, count); + return indexs; + } + + protected List getUsedFDFonts() { + List uniqueNewRefs = new ArrayList(); + for (FDIndexReference e : subsetFDSelect.values()) { + int fdIndex = e.getOldFDIndex(); + if (!uniqueNewRefs.contains(fdIndex)) { + uniqueNewRefs.add(fdIndex); + } + } + return uniqueNewRefs; + } + + protected List writeCIDDictsAndSubrs(List uniqueNewRefs) + throws IOException { + List privateDictOffsets = new ArrayList(); + List fdFonts = cffReader.getFDFonts(); + int i = 0; + for (int ref : uniqueNewRefs) { + FontDict curFDFont = fdFonts.get(ref); + byte[] fdPrivateDictByteData = curFDFont.getPrivateDictData(); + Map fdPrivateDict = cffReader.parseDictData(fdPrivateDictByteData); + int privateDictOffset = currentPos; + privateDictOffsets.add(privateDictOffset); + DICTEntry subrs = fdPrivateDict.get("Subrs"); + if (subrs != null) { + fdPrivateDictByteData = resizeToFitOpLen(fdPrivateDictByteData, subrs); + updateOffset(fdPrivateDictByteData, subrs.getOffset(), + subrs.getOperandLength(), + fdPrivateDictByteData.length); + } + writeBytes(fdPrivateDictByteData); + writeIndex(fdSubrs.get(i)); + i++; + } + return privateDictOffsets; + } + + private byte[] resizeToFitOpLen(byte[] fdPrivateDictByteData, DICTEntry subrs) throws IOException { + if (subrs.getOperandLength() == 2 && fdPrivateDictByteData.length < 108) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(fdPrivateDictByteData); + bos.write(new byte[108 - fdPrivateDictByteData.length]); + fdPrivateDictByteData = bos.toByteArray(); + } + return fdPrivateDictByteData; + } + + protected int writeFDArray(List uniqueNewRefs, List privateDictOffsets, + List fontNameSIDs) + throws IOException { + int offset = currentPos; + List fdFonts = cffReader.getFDFonts(); + List index = new ArrayList(); + + int i = 0; + for (int ref : uniqueNewRefs) { + FontDict fdFont = fdFonts.get(ref); + byte[] fdFontByteData = fdFont.getByteData(); + Map fdFontDict = cffReader.parseDictData(fdFontByteData); + //Update the SID to the FontName + updateOffset(fdFontByteData, fdFontDict.get("FontName").getOffset() - 1, + fdFontDict.get("FontName").getOperandLengths().get(0), + fontNameSIDs.get(i)); + //Update the Private dict reference + updateOffset(fdFontByteData, fdFontDict.get("Private").getOffset() + + fdFontDict.get("Private").getOperandLengths().get(0), + fdFontDict.get("Private").getOperandLengths().get(1), + privateDictOffsets.get(i)); + index.add(fdFontByteData); + i++; + } + writeIndex(index); + return offset; + } + + private static class FDIndexReference { + private int newFDIndex; + private int oldFDIndex; + + public FDIndexReference(int newFDIndex, int oldFDIndex) { + this.newFDIndex = newFDIndex; + this.oldFDIndex = oldFDIndex; + } + + public int getNewFDIndex() { + return newFDIndex; + } + + public int getOldFDIndex() { + return oldFDIndex; + } + } + + private void createCharStringData() throws IOException { + Map topDICT = cffReader.getTopDictEntries(); + + CFFIndexData charStringsIndex = cffReader.getCharStringIndex(); + + DICTEntry privateEntry = topDICT.get("Private"); + if (privateEntry != null) { + int privateOffset = privateEntry.getOperands().get(1).intValue(); + Map privateDICT = cffReader.getPrivateDict(privateEntry); + + if (privateDICT.get("Subrs") != null) { + int localSubrOffset = privateOffset + privateDICT.get("Subrs").getOperands().get(0).intValue(); + localIndexSubr = cffReader.readIndex(localSubrOffset); + } else { + localIndexSubr = cffReader.readIndex(null); + } + } + + globalIndexSubr = cffReader.getGlobalIndexSubr(); + + //Create the two lists which are to store the local and global subroutines + subsetLocalIndexSubr = new ArrayList(); + subsetGlobalIndexSubr = new ArrayList(); + + //Create the new char string index + subsetCharStringsIndex = new ArrayList(); + + localUniques = new ArrayList(); + globalUniques = new ArrayList(); + Map gidHintMaskLengths = new HashMap(); + for (int gid : subsetGlyphs.keySet()) { + type2Parser = new Type2Parser(); + byte[] data = charStringsIndex.getValue(gid); + preScanForSubsetIndexSize(data); + gidHintMaskLengths.put(gid, type2Parser.getMaskLength()); + } + + //Store the size of each subset index and clear the unique arrays + subsetLocalSubrCount = localUniques.size(); + subsetGlobalSubrCount = globalUniques.size(); + localUniques.clear(); + globalUniques.clear(); + + for (int gid : subsetGlyphs.keySet()) { + byte[] data = charStringsIndex.getValue(gid); + type2Parser = new Type2Parser(); + //Retrieve modified char string data and fill local / global subroutine arrays + type2Parser.setMaskLength(gidHintMaskLengths.get(gid)); + data = readCharStringData(data, subsetLocalSubrCount); + subsetCharStringsIndex.add(data); + } + } + + static class Type2Parser { + /** + * logging instance + */ + protected Log log = LogFactory.getLog(Type2Parser.class); + + private List stack = new ArrayList(); + private int hstemCount; + private int vstemCount; + private int lastOp = -1; + private int maskLength = -1; + + public void pushOperand(BytesNumber v) { + stack.add(v); + } + + public BytesNumber popOperand() { + return stack.remove(stack.size() - 1); + } + + public void clearStack() { + stack.clear(); + } + + public int[] getOperands(int numbers) { + int[] ret = new int[numbers]; + while (numbers > 0) { + numbers--; + ret[numbers] = this.popOperand().getNumber(); + } + return ret; + } + + public void setMaskLength(int maskLength) { + this.maskLength = maskLength; + } + + public int getMaskLength() { + // The number of data bytes for mask is exactly the number needed, one + // bit per hint, to reference the number of stem hints declared + // at the beginning of the charstring program. + if (maskLength > 0) { + return maskLength; + } + return 1 + (hstemCount + vstemCount - 1) / 8; + } + + private int exec(int b0, byte[] input, int curPos) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(input); + bis.skip(curPos + 1); + return exec(b0, bis); + } + + public int exec(int b0, InputStream data) throws IOException { + int posDelta = 0; + if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) { + if (b0 == 12) { + log.warn("May not guess the operand count correctly."); + posDelta = 1; + } else if (b0 == 1 || b0 == 18) { + // hstem(hm) operator + hstemCount += stack.size() / 2; + clearStack(); + } else if (b0 == 19 || b0 == 20) { + if (lastOp == 1 || lastOp == 18) { + //If hstem and vstem hints are both declared at the beginning of + //a charstring, and this sequence is followed directly by the + //hintmask or cntrmask operators, the vstem hint operator need + //not be included. + vstemCount += stack.size() / 2; + } + clearStack(); + posDelta = getMaskLength(); + } else if (b0 == 3 || b0 == 23) { + // vstem(hm) operator + vstemCount += stack.size() / 2; + clearStack(); + } + if (b0 != 11 && b0 != 12) { + lastOp = b0; + } + } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) { + BytesNumber operand = readNumber(b0, data); + pushOperand(operand); + posDelta = operand.getNumBytes() - 1; + } else { + throw new UnsupportedOperationException("Operator:" + b0 + " is not supported"); + } + return posDelta; + } + + private BytesNumber readNumber(int b0, InputStream input) throws IOException { + if (b0 == 28) { + int b1 = input.read(); + int b2 = input.read(); + return new BytesNumber((int) (short) (b1 << 8 | b2), 3); + } else if (b0 >= 32 && b0 <= 246) { + return new BytesNumber(b0 - 139, 1); + } else if (b0 >= 247 && b0 <= 250) { + int b1 = input.read(); + return new BytesNumber((b0 - 247) * 256 + b1 + 108, 2); + } else if (b0 >= 251 && b0 <= 254) { + int b1 = input.read(); + return new BytesNumber(-(b0 - 251) * 256 - b1 - 108, 2); + } else if (b0 == 255) { + int b1 = input.read(); + int b2 = input.read(); + int b3 = input.read(); + int b4 = input.read(); + return new BytesNumber((b1 << 24 | b2 << 16 | b3 << 8 | b4), 5); + } else { + throw new IllegalArgumentException(); + } + } + } + private void preScanForSubsetIndexSize(byte[] data) throws IOException { + boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0; + boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0; + for (int dataPos = 0; dataPos < data.length; dataPos++) { + int b0 = data[dataPos] & 0xff; + if (b0 == LOCAL_SUBROUTINE && hasLocalSubroutines) { + preScanForSubsetIndexSize(localIndexSubr, localUniques); + } else if (b0 == GLOBAL_SUBROUTINE && hasGlobalSubroutines) { + preScanForSubsetIndexSize(globalIndexSubr, globalUniques); + } else { + dataPos += type2Parser.exec(b0, data, dataPos); + } + } + } + + private void preScanForSubsetIndexSize(CFFIndexData indexSubr, List uniques) throws IOException { + int subrNumber = getSubrNumber(indexSubr.getNumObjects(), type2Parser.popOperand().getNumber()); + if (!uniques.contains(subrNumber) && subrNumber < indexSubr.getNumObjects()) { + uniques.add(subrNumber); + } + if (subrNumber < indexSubr.getNumObjects()) { + byte[] subr = indexSubr.getValue(subrNumber); + preScanForSubsetIndexSize(subr); + } else { + throw new IllegalArgumentException("callgsubr out of range"); + } + } + + private int getSubrNumber(int numSubroutines, int operand) { + int bias = getBias(numSubroutines); + return bias + operand; + } + + private byte[] readCharStringData(byte[] data, int subsetLocalSubrCount) throws IOException { + boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0; + boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0; + for (int dataPos = 0; dataPos < data.length; dataPos++) { + int b0 = data[dataPos] & 0xff; + if (b0 == 10 && hasLocalSubroutines) { + BytesNumber operand = type2Parser.popOperand(); + int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), operand.getNumber()); + + int newRef = getNewRefForReference(subrNumber, localUniques, localIndexSubr, subsetLocalIndexSubr, + subsetLocalSubrCount); + + if (newRef != -1) { + byte[] newData = constructNewRefData(dataPos, data, operand, subsetLocalSubrCount, + newRef, new int[] {10}); + dataPos -= data.length - newData.length; + data = newData; + } + } else if (b0 == 29 && hasGlobalSubroutines) { + BytesNumber operand = type2Parser.popOperand(); + int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), operand.getNumber()); + + int newRef = getNewRefForReference(subrNumber, globalUniques, globalIndexSubr, subsetGlobalIndexSubr, + subsetGlobalSubrCount); + + if (newRef != -1) { + byte[] newData = constructNewRefData(dataPos, data, operand, subsetGlobalSubrCount, + newRef, new int[] {29}); + dataPos -= data.length - newData.length; + data = newData; + } + } else { + dataPos += type2Parser.exec(b0, data, dataPos); + } + } + + //Return the data with the modified references to our arrays + return data; + } + + private int getNewRefForReference(int subrNumber, List uniquesArray, + CFFIndexData indexSubr, List subsetIndexSubr, int subrCount) throws IOException { + int newRef; + if (!uniquesArray.contains(subrNumber)) { + if (subrNumber < indexSubr.getNumObjects()) { + byte[] subr = indexSubr.getValue(subrNumber); + subr = readCharStringData(subr, subrCount); + uniquesArray.add(subrNumber); + subsetIndexSubr.add(subr); + newRef = subsetIndexSubr.size() - 1; + } else { + throw new IllegalArgumentException("subrNumber out of range"); + } + } else { + newRef = uniquesArray.indexOf(subrNumber); + } + return newRef; + } + + private int getBias(int subrCount) { + if (subrCount < 1240) { + return 107; + } else if (subrCount < 33900) { + return 1131; + } else { + return 32768; + } + } + + private byte[] constructNewRefData(int curDataPos, byte[] currentData, BytesNumber operand, + int fullSubsetIndexSize, int curSubsetIndexSize, int[] operatorCode) throws IOException { + //Create the new array with the modified reference + ByteArrayOutputStream newData = new ByteArrayOutputStream(); + int startRef = curDataPos - operand.getNumBytes(); + int length = operand.getNumBytes() + 1; + int newBias = getBias(fullSubsetIndexSize); + int newRef = curSubsetIndexSize - newBias; + byte[] newRefBytes = createNewRef(newRef, operatorCode, -1, false); + newData.write(currentData, 0, startRef); + newData.write(newRefBytes); + newData.write(currentData, startRef + length, currentData.length - (startRef + length)); + return newData.toByteArray(); + } + + public static byte[] createNewRef(int newRef, int[] operatorCode, int forceLength, boolean isDict) { + ByteArrayOutputStream newRefBytes = new ByteArrayOutputStream(); + if ((forceLength == -1 && newRef >= -107 && newRef <= 107) || forceLength == 1) { + //The index values are 0 indexed + newRefBytes.write(newRef + 139); + } else if ((forceLength == -1 && newRef >= -1131 && newRef <= 1131) || forceLength == 2) { + if (newRef <= -876) { + newRefBytes.write(254); + } else if (newRef <= -620) { + newRefBytes.write(253); + } else if (newRef <= -364) { + newRefBytes.write(252); + } else if (newRef <= -108) { + newRefBytes.write(251); + } else if (newRef <= 363) { + newRefBytes.write(247); + } else if (newRef <= 619) { + newRefBytes.write(248); + } else if (newRef <= 875) { + newRefBytes.write(249); + } else { + newRefBytes.write(250); + } + if (newRef > 0) { + newRefBytes.write(newRef - 108); + } else { + newRefBytes.write(-newRef - 108); + } + } else if ((forceLength == -1 && newRef >= -32768 && newRef <= 32767) || forceLength == 3) { + newRefBytes.write(28); + newRefBytes.write(newRef >> 8); + newRefBytes.write(newRef); + } else { + if (isDict) { + newRefBytes.write(29); + } else { + newRefBytes.write(255); + } + newRefBytes.write(newRef >> 24); + newRefBytes.write(newRef >> 16); + newRefBytes.write(newRef >> 8); + newRefBytes.write(newRef); + } + for (int i : operatorCode) { + newRefBytes.write(i); + } + return newRefBytes.toByteArray(); + } + + protected int writeIndex(List dataArray) { + int totLength = 1; + for (byte[] data : dataArray) { + totLength += data.length; + } + int offSize = getOffSize(totLength); + return writeIndex(dataArray, offSize); + } + + protected int writeIndex(List dataArray, int offSize) { + int hdrTotal = 3; + //2 byte number of items + this.writeCard16(dataArray.size()); + //Offset Size: 1 byte = 256, 2 bytes = 65536 etc. + //Offsets in the offset array are relative to the byte that precedes the object data. + //Therefore the first element of the offset array is always 1. + this.writeByte(offSize); + //Count the first offset 1 + hdrTotal += offSize; + int total = 0; + int i = 0; + for (byte[] data : dataArray) { + hdrTotal += offSize; + int length = data.length; + switch (offSize) { + case 1: + if (i == 0) { + writeByte(1); + } + total += length; + writeByte(total + 1); + break; + case 2: + if (i == 0) { + writeCard16(1); + } + total += length; + writeCard16(total + 1); + break; + case 3: + if (i == 0) { + writeThreeByteNumber(1); + } + total += length; + writeThreeByteNumber(total + 1); + break; + case 4: + if (i == 0) { + writeULong(1); + } + total += length; + writeULong(total + 1); + break; + default: + throw new AssertionError("Offset Size was not an expected value."); + } + i++; + } + for (byte[] aDataArray : dataArray) { + writeBytes(aDataArray); + } + return hdrTotal + total; + } + + private int getOffSize(int totLength) { + int offSize = 1; + if (totLength < (1 << 8)) { + offSize = 1; + } else if (totLength < (1 << 16)) { + offSize = 2; + } else if (totLength < (1 << 24)) { + offSize = 3; + } else { + offSize = 4; + } + return offSize; + } + /** + * A class used to store the last number operand and also it's size in bytes + */ + static class BytesNumber { + private int number; + private int numBytes; + + public BytesNumber(int number, int numBytes) { + this.number = number; + this.numBytes = numBytes; + } + + public int getNumber() { + return this.number; + } + + public int getNumBytes() { + return this.numBytes; + } + + public void clearNumber() { + this.number = -1; + this.numBytes = -1; + } + + public String toString() { + return Integer.toString(number); + } + + @Override + public boolean equals(Object entry) { + assert entry instanceof BytesNumber; + BytesNumber bnEntry = (BytesNumber)entry; + return this.number == bnEntry.getNumber() + && this.numBytes == bnEntry.getNumBytes(); + } + + @Override + public int hashCode() { + int hash = 1; + hash = hash * 17 + number; + hash = hash * 31 + numBytes; + return hash; + } + } + + private void writeCharsetTable(boolean cidFont) throws IOException { + if (cidFont) { + writeByte(2); + for (int entry : gidToSID.keySet()) { + if (entry == 0) { + continue; + } + writeCard16(entry); + writeCard16(gidToSID.size() - 1); + break; + } + } else { + writeByte(0); + for (int entry : gidToSID.values()) { + if (entry == 0) { + continue; + } + writeCard16(entry); + } + } + } + + protected void writePrivateDict() throws IOException { + Map topDICT = cffReader.getTopDictEntries(); + + DICTEntry privateEntry = topDICT.get("Private"); + if (privateEntry != null) { + writeBytes(cffReader.getPrivateDictBytes(privateEntry)); + } + } + + protected void updateOffsets(Offsets offsets) throws IOException { + Map topDICT = cffReader.getTopDictEntries(); + Map privateDICT = null; + + DICTEntry privateEntry = topDICT.get("Private"); + if (privateEntry != null) { + privateDICT = cffReader.getPrivateDict(privateEntry); + } + + updateFixedOffsets(topDICT, offsets); + + if (privateDICT != null) { + //Private index offset in the top dict + int oldPrivateOffset = offsets.topDictData + privateEntry.getOffset(); + updateOffset(oldPrivateOffset + privateEntry.getOperandLengths().get(0), + privateEntry.getOperandLengths().get(1), offsets.privateDict); + + //Update the local subroutine index offset in the private dict + DICTEntry subroutines = privateDICT.get("Subrs"); + if (subroutines != null) { + int oldLocalSubrOffset = offsets.privateDict + subroutines.getOffset(); + updateOffset(oldLocalSubrOffset, subroutines.getOperandLength(), + (offsets.localIndex - offsets.privateDict)); + } + } + } + + protected void updateFixedOffsets(Map topDICT, Offsets offsets) throws IOException { + //Charset offset in the top dict + DICTEntry charset = topDICT.get("charset"); + int oldCharsetOffset = offsets.topDictData + charset.getOffset(); + updateOffset(oldCharsetOffset, charset.getOperandLength(), offsets.charset); + + //Char string index offset in the private dict + DICTEntry charString = topDICT.get("CharStrings"); + int oldCharStringOffset = offsets.topDictData + charString.getOffset(); + updateOffset(oldCharStringOffset, charString.getOperandLength(), offsets.charString); + + DICTEntry encodingEntry = topDICT.get("Encoding"); + if (encodingEntry != null && encodingEntry.getOperands().get(0).intValue() != 0 + && encodingEntry.getOperands().get(0).intValue() != 1) { + int oldEncodingOffset = offsets.topDictData + encodingEntry.getOffset(); + updateOffset(oldEncodingOffset, encodingEntry.getOperandLength(), offsets.encoding); + } + } + + protected void updateCIDOffsets(Offsets offsets) throws IOException { + Map topDict = cffReader.getTopDictEntries(); + + DICTEntry fdArrayEntry = topDict.get("FDArray"); + if (fdArrayEntry != null) { + updateOffset(offsets.topDictData + fdArrayEntry.getOffset() - 1, + fdArrayEntry.getOperandLength(), offsets.fdArray); + } + + DICTEntry fdSelect = topDict.get("FDSelect"); + if (fdSelect != null) { + updateOffset(offsets.topDictData + fdSelect.getOffset() - 1, + fdSelect.getOperandLength(), offsets.fdSelect); + } + + updateFixedOffsets(topDict, offsets); + } + + private void updateOffset(int position, int length, int replacement) throws IOException { + byte[] outBytes = output.toByteArray(); + updateOffset(outBytes, position, length, replacement); + output.reset(); + output.write(outBytes); + } + + private void updateOffset(byte[] out, int position, int length, int replacement) { + switch (length) { + case 1: + out[position] = (byte)(replacement + 139); + break; + case 2: + assert replacement <= 1131; + if (replacement <= -876) { + out[position] = (byte)254; + } else if (replacement <= -620) { + out[position] = (byte)253; + } else if (replacement <= -364) { + out[position] = (byte)252; + } else if (replacement <= -108) { + out[position] = (byte)251; + } else if (replacement <= 363) { + out[position] = (byte)247; + } else if (replacement <= 619) { + out[position] = (byte)248; + } else if (replacement <= 875) { + out[position] = (byte)249; + } else { + out[position] = (byte)250; + } + if (replacement > 0) { + out[position + 1] = (byte)(replacement - 108); + } else { + out[position + 1] = (byte)(-replacement - 108); + } + break; + case 3: + assert replacement <= 32767; + out[position] = (byte)28; + out[position + 1] = (byte)((replacement >> 8) & 0xFF); + out[position + 2] = (byte)(replacement & 0xFF); + break; + case 5: + out[position] = (byte)29; + out[position + 1] = (byte)((replacement >> 24) & 0xFF); + out[position + 2] = (byte)((replacement >> 16) & 0xFF); + out[position + 3] = (byte)((replacement >> 8) & 0xFF); + out[position + 4] = (byte)(replacement & 0xFF); + break; + default: + } + } + + /** + * Returns the parsed CFF data for the original font. + * @return The CFFDataReader contaiing the parsed data + */ + public CFFDataReader getCFFReader() { + return cffReader; + } +}