blob: 6cd56490a64814f1f153063c35ad0ee50f8e8851 [file] [log] [blame]
/*
* 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
*
* https://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.
*
*/
package org.apache.tools.ant.taskdefs.optional.image;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Mapper;
import org.apache.tools.ant.types.optional.imageio.Draw;
import org.apache.tools.ant.types.optional.imageio.ImageOperation;
import org.apache.tools.ant.types.optional.imageio.Rotate;
import org.apache.tools.ant.types.optional.imageio.Scale;
import org.apache.tools.ant.types.optional.imageio.TransformOperation;
import org.apache.tools.ant.util.FileNameMapper;
import org.apache.tools.ant.util.IdentityMapper;
import org.apache.tools.ant.util.StringUtils;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* A MatchingTask which relies on Java ImageIO to read existing image files
* and write the results of AWT image manipulation operations.
* The operations are represented as ImageOperation DataType objects.
* The task replaces a JAI-based Image task which no longer works with Java 9+.
*
* @see ImageOperation
* @see org.apache.tools.ant.types.DataType
*/
public class ImageIOTask extends MatchingTask {
private final List<ImageOperation> instructions = new ArrayList<>();
private boolean overwrite = false;
private final List<FileSet> filesets = new ArrayList<>();
private File srcDir = null;
private File destDir = null;
private ImageFormat outputFormat;
private boolean garbageCollect = false;
private boolean failOnError = true;
private Mapper mapperElement = null;
/**
* Add a set of files to be deleted.
* @param set the FileSet to add.
*/
public void addFileset(FileSet set) {
filesets.add(set);
}
/**
* Set whether to fail on error.
* If false, note errors to the output but keep going.
* @param flag true or false.
*/
public void setFailOnError(boolean flag) {
failOnError = flag;
}
/**
* Set the source dir to find the image files.
* @param srcDir the directory in which the image files reside.
*/
public void setSrcdir(File srcDir) {
this.srcDir = srcDir;
}
/**
* Set the output image format.
* @param format an ImageFormat.
*/
public void setFormat(ImageFormat format) {
outputFormat = format;
}
/**
* Set whether to overwrite a file if there is a naming conflict.
* @param overwrite whether to overwrite.
*/
public void setOverwrite(boolean overwrite) {
this.overwrite = overwrite;
}
/**
* Set whether to invoke Garbage Collection after each image processed.
* Defaults to false.
* @param gc whether to invoke the garbage collector.
*/
public void setGc(boolean gc) {
garbageCollect = gc;
}
/**
* Set the destination directory for manipulated images.
* @param destDir The destination directory.
*/
public void setDestDir(File destDir) {
this.destDir = destDir;
}
/**
* Add an ImageOperation to chain.
* @param instr The ImageOperation to append to the chain.
*/
public void addImageOperation(ImageOperation instr) {
instructions.add(instr);
}
/**
* Add a Rotate ImageOperation to the chain.
* @param instr The Rotate operation to add to the chain.
* @see Rotate
*/
public void addRotate(Rotate instr) {
instructions.add(instr);
}
/**
* Add a Scale ImageOperation to the chain.
* @param instr The Scale operation to add to the chain.
* @see Scale
*/
public void addScale(Scale instr) {
instructions.add(instr);
}
/**
* Add a Draw ImageOperation to the chain. DrawOperation
* DataType objects can be nested inside the Draw object.
* @param instr The Draw operation to add to the chain.
* @see Draw
* @see org.apache.tools.ant.types.optional.image.DrawOperation
*/
public void addDraw(Draw instr) {
instructions.add(instr);
}
/**
* Add an ImageOperation to chain.
* @param instr The ImageOperation to append to the chain.
* @since Ant 1.7
*/
public void add(ImageOperation instr) {
addImageOperation(instr);
}
/**
* Defines the mapper to map source to destination files.
* @return a mapper to be configured
* @exception BuildException if more than one mapper is defined
* @since Ant 1.8.0
*/
public Mapper createMapper() throws BuildException {
if (mapperElement != null) {
throw new BuildException("Cannot define more than one mapper",
getLocation());
}
mapperElement = new Mapper(getProject());
return mapperElement;
}
/**
* Add a nested filenamemapper.
* @param fileNameMapper the mapper to add.
* @since Ant 1.8.0
*/
public void add(FileNameMapper fileNameMapper) {
createMapper().add(fileNameMapper);
}
/**
* Executes all the chained ImageOperations on the files inside
* the directory.
* @param srcDir File
* @param srcNames String[]
* @param dstDir File
* @param mapper FileNameMapper
* @return int
* @since Ant 1.8.0
*/
public int processDir(final File srcDir, final String[] srcNames,
final File dstDir, final FileNameMapper mapper) {
int writeCount = 0;
for (final String srcName : srcNames) {
final File srcFile = new File(srcDir, srcName).getAbsoluteFile();
final String[] dstNames = mapper.mapFileName(srcName);
if (dstNames == null) {
log(srcFile + " skipped, don't know how to handle it",
Project.MSG_VERBOSE);
continue;
}
for (String dstName : dstNames) {
final File dstFile = new File(dstDir, dstName).getAbsoluteFile();
if (dstFile.exists()) {
// avoid overwriting unless necessary
if (!overwrite
&& srcFile.lastModified() <= dstFile.lastModified()) {
log(srcFile + " omitted as " + dstFile
+ " is up to date.", Project.MSG_VERBOSE);
// don't overwrite the file
continue;
}
// avoid extra work while overwriting
if (!srcFile.equals(dstFile)) {
dstFile.delete();
}
}
processFile(srcFile, dstFile);
++writeCount;
}
}
// run the garbage collector if wanted
if (garbageCollect) {
System.gc();
}
return writeCount;
}
/**
* Executes all the chained ImageOperations on the file
* specified.
* @param file The file to be processed.
* @deprecated this method isn't used anymore
*/
@Deprecated
public void processFile(File file) {
processFile(file, new File(destDir == null
? srcDir : destDir, file.getName()));
}
/**
* Executes all the chained ImageOperations on the file
* specified.
* @param file The file to be processed.
* @param newFile The file to write to.
* @since Ant 1.8.0
*/
public void processFile(File file, File newFile) {
log("Processing File: " + file.getAbsolutePath());
try (ImageInputStream input = ImageIO.createImageInputStream(file)) {
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
log("No decoder available, skipping");
return;
}
ImageReader reader = readers.next();
if (outputFormat == null) {
outputFormat = new ImageFormat(reader.getFormatName());
}
reader.setInput(input);
BufferedImage image = reader.read(0);
reader.dispose();
for (ImageOperation instr : instructions) {
if (instr instanceof TransformOperation) {
image = ((TransformOperation) instr).executeTransformOperation(image);
} else {
log("Not a TransformOperation: " + instr);
}
}
File dstParent = newFile.getParentFile();
if (!dstParent.isDirectory()
&& !(dstParent.mkdirs() || dstParent.isDirectory())) {
throw new BuildException("Failed to create parent directory %s",
dstParent);
}
if (overwrite && newFile.exists() && !newFile.equals(file)) {
newFile.delete();
}
if (!ImageIO.write(image, outputFormat.getValue(), newFile)) {
log("Failed to save the transformed file");
}
} catch (IOException | RuntimeException err) {
if (!file.equals(newFile)) {
newFile.delete();
}
if (!failOnError) {
log("Error processing file: " + err);
} else {
throw new BuildException(err);
}
}
}
/**
* Executes the Task.
* @throws BuildException on error.
*/
@Override
public void execute() throws BuildException {
validateAttributes();
try {
File dest = (destDir != null) ? destDir : srcDir;
int writeCount = 0;
// build mapper
final FileNameMapper mapper = mapperElement == null
? new IdentityMapper() : mapperElement.getImplementation();
// deal with specified srcDir
if (srcDir != null) {
writeCount += processDir(srcDir,
super.getDirectoryScanner(srcDir).getIncludedFiles(), dest,
mapper);
}
// deal with the filesets
for (FileSet fs : filesets) {
writeCount += processDir(fs.getDir(),
fs.getDirectoryScanner().getIncludedFiles(),
dest, mapper);
}
if (writeCount > 0) {
log("Processed " + writeCount + (writeCount == 1 ? " image." : " images."));
}
} catch (Exception err) {
log(StringUtils.getStackTrace(err), Project.MSG_ERR);
throw new BuildException(err.getMessage());
}
}
/**
* Ensure we have a consistent and legal set of attributes, and set
* any internal flags necessary based on different combinations
* of attributes.
* @throws BuildException on error.
*/
protected void validateAttributes() throws BuildException {
if (srcDir == null && filesets.isEmpty()) {
throw new BuildException(
"Specify at least one source--a srcDir or a fileset.");
}
if (srcDir == null && destDir == null) {
throw new BuildException("Specify the destDir, or the srcDir.");
}
}
/**
* defines acceptable image formats.
*/
public static class ImageFormat extends EnumeratedAttribute {
private static final String[] VALUES = ImageIO.getReaderFormatNames();
/**
* Constructor
*/
public ImageFormat() {
}
/**
* Constructor using a string.
* @param value the value of the attribute
*/
public ImageFormat(String value) {
setValue(value);
}
/** {@inheritDoc}. */
@Override
public String[] getValues() {
return VALUES;
}
}
}