| /* |
| * 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); |
| } |
| |
| } |