From cb9936b4c92ee6f4dede21dbd2fe6d3395b9c31c Mon Sep 17 00:00:00 2001 From: Jeremias Maerki Date: Mon, 16 Aug 2010 09:39:34 +0000 Subject: [PATCH] Improvements/Bugfixes for PDF attachments (embedded files): - Acrobat doesn't like indirect objects for the /Length entry of embedded files (make on-the-fly generation more easily configurable). - Added the /Size entry for embedded files to keep Acrobat happy. - Moved the embedded files' Names tree to the end of the PDF to be on the safe side. - Made hyperlinks to embedded files work by using a JavaScript Action (thanks to Matthias Reischenbacher for that idea). - Added sorting to the names tree node because Acrobat relies on it. - Added some filename manipulation code to work around possible encoding problems with special characters in the filenames. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@985843 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/fop/pdf/AbstractPDFStream.java | 11 ++- src/java/org/apache/fop/pdf/PDFArray.java | 7 ++ .../org/apache/fop/pdf/PDFEmbeddedFile.java | 19 +++++ src/java/org/apache/fop/pdf/PDFFactory.java | 29 ++++++-- src/java/org/apache/fop/pdf/PDFFileSpec.java | 4 +- .../org/apache/fop/pdf/PDFGoToRemote.java | 25 +++++-- .../fop/pdf/PDFJavaScriptLaunchAction.java | 72 +++++++++++++++++++ .../org/apache/fop/pdf/PDFNameTreeNode.java | 33 ++++++++- src/java/org/apache/fop/pdf/PDFText.java | 36 ++++++++++ .../fop/render/pdf/PDFRenderingUtil.java | 10 ++- 10 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 src/java/org/apache/fop/pdf/PDFJavaScriptLaunchAction.java diff --git a/src/java/org/apache/fop/pdf/AbstractPDFStream.java b/src/java/org/apache/fop/pdf/AbstractPDFStream.java index 4107c7e7a..91605b293 100644 --- a/src/java/org/apache/fop/pdf/AbstractPDFStream.java +++ b/src/java/org/apache/fop/pdf/AbstractPDFStream.java @@ -191,7 +191,7 @@ public abstract class AbstractPDFStream extends PDFDictionary { StreamCache encodedStream = null; PDFNumber refLength = null; final Object lengthEntry; - if (getDocument().isEncodingOnTheFly()) { + if (isEncodingOnTheFly()) { refLength = new PDFNumber(); getDocumentSafely().registerObject(refLength); lengthEntry = refLength; @@ -217,6 +217,15 @@ public abstract class AbstractPDFStream extends PDFDictionary { return cout.getCount(); } + /** + * Indicates whether encoding may happen without buffering the encoded data. If this method + * returns true, the /Length entry will be an indirect object, a direct object otherwise. + * @return true if encoding should happen "on the fly" + */ + protected boolean isEncodingOnTheFly() { + return getDocument().isEncodingOnTheFly(); + } + /** * Populates the dictionary with all necessary entries for the stream. * Override this method if you need additional entries. diff --git a/src/java/org/apache/fop/pdf/PDFArray.java b/src/java/org/apache/fop/pdf/PDFArray.java index a7dfc388e..a79e2704f 100644 --- a/src/java/org/apache/fop/pdf/PDFArray.java +++ b/src/java/org/apache/fop/pdf/PDFArray.java @@ -172,6 +172,13 @@ public class PDFArray extends PDFObject { this.values.add(new Double(value)); } + /** + * Clears the PDF array. + */ + public void clear() { + this.values.clear(); + } + /** {@inheritDoc} */ protected int output(OutputStream stream) throws IOException { CountingOutputStream cout = new CountingOutputStream(stream); diff --git a/src/java/org/apache/fop/pdf/PDFEmbeddedFile.java b/src/java/org/apache/fop/pdf/PDFEmbeddedFile.java index 24a471e1e..a5b44710a 100644 --- a/src/java/org/apache/fop/pdf/PDFEmbeddedFile.java +++ b/src/java/org/apache/fop/pdf/PDFEmbeddedFile.java @@ -19,6 +19,7 @@ package org.apache.fop.pdf; +import java.io.IOException; import java.util.Date; /** @@ -37,4 +38,22 @@ public class PDFEmbeddedFile extends PDFStream { put("Params", params); } + /** {@inheritDoc} */ + protected boolean isEncodingOnTheFly() { + //Acrobat doesn't like an indirect /Length object in this case, + //but only when the embedded file is a PDF file. + return false; + } + + /** {@inheritDoc} */ + protected void populateStreamDict(Object lengthEntry) { + super.populateStreamDict(lengthEntry); + try { + PDFDictionary dict = (PDFDictionary)get("Params"); + dict.put("Size", new Integer(data.getSize())); + } catch (IOException ioe) { + //ignore and just skip this entry as it's optional + } + } + } diff --git a/src/java/org/apache/fop/pdf/PDFFactory.java b/src/java/org/apache/fop/pdf/PDFFactory.java index 80cfba911..4f1067d7c 100644 --- a/src/java/org/apache/fop/pdf/PDFFactory.java +++ b/src/java/org/apache/fop/pdf/PDFFactory.java @@ -877,7 +877,8 @@ public class PDFFactory { */ public PDFNames makeNames() { PDFNames names = new PDFNames(); - getDocument().registerObject(names); + getDocument().assignObjectNumber(names); + getDocument().addTrailerObject(names); return names; } @@ -1067,7 +1068,7 @@ public class PDFFactory { if (target.startsWith(EMBEDDED_FILE)) { // File Attachments (Embedded Files) String filename = target.substring(EMBEDDED_FILE.length()); - return getLaunchActionForEmbeddedFile(filename); + return getActionForEmbeddedFile(filename, newWindow); } else if (targetLo.startsWith("http://")) { // HTTP URL? return new PDFUri(target); @@ -1094,7 +1095,7 @@ public class PDFFactory { } } - private PDFAction getLaunchActionForEmbeddedFile(String filename) { + private PDFAction getActionForEmbeddedFile(String filename, boolean newWindow) { PDFNames names = getDocument().getRoot().getNames(); if (names == null) { throw new IllegalStateException( @@ -1107,6 +1108,9 @@ public class PDFFactory { "No /EmbeddedFiles name tree present." + " Cannot create Launch Action for embedded file: " + filename); } + + //Find filespec reference for the embedded file + filename = PDFText.toPDFString(filename, '_'); PDFArray files = embeddedFiles.getNames(); PDFReference embeddedFileRef = null; int i = 0; @@ -1124,8 +1128,23 @@ public class PDFFactory { throw new IllegalStateException( "No embedded file with name " + filename + " present."); } - PDFLaunch launch = new PDFLaunch(embeddedFileRef); - return launch; + + //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(filename); + scriptBuffer.append("\", nLaunch:2});)"); + + PDFJavaScriptLaunchAction action = new PDFJavaScriptLaunchAction(scriptBuffer.toString()); + return action; } /** diff --git a/src/java/org/apache/fop/pdf/PDFFileSpec.java b/src/java/org/apache/fop/pdf/PDFFileSpec.java index d688ac4ee..b9a46c8b8 100644 --- a/src/java/org/apache/fop/pdf/PDFFileSpec.java +++ b/src/java/org/apache/fop/pdf/PDFFileSpec.java @@ -19,10 +19,8 @@ package org.apache.fop.pdf; - /** - * class representing a /FileSpec object. - * + * Class representing a /FileSpec object. */ public class PDFFileSpec extends PDFDictionary { diff --git a/src/java/org/apache/fop/pdf/PDFGoToRemote.java b/src/java/org/apache/fop/pdf/PDFGoToRemote.java index ee7660875..93fbe47de 100644 --- a/src/java/org/apache/fop/pdf/PDFGoToRemote.java +++ b/src/java/org/apache/fop/pdf/PDFGoToRemote.java @@ -27,7 +27,7 @@ public class PDFGoToRemote extends PDFAction { /** * the file specification */ - private PDFFileSpec pdfFileSpec; + private PDFReference pdfFileSpec; private int pageReference = 0; private String destination = null; private boolean newWindow = false; @@ -43,12 +43,12 @@ public class PDFGoToRemote extends PDFAction { /* generic creation of object */ super(); - this.pdfFileSpec = pdfFileSpec; + this.pdfFileSpec = pdfFileSpec.makeReference(); this.newWindow = newWindow; } /** - * create an GoToR object. + * Create an GoToR object. * * @param pdfFileSpec the fileSpec associated with the action * @param page a page reference within the remote document @@ -56,7 +56,18 @@ public class PDFGoToRemote extends PDFAction { * displayed in a new window */ public PDFGoToRemote(PDFFileSpec pdfFileSpec, int page, boolean newWindow) { - /* generic creation of object */ + this(pdfFileSpec.makeReference(), page, newWindow); + } + + /** + * Create an GoToR object. + * + * @param pdfFileSpec the fileSpec associated with the action + * @param page a page reference within the remote document + * @param newWindow boolean indicating whether the target should be + * displayed in a new window + */ + public PDFGoToRemote(PDFReference pdfFileSpec, int page, boolean newWindow) { super(); this.pdfFileSpec = pdfFileSpec; @@ -76,7 +87,7 @@ public class PDFGoToRemote extends PDFAction { /* generic creation of object */ super(); - this.pdfFileSpec = pdfFileSpec; + this.pdfFileSpec = pdfFileSpec.makeReference(); this.destination = dest; this.newWindow = newWindow; } @@ -97,7 +108,7 @@ public class PDFGoToRemote extends PDFAction { StringBuffer sb = new StringBuffer(64); sb.append(getObjectID()); sb.append("<<\n/S /GoToR\n/F "); - sb.append(pdfFileSpec.referencePDF()); + sb.append(pdfFileSpec.toString()); sb.append("\n"); if (destination != null) { @@ -139,7 +150,7 @@ public class PDFGoToRemote extends PDFAction { PDFGoToRemote remote = (PDFGoToRemote)obj; - if (!remote.pdfFileSpec.referencePDF().equals(pdfFileSpec.referencePDF())) { + if (!remote.pdfFileSpec.toString().equals(pdfFileSpec.toString())) { return false; } diff --git a/src/java/org/apache/fop/pdf/PDFJavaScriptLaunchAction.java b/src/java/org/apache/fop/pdf/PDFJavaScriptLaunchAction.java new file mode 100644 index 000000000..dcd3c6fea --- /dev/null +++ b/src/java/org/apache/fop/pdf/PDFJavaScriptLaunchAction.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.pdf; + +/** + * PDF Action which executes some JavaScript code. + * @since PDF 1.3 + */ +public class PDFJavaScriptLaunchAction extends PDFAction { + + private String script; + + /** + * Creates a new /Launch action. + * @param script the script to run when the launch action is triggered + */ + public PDFJavaScriptLaunchAction(String script) { + this.script = script; + } + + /** {@inheritDoc} */ + public String getAction() { + return this.referencePDF(); + } + + /** {@inheritDoc} */ + public String toPDFString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getObjectID()); + sb.append("<<\n/S /JavaScript\n/JS ("); + sb.append(this.script); + sb.append(")\n>>\nendobj\n"); + return sb.toString(); + } + + /** {@inheritDoc} */ + protected boolean contentEquals(PDFObject obj) { + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof PDFJavaScriptLaunchAction)) { + return false; + } + + PDFJavaScriptLaunchAction launch = (PDFJavaScriptLaunchAction) obj; + + if (!launch.script.toString().equals(script.toString())) { + return false; + } + + return true; + } + +} diff --git a/src/java/org/apache/fop/pdf/PDFNameTreeNode.java b/src/java/org/apache/fop/pdf/PDFNameTreeNode.java index 4bc00ef9c..f294b0f68 100644 --- a/src/java/org/apache/fop/pdf/PDFNameTreeNode.java +++ b/src/java/org/apache/fop/pdf/PDFNameTreeNode.java @@ -19,6 +19,14 @@ package org.apache.fop.pdf; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Iterator; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + /** * Class representing a PDF name tree node. */ @@ -104,7 +112,6 @@ public class PDFNameTreeNode extends PDFDictionary { return (String)limits.get(1); } - private PDFArray prepareLimitsArray() { PDFArray limits = (PDFArray)get(LIMITS); if (limits == null) { @@ -117,5 +124,29 @@ public class PDFNameTreeNode extends PDFDictionary { return limits; } + /** {@inheritDoc} */ + protected void writeDictionary(OutputStream out, Writer writer) throws IOException { + sortNames(); //Sort the names before writing them out + super.writeDictionary(out, writer); + } + + private void sortNames() { + PDFArray names = getNames(); + SortedMap map = new TreeMap(); + int i = 0; + int c = names.length(); + while (i < c) { + String key = (String)names.get(i++); //Key must be a String + Object value = names.get(i++); + map.put(key, value); + } + names.clear(); + Iterator iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = (Map.Entry)iter.next(); + names.add(entry.getKey()); + names.add(entry.getValue()); + } + } } diff --git a/src/java/org/apache/fop/pdf/PDFText.java b/src/java/org/apache/fop/pdf/PDFText.java index 02f845266..d7d80fbe0 100644 --- a/src/java/org/apache/fop/pdf/PDFText.java +++ b/src/java/org/apache/fop/pdf/PDFText.java @@ -320,5 +320,41 @@ public class PDFText extends PDFObject { return bout.toByteArray(); } + /** + * Converts a text to PDF's "string" data type. Unsupported characters get converted to '?' + * characters (similar to what the Java "US-ASCII" encoding does). + * @see {@link #toPDFString(CharSequence, char)} + * @param text the text to convert + * @return the converted string + */ + public static String toPDFString(CharSequence text) { + return toPDFString(text, '?'); + } + + /** + * Converts a text to PDF's "string" data type. Unsupported characters get converted to the + * given replacement character. + *

+ * The PDF library currently doesn't properly distinguish between the PDF + * data types "string" and "text string", so we currently restrict "string" to US-ASCII, also + * because "string" seems somewhat under-specified concerning the upper 128 bytes. + * @param text the text to convert + * @param replacement the replacement character used when substituting a character + * @return the converted string + */ + public static String toPDFString(CharSequence text, char replacement) { + StringBuffer sb = new StringBuffer(); + for (int i = 0, c = text.length(); i < c; i++) { + char ch = text.charAt(i); + if (ch > 127) { + //TODO Revisit the restriction to US-ASCII once "string" and "text string" are + //"disentangled". + sb.append(replacement); + } else { + sb.append(ch); + } + } + return sb.toString(); + } } diff --git a/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java b/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java index f6123fb33..b7ad9cf71 100644 --- a/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java +++ b/src/java/org/apache/fop/render/pdf/PDFRenderingUtil.java @@ -61,6 +61,7 @@ import org.apache.fop.pdf.PDFNumsArray; import org.apache.fop.pdf.PDFOutputIntent; import org.apache.fop.pdf.PDFPageLabels; import org.apache.fop.pdf.PDFReference; +import org.apache.fop.pdf.PDFText; import org.apache.fop.pdf.PDFXMode; import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileExtensionAttachment; import org.apache.fop.util.ColorProfileUtil; @@ -453,7 +454,8 @@ class PDFRenderingUtil implements PDFConfigurationConstants { } PDFDictionary dict = new PDFDictionary(); dict.put("F", file); - PDFFileSpec fileSpec = new PDFFileSpec(embeddedFile.getFilename()); + String filename = PDFText.toPDFString(embeddedFile.getFilename(), '_'); + PDFFileSpec fileSpec = new PDFFileSpec(filename); fileSpec.setEmbeddedFile(dict); if (embeddedFile.getDesc() != null) { fileSpec.setDescription(embeddedFile.getDesc()); @@ -464,7 +466,8 @@ class PDFRenderingUtil implements PDFConfigurationConstants { PDFNameTreeNode embeddedFiles = names.getEmbeddedFiles(); if (embeddedFiles == null) { embeddedFiles = new PDFNameTreeNode(); - //this.pdfDoc.registerObject(embeddedFiles); + this.pdfDoc.assignObjectNumber(embeddedFiles); + this.pdfDoc.addTrailerObject(embeddedFiles); names.setEmbeddedFiles(embeddedFiles); } @@ -474,7 +477,8 @@ class PDFRenderingUtil implements PDFConfigurationConstants { nameArray = new PDFArray(); embeddedFiles.setNames(nameArray); } - nameArray.add(embeddedFile.getFilename()); + String name = PDFText.toPDFString(filename); + nameArray.add(name); nameArray.add(new PDFReference(fileSpec)); } -- 2.39.5