| /* |
| * 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.FileNotFoundException; |
| 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.image.loader.util.ImageUtil; |
| import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil; |
| import org.apache.xmlgraphics.xmp.Metadata; |
| import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter; |
| import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema; |
| |
| import org.apache.fop.accessibility.Accessibility; |
| 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.PDFArray; |
| import org.apache.fop.pdf.PDFConformanceException; |
| import org.apache.fop.pdf.PDFDictionary; |
| import org.apache.fop.pdf.PDFDocument; |
| import org.apache.fop.pdf.PDFEmbeddedFile; |
| import org.apache.fop.pdf.PDFEmbeddedFiles; |
| import org.apache.fop.pdf.PDFEncryptionManager; |
| import org.apache.fop.pdf.PDFEncryptionParams; |
| import org.apache.fop.pdf.PDFFileSpec; |
| 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.PDFNames; |
| import org.apache.fop.pdf.PDFNumsArray; |
| import org.apache.fop.pdf.PDFOutputIntent; |
| import org.apache.fop.pdf.PDFPageLabels; |
| import org.apache.fop.pdf.PDFReference; |
| import org.apache.fop.pdf.PDFText; |
| import org.apache.fop.pdf.PDFXMode; |
| import org.apache.fop.pdf.Version; |
| import org.apache.fop.pdf.VersionController; |
| import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileExtensionAttachment; |
| |
| /** |
| * 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; |
| |
| protected Version maxPDFVersion; |
| |
| |
| 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 userPassword = (String)userAgent.getRendererOptions().get(USER_PASSWORD); |
| if (userPassword != null) { |
| getEncryptionParams().setUserPassword(userPassword); |
| } |
| String ownerPassword = (String)userAgent.getRendererOptions().get(OWNER_PASSWORD); |
| if (ownerPassword != null) { |
| getEncryptionParams().setOwnerPassword(ownerPassword); |
| } |
| Object noPrint = userAgent.getRendererOptions().get(NO_PRINT); |
| if (noPrint != null) { |
| getEncryptionParams().setAllowPrint(!booleanValueOf(noPrint)); |
| } |
| Object noCopyContent = userAgent.getRendererOptions().get(NO_COPY_CONTENT); |
| if (noCopyContent != null) { |
| getEncryptionParams().setAllowCopyContent(!booleanValueOf(noCopyContent)); |
| } |
| Object noEditContent = userAgent.getRendererOptions().get(NO_EDIT_CONTENT); |
| if (noEditContent != null) { |
| getEncryptionParams().setAllowEditContent(!booleanValueOf(noEditContent)); |
| } |
| Object noAnnotations = userAgent.getRendererOptions().get(NO_ANNOTATIONS); |
| if (noAnnotations != null) { |
| getEncryptionParams().setAllowEditAnnotations(!booleanValueOf(noAnnotations)); |
| } |
| Object noFillInForms = userAgent.getRendererOptions().get(NO_FILLINFORMS); |
| if (noFillInForms != null) { |
| getEncryptionParams().setAllowFillInForms(!booleanValueOf(noFillInForms)); |
| } |
| Object noAccessContent = userAgent.getRendererOptions().get(NO_ACCESSCONTENT); |
| if (noAccessContent != null) { |
| getEncryptionParams().setAllowAccessContent(!booleanValueOf(noAccessContent)); |
| } |
| Object noAssembleDoc = userAgent.getRendererOptions().get(NO_ASSEMBLEDOC); |
| if (noAssembleDoc != null) { |
| getEncryptionParams().setAllowAssembleDocument(!booleanValueOf(noAssembleDoc)); |
| } |
| Object noPrintHQ = userAgent.getRendererOptions().get(NO_PRINTHQ); |
| if (noPrintHQ != null) { |
| getEncryptionParams().setAllowPrintHq(!booleanValueOf(noPrintHQ)); |
| } |
| String s = (String)userAgent.getRendererOptions().get(PDF_A_MODE); |
| if (s != null) { |
| this.pdfAMode = PDFAMode.valueOf(s); |
| } |
| if (this.pdfAMode.isPDFA1LevelA()) { |
| //Enable accessibility if PDF/A-1a is enabled because it requires tagged PDF. |
| userAgent.getRendererOptions().put(Accessibility.ACCESSIBILITY, Boolean.TRUE); |
| } |
| 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; |
| } |
| Object disableSRGBColorSpace = userAgent.getRendererOptions().get( |
| KEY_DISABLE_SRGB_COLORSPACE); |
| if (disableSRGBColorSpace != null) { |
| this.disableSRGBColorSpace = booleanValueOf(disableSRGBColorSpace); |
| } |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Gets the encryption parameters used by the PDF renderer. |
| * @return encryptionParams the encryption parameters |
| */ |
| PDFEncryptionParams getEncryptionParams() { |
| if (this.encryptionParams == null) { |
| this.encryptionParams = new PDFEncryptionParams(); |
| } |
| return this.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 = ColorProfileUtil.getICC_Profile(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"); |
| } |
| |
| String producer = userAgent.getProducer() != null ? userAgent.getProducer() : ""; |
| |
| if (maxPDFVersion == null) { |
| this.pdfDoc = new PDFDocument(producer); |
| } else { |
| VersionController controller |
| = VersionController.getFixedVersionController(maxPDFVersion); |
| this.pdfDoc = new PDFDocument(producer, controller); |
| } |
| 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(); |
| } |
| |
| this.pdfDoc.enableAccessibility(userAgent.isAccessibilityEnabled()); |
| |
| 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); |
| } |
| |
| /** |
| * Adds an embedded file to the PDF file. |
| * @param embeddedFile the object representing the embedded file to be added |
| * @throws IOException if an I/O error occurs |
| */ |
| public void addEmbeddedFile(PDFEmbeddedFileExtensionAttachment embeddedFile) |
| throws IOException { |
| this.pdfDoc.getProfile().verifyEmbeddedFilesAllowed(); |
| PDFNames names = this.pdfDoc.getRoot().getNames(); |
| if (names == null) { |
| //Add Names if not already present |
| names = this.pdfDoc.getFactory().makeNames(); |
| this.pdfDoc.getRoot().setNames(names); |
| } |
| |
| //Create embedded file |
| PDFEmbeddedFile file = new PDFEmbeddedFile(); |
| this.pdfDoc.registerObject(file); |
| Source src = getUserAgent().resolveURI(embeddedFile.getSrc()); |
| InputStream in = ImageUtil.getInputStream(src); |
| if (in == null) { |
| throw new FileNotFoundException(embeddedFile.getSrc()); |
| } |
| try { |
| OutputStream out = file.getBufferOutputStream(); |
| IOUtils.copyLarge(in, out); |
| } finally { |
| IOUtils.closeQuietly(in); |
| } |
| PDFDictionary dict = new PDFDictionary(); |
| dict.put("F", file); |
| String filename = PDFText.toPDFString(embeddedFile.getFilename(), '_'); |
| PDFFileSpec fileSpec = new PDFFileSpec(filename); |
| fileSpec.setEmbeddedFile(dict); |
| if (embeddedFile.getDesc() != null) { |
| fileSpec.setDescription(embeddedFile.getDesc()); |
| } |
| this.pdfDoc.registerObject(fileSpec); |
| |
| //Make sure there is an EmbeddedFiles in the Names dictionary |
| PDFEmbeddedFiles embeddedFiles = names.getEmbeddedFiles(); |
| if (embeddedFiles == null) { |
| embeddedFiles = new PDFEmbeddedFiles(); |
| this.pdfDoc.assignObjectNumber(embeddedFiles); |
| this.pdfDoc.addTrailerObject(embeddedFiles); |
| names.setEmbeddedFiles(embeddedFiles); |
| } |
| |
| //Add to EmbeddedFiles in the Names dictionary |
| PDFArray nameArray = embeddedFiles.getNames(); |
| if (nameArray == null) { |
| nameArray = new PDFArray(); |
| embeddedFiles.setNames(nameArray); |
| } |
| String name = PDFText.toPDFString(filename); |
| nameArray.add(name); |
| nameArray.add(new PDFReference(fileSpec)); |
| } |
| |
| /** |
| * Sets the PDF version of the output document. See {@link Version} for the format of |
| * <code>version</code>. |
| * @param version the PDF version |
| * @throws IllegalArgumentException if the format of version doesn't conform to that specified |
| * by {@link Version} |
| */ |
| public void setPDFVersion(String version) { |
| maxPDFVersion = Version.getValueOf(version); |
| } |
| } |