/* * 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. */ package org.apache.pdfbox.util; import java.awt.geom.AffineTransform; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Map; import java.util.Set; import org.apache.fontbox.util.BoundingBox; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectForm; import org.apache.pdfbox.pdmodel.markedcontent.PDPropertyList; /** * This class allows to import pages as Form XObjects into a PDF file and use them to create * layers (optional content groups). * * @version $Revision$ */ public class LayerUtility { private static final boolean DEBUG = true; private PDDocument targetDoc; private PDFCloneUtility cloner; /** * Creates a new instance. * @param document the PDF document to modify */ public LayerUtility(PDDocument document) { this.targetDoc = document; this.cloner = new PDFCloneUtility(document); } /** * Returns the PDF document we work on. * @return the PDF document */ public PDDocument getDocument() { return this.targetDoc; } /** * Some applications may not wrap their page content in a save/restore (q/Q) pair which can * lead to problems with coordinate system transformations when content is appended. This * method lets you add a q/Q pair around the existing page's content. * @param page the page * @throws IOException if an I/O error occurs */ public void wrapInSaveRestore(PDPage page) throws IOException { COSDictionary saveGraphicsStateDic = new COSDictionary(); COSStream saveGraphicsStateStream = new COSStream(saveGraphicsStateDic, getDocument().getDocument().getScratchFile()); OutputStream saveStream = saveGraphicsStateStream.createUnfilteredStream(); saveStream.write("q\n".getBytes("ISO-8859-1")); saveStream.flush(); COSStream restoreGraphicsStateStream = new COSStream(saveGraphicsStateDic, getDocument().getDocument().getScratchFile()); OutputStream restoreStream = restoreGraphicsStateStream.createUnfilteredStream(); restoreStream.write("Q\n".getBytes("ISO-8859-1")); restoreStream.flush(); //Wrap the existing page's content in a save/restore pair (q/Q) to have a controlled //environment to add additional content. COSDictionary pageDictionary = page.getCOSDictionary(); COSBase contents = pageDictionary.getDictionaryObject(COSName.CONTENTS); if (contents instanceof COSStream) { COSStream contentsStream = (COSStream)contents; COSArray array = new COSArray(); array.add(saveGraphicsStateStream); array.add(contentsStream); array.add(restoreGraphicsStateStream); pageDictionary.setItem(COSName.CONTENTS, array); } else if( contents instanceof COSArray ) { COSArray contentsArray = (COSArray)contents; contentsArray.add(0, saveGraphicsStateStream); contentsArray.add(restoreGraphicsStateStream); } else { throw new IOException("Contents are unknown type: " + contents.getClass().getName()); } } /** * Imports a page from some PDF file as a Form XObject so it can be placed on another page * in the target document. * @param sourceDoc the source PDF document that contains the page to be copied * @param pageNumber the page number of the page to be copied * @return a Form XObject containing the original page's content * @throws IOException if an I/O error occurs */ public PDXObjectForm importPageAsForm(PDDocument sourceDoc, int pageNumber) throws IOException { PDPage page = (PDPage)sourceDoc.getDocumentCatalog().getAllPages().get(pageNumber); return importPageAsForm(sourceDoc, page); } private static final Set<String> PAGE_TO_FORM_FILTER = new java.util.HashSet<String>( Arrays.asList(new String[] {"Group", "LastModified", "Metadata"})); /** * Imports a page from some PDF file as a Form XObject so it can be placed on another page * in the target document. * @param sourceDoc the source PDF document that contains the page to be copied * @param page the page in the source PDF document to be copied * @return a Form XObject containing the original page's content * @throws IOException if an I/O error occurs */ public PDXObjectForm importPageAsForm(PDDocument sourceDoc, PDPage page) throws IOException { COSStream pageStream = (COSStream)page.getContents().getCOSObject(); PDStream newStream = new PDStream(targetDoc, pageStream.getUnfilteredStream(), false); PDXObjectForm form = new PDXObjectForm(newStream); //Copy resources PDResources pageRes = page.findResources(); PDResources formRes = new PDResources(); cloner.cloneMerge(pageRes, formRes); form.setResources(formRes); //Transfer some values from page to form transferDict(page.getCOSDictionary(), form.getCOSStream(), PAGE_TO_FORM_FILTER, true); Matrix matrix = form.getMatrix(); AffineTransform at = matrix != null ? matrix.createAffineTransform() : new AffineTransform(); PDRectangle mediaBox = page.findMediaBox(); PDRectangle cropBox = page.findCropBox(); PDRectangle viewBox = (cropBox != null ? cropBox : mediaBox); //Handle the /Rotation entry on the page dict int rotation = getNormalizedRotation(page); //Transform to FOP's user space //at.scale(1 / viewBox.getWidth(), 1 / viewBox.getHeight()); at.translate(mediaBox.getLowerLeftX() - viewBox.getLowerLeftX(), mediaBox.getLowerLeftY() - viewBox.getLowerLeftY()); switch (rotation) { case 90: at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth()); at.translate(0, viewBox.getWidth()); at.rotate(-Math.PI / 2.0); break; case 180: at.translate(viewBox.getWidth(), viewBox.getHeight()); at.rotate(-Math.PI); break; case 270: at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth()); at.translate(viewBox.getHeight(), 0); at.rotate(-Math.PI * 1.5); default: //no additional transformations necessary } //Compensate for Crop Boxes not starting at 0,0 at.translate(-viewBox.getLowerLeftX(), -viewBox.getLowerLeftY()); if (!at.isIdentity()) { form.setMatrix(at); } BoundingBox bbox = new BoundingBox(); bbox.setLowerLeftX(viewBox.getLowerLeftX()); bbox.setLowerLeftY(viewBox.getLowerLeftY()); bbox.setUpperRightX(viewBox.getUpperRightX()); bbox.setUpperRightY(viewBox.getUpperRightY()); form.setBBox(new PDRectangle(bbox)); return form; } /** * Places the given form over the existing content of the indicated page (like an overlay). * The form is enveloped in a marked content section to indicate that it's part of an * optional content group (OCG), here used as a layer. This optional group is returned and * can be enabled and disabled through methods on {@link PDOptionalContentProperties}. * @param targetPage the target page * @param form the form to place * @param transform the transformation matrix that controls the placement * @param layerName the name for the layer/OCG to produce * @return the optional content group that was generated for the form usage * @throws IOException if an I/O error occurs */ public PDOptionalContentGroup appendFormAsLayer(PDPage targetPage, PDXObjectForm form, AffineTransform transform, String layerName) throws IOException { PDDocumentCatalog catalog = targetDoc.getDocumentCatalog(); PDOptionalContentProperties ocprops = catalog.getOCProperties(); if (ocprops == null) { ocprops = new PDOptionalContentProperties(); catalog.setOCProperties(ocprops); } if (ocprops.hasGroup(layerName)) { throw new IllegalArgumentException("Optional group (layer) already exists: " + layerName); } PDOptionalContentGroup layer = new PDOptionalContentGroup(layerName); ocprops.addGroup(layer); PDResources resources = targetPage.findResources(); PDPropertyList props = resources.getProperties(); if (props == null) { props = new PDPropertyList(); resources.setProperties(props); } //Find first free resource name with the pattern "MC<index>" int index = 0; PDOptionalContentGroup ocg; COSName resourceName; do { resourceName = COSName.getPDFName("MC" + index); ocg = props.getOptionalContentGroup(resourceName); index++; } while (ocg != null); //Put mapping for our new layer/OCG props.putMapping(resourceName, layer); PDPageContentStream contentStream = new PDPageContentStream( targetDoc, targetPage, true, !DEBUG); contentStream.beginMarkedContentSequence(COSName.OC, resourceName); contentStream.drawXObject(form, transform); contentStream.endMarkedContentSequence(); contentStream.close(); return layer; } private void transferDict(COSDictionary orgDict, COSDictionary targetDict, Set<String> filter, boolean inclusive) throws IOException { for (Map.Entry<COSName, COSBase> entry : orgDict.entrySet()) { COSName key = entry.getKey(); if (inclusive && !filter.contains(key.getName())) { continue; } else if (!inclusive && filter.contains(key.getName())) { continue; } targetDict.setItem(key, cloner.cloneForNewDocument(entry.getValue())); } } private static int getNormalizedRotation(PDPage page) { //Handle the /Rotation entry on the page dict int rotation = page.findRotation(); while (rotation >= 360) { rotation -= 360; } if (rotation < 0) { rotation = 0; } switch (rotation) { case 90: case 180: case 270: return rotation; default: return 0; } } }