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.cos.COSName;
026import org.apache.pdfbox.multipdf.Overlay;
027import org.apache.pdfbox.pdmodel.PDDocument;
028import org.apache.pdfbox.pdmodel.PDPage;
029import org.apache.pdfbox.pdmodel.PDPageContentStream;
030import org.apache.pdfbox.pdmodel.PDResources;
031import org.apache.pdfbox.pdmodel.common.PDRectangle;
032import org.apache.pdfbox.pdmodel.font.PDFont;
033import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
034import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
035import org.nuxeo.ecm.core.api.Blob;
036import org.nuxeo.ecm.core.api.Blobs;
037import org.nuxeo.ecm.core.api.CloseableFile;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.platform.pdf.PDFUtils;
040import org.nuxeo.ecm.platform.pdf.service.watermark.WatermarkProperties;
041import org.nuxeo.runtime.model.DefaultComponent;
042
043import java.awt.*;
044import java.awt.geom.AffineTransform;
045import java.awt.geom.Point2D;
046import java.awt.geom.Rectangle2D;
047import java.io.IOException;
048import java.util.Map;
049
050/**
051 * @since 8.10
052 */
053public class PDFTransformationServiceImpl extends DefaultComponent
054        implements PDFTransformationService {
055
056    protected static final Log log = LogFactory.getLog(PDFTransformationServiceImpl.class);
057
058    protected static final String MIME_TYPE = "application/pdf";
059
060    @Override
061    public WatermarkProperties getDefaultProperties() {
062        return new WatermarkProperties();
063    }
064
065    @Override
066    public Blob applyTextWatermark(Blob input, String text, WatermarkProperties properties) {
067
068        // Set up the graphic state to handle transparency
069        // Define a new extended graphic state
070        PDExtendedGraphicsState extendedGraphicsState = new PDExtendedGraphicsState();
071        // Set the transparency/opacity
072        extendedGraphicsState.setNonStrokingAlphaConstant((float) properties.getAlphaColor());
073        COSName transparentStateName = COSName.getPDFName("TransparentState");
074
075        try (PDDocument pdfDoc = PDDocument.load(input.getStream())) {
076
077            PDFont font = PDFUtils.getStandardType1Font(properties.getFontFamily());
078            float watermarkWidth = (float) (font.getStringWidth(text) * properties.getFontSize()
079                                / 1000f);
080            int[] rgb = PDFUtils.hex255ToRGB(properties.getHex255Color());
081
082            for (PDPage page : pdfDoc.getDocumentCatalog().getPages()) {
083                PDRectangle pageSize = page.getMediaBox();
084                PDResources resources = page.getResources();
085                resources.put(transparentStateName, extendedGraphicsState);
086
087                try (PDPageContentStream contentStream =
088                             new PDPageContentStream(pdfDoc, page, true, true, true)) {
089                    contentStream.beginText();
090                    contentStream.setFont(font, (float) properties.getFontSize());
091                    contentStream.appendRawCommands("/TransparentState gs\n");
092                    contentStream.setNonStrokingColor(rgb[0], rgb[1], rgb[2]);
093                    Point2D position = computeTranslationVector(
094                            pageSize.getWidth(),watermarkWidth,
095                            pageSize.getHeight(),properties.getFontSize(),properties);
096                    contentStream.setTextRotation(
097                            Math.toRadians(properties.getRotation()),
098                            position.getX(),
099                            position.getY());
100                    contentStream.drawString(text);
101                    contentStream.endText();
102                }
103            }
104            return saveInTempFile(pdfDoc);
105
106        } catch (IOException e) {
107            throw new NuxeoException(e);
108        }
109    }
110
111    @Override
112    public Blob applyImageWatermark(Blob input, Blob watermark, WatermarkProperties properties) {
113
114        // Set up the graphic state to handle transparency
115        // Define a new extended graphic state
116        PDExtendedGraphicsState extendedGraphicsState = new PDExtendedGraphicsState();
117        // Set the transparency/opacity
118        extendedGraphicsState.setNonStrokingAlphaConstant((float) properties.getAlphaColor());
119        COSName transparentStateName = COSName.getPDFName("TransparentState");
120
121        try (PDDocument pdfDoc = PDDocument.load(input.getStream()); //
122                CloseableFile image = watermark.getCloseableFile()) {
123            PDImageXObject ximage = PDImageXObject.createFromFileByContent(image.getFile(), pdfDoc);
124
125            for (PDPage page : pdfDoc.getDocumentCatalog().getPages()) {
126                PDRectangle pageSize = page.getMediaBox();
127                PDResources resources = page.getResources();
128                resources.put(transparentStateName, extendedGraphicsState);
129
130                try (PDPageContentStream contentStream = new PDPageContentStream(pdfDoc, page, true, true)) {
131                    contentStream.appendRawCommands("/TransparentState gs\n");
132                    contentStream.endMarkedContentSequence();
133
134                    double watermarkWidth = ximage.getWidth()*properties.getScale();
135                    double watermarkHeight = ximage.getHeight()*properties.getScale();
136
137                    Point2D position = computeTranslationVector(
138                            pageSize.getWidth(),watermarkWidth,
139                            pageSize.getHeight(),watermarkHeight,properties);
140
141                    contentStream.drawImage(
142                            ximage,
143                            (float)position.getX(),
144                            (float)position.getY(),
145                            (float)watermarkWidth,
146                            (float)watermarkHeight);
147                }
148            }
149            return saveInTempFile(pdfDoc);
150        } catch (IOException e) {
151            throw new NuxeoException(e);
152        }
153    }
154
155    @Override
156    public Blob overlayPDF(Blob input, Blob overlayBlob) {
157        try (PDDocument pdfDoc = PDDocument.load(input.getStream());
158             PDDocument pdfOverlayDoc = PDDocument.load(overlayBlob.getStream())) {
159            Overlay overlay = new Overlay();
160            overlay.setInputPDF(pdfDoc);
161            overlay.setAllPagesOverlayPDF(pdfOverlayDoc);
162            overlay.overlay(Map.of());
163            return saveInTempFile(pdfDoc);
164        } catch (IOException e) {
165            throw new NuxeoException(e);
166        }
167    }
168
169    public  Point2D computeTranslationVector(double pageWidth, double watermarkWidth,
170                                               double pageHeight, double watermarkHeight,
171                                               WatermarkProperties properties) {
172        double xRotationOffset = 0;
173        double yRotationOffset = 0;
174
175        if (properties.getRotation() != 0) {
176            Rectangle2D rectangle2D =
177                    new Rectangle2D.Double(
178                            0, -watermarkHeight, watermarkWidth, watermarkHeight);
179            AffineTransform at = AffineTransform.getRotateInstance(
180                    -Math.toRadians(properties.getRotation()), 0, 0);
181            Shape shape = at.createTransformedShape(rectangle2D);
182            Rectangle2D rotated = shape.getBounds2D();
183
184            watermarkWidth = rotated.getWidth();
185            if (!properties.isInvertX() || properties.isRelativeCoordinates()) {
186                xRotationOffset = -rotated.getX();
187            } else {
188                xRotationOffset = rotated.getX();
189            }
190
191            watermarkHeight = rotated.getHeight();
192            if (!properties.isInvertY() || properties.isRelativeCoordinates()) {
193                yRotationOffset = rotated.getY()+rotated.getHeight();
194            } else {
195                yRotationOffset = -(rotated.getY()+rotated.getHeight());
196            }
197        }
198
199        double xTranslation;
200        double yTranslation;
201
202        if (properties.isRelativeCoordinates()) {
203            xTranslation = (pageWidth - watermarkWidth ) * properties.getxPosition() + xRotationOffset;
204            yTranslation = (pageHeight - watermarkHeight ) * properties.getyPosition() + yRotationOffset;
205        } else {
206            xTranslation = properties.getxPosition() + xRotationOffset;
207            yTranslation = properties.getyPosition() + yRotationOffset;
208            if (properties.isInvertX()) {
209                xTranslation = pageWidth - watermarkWidth - xTranslation;
210            }
211            if (properties.isInvertY()) {
212                yTranslation = pageHeight - watermarkHeight - yTranslation;
213            }
214        }
215        return new Point2D.Double(xTranslation, yTranslation);
216    }
217
218    protected Blob saveInTempFile(PDDocument PdfDoc) throws IOException {
219        Blob blob = Blobs.createBlobWithExtension(".pdf"); // creates a tracked temporary file
220        blob.setMimeType(MIME_TYPE);
221        PdfDoc.save(blob.getFile());
222        return blob;
223    }
224
225}