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}