001/*
002 * (C) Copyright 2016-2018 Nuxeo (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 *     Miguel Nixo
019 */
020package org.nuxeo.ecm.platform.pdf;
021
022import java.io.File;
023import java.io.IOException;
024import java.util.List;
025
026import org.apache.commons.collections4.IteratorUtils;
027import org.apache.commons.lang3.StringUtils;
028import org.apache.pdfbox.pdmodel.PDDocument;
029import org.apache.pdfbox.pdmodel.PDPage;
030import org.apache.pdfbox.pdmodel.PDPageContentStream;
031import org.apache.pdfbox.pdmodel.common.PDRectangle;
032import org.apache.pdfbox.pdmodel.font.PDFont;
033import org.apache.pdfbox.pdmodel.font.PDType1Font;
034import org.nuxeo.ecm.core.api.Blob;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.NuxeoException;
037import org.nuxeo.ecm.core.api.impl.blob.FileBlob;
038import org.nuxeo.runtime.api.Framework;
039
040/**
041 * Add page numbers to a PDF, with misc parameters (font, size, color, position).
042 *
043 * @since 8.10
044 */
045public class PDFPageNumbering {
046
047    public static final float DEFAULT_FONT_SIZE = 16.0f;
048
049    private Blob blob;
050
051    private String password;
052
053    public enum PAGE_NUMBER_POSITION {
054        BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT, TOP_LEFT, TOP_CENTER, TOP_RIGHT
055    }
056
057    public PDFPageNumbering(Blob inBlob) {
058        blob = inBlob;
059    }
060
061    public PDFPageNumbering(DocumentModel inDoc, String inXPath) {
062        if (StringUtils.isBlank(inXPath)) {
063            inXPath = "file:content";
064        }
065        blob = (Blob) inDoc.getPropertyValue(inXPath);
066    }
067
068    /**
069     * Adds page numbers and returns a <i>new</i> Blob. Original blob is not modified. This code assumes:
070     * <ul>
071     * <li>There is no page numbers already (it will always draw the numbers).</li>
072     * <li>The PDF is not rotated.</li>
073     * <li>Default values apply:
074     * <ul>
075     * <li><code>inStartAtPage</code> and <code>inStartAtNumber</code> are set to 1 if they are passed as &lt; 1.</li>
076     * <li><code>inStartAtPage</code> is set to 1 if it is &gt; number of pages.</li>
077     * <li><code>inFontName</code> is set to "Helvetica" if "" or null.</li>
078     * <li><code>inFontSize</code> is set to 16 if it is &lt;= 0.</li>
079     * <li><code>inHex255Color</code> is set to black if "", null or if its length is &lt; 6. Expected format is
080     * <code>0xrrggbb</code>, <code>#rrggbb</code> or just <code>rrggbb</code>.</li>
081     * <li><code>inPosition</code> is set to <code>BOTTOM_RIGHT</code> if null.</li>
082     * </ul>
083     * </li>
084     * </ul>
085     *
086     * @param inStartAtPage Number of the first page to be numbered.
087     * @param inStartAtNumber Starting number for the page numbering.
088     * @param inFontName Name of the font to be used in the numbering.
089     * @param inFontSize Size of the font to be used in the numbering.
090     * @param inHex255Color Color of the font to be used in the numbering.
091     * @param inPosition Page positioning of the numbering.
092     * @return Blob
093     */
094    public Blob addPageNumbers(int inStartAtPage, int inStartAtNumber, String inFontName, float inFontSize,
095            String inHex255Color, PAGE_NUMBER_POSITION inPosition) throws NuxeoException {
096        Blob result;
097        inStartAtPage = inStartAtPage < 1 ? 1 : inStartAtPage;
098        int pageNumber = inStartAtNumber < 1 ? 1 : inStartAtNumber;
099        inFontSize = inFontSize <= 0 ? DEFAULT_FONT_SIZE : inFontSize;
100        int[] rgb = PDFUtils.hex255ToRGB(inHex255Color);
101        try (PDDocument doc = PDFUtils.load(blob, password)) {
102            List<?> allPages;
103            PDFont font;
104            int max;
105            if (StringUtils.isBlank(inFontName)) {
106                font = PDType1Font.HELVETICA;
107            } else {
108                font = PDFUtils.getStandardType1Font(inFontName);
109                if (font == null) {
110                    throw new NuxeoException("Not a standard font: " + inFontName);
111                }
112            }
113            allPages = IteratorUtils.toList(doc.getDocumentCatalog().getPages().iterator());
114            max = allPages.size();
115            inStartAtPage = inStartAtPage > max ? 1 : inStartAtPage;
116            for (int i = inStartAtPage; i <= max; i++) {
117                String pageNumAsStr = Integer.toString(pageNumber);
118                pageNumber += 1;
119                PDPage page = (PDPage) allPages.get(i - 1);
120                PDPageContentStream footercontentStream = new PDPageContentStream(doc, page, true, true);
121                float stringWidth = font.getStringWidth(pageNumAsStr) * inFontSize / 1000f;
122                float stringHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() * inFontSize / 1000;
123                PDRectangle pageRect = page.getMediaBox();
124                float xMoveAmount, yMoveAmount;
125                if (inPosition == null) {
126                    inPosition = PAGE_NUMBER_POSITION.BOTTOM_RIGHT;
127                }
128                switch (inPosition) {
129                case BOTTOM_LEFT:
130                    xMoveAmount = 10;
131                    yMoveAmount = pageRect.getLowerLeftY() + 10;
132                    break;
133                case BOTTOM_CENTER:
134                    xMoveAmount = (pageRect.getUpperRightX() / 2) - (stringWidth / 2);
135                    yMoveAmount = pageRect.getLowerLeftY() + 10;
136                    break;
137                case TOP_LEFT:
138                    xMoveAmount = 10;
139                    yMoveAmount = pageRect.getHeight() - stringHeight - 10;
140                    break;
141                case TOP_CENTER:
142                    xMoveAmount = (pageRect.getUpperRightX() / 2) - (stringWidth / 2);
143                    yMoveAmount = pageRect.getHeight() - stringHeight - 10;
144                    break;
145                case TOP_RIGHT:
146                    xMoveAmount = pageRect.getUpperRightX() - 10 - stringWidth;
147                    yMoveAmount = pageRect.getHeight() - stringHeight - 10;
148                    break;
149                // Bottom-right is the default
150                default:
151                    xMoveAmount = pageRect.getUpperRightX() - 10 - stringWidth;
152                    yMoveAmount = pageRect.getLowerLeftY() + 10;
153                    break;
154                }
155                footercontentStream.beginText();
156                footercontentStream.setFont(font, inFontSize);
157                footercontentStream.moveTextPositionByAmount(xMoveAmount, yMoveAmount);
158                footercontentStream.setNonStrokingColor(rgb[0], rgb[1], rgb[2]);
159                footercontentStream.drawString(pageNumAsStr);
160                footercontentStream.endText();
161                footercontentStream.close();
162            }
163            File tempFile = File.createTempFile("pdfutils-", ".pdf");
164            doc.save(tempFile);
165            result = new FileBlob(tempFile);
166            Framework.trackFile(tempFile, result);
167        } catch (IOException e) {
168            throw new NuxeoException("Failed to handle the pdf", e);
169        }
170        return result;
171    }
172
173    public void setPassword(String password) {
174        this.password = password;
175    }
176
177}