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