airviewer.AIRViewerModel.java Source code

Java tutorial

Introduction

Here is the source code for airviewer.AIRViewerModel.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 *@author Erik M. Buck
 */
package airviewer;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import static java.lang.Float.parseFloat;
import java.nio.file.Path;
import java.rmi.server.UID;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.rendering.PDFRenderer;
import static java.lang.Integer.parseInt;

/**
 * This class encapsulates the Model in the Model-View-Controller Design Pattern
 * and is primarily concerned with PDF document loading, rendering, editing, and
 * saving. The Model works even when the View and Controller subsystems have not
 * been created e.g. when the application is being used via a command line
 * script. As an implementation detail, the Open Source PDFBox library is used,
 * but no dependencies on any particular PDF support library should leak out of
 * the Model subsystem.
 *
 * Most of the functionality of this class is inherited from
 * DocumentCommandWrapper. This class adds methods like save() that are
 * specifically needed by the Controller subsystem but not already present in
 * DocumentCommandWrapper or only available as protected methods of
 * DocumentCommandWrapper. This class also defines and registers Command
 * subclasses to support moving, changing, and deleting selected annotations as
 * opposed to the inherited commands that do not use selection as a criteria.
 *
 * @author Erik M. Buck (Reviewed by Ankita Saxena
 */
public class AIRViewerModel extends DocumentCommandWrapper {

    /**
     * The object used to report errors other than "expected" errors like
     * incorrect strings that may have been input by users or provided by
     * scripts.
     */
    //private static final Logger LOGGER = Logger.getLogger(AIRViewerModel.class.getName());

    /**
     * The PDFBox renderer used to convert individual PDF "pages" into images.
     */
    private final PDFRenderer renderer;

    /**
     * Constructor: Loads the PDF document at the path (file system path).
     *
     * @param path A file system path to a PDF file.
     * @throws IOException If the PDF file cannot be read or does not contain
     * valid PDF data.
     */
    AIRViewerModel(Path path) throws IOException {
        super(PDDocument.load(path.toFile()), "");
        renderer = new PDFRenderer(wrappedDocument);
        AbstractDocumentCommandWrapper.registerCommandFactoryWithName(
                (AbstractDocumentCommandWrapper owner,
                        ArrayList<String> args) -> new MoveSelectedAnnotationDocumentCommand(owner, args),
                "MoveSelectedAnnotation");
        AbstractDocumentCommandWrapper.registerCommandFactoryWithName(
                (AbstractDocumentCommandWrapper owner,
                        ArrayList<String> args) -> new DeleteSelectedAnnotationDocumentCommand(owner, args),
                "DeleteSelectedAnnotation");
        AbstractDocumentCommandWrapper.registerCommandFactoryWithName(
                (AbstractDocumentCommandWrapper owner,
                        ArrayList<String> args) -> new ChangeSelectedTextAnnotationDocumentCommand(owner, args),
                "ChangeSelectedAnnotationText");
    }

    /**
     *
     * @param pageIndex Must be pageIndex >= 0 && pageIndex < numPages() 
     * @return An image produced by rendering the PDF page specified by pageIndex. The page index
     * is an index starting with zero in an array of "pages" obtained from the
     * loaded PDf document. Returns null if no such image can be produced.
     *
     */
    public Image getImage(int pageIndex) {
        assert pageIndex >= 0 && pageIndex < getPageCount();

        Image result = null;

        try {
            BufferedImage pageImage = renderer.renderImage(pageIndex);
            result = SwingFXUtils.toFXImage(pageImage, null);
        } catch (IOException ex) {
            throw new UncheckedIOException(
                    "Unable to render the page at index:<" + Integer.toString(pageIndex) + ">", ex);
        }

        return result;
    }

    /**
     * Save the loaded PDF document (if any) by writing all of its content and
     * annotations as PDF data into file. This operation replaces the entire
     * content of file. Both regular PDF content streams and PDF annotation
     * streams are stored.
     *
     * @param file The file into which PDF data is written.
     */
    public void save(File file) {
        assert file != null;
        try {
            wrappedDocument.save(file);
        } catch (IOException ex) {
            Logger.getLogger(AIRViewerModel.class.getName()).log(Level.SEVERE, "Unable to save PDF data.", ex);
        }
    }

    /**
     * Some PDF editors (and hand written files) produce unnamed annotations,
     * but the Model relies on annotations having unique names. Call this method
     * to ensure that all annotations on the specified page in wrappedDocument
     * have unique names regardless of the presence or absence of pre-existing
     * names.
     *
     * @param pageIndex pageIndex >= 0 && pageIndex < numPages()
     * @return A list of uniquely named annotations.
     */
    private List<PDAnnotation> getAllSanitizedAnnotationsOnPage(int pageIndex) {
        assert Integer.toString(pageIndex) != null;
        List<PDAnnotation> result = null;

        try {
            PDPage page = wrappedDocument.getPage(pageIndex);
            result = page.getAnnotations();
            for (PDAnnotation a : result) {
                // Other programs neglect to provide a name, so provide one
                // to uniquely identify selectde annotations.
                a.setAnnotationName(new UID().toString());

            }
        } catch (IOException ex) {
            Logger.getLogger(AbstractDocumentCommandWrapper.class.getName()).log(Level.SEVERE, null, ex);
        }

        return result;
    }

    /**
     * A Command to change the text "Contents" of an existing annotation.
     */
    public class ChangeSelectedTextAnnotationDocumentCommand extends AbstractDocumentCommand {

        /**
         *
         * @param anOwner
         * @param args
         */
        public ChangeSelectedTextAnnotationDocumentCommand(AbstractDocumentCommandWrapper anOwner,
                ArrayList<String> args) {
            super(anOwner, args);
            assert 2 == args.size();
        }

        /**
         *
         * @param anOwner
         * @param annotations
         * @param args
         */
        public ChangeSelectedTextAnnotationDocumentCommand(AbstractDocumentCommandWrapper anOwner,
                List<PDAnnotation> annotations, ArrayList<String> args) {
            super(anOwner, annotations, args);
            assert 1 == annotations.size();
            assert 2 == args.size();
        }

        /**
         *
         * @return If execute() succeeds, a Command that is the reciprocal of
         * the receiver is returned. Otherwise, null is returned.
         */
        @Override
        public AbstractDocumentCommand execute() {
            assert null != owner;
            assert null != arguments;

            AbstractDocumentCommand result = null;

            if (2 == arguments.size()) {
                List<PDAnnotation> candidates;

                if (null != annotations && 0 < annotations.size()) {
                    // In this case, the annotations to change are in annotations.
                    // We are probably undoing or redoing
                    candidates = annotations;
                } else {
                    // We should the change the selected annotations
                    candidates = owner.getSelectedAnnotations();
                }

                if (0 < candidates.size()) {

                    //int pageNumber = parseInt(arguments.get(0));
                    ArrayList<String> newArgs = new ArrayList<>(arguments);
                    newArgs.set(1, candidates.get(0).getContents());
                    result = new ChangeSelectedTextAnnotationDocumentCommand(owner, new ArrayList<>(candidates),
                            newArgs);

                    candidates.stream().map((a) -> {
                        a.setContents(arguments.get(1));
                        return a;
                    }).forEach((a) -> {
                        TextInAnnotationReplacer.replaceText(owner.wrappedDocument, a, arguments.get(1));
                    });
                }
            } else {
                System.err.printf("<%s> Expected 2 arguments but received %d.%n", getName(), arguments.size());

            }
            return result;
        }

        /**
         *
         * @return The name of the command as it will appear in a user interface
         * for undo and redo operations e.g. "Undo Delete Annotation" where the
         * string after "Undo " is returned from getName().
         */
        @Override
        public String getName() {
            return "Change Annotation Text";
        }

    }

    /**
     * A Command to move the selected annotations (if any).
     */
    public class MoveSelectedAnnotationDocumentCommand extends AbstractDocumentCommand {

        /**
         *
         * @param anOwner
         * @param args
         */
        public MoveSelectedAnnotationDocumentCommand(AbstractDocumentCommandWrapper anOwner,
                ArrayList<String> args) {
            super(anOwner, args);
            assert 3 == args.size();
        }

        /**
         *
         * @param anOwner
         * @param annotations
         * @param args
         */
        public MoveSelectedAnnotationDocumentCommand(AbstractDocumentCommandWrapper anOwner,
                List<PDAnnotation> annotations, ArrayList<String> args) {
            super(anOwner, annotations, args);
            assert 1 == annotations.size();
            assert 3 == args.size();
        }

        /**
         *
         * @return If execute() succeeds, a Command that is the reciprocal of
         * the receiver is returned. Otherwise, null is returned.
         */
        @Override
        public AbstractDocumentCommand execute() {
            assert null != owner;
            assert 3 == arguments.size();

            AbstractDocumentCommand result = null;
            List<PDAnnotation> candidates;

            if (null != annotations && 0 < annotations.size()) {
                // In this case, the annotations to move are in annotations.
                // We are probably undoing or redoing
                candidates = annotations;
            } else {
                // We should the move the selected annotations
                candidates = owner.getSelectedAnnotations();
            }

            if (0 < candidates.size()) {

                float dx = parseFloat(arguments.get(1));
                float dy = parseFloat(arguments.get(2));

                ArrayList<String> newArgs = new ArrayList<>(arguments);
                newArgs.set(1, Float.toString(-dx));
                newArgs.set(2, Float.toString(-dy));
                result = new MoveSelectedAnnotationDocumentCommand(owner, new ArrayList<>(candidates), newArgs);

                candidates.stream().forEach((a) -> {
                    PDRectangle position = a.getRectangle();
                    position.setLowerLeftX(position.getLowerLeftX() + dx);
                    position.setLowerLeftY(position.getLowerLeftY() + dy);
                    position.setUpperRightX(position.getUpperRightX() + dx);
                    position.setUpperRightY(position.getUpperRightY() + dy);
                    a.setRectangle(position);
                });
            }

            return result;
        }

        /**
         *
         * @return The name of the command as it will appear in a user interface
         * for undo and redo operations e.g. "Undo Delete Annotation" where the
         * string after "Undo " is returned from getName().
         */
        @Override
        public String getName() {
            return "Move Annotation";
        }

    }

    /**
     * Instances of this class encapsulate commands to annotate PDF documents
     * encapsulated by deleting an existing annotation.
     */
    public class DeleteSelectedAnnotationDocumentCommand extends AbstractDocumentCommand {

        /**
         *
         * @param anOwner
         * @param args
         */
        public DeleteSelectedAnnotationDocumentCommand(AbstractDocumentCommandWrapper anOwner,
                ArrayList<String> args) {
            super(anOwner, args);
        }

        /**
         *
         * @return If execute() succeeds, a Command that is the reciprocal of
         * the receiver is returned. Otherwise, null is returned.
         */
        @Override
        public AbstractDocumentCommand execute() {
            assert owner != null;
            List<PDAnnotation> oldAnnotations;
            List<PDAnnotation> selectedAnnotations = owner.getSelectedAnnotations();
            AbstractDocumentCommand result = null;

            if (0 < selectedAnnotations.size()) {
                assert 0 < arguments.size();

                try {
                    int pageNumber = parseInt(arguments.get(0));
                    PDPage page = owner.wrappedDocument.getPage(pageNumber);
                    oldAnnotations = getAllSanitizedAnnotationsOnPage(pageNumber);

                    if (null != oldAnnotations) {
                        result = new ReplaceAnnotationDocumentCommand(owner, new ArrayList<>(oldAnnotations),
                                arguments);
                        selectedAnnotations.stream().map((a) -> {
                            List<PDAnnotation> itemsToRemove = new ArrayList<>();
                            oldAnnotations.stream().map((p) -> {
                                assert null != p && null != p.getAnnotationName();
                                return p;
                            }).forEach((p) -> {
                                assert null != a && null != a.getAnnotationName();
                                if (p.getAnnotationName().equals(a.getAnnotationName())) {
                                    itemsToRemove.add(p);
                                }
                            });
                            return itemsToRemove;
                        }).forEach((itemsToRemove) -> {
                            itemsToRemove.stream().forEach((pa) -> {
                                oldAnnotations.remove(pa);
                            });
                        });
                        page.setAnnotations(oldAnnotations);
                    }
                    owner.deselectAll();
                } catch (NumberFormatException | NullPointerException ex) {
                    System.err.println("Non number encountered where floating point number expected.");
                    Logger.getLogger(DocumentCommandWrapper.AddBoxAnnotationDocumentCommand.class.getName())
                            .log(Level.SEVERE, null, ex);
                    result = null;
                }
            }

            return result;
        }

        /**
         *
         * @return The name of the command as it will appear in a user interface
         * for undo and redo operations e.g. "Undo Delete Annotation" where the
         * string after "Undo " is returned from getName().
         */
        @Override
        public String getName() {
            return "Delete Annotation";
        }
    }
}