001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Thibaud Arguillere
018 *     Michael Vachette
019 */
020package org.nuxeo.ecm.platform.pdf.service;
021
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025import org.apache.pdfbox.Overlay;
026import org.apache.pdfbox.exceptions.COSVisitorException;
027import org.apache.pdfbox.pdmodel.PDDocument;
028import org.apache.pdfbox.pdmodel.PDPage;
029import org.apache.pdfbox.pdmodel.PDResources;
030import org.apache.pdfbox.pdmodel.common.PDRectangle;
031import org.apache.pdfbox.pdmodel.edit.PDPageContentStream;
032import org.apache.pdfbox.pdmodel.font.PDFont;
033import org.apache.pdfbox.pdmodel.font.PDType1Font;
034import org.apache.pdfbox.pdmodel.graphics.PDExtendedGraphicsState;
035import org.apache.pdfbox.pdmodel.graphics.xobject.PDPixelMap;
036import org.apache.pdfbox.pdmodel.graphics.xobject.PDXObjectImage;
037import org.nuxeo.ecm.core.api.Blob;
038import org.nuxeo.ecm.core.api.Blobs;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.platform.pdf.PDFUtils;
041import org.nuxeo.ecm.platform.pdf.service.watermark.WatermarkProperties;
042import org.nuxeo.runtime.model.DefaultComponent;
043
044import javax.imageio.ImageIO;
045import java.awt.*;
046import java.awt.geom.AffineTransform;
047import java.awt.geom.Point2D;
048import java.awt.geom.Rectangle2D;
049import java.awt.image.BufferedImage;
050import java.io.IOException;
051import java.util.HashMap;
052import java.util.List;
053import java.util.Map;
054
055/**
056 * @since 8.10
057 */
058public class PDFTransformationServiceImpl extends DefaultComponent
059        implements PDFTransformationService {
060
061    protected static final Log log = LogFactory.getLog(PDFTransformationServiceImpl.class);
062
063    protected static final String MIME_TYPE = "application/pdf";
064
065    @Override
066    public WatermarkProperties getDefaultProperties() {
067        return new WatermarkProperties();
068    }
069
070    @Override
071    public Blob applyTextWatermark(Blob input, String text, WatermarkProperties properties) {
072
073        // Set up the graphic state to handle transparency
074        // Define a new extended graphic state
075        PDExtendedGraphicsState extendedGraphicsState = new PDExtendedGraphicsState();
076        // Set the transparency/opacity
077        extendedGraphicsState.setNonStrokingAlphaConstant((float) properties.getAlphaColor());
078
079        try (PDDocument pdfDoc = PDDocument.load(input.getStream())) {
080
081            PDFont font = PDType1Font.getStandardFont(properties.getFontFamily());
082            float watermarkWidth = (float) (font.getStringWidth(text) * properties.getFontSize()
083                                / 1000f);
084            int[] rgb = PDFUtils.hex255ToRGB(properties.getHex255Color());
085
086            for (PDPage page : (List<PDPage>)pdfDoc.getDocumentCatalog().getAllPages()) {
087                PDRectangle pageSize = page.findMediaBox();
088                PDResources resources = page.findResources();
089
090                // Get the defined graphic states.
091                Map<String, PDExtendedGraphicsState> graphicsStates = resources.getGraphicsStates();
092                if (graphicsStates == null) {
093                    graphicsStates = new HashMap<>();
094                }
095                graphicsStates.put("TransparentState", extendedGraphicsState);
096                resources.setGraphicsStates(graphicsStates);
097
098                try (PDPageContentStream contentStream =
099                             new PDPageContentStream(pdfDoc, page, true, true, true)) {
100                    contentStream.beginText();
101                    contentStream.setFont(font, (float) properties.getFontSize());
102                    contentStream.appendRawCommands("/TransparentState gs\n");
103                    contentStream.setNonStrokingColor(rgb[0], rgb[1], rgb[2]);
104                    Point2D position = computeTranslationVector(
105                            pageSize.getWidth(),watermarkWidth,
106                            pageSize.getHeight(),properties.getFontSize(),properties);
107                    contentStream.setTextRotation(
108                            Math.toRadians(properties.getRotation()),
109                            position.getX(),
110                            position.getY());
111                    contentStream.drawString(text);
112                    contentStream.endText();
113                }
114            }
115            return saveInTempFile(pdfDoc);
116
117        } catch (IOException | COSVisitorException e) {
118            throw new NuxeoException(e);
119        }
120    }
121
122    @Override
123    public Blob applyImageWatermark(Blob input, Blob watermark, WatermarkProperties properties) {
124
125        // Set up the graphic state to handle transparency
126        // Define a new extended graphic state
127        PDExtendedGraphicsState extendedGraphicsState = new PDExtendedGraphicsState();
128        // Set the transparency/opacity
129        extendedGraphicsState.setNonStrokingAlphaConstant((float) properties.getAlphaColor());
130
131        try (PDDocument pdfDoc = PDDocument.load(input.getStream())){
132            BufferedImage image = ImageIO.read(watermark.getStream());
133            PDXObjectImage ximage = new PDPixelMap(pdfDoc, image);
134
135            for (PDPage page : (List<PDPage>)pdfDoc.getDocumentCatalog().getAllPages()) {
136                PDRectangle pageSize = page.findMediaBox();
137                PDResources resources = page.findResources();
138
139                // Get the defined graphic states.
140                // Get the defined graphic states.
141                Map<String, PDExtendedGraphicsState> graphicsStates = resources.getGraphicsStates();
142                if (graphicsStates == null) {
143                    graphicsStates = new HashMap<>();
144                }
145                graphicsStates.put("TransparentState", extendedGraphicsState);
146                resources.setGraphicsStates(graphicsStates);
147
148                try (PDPageContentStream contentStream = new PDPageContentStream(pdfDoc, page, true, true)) {
149                    contentStream.appendRawCommands("/TransparentState gs\n");
150                    contentStream.endMarkedContentSequence();
151
152                    double watermarkWidth = ximage.getWidth()*properties.getScale();
153                    double watermarkHeight = ximage.getHeight()*properties.getScale();
154
155                    Point2D position = computeTranslationVector(
156                            pageSize.getWidth(),watermarkWidth,
157                            pageSize.getHeight(),watermarkHeight,properties);
158
159                    contentStream.drawXObject(
160                            ximage,
161                            (float)position.getX(),
162                            (float)position.getY(),
163                            (float)watermarkWidth,
164                            (float)watermarkHeight);
165                }
166            }
167            return saveInTempFile(pdfDoc);
168        } catch (COSVisitorException | IOException e) {
169            throw new NuxeoException(e);
170        }
171    }
172
173    @Override
174    public Blob overlayPDF(Blob input, Blob overlayBlob) {
175        try (PDDocument pdfDoc = PDDocument.load(input.getStream());
176             PDDocument pdfOverlayDoc = PDDocument.load(overlayBlob.getStream())) {
177            Overlay overlay = new Overlay();
178            overlay.overlay(pdfOverlayDoc, pdfDoc);
179            return saveInTempFile(pdfDoc);
180        } catch (IOException | COSVisitorException e) {
181            throw new NuxeoException(e);
182        }
183    }
184
185    public  Point2D computeTranslationVector(double pageWidth, double watermarkWidth,
186                                               double pageHeight, double watermarkHeight,
187                                               WatermarkProperties properties) {
188        double xRotationOffset = 0;
189        double yRotationOffset = 0;
190
191        if (properties.getRotation() != 0) {
192            Rectangle2D rectangle2D =
193                    new Rectangle2D.Double(
194                            0, -watermarkHeight, watermarkWidth, watermarkHeight);
195            AffineTransform at = AffineTransform.getRotateInstance(
196                    -Math.toRadians(properties.getRotation()), 0, 0);
197            Shape shape = at.createTransformedShape(rectangle2D);
198            Rectangle2D rotated = shape.getBounds2D();
199
200            watermarkWidth = rotated.getWidth();
201            if (!properties.isInvertX() || properties.isRelativeCoordinates()) {
202                xRotationOffset = -rotated.getX();
203            } else {
204                xRotationOffset = rotated.getX();
205            }
206
207            watermarkHeight = rotated.getHeight();
208            if (!properties.isInvertY() || properties.isRelativeCoordinates()) {
209                yRotationOffset = rotated.getY()+rotated.getHeight();
210            } else {
211                yRotationOffset = -(rotated.getY()+rotated.getHeight());
212            }
213        }
214
215        double xTranslation;
216        double yTranslation;
217
218        if (properties.isRelativeCoordinates()) {
219            xTranslation = (pageWidth - watermarkWidth ) * properties.getxPosition() + xRotationOffset;
220            yTranslation = (pageHeight - watermarkHeight ) * properties.getyPosition() + yRotationOffset;
221        } else {
222            xTranslation = properties.getxPosition() + xRotationOffset;
223            yTranslation = properties.getyPosition() + yRotationOffset;
224            if (properties.isInvertX()) {
225                xTranslation = pageWidth - watermarkWidth - xTranslation;
226            }
227            if (properties.isInvertY()) {
228                yTranslation = pageHeight - watermarkHeight - yTranslation;
229            }
230        }
231        return new Point2D.Double(xTranslation, yTranslation);
232    }
233
234    protected Blob saveInTempFile(PDDocument PdfDoc) throws IOException, COSVisitorException {
235        Blob blob = Blobs.createBlobWithExtension(".pdf"); // creates a tracked temporary file
236        blob.setMimeType(MIME_TYPE);
237        PdfDoc.save(blob.getFile());
238        return blob;
239    }
240
241}