Coverage Summary for Class: ImageGenerator (org.kitodo.production.services.image)
Class |
Class, %
|
Method, %
|
Line, %
|
ImageGenerator |
100%
(1/1)
|
100%
(21/21)
|
86%
(80/93)
|
/*
* (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.Optional;
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.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.Helper;
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 final Collection<Subfolder> outputs;
/**
* Current position in list.
*/
private int position;
/**
* Folder with source images.
*/
private final 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 final List<ContentToBeGenerated> contentToBeGenerated;
/**
* Variant of image generation, see there.
*/
private final 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<>();
}
/**
* Cleanup target folders if <i>all</i> output is generated.
*/
public void removeGeneratedContent() {
for (Subfolder subfolder : outputs) {
for (URI uri : subfolder.listContents(true).values()) {
try {
ServiceManager.getFileService().delete(uri);
} catch (IOException e) {
logger.error(e);
}
}
}
}
/**
* 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));
}
/**
* 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);
Optional<String> optionalFormatName = fileFormat.getFormatName();
if (optionalFormatName.isPresent()) {
ImageIO.write((RenderedImage) image, optionalFormatName.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> foldersThatNeedDerivatives = new ArrayList<>(outputs.size());
Predicate<? super Subfolder> requiresGeneration = mode.getFilter(canonical);
for (Subfolder folder : outputs) {
if (requiresGeneration.test(folder)) {
foldersThatNeedDerivatives.add(folder);
}
}
return foldersThatNeedDerivatives;
}
/**
* 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 isChangingDpi = imageProperties.getDpi().isPresent();
boolean isGettingSizedWebImage = imageProperties.getImageSize().isPresent();
Optional<Double> optionalDerivative = imageProperties.getDerivative();
if (optionalDerivative.isPresent() && destinationImage.getFileFormat().getImageFileFormat().isPresent()) {
imageService.createDerivative(sourceImage, optionalDerivative.get(), destinationImage.getUri(canonical),
destinationImage.getFileFormat().getImageFileFormat().orElseThrow(IllegalStateException::new));
} else if (isChangingDpi || 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 {
Optional<Integer> optionalDpi = imageProperties.getDpi();
Optional<Double> optionalImageScale = imageProperties.getImageScale();
Optional<Integer> optionalImageSize = imageProperties.getImageSize();
if (optionalDpi.isPresent()) {
return imageService.changeDpi(sourceImage, optionalDpi.get());
} else if (optionalImageScale.isPresent()) {
return imageService.getScaledWebImage(sourceImage, optionalImageScale.get());
} else if (optionalImageSize.isPresent()) {
return imageService.getSizedWebImage(sourceImage, optionalImageSize.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 task’s state.
*
* @see org.kitodo.production.helper.tasks.EmptyTask#run()
*/
@Override
public void run() {
do {
state.accept(this);
if (state.equals(ImageGeneratorStep.DETERMINE_WHICH_IMAGES_NEED_TO_BE_GENERATED) && position == -1
&& sources.isEmpty()) {
if (Objects.nonNull(supervisor)) {
supervisor.setProgress(100);
supervisor.setWorkDetail(Helper.getTranslation("noImagesToGenerate"));
}
return;
}
position++;
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;
}
}