diff --git a/Makefile b/Makefile index 814f450..3855b68 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.00 +JAR_VERSION := 2.01 JAR_FILE := mn2pdf-$(JAR_VERSION).jar all: target/$(JAR_FILE) diff --git a/README.adoc b/README.adoc index 4668062..2fa958f 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.00.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] +java -Xss5m -Xmx2048m -jar target/mn2pdf-2.01.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] ---- e.g. [source,sh] ---- -java -Xss5m -Xmx2048m -jar target/mn2pdf-2.00.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.01.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.00 +2.01 Metanorma XML to PDF converter ---- @@ -111,8 +111,8 @@ Tag the same version in Git: [source,xml] ---- -git tag v2.00 -git push origin v2.00 +git tag v2.01 +git push origin v2.01 ---- Then the corresponding GitHub release will be automatically created at: diff --git a/pom.xml b/pom.xml index 7765ca5..fc16ff4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.metanorma.fop mn2pdf - 2.00 + 2.01 Metanorma XML to PDF converter jar https://www.metanorma.org diff --git a/src/main/java/org/apache/fop/pdf/PDFFactory.java b/src/main/java/org/apache/fop/pdf/PDFFactory.java new file mode 100644 index 0000000..20891f6 --- /dev/null +++ b/src/main/java/org/apache/fop/pdf/PDFFactory.java @@ -0,0 +1,1716 @@ +/* + * 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.pdf; + +// Java +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.java2d.color.NamedColorSpace; +import org.apache.xmlgraphics.xmp.Metadata; + +import org.apache.fop.events.EventBroadcaster; +import org.apache.fop.fonts.CIDFont; +import org.apache.fop.fonts.CodePointMapping; +import org.apache.fop.fonts.CustomFont; +import org.apache.fop.fonts.EmbeddingMode; +import org.apache.fop.fonts.FontDescriptor; +import org.apache.fop.fonts.FontMetrics; +import org.apache.fop.fonts.FontType; +import org.apache.fop.fonts.LazyFont; +import org.apache.fop.fonts.MultiByteFont; +import org.apache.fop.fonts.SimpleSingleByteEncoding; +import org.apache.fop.fonts.SingleByteEncoding; +import org.apache.fop.fonts.SingleByteFont; +import org.apache.fop.fonts.Typeface; +import org.apache.fop.fonts.truetype.FontFileReader; +import org.apache.fop.fonts.truetype.OFFontLoader; +import org.apache.fop.fonts.truetype.OTFSubSetFile; +import org.apache.fop.fonts.truetype.TTFSubSetFile; +import org.apache.fop.fonts.type1.PFBData; +import org.apache.fop.fonts.type1.PFBParser; +import org.apache.fop.fonts.type1.Type1SubsetFile; + +/** + * This class provides method to create and register PDF objects. + */ +public class PDFFactory { + + /** Resolution of the User Space coordinate system (72dpi). */ + public static final int DEFAULT_PDF_RESOLUTION = 72; + + private PDFDocument document; + + private Log log = LogFactory.getLog(PDFFactory.class); + + private int subsetFontCounter = -1; + private Map dparts = new HashMap(); + private EventBroadcaster eventBroadcaster; + + /** + * Creates a new PDFFactory. + * @param document the parent PDFDocument needed to register the generated + * objects + */ + public PDFFactory(PDFDocument document) { + this.document = document; + } + + /** + * Returns the parent PDFDocument associated with this factory. + * @return PDFDocument the parent PDFDocument + */ + public final PDFDocument getDocument() { + return this.document; + } + + /* ========================= structure objects ========================= */ + + /** + * Make a /Catalog (Root) object. This object is written in + * the trailer. + * + * @param pages the pages pdf object that the root points to + * @return the new pdf root object for this document + */ + public PDFRoot makeRoot(PDFPages pages) { + //Make a /Pages object. This object is written in the trailer. + PDFRoot pdfRoot = new PDFRoot(document, pages); + pdfRoot.setDocument(getDocument()); + getDocument().addTrailerObject(pdfRoot); + return pdfRoot; + } + + /** + * Make a /Pages object. This object is written in the trailer. + * + * @return a new PDF Pages object for adding pages to + */ + public PDFPages makePages() { + PDFPages pdfPages = new PDFPages(getDocument()); + pdfPages.setDocument(getDocument()); + getDocument().addTrailerObject(pdfPages); + return pdfPages; + } + + /** + * Make a /Resources object. This object is written in the trailer. + * + * @return a new PDF resources object + */ + public PDFResources makeResources() { + PDFResources pdfResources = new PDFResources(getDocument()); + pdfResources.setDocument(getDocument()); + getDocument().addTrailerObject(pdfResources); + return pdfResources; + } + + /** + * make an /Info object + * + * @param prod string indicating application producing the PDF + * @return the created /Info object + */ + protected PDFInfo makeInfo(String prod) { + + /* + * create a PDFInfo with the next object number and add to + * list of objects + */ + PDFInfo pdfInfo = new PDFInfo(); + // set the default producer + pdfInfo.setProducer(prod); + getDocument().registerObject(pdfInfo); + return pdfInfo; + } + + /** + * Make a Metadata object. + * @param meta the DOM Document containing the XMP metadata. + * @param readOnly true if the metadata packet should be marked read-only + * @return the newly created Metadata object + */ + public PDFMetadata makeMetadata(Metadata meta, boolean readOnly) { + PDFMetadata pdfMetadata = new PDFMetadata(meta, readOnly); + getDocument().registerObject(pdfMetadata); + return pdfMetadata; + } + + /** + * Make a OutputIntent dictionary. + * @return the newly created OutputIntent dictionary + */ + public PDFOutputIntent makeOutputIntent() { + PDFOutputIntent outputIntent = new PDFOutputIntent(); + getDocument().registerObject(outputIntent); + return outputIntent; + } + + /** + * Make a /Page object. The page is assigned an object number immediately + * so references can already be made. The page must be added to the + * PDFDocument later using addObject(). + * + * @param resources resources object to use + * @param pageIndex index of the page (zero-based) + * @param mediaBox the MediaBox area + * @param cropBox the CropBox area + * @param bleedBox the BleedBox area + * @param trimBox the TrimBox area + * + * @return the created /Page object + */ + public PDFPage makePage(PDFResources resources, int pageIndex, + Rectangle2D mediaBox, Rectangle2D cropBox, + Rectangle2D bleedBox, Rectangle2D trimBox) { + /* + * create a PDFPage with the next object number, the given + * resources, contents and dimensions + */ + PDFPage page = new PDFPage(resources, pageIndex, mediaBox, cropBox, bleedBox, trimBox); + getDocument().assignObjectNumber(page); + getDocument().getPages().addPage(page); + return page; + } + + /** + * Make a /Page object. The page is assigned an object number immediately + * so references can already be made. The page must be added to the + * PDFDocument later using addObject(). + * + * @param resources resources object to use + * @param pageWidth width of the page in points + * @param pageHeight height of the page in points + * @param pageIndex index of the page (zero-based) + * + * @return the created /Page object + */ + public PDFPage makePage(PDFResources resources, + int pageWidth, int pageHeight, int pageIndex) { + Rectangle2D mediaBox = new Rectangle2D.Double(0, 0, pageWidth, pageHeight); + return makePage(resources, pageIndex, mediaBox, mediaBox, mediaBox, mediaBox); + } + + /** + * Make a /Page object. The page is assigned an object number immediately + * so references can already be made. The page must be added to the + * PDFDocument later using addObject(). + * + * @param resources resources object to use + * @param pageWidth width of the page in points + * @param pageHeight height of the page in points + * + * @return the created /Page object + */ + public PDFPage makePage(PDFResources resources, + int pageWidth, int pageHeight) { + return makePage(resources, pageWidth, pageHeight, -1); + } + + /* ========================= functions ================================= */ + + /** + * make a type Exponential interpolation function + * (for shading usually) + * @param domain List objects of Double objects. + * This is the domain of the function. + * See page 264 of the PDF 1.3 Spec. + * @param range List of Doubles that is the Range of the function. + * See page 264 of the PDF 1.3 Spec. + * @param cZero This is a vector of Double objects which defines the function result + * when x=0. + * + * This attribute is optional. + * It's described on page 268 of the PDF 1.3 spec. + * @param cOne This is a vector of Double objects which defines the function result + * when x=1. + * + * This attribute is optional. + * It's described on page 268 of the PDF 1.3 spec. + * @param interpolationExponentN This is the inerpolation exponent. + * + * This attribute is required. + * PDF Spec page 268 + * + * @return the PDF function that was created + */ + public PDFFunction makeFunction(List domain, List range, float[] cZero, float[] cOne, + double interpolationExponentN) { + PDFFunction function = new PDFFunction(domain, range, cZero, cOne, interpolationExponentN); + function = registerFunction(function); + return function; + } + + /** + * Registers a function against the document + * @param function The function to register + */ + public PDFFunction registerFunction(PDFFunction function) { + PDFFunction oldfunc = getDocument().findFunction(function); + if (oldfunc == null) { + getDocument().registerObject(function); + } else { + function = oldfunc; + } + return function; + } + + /* ========================= shadings ================================== */ + + /** + * Registers a shading object against the document + * @param res The PDF resource context + * @param shading The shading object to be registered + */ + public PDFShading registerShading(PDFResourceContext res, PDFShading shading) { + PDFShading oldshad = getDocument().findShading(shading); + if (oldshad == null) { + getDocument().registerObject(shading); + } else { + shading = oldshad; + } + + // add this shading to resources + if (res != null) { + res.addShading(shading); + } + return shading; + } + + /* ========================= patterns ================================== */ + + /** + * Make a tiling pattern + * + * @param res the PDF resource context to add the shading, may be null + * @param thePatternType the type of pattern, which is 1 for tiling. + * @param theResources the resources associated with this pattern + * @param thePaintType 1 or 2, colored or uncolored. + * @param theTilingType 1, 2, or 3, constant spacing, no distortion, or faster tiling + * @param theBBox List of Doubles: The pattern cell bounding box + * @param theXStep horizontal spacing + * @param theYStep vertical spacing + * @param theMatrix Optional List of Doubles transformation matrix + * @param theXUID Optional vector of Integers that uniquely identify the pattern + * @param thePatternDataStream The stream of pattern data to be tiled. + * @return the PDF pattern that was created + */ + public PDFPattern makePattern(PDFResourceContext res, int thePatternType, + PDFResources theResources, int thePaintType, int theTilingType, + List theBBox, double theXStep, + double theYStep, List theMatrix, + List theXUID, StringBuffer thePatternDataStream) { + // PDFResources theResources + PDFPattern pattern = new PDFPattern(theResources, 1, + thePaintType, theTilingType, + theBBox, theXStep, theYStep, + theMatrix, theXUID, + thePatternDataStream); + + PDFPattern oldpatt = getDocument().findPattern(pattern); + if (oldpatt == null) { + getDocument().registerObject(pattern); + } else { + pattern = oldpatt; + } + + if (res != null) { + res.addPattern(pattern); + } + + return (pattern); + } + + public PDFPattern registerPattern(PDFResourceContext res, PDFPattern pattern) { + PDFPattern oldpatt = getDocument().findPattern(pattern); + if (oldpatt == null) { + getDocument().registerObject(pattern); + } else { + pattern = oldpatt; + } + + if (res != null) { + res.addPattern(pattern); + } + return pattern; + } + + + /* ============= named destinations and the name dictionary ============ */ + + /** + * Registers and returns newdest if it is unique. Otherwise, returns + * the equal destination already present in the document. + * + * @param newdest a new, as yet unregistered destination + * @return newdest if unique, else the already registered instance + */ + protected PDFDestination getUniqueDestination(PDFDestination newdest) { + PDFDestination existing = getDocument().findDestination(newdest); + if (existing != null) { + return existing; + } else { + getDocument().addDestination(newdest); + return newdest; + } + } + + /** + * Make a named destination. + * + * @param idRef ID Reference for this destination (the name of the destination) + * @param goToRef Object reference to the GoTo Action + * @return the newly created destrination + */ + public PDFDestination makeDestination(String idRef, Object goToRef) { + PDFDestination destination = new PDFDestination(idRef, goToRef); + return getUniqueDestination(destination); + } + + /** + * Make a names dictionary (the /Names object). + * @return the new PDFNames object + */ + public PDFNames makeNames() { + PDFNames names = new PDFNames(); + getDocument().assignObjectNumber(names); + getDocument().addTrailerObject(names); + return names; + } + + /** + * Make a names dictionary (the /PageLabels object). + * @return the new PDFPageLabels object + */ + public PDFPageLabels makePageLabels() { + PDFPageLabels pageLabels = new PDFPageLabels(); + getDocument().assignObjectNumber(pageLabels); + getDocument().addTrailerObject(pageLabels); + return pageLabels; + } + + /** + * Make a the head object of the name dictionary (the /Dests object). + * + * @param destinationList a list of PDFDestination instances + * @return the new PDFDests object + */ + public PDFDests makeDests(List destinationList) { + PDFDests dests; + + //TODO: Check why the below conditional branch is needed. Condition is always true... + final boolean deep = true; + //true for a "deep" structure (one node per entry), true for a "flat" structure + if (deep) { + dests = new PDFDests(); + PDFArray kids = new PDFArray(dests); + for (Object aDestinationList : destinationList) { + PDFDestination dest = (PDFDestination) aDestinationList; + PDFNameTreeNode node = new PDFNameTreeNode(); + getDocument().registerObject(node); + node.setLowerLimit(dest.getIDRef()); + node.setUpperLimit(dest.getIDRef()); + node.setNames(new PDFArray(node)); + PDFArray names = node.getNames(); + names.add(dest); + kids.add(node); + } + dests.setLowerLimit(((PDFNameTreeNode)kids.get(0)).getLowerLimit()); + dests.setUpperLimit(((PDFNameTreeNode)kids.get(kids.length() - 1)).getUpperLimit()); + dests.setKids(kids); + } else { + dests = new PDFDests(destinationList); + } + getDocument().registerObject(dests); + return dests; + } + + /** + * Make a name tree node. + * + * @return the new name tree node + */ + public PDFNameTreeNode makeNameTreeNode() { + PDFNameTreeNode node = new PDFNameTreeNode(); + getDocument().registerObject(node); + return node; + } + + /* ========================= links ===================================== */ + // Some of the "yoffset-only" functions in this part are obsolete and can + // possibly be removed or deprecated. Some are still called by PDFGraphics2D + // (although that could be changed, they don't need the yOffset param anyway). + + /** + * Create a PDF link to an existing PDFAction object + * + * @param rect the hotspot position in absolute coordinates + * @param pdfAction the PDFAction that this link refers to + * @return the new PDFLink object, or null if either rect or pdfAction is null + */ + public PDFLink makeLink(Rectangle2D rect, PDFAction pdfAction) { + if (rect == null || pdfAction == null) { + return null; + } else { + PDFLink link = new PDFLink(rect); + link.setAction(pdfAction); + getDocument().registerObject(link); + return link; + // does findLink make sense? I mean, how often will it happen that several + // links have the same target *and* the same hot rect? And findLink has to + // walk and compare the entire link list everytime you call it... + } + } + + /** + * Make an internal link. + * + * @param rect the hotspot position in absolute coordinates + * @param page the target page reference value + * @param dest the position destination + * @return the new PDF link object + */ + public PDFLink makeLink(Rectangle2D rect, String page, String dest) { + PDFLink link = new PDFLink(rect); + getDocument().registerObject(link); + + PDFGoTo gt = new PDFGoTo(page); + gt.setDestination(dest); + getDocument().registerObject(gt); + PDFInternalLink internalLink = new PDFInternalLink(gt.referencePDF()); + link.setAction(internalLink); + + return link; + } + + /** + * Make an internal link. + * + * @param rect the hotspot position in absolute coordinates + * @param dest the position destination + * @param isNamedDestination set to true if dest param is a named destination + * @return the new PDF link object + */ + public PDFLink makeLink(Rectangle2D rect, String dest, boolean isNamedDestination) { + PDFLink link = new PDFLink(rect); + getDocument().registerObject(link); + + PDFAction pdfAction = new PDFGoTo(dest, isNamedDestination); + getDocument().registerObject(pdfAction); + + link.setAction(pdfAction); + + return link; + } + + /** + * Make a {@link PDFLink} object + * + * @param rect the clickable rectangle + * @param destination the destination file + * @param linkType the link type + * @param yoffset the yoffset on the page for an internal link + * @return the PDFLink object created + */ + public PDFLink makeLink(Rectangle2D rect, String destination, + int linkType, float yoffset) { + + //PDFLink linkObject; + PDFLink link = new PDFLink(rect); + + if (linkType == PDFLink.EXTERNAL) { + link.setAction(getExternalAction(destination, false)); + } else { + // linkType is internal + String goToReference = getGoToReference(destination, yoffset); + PDFInternalLink internalLink = new PDFInternalLink(goToReference); + link.setAction(internalLink); + } + + PDFLink oldlink = getDocument().findLink(link); + if (oldlink == null) { + getDocument().registerObject(link); + } else { + link = oldlink; + } + + return link; + } + + /** + * Create/find and return the appropriate external PDFAction according to the target + * + * @param target The external target. This may be a PDF file name + * (optionally with internal page number or destination) or any type of URI. + * @param newWindow boolean indicating whether the target should be + * displayed in a new window + * @return the PDFAction thus created or found + */ + public PDFAction getExternalAction(String target, boolean newWindow) { + URI uri = getTargetUri(target); + if (uri != null) { + String scheme = uri.getScheme(); + String filename = uri.getPath(); + if (filename == null) { + filename = uri.getSchemeSpecificPart(); + } + if (scheme == null && filename.toLowerCase().endsWith(".pdf")) { + scheme = "file"; + } + if (scheme == null) { + return new PDFUri(uri.toASCIIString()); + } else if (scheme.equalsIgnoreCase("embedded-file")) { + return getActionForEmbeddedFile(filename, newWindow); + } else if (scheme.equalsIgnoreCase("file")) { + if (filename.startsWith("//")) { + filename = filename.replace("/", "\\"); + } else if (filename.matches("^/[A-z]:/.*")) { + filename = filename.substring(1); + } + if (filename.toLowerCase().endsWith(".pdf")) { + int page = -1; + String dest = null; + String fragment = uri.getFragment(); + if (fragment != null) { + String fragmentLo = fragment.toLowerCase(); + if (fragmentLo.startsWith("page=")) { + page = Integer.parseInt(fragmentLo.substring(5)); + } else if (fragmentLo.startsWith("dest=")) { + dest = fragment.substring(5); + } + } + return getGoToPDFAction(filename, dest, page, newWindow); + } else { + if (uri.getQuery() != null || uri.getFragment() != null) { + return new PDFUri(uri.toASCIIString()); + } else { + return getLaunchAction(filename, newWindow); + } + } + } else { + return new PDFUri(uri.toASCIIString()); + } + } + return new PDFUri(target); + } + + private URI getTargetUri(String target) { + URI uri; + try { + uri = new URI(target); + String scheme = uri.getScheme(); + String schemeSpecificPart = uri.getSchemeSpecificPart(); + String authority = uri.getAuthority(); + if (scheme == null && schemeSpecificPart.matches("//.*")) { + uri = getFileUri(target); + } else if ((scheme == null) && schemeSpecificPart.matches("/.*")) { + uri = getFileUri(target); + } else if (scheme != null && scheme.matches("[A-z]")) { + uri = getFileUri(target); + } else if (scheme != null && scheme.equalsIgnoreCase("file") && authority != null) { + uri = getFileUri(target); + } + } catch (URISyntaxException e) { + uri = getFileUri(target); + } + return uri; + } + + private URI getFileUri(String target) { + URI uri; + String scheme = null; + String fragment = null; + String filename = target; + int index; + String targetLo = target.toLowerCase(); + if (((index = targetLo.indexOf(".pdf#page=")) > 0) + || ((index = targetLo.indexOf(".pdf#dest=")) > 0)) { + filename = target.substring(0, index + 4); + fragment = target.substring(index + 5); + } + + if (targetLo.startsWith("file://")) { + scheme = "file"; + filename = filename.substring("file://".length()); + } else if (targetLo.startsWith("embedded-file:")) { + scheme = "embedded-file"; + filename = filename.substring("embedded-file:".length()); + } else if (targetLo.startsWith("file:")) { + scheme = "file"; + filename = filename.substring("file:".length()); + } + + try { + filename = filename.replace("\\", "/"); + if (filename.matches("[A-z]:.*")) { + scheme = (scheme == null) ? "file" : scheme; + filename = "/" + filename; + } else if (filename.matches("//.*")) { + scheme = (scheme == null) ? "file" : scheme; + filename = "//" + filename; + } else if (filename.matches("/.*")) { + scheme = (scheme == null) ? "file" : scheme; + } + uri = new URI(scheme, filename, fragment); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + + return uri; + } + + private PDFAction getActionForEmbeddedFile(String filename, boolean newWindow) { + PDFNames names = getDocument().getRoot().getNames(); + if (names == null) { + throw new IllegalStateException( + "No Names dictionary present." + + " Cannot create Launch Action for embedded file: " + filename); + } + PDFNameTreeNode embeddedFiles = names.getEmbeddedFiles(); + if (embeddedFiles == null) { + throw new IllegalStateException( + "No /EmbeddedFiles name tree present." + + " Cannot create Launch Action for embedded file: " + filename); + } + + //Find filespec reference for the embedded file + PDFArray files = embeddedFiles.getNames(); + PDFFileSpec fileSpec = null; + int i = 0; + while (i < files.length()) { + i++; + PDFReference ref = (PDFReference)files.get(i); + if (ref.getObject() instanceof PDFFileSpec + && ((PDFFileSpec)ref.getObject()).getUnicodeFilename().equals(filename)) { + fileSpec = (PDFFileSpec)ref.getObject(); + break; + } + i++; + } + if (fileSpec == null) { + throw new IllegalStateException( + "No embedded file with name " + filename + " present."); + } + + //Finally create the action + //PDFLaunch action = new PDFLaunch(embeddedFileRef); + //This works with Acrobat 8 but not with Acrobat 9 + + //The following two options didn't seem to have any effect. + //PDFGoToEmbedded action = new PDFGoToEmbedded(embeddedFileRef, 0, newWindow); + //PDFGoToRemote action = new PDFGoToRemote(embeddedFileRef, 0, newWindow); + + //This finally seems to work: + StringBuffer scriptBuffer = new StringBuffer(); + scriptBuffer.append("this.exportDataObject({cName:\""); + scriptBuffer.append(fileSpec.getFilename()); + scriptBuffer.append("\", nLaunch:2});"); + + PDFAction action; + if (fileSpec.getLinkAsFileAnnotation() != null && fileSpec.getLinkAsFileAnnotation().equals("true")) { + action = new PDFFileAttachmentAnnotation(fileSpec); + } else { + action = new PDFJavaScriptLaunchAction(scriptBuffer.toString()); + } + + return action; + } + + /** + * Create or find a PDF GoTo with the given page reference string and Y offset, + * and return its PDF object reference + * + * @param pdfPageRef the PDF page reference, e.g. "23 0 R" + * @param yoffset the distance from the bottom of the page in points + * @return the GoTo's object reference + */ + public String getGoToReference(String pdfPageRef, float yoffset) { + return getPDFGoTo(pdfPageRef, new Point2D.Float(0.0f, yoffset)).referencePDF(); + } + + /** + * Finds and returns a PDFGoTo to the given page and position. + * Creates the PDFGoTo if not found. + * + * @param pdfPageRef the PDF page reference + * @param position the (X,Y) position in points + * + * @return the new or existing PDFGoTo object + */ + public PDFGoTo getPDFGoTo(String pdfPageRef, Point2D position) { + getDocument().getProfile().verifyActionAllowed(); + PDFGoTo gt = new PDFGoTo(pdfPageRef, position); + PDFGoTo oldgt = getDocument().findGoTo(gt); + if (oldgt == null) { + getDocument().assignObjectNumber(gt); + getDocument().addTrailerObject(gt); + } else { + gt = oldgt; + } + return gt; + } + + /** + * Create and return a goto pdf document action. + * This creates a pdf files spec and pdf goto remote action. + * It also checks available pdf objects so it will not create an + * object if it already exists. + * + * @param file the pdf file name + * @param dest the remote name destination, may be null + * @param page the remote page number, -1 means not specified + * @param newWindow boolean indicating whether the target should be + * displayed in a new window + * @return the pdf goto remote object + */ + private PDFGoToRemote getGoToPDFAction(String file, String dest, int page, boolean newWindow) { + getDocument().getProfile().verifyActionAllowed(); + PDFFileSpec fileSpec = new PDFFileSpec(file); + PDFFileSpec oldspec = getDocument().findFileSpec(fileSpec); + if (oldspec == null) { + getDocument().registerObject(fileSpec); + } else { + fileSpec = oldspec; + } + PDFGoToRemote remote; + + if (dest == null && page == -1) { + remote = new PDFGoToRemote(fileSpec, newWindow); + } else if (dest != null) { + remote = new PDFGoToRemote(fileSpec, dest, newWindow); + } else { + remote = new PDFGoToRemote(fileSpec, page, newWindow); + } + PDFGoToRemote oldremote = getDocument().findGoToRemote(remote); + if (oldremote == null) { + getDocument().registerObject(remote); + } else { + remote = oldremote; + } + return remote; + } + + /** + * Creates and returns a launch pdf document action using + * file to create a file specification for + * the document/file to be opened with an external application. + * + * @param file the pdf file name + * @param newWindow boolean indicating whether the target should be + * displayed in a new window + * @return the pdf launch object + */ + private PDFLaunch getLaunchAction(String file, boolean newWindow) { + getDocument().getProfile().verifyActionAllowed(); + + PDFFileSpec fileSpec = new PDFFileSpec(file); + PDFFileSpec oldSpec = getDocument().findFileSpec(fileSpec); + + if (oldSpec == null) { + getDocument().registerObject(fileSpec); + } else { + fileSpec = oldSpec; + } + PDFLaunch launch = new PDFLaunch(fileSpec, newWindow); + PDFLaunch oldLaunch = getDocument().findLaunch(launch); + + if (oldLaunch == null) { + getDocument().registerObject(launch); + } else { + launch = oldLaunch; + } + + return launch; + } + + /** + * Make an outline object and add it to the given parent + * + * @param parent the parent PDFOutline object (may be null) + * @param label the title for the new outline object + * @param actionRef the action reference string to be placed after the /A + * @param showSubItems whether to initially display child outline items + * @return the new PDF outline object + */ + public PDFOutline makeOutline(PDFOutline parent, String label, + PDFReference actionRef, boolean showSubItems) { + PDFOutline pdfOutline = new PDFOutline(label, actionRef, showSubItems); + if (parent != null) { + parent.addOutline(pdfOutline); + } + getDocument().registerObject(pdfOutline); + return pdfOutline; + } + + /** + * Make an outline object and add it to the given parent + * + * @param parent the parent PDFOutline object (may be null) + * @param label the title for the new outline object + * @param pdfAction the action that this outline item points to - must not be null! + * @param showSubItems whether to initially display child outline items + * @return the new PDFOutline object, or null if pdfAction is null + */ + public PDFOutline makeOutline(PDFOutline parent, String label, + PDFAction pdfAction, boolean showSubItems) { + return pdfAction == null + ? null + : makeOutline(parent, label, new PDFReference(pdfAction.getAction()), showSubItems); + } + + // This one is obsolete now, at least it isn't called from anywhere inside FOP + /** + * Make an outline object and add it to the given outline + * + * @param parent parent PDFOutline object which may be null + * @param label the title for the new outline object + * @param destination the reference string for the action to go to + * @param yoffset the yoffset on the destination page + * @param showSubItems whether to initially display child outline items + * @return the new PDF outline object + */ + public PDFOutline makeOutline(PDFOutline parent, String label, + String destination, float yoffset, + boolean showSubItems) { + + String goToRef = getGoToReference(destination, yoffset); + return makeOutline(parent, label, new PDFReference(goToRef), showSubItems); + } + + + /* ========================= fonts ===================================== */ + + /** + * make a /Encoding object + * + * @param encodingName character encoding scheme name + * @return the created /Encoding object + */ + public PDFEncoding makeEncoding(String encodingName) { + PDFEncoding encoding = new PDFEncoding(encodingName); + + getDocument().registerObject(encoding); + return encoding; + } + + /** + * Make a Type1 /Font object. + * + * @param fontname internal name to use for this font (eg "F1") + * @param basefont name of the base font (eg "Helvetica") + * @param encoding character encoding scheme used by the font + * @param metrics additional information about the font + * @param descriptor additional information about the font + * @return the created /Font object + */ + public PDFFont makeFont(String fontname, String basefont, + String encoding, FontMetrics metrics, + FontDescriptor descriptor) { + PDFFont preRegisteredfont = getDocument().findFont(fontname); + if (preRegisteredfont != null) { + return preRegisteredfont; + } + + boolean forceToUnicode = true; + + if (descriptor == null) { + //Usually Base 14 fonts + PDFFont font = new PDFFont(fontname, FontType.TYPE1, basefont, encoding); + getDocument().registerObject(font); + if (forceToUnicode && !PDFEncoding.isPredefinedEncoding(encoding)) { + SingleByteEncoding mapping; + if (encoding != null) { + mapping = CodePointMapping.getMapping(encoding); + } else { + //for Symbol and ZapfDingbats where encoding must be null in PDF + Typeface tf = (Typeface)metrics; + mapping = CodePointMapping.getMapping(tf.getEncodingName()); + } + generateToUnicodeCmap(font, mapping); + } + return font; + } else { + FontType fonttype = metrics.getFontType(); + + String fontPrefix = descriptor.isSubsetEmbedded() ? createSubsetFontPrefix() : ""; + + String subsetFontName = fontPrefix + basefont; + + PDFFontDescriptor pdfdesc = makeFontDescriptor(descriptor, fontPrefix); + + PDFFont font = null; + + font = PDFFont.createFont(fontname, fonttype, subsetFontName, null); + if (descriptor instanceof RefPDFFont) { + font.setObjectNumber(((RefPDFFont)descriptor).getRef().getObjectNumber()); + font.setDocument(getDocument()); + getDocument().addObject(font); + } else { + getDocument().registerObject(font); + } + + if ((fonttype == FontType.TYPE0 || fonttype == FontType.CIDTYPE0)) { + font.setEncoding(encoding); + CIDFont cidMetrics; + if (metrics instanceof LazyFont) { + cidMetrics = (CIDFont)((LazyFont) metrics).getRealFont(); + } else { + cidMetrics = (CIDFont)metrics; + } + PDFCIDSystemInfo sysInfo = new PDFCIDSystemInfo(cidMetrics.getRegistry(), + cidMetrics.getOrdering(), cidMetrics.getSupplement()); + sysInfo.setDocument(document); + assert pdfdesc instanceof PDFCIDFontDescriptor; + PDFCIDFont cidFont = new PDFCIDFont(subsetFontName, cidMetrics.getCIDType(), + cidMetrics.getDefaultWidth(), getFontWidths(cidMetrics), sysInfo, + (PDFCIDFontDescriptor) pdfdesc); + getDocument().registerObject(cidFont); + + PDFCMap cmap; + if (cidMetrics instanceof MultiByteFont && ((MultiByteFont) cidMetrics).getCmapStream() != null) { + cmap = new PDFCMap("fop-ucs-H", null); + try { + cmap.setData(IOUtils.toByteArray(((MultiByteFont) cidMetrics).getCmapStream())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + cmap = new PDFToUnicodeCMap(cidMetrics.getCIDSet().getChars(), "fop-ucs-H", + new PDFCIDSystemInfo("Adobe", "Identity", 0), false, eventBroadcaster); + } + getDocument().registerObject(cmap); + assert font instanceof PDFFontType0; + ((PDFFontType0)font).setCMAP(cmap); + ((PDFFontType0)font).setDescendantFonts(cidFont); + } else if (fonttype == FontType.TYPE1C + && (metrics instanceof LazyFont || metrics instanceof MultiByteFont)) { + handleType1CFont(pdfdesc, font, metrics, fontname, basefont, descriptor); + } else { + assert font instanceof PDFFontNonBase14; + PDFFontNonBase14 nonBase14 = (PDFFontNonBase14)font; + nonBase14.setDescriptor(pdfdesc); + + SingleByteFont singleByteFont; + if (metrics instanceof LazyFont) { + singleByteFont = (SingleByteFont)((LazyFont)metrics).getRealFont(); + } else { + singleByteFont = (SingleByteFont)metrics; + } + + int firstChar = 0; + int lastChar = 0; + boolean defaultChars = false; + if (singleByteFont.getEmbeddingMode() == EmbeddingMode.SUBSET) { + Map usedGlyphs = singleByteFont.getUsedGlyphs(); + if (fonttype == FontType.TYPE1 && usedGlyphs.size() > 0) { + SortedSet keys = new TreeSet(usedGlyphs.keySet()); + keys.remove(0); + if (keys.size() > 0) { + firstChar = keys.first(); + lastChar = keys.last(); + int[] newWidths = new int[(lastChar - firstChar) + 1]; + for (int i = firstChar; i < lastChar + 1; i++) { + if (usedGlyphs.get(i) != null) { + if (i - singleByteFont.getFirstChar() < metrics.getWidths().length) { + newWidths[i - firstChar] = metrics.getWidths()[i + - singleByteFont.getFirstChar()]; + } else { + defaultChars = true; + break; + } + } else { + newWidths[i - firstChar] = 0; + } + } + nonBase14.setWidthMetrics(firstChar, + lastChar, + new PDFArray(null, newWidths)); + } + } else { + defaultChars = true; + } + } else { + defaultChars = true; + } + + if (defaultChars) { + firstChar = singleByteFont.getFirstChar(); + lastChar = singleByteFont.getLastChar(); + nonBase14.setWidthMetrics(firstChar, + lastChar, + new PDFArray(null, metrics.getWidths())); + } + + //Handle encoding + SingleByteEncoding mapping = singleByteFont.getEncoding(); + if (singleByteFont.isSymbolicFont()) { + //no encoding, use the font's encoding + if (forceToUnicode) { + generateToUnicodeCmap(nonBase14, mapping); + } + } else if (PDFEncoding.isPredefinedEncoding(mapping.getName())) { + font.setEncoding(mapping.getName()); + //No ToUnicode CMap necessary if PDF 1.4, chapter 5.9 (page 368) is to be + //believed. + } else if (mapping.getName().equals("FOPPDFEncoding")) { + if (fonttype == FontType.TRUETYPE) { + font.setEncoding(encoding); + } else { + String[] charNameMap = mapping.getCharNameMap(); + char[] intmap = mapping.getUnicodeCharMap(); + PDFArray differences = new PDFArray(); + int len = intmap.length; + if (charNameMap.length < len) { + len = charNameMap.length; + } + int last = Integer.MIN_VALUE; + for (int i = 0; i < len; i++) { + if (intmap[i] - 1 != last) { + differences.add(intmap[i]); + } + last = intmap[i]; + differences.add(new PDFName(charNameMap[i])); + } + PDFEncoding pdfEncoding = new PDFEncoding(singleByteFont.getEncodingName()); + getDocument().registerObject(pdfEncoding); + pdfEncoding.setDifferences(differences); + font.setEncoding(pdfEncoding); + if (mapping.getUnicodeCharMap() != null) { + generateToUnicodeCmap(nonBase14, mapping); + } + } + } else { + Object pdfEncoding = createPDFEncoding(mapping, + singleByteFont.getFontName()); + if (pdfEncoding instanceof PDFEncoding) { + font.setEncoding((PDFEncoding)pdfEncoding); + } else { + font.setEncoding((String)pdfEncoding); + } + if (forceToUnicode) { + generateToUnicodeCmap(nonBase14, mapping); + } + } + + //Handle additional encodings (characters outside the primary encoding) + if (singleByteFont.hasAdditionalEncodings()) { + for (int i = 0, c = singleByteFont.getAdditionalEncodingCount(); i < c; i++) { + SimpleSingleByteEncoding addEncoding + = singleByteFont.getAdditionalEncoding(i); + String name = fontname + "_" + (i + 1); + Object pdfenc = createPDFEncoding(addEncoding, + singleByteFont.getFontName()); + PDFFontNonBase14 addFont = (PDFFontNonBase14)PDFFont.createFont( + name, fonttype, + basefont, pdfenc); + addFont.setDescriptor(pdfdesc); + addFont.setWidthMetrics( + addEncoding.getFirstChar(), + addEncoding.getLastChar(), + new PDFArray(null, singleByteFont.getAdditionalWidths(i))); + getDocument().registerObject(addFont); + getDocument().getResources().addFont(addFont); + if (forceToUnicode) { + generateToUnicodeCmap(addFont, addEncoding); + } + } + } + } + + return font; + } + } + + private void handleType1CFont(PDFFontDescriptor pdfdesc, PDFFont font, FontMetrics metrics, String fontname, + String basefont, FontDescriptor descriptor) { + PDFFontNonBase14 nonBase14 = (PDFFontNonBase14)font; + nonBase14.setDescriptor(pdfdesc); + MultiByteFont singleByteFont; + if (metrics instanceof LazyFont) { + singleByteFont = (MultiByteFont)((LazyFont)metrics).getRealFont(); + } else { + singleByteFont = (MultiByteFont)metrics; + } + Map usedGlyphs = singleByteFont.getUsedGlyphs(); + SortedSet keys = new TreeSet(usedGlyphs.keySet()); + keys.remove(0); + int count = keys.size(); + Iterator usedGlyphNames = singleByteFont.getUsedGlyphNames().values().iterator(); + count = setupFontMetrics(nonBase14, pdfdesc, usedGlyphNames, 0, count, metrics); + List additionalEncodings = addAdditionalEncodings(metrics, descriptor, fontname, basefont); + for (int j = 0; j < additionalEncodings.size(); j++) { + PDFFontNonBase14 additional = additionalEncodings.get(j); + int start = 256 * (j + 1); + count = setupFontMetrics(additional, pdfdesc, usedGlyphNames, start, count, metrics); + } + } + + private int setupFontMetrics(PDFFontNonBase14 font, PDFFontDescriptor pdfdesc, + Iterator usedGlyphNames, int start, int count, FontMetrics metrics) { + font.setDescriptor(pdfdesc); + PDFArray differences = new PDFArray(); + int firstChar = 0; + differences.add(firstChar); + int lastChar = Math.min(count, 255); + int[] newWidths = new int[lastChar + 1]; + for (int i = 0; i < newWidths.length; i++) { + newWidths[i] = metrics.getWidth(start + i, 1); + differences.add(new PDFName(usedGlyphNames.next())); + count--; + } + font.setWidthMetrics(firstChar, + lastChar, + new PDFArray(null, newWidths)); + PDFEncoding pdfEncoding = new PDFEncoding("WinAnsiEncoding"); + getDocument().registerTrailerObject(pdfEncoding); + pdfEncoding.setDifferences(differences); + font.setEncoding(pdfEncoding); + return count; + } + + private List addAdditionalEncodings(FontMetrics metrics, FontDescriptor descriptor, + String fontname, String basefont) { + List additionalEncodings = new ArrayList(); + FontType fonttype = metrics.getFontType(); + if (descriptor != null && (fonttype != FontType.TYPE0)) { + CustomFont singleByteFont; + if (metrics instanceof LazyFont) { + singleByteFont = (CustomFont)((LazyFont)metrics).getRealFont(); + } else { + singleByteFont = (CustomFont)metrics; + } + //Handle additional encodings (characters outside the primary encoding) + if (singleByteFont.hasAdditionalEncodings()) { + for (int i = additionalEncodings.size(), + c = singleByteFont.getAdditionalEncodingCount(); i < c; i++) { + SimpleSingleByteEncoding addEncoding + = singleByteFont.getAdditionalEncoding(i); + String name = fontname + "_" + (i + 1); + Object pdfenc = createPDFEncoding(addEncoding, singleByteFont.getFontName()); + PDFFontNonBase14 addFont = (PDFFontNonBase14)PDFFont.createFont( + name, fonttype, + basefont, pdfenc); + getDocument().registerObject(addFont); + getDocument().getResources().addFont(addFont); + additionalEncodings.add(addFont); + } + } + } + return additionalEncodings; + } + + private void generateToUnicodeCmap(PDFFont font, SingleByteEncoding encoding) { + PDFCMap cmap = new PDFToUnicodeCMap(encoding.getUnicodeCharMap(), + "fop-ucs-H", + new PDFCIDSystemInfo("Adobe", "Identity", 0), true, eventBroadcaster); + getDocument().registerObject(cmap); + font.setToUnicode(cmap); + } + + /** + * Creates a PDFEncoding instance from a CodePointMapping instance. + * @param encoding the code point mapping (encoding) + * @param fontName ... + * @return the PDF Encoding dictionary (or a String with the predefined encoding) + */ + public Object createPDFEncoding(SingleByteEncoding encoding, String fontName) { + return PDFEncoding.createPDFEncoding(encoding, fontName); + } + + private PDFWArray getFontWidths(CIDFont cidFont) { + // Create widths for reencoded chars + PDFWArray warray = new PDFWArray(); + if (cidFont instanceof MultiByteFont && ((MultiByteFont)cidFont).getWidthsMap() != null) { + Map map = ((MultiByteFont)cidFont).getWidthsMap(); + for (Map.Entry cid : map.entrySet()) { + warray.addEntry(cid.getKey(), new int[] {cid.getValue()}); + } +// List l = new ArrayList(map.keySet()); +// for (int i=0; i cids = new ArrayList(); +// cids.add(map.get(cid)); +// while (i> /F (" + annotationF + ") /Type /F\n" + + "/AFRelationship " + relationship + "\n" + + ">>\n" + + "/Contents (" + annotationDesc + ")\n" + + "/Name /Paperclip\n" + + "\n");*/ + sb.append("/FileAttachment\n" + + "/FS " + objNumber + "\n" + + "/Contents (" + annotationDesc + ")\n" + + "/Name /Paperclip\n" + + "\n"); + + return sb.toString(); + } + + /** {@inheritDoc} */ + public String toPDFString() { + StringBuffer sb = new StringBuffer(64); + sb.append("/FS " + objId); + sb.append("\n"); + return sb.toString(); + } + /** {@inheritDoc} */ + protected boolean contentEquals(PDFObject obj) { + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof PDFJavaScriptLaunchAction)) { + return false; + } + + PDFFileAttachmentAnnotation launch = (PDFFileAttachmentAnnotation) obj; + + if (!launch.objId.equals(objId )) { + return false; + } + + return true; + } + +} diff --git a/src/main/java/org/apache/fop/pdf/PDFFileSpec.java b/src/main/java/org/apache/fop/pdf/PDFFileSpec.java new file mode 100644 index 0000000..4e35f51 --- /dev/null +++ b/src/main/java/org/apache/fop/pdf/PDFFileSpec.java @@ -0,0 +1,125 @@ +/* + * 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.pdf; + +/** + * Class representing a /FileSpec object. + */ +public class PDFFileSpec extends PDFDictionary { + + private String linkAsFileAnnotation = "false"; + + /** + * create a /FileSpec object. + * + * @param filename the filename represented by this object + */ + public PDFFileSpec(String filename) { + this(filename, filename); + } + + /** + * create a /FileSpec object. + * + * @param filename the filename represented by this object + * @param unicodeFilename the unicode filename represented by this object + */ + public PDFFileSpec(String filename, String unicodeFilename) { + put("Type", new PDFName("Filespec")); + put("F", filename); + put("UF", unicodeFilename); // for non-ascii filenames, since PDF 1.7, 3.10.2 + } + + /** + * Gets the filename. + * @return filename + */ + public String getFilename() { + return (String)get("F"); + } + + /** + * Gets the unicode filename + * @return unicode filename + */ + public String getUnicodeFilename() { + return (String)get("UF"); + } + + /** + * Associates an dictionary with pointers to embedded file streams with this file spec. + * @param embeddedFileDict the dictionary with pointers to embedded file streams + */ + public void setEmbeddedFile(PDFDictionary embeddedFileDict) { + put("EF", embeddedFileDict); + } + + /** + * Sets a description for the file spec. + * @param description the description + * @since PDF 1.6 + */ + public void setDescription(String description) { + put("Desc", description); + } + + /** + * Sets a AFRelationship for the file spec. + * @param relationship the AFRelationship + */ + public void setAFRelationship(String relationship) { + put("AFRelationship", new PDFName(relationship)); + } + + /** + * Sets a linkAsFileAnnotation for the file spec. + * @param linkAsFileAnnotation the indication of file attachment annotation + */ + public void setLinkAsFileAnnotation(String linkAsFileAnnotation) { + this.linkAsFileAnnotation = linkAsFileAnnotation; + } + + /** + * Gets the linkAsFileAnnotation. + * @return linkAsFileAnnotation + */ + public String getLinkAsFileAnnotation() { + return linkAsFileAnnotation; + } + + /** {@inheritDoc} */ + protected boolean contentEquals(PDFObject obj) { + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof PDFFileSpec)) { + return false; + } + + PDFFileSpec spec = (PDFFileSpec)obj; + + if (!spec.getFilename().equals(getFilename())) { + return false; + } + + return true; + } +} diff --git a/src/main/java/org/apache/fop/pdf/PDFLink.java b/src/main/java/org/apache/fop/pdf/PDFLink.java new file mode 100644 index 0000000..86753ae --- /dev/null +++ b/src/main/java/org/apache/fop/pdf/PDFLink.java @@ -0,0 +1,176 @@ +/* + * 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.pdf; + +// Java +import java.awt.geom.Rectangle2D; +import java.util.Set; + +/** + * class representing an /Annot object of /Subtype /Link + */ +public class PDFLink extends PDFObject { + /** + * Used to represent an external link. + */ + public static final int EXTERNAL = 0; + + /** + * Used to represent an internal link. + */ + public static final int INTERNAL = 1; + + private float ulx; + private float uly; + private float brx; + private float bry; + private String color; + private PDFAction action; + private Integer structParent; + + /** + * create objects associated with a link annotation (GoToR) + * + * @param r the rectangle of the link hotspot in absolute coordinates + */ + public PDFLink(Rectangle2D r) { + /* generic creation of PDF object */ + super(); + + this.ulx = (float)r.getX(); + this.uly = (float)r.getY(); + this.brx = (float)(r.getX() + r.getWidth()); + this.bry = (float)(r.getY() + r.getHeight()); + this.color = "0 0 0"; // just for now + + } + + /** + * Set the pdf action for this link. + * @param action the pdf action that is activated for this link + */ + public void setAction(PDFAction action) { + this.action = action; + } + + + /** + * Sets the value of the StructParent entry for this link. + * + * @param structParent key in the structure parent tree + */ + public void setStructParent(int structParent) { + this.structParent = structParent; + } + + /** + * {@inheritDoc} + */ + public String toPDFString() { + getDocumentSafely().getProfile().verifyAnnotAllowed(); + String fFlag = ""; + if (getDocumentSafely().getProfile().getPDFAMode().isEnabled()) { + int f = 0; + f |= 1 << (3 - 1); //Print, bit 3 + if (!(this.action instanceof PDFFileAttachmentAnnotation)) { + f |= 1 << (4 - 1); //NoZoom, bit 4 + } + f |= 1 << (5 - 1); //NoRotate, bit 5 + fFlag = "/F " + f; + } + String s = "<< /Type /Annot\n" + "/Subtype /Link\n" + "/Rect [ " + + (ulx) + " " + (uly) + " " + + (brx) + " " + (bry) + " ]\n" + "/C [ " + + this.color + " ]\n" + "/Border [ 0 0 0 ]\n" + "/A " + + this.action.getAction() + "\n" + "/H /I\n" + + (this.structParent != null + ? "/StructParent " + this.structParent.toString() + "\n" : "") + + fFlag + "\n>>"; + + if (this.action instanceof PDFFileAttachmentAnnotation) { + PDFFileAttachmentAnnotation pdfFileAttachmentAnnotation = (PDFFileAttachmentAnnotation) this.action; + ulx = brx + 3; + uly+=5; + brx+=10; + bry+=5; + //uly = bry - 10;*/ + s = "<< /Type /Annot /Subtype " + pdfFileAttachmentAnnotation.getFileAttachmentAnnotation() + + "/Rect [ " + + (ulx) + " " + (uly) + " " + + (brx) + " " + (bry) + " ]\n" + "/C [ " + + this.color + " ]\n" + "/Border [ 0 0 0 ]\n" + + (this.structParent != null + ? "/StructParent " + this.structParent.toString() + "\n" : "") + + fFlag + "\n>>"; + } + + return s; + } + + /* + * example + * 19 0 obj + * << + * /Type /Annot + * /Subtype /Link + * /Rect [ 176.032 678.48412 228.73579 692.356 ] + * /C [ 0.86491 0.03421 0.02591 ] + * /Border [ 0 0 1 ] + * /A 28 0 R + * /H /I + * >> + * endobj + */ + + /** {@inheritDoc} */ + protected boolean contentEquals(PDFObject obj) { + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof PDFLink)) { + return false; + } + + PDFLink link = (PDFLink)obj; + + if (!((link.ulx == ulx) && (link.uly == uly) + && (link.brx == brx) && (link.bry == bry))) { + return false; + } + + if (!(link.color.equals(color) + && link.action.getAction().equals(action.getAction()))) { + return false; + } + + return true; + } + + @Override + public void getChildren(Set children) { + super.getChildren(children); + if (action.hasObjectNumber()) { + children.add(action); + } + action.getChildren(children); + } +} + diff --git a/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java b/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java new file mode 100644 index 0000000..94a85a2 --- /dev/null +++ b/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java @@ -0,0 +1,796 @@ +/* + * 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.render.pdf; + +import java.awt.color.ICC_Profile; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil; +import org.apache.xmlgraphics.util.DateFormatUtil; +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema; + +import org.apache.fop.accessibility.Accessibility; +import org.apache.fop.apps.FOUserAgent; +import org.apache.fop.apps.io.InternalResourceResolver; +import org.apache.fop.fo.extensions.ExtensionAttachment; +import org.apache.fop.fo.extensions.xmp.XMPMetadata; +import org.apache.fop.pdf.PDFAMode; +import org.apache.fop.pdf.PDFArray; +import org.apache.fop.pdf.PDFConformanceException; +import org.apache.fop.pdf.PDFDictionary; +import org.apache.fop.pdf.PDFDocument; +import org.apache.fop.pdf.PDFEmbeddedFile; +import org.apache.fop.pdf.PDFEmbeddedFiles; +import org.apache.fop.pdf.PDFEncryptionManager; +import org.apache.fop.pdf.PDFEncryptionParams; +import org.apache.fop.pdf.PDFFileSpec; +import org.apache.fop.pdf.PDFICCBasedColorSpace; +import org.apache.fop.pdf.PDFICCStream; +import org.apache.fop.pdf.PDFInfo; +import org.apache.fop.pdf.PDFLayer; +import org.apache.fop.pdf.PDFMetadata; +import org.apache.fop.pdf.PDFName; +import org.apache.fop.pdf.PDFNames; +import org.apache.fop.pdf.PDFNavigator; +import org.apache.fop.pdf.PDFNull; +import org.apache.fop.pdf.PDFNumber; +import org.apache.fop.pdf.PDFOutputIntent; +import org.apache.fop.pdf.PDFPage; +import org.apache.fop.pdf.PDFPageLabels; +import org.apache.fop.pdf.PDFReference; +import org.apache.fop.pdf.PDFSetOCGStateAction; +import org.apache.fop.pdf.PDFTransitionAction; +import org.apache.fop.pdf.PDFXMode; +import org.apache.fop.pdf.Version; +import org.apache.fop.pdf.VersionController; +import org.apache.fop.render.pdf.extensions.PDFActionExtension; +import org.apache.fop.render.pdf.extensions.PDFArrayExtension; +import org.apache.fop.render.pdf.extensions.PDFCollectionEntryExtension; +import org.apache.fop.render.pdf.extensions.PDFDictionaryAttachment; +import org.apache.fop.render.pdf.extensions.PDFDictionaryExtension; +import org.apache.fop.render.pdf.extensions.PDFDictionaryType; +import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileAttachment; +import org.apache.fop.render.pdf.extensions.PDFObjectType; +import org.apache.fop.render.pdf.extensions.PDFPageExtension; +import org.apache.fop.render.pdf.extensions.PDFReferenceExtension; + +import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_PARAMS; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ACCESSCONTENT; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ANNOTATIONS; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ASSEMBLEDOC; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_COPY_CONTENT; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_EDIT_CONTENT; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_FILLINFORMS; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINT; +import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINTHQ; +import static org.apache.fop.render.pdf.PDFEncryptionOption.OWNER_PASSWORD; +import static org.apache.fop.render.pdf.PDFEncryptionOption.USER_PASSWORD; + + +/** + * Utility class which enables all sorts of features that are not directly connected to the + * normal rendering process. + */ +class PDFRenderingUtil { + + /** logging instance */ + private static Log log = LogFactory.getLog(PDFRenderingUtil.class); + + private FOUserAgent userAgent; + + /** the PDF Document being created */ + private PDFDocument pdfDoc; + + private PDFRendererOptionsConfig rendererConfig; + + /** the ICC stream used as output profile by this document for PDF/A and PDF/X functionality. */ + private PDFICCStream outputProfile; + + /** the default sRGB color space. */ + private PDFICCBasedColorSpace sRGBColorSpace; + + PDFRenderingUtil(FOUserAgent userAgent) { + this.userAgent = userAgent; + initialize(); + } + + private void initialize() { + rendererConfig = PDFRendererOptionsConfig.DEFAULT.merge(createFromUserAgent(userAgent)); + if (rendererConfig.getPDFAMode().isLevelA()) { + // PDF/A Level A requires tagged PDF + userAgent.getRendererOptions().put(Accessibility.ACCESSIBILITY, Boolean.TRUE); + } + } + + protected static PDFRendererOptionsConfig createFromUserAgent(FOUserAgent userAgent) { + Map properties + = new EnumMap(PDFRendererOption.class); + for (PDFRendererOption option : PDFRendererOption.values()) { + Object value = userAgent.getRendererOption(option); + properties.put(option, option.parse(value)); + } + PDFEncryptionParams encryptionConfig = new EncryptionParamsBuilder().createParams(userAgent); + return new PDFRendererOptionsConfig(properties, encryptionConfig); + } + + void mergeRendererOptionsConfig(PDFRendererOptionsConfig config) { + rendererConfig = rendererConfig.merge(config); + } + + private void updateInfo() { + PDFInfo info = pdfDoc.getInfo(); + info.setCreator(userAgent.getCreator()); + info.setCreationDate(userAgent.getCreationDate()); + info.setAuthor(userAgent.getAuthor()); + info.setTitle(userAgent.getTitle()); + info.setSubject(userAgent.getSubject()); + info.setKeywords(userAgent.getKeywords()); + } + + private void updatePDFProfiles() { + pdfDoc.getProfile().setPDFAMode(rendererConfig.getPDFAMode()); + pdfDoc.getProfile().setPDFUAMode(rendererConfig.getPDFUAMode()); + userAgent.setPdfUAEnabled(pdfDoc.getProfile().getPDFUAMode().isEnabled()); + pdfDoc.getProfile().setPDFXMode(rendererConfig.getPDFXMode()); + pdfDoc.getProfile().setPDFVTMode(rendererConfig.getPDFVTMode()); + } + + private void addsRGBColorSpace() throws IOException { + if (rendererConfig.getDisableSRGBColorSpace()) { + if (rendererConfig.getPDFAMode() != PDFAMode.DISABLED + || rendererConfig.getPDFXMode() != PDFXMode.DISABLED + || rendererConfig.getOutputProfileURI() != null) { + throw new IllegalStateException("It is not possible to disable the sRGB color" + + " space if PDF/A or PDF/X functionality is enabled or an" + + " output profile is set!"); + } + } else { + if (this.sRGBColorSpace != null) { + return; + } + //Map sRGB as default RGB profile for DeviceRGB + this.sRGBColorSpace = PDFICCBasedColorSpace.setupsRGBAsDefaultRGBColorSpace(pdfDoc); + } + } + + private void addDefaultOutputProfile() throws IOException { + if (this.outputProfile != null) { + return; + } + ICC_Profile profile; + InputStream in = null; + URI outputProfileUri = rendererConfig.getOutputProfileURI(); + if (outputProfileUri != null) { + this.outputProfile = pdfDoc.getFactory().makePDFICCStream(); + in = userAgent.getResourceResolver().getResource(rendererConfig.getOutputProfileURI()); + try { + profile = ColorProfileUtil.getICC_Profile(in); + } finally { + IOUtils.closeQuietly(in); + } + this.outputProfile.setColorSpace(profile, null); + } else { + //Fall back to sRGB profile + outputProfile = sRGBColorSpace.getICCStream(); + } + } + + /** + * Adds an OutputIntent to the PDF as mandated by PDF/A-1 when uncalibrated color spaces + * are used (which is true if we use DeviceRGB to represent sRGB colors). + * @throws IOException in case of an I/O problem + */ + private void addPDFA1OutputIntent() throws IOException { + addDefaultOutputProfile(); + + String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile()); + PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent(); + outputIntent.setSubtype(PDFOutputIntent.GTS_PDFA1); + outputIntent.setDestOutputProfile(this.outputProfile); + outputIntent.setOutputConditionIdentifier(desc); + outputIntent.setInfo(outputIntent.getOutputConditionIdentifier()); + pdfDoc.getRoot().addOutputIntent(outputIntent); + } + + /** + * Adds an OutputIntent to the PDF as mandated by PDF/X when uncalibrated color spaces + * are used (which is true if we use DeviceRGB to represent sRGB colors). + * @throws IOException in case of an I/O problem + */ + private void addPDFXOutputIntent() throws IOException { + addDefaultOutputProfile(); + + String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile()); + int deviceClass = this.outputProfile.getICCProfile().getProfileClass(); + if (deviceClass != ICC_Profile.CLASS_OUTPUT) { + throw new PDFConformanceException(pdfDoc.getProfile().getPDFXMode() + " requires that" + + " the DestOutputProfile be an Output Device Profile. " + + desc + " does not match that requirement."); + } + PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent(); + outputIntent.setSubtype(PDFOutputIntent.GTS_PDFX); + outputIntent.setDestOutputProfile(this.outputProfile); + outputIntent.setOutputConditionIdentifier(desc); + outputIntent.setInfo(outputIntent.getOutputConditionIdentifier()); + pdfDoc.getRoot().addOutputIntent(outputIntent); + } + + public void renderXMPMetadata(XMPMetadata metadata) { + Metadata docXMP = metadata.getMetadata(); + Metadata fopXMP = PDFMetadata.createXMPFromPDFDocument(pdfDoc); + //Merge FOP's own metadata into the one from the XSL-FO document + List exclude = new ArrayList(); + if (pdfDoc.getProfile().getPDFAMode().isPart1()) { + exclude.add(DublinCoreSchema.class); + } + fopXMP.mergeInto(docXMP, exclude); + XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(docXMP); + //Metadata was changed so update metadata date + xmpBasic.setMetadataDate(new java.util.Date()); + PDFMetadata.updateInfoFromMetadata(docXMP, pdfDoc.getInfo()); + + PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata( + docXMP, metadata.isReadOnly()); + pdfDoc.getRoot().setMetadata(pdfMetadata); + } + + public void generateDefaultXMPMetadata() { + if (pdfDoc.getRoot().getMetadata() == null) { + //If at this time no XMP metadata for the overall document has been set, create it + //from the PDFInfo object. + Metadata xmp = PDFMetadata.createXMPFromPDFDocument(pdfDoc); + PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata( + xmp, true); + pdfDoc.getRoot().setMetadata(pdfMetadata); + } + } + + public void renderDictionaryExtension(PDFDictionaryAttachment attachment, PDFPage currentPage) { + PDFDictionaryExtension extension = attachment.getExtension(); + PDFDictionaryType type = extension.getDictionaryType(); + if (type == PDFDictionaryType.Action) { + addNavigatorAction(extension); + } else if (type == PDFDictionaryType.Layer) { + addLayer(extension); + } else if (type == PDFDictionaryType.Navigator) { + addNavigator(extension); + } else { + renderDictionaryExtension(extension, currentPage); + } + } + + public void addLayer(PDFDictionaryExtension extension) { + assert extension.getDictionaryType() == PDFDictionaryType.Layer; + String id = extension.getProperty(PDFDictionaryExtension.PROPERTY_ID); + if ((id != null) && (id.length() > 0)) { + PDFLayer layer = pdfDoc.getFactory().makeLayer(id); + layer.setResolver(new PDFLayer.Resolver(layer, extension) { + public void performResolution() { + PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); + Object name = extension.findEntryValue("Name"); + Object intent = extension.findEntryValue("Intent"); + Object usage = makeDictionary(extension.findEntryValue("Usage")); + getLayer().populate(name, intent, usage); + } + }); + } + } + + public void addNavigatorAction(PDFDictionaryExtension extension) { + assert extension.getDictionaryType() == PDFDictionaryType.Action; + String id = extension.getProperty(PDFDictionaryExtension.PROPERTY_ID); + if ((id != null) && (id.length() > 0)) { + String type = extension.getProperty(PDFActionExtension.PROPERTY_TYPE); + if (type != null) { + if (type.equals("SetOCGState")) { + PDFSetOCGStateAction action = pdfDoc.getFactory().makeSetOCGStateAction(id); + action.setResolver(new PDFSetOCGStateAction.Resolver(action, extension) { + public void performResolution() { + PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); + Object state = makeArray(extension.findEntryValue("State")); + Object preserveRB = extension.findEntryValue("PreserveRB"); + Object nextAction = makeDictionaryOrArray(extension.findEntryValue("Next")); + getAction().populate(state, preserveRB, nextAction); + } + }); + } else if (type.equals("Trans")) { + PDFTransitionAction action = pdfDoc.getFactory().makeTransitionAction(id); + action.setResolver(new PDFTransitionAction.Resolver(action, extension) { + public void performResolution() { + PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); + Object transition = makeDictionary(extension.findEntryValue("Trans")); + Object nextAction = makeDictionaryOrArray(extension.findEntryValue("Next")); + getAction().populate(transition, nextAction); + } + }); + } else { + throw new UnsupportedOperationException(); + } + } + } + } + + public void addNavigator(PDFDictionaryExtension extension) { + assert extension.getDictionaryType() == PDFDictionaryType.Navigator; + String id = extension.getProperty(PDFDictionaryExtension.PROPERTY_ID); + if ((id != null) && (id.length() > 0)) { + PDFNavigator navigator = pdfDoc.getFactory().makeNavigator(id); + navigator.setResolver(new PDFNavigator.Resolver(navigator, extension) { + public void performResolution() { + PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); + Object nextAction = makeDictionary(extension.findEntryValue("NA")); + Object next = makeDictionary(extension.findEntryValue("Next")); + Object prevAction = makeDictionary(extension.findEntryValue("PA")); + Object prev = makeDictionary(extension.findEntryValue("Prev")); + Object duration = extension.findEntryValue("Dur"); + getNavigator().populate(nextAction, next, prevAction, prev, duration); + } + }); + } + } + + private Object makeArray(Object value) { + if (value == null) { + return null; + } else if (value instanceof PDFReferenceExtension) { + return resolveReference((PDFReferenceExtension) value); + } else if (value instanceof List) { + return populateArray(new PDFArray(), (List) value); + } else { + throw new IllegalArgumentException(); + } + } + + private Object populateArray(PDFArray array, List entries) { + for (PDFCollectionEntryExtension entry : (List) entries) { + PDFObjectType type = entry.getType(); + if (type == PDFObjectType.Array) { + array.add(makeArray(entry.getValue())); + } else if (type == PDFObjectType.Boolean) { + array.add(entry.getValueAsBoolean()); + } else if (type == PDFObjectType.Dictionary) { + array.add(makeDictionary(entry.getValue())); + } else if (type == PDFObjectType.Name) { + array.add(new PDFName(entry.getValueAsString())); + } else if (type == PDFObjectType.Number) { + array.add(new PDFNumber(entry.getValueAsNumber())); + } else if (type == PDFObjectType.Reference) { + assert (entry instanceof PDFReferenceExtension); + array.add(resolveReference((PDFReferenceExtension) entry)); + } else if (type == PDFObjectType.String) { + array.add(entry.getValue()); + } + } + return array; + } + + private Object makeDictionary(Object value) { + if (value == null) { + return null; + } else if (value instanceof PDFReferenceExtension) { + return resolveReference((PDFReferenceExtension) value); + } else if (value instanceof List) { + return populateDictionary(new PDFDictionary(), (List) value); + } else { + throw new IllegalArgumentException(); + } + } + + private Object populateDictionary(PDFDictionary dictionary, List entries) { + for (PDFCollectionEntryExtension entry : (List) entries) { + PDFObjectType type = entry.getType(); + String key = entry.getKey(); + if (type == PDFObjectType.Array) { + dictionary.put(key, makeArray(entry.getValue())); + } else if (type == PDFObjectType.Boolean) { + dictionary.put(key, entry.getValueAsBoolean()); + } else if (type == PDFObjectType.Dictionary) { + dictionary.put(key, makeDictionary(entry.getValue())); + } else if (type == PDFObjectType.Name) { + dictionary.put(key, new PDFName(entry.getValueAsString())); + } else if (type == PDFObjectType.Number) { + dictionary.put(key, new PDFNumber(entry.getValueAsNumber())); + } else if (type == PDFObjectType.Reference) { + assert (entry instanceof PDFReferenceExtension); + dictionary.put(key, resolveReference((PDFReferenceExtension) entry)); + } else if (type == PDFObjectType.String) { + dictionary.put(key, entry.getValue()); + } + } + return dictionary; + } + + private Object makeDictionaryOrArray(Object value) { + if (value == null) { + return null; + } else if (value instanceof PDFReferenceExtension) { + return resolveReference((PDFReferenceExtension) value); + } else if (value instanceof List) { + if (hasKeyedEntry((List) value)) { + return populateDictionary(new PDFDictionary(), (List) value); + } else { + return populateArray(new PDFArray(), (List) value); + } + } else { + throw new IllegalArgumentException(); + } + } + + private boolean hasKeyedEntry(List entries) { + for (PDFCollectionEntryExtension entry : (List) entries) { + if (entry.getKey() != null) { + return true; + } + } + return false; + } + + public void renderDictionaryExtension(PDFDictionaryExtension extension, PDFPage currentPage) { + PDFDictionaryType type = extension.getDictionaryType(); + if (type == PDFDictionaryType.Catalog) { + augmentDictionary(pdfDoc.getRoot(), extension); + } else if (type == PDFDictionaryType.Page) { + assert extension instanceof PDFPageExtension; + if (((PDFPageExtension) extension).matchesPageNumber(currentPage.getPageIndex() + 1)) { + augmentDictionary(currentPage, extension); + renderExtension(currentPage, extension.getExtension()); + } + } else if (type == PDFDictionaryType.Info) { + PDFInfo info = pdfDoc.getInfo(); + for (PDFCollectionEntryExtension entry : extension.getEntries()) { + info.put(entry.getKey(), entry.getValueAsString()); + } + } else if (type == PDFDictionaryType.VT) { + if (currentPage.get("DPart") != null) { + augmentDictionary((PDFDictionary)currentPage.get("DPart"), extension); + } + } else if (type == PDFDictionaryType.PagePiece) { + String date = DateFormatUtil.formatPDFDate(new Date(), TimeZone.getDefault()); + if (currentPage.get("PieceInfo") == null) { + currentPage.put("PieceInfo", new PDFDictionary()); + currentPage.put("LastModified", date); + } + PDFDictionary d = augmentDictionary((PDFDictionary)currentPage.get("PieceInfo"), extension); + d.put("LastModified", date); + } else { + throw new IllegalStateException(); + } + } + + private void renderExtension(PDFPage currentPage, ExtensionAttachment extension) { + if (extension instanceof XMPMetadata) { + XMPMetadata metadata = (XMPMetadata) extension; + Metadata docXMP = metadata.getMetadata(); + PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(docXMP, metadata.isReadOnly()); + currentPage.setMetadata(pdfMetadata); + } + } + + private PDFDictionary augmentDictionary(PDFDictionary dictionary, PDFDictionaryExtension extension) { + for (PDFCollectionEntryExtension entry : extension.getEntries()) { + if (entry instanceof PDFDictionaryExtension) { + String[] keys = entry.getKey().split("/"); + for (int i = 0; i < keys.length; i++) { + if (keys[i].isEmpty()) { + throw new IllegalStateException("pdf:dictionary key: " + entry.getKey() + " not valid"); + } + if (i == keys.length - 1) { + dictionary.put(keys[i], + augmentDictionary(new PDFDictionary(dictionary), (PDFDictionaryExtension) entry)); + } else { + PDFDictionary d = new PDFDictionary(); + dictionary.put(keys[i], d); + dictionary = d; + } + } + } else if (entry instanceof PDFArrayExtension) { + dictionary.put(entry.getKey(), augmentArray(new PDFArray(dictionary), (PDFArrayExtension) entry)); + } else { + augmentDictionary(dictionary, entry); + } + } + return dictionary; + } + + private void augmentDictionary(PDFDictionary dictionary, PDFCollectionEntryExtension entry) { + PDFObjectType type = entry.getType(); + String key = entry.getKey(); + if (type == PDFObjectType.Boolean) { + dictionary.put(key, entry.getValueAsBoolean()); + } else if (type == PDFObjectType.Name) { + dictionary.put(key, new PDFName(entry.getValueAsString())); + } else if (type == PDFObjectType.Number) { + dictionary.put(key, new PDFNumber(entry.getValueAsNumber())); + } else if (type == PDFObjectType.Reference) { + assert entry instanceof PDFReferenceExtension; + dictionary.put(key, resolveReference((PDFReferenceExtension) entry)); + } else if (type == PDFObjectType.String) { + dictionary.put(key, entry.getValueAsString()); + } else { + throw new IllegalStateException(); + } + } + + private Object resolveReference(PDFReferenceExtension entry) { + PDFReference reference = (PDFReference) entry.getResolvedReference(); + if (reference == null) { + reference = pdfDoc.resolveExtensionReference(entry.getReferenceId()); + if (reference != null) { + entry.setResolvedReference(reference); + } + return reference; + } + return PDFNull.INSTANCE; + } + + private PDFArray augmentArray(PDFArray array, PDFArrayExtension extension) { + for (PDFCollectionEntryExtension entry : extension.getEntries()) { + if (entry instanceof PDFDictionaryExtension) { + array.add(augmentDictionary(new PDFDictionary(array), (PDFDictionaryExtension) entry)); + } else if (entry instanceof PDFArrayExtension) { + array.add(augmentArray(new PDFArray(array), (PDFArrayExtension) entry)); + } else { + augmentArray(array, entry); + } + } + return array; + } + + private void augmentArray(PDFArray array, PDFCollectionEntryExtension entry) { + PDFObjectType type = entry.getType(); + if (type == PDFObjectType.Boolean) { + array.add(entry.getValueAsBoolean()); + } else if (type == PDFObjectType.Name) { + array.add(new PDFName(entry.getValueAsString())); + } else if (type == PDFObjectType.Number) { + array.add(new PDFNumber(entry.getValueAsNumber())); + } else if (type == PDFObjectType.Reference) { + assert entry instanceof PDFReferenceExtension; + array.add(resolveReference((PDFReferenceExtension) entry)); + } else if (type == PDFObjectType.String) { + array.add(entry.getValueAsString()); + } else { + throw new IllegalStateException(); + } + } + + public PDFDocument setupPDFDocument(OutputStream out) throws IOException { + if (this.pdfDoc != null) { + throw new IllegalStateException("PDFDocument already set up"); + } + + String producer = userAgent.getProducer() != null ? userAgent.getProducer() : ""; + final Version maxPDFVersion = rendererConfig.getPDFVersion(); + if (maxPDFVersion == null) { + this.pdfDoc = new PDFDocument(producer); + } else { + VersionController controller + = VersionController.getFixedVersionController(maxPDFVersion); + this.pdfDoc = new PDFDocument(producer, controller); + } + pdfDoc.getFactory().setEventBroadcaster(userAgent.getEventBroadcaster()); + updateInfo(); + updatePDFProfiles(); + pdfDoc.setFilterMap(rendererConfig.getFilterMap()); + pdfDoc.outputHeader(out); + + //Setup encryption if necessary + PDFEncryptionManager.setupPDFEncryption(rendererConfig.getEncryptionParameters(), pdfDoc); + + addsRGBColorSpace(); + if (rendererConfig.getOutputProfileURI() != null) { + addDefaultOutputProfile(); + } + PDFXMode pdfXMode = rendererConfig.getPDFXMode(); + if (pdfXMode != PDFXMode.DISABLED) { + log.debug(pdfXMode + " is active."); + log.warn("Note: " + pdfXMode + + " support is work-in-progress and not fully implemented, yet!"); + addPDFXOutputIntent(); + } + PDFAMode pdfAMode = rendererConfig.getPDFAMode(); + if (pdfAMode.isEnabled()) { + log.debug("PDF/A is active. Conformance Level: " + pdfAMode); + addPDFA1OutputIntent(); + } + + this.pdfDoc.enableAccessibility(userAgent.isAccessibilityEnabled()); + pdfDoc.setMergeFontsEnabled(rendererConfig.getMergeFontsEnabled()); + pdfDoc.setMergeFormFieldsEnabled(rendererConfig.getMergeFormFieldsEnabled()); + pdfDoc.setLinearizationEnabled(rendererConfig.getLinearizationEnabled()); + pdfDoc.setFormXObjectEnabled(rendererConfig.getFormXObjectEnabled()); + + return this.pdfDoc; + } + + /** + * Generates a page label in the PDF document. + * @param pageIndex the index of the page + * @param pageNumber the formatted page number + */ + public void generatePageLabel(int pageIndex, String pageNumber) { + //Produce page labels + PDFPageLabels pageLabels = this.pdfDoc.getRoot().getPageLabels(); + if (pageLabels == null) { + //Set up PageLabels + pageLabels = this.pdfDoc.getFactory().makePageLabels(); + this.pdfDoc.getRoot().setPageLabels(pageLabels); + } + pageLabels.addPageLabel(pageIndex, pageNumber); + } + + /** + * Adds an embedded file to the PDF file. + * @param embeddedFile the object representing the embedded file to be added + * @throws IOException if an I/O error occurs + */ + public void addEmbeddedFile(PDFEmbeddedFileAttachment embeddedFile) + throws IOException { + this.pdfDoc.getProfile().verifyEmbeddedFilesAllowed(); + PDFNames names = this.pdfDoc.getRoot().getNames(); + if (names == null) { + //Add Names if not already present + names = this.pdfDoc.getFactory().makeNames(); + this.pdfDoc.getRoot().setNames(names); + } + + //Create embedded file + PDFEmbeddedFile file = new PDFEmbeddedFile(); + this.pdfDoc.registerObject(file); + URI srcURI; + try { + srcURI = InternalResourceResolver.cleanURI(embeddedFile.getSrc()); + } catch (URISyntaxException use) { + throw new RuntimeException(use); + } + InputStream in = userAgent.getResourceResolver().getResource(srcURI); + if (in == null) { + throw new FileNotFoundException(embeddedFile.getSrc()); + } + try { + OutputStream out = file.getBufferOutputStream(); + IOUtils.copyLarge(in, out); + } finally { + IOUtils.closeQuietly(in); + } + PDFDictionary dict = new PDFDictionary(); + dict.put("F", file); + dict.put("UF", file); + PDFFileSpec fileSpec = new PDFFileSpec(embeddedFile.getFilename(), embeddedFile.getUnicodeFilename()); + String filename = fileSpec.getFilename(); + pdfDoc.getRoot().addAF(fileSpec); + fileSpec.setEmbeddedFile(dict); + if (embeddedFile.getDesc() != null) { + fileSpec.setDescription(embeddedFile.getDesc()); + } + if (embeddedFile.getRel() != null) { + fileSpec.setAFRelationship(embeddedFile.getRel()); + } + if (embeddedFile.getLinkAsFileAnnotation() != null) { + fileSpec.setLinkAsFileAnnotation(embeddedFile.getLinkAsFileAnnotation()); + } + this.pdfDoc.registerObject(fileSpec); + + //Make sure there is an EmbeddedFiles in the Names dictionary + PDFEmbeddedFiles embeddedFiles = names.getEmbeddedFiles(); + if (embeddedFiles == null) { + embeddedFiles = new PDFEmbeddedFiles(); + this.pdfDoc.assignObjectNumber(embeddedFiles); + this.pdfDoc.addTrailerObject(embeddedFiles); + names.setEmbeddedFiles(embeddedFiles); + } + + //Add to EmbeddedFiles in the Names dictionary + PDFArray nameArray = embeddedFiles.getNames(); + if (nameArray == null) { + nameArray = new PDFArray(); + embeddedFiles.setNames(nameArray); + } + nameArray.add(filename); + nameArray.add(new PDFReference(fileSpec)); + } + + private static final class EncryptionParamsBuilder { + private PDFEncryptionParams params; + + private EncryptionParamsBuilder() { + } + + private PDFEncryptionParams createParams(FOUserAgent userAgent) { + params = (PDFEncryptionParams) userAgent.getRendererOptions().get(ENCRYPTION_PARAMS); + String userPassword = (String) userAgent.getRendererOption(USER_PASSWORD); + if (userPassword != null) { + getEncryptionParams().setUserPassword(userPassword); + } + String ownerPassword = (String) userAgent.getRendererOption(OWNER_PASSWORD); + if (ownerPassword != null) { + getEncryptionParams().setOwnerPassword(ownerPassword); + } + Object noPrint = userAgent.getRendererOption(NO_PRINT); + if (noPrint != null) { + getEncryptionParams().setAllowPrint(!booleanValueOf(noPrint)); + } + Object noCopyContent = userAgent.getRendererOption(NO_COPY_CONTENT); + if (noCopyContent != null) { + getEncryptionParams().setAllowCopyContent(!booleanValueOf(noCopyContent)); + } + Object noEditContent = userAgent.getRendererOption(NO_EDIT_CONTENT); + if (noEditContent != null) { + getEncryptionParams().setAllowEditContent(!booleanValueOf(noEditContent)); + } + Object noAnnotations = userAgent.getRendererOption(NO_ANNOTATIONS); + if (noAnnotations != null) { + getEncryptionParams().setAllowEditAnnotations(!booleanValueOf(noAnnotations)); + } + Object noFillInForms = userAgent.getRendererOption(NO_FILLINFORMS); + if (noFillInForms != null) { + getEncryptionParams().setAllowFillInForms(!booleanValueOf(noFillInForms)); + } + Object noAccessContent = userAgent.getRendererOption(NO_ACCESSCONTENT); + if (noAccessContent != null) { + getEncryptionParams().setAllowAccessContent(!booleanValueOf(noAccessContent)); + } + Object noAssembleDoc = userAgent.getRendererOption(NO_ASSEMBLEDOC); + if (noAssembleDoc != null) { + getEncryptionParams().setAllowAssembleDocument(!booleanValueOf(noAssembleDoc)); + } + Object noPrintHQ = userAgent.getRendererOption(NO_PRINTHQ); + if (noPrintHQ != null) { + getEncryptionParams().setAllowPrintHq(!booleanValueOf(noPrintHQ)); + } + return params; + } + + private PDFEncryptionParams getEncryptionParams() { + if (params == null) { + params = new PDFEncryptionParams(); + } + return params; + } + + private static boolean booleanValueOf(Object obj) { + if (obj instanceof Boolean) { + return (Boolean) obj; + } else if (obj instanceof String) { + return Boolean.valueOf((String) obj); + } else { + throw new IllegalArgumentException("Boolean or \"true\" or \"false\" expected."); + } + } + } +} diff --git a/src/main/java/org/apache/fop/render/pdf/extensions/PDFEmbeddedFileAttachment.java b/src/main/java/org/apache/fop/render/pdf/extensions/PDFEmbeddedFileAttachment.java new file mode 100644 index 0000000..c2d1971 --- /dev/null +++ b/src/main/java/org/apache/fop/render/pdf/extensions/PDFEmbeddedFileAttachment.java @@ -0,0 +1,236 @@ +/* + * 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.render.pdf.extensions; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import org.apache.fop.pdf.PDFText; + +/** + * This is the pass-through value object for the PDF extension. + */ +public class PDFEmbeddedFileAttachment extends PDFExtensionAttachment { + + private static final long serialVersionUID = -1L; + + /** element name */ + protected static final String ELEMENT = "embedded-file"; + + /** name of file to be embedded */ + private static final String ATT_NAME = "filename"; + + /** source of file to be embedded (URI) */ + private static final String ATT_SRC = "src"; + + /** a description of the file to be embedded */ + private static final String ATT_DESC = "description"; + + /** A relationship between + the component of this PDF document that refers to this file specification and + the associated file denoted by this file specification dictionary */ + private static final String ATT_REL = "afrelationship"; + + /** An indication how to process link to the embedded file */ + private static final String ATT_LINKASFILEANNOTATION = "linkasfileannotation"; + + /** filename attribute */ + private String filename; + + /** unicode filename attribute */ + private String unicodeFilename; + + /** description attribute (optional) */ + private String desc; + + /** source name attribute */ + private String src; + + /** associated file relationship */ + private String rel; + + /** add link as file annotation */ + private String linkAsFileAnnotation; + + /** + * No-argument contructor. + */ + public PDFEmbeddedFileAttachment() { + super(); + } + + /** + * Default constructor. + * @param filename the name of the file + * @param src the location of the file + * @param desc the description of the file + */ + public PDFEmbeddedFileAttachment(String filename, String src, String desc) { + super(); + this.setFilename(filename); + this.src = src; + this.desc = desc; + } + + public PDFEmbeddedFileAttachment(String filename, String src, String desc, String rel) { + super(); + this.setFilename(filename); + this.src = src; + this.desc = desc; + this.rel = rel; + } + + /** + * Returns the file name. + * @return the file name + */ + public String getFilename() { + return filename; + } + + /** + * Returns the unicode file name. + * @return the file name + */ + public String getUnicodeFilename() { + return unicodeFilename; + } + + /** + * Sets the file name. + * @param name The file name to set. + */ + public void setFilename(String name) { + if (!PDFText.toPDFString(name).equals(name)) { + // replace with auto generated filename, because non-ascii chars are used. + this.filename = "att" + name.hashCode(); + } else { + this.filename = name; + } + this.unicodeFilename = name; + } + + /** + * Returns the file description. + * @return the description + */ + public String getDesc() { + return desc; + } + + /** + * Sets the description of the file. + * @param desc the description to set + */ + public void setDesc(String desc) { + this.desc = desc; + } + + /** + * Returns the source URI of the file. + * @return the source URI + */ + public String getSrc() { + return src; + } + + /** + * Sets the source URI of the file. + * @param src the source URI + */ + public void setSrc(String src) { + this.src = src; + } + + /** + * Returns the relationship of the file. + * @return the AFRelationship + */ + public String getRel() { + return rel; + } + + /** + * Sets the relationship of the file. + * @param rel the AFRelationship + */ + public void setRel(String rel) { + this.rel = rel; + } + + /** + * Returns the indication of link as file annotation. + * @return the linkAsFileAnnotation + */ + public String getLinkAsFileAnnotation() { + return linkAsFileAnnotation; + } + + /** + * Sets the indication of link as file annotation. + * @param linkAsFileAnnotation the indication + */ + public void setLinkAsFileAnnotation(String linkAsFileAnnotation) { + this.linkAsFileAnnotation = linkAsFileAnnotation; + } + + /** {@inheritDoc} */ + public String getCategory() { + return CATEGORY; + } + + /** {@inheritDoc} */ + public String toString() { + return "PDFEmbeddedFile(name=" + getFilename() + ", " + getSrc() + ")"; + } + + /** + * @return the element name + */ + protected String getElement() { + return ELEMENT; + } + + /** {@inheritDoc} */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + if (filename != null && filename.length() > 0) { + atts.addAttribute("", ATT_NAME, ATT_NAME, "CDATA", filename); + } + if (src != null && src.length() > 0) { + atts.addAttribute("", ATT_SRC, ATT_SRC, "CDATA", src); + } + if (desc != null && desc.length() > 0) { + atts.addAttribute("", ATT_DESC, ATT_DESC, "CDATA", desc); + } + if (rel != null && rel.length() > 0) { + atts.addAttribute("", ATT_REL, ATT_REL, "CDATA", rel); + } + if (linkAsFileAnnotation != null && linkAsFileAnnotation.length() > 0) { + atts.addAttribute("", ATT_LINKASFILEANNOTATION, ATT_LINKASFILEANNOTATION, "CDATA", linkAsFileAnnotation); + } + + String element = getElement(); + handler.startElement(CATEGORY, element, element, atts); + handler.endElement(CATEGORY, element, element); + } + +} diff --git a/src/main/java/org/apache/fop/render/pdf/extensions/PDFEmbeddedFileElement.java b/src/main/java/org/apache/fop/render/pdf/extensions/PDFEmbeddedFileElement.java new file mode 100644 index 0000000..565cc7d --- /dev/null +++ b/src/main/java/org/apache/fop/render/pdf/extensions/PDFEmbeddedFileElement.java @@ -0,0 +1,114 @@ +/* + * 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.render.pdf.extensions; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; + +import org.apache.fop.apps.FOPException; +import org.apache.fop.datatypes.URISpecification; +import org.apache.fop.fo.Constants; +import org.apache.fop.fo.FONode; +import org.apache.fop.fo.PropertyList; +import org.apache.fop.fo.extensions.ExtensionAttachment; + +/** + * Extension element for pdf:embedded-file. + */ +public class PDFEmbeddedFileElement extends AbstractPDFExtensionElement { + + /** name of element */ + protected static final String ELEMENT = "embedded-file"; + + /** + * Main constructor + * @param parent parent FO node + */ + PDFEmbeddedFileElement(FONode parent) { + super(parent); + } + + @Override + public void startOfNode() throws FOPException { + super.startOfNode(); + if (parent.getNameId() != Constants.FO_DECLARATIONS) { + invalidChildError(getLocator(), parent.getName(), getNamespaceURI(), getName(), + "rule.childOfDeclarations"); + } + } + + @Override + public void processNode(String elementName, Locator locator, + Attributes attlist, PropertyList propertyList) + throws FOPException { + PDFEmbeddedFileAttachment embeddedFile + = (PDFEmbeddedFileAttachment)getExtensionAttachment(); + String desc = attlist.getValue("description"); + if (desc != null && desc.length() > 0) { + embeddedFile.setDesc(desc); + } + String src = attlist.getValue("src"); + src = URISpecification.getURL(src); + if (src != null && src.length() > 0) { + embeddedFile.setSrc(src); + } else { + missingPropertyError("src"); + } + String filename = attlist.getValue("filename"); + if (filename == null || filename.length() == 0) { + try { + URI uri = new URI(src); + String path = uri.getPath(); + int idx = path.lastIndexOf('/'); + if (idx > 0) { + filename = path.substring(idx + 1); + } else { + filename = path; + } + embeddedFile.setFilename(filename); + } catch (URISyntaxException e) { + //Filename could not be deduced from URI + missingPropertyError("name"); + } + } + embeddedFile.setFilename(filename); + String rel = attlist.getValue("afrelationship"); + if (rel != null && rel.length() > 0) { + embeddedFile.setRel(rel); + } + String linkAsFileAnnotation = attlist.getValue("link-as-file-annotation"); + if (linkAsFileAnnotation != null && linkAsFileAnnotation.length() > 0) { + embeddedFile.setLinkAsFileAnnotation(linkAsFileAnnotation); + } + } + + @Override + public String getLocalName() { + return ELEMENT; + } + + @Override + protected ExtensionAttachment instantiateExtensionAttachment() { + return new PDFEmbeddedFileAttachment(); + } +} diff --git a/src/main/java/org/apache/fop/render/pdf/extensions/PDFExtensionHandler.java b/src/main/java/org/apache/fop/render/pdf/extensions/PDFExtensionHandler.java new file mode 100644 index 0000000..3aa8b47 --- /dev/null +++ b/src/main/java/org/apache/fop/render/pdf/extensions/PDFExtensionHandler.java @@ -0,0 +1,256 @@ +/* + * 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.render.pdf.extensions; + +import java.util.Stack; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.DefaultHandler; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPHandler; + +import org.apache.fop.fo.extensions.xmp.XMPContentHandlerFactory; +import org.apache.fop.fo.extensions.xmp.XMPMetadata; +import org.apache.fop.util.ContentHandlerFactory; +import org.apache.fop.util.ContentHandlerFactory.ObjectBuiltListener; + +/** + * ContentHandler (parser) for restoring PDF extension objects from XML. + */ +public class PDFExtensionHandler extends DefaultHandler implements ContentHandlerFactory.ObjectSource { + + /** Logger instance */ + protected static final Log log = LogFactory.getLog(PDFExtensionHandler.class); + + private PDFExtensionAttachment returnedObject; + private ObjectBuiltListener listener; + + // PDFEmbeddedFileAttachment related state + private Attributes lastAttributes; + + // PDFDictionaryAttachment related + private Stack collections = new Stack(); + private boolean captureContent; + private StringBuffer characters; + private XMPHandler xmpHandler; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + if (PDFExtensionAttachment.CATEGORY.equals(uri)) { + if (localName.equals(PDFEmbeddedFileAttachment.ELEMENT)) { + lastAttributes = new AttributesImpl(attributes); + } else if (PDFDictionaryType.Action.elementName().equals(localName)) { + PDFActionExtension action = new PDFActionExtension(); + String id = attributes.getValue(PDFDictionaryElement.ATT_ID); + if (id != null) { + action.setProperty(PDFDictionaryExtension.PROPERTY_ID, id); + } + String type = attributes.getValue(PDFActionElement.ATT_TYPE); + if (type != null) { + action.setProperty(PDFActionExtension.PROPERTY_TYPE, type); + } + collections.push(action); + } else if (PDFObjectType.Array.elementName().equals(localName)) { + PDFArrayExtension array = new PDFArrayExtension(); + String key = attributes.getValue(PDFCollectionEntryElement.ATT_KEY); + if (key != null) { + array.setKey(key); + } + collections.push(array); + } else if (PDFDictionaryType.Catalog.elementName().equals(localName)) { + PDFCatalogExtension catalog = new PDFCatalogExtension(); + collections.push(catalog); + } else if (PDFDictionaryType.Dictionary.elementName().equals(localName)) { + PDFDictionaryExtension dictionary = new PDFDictionaryExtension(); + String key = attributes.getValue(PDFCollectionEntryElement.ATT_KEY); + if (key != null) { + dictionary.setKey(key); + } + collections.push(dictionary); + } else if (PDFDictionaryType.Layer.elementName().equals(localName)) { + PDFLayerExtension layer = new PDFLayerExtension(); + String id = attributes.getValue(PDFDictionaryElement.ATT_ID); + if (id != null) { + layer.setProperty(PDFDictionaryExtension.PROPERTY_ID, id); + } + collections.push(layer); + } else if (PDFDictionaryType.Navigator.elementName().equals(localName)) { + PDFNavigatorExtension navigator = new PDFNavigatorExtension(); + String id = attributes.getValue(PDFDictionaryElement.ATT_ID); + if (id != null) { + navigator.setProperty(PDFDictionaryExtension.PROPERTY_ID, id); + } + collections.push(navigator); + } else if (PDFDictionaryType.Page.elementName().equals(localName)) { + PDFPageExtension page = new PDFPageExtension(); + String pageNumbers = attributes.getValue(PDFPageElement.ATT_PAGE_NUMBERS); + if (pageNumbers != null) { + page.setProperty(PDFPageExtension.PROPERTY_PAGE_NUMBERS, pageNumbers); + } + collections.push(page); + } else if (PDFDictionaryType.Info.elementName().equals(localName)) { + PDFDocumentInformationExtension info = new PDFDocumentInformationExtension(); + collections.push(info); + } else if (PDFDictionaryType.VT.elementName().equals(localName)) { + PDFVTExtension dictionary = new PDFVTExtension(); + collections.push(dictionary); + } else if (PDFDictionaryType.PagePiece.elementName().equals(localName)) { + PDFPagePieceExtension dictionary = new PDFPagePieceExtension(); + collections.push(dictionary); + } else if (PDFObjectType.hasValueOfElementName(localName)) { + PDFCollectionEntryExtension entry; + if (PDFObjectType.Reference.elementName().equals(localName)) { + entry = new PDFReferenceExtension(); + } else { + entry = new PDFCollectionEntryExtension(PDFObjectType.valueOfElementName(localName)); + } + String key = attributes.getValue(PDFCollectionEntryElement.ATT_KEY); + if (key != null) { + entry.setKey(key); + } + if (entry instanceof PDFReferenceExtension) { + String refid = attributes.getValue(PDFReferenceElement.ATT_REFID); + if (refid != null) { + ((PDFReferenceExtension) entry).setReferenceId(refid); + } + } + if (!collections.empty()) { + PDFCollectionExtension collection = collections.peek(); + collection.addEntry(entry); + if (!(entry instanceof PDFReferenceExtension)) { + captureContent = true; + } + } + } else { + throw new SAXException("Unhandled element " + localName + " in namespace: " + uri); + } + } else if (XMPConstants.XMP_NAMESPACE.equals(uri) || xmpHandler != null) { + if (xmpHandler == null) { + xmpHandler = (XMPHandler) new XMPContentHandlerFactory().createContentHandler(); + } + xmpHandler.startElement(uri, localName, qName, attributes); + } else { + log.warn("Unhandled element " + localName + " in namespace: " + uri); + } + } + + @Override + public void characters(char[] data, int start, int length) throws SAXException { + if (captureContent) { + if (characters == null) { + characters = new StringBuffer((length < 16) ? 16 : length); + } + characters.append(data, start, length); + } + if (xmpHandler != null) { + xmpHandler.characters(data, start, length); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (PDFExtensionAttachment.CATEGORY.equals(uri)) { + setExtension(); + if (PDFEmbeddedFileAttachment.ELEMENT.equals(localName)) { + String name = lastAttributes.getValue("filename"); + String src = lastAttributes.getValue("src"); + String desc = lastAttributes.getValue("description"); + this.lastAttributes = null; + this.returnedObject = new PDFEmbeddedFileAttachment(name, src, desc); + } else if (PDFDictionaryType.hasValueOfElementName(localName)) { + if (!collections.empty() && (collections.peek() instanceof PDFDictionaryExtension)) { + PDFDictionaryExtension dictionary = (PDFDictionaryExtension) collections.pop(); + if (!collections.empty()) { + PDFCollectionExtension collectionOuter = collections.peek(); + collectionOuter.addEntry(dictionary); + } else if (dictionary.getDictionaryType() != PDFDictionaryType.Dictionary) { + this.returnedObject = new PDFDictionaryAttachment(dictionary); + } else { + throw new SAXException( + new IllegalStateException("generic dictionary not permitted at outer level")); + } + } else { + throw new SAXException(new IllegalStateException("collections stack is empty or not a dictionary")); + } + } else if (PDFObjectType.Array.elementName().equals(localName)) { + if (!collections.empty() && (collections.peek() instanceof PDFArrayExtension)) { + PDFArrayExtension array = (PDFArrayExtension) collections.pop(); + if (!collections.empty()) { + PDFCollectionExtension collectionOuter = collections.peek(); + collectionOuter.addEntry(array); + } else { + throw new SAXException(new IllegalStateException("array not permitted at outer level")); + } + } else { + throw new SAXException(new IllegalStateException("collections stack is empty or not an array")); + } + } else if (PDFObjectType.hasValueOfElementName(localName)) { + if (!collections.empty()) { + PDFCollectionExtension collection = collections.peek(); + PDFCollectionEntryExtension entry = collection.getLastEntry(); + if (entry != null) { + if (characters != null) { + entry.setValue(characters.toString()); + characters = null; + } + } else { + throw new SAXException(new IllegalStateException("no current entry")); + } + } else { + throw new SAXException(new IllegalStateException("entry not permitted at outer level")); + } + } + } + if (xmpHandler != null) { + xmpHandler.endElement(uri, localName, qName); + } + captureContent = false; + } + + private void setExtension() { + if (xmpHandler != null) { + PDFPageExtension pdfPageExtension = (PDFPageExtension) collections.peek(); + XMPMetadata wrapper = new XMPMetadata(xmpHandler.getMetadata()); + pdfPageExtension.setExtension(wrapper); + xmpHandler = null; + } + } + + @Override + public void endDocument() throws SAXException { + if (listener != null) { + listener.notifyObjectBuilt(getObject()); + } + } + + public Object getObject() { + return returnedObject; + } + + public void setObjectBuiltListener(ObjectBuiltListener listener) { + this.listener = listener; + } +} 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 new file mode 100644 index 0000000..6ff4e34 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java @@ -0,0 +1,912 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.interactive.annotation; + +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 + * (starting with page 390) for details on annotation types. + * + * @author Paul King + */ +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 + */ + + /** + * Constant for an annotation reply type. + */ + public static final String RT_REPLY = "R"; + + /** + * Constant for an annotation reply type. + */ + public static final String RT_GROUP = "Group"; + + /** + * Constructor. + */ + public PDAnnotationMarkup() + { + } + + /** + * Constructor. + * + * @param dict The annotations dictionary. + */ + public PDAnnotationMarkup(COSDictionary dict) + { + super(dict); + } + + /** + * Retrieve the string used as the title of the popup window shown when open and active (by convention this + * identifies who added the annotation). + * + * @return The title of the popup. + */ + public String getTitlePopup() + { + return getCOSObject().getString(COSName.T); + } + + /** + * Set the string used as the title of the popup window shown when open and active (by convention this identifies + * who added the annotation). + * + * @param t The title of the popup. + */ + public void setTitlePopup(String t) + { + getCOSObject().setString(COSName.T, t); + } + + /** + * This will retrieve the popup annotation used for entering/editing the text for this annotation. + * + * @return the popup annotation. + */ + public PDAnnotationPopup getPopup() + { + COSDictionary popup = (COSDictionary) getCOSObject().getDictionaryObject("Popup"); + if (popup != null) + { + return new PDAnnotationPopup(popup); + } + else + { + return null; + } + } + + /** + * This will set the popup annotation used for entering/editing the text for this annotation. + * + * @param popup the popup annotation. + */ + public void setPopup(PDAnnotationPopup popup) + { + getCOSObject().setItem("Popup", popup); + } + + /** + * This will retrieve the constant opacity value used when rendering the annotation (excluing any popup). + * + * @return the constant opacity value. + */ + public float getConstantOpacity() + { + return getCOSObject().getFloat(COSName.CA, 1); + } + + /** + * This will set the constant opacity value used when rendering the annotation (excluing any popup). + * + * @param ca the constant opacity value. + */ + public void setConstantOpacity(float ca) + { + getCOSObject().setFloat(COSName.CA, ca); + } + + /** + * This will retrieve the rich text stream which is displayed in the popup window. + * + * @return the rich text stream. + */ + public String getRichContents() + { + COSBase base = getCOSObject().getDictionaryObject(COSName.RC); + if (base instanceof COSString) + { + return ((COSString) base).getString(); + } + else if (base instanceof COSStream) + { + return ((COSStream) base).toTextString(); + } + else + { + return null; + } + } + + /** + * This will set the rich text stream which is displayed in the popup window. + * + * @param rc the rich text stream. + */ + public void setRichContents(String rc) + { + getCOSObject().setItem(COSName.RC, new COSString(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 + { + return getCOSObject().getDate(COSName.CREATION_DATE); + } + + /** + * This will set the date and time the annotation was created. + * + * @param creationDate the date and time the annotation was created. + */ + public void setCreationDate(Calendar creationDate) + { + getCOSObject().setDate(COSName.CREATION_DATE, creationDate); + } + + /** + * This will retrieve the annotation to which this one is "In Reply To" the actual relationship + * is specified by the RT entry. + * + * @return the other annotation or null if there is none. + * @throws IOException if there is an error creating the other annotation. + */ + public PDAnnotation getInReplyTo() throws IOException + { + COSBase base = getCOSObject().getDictionaryObject("IRT"); + if (base instanceof COSDictionary) + { + return PDAnnotation.createAnnotation(base); + } + return null; + } + + /** + * This will set the annotation to which this one is "In Reply To" the actual relationship is specified by the RT + * entry. + * + * @param irt the annotation this one is "In Reply To". + */ + public void setInReplyTo(PDAnnotation irt) + { + getCOSObject().setItem("IRT", irt); + } + + /** + * This will retrieve the short description of the subject of the annotation. + * + * @return the subject. + */ + public String getSubject() + { + return getCOSObject().getString(COSName.SUBJ); + } + + /** + * This will set the short description of the subject of the annotation. + * + * @param subj short description of the subject. + */ + public void setSubject(String subj) + { + getCOSObject().setString(COSName.SUBJ, subj); + } + + /** + * This will retrieve the Reply Type (relationship) with the annotation in the IRT entry See the RT_* constants for + * the available values. + * + * @return the relationship. + */ + public String getReplyType() + { + return getCOSObject().getNameAsString("RT", RT_REPLY); + } + + /** + * This will set the Reply Type (relationship) with the annotation in the IRT entry See the RT_* constants for the + * available values. + * + * @param rt the reply type. + */ + public void setReplyType(String rt) + { + getCOSObject().setName("RT", rt); + } + + /** + * This will retrieve the intent of the annotation The values and meanings are specific to the actual annotation See + * the IT_* constants for the annotation classes. + * + * @return the intent + */ + public String getIntent() + { + return getCOSObject().getNameAsString(COSName.IT); + } + + /** + * This will set the intent of the annotation The values and meanings are specific to the actual annotation See the + * IT_* constants for the annotation classes. + * + * @param it the intent + */ + public void setIntent(String it) + { + getCOSObject().setName(COSName.IT, it); + } + + /** + * This will return the external data dictionary. + * + * @return the external data dictionary + */ + public PDExternalDataDictionary getExternalData() + { + COSBase exData = this.getCOSObject().getDictionaryObject("ExData"); + if (exData instanceof COSDictionary) + { + return new PDExternalDataDictionary((COSDictionary) exData); + } + return null; + } + + /** + * This will set the external data dictionary. + * + * @param externalData the external data dictionary + */ + public void setExternalData(PDExternalDataDictionary externalData) + { + this.getCOSObject().setItem("ExData", externalData); + } + + /** + * This will set the border style dictionary, specifying the width and dash pattern used in drawing the line. + * + * @param bs the border style dictionary to set. + * + */ + public void setBorderStyle(PDBorderStyleDictionary bs) + { + this.getCOSObject().setItem(COSName.BS, bs); + } + + /** + * This will retrieve the border style dictionary, specifying the width and dash pattern used in drawing the line. + * + * @return the border style dictionary. + */ + 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; + } + + + /** + * 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 new file mode 100644 index 0000000..63fe189 --- /dev/null +++ b/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.pdfbox.pdmodel.interactive.annotation.handlers; + +import java.io.IOException; + +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; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; + +/** + * + * @author Tilman Hausherr + */ +public class PDFileAttachmentAppearanceHandler extends PDAbstractAppearanceHandler +{ + private static final Log LOG = LogFactory.getLog(PDFileAttachmentAppearanceHandler.class); + + public PDFileAttachmentAppearanceHandler(PDAnnotation annotation) + { + super(annotation); + } + + public PDFileAttachmentAppearanceHandler(PDAnnotation annotation, PDDocument document) + { + super(annotation, document); + } + + @Override + public void generateAppearanceStreams() + { + generateNormalAppearance(); + generateRolloverAppearance(); + generateDownAppearance(); + } + + @Override + public void generateNormalAppearance() + { + PDAnnotationFileAttachment annotation = (PDAnnotationFileAttachment) getAnnotation(); + + PDAppearanceContentStream contentStream = null; + try + { + 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); + annotation.getNormalAppearanceStream().setBBox(new PDRectangle(size, size)); + + //TODO support Graph, PushPin, Paperclip, Tag + drawPaperclip(contentStream); + } + catch (IOException e) + { + LOG.error(e); + } + finally + { + IOUtils.closeQuietly(contentStream); + } + } + + /** + * Draw a paperclip. Shape is from + * tablesCellsCountMap = new HashMap<>(); private boolean hasMath = false; @@ -96,6 +97,8 @@ public SourceXMLDocument(String strXML) { private void readMetaInformation() { String element_review = readValue("//*[local-name() = 'review'][1]"); hasAnnotations = element_review.length() != 0; + String element_link_to_attachment = readValue("//*[local-name() = 'link'][@attachment = 'true'][1]"); + hasFileAttachmentAnnotations = element_link_to_attachment.length() != 0; String element_math = readValue("//*[local-name() = 'math'][1]"); hasMath = element_math.length() != 0; //tables without colgroup/col (width) or dl @@ -488,6 +491,11 @@ public boolean hasAnnotations() { return hasAnnotations; } + public boolean hasFileAttachmentAnnotations() { + return hasFileAttachmentAnnotations; + } + + // find tag 'table' or 'dl' public boolean hasTables() { return hasTables; diff --git a/src/main/java/org/metanorma/fop/Util.java b/src/main/java/org/metanorma/fop/Util.java index 9d12497..da7b8d9 100644 --- a/src/main/java/org/metanorma/fop/Util.java +++ b/src/main/java/org/metanorma/fop/Util.java @@ -802,6 +802,16 @@ public static Node parseCSS(String cssString) { return node; } + public static String getFilenameFromPath(String filepath) { + filepath = filepath.replace("\\", "/"); + File file = new File(filepath); + return file.getName(); + /* + String[] filepathComponents = filepath.split("/"); + + return filepathComponents[filepathComponents.length - 1];*/ + } + private static String nodeToString(Node node) { StringWriter sw = new StringWriter(); try { diff --git a/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java new file mode 100644 index 0000000..882d4ef --- /dev/null +++ b/src/main/java/org/metanorma/fop/annotations/FileAttachmentAnnotation.java @@ -0,0 +1,88 @@ +package org.metanorma.fop.annotations; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; +import org.metanorma.utils.LoggerHelper; +import java.io.*; +import java.util.*; +import java.util.logging.Logger; + +/** + * + * @author Alexander Dyuzhev + */ +public class FileAttachmentAnnotation { + + protected static final Logger logger = Logger.getLogger(LoggerHelper.LOGGER_NAME); + + private boolean DEBUG = false; + + public void process(File pdf) throws IOException { + PDDocument document = null; + + try { + document = PDDocument.load(pdf); + + ArrayList embeddedFileAnnotations = new ArrayList<>(); + + int numberOfPages = document.getNumberOfPages(); + for (int pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { + PDPage page = document.getPage(pageIndex); + List annotations = page.getAnnotations(); + + for (PDAnnotation annotation: annotations) { + if (annotation instanceof PDAnnotationFileAttachment) { + annotation.constructAppearances(); + PDFileSpecification f = ((PDAnnotationFileAttachment) annotation).getFile(); + embeddedFileAnnotations.add(f.getFile()); + //annotations.set(annotataionIndex, annotation); + } + } + //document.getPage(pageIndex).setAnnotations(annotations); + } + + // remove attachments which have FileAttachment annotation equivalent + PDDocumentNameDictionary namesDictionary = new PDDocumentNameDictionary(document.getDocumentCatalog()); + PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles(); + if (efTree != null) + { + Map names = efTree.getNames(); + Map newnames = new HashMap<>(); + + Iterator> iter = names.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + PDComplexFileSpecification fileSpec = entry.getValue(); + String embeddedFileName = fileSpec.getFile(); + if(!embeddedFileAnnotations.contains(embeddedFileName)){ + newnames.put(entry.getKey(), entry.getValue()); + } + } + + efTree.setNames(newnames); + namesDictionary.setEmbeddedFiles(efTree); + } + + document.save(pdf); + + } catch (IOException ex) { + logger.severe("Can't read annotation data from PDF."); + ex.printStackTrace(); + } + + + finally { + if( document != null ) { + document.close(); + } + } + + } + +} diff --git a/src/main/resources/xfdf_simple.xsl b/src/main/resources/xfdf_simple.xsl index e012915..87bc2f1 100644 --- a/src/main/resources/xfdf_simple.xsl +++ b/src/main/resources/xfdf_simple.xsl @@ -135,6 +135,7 @@ + print diff --git a/src/test/java/org/metanorma/fop/mn2pdfTests.java b/src/test/java/org/metanorma/fop/mn2pdfTests.java index 2b05c7d..8ee06b6 100644 --- a/src/test/java/org/metanorma/fop/mn2pdfTests.java +++ b/src/test/java/org/metanorma/fop/mn2pdfTests.java @@ -4,30 +4,29 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; +import java.util.Map; import java.util.logging.Handler; import java.util.logging.Logger; import java.util.logging.StreamHandler; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.stream.StreamSource; -import com.steadystate.css.dom.CSSStyleRuleImpl; -import com.steadystate.css.parser.CSSOMParser; -import com.steadystate.css.parser.SACParserCSS3; import org.apache.commons.cli.ParseException; import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.*; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification; import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.PDEncryption; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; import org.apache.pdfbox.text.PDFTextStripper; import org.junit.Rule; import org.junit.Test; @@ -43,14 +42,9 @@ import org.junit.contrib.java.lang.system.SystemOutRule; import org.junit.contrib.java.lang.system.SystemErrRule; import org.metanorma.Constants; -import static org.metanorma.Constants.ERROR_EXIT_CODE; -import static org.metanorma.fop.PDFGenerator.logger; +import org.metanorma.fop.annotations.Annotation; import org.metanorma.utils.LoggerHelper; -import org.w3c.css.sac.InputSource; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Node; -import org.w3c.dom.css.*; public class mn2pdfTests { @@ -363,20 +357,6 @@ public void checkResultedEncryptedPDF() throws ParseException { assertTrue(encryptMetadata == true); } - - - @Test - public void testSyntaxHighlight() throws TransformerException, TransformerConfigurationException { - System.out.println(name.getMethodName()); - String code = "text"; - Node node = Util.syntaxHighlight(code, "xml"); - StringWriter writer = new StringWriter(); - Transformer transformer = TransformerFactory.newInstance().newTransformer(); - transformer.transform(new DOMSource(node), new StreamResult(writer)); - String value = writer.toString(); - String exprectedValue = "<root><a></a><b>text</b><c key='value'/></root>"; - assertTrue(value.equals(exprectedValue)); - } @Test public void successSVGRendering() throws ParseException, IOException { @@ -428,26 +408,55 @@ public void checkSpacesInPDF() throws ParseException { } @Test - public void checkCSSparsing() throws IOException { + public void checkAttachments() throws ParseException { System.out.println(name.getMethodName()); - String cssString = "sourcecode .c, sourcecode .ch {\n" + - " color: #FF0000;\n" + - "}"; - Node xmlNode = Util.parseCSS(cssString); - String xmlStr = nodeToString(xmlNode); - assertEquals("", xmlStr); - } + ClassLoader classLoader = getClass().getClassLoader(); + String fontpath = Paths.get(System.getProperty("buildDirectory"), ".." , "fonts").toString(); + String xml = classLoader.getResource("test_attachments.xml").getFile(); + String xsl = classLoader.getResource("iso.international-standard.xsl").getFile(); + Path pdf = Paths.get(System.getProperty("buildDirectory"), "test.attachments.pdf"); + + String[] args = new String[]{"--font-path", fontpath, "--xml-file", xml, "--xsl-file", xsl, "--pdf-file", pdf.toAbsolutePath().toString()}; + mn2pdf.main(args); + + assertTrue(Files.exists(pdf)); + // check two attachments - one is embedded file, one is fileattachment annotation - private static String nodeToString(Node node) { - StringWriter sw = new StringWriter(); + PDDocument doc; + int countFileAttachmentAnnotation = 0; + int countFileAttachmentEmbedded = 0; try { - Transformer t = TransformerFactory.newInstance().newTransformer(); - t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - t.transform(new DOMSource(node), new StreamResult(sw)); - } catch (TransformerException te) { - System.out.println("nodeToString Transformer Exception"); + doc = PDDocument.load(pdf.toFile()); + + int numberOfPages = doc.getNumberOfPages(); + for (int pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { + PDPage page = doc.getPage(pageIndex); + List annotations = page.getAnnotations(); + + for (PDAnnotation annotation: annotations) { + if (annotation instanceof PDAnnotationFileAttachment) { + countFileAttachmentAnnotation ++; + } + } + //document.getPage(pageIndex).setAnnotations(annotations); + } + + PDDocumentNameDictionary namesDictionary = new PDDocumentNameDictionary(doc.getDocumentCatalog()); + PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles(); + if (efTree != null) + { + Map names = efTree.getNames(); + countFileAttachmentEmbedded = names.size(); + } + + } catch (IOException ex) { + System.out.println(ex.toString()); } - return sw.toString(); + + assertTrue(countFileAttachmentAnnotation == 1); + assertTrue(countFileAttachmentEmbedded == 1); + + } diff --git a/src/test/java/org/metanorma/fop/utilTests.java b/src/test/java/org/metanorma/fop/utilTests.java new file mode 100644 index 0000000..3900e8a --- /dev/null +++ b/src/test/java/org/metanorma/fop/utilTests.java @@ -0,0 +1,158 @@ +package org.metanorma.fop; + +import org.apache.commons.cli.ParseException; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.*; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; +import org.apache.pdfbox.pdmodel.encryption.PDEncryption; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.EnvironmentVariables; +import org.junit.contrib.java.lang.system.ExpectedSystemExit; +import org.junit.contrib.java.lang.system.SystemErrRule; +import org.junit.contrib.java.lang.system.SystemOutRule; +import org.junit.rules.TestName; +import org.metanorma.Constants; +import org.metanorma.utils.LoggerHelper; +import org.w3c.dom.Node; + +import javax.xml.transform.*; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Logger; +import java.util.logging.StreamHandler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class utilTests { + + private static final Logger logger = Logger.getLogger(LoggerHelper.LOGGER_NAME); + + private static OutputStream logCapturingStream; + private static StreamHandler customLogHandler; + + @Rule + public final ExpectedSystemExit exitRule = ExpectedSystemExit.none(); + + @Rule + public final SystemOutRule systemOutRule = new SystemOutRule().enableLog(); + + @Rule + public final SystemErrRule systemErrRule = new SystemErrRule().enableLog(); + + @Rule + public final EnvironmentVariables envVarRule = new EnvironmentVariables(); + + @Rule public TestName name = new TestName(); + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + LoggerHelper.setupLogger(); + } + + @Before + public void attachLogCapturer() + { + logCapturingStream = new ByteArrayOutputStream(); + Handler[] handlers = logger.getParent().getHandlers(); + customLogHandler = new StreamHandler(logCapturingStream, handlers[0].getFormatter()); + logger.addHandler(customLogHandler); + } + + public String getTestCapturedLog() throws IOException + { + customLogHandler.flush(); + return logCapturingStream.toString(); + } + + + @Test + public void testSyntaxHighlight() throws TransformerException, TransformerConfigurationException { + System.out.println(name.getMethodName()); + String code = "text"; + Node node = Util.syntaxHighlight(code, "xml"); + StringWriter writer = new StringWriter(); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.transform(new DOMSource(node), new StreamResult(writer)); + String value = writer.toString(); + String exprectedValue = "<root><a></a><b>text</b><c key='value'/></root>"; + assertTrue(value.equals(exprectedValue)); + } + + @Test + public void checkCSSparsing() throws IOException { + System.out.println(name.getMethodName()); + String cssString = "sourcecode .c, sourcecode .ch {\n" + + " color: #FF0000;\n" + + "}"; + Node xmlNode = Util.parseCSS(cssString); + String xmlStr = nodeToString(xmlNode); + assertEquals("", xmlStr); + } + + private static String nodeToString(Node node) { + StringWriter sw = new StringWriter(); + try { + Transformer t = TransformerFactory.newInstance().newTransformer(); + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + t.transform(new DOMSource(node), new StreamResult(sw)); + } catch (TransformerException te) { + System.out.println("nodeToString Transformer Exception"); + } + return sw.toString(); + } + + @Test + public void testDates() throws IOException { + System.out.println(name.getMethodName()); + String date1 = "20180125T0121"; + Calendar cdate1 = Util.getCalendarDate(date1); + Calendar cdate1_etalon = Calendar.getInstance(); + cdate1_etalon.clear(); + cdate1_etalon.set(2018,0,25,1,21,0); + + assertTrue(cdate1_etalon.compareTo(cdate1) == 0); + + String date2 = "20220422T000000"; + Calendar cdate2 = Util.getCalendarDate(date2); + Calendar cdate2_etalon = Calendar.getInstance(); + cdate2_etalon.clear(); + cdate2_etalon.set(2022,03,22,0,0,0); + assertTrue(cdate2_etalon.compareTo(cdate2) == 0); + + String date3 = "2017-01-01T00:00:00Z"; + Calendar cdate3 = Util.getCalendarDate(date3); + Calendar cdate3_etalon = Calendar.getInstance(); + cdate3_etalon.clear(); + cdate3_etalon.set(2017,0,1,0,0,0); + assertTrue(cdate3_etalon.compareTo(cdate3) == 0); + } + + @Test + public void testFilenameFromPath() throws IOException { + System.out.println(name.getMethodName()); + String file1 = "_test_attachments_attachments/program.c"; + String file2 = "program.c"; + String file3 = "_test_attachments_attachments\\program.c"; + assertTrue(Util.getFilenameFromPath(file1).equals("program.c")); + assertTrue(Util.getFilenameFromPath(file2).equals("program.c")); + assertTrue(Util.getFilenameFromPath(file3).equals("program.c")); + } + +} diff --git a/src/test/resources/iso.international-standard.xsl b/src/test/resources/iso.international-standard.xsl index 6eb8c0f..068c6ec 100644 --- a/src/test/resources/iso.international-standard.xsl +++ b/src/test/resources/iso.international-standard.xsl @@ -11909,20 +11909,28 @@ + + + + + + + - - + + + +          + @@ -17638,6 +17655,39 @@ + + + + + http://www.aiim.org/pdfua/ns/id/ + pdfuaid + PDF/UA identification schema + + + + internal + PDF/UA version identifier + part + Integer + + + internal + PDF/UA amendment identifier + amd + Text + + + internal + PDF/UA corrigenda identifier + corr + Text + + + + + + + @@ -17648,33 +17698,57 @@ - - - - - -   - - + + + + + + + +   + + + + - + - - - ; - + + + + + + + + - - + + + + + + + + - - + + + + + + + + + + + + true @@ -17689,26 +17763,51 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + diff --git a/src/test/resources/test_attachments.xml b/src/test/resources/test_attachments.xml new file mode 100644 index 0000000..dfe37c5 --- /dev/null +++ b/src/test/resources/test_attachments.xml @@ -0,0 +1,203 @@ + + + +Attachment test — Attachment test + +Attachment test + +Attachment test + + + + +ISO 1234:2024ISO 1234:2024(E)urn:iso:std:iso:1234:stage-60.60:enISO 1234ISO 1234:2024(en)1234 +International Organization for Standardization +ISOAgency +International Organization for Standardization +ISO +International Organization for Standardization +ISO2024-01-01en60International Standard602024 +International Organization for Standardization +ISOinternational-standardInternational StandardISOISOISO 1234International Standard

ISO and IEC maintain terminology databases for use in +standardization at the following addresses:

+ +
    +
  • ISO Online browsing platform: available at +

  • +
  • IEC Electropedia: available at +

  • +
+
ScopeSymbols and abbreviated termsAbbreviated termsSymbolsContentsIntroductionForewordAbstractAcknowledgementsTerms and definitionsTerms, definitions, symbols and abbreviated termsTerms, definitions and symbolsTerms, definitions and abbreviated termsNormative referencesBibliographyPrefaceSectionClauseAnnexAppendixcontinuedNo terms and definitions are listed in this document. +For the purposes of this document, the following terms and definitions apply. +The following documents are referred to in the text in such a way that some or all of their content constitutes requirements of this document. For dated references, only the edition cited applies. For undated references, the latest edition of the referenced document (including any amendments) applies.There are no normative references in this document.For the purposes of this document, the terms and definitions given in % apply. +For the purposes of this document, the terms and definitions given in % and the following apply. +[NO INFORMATION AVAILABLE](%)%1 and %2%1 and %2%1 or %2%1 or %2%1 and %2%1 or %2%1 from %2%1 to %2%1, %2%1 %2spellout-ordinaldigits-ordinalNOTENoteNote % to entryListDefinition ListFigureDiagramFormulaFormulaTableRequirementRecommendationPermissionBox(NO ID)Recommendation testRequirement testPermission testRecommendations classRequirements classPermissions classAbstract testConformance classKeyEXAMPLEExamplewherewhereWhole of textdraftinformativenormativemodifiedadaptedDEPRECATEDSOURCEandAll Parts{{ var1 | ordinal_word: '', '' }} editioneditionversionList of figuresList of tablesList of recommendationsJanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecemberObligationDangerWarningCautionImportantSafety PrecautionsEditorial NoteSectionClausePartParagraphChapterPageTableAnnexFigureExampleNoteFormulaAppendixmfncommonsgdualplpreppartadjadvnounverbdeprecatessupersedesnarrowerbroaderequivalentcomparecontrastseesee alsoClauseClausesAnnexAnnexesAppendixAppendixesNoteNotesNote % to entryNotes % to entryListListsFigureFiguresFormulaFormulasTableTablesRequirementRequirementsRecommendationRecommendationsPermissionPermissionsExampleExamplesPartPartsSectionSectionsParagraphParagraphsChapterChaptersPagePagesProvisionProvisionsStatementStatementsDescriptorDescriptorsAll rights reservedReference numberRef. No.Price based on % pagesPriceDate of the first printingDate of the {{ var1 | ordinal_word: '', '' }} printingDescriptorUnder preparation. (Stage at the time of publication %).Withdrawn.Cancelled and replaced by %.DateIndexseesee alsoSecretariat{{ var1 | ordinal_num: 'edition', '' }} EDITIONThis {{ var1 | ordinal_word: 'edition', '' }} edition supersedes the {{ var2 | ordinal_word: 'edition', '' }} editionTerms related toAvailable fromVoting begins onVoting terminates onCorrected versionFast track procedureUDCInternational StandardTechnical SpecificationTechnical ReportPublicly Available SpecificationInternational Workshop AgreementGuideAmendmentTechnical CorrigendumDirectiveCommittee DocumentISO RecommendationPreliminary work itemNew work item proposalWorking draftCommittee draftDraft International StandardDraft Technical SpecificationDraft Technical ReportDraft Publicly Available SpecificationDraft International Workshop AgreementDraft GuideDraft AmendmentDraft Technical CorrigendumDraft DirectiveFinal Draft International StandardFinal Draft Technical SpecificationFinal Draft Technical ReportFinal Draft Publicly Available SpecificationFinal Draft International Workshop AgreementFinal Draft GuideFinal Draft AmendmentFinal Draft Technical CorrigendumFinal Draft DirectiveInternational StandardTechnical SpecificationTechnical ReportPublicly Available SpecificationInternational Workshop AgreementGuideAmendmentTechnical CorrigendumDirectiveReviewWithdrawalProvisionConformance testenLatn
document-scheme2024data:text/x-csrc;base64,I2luY2x1ZGUgPHN0ZGlvLmg+DQppbnQgbWFp +bigpIHsNCiAgIHByaW50ZigiSGVsbG8sIFdvcmxkISIpOw0KICAgcmV0dXJu +IDA7DQp9 +data:application/octet-stream;base64,cHJvZ3JhbSBIZWxsb1dvcmx +kOw0KDQpiZWdpbg0KICAgd3JpdGVsbignSGVsbG8sIFdvcmxkIScpOw0KICA +gcmVhZGtleTsNCmVuZC4NCg== +TOC Heading Levels2HTML TOC Heading Levels2DOC TOC Heading Levels3PDF TOC Heading Levels3 + +Attachment test — Attachment test + +Attachment test + +Attachment test + + + + +ISO 1234:2024ISO 1234:2024(E)urn:iso:std:iso:1234:stage-60.60:enISO 1234ISO 1234:2024(en)1234 +International Organization for Standardization +ISOAgency +International Organization for Standardization +ISO +International Organization for Standardization +ISO2024-01-01enLatn60602024 +International Organization for Standardization +ISOinternational-standardISOISOISO 1234International Standarddocument-scheme2024data:text/x-csrc;base64,I2luY2x1ZGUgPHN0ZGlvLmg+DQppbnQgbWFp +bigpIHsNCiAgIHByaW50ZigiSGVsbG8sIFdvcmxkISIpOw0KICAgcmV0dXJu +IDA7DQp9 +data:application/octet-stream;base64,cHJvZ3JhbSBIZWxsb1dvcmx +kOw0KDQpiZWdpbg0KICAgd3JpdGVsbignSGVsbG8sIFdvcmxkIScpOw0KICA +gcmVhZGtleTsNCmVuZC4NCg== +TOC Heading Levels2HTML TOC Heading Levels2DOC TOC Heading Levels3PDF TOC Heading Levels3 + + + +COPYRIGHT PROTECTED DOCUMENT +© ISO 2024 + +All rights reserved. Unless otherwise specified, or required in the context of its implementation, no part of this publication may be reproduced or utilized otherwise in any form or by any means, electronic or mechanical, including photocopying, or posting on the internet or an intranet, without prior written permission. Permission can be requested from either ISO at the address below or ISO’s member body in the country of the requester. + +ISO copyright office CP 401 • Ch. de Blandonnet 8 CH-1214 Vernier, Geneva Phone: +41 22 749 01 11 Email: Website: www.iso.org + +Published in Switzerland + + + +Scope +The sample PDF attachment: + + + + +Bibliography + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +sourcecode table td { padding: 5px; } +sourcecode table pre { margin: 0; } +sourcecode, sourcecode .w { + color: #444444; +} +sourcecode .cp { + color: #CC00A3; +} +sourcecode .cs { + color: #CC00A3; +} +sourcecode .c, sourcecode .ch, sourcecode .cd, sourcecode .cm, sourcecode .cpf, sourcecode .c1 { + color: #FF0000; +} +sourcecode .kc { + color: #C34E00; +} +sourcecode .kd { + color: #0000FF; +} +sourcecode .kr { + color: #007575; +} +sourcecode .k, sourcecode .kn, sourcecode .kp, sourcecode .kt, sourcecode .kv { + color: #0000FF; +} +sourcecode .s, sourcecode .sb, sourcecode .sc, sourcecode .ld, sourcecode .sd, sourcecode .s2, sourcecode .se, sourcecode .sh, sourcecode .si, sourcecode .sx, sourcecode .sr, sourcecode .s1, sourcecode .ss { + color: #009C00; +} +sourcecode .sa { + color: #0000FF; +} +sourcecode .nb, sourcecode .bp { + color: #C34E00; +} +sourcecode .nt { + color: #0000FF; +} + + + + + +COPYRIGHT PROTECTED DOCUMENT +

© ISO 2024

+ +

All rights reserved. Unless otherwise specified, or required in the context of its implementation, no part of this publication may be reproduced or utilized otherwise in any form or by any means, electronic or mechanical, including photocopying, or posting on the internet or an intranet, without prior written permission. Permission can be requested from either ISO at the address below or ISO’s member body in the country of the requester.

+ +

ISO copyright office
CP 401 • Ch. de Blandonnet 8
CH-1214 Vernier, Geneva
Phone: +41 22 749 01 11
Email:
Website: www.iso.org

+ +

Published in Switzerland

+
+
+
Contents +

Attachment test — Attachment test

+1<tab/>Scope +

The sample PDF attachment: sources/program.c

+
+ + +
+