blob: 897ed8fb826689e026de0817c42f7208bb9a2519 [file] [log] [blame]
/*
* 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.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 = 0;
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());
}
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 PSGenerator(out) {
/** Need to subclass PSGenerator to have better URI resolution */
public Source resolveURI(String uri) {
return getUserAgent().resolveURI(uri);
}
};
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);
}
}
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, new Integer(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, new Integer(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,
new Integer(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(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);
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 = new Integer(0);
Rectangle2D pageBoundingBox = new Rectangle2D.Double();
if (rotate) {
pageBoundingBox.setRect(0, 0, pageHeight, pageWidth);
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 {
pageBoundingBox.setRect(0, 0, pageWidth, pageHeight);
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 (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
*/
protected 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;
}
}
}