/*
 * 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.examples.handlers.DefaultImageHelper;
import org.jpedal.exception.PdfException;
import org.jpedal.objects.PdfImageData;
import org.jpedal.utils.LogWriter;
import org.jpedal.utils.SecureDocumentBuilderFactory;
import org.jpedal.utils.SecureTransformerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

/**
 <h2>Image Extraction from PDF files</h2>
 <br>
 * This class provides a simple Java API to extract images from a PDF file and also
 * a static convenience method if you just want to dump all the images from a PDF file
 * or directory containing PDF files.<br>
 <br>
 <h3>Example 1 - access API methods</h3>
 <pre><code>ExtractImages extract=new ExtractImages("C:/pdfs/mypdf.pdf");
 * //extract.setPassword("password");
 * if (extract.openPDFFile()) {
 *     int pageCount=extract.getPageCount();
 *     for (int page=1; page&lt;=pageCount; page++) {
 *
 *        int imagesOnPageCount=extract.getImageCount(page);
 *        for (int image=0; image&lt;imagesOnPageCount; image++) {
 *             BufferedImage image=extract.getImage(page, image, true);
 *         }
 *     }
 * }
 *
 * extract.closePDFfile();</code></pre>
 <br>
 <h3>Example 2 - convenience static method</h3>
 * Extract all images with no metadata XML file into a directory<br>
 <br>
 <pre><code>ExtractImages.writeAllImagesToDir("pdfs", "images", false, false);</code></pre>
 *
 <h3>Example 3 -Access directly from the Jar</h3>
 * ExtractImages 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/ExtractImages inputValues</code><br>
 *
 * Where inputValues is 3 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:    Required output image type (default is png if nothing specified). Options are tiff, png, jpg.</li>
 </ul>
 *
 <br><a href="https://support.idrsolutions.com/hc/en-us/articles/115001993492">See our Support Pages for more info on Image Extraction.</a>
 */
public class ExtractImages extends BaseImageExtraction {

    private boolean outputPagesInSeparateDirs = true;

    private String defaultOutputDir;

    private boolean writeOutMetadata = true;

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

        init();
    }

    /**
     * Sets up an ExtractImages instance to open  a PDF file contained as a BLOB within a byte[] stream
     *
     @param byteArray
     */
    public ExtractImages(final byte[] byteArray) {
        super(byteArray);

        init();
    }

    /**
     @param writeOutMetadata - if true include an XML file for
     *                         each image with size, location and other metadata
     */
    private void setCreateMetsDataXMLfile(final boolean writeOutMetadata) {
        this.writeOutMetadata = writeOutMetadata;
    }

    /**
     * Convenience method to Extract all the images in a directory of PDF files
     *
     @param inputDir             directory containing PDF files
     @param outputDir            directory for writing out images
     @param generateMetaData     if true include additional XML file with metadata on image
     @param outputPagesInSepDirs if true place images from each page in separate sub-directory
     @throws org.jpedal.exception.PdfException
     */
    public static void writeAllImagesToDir(final String inputDir, final String outputDir, final String imageType, final boolean generateMetaData, final boolean outputPagesInSepDirsthrows PdfException {
        if (JDeliHelper.hasEncoderSupportForImageFormat(imageType)) {
            final ExtractImages extract = new ExtractImages(inputDir);

            extract.setup(outputDir, imageType, generateMetaData, outputPagesInSepDirs);

            extract.processFiles(inputDir);

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

    @Override
    void decodeFile(final String fileNamethrows PdfException {

        if (openPDFFile()) {

            //page range
            final int start = 1;
            final int end = getPageCount();
            
            /*
             * create output dir for images
             */
            if (defaultOutputDir == null) {
                output_dir = user_dir + "images" + separator + name + separator;
            else {
                output_dir = defaultOutputDir;
            }

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


            for (int page = start; page < end + 1; page++) {

                //image count (note image 1 is item 0, so any loop runs 0 to count-1)
                final int image_count = getImageCount(page);

//tell user
                if (image_count > 0) {

                    //create a directory for page our put all in same dir
                    String target = output_dir;
                    if (outputPagesInSeparateDirs) {
                        target = output_dir + separator + page;
                    }

                    final File page_path = new File(target);
                    if (!page_path.exists()) {
                        page_path.mkdirs();
                    }
                }

                try {
                    writeImagesFromPage(image_count, page);
                catch (final Exception ex) { // Cascade up
                    throw new PdfException(ex.getMessage());
                }

                // Flush images in case we do more than 1 page so only contains
                // images from current page
                decode_pdf.flushObjectValues(true);
            }
        }
    }

    private void writeImagesFromPage(final int image_count, final int pagethrows Exception {

        BufferedImage image_to_save;

        String outputDir = output_dir;
        if (outputPagesInSeparateDirs) {
            outputDir = output_dir + page + separator;
        }

        // Work through and save each image
        for (int i = 0; i < image_count; i++) {


            final String image_name = getImageName(page, i);

            //get raw version of image (R imageType for raw image)
            image_to_save = getImage(page, image_name, false);

            saveImage(image_to_save, outputDir + 'R' + image_name + '_' + page + '.' + imageType, imageType);

            //load processed version of image (converted to rgb)
            image_to_save = getImage(page, image_name, true);

            //used to check tests still fail
            //image_to_save=new BufferedImage(1,1,BufferedImage.TYPE_3BYTE_BGR);

            //save image
            if (image_to_save != null) {
                saveImage(image_to_save, outputDir + image_name + '_' + page + '.' + imageType, imageType);
            }
            //save metadata as XML file
            if (writeOutMetadata) {
                outputMetaDataToXML(fileName, page, pdf_images, i, image_name);
            }
        }
    }

    @Override
    void init() {
        type = ExtractTypes.IMAGES;

        super.init();
    }

    /**
     * save image - different versions have different bugs for file formats so we use best for
     * each image type
     *
     @param image_to_save
     */
    private static void saveImage(final BufferedImage image_to_save, final String fileName, final String prefixthrows Exception {

        if (prefix.contains("tif")) {

            final FileOutputStream os = new FileOutputStream(fileName);

            //get tiff compression
            final String tiffFlag = System.getProperty("org.jpedal.compress_tiff");
            final boolean compressTiffs = tiffFlag != null;

            final TiffEncoder tiffEncoder = new TiffEncoder();
            tiffEncoder.setCompressed(compressTiffs);
            tiffEncoder.write(image_to_save, os);

            os.flush();
            os.close();

        else //other images

            DefaultImageHelper.write(image_to_save, prefix, fileName);

        }
    }

    /**
     * write out details of image to XML file
     */
    private void outputMetaDataToXML(final String file_name, final int page, final PdfImageData pdf_images, final int i, final String image_name) {

        final float x1 = pdf_images.getImageXCoord(i);
        final float y1 = pdf_images.getImageYCoord(i);
        final float w = pdf_images.getImageWidth(i);
        final float h = pdf_images.getImageHeight(i);

        InputStream stylesheet = null;
        
        try {
            //create doc and set root
            final SecureDocumentBuilderFactory dbf = new SecureDocumentBuilderFactory();
            final DocumentBuilder db = dbf.newDocumentBuilder();
            final Document doc = db.newDocument();

            final Node root = doc.createElement("meta");
            doc.appendChild(root);

            //add comments
            final Node creation = doc.createComment("Created " + org.jpedal.utils.TimeNow.getShortTimeNow());
            doc.appendChild(creation);
            final Node info = doc.createComment("Pixel Location of image x1,y1,x2,y2");
            doc.appendChild(info);
            final Node moreInfo = doc.createComment("x1,y1 is top left corner origin is bottom left corner");
            doc.appendChild(moreInfo);

            //add location
            final Element location = doc.createElement("PAGELOCATION");
            location.setAttribute("x1", String.valueOf(x1));
            location.setAttribute("y1", String.valueOf((y1 + h)));
            location.setAttribute("x2", String.valueOf((x1 + w)));
            location.setAttribute("y2", String.valueOf(y1));
            root.appendChild(location);

            //add pdf file extracted from
            final Element fileName = doc.createElement("FILE");
            fileName.setAttribute("value", file_name);
            root.appendChild(fileName);

            //write out
            stylesheet = this.getClass().getResourceAsStream("/org/jpedal/examples/text/xmlstyle.xslt");

            final TransformerFactory transformerFactory = SecureTransformerFactory.newInstance();
            final Transformer transformer = transformerFactory.newTransformer(new StreamSource(stylesheet));
            String outputDir = output_dir;
            if (outputPagesInSeparateDirs) {
                outputDir = output_dir + page + separator;
            }
            transformer.transform(new DOMSource(doc)new StreamResult(outputDir + image_name + ".xml"));

        catch (final Exception e) {
            e.printStackTrace();
        finally {
            try {
                if (stylesheet != null) {
                    stylesheet.close();
                }
            catch (Exception e) {
                LogWriter.writeLog(e);
            }
        }
    }

    //////////////////////////////////////////////////////////////////////////

    /**
     * This class will allow you to extract Images via command line from a single PDF file or a directory of PDF files.
     <br>
     * The example expects three parameters:
     <ul>
     <li>Value 1 is the file name or directory of PDF files to process</li>
     <li>Value 2 is directory to write out the images</li>
     <li>Value 3 is image type (jpeg,tiff,png). Default is png</li>
     </ul>
     *
     @param args The expected arguments are described above.
     */
    @SuppressWarnings("unused")
    public static void main(final String[] args) {
        //check user has passed us a filename, output location and image type
        final int len = args.length;

        if (len != 3) {
            System.out.println("Class takes 3 parameters: ");
            System.out.println("Value 1 is the file name or directory of PDF files to process");
            System.out.println("Value 2 is Directory for writing the images");
            System.out.println("Value 3 is image type (jpeg,tiff,png).");

            if (len > 3) {
                System.out.println("\nToo many arguments entered");

                final StringBuilder arguments = new StringBuilder();
                for (final String arg : args) {
                    arguments.append(arg).append('\n');
                }
                System.out.println("You entered:\n" + arguments);
            }
            System.exit(0);

        else {
            try {
                ExtractImages.writeAllImagesToDir(args[0], args[1], args[2], true, false);
            catch (final PdfException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Controls if each page written to separate sub-directory
     * Default is true
     *
     @param outputPagesInSeparateDirs boolean flag
     */
    private void setOutputPagesInSeparateDirs(final boolean outputPagesInSeparateDirs) {
        this.outputPagesInSeparateDirs = outputPagesInSeparateDirs;
    }

    /**
     * extract any image from any page - recommended you process images on each page in turn as quicker
     *
     @param page             logical page number (1 is first page)
     @param imageNumber      image on page (0 is first image)
     @param imageAsDisplayed if true return image as displayed (with scaling/rotation) otherwise use raw stored image (often but not always the same). Neither is clipped
     @return BufferedImage
     @throws PdfException
     */
    public BufferedImage getImage(final int page, final int imageNumber, final boolean imageAsDisplayedthrows PdfException {

        checkFileOpened();

        return getImage(page, getImageName(page, imageNumber), imageAsDisplayed);
    }

    /**
     * extract any image from any page - recommended you process images on each page in turn as quicker
     *
     @param page             logical page number (1 is first page)
     @param image_name       name of image
     @param imageAsDisplayed if true return image as displayed (with scaling/rotation) otherwise use raw stored image (often but not always the same). Neither is clipped
     @return BufferedImage
     @throws PdfException
     */
    private BufferedImage getImage(final int page, final String image_name, final boolean imageAsDisplayedthrows PdfException {

        selectPage(page);

        if (imageAsDisplayed) {
            return decode_pdf.getObjectStore().loadStoredImage(image_name);
        else {
            return decode_pdf.getObjectStore().loadStoredImage('R' + image_name);
        }
    }

    private void setup(String outputDir, final String imageType, final boolean generateMetaData, final boolean outputPagesInSepDirs) {

        this.imageType = imageType;

        if (outputDir != null) {

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

        writeOutMetadata = generateMetaData;
        outputPagesInSeparateDirs = outputPagesInSepDirs;

    }

    /**
     * returns an image count for the selected page
     *
     @param page logical page number
     @return int number of images (0 if no images)
     @throws PdfException
     */
    public int getImageCount(final int pagethrows PdfException {

        checkFileOpened();

        selectPage(page);

        //image count (note image 1 is item 0, so any loop runs 0 to count-1)
        return pdf_images.getImageCount();
    }


    /**
     * Return name of image (composite of filename and Internal PDF image name)
     *
     @param page        - logical page number
     @param imageNumber - number of image (0 is first image)
     @return - String containing image name
     @throws PdfException
     */
    private String getImageName(final int page, final int imageNumberthrows PdfException {

        checkFileOpened();

        selectPage(page);

        return pdf_images.getImageName(imageNumber);
    }
}