001/*
002 *  Copyright 2011 Anyware Services
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 */
016package org.ametys.plugins.flipbook.pdfbox;
017
018import java.awt.Dimension;
019import java.awt.Graphics;
020import java.awt.image.BufferedImage;
021import java.io.File;
022import java.io.FileFilter;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.OutputStream;
026import java.util.ArrayList;
027import java.util.List;
028
029import javax.imageio.ImageIO;
030
031import org.apache.avalon.framework.logger.AbstractLogEnabled;
032import org.apache.pdfbox.pdmodel.PDDocument;
033import org.apache.pdfbox.pdmodel.PDPageTree;
034import org.apache.pdfbox.rendering.ImageType;
035import org.apache.pdfbox.rendering.PDFRenderer;
036import org.apache.pdfbox.tools.imageio.ImageIOUtil;
037
038import org.ametys.plugins.flipbook.Document2ImagesConvertorPolicy;
039import org.ametys.plugins.flipbook.FlipbookException;
040
041import net.coobird.thumbnailator.makers.FixedSizeThumbnailMaker;
042import net.coobird.thumbnailator.resizers.DefaultResizerFactory;
043
044/**
045 * PDF to PNG convertor which makes use of the pdfbox library.
046 * Based on pdfbox's {@link PDFRenderer} utility class, adding the possibility to specify the file name pattern.
047 */
048public class PdfboxConvertor extends AbstractLogEnabled implements Document2ImagesConvertorPolicy
049{
050    @Override
051    public void convert(File pdfFile, File folder) throws IOException, FlipbookException
052    {
053        String outputPrefix = "page";
054        String imageFormat = "png";
055        
056        try (PDDocument document = PDDocument.load(pdfFile))
057        {
058            if (document.isEncrypted())
059            {
060                throw new IOException("The PDF file is encrypted, cannot read it.");
061            }
062            
063            long start = System.currentTimeMillis();
064            if (getLogger().isInfoEnabled())
065            {
066                getLogger().info("Converting PDF to PNG images using pdfbox.");
067            }
068            
069            writeImages(document, folder, imageFormat, outputPrefix, 120);
070            
071            // Generate preview
072            writePreview(folder, outputPrefix);
073            
074            long end = System.currentTimeMillis();
075            if (getLogger().isInfoEnabled())
076            {
077                getLogger().info("PDF converted to PNG in " + (end - start) + "ms.");
078            }
079        }
080    }
081    
082    /**
083     * Converts a given page range of a PDF document to bitmap images.
084     * @param document the PDF document
085     * @param folder the folder where to write
086     * @param imageFormat the target format (ex. "png")
087     * @param outputPrefix used to construct the filename for the individual images
088     * @return true if the images were produced, false if there was an error
089     * @throws IOException if an I/O error occurs
090     * @throws FlipbookException if an error occurs when manipulating the flipbook
091     */
092    protected List<String> writeImages(PDDocument document, File folder, String imageFormat, String outputPrefix) throws IOException, FlipbookException
093    {
094        return writeImages(document, folder, imageFormat, outputPrefix, 96);
095    }
096    
097    /**
098     * Converts a given page range of a PDF document to bitmap images.
099     * @param document the PDF document
100     * @param folder the folder where to write
101     * @param imageFormat the target format (ex. "png")
102     * @param outputPrefix used to construct the filename for the individual images
103     * @param resolution the resolution in dpi (dots per inch)
104     * @return true if the images were produced, false if there was an error
105     * @throws IOException if an I/O error occurs
106     * @throws FlipbookException if an error occurs when manipulating the flipbook
107     */
108    protected List<String> writeImages(PDDocument document, File folder, String imageFormat, String outputPrefix, int resolution) throws IOException, FlipbookException
109    {
110        return writeImages(document, folder, imageFormat, "", 1, Integer.MAX_VALUE, outputPrefix, ImageType.RGB, resolution, 1.0f);
111    }
112    
113    /**
114     * Converts a given page range of a PDF document to bitmap images.
115     * @param document the PDF document
116     * @param folder the folder where to write
117     * @param imageFormat the target format (ex. "png")
118     * @param password the password (needed if the PDF is encrypted)
119     * @param startPage the start page (1 is the first page)
120     * @param endPage the end page (set to Integer.MAX_VALUE for all pages)
121     * @param outputPrefix used to construct the filename for the individual images
122     * @param imageType the image type (see {@link BufferedImage}.TYPE_*)
123     * @param resolution the resolution in dpi (dots per inch)
124     * @param quality the image compression quality (0 &lt; quality &lt; 1.0f).
125     * @return true if the images were produced, false if there was an error
126     * @throws IOException if an I/O error occurs
127     * @throws FlipbookException if an error occurs when manipulating the flipbook
128     */
129    protected List<String> writeImages(PDDocument document, File folder, String imageFormat, String password, int startPage, int endPage, String outputPrefix, ImageType imageType, int resolution, float quality) throws IOException, FlipbookException
130    {
131        List<String> fileNames = new ArrayList<>();
132        
133        PDPageTree pageTree = document.getPages();
134        int pageCount = pageTree.getCount();
135        int digitCount = Integer.toString(pageCount).length();
136        
137        // %03d.png
138        String format = "%0" + digitCount + "d." + imageFormat;
139        for (int i = startPage - 1; i < endPage && i < pageCount; i++)
140        {
141            PDFRenderer pdfRenderer = new PDFRenderer(document);
142                    
143            BufferedImage image = pdfRenderer.renderImageWithDPI(i, resolution, imageType);
144            String fileName = outputPrefix + String.format(format, i + 1, imageFormat);
145            
146            fileNames.add(fileName);
147            
148            File imageFile = new File(folder, fileName + ".part");
149            
150            try (OutputStream os = new FileOutputStream(imageFile))
151            {
152                if (!ImageIOUtil.writeImage(image, imageFormat, os, resolution, quality))
153                {
154                    throw new FlipbookException("Unable to write PDF page " + i + " to " + imageFile.getAbsolutePath());
155                }
156            }
157
158            imageFile.renameTo(new File(folder, fileName));
159        }
160        
161        return fileNames;
162    }
163
164    public List<String> getSupportedMimeTypes()
165    {
166        List<String> mimeTypeSupported = new ArrayList<>();
167        mimeTypeSupported.add("application/pdf");
168        
169        return mimeTypeSupported;
170    }
171    
172    /**
173     * Generate the preview image
174     * @param folder The folder with the PDF images
175     * @param outputPrefix The prefix of PDF images
176     * @throws IOException if an I/O error occurs
177     */
178    protected void writePreview(File folder, String outputPrefix) throws IOException
179    {
180        File[] images = folder.listFiles(new FileFilter() 
181        {
182            public boolean accept(File pathname)
183            {
184                return pathname.getName().startsWith(outputPrefix);
185            }
186        });
187        
188        // Compute preview dimension from first image
189        File firstImage = images[0];
190        BufferedImage bfi = ImageIO.read(firstImage);
191        double imgRatio = (double) bfi.getHeight() / bfi.getWidth();
192        
193        int rows = (int) Math.ceil(1 + images.length / 2);
194        
195        int imgWidthInPreview = 56;
196        int imgHeightInPreview = (int) Math.round(imgWidthInPreview * imgRatio);
197        
198        // Create a image on 2 columns 
199        BufferedImage result = new BufferedImage(imgWidthInPreview * 2, imgHeightInPreview * rows, BufferedImage.TYPE_INT_RGB);
200        Graphics g = result.getGraphics();
201
202        int x = 0;
203        int y = 0;
204        int count = 0;
205        for (File image : images)
206        {
207            BufferedImage bi = ImageIO.read(image);
208            BufferedImage ri = _resizeImage(bi, imgHeightInPreview, imgWidthInPreview);
209            g.drawImage(ri, x, y, null);
210            x += imgWidthInPreview;
211            if (count == 0 || x >= result.getWidth())
212            {
213                x = 0;
214                y += ri.getHeight();
215            }
216            count++;
217        }
218        
219        ImageIO.write(result, "jpg" , new File(folder, "preview.jpg"));
220    }
221    
222    private static BufferedImage _resizeImage(BufferedImage src, int maxHeight, int maxWidth)
223    {
224        int srcHeight = src.getHeight();
225        int srcWidth = src.getWidth();
226        
227        int destHeight = 0;
228        int destWidth = 0;
229        
230        boolean keepAspectRatio = true;
231        
232        if (srcHeight <= maxHeight && srcWidth <= maxWidth || destHeight == srcHeight && destWidth == srcWidth)
233        {
234            // the source image is already smaller than the destination box or already the good format, don't change anything
235            return src;
236        }
237        
238        destWidth = maxWidth;
239        destHeight = maxHeight;
240        
241        Dimension srcDimension = new Dimension(srcWidth, srcHeight);
242        Dimension thumbnailDimension = new Dimension(destWidth, destHeight);
243        
244        BufferedImage thumbImage = new FixedSizeThumbnailMaker(destWidth, destHeight, keepAspectRatio, true)
245                                   .resizer(DefaultResizerFactory.getInstance().getResizer(srcDimension, thumbnailDimension))
246                                   .imageType(src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB)
247                                   .make(src); 
248        
249        return thumbImage;
250    }
251}