org.kitodo.production.services.image.ImageGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.kitodo.production.services.image.ImageGenerator.java

Source

/*
 * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
 *
 * This file is part of the Kitodo project.
 *
 * It is licensed under GNU General Public License version 3 or later.
 *
 * For the full copyright and license information, please read the
 * GPL3-License.txt file that was distributed with this source code.
 */

package org.kitodo.production.services.image;

import java.awt.Image;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.imageio.ImageIO;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.api.imagemanagement.ImageFileFormat;
import org.kitodo.config.xml.fileformats.FileFormat;
import org.kitodo.data.database.beans.Folder;
import org.kitodo.production.enums.GenerationMode;
import org.kitodo.production.enums.ImageGeneratorStep;
import org.kitodo.production.helper.tasks.EmptyTask;
import org.kitodo.production.model.Subfolder;
import org.kitodo.production.services.ServiceManager;
import org.kitodo.production.services.file.FileService;
import org.kitodo.production.thread.TaskImageGeneratorThread;
import org.kitodo.production.thread.TaskScriptThread;

/**
 * A program that generates images using the image management interface. This
 * program is run by the {@link TaskImageGeneratorThread} when the user manually
 * initiates the creation of the images. If the images are generated when the
 * task is completed, this is done by the {@link TaskScriptThread}.
 */
public class ImageGenerator implements Runnable {
    private static final Logger logger = LogManager.getLogger(ImageGenerator.class);
    private final FileService fileService = ServiceManager.getFileService();
    private final ImageService imageService = ServiceManager.getImageService();

    /**
     * Output folders.
     */
    private Collection<Subfolder> outputs;

    /**
     * Current position in list.
     */
    private int position;

    /**
     * Folder with source images.
     */
    private Subfolder sourceFolder;

    /**
     * List of possible source images.
     */
    private List<Pair<String, URI>> sources;

    /**
     * Current step of the generation process.
     */
    private ImageGeneratorStep state;

    /**
     * Task in the TaskManager that runs this ImageGenerator.
     */
    private EmptyTask supervisor;

    /**
     * List of elements to be generated.
     */
    private List<ContentToBeGenerated> contentToBeGenerated;

    /**
     * Variant of image generation, see there.
     */
    private GenerationMode mode;

    /**
     * Creates a new image generator.
     *
     * @param sourceFolder
     *            image source folder
     * @param mode
     *            whether all, or only a few images are to be generated
     * @param outputs
     *            output folders to generate to
     */
    public ImageGenerator(Subfolder sourceFolder, GenerationMode mode, Collection<Subfolder> outputs) {
        this.sourceFolder = sourceFolder;
        this.mode = mode;
        this.outputs = outputs;
        this.state = ImageGeneratorStep.LIST_SOURCE_FOLDER;
        this.sources = Collections.emptyList();
        this.contentToBeGenerated = new LinkedList<>();
    }

    /**
     * Appends the element to the list of elements to be generated.
     *
     * @param canonical
     *            the canonical part of the file name
     * @param sourceURI
     *            the source URI of the content to be generated
     * @param subfoldersWhoseContentsAreToBeGenerated
     *            subfolders whose contents are to be generated
     */
    public void addToContentToBeGenerated(String canonical, URI sourceURI,
            List<Subfolder> subfoldersWhoseContentsAreToBeGenerated) {

        contentToBeGenerated
                .add(new ContentToBeGenerated(canonical, sourceURI, subfoldersWhoseContentsAreToBeGenerated));
    }

    /**
     * Invokes the image management interface method that creates a derivative.
     * What kind of derivative should be created is determined from the folder
     * configuration.
     *
     * @param sourceImage
     *            address of the source image from which the derivative is to be
     *            calculated.
     * @param imageProperties
     *            configuration for the target image
     * @param imageFileFormat
     *            image file format to create
     * @param destinationImage
     *            image file to write
     * @throws IOException
     *             if an underlying disk operation fails
     */
    private void createDerivative(URI sourceImage, Folder imageProperties, ImageFileFormat imageFileFormat,
            URI destinationImage) throws IOException {

        imageService.createDerivative(sourceImage, imageProperties.getDerivative().get(), destinationImage,
                imageFileFormat);
    }

    /**
     * Generates a set of derivatives.
     *
     * @param instruction
     *            Instruction, which pictures are to be generated. Left: image
     *            source, right: destination folder. The image source consists
     *            of the canonical part of the file name and the resolved file
     *            name for the source image. The canonical part of the file name
     *            is needed to calculate the corresponding file name in the
     *            destination folder. The type of derivative to be generated is
     *            defined in the properties of the destination folder.
     */
    public void createDerivatives(ContentToBeGenerated instruction) {
        try {
            for (Subfolder destinationFolder : instruction.getSubfoldersWhoseContentsAreToBeGenerated()) {
                generateDerivative(instruction.getSourceURI(), destinationFolder, instruction.getCanonical());
            }
        } catch (IOException e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    /**
     * Generates a derived image and saves it with the on-board tools of Java.
     * The image is created by the image management interface. Which method of
     * the interface is called and its parameters are determined in the
     * configuration of the folder. The same is true for the file type under
     * which Java stores the image.
     *
     * @param sourceImage
     *            reference to the image that serves as a template for the
     *            reproduction process
     * @param imageProperties
     *            folder settings define what an image is created
     * @param fileFormat
     *            the file format specifies how the image should be saved
     * @param destinationImage
     *            specifies the location where the image should be written
     * @throws IOException
     *             if an underlying disk operation fails
     */
    private void createImageWithImageIO(URI sourceImage, Folder imageProperties, FileFormat fileFormat,
            URI destinationImage) throws IOException {

        try (OutputStream outputStream = fileService.write(destinationImage)) {
            Image image = retrieveJavaImage(sourceImage, imageProperties);
            if (fileFormat.getFormatName().isPresent()) {
                ImageIO.write((RenderedImage) image, fileFormat.getFormatName().get(), outputStream);
            }
        }
    }

    /**
     * Determines the folders in which a derivative must be created. Because the
     * ModuleLoader does not work when invoked from a parallelStream(), we use a
     * classic loop here.
     *
     * @param canonical
     *            canonical part of the file name, to determine the file names
     *            in the destination folder
     * @return the images to be generated
     */
    public List<Subfolder> determineFoldersThatNeedDerivatives(String canonical) {
        List<Subfolder> result = new ArrayList<>(outputs.size());
        Predicate<? super Subfolder> requiresGeneration = mode.getFilter(canonical);
        for (Subfolder folder : outputs) {
            if (requiresGeneration.test(folder)) {
                result.add(folder);
            }
        }
        return result;
    }

    /**
     * Gets the file list from the content folder, converts it into the required
     * form, and stores it in the sources field.
     */
    public void determineSources() {
        Map<String, URI> contents = sourceFolder.listContents();
        Stream<Entry<String, URI>> contentsStream = contents.entrySet().stream();
        Stream<Pair<String, URI>> sourcesStream = contentsStream
                .map(lambda -> Pair.of(lambda.getKey(), lambda.getValue()));
        this.sources = sourcesStream.collect(Collectors.toList());
    }

    /**
     * Generates the derivative depending on the declared generator function.
     *
     * @param sourceImage
     *            source file
     * @param destinationImage
     *            path to the target file to be generated
     * @param canonical
     *            the canonical part of the file name
     * @throws IOException
     *             if filesystem I/O fails
     */
    private void generateDerivative(URI sourceImage, Subfolder destinationImage, String canonical)
            throws IOException {

        Folder imageProperties = destinationImage.getFolder();
        boolean isCreatingDerivative = imageProperties.getDerivative().isPresent();
        boolean isChangingDpi = imageProperties.getDpi().isPresent();
        boolean isGettingScaledWebImage = imageProperties.getImageScale().isPresent();
        boolean isGettingSizedWebImage = imageProperties.getImageSize().isPresent();

        if (isCreatingDerivative && destinationImage.getFileFormat().getImageFileFormat().isPresent()) {
            createDerivative(sourceImage, imageProperties,
                    destinationImage.getFileFormat().getImageFileFormat().get(),
                    destinationImage.getUri(canonical));
        } else if (isChangingDpi || isGettingScaledWebImage || isGettingSizedWebImage) {
            createImageWithImageIO(sourceImage, imageProperties, destinationImage.getFileFormat(),
                    destinationImage.getUri(canonical));
        }
    }

    /**
     * Returns from contentToBeGenerated the item specified by position.
     *
     * @return the item specified by position
     */
    public ContentToBeGenerated getFromContentToBeGeneratedByPosition() {
        return contentToBeGenerated.get(position);
    }

    /**
     * Returns the current position in the list.
     *
     * @return the position
     */
    public int getPosition() {
        return position;
    }

    /**
     * Returns the list of source images.
     *
     * @return the list of source images
     */
    public List<Pair<String, URI>> getSources() {
        return sources;
    }

    /**
     * Returns the list of image generation task descriptions.
     *
     * @return the list of image generation task descriptions
     */
    public List<ContentToBeGenerated> getContentToBeGenerated() {
        return contentToBeGenerated;
    }

    /**
     * Returns the enum constant inicating the variant of the image generator
     * task.
     *
     * @return the variant of the image generator task
     */
    public GenerationMode getMode() {
        return mode;
    }

    /**
     * If there is a supervisor, lets him take an action. Otherwise nothing
     * happens.
     *
     * @param action
     *            what the supervisor should do
     */
    public void letTheSupervisorDo(Consumer<EmptyTask> action) {
        if (Objects.nonNull(supervisor)) {
            action.accept(supervisor);
        }
    }

    /**
     * Invokes one of the three methods of the image management interface that
     * return a Java image. Which method is called and its parameters are
     * determined in the configuration of the folder.
     *
     * @param sourceImage
     *            address of the source image from which the derivative is to be
     *            calculated.
     * @param imageProperties
     *            configuration for the target image
     * @return an image in memory
     * @throws IOException
     *             if an underlying disk operation fails
     */
    private Image retrieveJavaImage(URI sourceImage, Folder imageProperties) throws IOException {
        if (imageProperties.getDpi().isPresent()) {
            return imageService.changeDpi(sourceImage, imageProperties.getDpi().get());
        } else if (imageProperties.getImageScale().isPresent()) {
            return imageService.getScaledWebImage(sourceImage, imageProperties.getImageScale().get());
        } else if (imageProperties.getImageSize().isPresent()) {
            return imageService.getSizedWebImage(sourceImage, imageProperties.getImageSize().get());
        }
        throw new IllegalArgumentException(imageProperties + " does not give any method to create a java image");
    }

    /**
     * If the task is started, it will execute this run() method which will
     * start the export on the ExportDms. This task instance is passed in
     * addition so that the ExportDms can update the tasks state.
     *
     * @see org.kitodo.production.helper.tasks.EmptyTask#run()
     */
    @Override
    public void run() {
        do {
            state.accept(this);
            setPosition(getPosition() + 1);
            setProgress();
            if (Objects.nonNull(supervisor) && supervisor.isInterrupted()) {
                return;
            }
        } while (!(state.equals(ImageGeneratorStep.GENERATE_IMAGES)
                && getPosition() == getContentToBeGenerated().size()));
        logger.info("Completed");
    }

    /**
     * Sets the current position in the list.
     *
     * @param position
     *            position to set
     */
    public void setPosition(int position) {
        this.position = position;
    }

    /**
     * Calculates and reports the progress of the task.
     */
    private void setProgress() {
        if (Objects.nonNull(supervisor)) {
            int checked = state.equals(ImageGeneratorStep.GENERATE_IMAGES)
                    ? getMode().equals(GenerationMode.ALL) ? 1 : sources.size()
                    : 0;
            int generated = getMode().equals(GenerationMode.ALL)
                    && state.equals(ImageGeneratorStep.DETERMINE_WHICH_IMAGES_NEED_TO_BE_GENERATED) ? 0
                            : getPosition();
            int total = sources.size()
                    + (getMode().equals(GenerationMode.ALL) ? 1 : getContentToBeGenerated().size()) + 1;
            supervisor.setProgress(100d * (1 + checked + generated) / total);
        }
    }

    /**
     * Sets the current processing state.
     *
     * @param state
     *            state to set
     */
    public void setState(ImageGeneratorStep state) {
        this.state = state;
    }

    /**
     * Set a supervisor for this activity. If a supervisor is set, the progress
     * is reported back to him, and he responds to his interrupt requests.
     *
     * @param supervisor
     *            supervisor task to set
     */
    public void setSupervisor(EmptyTask supervisor) {
        this.supervisor = supervisor;
    }
}