Swati4star/Images-to-PDF

View on GitHub
app/src/main/java/swati4star/createpdf/util/PDFUtils.java

Summary

Maintainability
A
3 hrs
Test Coverage
package swati4star.createpdf.util;

import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;

import android.app.Activity;
import android.app.AlertDialog;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.pdf.PdfRenderer;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.ParcelFileDescriptor;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PRStream;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfImportedPage;
import com.itextpdf.text.pdf.PdfName;
import com.itextpdf.text.pdf.PdfNumber;
import com.itextpdf.text.pdf.PdfObject;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.parser.PdfImageObject;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;

import swati4star.createpdf.R;
import swati4star.createpdf.database.DatabaseHelper;
import swati4star.createpdf.interfaces.OnPDFCompressedInterface;
import swati4star.createpdf.interfaces.OnPdfReorderedInterface;

public class PDFUtils {

    private final Activity mContext;
    private final FileUtils mFileUtils;

    public PDFUtils(Activity context) {
        this.mContext = context;
        this.mFileUtils = new FileUtils(mContext);
    }

    /**
     * Creates a mDialog with details of given PDF file
     *
     * @param file - file name
     */
    public void showDetails(File file) {
        String name = file.getName();
        String path = file.getPath();
        String size = FileInfoUtils.getFormattedSize(file);
        String lastModDate = FileInfoUtils.getFormattedSize(file);

        TextView message = new TextView(mContext);
        TextView title = new TextView(mContext);
        message.setText(String.format
                (mContext.getResources().getString(R.string.file_info), name, path, size, lastModDate));
        message.setTextIsSelectable(true);
        title.setText(R.string.details);
        title.setPadding(20, 10, 10, 10);
        title.setTextSize(30);
        title.setTextColor(mContext.getResources().getColor(R.color.black));
        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
        final AlertDialog dialog = builder.create();
        builder.setView(message);
        builder.setCustomTitle(title);
        builder.setPositiveButton(mContext.getResources().getString(R.string.ok),
                (dialogInterface, i) -> dialog.dismiss());
        builder.create();
        builder.show();
    }

    /**
     * Check if a PDF at given path is encrypted
     *
     * @param path - path of PDF
     * @return true - if encrypted otherwise false
     */
    @WorkerThread
    public boolean isPDFEncrypted(String path) {
        boolean isEncrypted;
        PdfReader pdfReader = null;
        try {
            pdfReader = new PdfReader(path);
            isEncrypted = pdfReader.isEncrypted();
        } catch (IOException e) {
            isEncrypted = true;
        } finally {
            if (pdfReader != null) pdfReader.close();
        }
        return isEncrypted;
    }

    public void compressPDF(String inputPath, String outputPath, int quality,
                            OnPDFCompressedInterface onPDFCompressedInterface) {
        new CompressPdfAsync(inputPath, outputPath, quality, onPDFCompressedInterface)
                .execute();
    }

    /**
     * Main function to add images to PDF
     *
     * @param inputPath - path of input PDF
     * @param output    - path of output PDF
     * @param imagesUri - list of images to add
     * @return true, if succeeded, otherwise false
     */
    public boolean addImagesToPdf(String inputPath, String output, ArrayList<String> imagesUri) {
        try {
            PdfReader reader = new PdfReader(inputPath);
            Document document = new Document();
            PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(output));
            document.open();
            initDoc(reader, document, writer);
            appendImages(document, imagesUri);
            document.close();

            StringUtils.getInstance().getSnackbarwithAction(mContext, R.string.snackbar_pdfCreated)
                    .setAction(R.string.snackbar_viewAction, v ->
                            mFileUtils.openFile(output, FileUtils.FileType.e_PDF)).show();
            new DatabaseHelper(mContext).insertRecord(output, mContext.getString(R.string.created));

            return true;
        } catch (IOException | DocumentException e) {
            e.printStackTrace();
            StringUtils.getInstance().showSnackbar(mContext, R.string.remove_pages_error);
            return false;
        }
    }

    /**
     * Initialise document with pages from reader to writer
     *
     * @param reader   -
     * @param document
     * @param writer
     */
    private void initDoc(PdfReader reader, Document document, PdfWriter writer) {
        int numOfPages = reader.getNumberOfPages();
        PdfContentByte cb = writer.getDirectContent();
        PdfImportedPage importedPage;
        for (int page = 1; page <= numOfPages; page++) {
            importedPage = writer.getImportedPage(reader, page);
            document.newPage();
            cb.addTemplate(importedPage, 0, 0);
        }
    }

    /**
     * Add images at given URIs to end of given document
     *
     * @param document
     * @param imagesUri
     * @throws DocumentException
     * @throws IOException
     */
    private void appendImages(Document document, ArrayList<String> imagesUri) throws DocumentException, IOException {
        Rectangle documentRect = document.getPageSize();
        for (int i = 0; i < imagesUri.size(); i++) {
            document.newPage();
            Image image = Image.getInstance(imagesUri.get(i));
            image.setBorder(0);
            float pageWidth = document.getPageSize().getWidth(); // - (mMarginLeft + mMarginRight);
            float pageHeight = document.getPageSize().getHeight(); // - (mMarginBottom + mMarginTop);
            image.scaleToFit(pageWidth, pageHeight);
            image.setAbsolutePosition(
                    (documentRect.getWidth() - image.getScaledWidth()) / 2,
                    (documentRect.getHeight() - image.getScaledHeight()) / 2);
            document.add(image);
        }
    }

    public boolean reorderRemovePDF(String inputPath, String output, String pages) {
        try {
            PdfReader reader = new PdfReader(inputPath);
            reader.selectPages(pages);
            if (reader.getNumberOfPages() == 0) {
                StringUtils.getInstance().showSnackbar(mContext, R.string.remove_pages_error);
                return false;
            }
            //if (reader.getNumberOfPages() )
            PdfStamper pdfStamper = new PdfStamper(reader,
                    new FileOutputStream(output));
            pdfStamper.close();
            StringUtils.getInstance().getSnackbarwithAction(mContext, R.string.snackbar_pdfCreated)
                    .setAction(R.string.snackbar_viewAction, v ->
                            mFileUtils.openFile(output, FileUtils.FileType.e_PDF)).show();
            new DatabaseHelper(mContext).insertRecord(output,
                    mContext.getString(R.string.created));
            return true;

        } catch (IOException | DocumentException e) {
            e.printStackTrace();
            StringUtils.getInstance().showSnackbar(mContext, R.string.remove_pages_error);
            return false;
        }
    }

    /**
     * @param uri                     Uri of the pdf
     * @param path                    Absolute path of the pdf
     * @param onPdfReorderedInterface interface to update  pdf reorder progress
     */
    public void reorderPdfPages(Uri uri, String path, @NonNull OnPdfReorderedInterface onPdfReorderedInterface) {
        new ReorderPdfPagesAsync(uri, path, mContext, onPdfReorderedInterface).execute();
    }

    private static class CompressPdfAsync extends AsyncTask<String, String, String> {

        final int quality;
        final String inputPath;
        final String outputPath;
        final OnPDFCompressedInterface mPDFCompressedInterface;
        boolean success;

        CompressPdfAsync(String inputPath, String outputPath, int quality,
                         OnPDFCompressedInterface onPDFCompressedInterface) {
            this.inputPath = inputPath;
            this.outputPath = outputPath;
            this.quality = quality;
            this.mPDFCompressedInterface = onPDFCompressedInterface;
            success = false;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mPDFCompressedInterface.pdfCompressionStarted();
        }

        @Override
        protected String doInBackground(String... strings) {
            try {
                PdfReader reader = new PdfReader(inputPath);
                compressReader(reader);
                saveReader(reader);
                reader.close();
                success = true;
            } catch (IOException | DocumentException e) {
                e.printStackTrace();
                success = false;
            }
            return null;
        }

        /**
         * Attempt to compress each object in a PdfReader
         *
         * @param reader - PdfReader to have objects compressed
         * @throws IOException
         */
        private void compressReader(PdfReader reader) throws IOException {
            int n = reader.getXrefSize();
            PdfObject object;
            PRStream stream;

            for (int i = 0; i < n; i++) {
                object = reader.getPdfObject(i);
                if (object == null || !object.isStream())
                    continue;
                stream = (PRStream) object;
                compressStream(stream);
            }

            reader.removeUnusedObjects();
        }

        /**
         * If given stream is image compress it
         *
         * @param stream - Steam to be compressed
         * @throws IOException
         */
        private void compressStream(PRStream stream) throws IOException {
            PdfObject pdfSubType = stream.get(PdfName.SUBTYPE);
            System.out.println(stream.type());
            if (pdfSubType != null && pdfSubType.toString().equals(PdfName.IMAGE.toString())) {
                PdfImageObject image = new PdfImageObject(stream);
                byte[] imageBytes = image.getImageAsBytes();
                Bitmap bmp;
                bmp = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
                if (bmp == null) return;

                int width = bmp.getWidth();
                int height = bmp.getHeight();

                Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                Canvas outCanvas = new Canvas(outBitmap);
                outCanvas.drawBitmap(bmp, 0f, 0f, null);

                ByteArrayOutputStream imgBytes = new ByteArrayOutputStream();
                outBitmap.compress(Bitmap.CompressFormat.JPEG, quality, imgBytes);
                stream.clear();
                stream.setData(imgBytes.toByteArray(), false, PRStream.BEST_COMPRESSION);
                stream.put(PdfName.TYPE, PdfName.XOBJECT);
                stream.put(PdfName.SUBTYPE, PdfName.IMAGE);
                stream.put(PdfName.FILTER, PdfName.DCTDECODE);
                stream.put(PdfName.WIDTH, new PdfNumber(width));
                stream.put(PdfName.HEIGHT, new PdfNumber(height));
                stream.put(PdfName.BITSPERCOMPONENT, new PdfNumber(8));
                stream.put(PdfName.COLORSPACE, PdfName.DEVICERGB);
            }
        }

        /**
         * Save changes to given reader's data to the output path
         *
         * @param reader - changed reader
         * @throws DocumentException
         * @throws IOException
         */
        private void saveReader(PdfReader reader) throws DocumentException, IOException {
            PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPath));
            stamper.setFullCompression();
            stamper.close();
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            mPDFCompressedInterface.pdfCompressionEnded(outputPath, success);
        }
    }

    private class ReorderPdfPagesAsync extends AsyncTask<String, String, ArrayList<Bitmap>> {

        private final Uri mUri;
        private final String mPath;
        private final OnPdfReorderedInterface mOnPdfReorderedInterface;
        private final Activity mActivity;

        /**
         * @param uri                     Uri of the pdf
         * @param path                    Absolute path of the pdf
         * @param onPdfReorderedInterface interface to update  pdf reorder progress
         * @param activity                Its needed to get the current context
         */

        ReorderPdfPagesAsync(Uri uri,
                             String path,
                             Activity activity,
                             OnPdfReorderedInterface onPdfReorderedInterface) {
            this.mUri = uri;
            this.mPath = path;
            this.mOnPdfReorderedInterface = onPdfReorderedInterface;
            this.mActivity = activity;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            mOnPdfReorderedInterface.onPdfReorderStarted();
        }

        @Override
        protected ArrayList<Bitmap> doInBackground(String... strings) {
            ArrayList<Bitmap> bitmaps = new ArrayList<>();
            ParcelFileDescriptor fileDescriptor = null;
            try {
                if (mUri != null)
                    fileDescriptor = mActivity.getContentResolver().openFileDescriptor(mUri, "r");
                else if (mPath != null)
                    fileDescriptor = ParcelFileDescriptor.open(new File(mPath), MODE_READ_ONLY);
                if (fileDescriptor != null) {
                    PdfRenderer renderer = new PdfRenderer(fileDescriptor);
                    bitmaps = getBitmaps(renderer);
                    // close the renderer
                    renderer.close();
                }
            } catch (IOException | SecurityException | IllegalArgumentException |
                     OutOfMemoryError e) {
                e.printStackTrace();
            }
            return bitmaps;
        }

        /**
         * Get list of Bitmaps from PdfRenderer
         *
         * @param renderer
         * @return
         */
        private ArrayList<Bitmap> getBitmaps(PdfRenderer renderer) {
            ArrayList<Bitmap> bitmaps = new ArrayList<>();
            final int pageCount = renderer.getPageCount();
            for (int i = 0; i < pageCount; i++) {
                PdfRenderer.Page page = renderer.openPage(i);
                Bitmap bitmap = Bitmap.createBitmap(page.getWidth(), page.getHeight(),
                        Bitmap.Config.ARGB_8888);
                // say we render for showing on the screen
                page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

                // do stuff with the bitmap
                bitmaps.add(bitmap);
                // close the page
                page.close();
            }
            return bitmaps;
        }

        @Override
        protected void onPostExecute(ArrayList<Bitmap> bitmaps) {
            super.onPostExecute(bitmaps);
            if (bitmaps != null && !bitmaps.isEmpty()) {
                mOnPdfReorderedInterface.onPdfReorderCompleted(bitmaps);
            } else {
                mOnPdfReorderedInterface.onPdfReorderFailed();
            }
        }
    }

}