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