| /* |
| * 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.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.EnumMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.TimeZone; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil; |
| import org.apache.xmlgraphics.util.DateFormatUtil; |
| import org.apache.xmlgraphics.xmp.Metadata; |
| import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; |
| 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.apps.io.InternalResourceResolver; |
| import org.apache.fop.fo.extensions.ExtensionAttachment; |
| 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.PDFLayer; |
| import org.apache.fop.pdf.PDFMetadata; |
| import org.apache.fop.pdf.PDFName; |
| import org.apache.fop.pdf.PDFNames; |
| import org.apache.fop.pdf.PDFNavigator; |
| import org.apache.fop.pdf.PDFNull; |
| import org.apache.fop.pdf.PDFNumber; |
| import org.apache.fop.pdf.PDFOutputIntent; |
| import org.apache.fop.pdf.PDFPage; |
| import org.apache.fop.pdf.PDFPageLabels; |
| import org.apache.fop.pdf.PDFReference; |
| import org.apache.fop.pdf.PDFSetOCGStateAction; |
| import org.apache.fop.pdf.PDFTransitionAction; |
| 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.PDFActionExtension; |
| import org.apache.fop.render.pdf.extensions.PDFArrayExtension; |
| import org.apache.fop.render.pdf.extensions.PDFCollectionEntryExtension; |
| import org.apache.fop.render.pdf.extensions.PDFDictionaryAttachment; |
| import org.apache.fop.render.pdf.extensions.PDFDictionaryExtension; |
| import org.apache.fop.render.pdf.extensions.PDFDictionaryType; |
| import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileAttachment; |
| import org.apache.fop.render.pdf.extensions.PDFObjectType; |
| import org.apache.fop.render.pdf.extensions.PDFPageExtension; |
| import org.apache.fop.render.pdf.extensions.PDFReferenceExtension; |
| |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_PARAMS; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ACCESSCONTENT; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ANNOTATIONS; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ASSEMBLEDOC; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_COPY_CONTENT; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_EDIT_CONTENT; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_FILLINFORMS; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINT; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINTHQ; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.OWNER_PASSWORD; |
| import static org.apache.fop.render.pdf.PDFEncryptionOption.USER_PASSWORD; |
| |
| |
| /** |
| * Utility class which enables all sorts of features that are not directly connected to the |
| * normal rendering process. |
| */ |
| class PDFRenderingUtil { |
| |
| /** logging instance */ |
| private static Log log = LogFactory.getLog(PDFRenderingUtil.class); |
| |
| private FOUserAgent userAgent; |
| |
| /** the PDF Document being created */ |
| private PDFDocument pdfDoc; |
| |
| private PDFRendererOptionsConfig rendererConfig; |
| |
| /** the ICC stream used as output profile by this document for PDF/A and PDF/X functionality. */ |
| private PDFICCStream outputProfile; |
| |
| /** the default sRGB color space. */ |
| private PDFICCBasedColorSpace sRGBColorSpace; |
| |
| PDFRenderingUtil(FOUserAgent userAgent) { |
| this.userAgent = userAgent; |
| initialize(); |
| } |
| |
| private void initialize() { |
| rendererConfig = PDFRendererOptionsConfig.DEFAULT.merge(createFromUserAgent(userAgent)); |
| if (rendererConfig.getPDFAMode().isLevelA()) { |
| // PDF/A Level A requires tagged PDF |
| userAgent.getRendererOptions().put(Accessibility.ACCESSIBILITY, Boolean.TRUE); |
| } |
| } |
| |
| protected static PDFRendererOptionsConfig createFromUserAgent(FOUserAgent userAgent) { |
| Map<PDFRendererOption, Object> properties |
| = new EnumMap<PDFRendererOption, Object>(PDFRendererOption.class); |
| for (PDFRendererOption option : PDFRendererOption.values()) { |
| Object value = userAgent.getRendererOption(option); |
| properties.put(option, option.parse(value)); |
| } |
| PDFEncryptionParams encryptionConfig = new EncryptionParamsBuilder().createParams(userAgent); |
| return new PDFRendererOptionsConfig(properties, encryptionConfig); |
| } |
| |
| void mergeRendererOptionsConfig(PDFRendererOptionsConfig config) { |
| rendererConfig = rendererConfig.merge(config); |
| } |
| |
| 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(rendererConfig.getPDFAMode()); |
| pdfDoc.getProfile().setPDFUAMode(rendererConfig.getPDFUAMode()); |
| userAgent.setPdfUAEnabled(pdfDoc.getProfile().getPDFUAMode().isEnabled()); |
| pdfDoc.getProfile().setPDFXMode(rendererConfig.getPDFXMode()); |
| pdfDoc.getProfile().setPDFVTMode(rendererConfig.getPDFVTMode()); |
| } |
| |
| private void addsRGBColorSpace() throws IOException { |
| if (rendererConfig.getDisableSRGBColorSpace()) { |
| if (rendererConfig.getPDFAMode() != PDFAMode.DISABLED |
| || rendererConfig.getPDFXMode() != PDFXMode.DISABLED |
| || rendererConfig.getOutputProfileURI() != 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; |
| URI outputProfileUri = rendererConfig.getOutputProfileURI(); |
| if (outputProfileUri != null) { |
| this.outputProfile = pdfDoc.getFactory().makePDFICCStream(); |
| in = userAgent.getResourceResolver().getResource(rendererConfig.getOutputProfileURI()); |
| 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 |
| List<Class> exclude = new ArrayList<Class>(); |
| if (pdfDoc.getProfile().getPDFAMode().isPart1()) { |
| exclude.add(DublinCoreSchema.class); |
| } |
| fopXMP.mergeInto(docXMP, exclude); |
| 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 void renderDictionaryExtension(PDFDictionaryAttachment attachment, PDFPage currentPage) { |
| PDFDictionaryExtension extension = attachment.getExtension(); |
| PDFDictionaryType type = extension.getDictionaryType(); |
| if (type == PDFDictionaryType.Action) { |
| addNavigatorAction(extension); |
| } else if (type == PDFDictionaryType.Layer) { |
| addLayer(extension); |
| } else if (type == PDFDictionaryType.Navigator) { |
| addNavigator(extension); |
| } else { |
| renderDictionaryExtension(extension, currentPage); |
| } |
| } |
| |
| public void addLayer(PDFDictionaryExtension extension) { |
| assert extension.getDictionaryType() == PDFDictionaryType.Layer; |
| String id = extension.getProperty(PDFDictionaryExtension.PROPERTY_ID); |
| if ((id != null) && (id.length() > 0)) { |
| PDFLayer layer = pdfDoc.getFactory().makeLayer(id); |
| layer.setResolver(new PDFLayer.Resolver(layer, extension) { |
| public void performResolution() { |
| PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); |
| Object name = extension.findEntryValue("Name"); |
| Object intent = extension.findEntryValue("Intent"); |
| Object usage = makeDictionary(extension.findEntryValue("Usage")); |
| getLayer().populate(name, intent, usage); |
| } |
| }); |
| } |
| } |
| |
| public void addNavigatorAction(PDFDictionaryExtension extension) { |
| assert extension.getDictionaryType() == PDFDictionaryType.Action; |
| String id = extension.getProperty(PDFDictionaryExtension.PROPERTY_ID); |
| if ((id != null) && (id.length() > 0)) { |
| String type = extension.getProperty(PDFActionExtension.PROPERTY_TYPE); |
| if (type != null) { |
| if (type.equals("SetOCGState")) { |
| PDFSetOCGStateAction action = pdfDoc.getFactory().makeSetOCGStateAction(id); |
| action.setResolver(new PDFSetOCGStateAction.Resolver(action, extension) { |
| public void performResolution() { |
| PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); |
| Object state = makeArray(extension.findEntryValue("State")); |
| Object preserveRB = extension.findEntryValue("PreserveRB"); |
| Object nextAction = makeDictionaryOrArray(extension.findEntryValue("Next")); |
| getAction().populate(state, preserveRB, nextAction); |
| } |
| }); |
| } else if (type.equals("Trans")) { |
| PDFTransitionAction action = pdfDoc.getFactory().makeTransitionAction(id); |
| action.setResolver(new PDFTransitionAction.Resolver(action, extension) { |
| public void performResolution() { |
| PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); |
| Object transition = makeDictionary(extension.findEntryValue("Trans")); |
| Object nextAction = makeDictionaryOrArray(extension.findEntryValue("Next")); |
| getAction().populate(transition, nextAction); |
| } |
| }); |
| } else { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| } |
| } |
| |
| public void addNavigator(PDFDictionaryExtension extension) { |
| assert extension.getDictionaryType() == PDFDictionaryType.Navigator; |
| String id = extension.getProperty(PDFDictionaryExtension.PROPERTY_ID); |
| if ((id != null) && (id.length() > 0)) { |
| PDFNavigator navigator = pdfDoc.getFactory().makeNavigator(id); |
| navigator.setResolver(new PDFNavigator.Resolver(navigator, extension) { |
| public void performResolution() { |
| PDFDictionaryExtension extension = (PDFDictionaryExtension) getExtension(); |
| Object nextAction = makeDictionary(extension.findEntryValue("NA")); |
| Object next = makeDictionary(extension.findEntryValue("Next")); |
| Object prevAction = makeDictionary(extension.findEntryValue("PA")); |
| Object prev = makeDictionary(extension.findEntryValue("Prev")); |
| Object duration = extension.findEntryValue("Dur"); |
| getNavigator().populate(nextAction, next, prevAction, prev, duration); |
| } |
| }); |
| } |
| } |
| |
| private Object makeArray(Object value) { |
| if (value == null) { |
| return null; |
| } else if (value instanceof PDFReferenceExtension) { |
| return resolveReference((PDFReferenceExtension) value); |
| } else if (value instanceof List<?>) { |
| return populateArray(new PDFArray(), (List<?>) value); |
| } else { |
| throw new IllegalArgumentException(); |
| } |
| } |
| |
| private Object populateArray(PDFArray array, List<?> entries) { |
| for (PDFCollectionEntryExtension entry : (List<PDFCollectionEntryExtension>) entries) { |
| PDFObjectType type = entry.getType(); |
| if (type == PDFObjectType.Array) { |
| array.add(makeArray(entry.getValue())); |
| } else if (type == PDFObjectType.Boolean) { |
| array.add(entry.getValueAsBoolean()); |
| } else if (type == PDFObjectType.Dictionary) { |
| array.add(makeDictionary(entry.getValue())); |
| } else if (type == PDFObjectType.Name) { |
| array.add(new PDFName(entry.getValueAsString())); |
| } else if (type == PDFObjectType.Number) { |
| array.add(new PDFNumber(entry.getValueAsNumber())); |
| } else if (type == PDFObjectType.Reference) { |
| assert (entry instanceof PDFReferenceExtension); |
| array.add(resolveReference((PDFReferenceExtension) entry)); |
| } else if (type == PDFObjectType.String) { |
| array.add(entry.getValue()); |
| } |
| } |
| return array; |
| } |
| |
| private Object makeDictionary(Object value) { |
| if (value == null) { |
| return null; |
| } else if (value instanceof PDFReferenceExtension) { |
| return resolveReference((PDFReferenceExtension) value); |
| } else if (value instanceof List<?>) { |
| return populateDictionary(new PDFDictionary(), (List<?>) value); |
| } else { |
| throw new IllegalArgumentException(); |
| } |
| } |
| |
| private Object populateDictionary(PDFDictionary dictionary, List<?> entries) { |
| for (PDFCollectionEntryExtension entry : (List<PDFCollectionEntryExtension>) entries) { |
| PDFObjectType type = entry.getType(); |
| String key = entry.getKey(); |
| if (type == PDFObjectType.Array) { |
| dictionary.put(key, makeArray(entry.getValue())); |
| } else if (type == PDFObjectType.Boolean) { |
| dictionary.put(key, entry.getValueAsBoolean()); |
| } else if (type == PDFObjectType.Dictionary) { |
| dictionary.put(key, makeDictionary(entry.getValue())); |
| } else if (type == PDFObjectType.Name) { |
| dictionary.put(key, new PDFName(entry.getValueAsString())); |
| } else if (type == PDFObjectType.Number) { |
| dictionary.put(key, new PDFNumber(entry.getValueAsNumber())); |
| } else if (type == PDFObjectType.Reference) { |
| assert (entry instanceof PDFReferenceExtension); |
| dictionary.put(key, resolveReference((PDFReferenceExtension) entry)); |
| } else if (type == PDFObjectType.String) { |
| dictionary.put(key, entry.getValue()); |
| } |
| } |
| return dictionary; |
| } |
| |
| private Object makeDictionaryOrArray(Object value) { |
| if (value == null) { |
| return null; |
| } else if (value instanceof PDFReferenceExtension) { |
| return resolveReference((PDFReferenceExtension) value); |
| } else if (value instanceof List<?>) { |
| if (hasKeyedEntry((List<?>) value)) { |
| return populateDictionary(new PDFDictionary(), (List<?>) value); |
| } else { |
| return populateArray(new PDFArray(), (List<?>) value); |
| } |
| } else { |
| throw new IllegalArgumentException(); |
| } |
| } |
| |
| private boolean hasKeyedEntry(List<?> entries) { |
| for (PDFCollectionEntryExtension entry : (List<PDFCollectionEntryExtension>) entries) { |
| if (entry.getKey() != null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public void renderDictionaryExtension(PDFDictionaryExtension extension, PDFPage currentPage) { |
| PDFDictionaryType type = extension.getDictionaryType(); |
| if (type == PDFDictionaryType.Catalog) { |
| augmentDictionary(pdfDoc.getRoot(), extension); |
| } else if (type == PDFDictionaryType.Page) { |
| assert extension instanceof PDFPageExtension; |
| if (((PDFPageExtension) extension).matchesPageNumber(currentPage.getPageIndex() + 1)) { |
| augmentDictionary(currentPage, extension); |
| renderExtension(currentPage, extension.getExtension()); |
| } |
| } else if (type == PDFDictionaryType.Info) { |
| PDFInfo info = pdfDoc.getInfo(); |
| for (PDFCollectionEntryExtension entry : extension.getEntries()) { |
| info.put(entry.getKey(), entry.getValueAsString()); |
| } |
| } else if (type == PDFDictionaryType.VT) { |
| if (currentPage.get("DPart") != null) { |
| augmentDictionary((PDFDictionary)currentPage.get("DPart"), extension); |
| } |
| } else if (type == PDFDictionaryType.PagePiece) { |
| String date = DateFormatUtil.formatPDFDate(new Date(), TimeZone.getDefault()); |
| if (currentPage.get("PieceInfo") == null) { |
| currentPage.put("PieceInfo", new PDFDictionary()); |
| currentPage.put("LastModified", date); |
| } |
| PDFDictionary d = augmentDictionary((PDFDictionary)currentPage.get("PieceInfo"), extension); |
| d.put("LastModified", date); |
| } else { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| private void renderExtension(PDFPage currentPage, ExtensionAttachment extension) { |
| if (extension instanceof XMPMetadata) { |
| XMPMetadata metadata = (XMPMetadata) extension; |
| Metadata docXMP = metadata.getMetadata(); |
| PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(docXMP, metadata.isReadOnly()); |
| currentPage.setMetadata(pdfMetadata); |
| } |
| } |
| |
| private PDFDictionary augmentDictionary(PDFDictionary dictionary, PDFDictionaryExtension extension) { |
| for (PDFCollectionEntryExtension entry : extension.getEntries()) { |
| if (entry instanceof PDFDictionaryExtension) { |
| String[] keys = entry.getKey().split("/"); |
| for (int i = 0; i < keys.length; i++) { |
| if (keys[i].isEmpty()) { |
| throw new IllegalStateException("pdf:dictionary key: " + entry.getKey() + " not valid"); |
| } |
| if (i == keys.length - 1) { |
| dictionary.put(keys[i], |
| augmentDictionary(new PDFDictionary(dictionary), (PDFDictionaryExtension) entry)); |
| } else { |
| PDFDictionary d = new PDFDictionary(); |
| dictionary.put(keys[i], d); |
| dictionary = d; |
| } |
| } |
| } else if (entry instanceof PDFArrayExtension) { |
| dictionary.put(entry.getKey(), augmentArray(new PDFArray(dictionary), (PDFArrayExtension) entry)); |
| } else { |
| augmentDictionary(dictionary, entry); |
| } |
| } |
| return dictionary; |
| } |
| |
| private void augmentDictionary(PDFDictionary dictionary, PDFCollectionEntryExtension entry) { |
| PDFObjectType type = entry.getType(); |
| String key = entry.getKey(); |
| if (type == PDFObjectType.Boolean) { |
| dictionary.put(key, entry.getValueAsBoolean()); |
| } else if (type == PDFObjectType.Name) { |
| dictionary.put(key, new PDFName(entry.getValueAsString())); |
| } else if (type == PDFObjectType.Number) { |
| dictionary.put(key, new PDFNumber(entry.getValueAsNumber())); |
| } else if (type == PDFObjectType.Reference) { |
| assert entry instanceof PDFReferenceExtension; |
| dictionary.put(key, resolveReference((PDFReferenceExtension) entry)); |
| } else if (type == PDFObjectType.String) { |
| dictionary.put(key, entry.getValueAsString()); |
| } else { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| private Object resolveReference(PDFReferenceExtension entry) { |
| PDFReference reference = (PDFReference) entry.getResolvedReference(); |
| if (reference == null) { |
| reference = pdfDoc.resolveExtensionReference(entry.getReferenceId()); |
| if (reference != null) { |
| entry.setResolvedReference(reference); |
| } |
| return reference; |
| } |
| return PDFNull.INSTANCE; |
| } |
| |
| private PDFArray augmentArray(PDFArray array, PDFArrayExtension extension) { |
| for (PDFCollectionEntryExtension entry : extension.getEntries()) { |
| if (entry instanceof PDFDictionaryExtension) { |
| array.add(augmentDictionary(new PDFDictionary(array), (PDFDictionaryExtension) entry)); |
| } else if (entry instanceof PDFArrayExtension) { |
| array.add(augmentArray(new PDFArray(array), (PDFArrayExtension) entry)); |
| } else { |
| augmentArray(array, entry); |
| } |
| } |
| return array; |
| } |
| |
| private void augmentArray(PDFArray array, PDFCollectionEntryExtension entry) { |
| PDFObjectType type = entry.getType(); |
| if (type == PDFObjectType.Boolean) { |
| array.add(entry.getValueAsBoolean()); |
| } else if (type == PDFObjectType.Name) { |
| array.add(new PDFName(entry.getValueAsString())); |
| } else if (type == PDFObjectType.Number) { |
| array.add(new PDFNumber(entry.getValueAsNumber())); |
| } else if (type == PDFObjectType.Reference) { |
| assert entry instanceof PDFReferenceExtension; |
| array.add(resolveReference((PDFReferenceExtension) entry)); |
| } else if (type == PDFObjectType.String) { |
| array.add(entry.getValueAsString()); |
| } else { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| 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() : ""; |
| final Version maxPDFVersion = rendererConfig.getPDFVersion(); |
| if (maxPDFVersion == null) { |
| this.pdfDoc = new PDFDocument(producer); |
| } else { |
| VersionController controller |
| = VersionController.getFixedVersionController(maxPDFVersion); |
| this.pdfDoc = new PDFDocument(producer, controller); |
| } |
| updateInfo(); |
| updatePDFProfiles(); |
| pdfDoc.setFilterMap(rendererConfig.getFilterMap()); |
| pdfDoc.outputHeader(out); |
| |
| //Setup encryption if necessary |
| PDFEncryptionManager.setupPDFEncryption(rendererConfig.getEncryptionParameters(), pdfDoc); |
| |
| addsRGBColorSpace(); |
| if (rendererConfig.getOutputProfileURI() != null) { |
| addDefaultOutputProfile(); |
| } |
| PDFXMode pdfXMode = rendererConfig.getPDFXMode(); |
| if (pdfXMode != PDFXMode.DISABLED) { |
| log.debug(pdfXMode + " is active."); |
| log.warn("Note: " + pdfXMode |
| + " support is work-in-progress and not fully implemented, yet!"); |
| addPDFXOutputIntent(); |
| } |
| PDFAMode pdfAMode = rendererConfig.getPDFAMode(); |
| if (pdfAMode.isEnabled()) { |
| log.debug("PDF/A is active. Conformance Level: " + pdfAMode); |
| addPDFA1OutputIntent(); |
| } |
| |
| this.pdfDoc.enableAccessibility(userAgent.isAccessibilityEnabled()); |
| pdfDoc.setMergeFontsEnabled(rendererConfig.getMergeFontsEnabled()); |
| pdfDoc.setMergeFormFieldsEnabled(rendererConfig.getMergeFormFieldsEnabled()); |
| pdfDoc.setLinearizationEnabled(rendererConfig.getLinearizationEnabled()); |
| pdfDoc.setFormXObjectEnabled(rendererConfig.getFormXObjectEnabled()); |
| |
| 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); |
| } |
| pageLabels.addPageLabel(pageIndex, pageNumber); |
| } |
| |
| /** |
| * 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(PDFEmbeddedFileAttachment 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); |
| URI srcURI; |
| try { |
| srcURI = InternalResourceResolver.cleanURI(embeddedFile.getSrc()); |
| } catch (URISyntaxException use) { |
| throw new RuntimeException(use); |
| } |
| InputStream in = userAgent.getResourceResolver().getResource(srcURI); |
| 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); |
| PDFFileSpec fileSpec = new PDFFileSpec(embeddedFile.getFilename(), embeddedFile.getUnicodeFilename()); |
| String filename = fileSpec.getFilename(); |
| pdfDoc.getRoot().addAF(fileSpec); |
| 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); |
| } |
| nameArray.add(filename); |
| nameArray.add(new PDFReference(fileSpec)); |
| } |
| |
| private static final class EncryptionParamsBuilder { |
| private PDFEncryptionParams params; |
| |
| private EncryptionParamsBuilder() { |
| } |
| |
| private PDFEncryptionParams createParams(FOUserAgent userAgent) { |
| params = (PDFEncryptionParams) userAgent.getRendererOptions().get(ENCRYPTION_PARAMS); |
| String userPassword = (String) userAgent.getRendererOption(USER_PASSWORD); |
| if (userPassword != null) { |
| getEncryptionParams().setUserPassword(userPassword); |
| } |
| String ownerPassword = (String) userAgent.getRendererOption(OWNER_PASSWORD); |
| if (ownerPassword != null) { |
| getEncryptionParams().setOwnerPassword(ownerPassword); |
| } |
| Object noPrint = userAgent.getRendererOption(NO_PRINT); |
| if (noPrint != null) { |
| getEncryptionParams().setAllowPrint(!booleanValueOf(noPrint)); |
| } |
| Object noCopyContent = userAgent.getRendererOption(NO_COPY_CONTENT); |
| if (noCopyContent != null) { |
| getEncryptionParams().setAllowCopyContent(!booleanValueOf(noCopyContent)); |
| } |
| Object noEditContent = userAgent.getRendererOption(NO_EDIT_CONTENT); |
| if (noEditContent != null) { |
| getEncryptionParams().setAllowEditContent(!booleanValueOf(noEditContent)); |
| } |
| Object noAnnotations = userAgent.getRendererOption(NO_ANNOTATIONS); |
| if (noAnnotations != null) { |
| getEncryptionParams().setAllowEditAnnotations(!booleanValueOf(noAnnotations)); |
| } |
| Object noFillInForms = userAgent.getRendererOption(NO_FILLINFORMS); |
| if (noFillInForms != null) { |
| getEncryptionParams().setAllowFillInForms(!booleanValueOf(noFillInForms)); |
| } |
| Object noAccessContent = userAgent.getRendererOption(NO_ACCESSCONTENT); |
| if (noAccessContent != null) { |
| getEncryptionParams().setAllowAccessContent(!booleanValueOf(noAccessContent)); |
| } |
| Object noAssembleDoc = userAgent.getRendererOption(NO_ASSEMBLEDOC); |
| if (noAssembleDoc != null) { |
| getEncryptionParams().setAllowAssembleDocument(!booleanValueOf(noAssembleDoc)); |
| } |
| Object noPrintHQ = userAgent.getRendererOption(NO_PRINTHQ); |
| if (noPrintHQ != null) { |
| getEncryptionParams().setAllowPrintHq(!booleanValueOf(noPrintHQ)); |
| } |
| return params; |
| } |
| |
| private PDFEncryptionParams getEncryptionParams() { |
| if (params == null) { |
| params = new PDFEncryptionParams(); |
| } |
| return params; |
| } |
| |
| private static boolean booleanValueOf(Object obj) { |
| if (obj instanceof Boolean) { |
| return (Boolean) obj; |
| } else if (obj instanceof String) { |
| return Boolean.valueOf((String) obj); |
| } else { |
| throw new IllegalArgumentException("Boolean or \"true\" or \"false\" expected."); |
| } |
| } |
| } |
| } |