blob: 2e3c8312636e6ebe4e58af2bfa9d917325c07fa1 [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.pdf;
import java.awt.color.ICC_Profile;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Map;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.xmp.Metadata;
import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter;
import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.fo.extensions.xmp.XMPMetadata;
import org.apache.fop.pdf.PDFAMode;
import org.apache.fop.pdf.PDFConformanceException;
import org.apache.fop.pdf.PDFDictionary;
import org.apache.fop.pdf.PDFDocument;
import org.apache.fop.pdf.PDFEncryptionManager;
import org.apache.fop.pdf.PDFEncryptionParams;
import org.apache.fop.pdf.PDFICCBasedColorSpace;
import org.apache.fop.pdf.PDFICCStream;
import org.apache.fop.pdf.PDFInfo;
import org.apache.fop.pdf.PDFMetadata;
import org.apache.fop.pdf.PDFNumsArray;
import org.apache.fop.pdf.PDFOutputIntent;
import org.apache.fop.pdf.PDFPageLabels;
import org.apache.fop.pdf.PDFXMode;
import org.apache.fop.util.ColorProfileUtil;
/**
* Utility class which enables all sorts of features that are not directly connected to the
* normal rendering process.
*/
class PDFRenderingUtil implements PDFConfigurationConstants {
/** logging instance */
private static Log log = LogFactory.getLog(PDFRenderingUtil.class);
private FOUserAgent userAgent;
/** the PDF Document being created */
protected PDFDocument pdfDoc;
/** the PDF/A mode (Default: disabled) */
protected PDFAMode pdfAMode = PDFAMode.DISABLED;
/** the PDF/X mode (Default: disabled) */
protected PDFXMode pdfXMode = PDFXMode.DISABLED;
/** the (optional) encryption parameters */
protected PDFEncryptionParams encryptionParams;
/** Registry of PDF filters */
protected Map filterMap;
/** the ICC stream used as output profile by this document for PDF/A and PDF/X functionality. */
protected PDFICCStream outputProfile;
/** the default sRGB color space. */
protected PDFICCBasedColorSpace sRGBColorSpace;
/** controls whether the sRGB color space should be installed */
protected boolean disableSRGBColorSpace = false;
/** Optional URI to an output profile to be used. */
protected String outputProfileURI;
PDFRenderingUtil(FOUserAgent userAgent) {
this.userAgent = userAgent;
initialize();
}
private static boolean booleanValueOf(Object obj) {
if (obj instanceof Boolean) {
return ((Boolean)obj).booleanValue();
} else if (obj instanceof String) {
return Boolean.valueOf((String)obj).booleanValue();
} else {
throw new IllegalArgumentException("Boolean or \"true\" or \"false\" expected.");
}
}
private void initialize() {
PDFEncryptionParams params
= (PDFEncryptionParams)userAgent.getRendererOptions().get(ENCRYPTION_PARAMS);
if (params != null) {
this.encryptionParams = params; //overwrite if available
}
String pwd;
pwd = (String)userAgent.getRendererOptions().get(USER_PASSWORD);
if (pwd != null) {
if (encryptionParams == null) {
this.encryptionParams = new PDFEncryptionParams();
}
this.encryptionParams.setUserPassword(pwd);
}
pwd = (String)userAgent.getRendererOptions().get(OWNER_PASSWORD);
if (pwd != null) {
if (encryptionParams == null) {
this.encryptionParams = new PDFEncryptionParams();
}
this.encryptionParams.setOwnerPassword(pwd);
}
Object setting;
setting = userAgent.getRendererOptions().get(NO_PRINT);
if (setting != null) {
if (encryptionParams == null) {
this.encryptionParams = new PDFEncryptionParams();
}
this.encryptionParams.setAllowPrint(!booleanValueOf(setting));
}
setting = userAgent.getRendererOptions().get(NO_COPY_CONTENT);
if (setting != null) {
if (encryptionParams == null) {
this.encryptionParams = new PDFEncryptionParams();
}
this.encryptionParams.setAllowCopyContent(!booleanValueOf(setting));
}
setting = userAgent.getRendererOptions().get(NO_EDIT_CONTENT);
if (setting != null) {
if (encryptionParams == null) {
this.encryptionParams = new PDFEncryptionParams();
}
this.encryptionParams.setAllowEditContent(!booleanValueOf(setting));
}
setting = userAgent.getRendererOptions().get(NO_ANNOTATIONS);
if (setting != null) {
if (encryptionParams == null) {
this.encryptionParams = new PDFEncryptionParams();
}
this.encryptionParams.setAllowEditAnnotations(!booleanValueOf(setting));
}
String s = (String)userAgent.getRendererOptions().get(PDF_A_MODE);
if (s != null) {
this.pdfAMode = PDFAMode.valueOf(s);
}
s = (String)userAgent.getRendererOptions().get(PDF_X_MODE);
if (s != null) {
this.pdfXMode = PDFXMode.valueOf(s);
}
s = (String)userAgent.getRendererOptions().get(KEY_OUTPUT_PROFILE);
if (s != null) {
this.outputProfileURI = s;
}
setting = userAgent.getRendererOptions().get(KEY_DISABLE_SRGB_COLORSPACE);
if (setting != null) {
this.disableSRGBColorSpace = booleanValueOf(setting);
}
}
public FOUserAgent getUserAgent() {
return this.userAgent;
}
/**
* Sets the PDF/A mode for the PDF renderer.
* @param mode the PDF/A mode
*/
public void setAMode(PDFAMode mode) {
this.pdfAMode = mode;
}
/**
* Sets the PDF/X mode for the PDF renderer.
* @param mode the PDF/X mode
*/
public void setXMode(PDFXMode mode) {
this.pdfXMode = mode;
}
/**
* Sets the output color profile for the PDF renderer.
* @param outputProfileURI the URI to the output color profile
*/
public void setOutputProfileURI(String outputProfileURI) {
this.outputProfileURI = outputProfileURI;
}
/**
* Enables or disables the default sRGB color space needed for the PDF document to preserve
* the sRGB colors used in XSL-FO.
* @param disable true to disable, false to enable
*/
public void setDisableSRGBColorSpace(boolean disable) {
this.disableSRGBColorSpace = disable;
}
/**
* Sets the filter map to be used by the PDF renderer.
* @param filterMap the filter map
*/
public void setFilterMap(Map filterMap) {
this.filterMap = filterMap;
}
/**
* Sets the encryption parameters used by the PDF renderer.
* @param encryptionParams the encryption parameters
*/
public void setEncryptionParams(PDFEncryptionParams encryptionParams) {
this.encryptionParams = encryptionParams;
}
private void updateInfo() {
PDFInfo info = pdfDoc.getInfo();
info.setCreator(userAgent.getCreator());
info.setCreationDate(userAgent.getCreationDate());
info.setAuthor(userAgent.getAuthor());
info.setTitle(userAgent.getTitle());
info.setSubject(userAgent.getSubject());
info.setKeywords(userAgent.getKeywords());
}
private void updatePDFProfiles() {
pdfDoc.getProfile().setPDFAMode(this.pdfAMode);
pdfDoc.getProfile().setPDFXMode(this.pdfXMode);
}
private void addsRGBColorSpace() throws IOException {
if (disableSRGBColorSpace) {
if (this.pdfAMode != PDFAMode.DISABLED
|| this.pdfXMode != PDFXMode.DISABLED
|| this.outputProfileURI != null) {
throw new IllegalStateException("It is not possible to disable the sRGB color"
+ " space if PDF/A or PDF/X functionality is enabled or an"
+ " output profile is set!");
}
} else {
if (this.sRGBColorSpace != null) {
return;
}
//Map sRGB as default RGB profile for DeviceRGB
this.sRGBColorSpace = PDFICCBasedColorSpace.setupsRGBAsDefaultRGBColorSpace(pdfDoc);
}
}
private void addDefaultOutputProfile() throws IOException {
if (this.outputProfile != null) {
return;
}
ICC_Profile profile;
InputStream in = null;
if (this.outputProfileURI != null) {
this.outputProfile = pdfDoc.getFactory().makePDFICCStream();
Source src = getUserAgent().resolveURI(this.outputProfileURI);
if (src == null) {
throw new IOException("Output profile not found: " + this.outputProfileURI);
}
if (src instanceof StreamSource) {
in = ((StreamSource)src).getInputStream();
} else {
in = new URL(src.getSystemId()).openStream();
}
try {
profile = ICC_Profile.getInstance(in);
} finally {
IOUtils.closeQuietly(in);
}
this.outputProfile.setColorSpace(profile, null);
} else {
//Fall back to sRGB profile
outputProfile = sRGBColorSpace.getICCStream();
}
}
/**
* Adds an OutputIntent to the PDF as mandated by PDF/A-1 when uncalibrated color spaces
* are used (which is true if we use DeviceRGB to represent sRGB colors).
* @throws IOException in case of an I/O problem
*/
private void addPDFA1OutputIntent() throws IOException {
addDefaultOutputProfile();
String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile());
PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent();
outputIntent.setSubtype(PDFOutputIntent.GTS_PDFA1);
outputIntent.setDestOutputProfile(this.outputProfile);
outputIntent.setOutputConditionIdentifier(desc);
outputIntent.setInfo(outputIntent.getOutputConditionIdentifier());
pdfDoc.getRoot().addOutputIntent(outputIntent);
}
/**
* Adds an OutputIntent to the PDF as mandated by PDF/X when uncalibrated color spaces
* are used (which is true if we use DeviceRGB to represent sRGB colors).
* @throws IOException in case of an I/O problem
*/
private void addPDFXOutputIntent() throws IOException {
addDefaultOutputProfile();
String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile());
int deviceClass = this.outputProfile.getICCProfile().getProfileClass();
if (deviceClass != ICC_Profile.CLASS_OUTPUT) {
throw new PDFConformanceException(pdfDoc.getProfile().getPDFXMode() + " requires that"
+ " the DestOutputProfile be an Output Device Profile. "
+ desc + " does not match that requirement.");
}
PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent();
outputIntent.setSubtype(PDFOutputIntent.GTS_PDFX);
outputIntent.setDestOutputProfile(this.outputProfile);
outputIntent.setOutputConditionIdentifier(desc);
outputIntent.setInfo(outputIntent.getOutputConditionIdentifier());
pdfDoc.getRoot().addOutputIntent(outputIntent);
}
public void renderXMPMetadata(XMPMetadata metadata) {
Metadata docXMP = metadata.getMetadata();
Metadata fopXMP = PDFMetadata.createXMPFromPDFDocument(pdfDoc);
//Merge FOP's own metadata into the one from the XSL-FO document
fopXMP.mergeInto(docXMP);
XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(docXMP);
//Metadata was changed so update metadata date
xmpBasic.setMetadataDate(new java.util.Date());
PDFMetadata.updateInfoFromMetadata(docXMP, pdfDoc.getInfo());
PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
docXMP, metadata.isReadOnly());
pdfDoc.getRoot().setMetadata(pdfMetadata);
}
public void generateDefaultXMPMetadata() {
if (pdfDoc.getRoot().getMetadata() == null) {
//If at this time no XMP metadata for the overall document has been set, create it
//from the PDFInfo object.
Metadata xmp = PDFMetadata.createXMPFromPDFDocument(pdfDoc);
PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
xmp, true);
pdfDoc.getRoot().setMetadata(pdfMetadata);
}
}
public PDFDocument setupPDFDocument(OutputStream out) throws IOException {
if (this.pdfDoc != null) {
throw new IllegalStateException("PDFDocument already set up");
}
this.pdfDoc = new PDFDocument(
userAgent.getProducer() != null ? userAgent.getProducer() : "");
updateInfo();
updatePDFProfiles();
pdfDoc.setFilterMap(filterMap);
pdfDoc.outputHeader(out);
//Setup encryption if necessary
PDFEncryptionManager.setupPDFEncryption(encryptionParams, pdfDoc);
addsRGBColorSpace();
if (this.outputProfileURI != null) {
addDefaultOutputProfile();
}
if (pdfXMode != PDFXMode.DISABLED) {
log.debug(pdfXMode + " is active.");
log.warn("Note: " + pdfXMode
+ " support is work-in-progress and not fully implemented, yet!");
addPDFXOutputIntent();
}
if (pdfAMode.isPDFA1LevelB()) {
log.debug("PDF/A is active. Conformance Level: " + pdfAMode);
addPDFA1OutputIntent();
}
return this.pdfDoc;
}
/**
* Generates a page label in the PDF document.
* @param pageIndex the index of the page
* @param pageNumber the formatted page number
*/
public void generatePageLabel(int pageIndex, String pageNumber) {
//Produce page labels
PDFPageLabels pageLabels = this.pdfDoc.getRoot().getPageLabels();
if (pageLabels == null) {
//Set up PageLabels
pageLabels = this.pdfDoc.getFactory().makePageLabels();
this.pdfDoc.getRoot().setPageLabels(pageLabels);
}
PDFNumsArray nums = pageLabels.getNums();
PDFDictionary dict = new PDFDictionary(nums);
dict.put("P", pageNumber);
//TODO If the sequence of generated page numbers were inspected, this could be
//expressed in a more space-efficient way
nums.put(pageIndex, dict);
}
}