/* * 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.awt.Dimension; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.transform.Source; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xmlgraphics.io.TempResourceURIGenerator; import org.apache.xmlgraphics.java2d.Dimension2DDouble; import org.apache.xmlgraphics.ps.DSCConstants; import org.apache.xmlgraphics.ps.PSDictionary; import org.apache.xmlgraphics.ps.PSDictionaryFormatException; import org.apache.xmlgraphics.ps.PSGenerator; import org.apache.xmlgraphics.ps.PSPageDeviceDictionary; import org.apache.xmlgraphics.ps.PSProcSets; import org.apache.xmlgraphics.ps.PSResource; import org.apache.xmlgraphics.ps.dsc.DSCException; import org.apache.xmlgraphics.ps.dsc.ResourceTracker; import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBoundingBox; import org.apache.xmlgraphics.ps.dsc.events.DSCCommentHiResBoundingBox; import org.apache.fop.apps.MimeConstants; import org.apache.fop.render.intermediate.AbstractBinaryWritingIFDocumentHandler; import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFDocumentHandlerConfigurator; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFPainter; import org.apache.fop.render.ps.PSRendererConfig.PSRendererConfigParser; import org.apache.fop.render.ps.extensions.PSCommentAfter; import org.apache.fop.render.ps.extensions.PSCommentBefore; import org.apache.fop.render.ps.extensions.PSPageTrailerCodeBefore; import org.apache.fop.render.ps.extensions.PSSetPageDevice; import org.apache.fop.render.ps.extensions.PSSetupCode; /** * {@link org.apache.fop.render.intermediate.IFDocumentHandler} implementation * that produces PostScript. */ public class PSDocumentHandler extends AbstractBinaryWritingIFDocumentHandler { /** logging instance */ private static Log log = LogFactory.getLog(PSDocumentHandler.class); /** * Utility class which enables all sorts of features that are not directly connected to the * normal rendering process. */ private PSRenderingUtil psUtil; /** The PostScript generator used to output the PostScript */ PSGenerator gen; /** the temporary file in case of two-pass processing */ private URI tempURI; private static final TempResourceURIGenerator TEMP_URI_GENERATOR = new TempResourceURIGenerator("ps-optimize"); private int currentPageNumber; private PageDefinition currentPageDefinition; /** Is used to determine the document's bounding box */ private Rectangle2D documentBoundingBox; /** Used to temporarily store PSSetupCode instance until they can be written. */ private List setupCodeList; /** This is a cache of PSResource instances of all fonts defined */ private FontResourceCache 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; /** This is a collection holding all document header comments */ private Collection[] comments = new Collection[4]; private static final int COMMENT_DOCUMENT_HEADER = 0; private static final int COMMENT_DOCUMENT_TRAILER = 1; private static final int COMMENT_PAGE_TRAILER = 2; private static final int PAGE_TRAILER_CODE_BEFORE = 3; private PSEventProducer eventProducer; /** * Default constructor. */ public PSDocumentHandler(IFContext context) { super(context); this.psUtil = new PSRenderingUtil(context.getUserAgent()); } /** {@inheritDoc} */ public boolean supportsPagesOutOfOrder() { return false; } /** {@inheritDoc} */ public String getMimeType() { return MimeConstants.MIME_POSTSCRIPT; } PSGenerator getGenerator() { return gen; } /** {@inheritDoc} */ public IFDocumentHandlerConfigurator getConfigurator() { return new PSRendererConfigurator(getUserAgent(), new PSRendererConfigParser()); } public PSRenderingUtil getPSUtil() { return this.psUtil; } /** {@inheritDoc} */ public void startDocument() throws IFException { super.startDocument(); this.fontResources = new FontResourceCache(getFontInfo()); try { final OutputStream out; if (psUtil.isOptimizeResources()) { tempURI = TEMP_URI_GENERATOR.generate(); out = new BufferedOutputStream(getUserAgent().getResourceResolver().getOutputStream(tempURI)); } else { out = this.outputStream; } //Setup for PostScript generation this.gen = new FOPPSGeneratorImpl(out); this.gen.setPSLevel(psUtil.getLanguageLevel()); this.currentPageNumber = 0; this.documentBoundingBox = new Rectangle2D.Double(); //Initial default page device dictionary settings this.pageDeviceDictionary = new PSPageDeviceDictionary(); pageDeviceDictionary.setFlushOnRetrieval(!psUtil.isDSCComplianceEnabled()); pageDeviceDictionary.put("/ImagingBBox", "null"); } catch (IOException e) { throw new IFException("I/O error in startDocument()", e); } } public interface FOPPSGenerator { PSDocumentHandler getHandler(); BufferedOutputStream getTempStream(URI uri) throws IOException; Map<Integer, URI> getImages(); } public class FOPPSGeneratorImpl extends PSGenerator implements FOPPSGenerator { private Map<Integer, URI> images = new HashMap<Integer, URI>(); public FOPPSGeneratorImpl(OutputStream out) { super(out); } /** Need to subclass PSGenerator to have better URI resolution */ @Override public Source resolveURI(String uri) { return getUserAgent().resolveURI(uri); } public PSDocumentHandler getHandler() { return PSDocumentHandler.this; } public BufferedOutputStream getTempStream(URI uri) throws IOException { return new BufferedOutputStream(getUserAgent().getResourceResolver().getOutputStream(uri)); } public Map<Integer, URI> getImages() { return images; } } private void writeHeader() throws IOException { //PostScript Header gen.writeln(DSCConstants.PS_ADOBE_30); gen.writeDSCComment(DSCConstants.CREATOR, new String[] {getUserAgent().getProducer()}); gen.writeDSCComment(DSCConstants.CREATION_DATE, new Object[] {new java.util.Date()}); gen.writeDSCComment(DSCConstants.LANGUAGE_LEVEL, gen.getPSLevel()); gen.writeDSCComment(DSCConstants.PAGES, new Object[] {DSCConstants.ATEND}); gen.writeDSCComment(DSCConstants.BBOX, DSCConstants.ATEND); gen.writeDSCComment(DSCConstants.HIRES_BBOX, DSCConstants.ATEND); gen.writeDSCComment(DSCConstants.DOCUMENT_SUPPLIED_RESOURCES, new Object[] {DSCConstants.ATEND}); writeExtensions(COMMENT_DOCUMENT_HEADER); gen.writeDSCComment(DSCConstants.END_COMMENTS); //Defaults gen.writeDSCComment(DSCConstants.BEGIN_DEFAULTS); gen.writeDSCComment(DSCConstants.END_DEFAULTS); //Prolog and Setup written right before the first page-sequence, see startPageSequence() //Do this only once, as soon as we have all the content for the Setup section! //Prolog gen.writeDSCComment(DSCConstants.BEGIN_PROLOG); PSProcSets.writeStdProcSet(gen); PSProcSets.writeEPSProcSet(gen); FOPProcSet.INSTANCE.writeTo(gen); gen.writeDSCComment(DSCConstants.END_PROLOG); //Setup gen.writeDSCComment(DSCConstants.BEGIN_SETUP); PSRenderingUtil.writeSetupCodeList(gen, setupCodeList, "SetupCode"); if (!psUtil.isOptimizeResources()) { this.fontResources.addAll(PSFontUtils.writeFontDict(gen, fontInfo, eventProducer)); } else { gen.commentln("%FOPFontSetup"); //Place-holder, will be replaced in the second pass } gen.writeDSCComment(DSCConstants.END_SETUP); } /** {@inheritDoc} */ public void endDocumentHeader() throws IFException { try { writeHeader(); } catch (IOException ioe) { throw new IFException("I/O error writing the PostScript header", ioe); } } /** {@inheritDoc} */ public void endDocument() throws IFException { try { //Write trailer gen.writeDSCComment(DSCConstants.TRAILER); writeExtensions(COMMENT_DOCUMENT_TRAILER); gen.writeDSCComment(DSCConstants.PAGES, this.currentPageNumber); new DSCCommentBoundingBox(this.documentBoundingBox).generate(gen); new DSCCommentHiResBoundingBox(this.documentBoundingBox).generate(gen); gen.getResourceTracker().writeResources(false, gen); gen.writeDSCComment(DSCConstants.EOF); gen.flush(); log.debug("Rendering to PostScript complete."); if (psUtil.isOptimizeResources()) { IOUtils.closeQuietly(gen.getOutputStream()); rewritePostScriptFile(); } if (pageDeviceDictionary != null) { pageDeviceDictionary.clear(); } } catch (IOException ioe) { throw new IFException("I/O error in endDocument()", ioe); } super.endDocument(); } /** * Used for two-pass production. This will rewrite the PostScript file from the temporary * file while adding all needed resources. * @throws IOException In case of an I/O error. */ private void rewritePostScriptFile() throws IOException { log.debug("Processing PostScript resources..."); long startTime = System.currentTimeMillis(); ResourceTracker resTracker = gen.getResourceTracker(); InputStream in = new BufferedInputStream(getUserAgent().getResourceResolver().getResource(tempURI)); try { try { ResourceHandler handler = new ResourceHandler(getUserAgent(), eventProducer, this.fontInfo, resTracker, this.formResources); handler.process(in, this.outputStream, this.currentPageNumber, this.documentBoundingBox); this.outputStream.flush(); } catch (DSCException e) { throw new RuntimeException(e.getMessage()); } } finally { IOUtils.closeQuietly(in); } if (log.isDebugEnabled()) { long duration = System.currentTimeMillis() - startTime; log.debug("Resource Processing complete in " + duration + " ms."); } } /** {@inheritDoc} */ public void startPageSequence(String id) throws IFException { //nop } /** {@inheritDoc} */ public void endPageSequence() throws IFException { //nop } /** {@inheritDoc} */ public void startPage(int index, String name, String pageMasterName, Dimension size) throws IFException { try { /* if (this.currentPageNumber == 0) { //writeHeader(); } */ this.currentPageNumber++; gen.getResourceTracker().notifyStartNewPage(); gen.getResourceTracker().notifyResourceUsageOnPage(PSProcSets.STD_PROCSET); gen.writeDSCComment(DSCConstants.PAGE, new Object[] {name, this.currentPageNumber}); double pageWidth = size.width / 1000.0; double pageHeight = size.height / 1000.0; boolean rotate = false; List pageSizes = new java.util.ArrayList(); if (this.psUtil.isAutoRotateLandscape() && (pageHeight < pageWidth)) { rotate = true; pageSizes.add(Math.round(pageHeight)); pageSizes.add(Math.round(pageWidth)); } else { pageSizes.add(Math.round(pageWidth)); pageSizes.add(Math.round(pageHeight)); } pageDeviceDictionary.put("/PageSize", pageSizes); this.currentPageDefinition = new PageDefinition( new Dimension2DDouble(pageWidth, pageHeight), rotate); //TODO Handle extension attachments for the page!!!!!!! /* 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) { PSEventProducer eventProducer = PSEventProducer.Provider.get( getUserAgent().getEventBroadcaster()); eventProducer.postscriptDictionaryParseError(this, content, e); } } } } }*/ final Integer zero = 0; Rectangle2D pageBoundingBox = new Rectangle2D.Double(); if (rotate) { pageBoundingBox.setRect(0, 0, pageHeight, pageWidth); gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] { zero, zero, Math.round(pageHeight), Math.round(pageWidth)}); gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] { zero, zero, pageHeight, pageWidth}); gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION, "Landscape"); } else { pageBoundingBox.setRect(0, 0, pageWidth, pageHeight); gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] { zero, zero, Math.round(pageWidth), Math.round(pageHeight)}); gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] { zero, zero, pageWidth, pageHeight}); if (psUtil.isAutoRotateLandscape()) { gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION, "Portrait"); } } this.documentBoundingBox.add(pageBoundingBox); gen.writeDSCComment(DSCConstants.PAGE_RESOURCES, new Object[] {DSCConstants.ATEND}); gen.commentln("%FOPSimplePageMaster: " + pageMasterName); } catch (IOException ioe) { throw new IFException("I/O error in startPage()", ioe); } } /** {@inheritDoc} */ public void startPageHeader() throws IFException { super.startPageHeader(); try { gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP); } catch (IOException ioe) { throw new IFException("I/O error in startPageHeader()", ioe); } } /** {@inheritDoc} */ public void endPageHeader() throws IFException { try { // Write any unwritten changes to page device dictionary if (!pageDeviceDictionary.isEmpty()) { String content = pageDeviceDictionary.getContent(); if (psUtil.isSafeSetPageDevice()) { content += " SSPD"; } else { content += " setpagedevice"; } PSRenderingUtil.writeEnclosedExtensionAttachment(gen, new PSSetPageDevice(content)); } double pageHeight = this.currentPageDefinition.dimensions.getHeight(); if (this.currentPageDefinition.rotate) { gen.writeln(gen.formatDouble(pageHeight) + " 0 translate"); gen.writeln("90 rotate"); } gen.concatMatrix(1, 0, 0, -1, 0, pageHeight); gen.writeDSCComment(DSCConstants.END_PAGE_SETUP); } catch (IOException ioe) { throw new IFException("I/O error in endPageHeader()", ioe); } super.endPageHeader(); } private void writeExtensions(int which) throws IOException { Collection extensions = comments[which]; if (extensions != null) { PSRenderingUtil.writeEnclosedExtensionAttachments(gen, extensions); extensions.clear(); } } /** {@inheritDoc} */ public IFPainter startPageContent() throws IFException { return new PSPainter(this); } /** {@inheritDoc} */ public void endPageContent() throws IFException { try { gen.showPage(); } catch (IOException ioe) { throw new IFException("I/O error in endPageContent()", ioe); } } /** {@inheritDoc} */ public void startPageTrailer() throws IFException { try { writeExtensions(PAGE_TRAILER_CODE_BEFORE); super.startPageTrailer(); gen.writeDSCComment(DSCConstants.PAGE_TRAILER); } catch (IOException ioe) { throw new IFException("I/O error in startPageTrailer()", ioe); } } /** {@inheritDoc} */ public void endPageTrailer() throws IFException { try { writeExtensions(COMMENT_PAGE_TRAILER); } catch (IOException ioe) { throw new IFException("I/O error in endPageTrailer()", ioe); } super.endPageTrailer(); } /** {@inheritDoc} */ public void endPage() throws IFException { try { gen.getResourceTracker().writeResources(true, gen); } catch (IOException ioe) { throw new IFException("I/O error in endPage()", ioe); } this.currentPageDefinition = null; } private boolean inPage() { return this.currentPageDefinition != null; } /** {@inheritDoc} */ public void handleExtensionObject(Object extension) throws IFException { try { if (extension instanceof PSSetupCode) { if (inPage()) { PSRenderingUtil.writeEnclosedExtensionAttachment(gen, (PSSetupCode)extension); } else { //A special collection for setup code as it's put in a different place //than the "before comments". if (setupCodeList == null) { setupCodeList = new java.util.ArrayList(); } if (!setupCodeList.contains(extension)) { setupCodeList.add(extension); } } } else if (extension 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)extension; String content = setPageDevice.getContent(); if (content != null) { try { this.pageDeviceDictionary.putAll(PSDictionary.valueOf(content)); } catch (PSDictionaryFormatException e) { PSEventProducer eventProducer = PSEventProducer.Provider.get( getUserAgent().getEventBroadcaster()); eventProducer.postscriptDictionaryParseError(this, content, e); } } } else if (extension instanceof PSCommentBefore) { if (inPage()) { PSRenderingUtil.writeEnclosedExtensionAttachment( gen, (PSCommentBefore)extension); } else { if (comments[COMMENT_DOCUMENT_HEADER] == null) { comments[COMMENT_DOCUMENT_HEADER] = new java.util.ArrayList(); } comments[COMMENT_DOCUMENT_HEADER].add(extension); } } else if (extension instanceof PSCommentAfter) { int targetCollection = (inPage() ? COMMENT_PAGE_TRAILER : COMMENT_DOCUMENT_TRAILER); if (comments[targetCollection] == null) { comments[targetCollection] = new java.util.ArrayList(); } comments[targetCollection].add(extension); } else if (extension instanceof PSPageTrailerCodeBefore) { if (comments[PAGE_TRAILER_CODE_BEFORE] == null) { comments[PAGE_TRAILER_CODE_BEFORE] = new ArrayList(); } comments[PAGE_TRAILER_CODE_BEFORE].add(extension); } } catch (IOException ioe) { throw new IFException("I/O error in handleExtensionObject()", ioe); } } /** * Returns the PSResource for the given font key. * @param key the font key ("F*") * @return the matching PSResource */ protected PSFontResource getPSResourceForFontKey(String key) { return this.fontResources.getFontResourceForFontKey(key); } /** * Returns a PSResource instance representing a image as a PostScript form. * @param uri the image URI * @return a PSResource instance */ public PSResource getFormForImage(String uri) { if (uri == null || "".equals(uri)) { throw new IllegalArgumentException("uri must not be empty or null"); } if (this.formResources == null) { this.formResources = new java.util.HashMap(); } PSResource form = (PSResource)this.formResources.get(uri); if (form == null) { form = new PSImageFormResource(this.formResources.size() + 1, uri); this.formResources.put(uri, form); } return form; } private static final class PageDefinition { private Dimension2D dimensions; private boolean rotate; private PageDefinition(Dimension2D dimensions, boolean rotate) { this.dimensions = dimensions; this.rotate = rotate; } } }