From 2442659ffd10775878356396cf8037cc3e6ea184 Mon Sep 17 00:00:00 2001 From: Chris Bowditch Date: Thu, 4 Oct 2007 13:51:25 +0000 Subject: [PATCH] Bugzilla #42144 Safely set postscript page device dictionary Submitted by Adrian Cumiskey git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@581906 13f79535-47bb-0310-9956-ffa450edef68 --- .../content/xdocs/trunk/output.xml | 17 + .../org/apache/fop/area/PageViewport.java | 5 + .../apache/fop/render/ps/PSDictionary.java | 312 ++++++++++++++++++ .../ps/PSDictionaryFormatException.java | 37 +++ .../fop/render/ps/PSPageDeviceDictionary.java | 110 ++++++ .../org/apache/fop/render/ps/PSRenderer.java | 275 +++++++++++---- .../fop/render/ps/PSRendererConfigurator.java | 5 + .../ps/extensions/PSExtensionAttachment.java | 108 ++++++ .../extensions/PSExtensionElementMapping.java | 6 + .../ps/extensions/PSExtensionHandler.java | 24 +- .../render/ps/extensions/PSSetPageDevice.java | 114 +++++++ .../ps/extensions/PSSetPageDeviceElement.java | 93 ++++++ .../fop/render/ps/extensions/PSSetupCode.java | 56 ++-- .../standard-testcases/ps-extension_2.xml | 70 ++++ 14 files changed, 1122 insertions(+), 110 deletions(-) create mode 100644 src/java/org/apache/fop/render/ps/PSDictionary.java create mode 100644 src/java/org/apache/fop/render/ps/PSDictionaryFormatException.java create mode 100644 src/java/org/apache/fop/render/ps/PSPageDeviceDictionary.java create mode 100644 src/java/org/apache/fop/render/ps/extensions/PSExtensionAttachment.java create mode 100644 src/java/org/apache/fop/render/ps/extensions/PSSetPageDevice.java create mode 100644 src/java/org/apache/fop/render/ps/extensions/PSSetPageDeviceElement.java create mode 100644 test/layoutengine/standard-testcases/ps-extension_2.xml diff --git a/src/documentation/content/xdocs/trunk/output.xml b/src/documentation/content/xdocs/trunk/output.xml index 21743519f..798c61811 100644 --- a/src/documentation/content/xdocs/trunk/output.xml +++ b/src/documentation/content/xdocs/trunk/output.xml @@ -212,6 +212,8 @@ out = proc.getOutputStream();]]> false 3 false + false + true ]]>

The default value for the "auto-rotate-landscape" setting is "false". Setting it @@ -230,6 +232,20 @@ out = proc.getOutputStream();]]> reduce file size but can potentially increase the memory needed in the interpreter to process.

+

+ The default value for the "safe-set-page-device" setting is "false". Setting it + to "true" will cause the renderer to invoke a postscript macro which guards against + the possibility of invalid/unsupported postscript key/values being issued to the + implementing postscript page device. +

+

+ The default value for the "dsc-compliant" setting is "true". Setting it + to "false" will break DSC compliance by minimizing the number of setpagedevice + calls in the postscript document output. This feature may be useful when unwanted + blank pages are experienced in your postscript output. This problem is caused by + the particular postscript implementation issuing unwanted postscript subsystem + initgraphics/erasepage calls on each setpagedevice call. +

Limitations @@ -820,3 +836,4 @@ out = proc.getOutputStream();]]> + diff --git a/src/java/org/apache/fop/area/PageViewport.java b/src/java/org/apache/fop/area/PageViewport.java index 184eb9d32..af557ade9 100644 --- a/src/java/org/apache/fop/area/PageViewport.java +++ b/src/java/org/apache/fop/area/PageViewport.java @@ -666,4 +666,9 @@ public class PageViewport extends AreaTreeObject implements Resolvable, Cloneabl public RegionReference getRegionReference(int id) { return getPage().getRegionViewport(id).getRegionReference(); } + + /** @return whether this page viewport has any extension attachments */ + public boolean hasExtensionAttachments() { + return this.extensionAttachments != null && !this.extensionAttachments.isEmpty(); + } } diff --git a/src/java/org/apache/fop/render/ps/PSDictionary.java b/src/java/org/apache/fop/render/ps/PSDictionary.java new file mode 100644 index 000000000..dbf7173a5 --- /dev/null +++ b/src/java/org/apache/fop/render/ps/PSDictionary.java @@ -0,0 +1,312 @@ +/* + * 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.ps; + +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +/** + * This class is used to encapsulate postscript dictionary objects. + */ +public class PSDictionary extends java.util.HashMap { + + private static final long serialVersionUID = 815367222496219197L; + + /** + * This class is used to parse dictionary strings. + */ + private static class Maker { + + /** + * Simple token holding class + */ + private class Token { + /** + * start index in string + */ + private int startIndex = -1; + + /** + * end index in string + */ + private int endIndex = -1; + + /** + * token string value + */ + private String value; + } + + private static final String[][] BRACES = { + {"<<", ">>"}, + {"[", "]"}, + {"{", "}"} + }; + + private static final int OPENING = 0; + private static final int CLOSING = 1; + private static final int DICTIONARY = 0; + private static final int ARRAY = 1; + private static final int PROCEDURE = 2; + + /** + * Returns a Token containing the start, end index and value of the next token + * found in a given string + * + * @param str + * string to search + * @param fromIndex + * search from index + * @return Token containing the start, end index and value of the next token + */ + protected Token nextToken(String str, int fromIndex) { + Token t = null; + for (int i = fromIndex; i < str.length(); i++) { + boolean isWhitespace = Character.isWhitespace(str.charAt(i)); + // start index found + if (t == null && !isWhitespace) { + t = new Token(); + t.startIndex = i; + // end index found + } else if (t != null && isWhitespace) { + t.endIndex = i; + break; + } + } + // start index found + if (t != null) { + // end index not found so take end of string + if (t.endIndex == -1) { + t.endIndex = str.length(); + } + t.value = str.substring(t.startIndex, t.endIndex); + } + return t; + } + + /** + * Returns the closing brace index from a given string searches from a + * given index + * + * @param str + * string to search + * @param braces + * string array of opening and closing brace + * @param fromIndex + * searches from index + * @return matching brace index + * @throws org.apache.fop.render.ps.PSDictionaryFormatException + * thrown in the event that a parsing error occurred + */ + private int indexOfMatchingBrace(String str, String[] braces, + int fromIndex) throws PSDictionaryFormatException { + final int len = str.length(); + if (braces.length != 2) { + throw new PSDictionaryFormatException("Wrong number of braces"); + } + for (int openCnt = 0, closeCnt = 0; fromIndex < len; fromIndex++) { + if (str.startsWith(braces[OPENING], fromIndex)) { + openCnt++; + } else if (str.startsWith(braces[CLOSING], fromIndex)) { + closeCnt++; + if (openCnt > 0 && openCnt == closeCnt) { + return fromIndex; // found + } + } + } + return -1; // not found + } + + /** + * Strips braces from complex object string + * + * @param str + * String to parse + * @param braces + * String array containing opening and closing braces + * @return String with braces stripped + * @throws + * org.apache.fop.render.ps.PSDictionaryFormatException object format exception + */ + private String stripBraces(String str, String[] braces) throws PSDictionaryFormatException { + // find first opening brace + int firstIndex = str.indexOf(braces[OPENING]); + if (firstIndex == -1) { + throw new PSDictionaryFormatException( + "Failed to find opening parameter '" + braces[OPENING] + + ""); + } + + // find last matching brace + int lastIndex = indexOfMatchingBrace(str, braces, firstIndex); + if (lastIndex == -1) { + throw new PSDictionaryFormatException( + "Failed to find matching closing parameter '" + + braces[CLOSING] + "'"); + } + + // strip brace and trim + int braceLen = braces[OPENING].length(); + str = str.substring(firstIndex + braceLen, lastIndex).trim(); + return str; + } + + /** + * Parses a dictionary string and provides a dictionary object + * + * @param str a dictionary string + * @return A postscript dictionary object + * @throws + * PSDictionaryFormatException thrown if a dictionary format exception occurs + */ + public PSDictionary parseDictionary(String str) throws PSDictionaryFormatException { + PSDictionary dictionary = new PSDictionary(); + str = stripBraces(str.trim(), BRACES[DICTIONARY]); + // length of dictionary string + final int len = str.length(); + + Token keyToken; + for (int currIndex = 0; (keyToken = nextToken(str, currIndex)) != null + && currIndex <= len;) { + if (keyToken.value == null) { + throw new PSDictionaryFormatException("Failed to parse object key"); + } + Token valueToken = nextToken(str, keyToken.endIndex + 1); + String[] braces = null; + for (int i = 0; i < BRACES.length; i++) { + if (valueToken.value.startsWith(BRACES[i][OPENING])) { + braces = BRACES[i]; + break; + } + } + Object obj = null; + if (braces != null) { + // find closing brace + valueToken.endIndex = indexOfMatchingBrace(str, braces, + valueToken.startIndex) + + braces[OPENING].length(); + if (valueToken.endIndex < 0) { + throw new PSDictionaryFormatException("Closing value brace '" + + braces[CLOSING] + "' not found for key '" + + keyToken.value + "'"); + } + valueToken.value = str.substring(valueToken.startIndex, valueToken.endIndex); + } + if (braces == null || braces == BRACES[PROCEDURE]) { + obj = valueToken.value; + } else if (BRACES[ARRAY] == braces) { + List objList = new java.util.ArrayList(); + String objString = stripBraces(valueToken.value, braces); + StringTokenizer tokenizer = new StringTokenizer(objString, ","); + while (tokenizer.hasMoreTokens()) { + objList.add(tokenizer.nextToken()); + } + obj = objList; + } else if (BRACES[DICTIONARY] == braces) { + obj = parseDictionary(valueToken.value); + } + dictionary.put(keyToken.value, obj); + currIndex = valueToken.endIndex + 1; + } + return dictionary; + } + } + + /** + * Parses a given a dictionary string and returns an object + * + * @param str dictionary string + * @return dictionary object + * @throws PSDictionaryFormatException object format exception + */ + public static PSDictionary valueOf(String str) throws PSDictionaryFormatException { + return (new Maker()).parseDictionary(str); + } + + /** + * @param obj object to test equality against + * @return whether a given object is equal to this dictionary object + * @see java.lang.Object#equals(Object) + */ + public boolean equals(Object obj) { + if (!(obj instanceof PSPageDeviceDictionary)) { + return false; + } + PSDictionary dictionaryObj = (PSDictionary) obj; + if (dictionaryObj.size() != size()) { + return false; + } + for (Iterator it = keySet().iterator(); it.hasNext();) { + String key = (String) it.next(); + if (!dictionaryObj.containsKey(key)) { + return false; + } + if (!dictionaryObj.get(key).equals(get(key))) { + return false; + } + } + return true; + } + + /** + * @return a hash code value for this object. + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + int hashCode = 7; + for (Iterator it = values().iterator(); it.hasNext();) { + Object value = it.next(); + hashCode += value.hashCode(); + } + return hashCode; + } + + /** + * @return a string representation of this dictionary + * @see java.lang.String#toString() + */ + public String toString() { + if (isEmpty()) { + return ""; + } + StringBuffer sb = new StringBuffer("<<\n"); + for (Iterator it = super.keySet().iterator(); it.hasNext();) { + String key = (String) it.next(); + sb.append(" " + key + " "); + Object obj = super.get(key); + if (obj instanceof java.util.ArrayList) { + List array = (List)obj; + String str = "["; + for (int i = 0; i < array.size(); i++) { + Object element = array.get(i); + str += element + " "; + } + str = str.trim(); + str += "]"; + sb.append(str + "\n"); + } else { + sb.append(obj.toString() + "\n"); + } + } + sb.append(">>"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/fop/render/ps/PSDictionaryFormatException.java b/src/java/org/apache/fop/render/ps/PSDictionaryFormatException.java new file mode 100644 index 000000000..2153e8116 --- /dev/null +++ b/src/java/org/apache/fop/render/ps/PSDictionaryFormatException.java @@ -0,0 +1,37 @@ +/* + * 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.ps; + +/** + * Thrown to indicate that a formatting error has occured when + * trying to parse a postscript dictionary object + */ +public class PSDictionaryFormatException extends Exception { + + private static final long serialVersionUID = 6492321557297860931L; + + /** + * Default constructor + * @param string error message + */ + public PSDictionaryFormatException(String string) { + super(string); + } +} diff --git a/src/java/org/apache/fop/render/ps/PSPageDeviceDictionary.java b/src/java/org/apache/fop/render/ps/PSPageDeviceDictionary.java new file mode 100644 index 000000000..c327423ef --- /dev/null +++ b/src/java/org/apache/fop/render/ps/PSPageDeviceDictionary.java @@ -0,0 +1,110 @@ +/* + * 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.ps; + +/** + * Postscript page device dictionary object + * + * This object is used by the postscript renderer to hold postscript page device + * values. It can also be used to minimize the number of setpagedevice calls when + * DSC compliance is false. + */ +public class PSPageDeviceDictionary extends PSDictionary { + + private static final long serialVersionUID = 845943256485806509L; + + /** + * Whether or not the contents of the dictionary are flushed on retrieval + */ + private boolean flushOnRetrieval = false; + + /** + * Dictionary content that has not been output/written yet + */ + private PSDictionary unRetrievedContentDictionary; + + /** + * @param key key with which the specified value is to be associated. + * @param value value to be associated with the specified key. + * @return the previous value associated with the key or null + * @see java.util.Map#put(Object, Object) + */ + public Object put(Object key, Object value) { + Object previousValue = super.put(key, value); + if (flushOnRetrieval) { + if (previousValue == null || !previousValue.equals(value)) { + unRetrievedContentDictionary.put(key, value); + } + } + return previousValue; + } + + /** + * @see java.util.Map#clear() + */ + public void clear() { + super.clear(); + if (unRetrievedContentDictionary != null) { + unRetrievedContentDictionary.clear(); + } + } + + /** + * Returns true if this map contains no key-value mappings. + * + * @return true if this map contains no key-value mappings. + */ + public boolean isEmpty() { + if (flushOnRetrieval) { + return unRetrievedContentDictionary.isEmpty(); + } + return super.isEmpty(); + } + + /** + * The contents of the dictionary are flushed when written + * @param flushOnRetrieval boolean value + */ + public void setFlushOnRetrieval(boolean flushOnRetrieval) { + this.flushOnRetrieval = flushOnRetrieval; + if (flushOnRetrieval) { + unRetrievedContentDictionary = new PSDictionary(); + } + } + + /** + * Returns a dictionary string with containing all unwritten content note: + * unnecessary writes are important as there is a device specific + * initgraphics call by the underlying postscript interpreter on every + * setpagedevice call which can result in blank pages etc. + * + * @return unwritten content dictionary string + */ + public String getContent() { + String content; + if (flushOnRetrieval) { + content = unRetrievedContentDictionary.toString(); + unRetrievedContentDictionary.clear(); + } else { + content = super.toString(); + } + return content; + } +} diff --git a/src/java/org/apache/fop/render/ps/PSRenderer.java b/src/java/org/apache/fop/render/ps/PSRenderer.java index bb77dd55a..5d823e86e 100644 --- a/src/java/org/apache/fop/render/ps/PSRenderer.java +++ b/src/java/org/apache/fop/render/ps/PSRenderer.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.LineNumberReader; import java.io.OutputStream; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -69,6 +70,8 @@ import org.apache.fop.render.Graphics2DAdapter; import org.apache.fop.render.AbstractPathOrientedRenderer; import org.apache.fop.render.ImageAdapter; import org.apache.fop.render.RendererContext; +import org.apache.fop.render.ps.extensions.PSExtensionAttachment; +import org.apache.fop.render.ps.extensions.PSSetPageDevice; import org.apache.fop.render.ps.extensions.PSSetupCode; import org.apache.fop.util.CharUtilities; @@ -141,7 +144,16 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda private Map fontResources; /** This is a map of PSResource instances of all forms (key: uri) */ private Map formResources; - + + /** encapsulation of dictionary used in setpagedevice instruction **/ + private PSPageDeviceDictionary pageDeviceDictionary; + + /** Whether or not the safe set page device macro will be used or not */ + private boolean safeSetPageDevice = false; + + /** Whether or not Dublin Core Standard (dsc) compliant output is enforced */ + private boolean dscCompliant = true; + /** * {@inheritDoc} */ @@ -713,6 +725,11 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda this.gen.setPSLevel(this.languageLevel); this.currentPageNumber = 0; + //Initial default page device dictionary settings + this.pageDeviceDictionary = new PSPageDeviceDictionary(); + pageDeviceDictionary.setFlushOnRetrieval(!this.dscCompliant); + pageDeviceDictionary.put("/ImagingBBox", "null"); + //PostScript Header writeln(DSCConstants.PS_ADOBE_30); gen.writeDSCComment(DSCConstants.CREATOR, new String[] {userAgent.getProducer()}); @@ -755,6 +772,7 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda IOUtils.closeQuietly(gen.getOutputStream()); rewritePostScriptFile(); } + this.pageDeviceDictionary.clear(); } /** @@ -796,12 +814,34 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda } if (oDI instanceof OffDocumentExtensionAttachment) { ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)oDI).getAttachment(); - if (PSSetupCode.CATEGORY.equals(attachment.getCategory())) { - PSSetupCode setupCode = (PSSetupCode)attachment; - if (setupCodeList == null) { - setupCodeList = new java.util.ArrayList(); + if (attachment != null) { + if (PSExtensionAttachment.CATEGORY.equals(attachment.getCategory())) { + if (attachment instanceof PSSetupCode) { + if (setupCodeList == null) { + setupCodeList = new java.util.ArrayList(); + } + if (!setupCodeList.contains(attachment)) { + setupCodeList.add(attachment); + } + } else if (attachment instanceof PSSetPageDevice) { + /** + * Extract all PSSetPageDevice instances from the + * attachment list on the s-p-m and add all dictionary + * entries to our internal representation of the the + * page device dictionary. + */ + PSSetPageDevice setPageDevice = (PSSetPageDevice)attachment; + String content = setPageDevice.getContent(); + if (content != null) { + try { + this.pageDeviceDictionary.putAll(PSDictionary.valueOf(content)); + } catch (PSDictionaryFormatException e) { + log.error("Failed to parse dictionary string: " + + e.getMessage() + ", content = '" + content + "'"); + } + } + } } - setupCodeList.add(setupCode); } } super.processOffDocumentItem(oDI); @@ -872,91 +912,114 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda log.debug("renderPage(): " + page); this.currentPageNumber++; + gen.getResourceTracker().notifyStartNewPage(); gen.getResourceTracker().notifyResourceUsageOnPage(PSProcSets.STD_PROCSET); gen.writeDSCComment(DSCConstants.PAGE, new Object[] {page.getPageNumberString(), new Integer(this.currentPageNumber)}); - final Integer zero = new Integer(0); - final long pagewidth = Math.round(page.getViewArea().getWidth()); - final long pageheight = Math.round(page.getViewArea().getHeight()); - final double pspagewidth = pagewidth / 1000f; - final double pspageheight = pageheight / 1000f; + + double pageWidth = Math.round(page.getViewArea().getWidth()) / 1000f; + double pageHeight = Math.round(page.getViewArea().getHeight()) / 1000f; boolean rotate = false; - if (this.autoRotateLandscape && (pageheight < pagewidth)) { + List pageSizes = new java.util.ArrayList(); + if (this.autoRotateLandscape && (pageHeight < pageWidth)) { rotate = true; - gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] - {zero, - zero, - new Long(Math.round(pspageheight)), - new Long(Math.round(pspagewidth))}); - gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] - {zero, - zero, - new Double(pspageheight), - new Double(pspagewidth)}); + pageSizes.add(new Long(Math.round(pageHeight))); + pageSizes.add(new Long(Math.round(pageWidth))); + } else { + pageSizes.add(new Long(Math.round(pageWidth))); + pageSizes.add(new Long(Math.round(pageHeight))); + } + pageDeviceDictionary.put("/PageSize", pageSizes); + + if (page.hasExtensionAttachments()) { + for (Iterator iter = page.getExtensionAttachments().iterator(); + iter.hasNext();) { + ExtensionAttachment attachment = (ExtensionAttachment) iter.next(); + if (attachment instanceof PSSetPageDevice) { + /** + * Extract all PSSetPageDevice instances from the + * attachment list on the s-p-m and add all + * dictionary entries to our internal representation + * of the the page device dictionary. + */ + PSSetPageDevice setPageDevice = (PSSetPageDevice)attachment; + String content = setPageDevice.getContent(); + if (content != null) { + try { + pageDeviceDictionary.putAll(PSDictionary.valueOf(content)); + } catch (PSDictionaryFormatException e) { + log.error("failed to parse dictionary string: " + + e.getMessage() + ", [" + content + "]"); + } + } + } + } + } + + try { + if (setupCodeList != null) { + writeEnclosedExtensionAttachments(setupCodeList); + setupCodeList.clear(); + } + } catch (IOException e) { + log.error(e.getMessage()); + } + final Integer zero = new Integer(0); + if (rotate) { + gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] { + zero, zero, new Long(Math.round(pageHeight)), + new Long(Math.round(pageWidth)) }); + gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] { + zero, zero, new Double(pageHeight), + new Double(pageWidth) }); gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION, "Landscape"); } else { - gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] - {zero, - zero, - new Long(Math.round(pspagewidth)), - new Long(Math.round(pspageheight))}); - gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] - {zero, - zero, - new Double(pspagewidth), - new Double(pspageheight)}); - if (this.autoRotateLandscape) { - gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION, "Portrait"); + gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] { + zero, zero, new Long(Math.round(pageWidth)), + new Long(Math.round(pageHeight)) }); + gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] { + zero, zero, new Double(pageWidth), + new Double(pageHeight) }); + if (autoRotateLandscape) { + gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION, + "Portrait"); } } - gen.writeDSCComment(DSCConstants.PAGE_RESOURCES, + gen.writeDSCComment(DSCConstants.PAGE_RESOURCES, new Object[] {DSCConstants.ATEND}); + gen.commentln("%FOPSimplePageMaster: " + page.getSimplePageMasterName()); + gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP); - - //Handle PSSetupCode instances on simple-page-master - if (page.getExtensionAttachments() != null - && page.getExtensionAttachments().size() > 0) { - List list = new java.util.ArrayList(); - //Extract all PSSetupCode instances from the attachment list on the s-p-m - Iterator i = page.getExtensionAttachments().iterator(); - while (i.hasNext()) { - ExtensionAttachment attachment = (ExtensionAttachment)i.next(); - if (PSSetupCode.CATEGORY.equals(attachment.getCategory())) { - list.add(attachment); - } + + // Write any unwritten changes to page device dictionary + if (!pageDeviceDictionary.isEmpty()) { + String content = pageDeviceDictionary.getContent(); + if (safeSetPageDevice) { + content += " SSPD"; + } else { + content += " setpagedevice"; } - writeSetupCodeList(list, "PageSetupCode"); + writeEnclosedExtensionAttachment(new PSSetPageDevice(content)); } - + if (rotate) { - gen.writeln("<<"); - gen.writeln("/PageSize [" - + Math.round(pspageheight) + " " - + Math.round(pspagewidth) + "]"); - gen.writeln("/ImagingBBox null"); - gen.writeln(">> setpagedevice"); - gen.writeln(Math.round(pspageheight) + " 0 translate"); + gen.writeln(Math.round(pageHeight) + " 0 translate"); gen.writeln("90 rotate"); - } else { - gen.writeln("<<"); - gen.writeln("/PageSize [" - + Math.round(pspagewidth) + " " - + Math.round(pspageheight) + "]"); - gen.writeln("/ImagingBBox null"); - gen.writeln(">> setpagedevice"); } - concatMatrix(1, 0, 0, -1, 0, pageheight / 1000f); - - gen.writeDSCComment(DSCConstants.END_PAGE_SETUP); + concatMatrix(1, 0, 0, -1, 0, pageHeight); + gen.writeDSCComment(DSCConstants.END_PAGE_SETUP); + //Process page super.renderPage(page); + //Show page writeln("showpage"); gen.writeDSCComment(DSCConstants.PAGE_TRAILER); + gen.getResourceTracker().writeResources(true, gen); } @@ -1259,7 +1322,6 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda /** * {@inheritDoc} - * int, int, int, int, java.util.Map) */ protected RendererContext createRendererContext(int x, int y, int width, int height, Map foreignAttributes) { @@ -1275,5 +1337,84 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda return MIME_TYPE; } + /** + * Formats and writes a PSExtensionAttachment to the output stream. + * + * @param attachment an PSExtensionAttachment instance + */ + private void writeEnclosedExtensionAttachment(PSExtensionAttachment attachment) + throws IOException { + String info = ""; + if (attachment instanceof PSSetupCode) { + PSSetupCode setupCodeAttach = (PSSetupCode)attachment; + String name = setupCodeAttach.getName(); + if (name != null) { + info += ": (" + name + ")"; + } + } + String type = attachment.getType(); + gen.commentln("%FOPBegin" + type + info); + LineNumberReader reader = new LineNumberReader( + new java.io.StringReader(attachment.getContent())); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() > 0) { + gen.writeln(line); + } + } + gen.commentln("%FOPEnd" + type); + } + + /** + * Formats and writes a Collection of PSExtensionAttachment instances to + * the output stream. + * + * @param attachmentCollection + * a Collection of PSExtensionAttachment instances + */ + private void writeEnclosedExtensionAttachments(Collection attachmentCollection) + throws IOException { + Iterator iter = attachmentCollection.iterator(); + while (iter.hasNext()) { + PSExtensionAttachment attachment = (PSExtensionAttachment)iter + .next(); + if (attachment != null) { + writeEnclosedExtensionAttachment(attachment); + } + iter.remove(); + } + } + /** + * Sets whether or not the safe set page device macro should be used + * (as opposed to directly invoking setpagedevice) when setting the + * postscript page device. + * + * This option is a useful option when you want to guard against the possibility + * of invalid/unsupported postscript key/values being placed in the page device. + * + * @param safeSetPageDevice setting to false and the renderer will make a + * standard "setpagedevice" call, setting to true will make a safe set page + * device macro call (default is false). + */ + public void setSafeSetPageDevice(boolean safeSetPageDevice) { + this.safeSetPageDevice = safeSetPageDevice; + } + + /** + * Sets whether or not Dublin Core Standard (dsc) compliance is enforced. + * + * It can cause problems (unwanted postscript subsystem initgraphics/erasepage calls) + * on some printers when the pagedevice is set. If this causes problems on a + * particular implementation then use this setting with a 'false' value to try and + * minimize the number of setpagedevice calls in the postscript document output. + * + * Set this value to false if you experience unwanted blank pages in your + * postscript output. + * @param dscCompliant boolean value (default is true) + */ + public void setDSCCompliant(boolean dscCompliant) { + this.dscCompliant = dscCompliant; + } } diff --git a/src/java/org/apache/fop/render/ps/PSRendererConfigurator.java b/src/java/org/apache/fop/render/ps/PSRendererConfigurator.java index c7b5a025b..6caa92c85 100644 --- a/src/java/org/apache/fop/render/ps/PSRendererConfigurator.java +++ b/src/java/org/apache/fop/render/ps/PSRendererConfigurator.java @@ -49,8 +49,13 @@ public class PSRendererConfigurator extends PrintRendererConfigurator { super.configure(renderer); PSRenderer psRenderer = (PSRenderer)renderer; + psRenderer.setAutoRotateLandscape( cfg.getChild("auto-rotate-landscape").getValueAsBoolean(false)); + psRenderer.setSafeSetPageDevice( + cfg.getChild("safe-set-page-device").getValueAsBoolean(false)); + psRenderer.setDSCCompliant( + cfg.getChild("dsc-compliant").getValueAsBoolean(true)); } } } diff --git a/src/java/org/apache/fop/render/ps/extensions/PSExtensionAttachment.java b/src/java/org/apache/fop/render/ps/extensions/PSExtensionAttachment.java new file mode 100644 index 000000000..0fb623bdc --- /dev/null +++ b/src/java/org/apache/fop/render/ps/extensions/PSExtensionAttachment.java @@ -0,0 +1,108 @@ +/* + * 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.ps.extensions; + +import org.apache.fop.fo.extensions.ExtensionAttachment; +import org.apache.fop.util.XMLizable; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * This is the pass-through value object for the PostScript extension. + */ +public abstract class PSExtensionAttachment implements ExtensionAttachment, XMLizable { + + /** extension node content */ + protected String content; + + /** The category URI for this extension attachment. */ + public static final String CATEGORY = "apache:fop:extensions:postscript"; + + /** + * Default constructor. + * @param content the content of the setup code object + */ + public PSExtensionAttachment(String content) { + this.content = content; + } + + /** + * No-argument contructor. + */ + public PSExtensionAttachment() { + } + + /** + * @return the category URI + * @see org.apache.fop.fo.extensions.ExtensionAttachment#getCategory() + */ + public String getCategory() { + return CATEGORY; + } + + /** @return the content */ + public String getContent() { + return content; + } + + /** + * Sets the content for the setup code object. + * @param content The content to set. + */ + public void setContent(String content) { + this.content = content; + } + + /** + * Generates SAX events representing the object's state. + * + * @param handler ContentHandler instance to send the SAX events to + * @throws SAXException if there's a problem generating the SAX events + * @see org.apache.fop.util.XMLizable#toSAX(org.xml.sax.ContentHandler) + */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + String element = getElement(); + handler.startElement(CATEGORY, element, element, atts); + if (content != null && content.length() > 0) { + char[] chars = content.toCharArray(); + handler.characters(chars, 0, chars.length); + } + handler.endElement(CATEGORY, element, element); + } + + /** @return type name */ + public String getType() { + String className = getClass().getName(); + return className.substring(className.lastIndexOf('.') + 3); + } + + /** + * @return a string representation of this object + * @see java.lang.Object#toString() + */ + public String toString() { + return getType() + ": content=" + content; + } + + /** @return element */ + protected abstract String getElement(); +} diff --git a/src/java/org/apache/fop/render/ps/extensions/PSExtensionElementMapping.java b/src/java/org/apache/fop/render/ps/extensions/PSExtensionElementMapping.java index a2aed74ff..a90af2a9c 100644 --- a/src/java/org/apache/fop/render/ps/extensions/PSExtensionElementMapping.java +++ b/src/java/org/apache/fop/render/ps/extensions/PSExtensionElementMapping.java @@ -41,6 +41,7 @@ public class PSExtensionElementMapping extends ElementMapping { foObjs = new java.util.HashMap(); foObjs.put("ps-setup-code", new PSSetupCodeMaker()); foObjs.put("ps-page-setup-code", new PSPageSetupCodeMaker()); + foObjs.put("ps-setpagedevice", new PSSetPageDeviceMaker()); } } @@ -56,4 +57,9 @@ public class PSExtensionElementMapping extends ElementMapping { } } + static class PSSetPageDeviceMaker extends ElementMapping.Maker { + public FONode make(FONode parent) { + return new PSSetPageDeviceElement(parent); + } + } } diff --git a/src/java/org/apache/fop/render/ps/extensions/PSExtensionHandler.java b/src/java/org/apache/fop/render/ps/extensions/PSExtensionHandler.java index 071ec1c25..6cc41f8cc 100644 --- a/src/java/org/apache/fop/render/ps/extensions/PSExtensionHandler.java +++ b/src/java/org/apache/fop/render/ps/extensions/PSExtensionHandler.java @@ -15,7 +15,7 @@ * limitations under the License. */ -/* $Id$ */ +/* $Id: $ */ package org.apache.fop.render.ps.extensions; @@ -39,24 +39,24 @@ public class PSExtensionHandler extends DefaultHandler private StringBuffer content = new StringBuffer(); private Attributes lastAttributes; - private PSSetupCode returnedObject; + private PSExtensionAttachment returnedObject; private ObjectBuiltListener listener; /** {@inheritDoc} */ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { boolean handled = false; - if (PSSetupCode.CATEGORY.equals(uri)) { + if (PSExtensionAttachment.CATEGORY.equals(uri)) { lastAttributes = attributes; - handled = true; - if ("ps-setup-code".equals(localName)) { + handled = false; + if (localName.equals(PSSetupCode.ELEMENT) + || localName.equals(PSSetPageDevice.ELEMENT)) { //handled in endElement - } else { - handled = false; + handled = true; } } if (!handled) { - if (PSSetupCode.CATEGORY.equals(uri)) { + if (PSExtensionAttachment.CATEGORY.equals(uri)) { throw new SAXException("Unhandled element " + localName + " in namespace: " + uri); } else { @@ -68,10 +68,13 @@ public class PSExtensionHandler extends DefaultHandler /** {@inheritDoc} */ public void endElement(String uri, String localName, String qName) throws SAXException { - if (PSSetupCode.CATEGORY.equals(uri)) { - if ("ps-setup-code".equals(localName)) { + if (PSExtensionAttachment.CATEGORY.equals(uri)) { + if (PSSetupCode.ELEMENT.equals(localName)) { String name = lastAttributes.getValue("name"); this.returnedObject = new PSSetupCode(name, content.toString()); + } else if (PSSetPageDevice.ELEMENT.equals(localName)) { + String name = lastAttributes.getValue("name"); + this.returnedObject = new PSSetPageDevice(name, content.toString()); } } content.setLength(0); //Reset text buffer (see characters()) @@ -104,5 +107,4 @@ public class PSExtensionHandler extends DefaultHandler public void setObjectBuiltListener(ObjectBuiltListener listener) { this.listener = listener; } - } diff --git a/src/java/org/apache/fop/render/ps/extensions/PSSetPageDevice.java b/src/java/org/apache/fop/render/ps/extensions/PSSetPageDevice.java new file mode 100644 index 000000000..5684ba6a3 --- /dev/null +++ b/src/java/org/apache/fop/render/ps/extensions/PSSetPageDevice.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.ps.extensions; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Element for postscript setpagedevice instruction + * This is a an extension which provides a pass-through value + * dictionary object for the postscript setpagedevice instruction. + */ +public class PSSetPageDevice extends PSExtensionAttachment { + /** element name */ + protected static final String ELEMENT = "ps-setpagedevice"; + + private static final String ATT_NAME = "name"; + + /** + * name attribute + */ + protected String name = null; + + /** + * default constructor + * @param content set page device dictionary + */ + public PSSetPageDevice(String content) { + super(content); + } + + /** + * constructor + * @param name name attribute of this setpagedevice content + * @param content set page device dictionary + */ + public PSSetPageDevice(String name, String content) { + this(content); + this.name = name; + } + + /** + * constructor + */ + public PSSetPageDevice() { + } + + /** @return the name */ + public String getName() { + return name; + } + + /** + * Sets the name of the setup code object. + * @param name The name to set. + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return a string representation of this object + * @see java.lang.Object#toString() + */ + public String toString() { + return "PSSetPageDevice(name=" + getName() + ", content='" + getContent() + "')"; + } + + /** + * @return a string representation of this object + * @see org.apache.fop.render.ps.extensions.PSExtensionAttachment#getElement() + */ + protected String getElement() { + return ELEMENT; + } + + /** + * Generates SAX events representing the object's state. + * @param handler ContentHandler instance to send the SAX events to + * @throws SAXException if there's a problem generating the SAX events + * @see org.apache.fop.util.XMLizable#toSAX(org.xml.sax.ContentHandler) + */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + if (name != null && name.length() > 0) { + atts.addAttribute(null, ATT_NAME, ATT_NAME, "CDATA", name); + } + String element = getElement(); + handler.startElement(CATEGORY, element, element, atts); + if (content != null && content.length() > 0) { + char[] chars = content.toCharArray(); + handler.characters(chars, 0, chars.length); + } + handler.endElement(CATEGORY, element, element); + } +} diff --git a/src/java/org/apache/fop/render/ps/extensions/PSSetPageDeviceElement.java b/src/java/org/apache/fop/render/ps/extensions/PSSetPageDeviceElement.java new file mode 100644 index 000000000..da51e9531 --- /dev/null +++ b/src/java/org/apache/fop/render/ps/extensions/PSSetPageDeviceElement.java @@ -0,0 +1,93 @@ +/* + * 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.ps.extensions; + +import org.apache.fop.apps.FOPException; +import org.apache.fop.fo.Constants; +import org.apache.fop.fo.FONode; +import org.apache.fop.fo.PropertyList; +import org.apache.fop.fo.ValidationException; +import org.apache.fop.fo.extensions.ExtensionAttachment; +import org.xml.sax.Attributes; +import org.xml.sax.Locator; + +/** + * Extension element for ps:ps-setpagedevice. + */ +public class PSSetPageDeviceElement extends AbstractPSExtensionElement { + + /** + * Main constructor + * @param parent parent FO node + */ + protected PSSetPageDeviceElement(FONode parent) { + super(parent); + } + + /** + * Called after processNode() is called. Subclasses can do additional processing. + * @throws FOPException if there's a problem during processing + * @see org.apache.fop.fo.FONode#startOfNode() + */ + protected void startOfNode() throws FOPException { + super.startOfNode(); + if ( !((parent.getNameId() == Constants.FO_DECLARATIONS) + || (parent.getNameId() == Constants.FO_SIMPLE_PAGE_MASTER)) ) { + throw new ValidationException( getName() + + " must be a child of fo:declarations or fo:simple-page-master."); + } + } + + /** + * Initialize the node with its name, location information, and attributes + * The attributes must be used immediately as the sax attributes + * will be altered for the next element. + * @param elementName element name (e.g., "fo:block") + * @param locator Locator object (ignored by default) + * @param attlist Collection of attributes passed to us from the parser. + * @param propertyList property list + * @throws FOPException if there's a problem during processing + * @see org.apache.fop.fo.FONode#processNode + */ + public void processNode(String elementName, Locator locator, + Attributes attlist, PropertyList propertyList) + throws FOPException { + String name = attlist.getValue("name"); + if (name != null && name.length() > 0) { + ((PSSetPageDevice)getExtensionAttachment()).setName(name); + } + } + + /** + * @return local name + * @see org.apache.fop.fo.FONode#getLocalName() */ + public String getLocalName() { + return "ps-setpagedevice"; + } + + /** + * @return a new PSSetPageDevice object + * @see org.apache.fop.render.ps.extensions.AbstractPSExtensionElement + * #instantiateExtensionAttachment() + */ + protected ExtensionAttachment instantiateExtensionAttachment() { + return new PSSetPageDevice(); + } +} diff --git a/src/java/org/apache/fop/render/ps/extensions/PSSetupCode.java b/src/java/org/apache/fop/render/ps/extensions/PSSetupCode.java index 7ad66427c..eb3ed0e39 100644 --- a/src/java/org/apache/fop/render/ps/extensions/PSSetupCode.java +++ b/src/java/org/apache/fop/render/ps/extensions/PSSetupCode.java @@ -19,10 +19,6 @@ package org.apache.fop.render.ps.extensions; -import java.io.Serializable; - -import org.apache.fop.fo.extensions.ExtensionAttachment; -import org.apache.fop.util.XMLizable; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; @@ -30,19 +26,23 @@ import org.xml.sax.helpers.AttributesImpl; /** * This is the pass-through value object for the PostScript extension. */ -public class PSSetupCode implements ExtensionAttachment, Serializable, XMLizable { - - /** The category URI for this extension attachment. */ - public static final String CATEGORY = "apache:fop:extensions:postscript"; +public class PSSetupCode extends PSExtensionAttachment { + /** + * element name + */ + protected static final String ELEMENT = "ps-setup-code"; - private String name; - private String content; + private static final String ATT_NAME = "name"; + + /** + * name attribute + */ + protected String name = null; /** * No-argument contructor. */ public PSSetupCode() { - //nop } /** @@ -51,23 +51,10 @@ public class PSSetupCode implements ExtensionAttachment, Serializable, XMLizable * @param content the content of the setup code object */ public PSSetupCode(String name, String content) { + super(content); this.name = name; - this.content = content; - } - - /** @return the content */ - public String getContent() { - return content; } - - /** - * Sets the content for the setup code object. - * @param content The content to set. - */ - public void setContent(String content) { - this.content = content; - } - + /** @return the name */ public String getName() { return name; @@ -88,11 +75,16 @@ public class PSSetupCode implements ExtensionAttachment, Serializable, XMLizable /** {@inheritDoc} */ public String toString() { - return "PSSetupCode(name=" + getName() + ")"; + return "PSSetupCode(name=" + getName() + ", content='" + getContent() + "')"; } - private static final String ATT_NAME = "name"; - private static final String ELEMENT = "ps-setup-code"; + /** + * @return the element name + * @see org.apache.fop.render.ps.extensions.PSExtensionAttachment#getElement() + */ + protected String getElement() { + return ELEMENT; + } /** {@inheritDoc} */ public void toSAX(ContentHandler handler) throws SAXException { @@ -100,12 +92,12 @@ public class PSSetupCode implements ExtensionAttachment, Serializable, XMLizable if (name != null && name.length() > 0) { atts.addAttribute(null, ATT_NAME, ATT_NAME, "CDATA", name); } - handler.startElement(CATEGORY, ELEMENT, ELEMENT, atts); + String element = getElement(); + handler.startElement(CATEGORY, element, element, atts); if (content != null && content.length() > 0) { char[] chars = content.toCharArray(); handler.characters(chars, 0, chars.length); } - handler.endElement(CATEGORY, ELEMENT, ELEMENT); + handler.endElement(CATEGORY, element, element); } - } diff --git a/test/layoutengine/standard-testcases/ps-extension_2.xml b/test/layoutengine/standard-testcases/ps-extension_2.xml new file mode 100644 index 000000000..d0c18a977 --- /dev/null +++ b/test/layoutengine/standard-testcases/ps-extension_2.xml @@ -0,0 +1,70 @@ + + + + + +

+ This test checks the PostScript extension for custom setpagedevice calls and comments. + The extension attachments need to show up in the area tree XML so the AreaTreeParser can fully restore the area tree. +

+
+ + + + + > ]]> + + + + > ]]> + + + + + + + + + + > ]]> + + + + Hello World! + + Hello World! + + Hello World! + + + + + + + + + + + + + + + + + +
-- 2.39.5