/*
 * Copyright (c) 1997-2019 IDRsolutions (https://www.idrsolutions.com)
 */

package org.jpedal.examples.images;

import com.idrsolutions.image.tiff.TiffEncoder;
import org.jpedal.JDeliHelper;
import org.jpedal.PdfDecoderServer;
import org.jpedal.color.ColorSpaces;
import org.jpedal.constants.PageInfo;
import org.jpedal.exception.PdfException;
import org.jpedal.fonts.FontMappings;
import org.jpedal.objects.PdfFileInformation;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;

/**
 <h2>Image Extraction from PDF files</h2>
 <br>
 * This class provides a simple Java API to extract pages as images from a PDF file and also
 * a static convenience method if you just want to dump all the pages as images from a PDF file
 * or directory containing PDF files
 * isBackgroundTransparent <b>MUST</b> be false for generating JPEG images
 <br>
 <br>
 * Note: It is recommended to resort to ConverPagesToHiResImages as the first choice for generating better quality images when converting with non-default settings.
 <br>
 <h3>Example 1 - access API methods</h3>
 <pre><code>ConvertPagesToImages extract=new ConvertPagesToImages("C:/pdfs/mypdf.pdf");
 * //extract.setPassword("password");
 *
 * //reduce size of image (also reduces memory usage
 * //extract.setPageScaling(0.25f); // default values is 1.33f to give same size as Adobe at 100%
 *
 * if (extract.openPDFFile()) {
 *     int pageCount=extract.getPageCount();
 *
 *        BufferedImage image=extract.getPageAsImage(page, isBackgroundTransparent);
 *     }
 * }
 *
 * extract.closePDFfile();</code></pre>
 <br>
 <h3>Example 2 - convenience static method</h3>
 * Extract all pages as images<br>
 <br>
 <pre><code>ConvertPagesToImages.writeAllPagesAsImagesToDir("pdfs", "images" , "png", 1.33f);</code></pre>
 *
 <h3>Example 3 - Access directly from the Jar</h3>
 * ConvertPagesToImages can run from jar directly using the command and will extract all files from a PDF file or directory
 * to a defined output directory:<br>
 *
 <code>java -cp libraries_needed org/jpedal/examples/images/ConvertPagesToImages inputValues</code><br>
 *
 * Where inputValues is 4 values:
 <ul>
 <li>First value:    The PDF filename (including the path if needed) or a directory containing PDF files. If it contains spaces it must be enclosed by double quotes (ie "C:/Path with spaces/").</li>
 <li>Second value: The location to write out images extracted from the PDF file or files. If it contains spaces it must be enclosed by double quotes (ie "C:/Path with spaces/").</li>
 <li>Third value:    This indicates the required output image type (default is png if nothing specified). Options are tiff, bmp, png, jpg.</li>
 <li>Fourth value: Scaling of page 1.33f gives same size as page appears in Acrobat at 100%</li>
 </ul>
 *
 * There is another example (org.jpedal.examples.images.ConvertPagesToHiResImages) for producing higher res images of pages (but likely to be slower).
 <br><a href="http://www.idrsolutions.com/how-to-convert-pdf-files-to-image">Click here for a list of code examples to convert images</a><br>
 */
public class ConvertPagesToImages extends BaseImageExtraction {

    /**
     * use 96 dpi as default so pages correct size (72 will be smaller)
     */
    private float pageScaling = 1.33f;

    /**
     * holding all creators that produce OCR pdf's
     */
    private final String[] ocr = {"TeleForm"};

    /**
     * used as part of test to limit pages to first 10 - please do not use
     */
    public static int maxPageCount = -1;

    //only used if between 0 and 1
    private float JPEGcompression = -1f;

    /**
     * convenience static method to convert PDF file or directory of files
     @param inputDir directory of files to convert
     @param outDir directory of output
     @param format format of images
     @param pageScaling scaling
     @throws org.jpedal.exception.PdfException PdfException
     */
    public static void writeAllPagesAsImagesToDir(final String inputDir, final String outDir, final String format, final float pageScalingthrows PdfException {

        if (JDeliHelper.hasEncoderSupportForImageFormat(format)) {
            final ConvertPagesToImages convert = new ConvertPagesToImages(inputDir);

            convert.setup(format, outDir, pageScaling);

            convert.processFiles(inputDir);

            convert.closePDFfile();
        else {
            throw new RuntimeException("Unknown image format - " + format);
        }
    }

    /**
     * alter page scaling (default is 1.33f which gives same size as Acrobat at 100)
     *
     @param pageScaling scaling
     */
    public void setPageScaling(final float pageScaling) {
        this.pageScaling = pageScaling;

        decode_pdf.setExtractionMode(0, pageScaling);
    }

    private void setup(final String format, String outDir, final float pageScalingthrows RuntimeException {

        //check output dir has separator
        if (!outDir.endsWith(separator)) {
            outDir += separator;
        }

        this.imageType = format;
        this.output_dir = outDir;
        this.pageScaling = pageScaling;

        //check output dir has separator
        if (!user_dir.endsWith(separator)) {
            user_dir += separator;
        }
    }

    @Override
    void processFiles(String inputDirthrows PdfException, RuntimeException {

        //check file exists
        final File pdf_file = new File(inputDir);
        //if file exists, open and get number of pages
        if (!pdf_file.exists()) {
            throw new RuntimeException("File " + pdf_file + " not found");
        }

        //check output dir has separator
        if (!user_dir.endsWith(separator)) {
            user_dir += separator;
        }
        /*
         * if file name ends pdf, do the file otherwise
         * do every pdf file in the directory. We already know file or
         * directory exists so no need to check that, but we do need to
         * check its a directory
         */
        if (inputDir.toLowerCase().endsWith(".pdf")) {

            try {
                decodeFile(inputDir);
            catch (final Exception e) {
                System.err.println("ConvertPagesToImages " + e + " in pdf code in " + inputDir);
            }
        else {

            String[] files = null;
            final File inputFiles;
            //make sure name ends with a deliminator for correct path later
            if (!inputDir.endsWith(separator)) {
                inputDir += separator;
            }
            try {
                inputFiles = new File(inputDir);
                if (!inputFiles.isDirectory()) {
                    System.err.println(inputDir + " is not a directory. Exiting program");
                else {
                    files = inputFiles.list();
                }
            catch (final Exception ee) {
                throw new PdfException("Exception trying to access file " + ee.getMessage());

            }
            if (files != null) {
                //now work through all pdf files
                for (final String file : files) {
                    if (file.toLowerCase().endsWith(".pdf")) {

                        try {
                            decodeFile(inputDir + file);
                        catch (final Exception e1) {
                            System.err.println("ConvertPagesToImages " + e1 + " in pdf code in " + inputDir);
                        }
                    }
                }
            }
        }
    }

    /**
     * Sets up an ConvertPagesToImages instance to open a PDF File
     *
     @param fileName full path to a single PDF file
     */
    public ConvertPagesToImages(final String fileName) {
        super(fileName);

        init();
    }

    /**
     * Sets up an ConvertPagesToImages instance to open  a PDF file contained as a BLOB within a byte[] stream
     * (do not pad with additional empty bytes)
     *
     @param byteArray file's BLOB
     */
    public ConvertPagesToImages(final byte[] byteArray) {
        super(byteArray);

        init();

    }

    /**
     * routine to decode a file
     */
    @Override
    void decodeFile(final String file_namethrows PdfException {


        String name = "demo"//set a default just in case

        int pointer = file_name.lastIndexOf(separator);

        if (pointer == -1) {
            pointer = file_name.lastIndexOf('/');
        }

        if (pointer != -1) {
            name = file_name.substring(pointer + 1, file_name.length() 4);
        else if (file_name.toLowerCase().endsWith(".pdf")) {
            name = file_name.substring(0, file_name.length() 4);
        }

        //fix for odd files on Linux created when you view pages
        if (name.startsWith(".")) {
            return;
        }

        //create output dir for images
        if (output_dir == null) {
            output_dir = user_dir + "thumbnails" + separator;
        }

        //true as we are rendering page
        decode_pdf.setExtractionMode(0, pageScaling);
        //don't bother to extract text and images

        fileName = file_name;

        if (openPDFFile()) {

            //create a directory if it doesn't exist
            final File output_path = new File(output_dir);
            if (!output_path.exists()) {
                output_path.mkdirs();
            }

                         /*
             * allow output to multiple images with different values on each
             *
             * Note we REMOVE shapes as it is a new feature and we do not want to break existing functions
             */
            final String separation = System.getProperty("org.jpedal.separation");
            if (separation != null) {

                Object[] sepValues = {7"", Boolean.FALSE}//default of normal
                if (separation.equals("all")) {
                    sepValues = new Object[]{PdfDecoderServer.RENDERIMAGES, "image_and_shapes", Boolean.FALSE,
                            PdfDecoderServer.RENDERIMAGES + PdfDecoderServer.REMOVE_RENDERSHAPES, "image_without_shapes", Boolean.FALSE,
                            PdfDecoderServer.RENDERTEXT, "text_and_shapes", Boolean.TRUE,
                            7"all", Boolean.FALSE,
                            PdfDecoderServer.RENDERTEXT + PdfDecoderServer.REMOVE_RENDERSHAPES, "text_without_shapes", Boolean.TRUE
                    };
                }

                final int sepCount = sepValues.length;
                for (int seps = 0; seps < sepCount; seps += 3) {

                    decode_pdf.setRenderMode((IntegersepValues[seps]);
                    extractPagesAsImages(file_name, output_dir, name + '_' + sepValues[seps + 1](BooleansepValues[seps + 2])//boolean makes last transparent so we can see white text

                }

            else {
                //just get the page
                extractPagesAsImages(file_name, output_dir, name, false);
            }
        }

        closePDFfile();

    }

    private void extractPagesAsImages(final String file_name, final String output_dir, final String name, final boolean isTransparent) {

        //create a directory if it doesn't exist
        final File output_path = new File(output_dir);
        if (!output_path.exists()) {
            output_path.mkdirs();
        }

        final String multiPageFlag = System.getProperty("org.jpedal.multipage_tiff");
        final boolean isSingleOutputFile = multiPageFlag != null && multiPageFlag.equalsIgnoreCase("true");

        //allow user to specify value
        final String rawJPEGComp = System.getProperty("org.jpedal.compression_jpeg");
        if (rawJPEGComp != null) {
            try {
                JPEGcompression = Float.parseFloat(rawJPEGComp);
            catch (final Exception e) {
                e.printStackTrace();
            }
            if (JPEGcompression < || JPEGcompression > 1) {
                throw new RuntimeException("Invalid value for JPEG compression - must be between 0 and 1");
            }

        }

        final String tiffFlag = System.getProperty("org.jpedal.compress_tiff");
        final boolean compressTiffs = tiffFlag != null && tiffFlag.equalsIgnoreCase("true");

        //page range
        final int start = 1;
        int end = getPageCount();

        //limit to 1st ten pages in testing
        if (end > 10 && maxPageCount > 0) {
            end = maxPageCount;
        }

        try {

            for (int page = start; page < end + 1; page++) {
                getPage(output_dir, name, isTransparent, isSingleOutputFile, compressTiffs, end, page);
            }


        catch (final Exception e) {

            throw new RuntimeException("Exception " + e.getMessage() " with thumbnails on File=" + file_name);
        }
    }

    private void getPage(final String output_dir, final String name, final boolean isTransparent,
                         final boolean isSingleOutputFile,
                         final boolean compressTiffs, final int end, final int page)
            throws PdfException, IOException {
        //read pages

            // create a name with zeros for if more than 9 pages appears in correct order
            final StringBuilder pageAsString = new StringBuilder(String.valueOf(page));
            final String maxPageSize = String.valueOf(end);
            final int padding = maxPageSize.length() - pageAsString.length();
            for (int ii = 0; ii < padding; ii++) {
                pageAsString.insert(0'0');
            }

            final String image_name;
            if (isSingleOutputFile) {
                image_name = name;
            else {
                image_name = name + "_page_" + pageAsString;
            }

            /*
             * get PRODUCER and if OCR disable text printing
             */
            final PdfFileInformation currentFileInformation = decode_pdf.getFileInformationData();

            final String[] values = currentFileInformation.getFieldValues();
            final String[] fields = PdfFileInformation.getFieldNames();

            for (int i = 0; i < fields.length; i++) {

                if (fields[i].equals("Creator")) {

                    for (final String anOcr : ocr) {

                        if (values[i].equals(anOcr)) {

                            decode_pdf.setRenderMode(PdfDecoderServer.RENDERIMAGES);

                        }
                    }
                }
            }

            /*
             * get the current page as a BufferedImage
             */
            BufferedImage image_to_save = getPageAsImage(page, isTransparent);

            if (isTransparent) {

                
                //java adds odd tint if you save this as JPEG which does not have transparency
                // so put as RGB on white background
                // (or save as PNG or TIFF which has transparency)
                // or just call decode_pdf.getPageAsImage(page)
                if (image_to_save != null && imageType.toLowerCase().startsWith("jp")) { //&& !imageType.equalsIgnoreCase("jp2"))
                    image_to_save = saveJPEGwithoutTransparency(image_to_save);
                }
            }

            
            //if just gray we can reduce memory usage by converting image to Grayscale

            /*
             * see what Colorspaces used and reduce image if appropriate
             * (only does Gray at present)
             *
             * null if JPedal unsure
             */
            final Iterator<Integer> colorspacesUsed = decode_pdf.getPageInfo(PageInfo.COLORSPACES);

            int nextID;
            boolean isGrayOnly = colorspacesUsed != null//assume true and disprove
            while (colorspacesUsed != null && colorspacesUsed.hasNext()) {
                nextID = colorspacesUsed.next();

                if (nextID != ColorSpaces.DeviceGray && nextID != ColorSpaces.CalGray) {
                    isGrayOnly = false;
                }
            }

            //draw onto GRAY image to reduce colour depth
            //(converts ARGB to gray)
            if (isGrayOnly && image_to_save != null) {
                final BufferedImage image_to_save2 = new BufferedImage(image_to_save.getWidth(), image_to_save.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
                image_to_save2.getGraphics().drawImage(image_to_save, 00null);
                image_to_save = image_to_save2;
            }

            if (image_to_save != null) {

                /*allow user to specify maximum dimension for thumbnail*/
                final String maxDimensionAsString = System.getProperty("maxDimension");
                int maxDimension = -1;

                if (maxDimensionAsString != null) {
                    maxDimension = Integer.parseInt(maxDimensionAsString);
                }

                if (maxDimension != -1) {
                    image_to_save = resizeImage(image_to_save, maxDimension, imageType);
                }

                final String imageFormat = System.getProperty("org.jpedal.imageType");
                if (imageFormat != null) {
                    image_to_save = convertImage(image_to_save, imageFormat);
                }

                if (imageType.toLowerCase().startsWith("tif")) {
                    saveTiff(output_dir, isSingleOutputFile, compressTiffs, page, pageAsString, image_name, image_to_save);

                else {
                    //save image
                    decode_pdf.getObjectStore().saveStoredImage(output_dir + image_name, image_to_save, true, imageType);
                }
            }

            //flush images in case we do more than 1 page so only contains
            //images from current page
            decode_pdf.flushObjectValues(true);
            //flush any text data read

        }
    }

    private static BufferedImage resizeImage(BufferedImage image_to_save, int maxDimension, String imageType) {
        int newWidth = image_to_save.getWidth();
        int newHeight = image_to_save.getHeight();

        final Image scaledImage;
        if (newWidth > maxDimension || newHeight > maxDimension) {
            if (newWidth > newHeight) {
                newWidth = maxDimension;
                scaledImage = image_to_save.getScaledInstance(newWidth, -1, BufferedImage.SCALE_SMOOTH);
            else {
                newHeight = maxDimension;
                scaledImage = image_to_save.getScaledInstance(-1, newHeight, BufferedImage.SCALE_SMOOTH);
            }
        else {
            scaledImage = image_to_save.getScaledInstance(newWidth, -1, BufferedImage.SCALE_SMOOTH);
        }

        if (imageType.toLowerCase().startsWith("jp")) { //&& !imageType.equalsIgnoreCase("jp2")) {
            image_to_save = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), BufferedImage.TYPE_INT_RGB);
        else {
            image_to_save = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);
        }

        final Graphics2D g2 = image_to_save.createGraphics();

        g2.drawImage(scaledImage, 00null);
        return image_to_save;
    }

    private static BufferedImage convertImage(BufferedImage image_to_save, String imageFormat) {
        if (isNumber(imageFormat)) {
            final int iFormat = Integer.parseInt(imageFormat);
            if (iFormat > -&& iFormat < 14) {
                final BufferedImage tempImage = new BufferedImage(image_to_save.getWidth(), image_to_save.getHeight(), iFormat);
                final Graphics2D g = tempImage.createGraphics();
                g.drawImage(image_to_save, null, null);

                image_to_save = tempImage;
            else {
                System.err.println("Image Type is not valid. Value should be a digit between 0 - 13 based on the BufferedImage TYPE variables.");
            }
        else {
            System.err.println("Image Type provided is not an Integer. Value should be a digit between 0 - 13 based on the BufferedImage TYPE variables.");
        }
        return image_to_save;
    }

    private static void saveTiff(String output_dir, boolean isSingleOutputFile, boolean compressTiffs, int page, StringBuilder pageAsString, String image_name, BufferedImage image_to_savethrows IOException {
        final String outputFileName;
        final boolean isFirstPage = page == 1;

        final TiffEncoder tiffEncoder = new TiffEncoder();
        tiffEncoder.setCompressed(compressTiffs);

        if (isSingleOutputFile) {
            outputFileName = output_dir + image_name + ".tif";
            final File file = new File(outputFileName);
            if (isFirstPage && file.exists()) {
                file.delete();
                file.createNewFile();
            }
            tiffEncoder.append(image_to_save, outputFileName);

        else {
            outputFileName = output_dir + pageAsString + image_name + ".tif";
            final File file = new File(outputFileName);
            final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
            tiffEncoder.write(image_to_save, bos);
            bos.flush();
            bos.close();
        }
    }

    private static BufferedImage saveJPEGwithoutTransparency(BufferedImage image_to_save) {
        final BufferedImage rawVersion = image_to_save;

        final int w = rawVersion.getWidth();
        final int h = rawVersion.getHeight();
        //blank canvas
        image_to_save = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);

        //
        final Graphics2D g2 = image_to_save.createGraphics();
        //white background
        g2.setPaint(Color.WHITE);
        g2.fillRect(00, w, h);
        //paint on image
        g2.drawImage(rawVersion, 00null);
        return image_to_save;
    }
    //////////////////////////////////////////////////////////////////////////

    /**
     * main routine which checks  parameters passed in and runs the conversion
     @param args arguments
     */
    @SuppressWarnings("unused")
    public static void main(final String[] args) {

        //long start=System.currentTimeMillis();

        System.out.println("Simple demo to extract images from a page");

        //check values first and exit with info if too many
        final int count = args.length;
        final boolean failed = count < || count > 4;

        if (failed) {

            if (count > 0) {
                System.out.println("wrong arguments entered");
                System.out.println("2-4 values expected - 1. file \n 2. output location for images\n 3. output image type (png, tiff, jpeg) (optional)\n 4. Scaling (optional)");

                final StringBuilder arguments = new StringBuilder();
                for (final String arg : args) {
                    arguments.append(arg).append('\n');
                }
                System.out.println("you entered:\n" + arguments + "as the arguments");
            }
        else {

            try {
                switch (count) {
                    case 2:
                        writeAllPagesAsImagesToDir(args[0], args[1]"png"1.33f);
                        break;
                    case 4:
                        writeAllPagesAsImagesToDir(args[0], args[1], args[2], Float.valueOf(args[3]));
                        break;
                    default:
                        final String s = args[2].toLowerCase();
                        if (s.endsWith("g"|| s.endsWith("f")) {
                            writeAllPagesAsImagesToDir(args[0], args[1], s, 1.33f);
                        else {
                            writeAllPagesAsImagesToDir(args[0], args[1]"png", Float.valueOf(s));
                        }
                        break;
                }

                //System.out.println("Took="+(System.currentTimeMillis()-start)/1000);
            catch (final PdfException ex) {
                throw new RuntimeException(ex.getMessage());
            }
        }
    }

    /**
     * test to see if string or number
     */
    private static boolean isNumber(final String value) {

        //assume true and see if proved wrong
        boolean isNumber = true;

        final int charCount = value.length();
        for (int i = 0; i < charCount; i++) {
            final char c = value.charAt(i);
            if ((c < '0'(c > '9')) {
                isNumber = false;
                i = charCount;
            }
        }

        return isNumber;
    }

    @Override
    void init() {

        //mappings for non-embedded fonts to use
        FontMappings.setFontReplacements();

        type = ExtractTypes.RASTERIZED_PAGE;

        super.init();

        decode_pdf.setExtractionMode(0, pageScaling);
    }

    /**
     @param page                    Logical page number in PDF (first page is 1)
     @param isBackgroundTransparent defines if BufferedImage has a white or transparent background
     @return BufferedImage of PDF page
     @throws PdfException is any issues decoding PDF file
     */
    public BufferedImage getPageAsImage(final int page, final boolean isBackgroundTransparentthrows PdfException {

        checkFileOpened();

        if (!isBackgroundTransparent) {
            return decode_pdf.getPageAsImage(page);
        else //use this if you want a transparent image
            return decode_pdf.getPageAsTransparentImage(page);

        }
    }

    /**
     @param page Logical page number in PDF (first page is 1)
     @return BufferedImage of PDF page with white background
     @throws PdfException is any issues decoding PDF file
     */
    public BufferedImage getPageAsImage(final int pagethrows PdfException {

        return getPageAsImage(page, false);
    }
}