From 293746c72a3686870c8efc696ae17350849d42f3 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Thu, 7 Jan 2016 21:08:28 -0600 Subject: [PATCH 01/86] Log CPU core count at startup --- src/main/java/edu/illinois/library/cantaloupe/Application.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/edu/illinois/library/cantaloupe/Application.java b/src/main/java/edu/illinois/library/cantaloupe/Application.java index 4a9120b4d..a9064ac02 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/Application.java +++ b/src/main/java/edu/illinois/library/cantaloupe/Application.java @@ -218,6 +218,8 @@ private void handle(Path path) { Runtime runtime = Runtime.getRuntime(); logger.info(System.getProperty("java.vm.name") + " / " + System.getProperty("java.vm.info")); + logger.info("{} available processor cores", + runtime.availableProcessors()); logger.info("Heap total: {}MB; max: {}MB", runtime.totalMemory() / mb, runtime.maxMemory() / mb); logger.info("\uD83C\uDF48 Starting Cantaloupe {}", getVersion()); From 5209e91a6535b71f08187b80a2b18b137fc31c3f Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Thu, 7 Jan 2016 21:21:26 -0600 Subject: [PATCH 02/86] Refactoring and doc improvements --- .../processor/ImageIoImageReader.java | 221 +++++++++--------- 1 file changed, 115 insertions(+), 106 deletions(-) diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java index 257a3c7b2..a2deba3eb 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java @@ -13,7 +13,6 @@ import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; -import javax.imageio.stream.ImageInputStream; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.BufferedImage; @@ -29,18 +28,43 @@ */ class ImageIoImageReader { - private static Logger logger = LoggerFactory.getLogger(Java2dUtil.class); + private static Logger logger = LoggerFactory. + getLogger(ImageIoImageReader.class); public enum ReaderHint { ALREADY_CROPPED } /** - * Simple and not necessarily efficient method wrapping - * {@link ImageIO#read}. + * @param inputSource {@link File} or {@link ReadableByteChannel} + * @param sourceFormat Format of the source image + * @return New ImageReader instance with input already set. Should be + * disposed after using. + * @throws IOException + * @throws UnsupportedSourceFormatException + */ + private static ImageReader newImageReader(Object inputSource, + final SourceFormat sourceFormat) + throws IOException, UnsupportedSourceFormatException { + Iterator it = ImageIO.getImageReadersByMIMEType( + sourceFormat.getPreferredMediaType().toString()); + if (it.hasNext()) { + final ImageReader reader = it.next(); + reader.setInput(ImageIO.createImageInputStream(inputSource)); + return reader; + } else { + throw new UnsupportedSourceFormatException(sourceFormat); + } + } + + /** + * Expedient but not necessarily efficient method wrapping + * {@link ImageIO#read} that reads a whole image (excluding subimages) in + * one shot. * * @param readableChannel Image channel to read. - * @return RGB BufferedImage + * @return BufferedImage guaranteed to not be of type + * {@link BufferedImage#TYPE_CUSTOM}. * @throws IOException */ public BufferedImage read(ReadableByteChannel readableChannel) @@ -56,21 +80,22 @@ public BufferedImage read(ReadableByteChannel readableChannel) /** *

Attempts to reads an image as efficiently as possible, utilizing its - * tile layout and/or sub-images, if possible.

- * + * tile layout and/or subimages, if possible.

+ *

*

After reading, clients should check the reader hints to see whether * the returned image will require cropping.

* - * @param imageFile Image file to read. - * @param sourceFormat + * @param imageFile Image file to read. + * @param sourceFormat Format of the source image. * @param ops - * @param fullSize Full size of the source image. + * @param fullSize Full size of the source image. * @param reductionFactor {@link ReductionFactor#factor} property will be * modified to reflect the reduction factor of the * returned image. - * @param hints Will be populated by information returned by the reader. - * @return RGB BufferedImage best matching the given parameters. Clients - * should check the hints set to see whether they need to perform + * @param hints Will be populated by information returned by the reader. + * @return BufferedImage best matching the given parameters, guaranteed to + * not be of {@link BufferedImage#TYPE_CUSTOM}. Clients should + * check the hints set to see whether they need to perform * additional cropping. * @throws IOException * @throws ProcessorException @@ -87,22 +112,22 @@ public BufferedImage read(final File imageFile, } /** - * @see #read(File, SourceFormat, OperationList, Dimension, - * ReductionFactor, Set< ReaderHint >) - * * @param readableChannel Image channel to read. - * @param sourceFormat + * @param sourceFormat Format of the source image. * @param ops - * @param fullSize Full size of the source image. + * @param fullSize Full size of the source image. * @param reductionFactor {@link ReductionFactor#factor} property will be * modified to reflect the reduction factor of the * returned image. - * @param hints Will be populated by information returned by the reader. - * @return RGB BufferedImage best matching the given parameters. Clients - * should check the hints set to see whether they need to perform + * @param hints Will be populated by information returned by the reader. + * @return BufferedImage best matching the given parameters, guaranteed to + * not be of {@link BufferedImage#TYPE_CUSTOM}. Clients should + * check the hints set to see whether they need to perform * additional cropping. * @throws IOException * @throws ProcessorException + * @see #read(File, SourceFormat, OperationList, Dimension, + * ReductionFactor, Set< ReaderHint >) */ public BufferedImage read(final ReadableByteChannel readableChannel, final SourceFormat sourceFormat, @@ -116,21 +141,20 @@ public BufferedImage read(final ReadableByteChannel readableChannel, } /** - * @see #read(File, SourceFormat, OperationList, Dimension, - * ReductionFactor, Set< ReaderHint >) - * - * @param inputSource {@link ReadableByteChannel} or {@link File} - * @param sourceFormat + * @param inputSource {@link ReadableByteChannel} or {@link File} + * @param sourceFormat Format of the source image. * @param ops - * @param fullSize Full size of the source image. - * @param rf {@link ReductionFactor#factor} property will be modified to - * reflect the reduction factor of the returned image. - * @param hints Will be populated by information returned by the reader. - * @return RGB BufferedImage best matching the given parameters. Clients + * @param fullSize Full size of the source image. + * @param rf {@link ReductionFactor#factor} property will be modified to + * reflect the reduction factor of the returned image. + * @param hints Will be populated by information returned by the reader. + * @return BufferedImage best matching the given parameters. Clients * should check the hints set to see whether they need to perform * additional cropping. * @throws IOException * @throws ProcessorException + * @see #read(File, SourceFormat, OperationList, Dimension, + * ReductionFactor, Set< ReaderHint >) */ private BufferedImage multiLevelAwareRead(final Object inputSource, final SourceFormat sourceFormat, @@ -139,65 +163,45 @@ private BufferedImage multiLevelAwareRead(final Object inputSource, final ReductionFactor rf, final Set hints) throws IOException, ProcessorException { + final ImageReader reader = newImageReader(inputSource, sourceFormat); BufferedImage image = null; - switch (sourceFormat) { - case TIF: - Iterator it = ImageIO. - getImageReadersByMIMEType("image/tiff"); - if (it.hasNext()) { - ImageReader reader = it.next(); - try { - Crop crop = new Crop(); - crop.setFull(true); - Scale scale = new Scale(); - scale.setMode(Scale.Mode.FULL); - for (Operation op : ops) { - if (op instanceof Crop) { - crop = (Crop) op; - } else if (op instanceof Scale) { - scale = (Scale) op; - } + try { + switch (sourceFormat) { + case TIF: + Crop crop = new Crop(); + crop.setFull(true); + Scale scale = new Scale(); + scale.setMode(Scale.Mode.FULL); + for (Operation op : ops) { + if (op instanceof Crop) { + crop = (Crop) op; + } else if (op instanceof Scale) { + scale = (Scale) op; } - ImageInputStream iis = - ImageIO.createImageInputStream(inputSource); - reader.setInput(iis); - image = readSmallestUsableSubimage(reader, crop, scale, - rf, hints); - } finally { - reader.dispose(); } - } - break; - // This is similar to the TIF case; the main difference is that it - // doesn't scan for sub-images, which is costly to do. - default: - it = ImageIO.getImageReadersByMIMEType( - sourceFormat.getPreferredMediaType().toString()); - if (it.hasNext()) { - ImageReader reader = it.next(); - try { - ImageInputStream iis = - ImageIO.createImageInputStream(inputSource); - reader.setInput(iis); - - Crop crop = null; - for (Operation op : ops) { - if (op instanceof Crop) { - crop = (Crop) op; - break; - } - } - if (crop != null) { - image = tileAwareRead(reader, 0, - crop.getRectangle(fullSize), hints); - } else { - image = reader.read(0); + image = readSmallestUsableSubimage(reader, crop, scale, + rf, hints); + break; + // This is similar to the TIF case, except it doesn't scan for + // subimages, which is costly to do. + default: + crop = null; + for (Operation op : ops) { + if (op instanceof Crop) { + crop = (Crop) op; + break; } - } finally { - reader.dispose(); } - } - break; + if (crop != null) { + image = tileAwareRead(reader, 0, + crop.getRectangle(fullSize), hints); + } else { + image = reader.read(0); + } + break; + } + } finally { + reader.dispose(); } if (image == null) { throw new UnsupportedSourceFormatException(sourceFormat); @@ -211,14 +215,15 @@ private BufferedImage multiLevelAwareRead(final Object inputSource, } /** - * Reads the smallest usable image from a multi-resolution image. + * Reads the smallest image that can fulfill the given crop and scale from + * a multi-resolution image. * * @param reader ImageReader with input source already set - * @param crop Requested crop - * @param scale Requested scale - * @param rf {@link ReductionFactor#factor} will be set to the reduction - * factor of the returned image. - * @param hints Will be populated by information returned by the reader. + * @param crop Requested crop + * @param scale Requested scale + * @param rf {@link ReductionFactor#factor} will be set to the reduction + * factor of the returned image. + * @param hints Will be populated by information returned by the reader. * @return The smallest image fitting the requested crop and scale * operations from the given reader. * @throws IOException @@ -229,11 +234,11 @@ private BufferedImage readSmallestUsableSubimage(final ImageReader reader, final ReductionFactor rf, final Set hints) throws IOException { - BufferedImage bestImage = null; - final Dimension fullSize = new Dimension(reader.getWidth(0), - reader.getHeight(0)); + final Dimension fullSize = new Dimension( + reader.getWidth(0), reader.getHeight(0)); final Rectangle regionRect = crop.getRectangle(fullSize); final ImageReadParam param = reader.getDefaultReadParam(); + BufferedImage bestImage = null; if (scale.isNoOp()) { // ImageReader loves to read TIFFs into BufferedImages of type // TYPE_CUSTOM, which need to be redrawn into a new image of type @@ -317,21 +322,25 @@ private BufferedImage readSmallestUsableSubimage(final ImageReader reader, /** *

Returns an image for the requested source area by reading the tiles - * of the source image and joining them into a single image.

- * + * (or strips) of the source image and joining them into a single image.

+ *

+ *

This method is intended to be compatible with all source images, no + * matter the data layout (tiled or not). For some image types, including + * GIF and PNG, a tiled reading strategy will not work, and so this method + * will read the entire image.

+ *

*

This method may populate hints with * {@link ReaderHint#ALREADY_CROPPED}, in which case cropping will have * already been performed according to the - * requestedSourceArea parameter and no further cropping will - * be necessary (assuming it would have covered the same area).

+ * requestedSourceArea parameter.

* - * @param reader ImageReader with input source already set - * @param imageIndex Index of the image to read from the ImageReader. + * @param reader ImageReader with input source already set + * @param imageIndex Index of the image to read from the ImageReader. * @param requestedSourceArea Source image area to retrieve. The returned * image will be this size or smaller if it * would overlap the right or bottom edge of the * source image. - * @param hints Will be populated by information returned by the reader. + * @param hints Will be populated by information returned by the reader. * @return Cropped image * @throws IOException * @throws IllegalArgumentException If the source image is not tiled. @@ -342,11 +351,11 @@ private BufferedImage tileAwareRead(final ImageReader reader, final Set hints) throws IOException, IllegalArgumentException { - // These readers are uncooperative with the tile-aware loading + // These formats are uncooperative with the tile-aware loading // strategy. if (reader instanceof GIFImageReader || reader instanceof PNGImageReader) { - logger.debug("Reading full image {}", imageIndex); + logger.debug("tileAwareRead(): reading full image {}", imageIndex); return reader.read(imageIndex); } @@ -370,10 +379,10 @@ private BufferedImage tileAwareRead(final ImageReader reader, final int offsetY = requestedSourceArea.y - tileY1 * tileHeight; if (tileHeight == 1) { - logger.debug("Reading {}-column strip", numYTiles); + logger.debug("tileAwareRead(): reading {}-column strip", numYTiles); } else { - logger.debug("Reading tile rows {}-{} of {} and columns {}-{} of {} " + - "({}x{} tiles; {}x{} offset)", + logger.debug("tileAwareRead(): reading tile rows {}-{} of {} and " + + "columns {}-{} of {} ({}x{} tiles; {}x{} offset)", tileX1 + 1, tileX2 + 1, numXTiles, tileY1 + 1, tileY2 + 1, numYTiles, tileWidth, tileHeight, offsetX, offsetY); From 872d00979210e36a8cce9350b5c36277476d0a14 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Thu, 7 Jan 2016 11:17:54 -0600 Subject: [PATCH 03/86] ChannelResolvers return a ChannelSource instead of a ReadableByteChannel in order to facilitate multi-threaded reading --- .../processor/ChannelProcessor.java | 18 ++-- .../processor/GraphicsMagickProcessor.java | 74 ++++++--------- .../processor/ImageIoImageReader.java | 18 ++-- .../processor/ImageMagickProcessor.java | 16 +++- .../cantaloupe/processor/JaiProcessor.java | 11 +-- .../cantaloupe/processor/Java2dProcessor.java | 5 +- .../cantaloupe/processor/ProcessorUtil.java | 22 +++-- .../cantaloupe/resolver/AmazonS3Resolver.java | 20 ++++- .../cantaloupe/resolver/ChannelResolver.java | 5 +- .../cantaloupe/resolver/ChannelSource.java | 17 ++++ .../resolver/FilesystemResolver.java | 19 +++- .../cantaloupe/resolver/HttpResolver.java | 89 +++++++++++-------- .../cantaloupe/resolver/JdbcResolver.java | 25 +++++- .../cantaloupe/resource/AbstractResource.java | 24 ++--- .../resource/ImageRepresentation.java | 69 +++++++------- .../cantaloupe/processor/ProcessorTest.java | 26 +++--- .../processor/TestChannelSource.java | 23 +++++ .../resolver/AmazonS3ResolverTest.java | 9 +- .../resolver/FilesystemResolverTest.java | 5 +- .../cantaloupe/resolver/HttpResolverTest.java | 6 +- .../cantaloupe/resolver/JdbcResolverTest.java | 4 +- 21 files changed, 304 insertions(+), 201 deletions(-) create mode 100644 src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelSource.java create mode 100644 src/test/java/edu/illinois/library/cantaloupe/processor/TestChannelSource.java diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ChannelProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ChannelProcessor.java index e48f9bdf4..9853a2674 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ChannelProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ChannelProcessor.java @@ -2,6 +2,7 @@ import edu.illinois.library.cantaloupe.image.SourceFormat; import edu.illinois.library.cantaloupe.image.OperationList; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import java.awt.Dimension; import java.nio.channels.ReadableByteChannel; @@ -9,12 +10,13 @@ /** * Interface to be implemented by image processors that support input via - * streams. + * channels. */ public interface ChannelProcessor extends Processor { /** - * @param readableChannel Source image. Implementations should not close it. + * @param readableChannel Channel for reading the source image. + * Implementations should close it. * @param sourceFormat Format of the source image * @return Scale of the source image in pixels. * @throws ProcessorException @@ -32,16 +34,16 @@ Dimension getSize(ReadableByteChannel readableChannel, * ({@link edu.illinois.library.cantaloupe.image.Operation#isNoOp()}) * before performing it.

* - *

Implementations should use the sourceSize parameter and not their - * own {#link #getSize} method to avoid reusing a potentially unreusable - * InputStream.

+ *

Implementations should get the full size of the source image from + * the sourceSize parameter instead of their {#link #getSize} method, + * for efficiency.

* * @param ops OperationList of the image to process. * @param sourceFormat Format of the source image. Will never be * {@link SourceFormat#UNKNOWN}. * @param sourceSize Scale of the source image. - * @param readableChannel Stream from which to read the image. - * Implementations should not close it. + * @param channelSource Source for acquiring channels from which to read + * the imagee. * @param writableChannel Writable channel to write the image to. * Implementations should not close it. * @throws UnsupportedOutputFormatException @@ -49,7 +51,7 @@ Dimension getSize(ReadableByteChannel readableChannel, * @throws ProcessorException */ void process(OperationList ops, SourceFormat sourceFormat, - Dimension sourceSize, ReadableByteChannel readableChannel, + Dimension sourceSize, ChannelSource channelSource, WritableByteChannel writableChannel) throws ProcessorException; } diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/GraphicsMagickProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/GraphicsMagickProcessor.java index 00fa6e9b8..2f63b31a9 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/GraphicsMagickProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/GraphicsMagickProcessor.java @@ -10,6 +10,7 @@ import edu.illinois.library.cantaloupe.image.SourceFormat; import edu.illinois.library.cantaloupe.image.OutputFormat; import edu.illinois.library.cantaloupe.image.Transpose; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.resource.iiif.ProcessorFeature; import org.apache.commons.configuration.Configuration; import org.im4java.core.ConvertCmd; @@ -167,17 +168,28 @@ public Set getAvailableOutputFormats(SourceFormat sourceFormat) { return formats; } - public Dimension getSize(File file, SourceFormat sourceFormat) - throws ProcessorException { - return doGetSize(file.getAbsolutePath(), null, sourceFormat); - } - @Override - public Dimension getSize(ReadableByteChannel readableChannel, - SourceFormat sourceFormat) + public Dimension getSize(final ReadableByteChannel readableChannel, + final SourceFormat sourceFormat) throws ProcessorException { - return doGetSize(sourceFormat.getPreferredExtension() + ":-", - readableChannel, sourceFormat); + if (getAvailableOutputFormats(sourceFormat).size() < 1) { + throw new UnsupportedSourceFormatException(sourceFormat); + } + try { + Info sourceInfo = new Info( + sourceFormat.getPreferredExtension() + ":-", + Channels.newInputStream(readableChannel), true); + return new Dimension(sourceInfo.getImageWidth(), + sourceInfo.getImageHeight()); + } catch (IM4JavaException e) { + throw new ProcessorException(e.getMessage(), e); + } finally { + try { + readableChannel.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } } @Override @@ -216,10 +228,10 @@ public Set getSupportedFeatures( public void process(final OperationList ops, final SourceFormat sourceFormat, final Dimension fullSize, - final ReadableByteChannel readableChannel, + final ChannelSource channelSource, final WritableByteChannel writableChannel) throws ProcessorException { - doProcess(sourceFormat.getPreferredExtension() + ":-", readableChannel, + doProcess(sourceFormat.getPreferredExtension() + ":-", channelSource, ops, sourceFormat, fullSize, writableChannel); } @@ -289,36 +301,7 @@ private void assembleOperation(IMOperation imOp, OperationList ops, /** * @param inputPath Absolute filename pathname or "-" to use a stream - * @param readableChannel Can be null - * @param sourceFormat - * @return - * @throws ProcessorException - */ - private Dimension doGetSize(final String inputPath, - final ReadableByteChannel readableChannel, - final SourceFormat sourceFormat) - throws ProcessorException { - if (getAvailableOutputFormats(sourceFormat).size() < 1) { - throw new UnsupportedSourceFormatException(sourceFormat); - } - try { - Info sourceInfo; - if (readableChannel != null) { - sourceInfo = new Info(inputPath, - Channels.newInputStream(readableChannel), true); - } else { - sourceInfo = new Info(inputPath, true); - } - return new Dimension(sourceInfo.getImageWidth(), - sourceInfo.getImageHeight()); - } catch (IM4JavaException e) { - throw new ProcessorException(e.getMessage(), e); - } - } - - /** - * @param inputPath Absolute filename pathname or "-" to use a stream - * @param readableChannel Can be null + * @param channelSource * @param ops * @param sourceFormat * @param fullSize @@ -326,7 +309,7 @@ private Dimension doGetSize(final String inputPath, * @throws ProcessorException */ private void doProcess(final String inputPath, - final ReadableByteChannel readableChannel, + final ChannelSource channelSource, final OperationList ops, final SourceFormat sourceFormat, final Dimension fullSize, @@ -354,10 +337,9 @@ private void doProcess(final String inputPath, if (binaryPath.length() > 0) { convert.setSearchPath(binaryPath); } - if (readableChannel != null) { - convert.setInputProvider(new Pipe( - Channels.newInputStream(readableChannel), null)); - } + + convert.setInputProvider(new Pipe( + Channels.newInputStream(channelSource.newChannel()), null)); convert.setOutputConsumer( new Pipe(null, Channels.newOutputStream(writableChannel))); convert.run(op); diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java index a2deba3eb..3ee80195f 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java @@ -7,6 +7,7 @@ import edu.illinois.library.cantaloupe.image.OperationList; import edu.illinois.library.cantaloupe.image.Scale; import edu.illinois.library.cantaloupe.image.SourceFormat; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -112,14 +113,17 @@ public BufferedImage read(final File imageFile, } /** - * @param readableChannel Image channel to read. - * @param sourceFormat Format of the source image. + * @see #read(File, SourceFormat, OperationList, Dimension, + * ReductionFactor, Set< ReaderHint >) + * + * @param channelSource Source of image channels to read + * @param sourceFormat Format of the source image * @param ops - * @param fullSize Full size of the source image. + * @param fullSize Full size of the source image. * @param reductionFactor {@link ReductionFactor#factor} property will be * modified to reflect the reduction factor of the * returned image. - * @param hints Will be populated by information returned by the reader. + * @param hints Will be populated by information returned by the reader. * @return BufferedImage best matching the given parameters, guaranteed to * not be of {@link BufferedImage#TYPE_CUSTOM}. Clients should * check the hints set to see whether they need to perform @@ -129,15 +133,15 @@ public BufferedImage read(final File imageFile, * @see #read(File, SourceFormat, OperationList, Dimension, * ReductionFactor, Set< ReaderHint >) */ - public BufferedImage read(final ReadableByteChannel readableChannel, + public BufferedImage read(final ChannelSource channelSource, final SourceFormat sourceFormat, final OperationList ops, final Dimension fullSize, final ReductionFactor reductionFactor, final Set hints) throws IOException, ProcessorException { - return multiLevelAwareRead(readableChannel, sourceFormat, ops, - fullSize, reductionFactor, hints); + return multiLevelAwareRead(channelSource.newChannel(), sourceFormat, + ops, fullSize, reductionFactor, hints); } /** diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageMagickProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageMagickProcessor.java index 0895afdf0..933d4c167 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageMagickProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageMagickProcessor.java @@ -10,6 +10,7 @@ import edu.illinois.library.cantaloupe.image.Crop; import edu.illinois.library.cantaloupe.image.Rotate; import edu.illinois.library.cantaloupe.image.Transpose; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.resource.iiif.ProcessorFeature; import org.apache.commons.configuration.Configuration; import org.im4java.core.ConvertCmd; @@ -173,12 +174,19 @@ public Dimension getSize(final ReadableByteChannel readableChannel, throw new UnsupportedSourceFormatException(sourceFormat); } try { - Info sourceInfo = new Info(sourceFormat.getPreferredExtension() + - ":-", Channels.newInputStream(readableChannel), true); + Info sourceInfo = new Info( + sourceFormat.getPreferredExtension() + ":-", + Channels.newInputStream(readableChannel), true); return new Dimension(sourceInfo.getImageWidth(), sourceInfo.getImageHeight()); } catch (IM4JavaException e) { throw new ProcessorException(e.getMessage(), e); + } finally { + try { + readableChannel.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } } } @@ -218,7 +226,7 @@ public Set getSupportedFeatures( public void process(final OperationList ops, final SourceFormat sourceFormat, final Dimension fullSize, - final ReadableByteChannel readableChannel, + final ChannelSource channelSource, final WritableByteChannel writableChannel) throws ProcessorException { final Set availableOutputFormats = @@ -238,7 +246,7 @@ public void process(final OperationList ops, op.addImage(ops.getOutputFormat().getExtension() + ":-"); // write to stdout Pipe pipeIn = new Pipe( - Channels.newInputStream(readableChannel), null); + Channels.newInputStream(channelSource.newChannel()), null); Pipe pipeOut = new Pipe(null, Channels.newOutputStream(writableChannel)); diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java index 75e5f3bff..4a12eb3e9 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java @@ -9,6 +9,7 @@ import edu.illinois.library.cantaloupe.image.OutputFormat; import edu.illinois.library.cantaloupe.image.Crop; import edu.illinois.library.cantaloupe.image.Transpose; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.resource.iiif.ProcessorFeature; import it.geosolutions.jaiext.JAIExt; import org.restlet.data.MediaType; @@ -175,10 +176,10 @@ public void process(final OperationList ops, public void process(final OperationList ops, final SourceFormat sourceFormat, final Dimension fullSize, - final ReadableByteChannel readableChannel, + final ChannelSource channelSource, final WritableByteChannel writableChannel) throws ProcessorException { - doProcess(ops, sourceFormat, fullSize, readableChannel, writableChannel); + doProcess(ops, sourceFormat, fullSize, channelSource, writableChannel); } private void doProcess(final OperationList ops, @@ -198,9 +199,9 @@ private void doProcess(final OperationList ops, try { RenderedImage renderedImage = null; ReductionFactor rf = new ReductionFactor(); - if (input instanceof ReadableByteChannel) { - renderedImage = JaiUtil.readImage( - (ReadableByteChannel) input, sourceFormat, ops, + if (input instanceof ChannelSource) { + ReadableByteChannel channel = ((ChannelSource) input).newChannel(); + renderedImage = JaiUtil.readImage(channel, sourceFormat, ops, fullSize, rf); } else if (input instanceof File) { renderedImage = JaiUtil.readImage( diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java index d4180ad10..b86de52cb 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java @@ -10,6 +10,7 @@ import edu.illinois.library.cantaloupe.image.SourceFormat; import edu.illinois.library.cantaloupe.image.OutputFormat; import edu.illinois.library.cantaloupe.image.Transpose; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.resource.iiif.ProcessorFeature; import org.restlet.data.MediaType; @@ -211,7 +212,7 @@ public void process(final OperationList ops, public void process(final OperationList ops, final SourceFormat sourceFormat, final Dimension fullSize, - final ReadableByteChannel readableChannel, + final ChannelSource channelSource, final WritableByteChannel writableChannel) throws ProcessorException { final Set availableOutputFormats = @@ -226,7 +227,7 @@ public void process(final OperationList ops, final ReductionFactor reductionFactor = new ReductionFactor(); final Set readerHints = new HashSet<>(); final ImageIoImageReader reader = new ImageIoImageReader(); - BufferedImage image = reader.read(readableChannel, + BufferedImage image = reader.read(channelSource, sourceFormat, ops, fullSize, reductionFactor, readerHints); for (Operation op : ops) { diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ProcessorUtil.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ProcessorUtil.java index 07d964725..a3601dea1 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ProcessorUtil.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ProcessorUtil.java @@ -2,6 +2,8 @@ import edu.illinois.library.cantaloupe.image.OutputFormat; import edu.illinois.library.cantaloupe.image.SourceFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.imageio.ImageReader; @@ -15,6 +17,8 @@ abstract class ProcessorUtil { + private static Logger logger = LoggerFactory.getLogger(ProcessorUtil.class); + /** * Gets a reduction factor where the corresponding scale is 1/(2^rf). * @@ -51,8 +55,7 @@ public static double getScale(ReductionFactor reductionFactor) { } /** - * Efficiently reads the width & height of an image without reading the - * entire image into memory. + * Efficiently reads the width & height of an image. * * @param inputFile * @param sourceFormat @@ -65,10 +68,9 @@ public static Dimension getSize(File inputFile, SourceFormat sourceFormat) } /** - * Efficiently reads the width & height of an image without reading the - * entire image into memory. + * Efficiently reads the width & height of an image. * - * @param readableChannel + * @param readableChannel Will be closed. * @param sourceFormat * @return Dimensions in pixels * @throws ProcessorException @@ -76,7 +78,15 @@ public static Dimension getSize(File inputFile, SourceFormat sourceFormat) public static Dimension getSize(ReadableByteChannel readableChannel, SourceFormat sourceFormat) throws ProcessorException { - return doGetSize(readableChannel, sourceFormat); + try { + return doGetSize(readableChannel, sourceFormat); + } finally { + try { + readableChannel.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } } /** diff --git a/src/main/java/edu/illinois/library/cantaloupe/resolver/AmazonS3Resolver.java b/src/main/java/edu/illinois/library/cantaloupe/resolver/AmazonS3Resolver.java index aff0210df..f7fb07e0f 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resolver/AmazonS3Resolver.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resolver/AmazonS3Resolver.java @@ -30,6 +30,21 @@ */ class AmazonS3Resolver extends AbstractResolver implements ChannelResolver { + private static class AmazonS3ChannelSource implements ChannelSource { + + private final S3Object object; + + public AmazonS3ChannelSource(S3Object object) { + this.object = object; + } + + @Override + public ReadableByteChannel newChannel() throws IOException { + return Channels.newChannel(object.getObjectContent()); + } + + } + private static Logger logger = LoggerFactory. getLogger(AmazonS3Resolver.class); @@ -87,10 +102,9 @@ public String getAWSSecretKey() { } @Override - public ReadableByteChannel getChannel(Identifier identifier) + public ChannelSource getChannelSource(Identifier identifier) throws IOException { - final S3Object object = getObject(identifier); - return Channels.newChannel(object.getObjectContent()); + return new AmazonS3ChannelSource(getObject(identifier)); } private S3Object getObject(Identifier identifier) throws IOException { diff --git a/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelResolver.java b/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelResolver.java index 4355ad2bf..10be2026a 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelResolver.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelResolver.java @@ -4,7 +4,6 @@ import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.channels.ReadableByteChannel; import java.nio.file.AccessDeniedException; /** @@ -15,13 +14,13 @@ public interface ChannelResolver extends Resolver { /** * @param identifier - * @return Channel for reading the source image; never null. + * @return ChannelSource for reading the source image; never null. * @throws FileNotFoundException if the image corresponding to the given * identifier does not exist * @throws AccessDeniedException if the image corresponding to the given * identifier is not readable * @throws IOException if there is some other issue accessing the image */ - ReadableByteChannel getChannel(Identifier identifier) throws IOException; + ChannelSource getChannelSource(Identifier identifier) throws IOException; } diff --git a/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelSource.java b/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelSource.java new file mode 100644 index 000000000..49558651d --- /dev/null +++ b/src/main/java/edu/illinois/library/cantaloupe/resolver/ChannelSource.java @@ -0,0 +1,17 @@ +package edu.illinois.library.cantaloupe.resolver; + +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; + +/** + * Provides new channels to read from. + */ +public interface ChannelSource { + + /** + * @return New channel to read from. + * @throws IOException + */ + ReadableByteChannel newChannel() throws IOException; + +} diff --git a/src/main/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolver.java b/src/main/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolver.java index bb5cb3e77..f31a8b256 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolver.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolver.java @@ -24,6 +24,21 @@ class FilesystemResolver extends AbstractResolver implements ChannelResolver, FileResolver { + private static class FilesystemChannelSource implements ChannelSource { + + private final File file; + + public FilesystemChannelSource(File file) { + this.file = file; + } + + @Override + public ReadableByteChannel newChannel() throws IOException { + return new FileInputStream(file).getChannel(); + } + + } + private static Logger logger = LoggerFactory. getLogger(FilesystemResolver.class); @@ -40,9 +55,9 @@ class FilesystemResolver extends AbstractResolver } @Override - public ReadableByteChannel getChannel(Identifier identifier) + public ChannelSource getChannelSource(Identifier identifier) throws IOException { - return new FileInputStream(getFile(identifier)).getChannel(); + return new FilesystemChannelSource(getFile(identifier)); } @Override diff --git a/src/main/java/edu/illinois/library/cantaloupe/resolver/HttpResolver.java b/src/main/java/edu/illinois/library/cantaloupe/resolver/HttpResolver.java index 354955e62..b8aa7a00d 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resolver/HttpResolver.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resolver/HttpResolver.java @@ -23,11 +23,33 @@ import java.io.IOException; import java.nio.channels.ReadableByteChannel; import java.nio.file.AccessDeniedException; -import java.util.ArrayList; -import java.util.List; +import java.util.Arrays; class HttpResolver extends AbstractResolver implements ChannelResolver { + private static class HttpChannelSource implements ChannelSource { + + private final Client client = new Client( + Arrays.asList(Protocol.HTTP, Protocol.HTTPS)); + private final Reference url; + + public HttpChannelSource(Reference url) { + this.url = url; + } + + @Override + public ReadableByteChannel newChannel() throws IOException { + ClientResource resource = newClientResource(url); + resource.setNext(client); + try { + return resource.get().getChannel(); + } catch (ResourceException e) { + throw new IOException(e.getMessage(), e); + } + } + + } + private static Logger logger = LoggerFactory.getLogger(HttpResolver.class); public static final String BASIC_AUTH_SECRET_CONFIG_KEY = @@ -41,24 +63,38 @@ class HttpResolver extends AbstractResolver implements ChannelResolver { public static final String URL_SUFFIX_CONFIG_KEY = "HttpResolver.BasicLookupStrategy.url_suffix"; - private static Client client; - - static { - List protocols = new ArrayList<>(); - protocols.add(Protocol.HTTP); - protocols.add(Protocol.HTTPS); - client = new Client(protocols); + /** + * Factory method. + * + * @param url + * @return New ClientResource respecting HttpResolver configuration + * options. + */ + private static ClientResource newClientResource(final Reference url) { + final ClientResource resource = new ClientResource(url); + final Configuration config = Application.getConfiguration(); + final String username = config.getString(BASIC_AUTH_USERNAME_CONFIG_KEY, ""); + final String secret = config.getString(BASIC_AUTH_SECRET_CONFIG_KEY, ""); + if (username.length() > 0 && secret.length() > 0) { + resource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, + username, secret); + } + return resource; } @Override - public ReadableByteChannel getChannel(Identifier identifier) + public ChannelSource getChannelSource(final Identifier identifier) throws IOException { Reference url = getUrl(identifier); logger.debug("Resolved {} to {}", identifier, url); - ClientResource resource = newClientResource(url); - resource.setNext(client); try { - return resource.get().getChannel(); + // Issue an HTTP HEAD request to check whether the underlying + // resource is accessible + Client client = new Client(new Context(), url.getSchemeProtocol()); + ClientResource resource = new ClientResource(url); + resource.setNext(client); + resource.head(); + return new HttpChannelSource(url); } catch (ResourceException e) { if (e.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND) || e.getStatus().equals(Status.CLIENT_ERROR_GONE)) { @@ -66,7 +102,7 @@ public ReadableByteChannel getChannel(Identifier identifier) } else if (e.getStatus().equals(Status.CLIENT_ERROR_FORBIDDEN)) { throw new AccessDeniedException(e.getMessage()); } else { - throw new IOException(e.getMessage()); + throw new IOException(e.getMessage(), e); } } } @@ -78,11 +114,11 @@ public SourceFormat getSourceFormat(final Identifier identifier) if (format == SourceFormat.UNKNOWN) { format = getSourceFormatFromContentTypeHeader(identifier); } - getChannel(identifier); // throws IOException if not found etc. + getChannelSource(identifier).newChannel(); // throws IOException if not found etc. return format; } - public Reference getUrl(Identifier identifier) throws IOException { + public Reference getUrl(final Identifier identifier) throws IOException { final Configuration config = Application.getConfiguration(); switch (config.getString(LOOKUP_STRATEGY_CONFIG_KEY)) { @@ -115,7 +151,7 @@ private SourceFormat getSourceFormatFromContentTypeHeader(Identifier identifier) String contentType = ""; Reference url = getUrl(identifier); try { - Client client = new Client(new Context(), Protocol.HTTP); + Client client = new Client(new Context(), url.getSchemeProtocol()); ClientResource resource = new ClientResource(url); resource.setNext(client); resource.head(); @@ -169,23 +205,4 @@ private Reference getUrlWithScriptStrategy(Identifier identifier) return new Reference((String) result); } - /** - * Factory method. - * - * @param url - * @return New ClientResource respecting HttpResolver configuration - * options. - */ - private ClientResource newClientResource(final Reference url) { - final ClientResource resource = new ClientResource(url); - final Configuration config = Application.getConfiguration(); - final String username = config.getString(BASIC_AUTH_USERNAME_CONFIG_KEY, ""); - final String secret = config.getString(BASIC_AUTH_SECRET_CONFIG_KEY, ""); - if (username.length() > 0 && secret.length() > 0) { - resource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, - username, secret); - } - return resource; - } - } diff --git a/src/main/java/edu/illinois/library/cantaloupe/resolver/JdbcResolver.java b/src/main/java/edu/illinois/library/cantaloupe/resolver/JdbcResolver.java index 57cad2db6..8b13e319b 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resolver/JdbcResolver.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resolver/JdbcResolver.java @@ -23,6 +23,27 @@ class JdbcResolver extends AbstractResolver implements ChannelResolver { + private static class JdbcChannelSource implements ChannelSource { + + private final int column; + private final ResultSet resultSet; + + public JdbcChannelSource(ResultSet resultSet, int column) { + this.resultSet = resultSet; + this.column = column; + } + + @Override + public ReadableByteChannel newChannel() throws IOException { + try { + return Channels.newChannel(resultSet.getBinaryStream(column)); + } catch (SQLException e) { + throw new IOException(e.getMessage(), e); + } + } + + } + private static Logger logger = LoggerFactory.getLogger(JdbcResolver.class); public static final String CONNECTION_TIMEOUT_CONFIG_KEY = @@ -81,7 +102,7 @@ public static synchronized Connection getConnection() throws SQLException { } @Override - public ReadableByteChannel getChannel(Identifier identifier) + public ChannelSource getChannelSource(Identifier identifier) throws IOException { try (Connection connection = getConnection()) { Configuration config = Application.getConfiguration(); @@ -96,7 +117,7 @@ public ReadableByteChannel getChannel(Identifier identifier) statement.setString(1, executeGetDatabaseIdentifier(identifier)); ResultSet result = statement.executeQuery(); if (result.next()) { - return Channels.newChannel(result.getBinaryStream(1)); + return new JdbcChannelSource(result, 1); } } catch (ScriptException | SQLException e) { throw new IOException(e.getMessage(), e); diff --git a/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java b/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java index 2e7f72642..9f47ffbbd 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java @@ -12,6 +12,7 @@ import edu.illinois.library.cantaloupe.processor.ProcessorException; import edu.illinois.library.cantaloupe.processor.ChannelProcessor; import edu.illinois.library.cantaloupe.resolver.ChannelResolver; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.resolver.FileResolver; import edu.illinois.library.cantaloupe.resolver.Resolver; import org.apache.commons.configuration.Configuration; @@ -165,8 +166,9 @@ protected void checkProcessorResolverCompatibility(Resolver resolver, * This method enables the use of an alternate string to represent a slash * via {@link #SLASH_SUBSTITUTE_CONFIG_KEY}. * - * @param uriPathComponent - * @return + * @param uriPathComponent Path component (a part of the path before, + * after, or between slashes) + * @return Path component with slashes decoded */ protected String decodeSlashes(final String uriPathComponent) { final String substitute = Application.getConfiguration(). @@ -257,22 +259,20 @@ protected ImageRepresentation getRepresentation(OperationList ops, final ChannelResolver chRes = (ChannelResolver) resolver; if (proc instanceof ChannelProcessor) { final ChannelProcessor sproc = (ChannelProcessor) proc; - ReadableByteChannel readableChannel = chRes. - getChannel(ops.getIdentifier()); - final Dimension fullSize = sproc.getSize(readableChannel, - sourceFormat); + final ChannelSource channelSource = chRes. + getChannelSource(ops.getIdentifier()); + final Dimension fullSize = sproc.getSize( + channelSource.newChannel(), sourceFormat); final Dimension effectiveSize = ops.getResultingSize(fullSize); if (maxAllowedSize > 0 && effectiveSize.width * effectiveSize.height > maxAllowedSize) { throw new PayloadTooLargeException(); } - // avoid reusing the channel - readableChannel = chRes.getChannel(ops.getIdentifier()); return new ImageRepresentation(mediaType, sourceFormat, - fullSize, ops, disposition, readableChannel); + fullSize, ops, disposition, channelSource); } } - return null; // should never happen + return null; // should never hit } /** @@ -326,7 +326,7 @@ protected Dimension readSize(Identifier identifier, Resolver resolver, sourceFormat); } else if (proc instanceof ChannelProcessor) { size = ((ChannelProcessor) proc).getSize( - ((ChannelResolver) resolver).getChannel(identifier), + ((ChannelResolver) resolver).getChannelSource(identifier).newChannel(), sourceFormat); } } else if (resolver instanceof ChannelResolver) { @@ -334,7 +334,7 @@ protected Dimension readSize(Identifier identifier, Resolver resolver, // ChannelResolvers and FileProcessors are incompatible } else { size = ((ChannelProcessor) proc).getSize( - ((ChannelResolver) resolver).getChannel(identifier), + ((ChannelResolver) resolver).getChannelSource(identifier).newChannel(), sourceFormat); } } diff --git a/src/main/java/edu/illinois/library/cantaloupe/resource/ImageRepresentation.java b/src/main/java/edu/illinois/library/cantaloupe/resource/ImageRepresentation.java index bee158fff..9a69c43db 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resource/ImageRepresentation.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resource/ImageRepresentation.java @@ -8,6 +8,7 @@ import edu.illinois.library.cantaloupe.processor.FileProcessor; import edu.illinois.library.cantaloupe.processor.Processor; import edu.illinois.library.cantaloupe.processor.ProcessorFactory; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.util.IOUtils; import edu.illinois.library.cantaloupe.util.TeeWritableByteChannel; import org.restlet.data.Disposition; @@ -35,7 +36,7 @@ public class ImageRepresentation extends WritableRepresentation { private File file; private Dimension fullSize; - private ReadableByteChannel readableChannel; + private ChannelSource channelSource; private OperationList ops; private SourceFormat sourceFormat; @@ -47,16 +48,16 @@ public class ImageRepresentation extends WritableRepresentation { * @param fullSize * @param ops * @param disposition - * @param readableChannel + * @param channelSource */ public ImageRepresentation(final MediaType mediaType, final SourceFormat sourceFormat, final Dimension fullSize, final OperationList ops, final Disposition disposition, - final ReadableByteChannel readableChannel) { + final ChannelSource channelSource) { super(mediaType); - this.readableChannel = readableChannel; + this.channelSource = channelSource; this.ops = ops; this.sourceFormat = sourceFormat; this.fullSize = fullSize; @@ -96,42 +97,32 @@ public ImageRepresentation(MediaType mediaType, @Override public void write(WritableByteChannel writableChannel) throws IOException { Cache cache = CacheFactory.getInstance(); - try { - if (cache != null) { - WritableByteChannel cacheWritableChannel = null; - try (ReadableByteChannel cacheReadableChannel = - cache.getImageReadableChannel(this.ops)) { - if (cacheReadableChannel != null) { - // a cached image is available; write it to the - // response output stream. - IOUtils.copy(cacheReadableChannel, writableChannel); - } else { - // create a TeeOutputStream to write to both the - // response output stream and the cache simultaneously. - cacheWritableChannel = cache.getImageWritableChannel(this.ops); - TeeWritableByteChannel teeChannel = new TeeWritableByteChannel( - writableChannel, cacheWritableChannel); - doCacheAwareWrite(teeChannel, cache); - } - } catch (Exception e) { - throw new IOException(e); - } finally { - if (cacheWritableChannel != null && - cacheWritableChannel.isOpen()) { - cacheWritableChannel.close(); - } + if (cache != null) { + WritableByteChannel cacheWritableChannel = null; + try (ReadableByteChannel cacheReadableChannel = + cache.getImageReadableChannel(this.ops)) { + if (cacheReadableChannel != null) { + // a cached image is available; write it to the + // response output stream. + IOUtils.copy(cacheReadableChannel, writableChannel); + } else { + // create a TeeOutputStream to write to both the + // response output stream and the cache simultaneously. + cacheWritableChannel = cache.getImageWritableChannel(this.ops); + TeeWritableByteChannel teeChannel = new TeeWritableByteChannel( + writableChannel, cacheWritableChannel); + doCacheAwareWrite(teeChannel, cache); } - } else { - doWrite(writableChannel); - } - } finally { - try { - if (readableChannel != null && readableChannel.isOpen()) { - readableChannel.close(); + } catch (Exception e) { + throw new IOException(e); + } finally { + if (cacheWritableChannel != null && + cacheWritableChannel.isOpen()) { + cacheWritableChannel.close(); } - } catch (IOException e) { - logger.error(e.getMessage(), e); } + } else { + doWrite(writableChannel); } } @@ -163,7 +154,7 @@ private void doWrite(WritableByteChannel writableChannel) throws IOException { IOUtils.copy(new FileInputStream(this.file).getChannel(), writableChannel); } else { - IOUtils.copy(readableChannel, writableChannel); + IOUtils.copy(channelSource.newChannel(), writableChannel); } logger.info("Streamed with no processing in {} msec: {}", System.currentTimeMillis() - msec, ops); @@ -177,7 +168,7 @@ private void doWrite(WritableByteChannel writableChannel) throws IOException { } else { ChannelProcessor sproc = (ChannelProcessor) proc; sproc.process(this.ops, this.sourceFormat, - this.fullSize, readableChannel, writableChannel); + this.fullSize, channelSource, writableChannel); } logger.info("{} processed in {} msec: {}", proc.getClass().getSimpleName(), diff --git a/src/test/java/edu/illinois/library/cantaloupe/processor/ProcessorTest.java b/src/test/java/edu/illinois/library/cantaloupe/processor/ProcessorTest.java index 211fdcbcd..9281d25df 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/processor/ProcessorTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/processor/ProcessorTest.java @@ -11,6 +11,7 @@ import edu.illinois.library.cantaloupe.image.SourceFormat; import edu.illinois.library.cantaloupe.image.OutputFormat; import edu.illinois.library.cantaloupe.image.Transpose; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.test.TestUtil; import org.apache.commons.configuration.BaseConfiguration; @@ -111,16 +112,16 @@ public void testProcessWithUnsupportedSourceFormats() throws Exception { for (SourceFormat sourceFormat : SourceFormat.values()) { if (getProcessor().getAvailableOutputFormats(sourceFormat).size() == 0) { if (getProcessor() instanceof ChannelProcessor) { - ReadableByteChannel sizeReadableChannel = new FileInputStream( - TestUtil.getFixture(sourceFormat.getPreferredExtension())).getChannel(); - ReadableByteChannel processReadableChannel = new FileInputStream( + final ChannelSource source = new TestChannelSource( + TestUtil.getFixture(sourceFormat.getPreferredExtension())); + + final ReadableByteChannel processReadableChannel = new FileInputStream( TestUtil.getFixture(sourceFormat.getPreferredExtension())).getChannel(); try { ChannelProcessor proc = (ChannelProcessor) getProcessor(); - Dimension size = proc.getSize(sizeReadableChannel, + Dimension size = proc.getSize(source.newChannel(), sourceFormat); - proc.process(ops, sourceFormat, size, - processReadableChannel, + proc.process(ops, sourceFormat, size, source, new NullWritableByteChannel()); fail("Expected exception"); } catch (ProcessorException e) { @@ -128,7 +129,6 @@ public void testProcessWithUnsupportedSourceFormats() throws Exception { sourceFormat.getPreferredExtension(), e.getMessage()); } finally { - sizeReadableChannel.close(); processReadableChannel.close(); } } @@ -299,21 +299,21 @@ private void doProcessTest(OperationList ops) throws Exception { for (SourceFormat sourceFormat : SourceFormat.values()) { if (getProcessor().getAvailableOutputFormats(sourceFormat).size() > 0) { if (getProcessor() instanceof ChannelProcessor) { - ReadableByteChannel sizeReadableChannel = new FileInputStream( - TestUtil.getFixture(sourceFormat.getPreferredExtension())).getChannel(); + ChannelSource source = new TestChannelSource( + TestUtil.getFixture(sourceFormat.getPreferredExtension())); + ReadableByteChannel processReadableChannel = new FileInputStream( TestUtil.getFixture(sourceFormat.getPreferredExtension())).getChannel(); try { ChannelProcessor proc = (ChannelProcessor) getProcessor(); - Dimension size = proc.getSize(sizeReadableChannel, + Dimension size = proc.getSize(source.newChannel(), sourceFormat); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); WritableByteChannel outputChannel = Channels.newChannel(outputStream); - proc.process(ops, sourceFormat, size, - processReadableChannel, outputChannel); + proc.process(ops, sourceFormat, size, source, + outputChannel); assertTrue(outputStream.toByteArray().length > 100); // TODO: actually read this } finally { - sizeReadableChannel.close(); processReadableChannel.close(); } } diff --git a/src/test/java/edu/illinois/library/cantaloupe/processor/TestChannelSource.java b/src/test/java/edu/illinois/library/cantaloupe/processor/TestChannelSource.java new file mode 100644 index 000000000..30a8803d1 --- /dev/null +++ b/src/test/java/edu/illinois/library/cantaloupe/processor/TestChannelSource.java @@ -0,0 +1,23 @@ +package edu.illinois.library.cantaloupe.processor; + + +import edu.illinois.library.cantaloupe.resolver.ChannelSource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; + +class TestChannelSource implements ChannelSource { + + private File file; + + public TestChannelSource(File file) { + this.file = file; + } + + @Override + public ReadableByteChannel newChannel() throws IOException { + return new FileInputStream(file).getChannel(); + } +} diff --git a/src/test/java/edu/illinois/library/cantaloupe/resolver/AmazonS3ResolverTest.java b/src/test/java/edu/illinois/library/cantaloupe/resolver/AmazonS3ResolverTest.java index 49d05f207..59f288473 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resolver/AmazonS3ResolverTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resolver/AmazonS3ResolverTest.java @@ -66,13 +66,13 @@ public void setUp() throws IOException { public void testGetChannelWithBasicLookupStrategy() { // present, readable image try { - assertNotNull(instance.getChannel(IMAGE)); + assertNotNull(instance.getChannelSource(IMAGE)); } catch (IOException e) { fail(); } // missing image try { - instance.getChannel(new Identifier("bogus")); + instance.getChannelSource(new Identifier("bogus")); fail("Expected exception"); } catch (FileNotFoundException e) { // pass @@ -90,13 +90,14 @@ public void testGetChannelWithScriptLookupStrategy() throws Exception { TestUtil.getFixture("delegate.rb").getAbsolutePath()); // present image try { - assertNotNull(instance.getChannel(IMAGE)); + ChannelSource source = instance.getChannelSource(IMAGE); + assertNotNull(source.newChannel()); } catch (IOException e) { fail(); } // missing image try { - instance.getChannel(new Identifier("bogus")); + ChannelSource source = instance.getChannelSource(new Identifier("bogus")); fail("Expected exception"); } catch (FileNotFoundException e) { // pass diff --git a/src/test/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolverTest.java b/src/test/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolverTest.java index 1faab5cae..e354d245a 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolverTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resolver/FilesystemResolverTest.java @@ -12,7 +12,6 @@ import org.junit.Before; import org.junit.Test; -import javax.script.ScriptException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -45,13 +44,13 @@ public void setUp() throws IOException { public void testGetChannel() { // present, readable image try { - assertNotNull(instance.getChannel(IDENTIFIER)); + assertNotNull(instance.getChannelSource(IDENTIFIER)); } catch (IOException e) { fail(); } // missing image try { - instance.getChannel(new Identifier("bogus")); + instance.getChannelSource(new Identifier("bogus")); fail("Expected exception"); } catch (FileNotFoundException e) { // pass diff --git a/src/test/java/edu/illinois/library/cantaloupe/resolver/HttpResolverTest.java b/src/test/java/edu/illinois/library/cantaloupe/resolver/HttpResolverTest.java index 011aaa2c8..ee4b35f93 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resolver/HttpResolverTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resolver/HttpResolverTest.java @@ -15,11 +15,9 @@ import org.junit.Test; import org.restlet.data.Reference; -import javax.script.ScriptException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.file.AccessDeniedException; public class HttpResolverTest { @@ -51,7 +49,7 @@ public void tearDown() throws Exception { @Test public void testGetChannelWithPresentReadableImage() throws IOException { try { - assertNotNull(instance.getChannel(IDENTIFIER)); + assertNotNull(instance.getChannelSource(IDENTIFIER)); } catch (IOException e) { fail(); } @@ -60,7 +58,7 @@ public void testGetChannelWithPresentReadableImage() throws IOException { @Test public void testGetChannelWithMissingImage() throws IOException { try { - instance.getChannel(new Identifier("bogus")); + instance.getChannelSource(new Identifier("bogus")); fail("Expected exception"); } catch (FileNotFoundException e) { // pass diff --git a/src/test/java/edu/illinois/library/cantaloupe/resolver/JdbcResolverTest.java b/src/test/java/edu/illinois/library/cantaloupe/resolver/JdbcResolverTest.java index 3270a8b43..3f50c9a77 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resolver/JdbcResolverTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resolver/JdbcResolverTest.java @@ -70,13 +70,13 @@ public void tearDown() throws Exception { public void testGetChannel() throws IOException { // present, readable image try { - assertNotNull(instance.getChannel(new Identifier("jpg.jpg"))); + assertNotNull(instance.getChannelSource(new Identifier("jpg.jpg"))); } catch (IOException e) { fail(); } // missing image try { - instance.getChannel(new Identifier("bogus")); + instance.getChannelSource(new Identifier("bogus")); fail("Expected exception"); } catch (FileNotFoundException e) { // pass From 521db138335a4899d59bb7e7be2f7e1cb86ad39c Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Thu, 7 Jan 2016 23:02:53 -0600 Subject: [PATCH 04/86] JaiProcessor (and KakaduProcessor/OpenJpegProcessor JAI processors) use RenderedImages returned from ImageReader, rather than JAI ImageDecoder --- .../processor/ImageIoImageReader.java | 216 ++++++++++++++++++ .../cantaloupe/processor/JaiProcessor.java | 22 +- .../library/cantaloupe/processor/JaiUtil.java | 214 +---------------- .../cantaloupe/processor/KakaduProcessor.java | 4 +- .../processor/OpenJpegProcessor.java | 4 +- .../cantaloupe/processor/JaiUtilTest.java | 22 +- website/changes.html | 6 + website/source-formats.html | 2 +- 8 files changed, 247 insertions(+), 243 deletions(-) diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java index 3ee80195f..57cb3730c 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageReader.java @@ -18,6 +18,7 @@ import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.Raster; +import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.nio.channels.ReadableByteChannel; @@ -29,6 +30,10 @@ */ class ImageIoImageReader { + // Note: methods that return BufferedImages (for Java 2D) are arranged + // toward the beginning of the class; methods that return RenderedImages + // (for JAI) are toward the end. + private static Logger logger = LoggerFactory. getLogger(ImageIoImageReader.class); @@ -58,6 +63,8 @@ private static ImageReader newImageReader(Object inputSource, } } + /////////////////////// BufferedImage methods ////////////////////////// + /** * Expedient but not necessarily efficient method wrapping * {@link ImageIO#read} that reads a whole image (excluding subimages) in @@ -418,4 +425,213 @@ private BufferedImage tileAwareRead(final ImageReader reader, return outImage; } + /////////////////////// RenderedImage methods ////////////////////////// + + /** + * Reads an image (excluding subimages). + * + * @param readableChannel Image channel to read. + * @param sourceFormat + * @return RenderedImage + * @throws IOException + * @throws UnsupportedSourceFormatException + */ + public RenderedImage readRendered(final ReadableByteChannel readableChannel, + final SourceFormat sourceFormat) + throws IOException, UnsupportedSourceFormatException { + ImageReader reader = newImageReader(readableChannel, sourceFormat); + return reader.readAsRenderedImage(0, reader.getDefaultReadParam()); + } + + /** + *

Attempts to reads an image as efficiently as possible, utilizing its + * tile layout and/or subimages, if possible.

+ * + * @param imageFile Image file to read. + * @param sourceFormat Format of the source image. + * @param ops + * @param reductionFactor {@link ReductionFactor#factor} property will be + * modified to reflect the reduction factor of the + * returned image. + * @return RenderedImage best matching the given parameters. + * @throws IOException + * @throws ProcessorException + */ + public RenderedImage read(final File imageFile, + final SourceFormat sourceFormat, + final OperationList ops, + final ReductionFactor reductionFactor) + throws IOException, ProcessorException { + return multiLevelAwareRead(imageFile, sourceFormat, ops, + reductionFactor); + } + + /** + * @see #read(File, SourceFormat, OperationList, ReductionFactor) + * + * @param channelSource Source of image channels to read + * @param sourceFormat Format of the source image + * @param ops + * @param reductionFactor {@link ReductionFactor#factor} property will be + * modified to reflect the reduction factor of the + * returned image. + * @return BufferedImage best matching the given parameters. + * @throws IOException + * @throws ProcessorException + * @see #read(File, SourceFormat, OperationList, ReductionFactor) + */ + public RenderedImage read(final ChannelSource channelSource, + final SourceFormat sourceFormat, + final OperationList ops, + final ReductionFactor reductionFactor) + throws IOException, ProcessorException { + return multiLevelAwareRead(channelSource, sourceFormat, + ops, reductionFactor); + } + + /** + * @param inputSource {@link ChannelSource} or {@link File} + * @param sourceFormat Format of the source image. + * @param ops + * @param rf {@link ReductionFactor#factor} property will be modified to + * reflect the reduction factor of the returned image. + * @return BufferedImage best matching the given parameters. + * @throws IOException + * @throws ProcessorException + * @see #read(File, SourceFormat, OperationList, ReductionFactor) + */ + private RenderedImage multiLevelAwareRead(final Object inputSource, + final SourceFormat sourceFormat, + final OperationList ops, + final ReductionFactor rf) + throws IOException, ProcessorException { + final ImageReader reader = newImageReader(inputSource, sourceFormat); + RenderedImage image = null; + try { + switch (sourceFormat) { + case TIF: + Crop crop = new Crop(); + crop.setFull(true); + Scale scale = new Scale(); + scale.setMode(Scale.Mode.FULL); + for (Operation op : ops) { + if (op instanceof Crop) { + crop = (Crop) op; + } else if (op instanceof Scale) { + scale = (Scale) op; + } + } + image = readSmallestUsableSubimage(reader, crop, scale, rf); + break; + // This is similar to the TIF case, except it doesn't scan for + // subimages, which is costly to do. + default: + crop = null; + for (Operation op : ops) { + if (op instanceof Crop) { + crop = (Crop) op; + break; + } + } + if (crop != null) { + image = reader.readAsRenderedImage(0, + reader.getDefaultReadParam()); + } else { + image = reader.read(0); + } + break; + } + } finally { + reader.dispose(); + } + if (image == null) { + throw new UnsupportedSourceFormatException(sourceFormat); + } + return image; + } + + /** + * Reads the smallest image that can fulfill the given crop and scale from + * a multi-resolution image. + * + * @param reader ImageReader with input source already set + * @param crop Requested crop + * @param scale Requested scale + * @param rf {@link ReductionFactor#factor} will be set to the reduction + * factor of the returned image. + * @return The smallest image fitting the requested crop and scale + * operations from the given reader. + * @throws IOException + */ + private RenderedImage readSmallestUsableSubimage(final ImageReader reader, + final Crop crop, + final Scale scale, + final ReductionFactor rf) + throws IOException { + final Dimension fullSize = new Dimension( + reader.getWidth(0), reader.getHeight(0)); + final Rectangle regionRect = crop.getRectangle(fullSize); + final ImageReadParam param = reader.getDefaultReadParam(); + RenderedImage bestImage = null; + if (scale.isNoOp()) { + bestImage = reader.readAsRenderedImage(0, param); + logger.debug("Using a {}x{} source image (0x reduction factor)", + bestImage.getWidth(), bestImage.getHeight()); + } else { + // Pyramidal TIFFs will have > 1 image, each half the dimensions of + // the next larger. The "true" parameter tells getNumImages() to + // scan for images, which seems to be necessary for at least some + // files, but is slower. + int numImages = reader.getNumImages(false); + if (numImages > 1) { + logger.debug("Detected {} subimage(s)", numImages - 1); + } else if (numImages == -1) { + numImages = reader.getNumImages(true); + if (numImages > 1) { + logger.debug("Scan revealed {} subimage(s)", numImages - 1); + } + } + if (numImages == 1) { + bestImage = reader.read(0, param); + logger.debug("Using a {}x{} source image (0x reduction factor)", + bestImage.getWidth(), bestImage.getHeight()); + } else if (numImages > 1) { + // Loop through the reduced images from smallest to largest to + // find the first one that can supply the requested scale + for (int i = numImages - 1; i >= 0; i--) { + final int reducedWidth = reader.getWidth(i); + final int reducedHeight = reader.getHeight(i); + + final double reducedScale = (double) reducedWidth / + (double) fullSize.width; + boolean fits = false; + if (scale.getMode() == Scale.Mode.ASPECT_FIT_WIDTH) { + fits = (scale.getWidth() / (float) regionRect.width <= reducedScale); + } else if (scale.getMode() == Scale.Mode.ASPECT_FIT_HEIGHT) { + fits = (scale.getHeight() / (float) regionRect.height <= reducedScale); + } else if (scale.getMode() == Scale.Mode.ASPECT_FIT_INSIDE) { + fits = (scale.getWidth() / (float) regionRect.width <= reducedScale && + scale.getHeight() / (float) regionRect.height <= reducedScale); + } else if (scale.getMode() == Scale.Mode.NON_ASPECT_FILL) { + fits = (scale.getWidth() / (float) regionRect.width <= reducedScale && + scale.getHeight() / (float) regionRect.height <= reducedScale); + } else if (scale.getPercent() != null) { + float pct = scale.getPercent(); + fits = ((pct * fullSize.width) / (float) regionRect.width <= reducedScale && + (pct * fullSize.height) / (float) regionRect.height <= reducedScale); + } + if (fits) { + rf.factor = ProcessorUtil. + getReductionFactor(reducedScale, 0).factor; + logger.debug("Using a {}x{} source image ({}x reduction factor)", + reducedWidth, reducedHeight, rf.factor); + bestImage = reader.readAsRenderedImage(i, param); + break; + } + } + } + } + return bestImage; + } + } diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java index 4a12eb3e9..5e7fbf311 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java @@ -31,8 +31,6 @@ */ class JaiProcessor implements FileProcessor, ChannelProcessor { - // TODO: this should be used in conjunction with tile offset to match the crop region - private static final int JAI_TILE_SIZE = 512; private static final Set SUPPORTED_FEATURES = new HashSet<>(); private static final Set @@ -169,7 +167,7 @@ public void process(final OperationList ops, final File inputFile, final WritableByteChannel writableChannel) throws ProcessorException { - doProcess(ops, sourceFormat, fullSize, inputFile, writableChannel); + doProcess(ops, sourceFormat, inputFile, writableChannel); } @Override @@ -179,12 +177,11 @@ public void process(final OperationList ops, final ChannelSource channelSource, final WritableByteChannel writableChannel) throws ProcessorException { - doProcess(ops, sourceFormat, fullSize, channelSource, writableChannel); + doProcess(ops, sourceFormat, channelSource, writableChannel); } private void doProcess(final OperationList ops, final SourceFormat sourceFormat, - final Dimension fullSize, final Object input, final WritableByteChannel writableChannel) throws ProcessorException { @@ -197,20 +194,21 @@ private void doProcess(final OperationList ops, } try { + final ImageIoImageReader reader = new ImageIoImageReader(); + final ReductionFactor rf = new ReductionFactor(); RenderedImage renderedImage = null; - ReductionFactor rf = new ReductionFactor(); if (input instanceof ChannelSource) { - ReadableByteChannel channel = ((ChannelSource) input).newChannel(); - renderedImage = JaiUtil.readImage(channel, sourceFormat, ops, - fullSize, rf); + renderedImage = reader.read((ChannelSource) input, + sourceFormat, ops, rf); } else if (input instanceof File) { - renderedImage = JaiUtil.readImage( - (File) input, sourceFormat, ops, fullSize, rf); + renderedImage = reader.read((File) input, sourceFormat, ops, + rf); } if (renderedImage != null) { RenderedOp renderedOp = JaiUtil.reformatImage( RenderedOp.wrapRenderedImage(renderedImage), - new Dimension(JAI_TILE_SIZE, JAI_TILE_SIZE)); + new Dimension(renderedImage.getTileWidth(), + renderedImage.getTileHeight())); for (Operation op : ops) { if (op instanceof Crop) { renderedOp = JaiUtil. diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java index cb3481410..5b9befc77 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java @@ -1,8 +1,6 @@ package edu.illinois.library.cantaloupe.processor; import com.sun.media.imageio.plugins.jpeg2000.J2KImageWriteParam; -import com.sun.media.jai.codec.ImageCodec; -import com.sun.media.jai.codec.ImageDecoder; import com.sun.media.jai.codec.ImageEncoder; import com.sun.media.jai.codecimpl.TIFFImageEncoder; import edu.illinois.library.cantaloupe.image.Crop; @@ -14,11 +12,14 @@ import edu.illinois.library.cantaloupe.image.Scale; import edu.illinois.library.cantaloupe.image.SourceFormat; import edu.illinois.library.cantaloupe.image.Transpose; +import edu.illinois.library.cantaloupe.resolver.ChannelSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; @@ -26,7 +27,6 @@ import javax.media.jai.Interpolation; import javax.media.jai.JAI; import javax.media.jai.OpImage; -import javax.media.jai.ParameterBlockJAI; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import javax.media.jai.TileCache; @@ -38,7 +38,6 @@ import java.awt.image.renderable.ParameterBlock; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; @@ -47,7 +46,7 @@ abstract class JaiUtil { - private static Logger logger = LoggerFactory.getLogger(JaiUtil.class); + private static Logger logger = LoggerFactory.getLogger(JaiProcessor.class); /** * @param inImage Image to crop @@ -164,211 +163,6 @@ public static RenderedOp filterImage(RenderedOp inImage, Filter filter) { return filteredImage; } - /** - * @param readableChannel - * @return - */ - public static RenderedImage readImage( - ReadableByteChannel readableChannel) throws IOException { - final ParameterBlockJAI pbj = new ParameterBlockJAI("ImageRead"); - pbj.setParameter("Input", ImageIO.createImageInputStream(readableChannel)); - return JAI.create("ImageRead", pbj, - defaultRenderingHints(new Dimension(512, 512))); - } - - /** - * @param inputFile - * @param sourceFormat - * @param ops - * @param fullSize - * @param reductionFactor - * @return The read image. Use {@link #reformatImage} to convert into a - * RenderedOp. - * @throws IOException - * @throws ProcessorException - */ - public static RenderedImage readImage(File inputFile, - SourceFormat sourceFormat, - OperationList ops, - Dimension fullSize, - ReductionFactor reductionFactor) - throws IOException, ProcessorException { - return doReadImage(inputFile, sourceFormat, ops, fullSize, - reductionFactor); - } - - /** - * @param readableChannel - * @param sourceFormat - * @param ops - * @param fullSize - * @param reductionFactor - * @return The read image. Use {@link #reformatImage} to convert into a - * RenderedOp. - * @throws IOException - * @throws ProcessorException - */ - public static RenderedImage readImage(ReadableByteChannel readableChannel, - SourceFormat sourceFormat, - OperationList ops, - Dimension fullSize, - ReductionFactor reductionFactor) - throws IOException, ProcessorException { - return doReadImage(readableChannel, sourceFormat, ops, fullSize, - reductionFactor); - } - - /** - * @param inputSource {@link InputStream} or {@link File} - * @param sourceFormat - * @param ops - * @param fullSize - * @param reductionFactor - * @return - * @throws IOException - * @throws UnsupportedSourceFormatException - */ - private static RenderedImage doReadImage(Object inputSource, - SourceFormat sourceFormat, - OperationList ops, - Dimension fullSize, - ReductionFactor reductionFactor) - throws IOException, UnsupportedSourceFormatException { - RenderedImage image; - switch (sourceFormat) { - case TIF: - image = readImageWithTiffImageDecoder(inputSource, ops, fullSize, - reductionFactor); - break; - default: - final ParameterBlockJAI pbj = new ParameterBlockJAI("ImageRead"); - pbj.setParameter("Input", inputSource); - image = JAI.create("ImageRead", pbj, - defaultRenderingHints(new Dimension(512, 512))); - break; - } - if (image == null) { - throw new UnsupportedSourceFormatException(sourceFormat); - } - return image; - } - - /** - * Reads a TIFF image using the JAI TIFFImageDecoder. - * - * @param inputSource {@link InputStream} or {@link File} - * @param ops - * @param fullSize - * @param reductionFactor - * @return - * @throws IOException - * @throws IllegalArgumentException if inputSource is invalid - */ - public static RenderedImage readImageWithTiffImageDecoder( - Object inputSource, OperationList ops, Dimension fullSize, - ReductionFactor reductionFactor) throws IOException { - RenderedImage image = null; - try { - ImageDecoder dec; - if (inputSource instanceof ReadableByteChannel) { - dec = ImageCodec.createImageDecoder("tiff", - Channels.newInputStream((ReadableByteChannel) inputSource), null); - } else if (inputSource instanceof File) { - dec = ImageCodec.createImageDecoder("tiff", - (File) inputSource, null); - } else { - throw new IllegalArgumentException("Invalid inputSource parameter"); - } - if (dec != null) { - Crop crop = new Crop(); - crop.setFull(true); - Scale scale = new Scale(); - scale.setMode(Scale.Mode.FULL); - for (Operation op : ops) { - if (op instanceof Crop) { - crop = (Crop) op; - } else if (op instanceof Scale) { - scale = (Scale) op; - } - } - image = getSmallestUsableImage(dec, fullSize, crop, scale, - reductionFactor); - } - } finally { - if (inputSource instanceof InputStream) { - ((InputStream) inputSource).close(); - } - } - return image; - } - - /** - * Returns the smallest image fitting the requested size from the given - * reader. Useful for e.g. pyramidal TIFF. - * - * @param decoder ImageDecoder with input source already set - * @param fullSize - * @param crop Requested crop - * @param scale Requested scale - * @param rf Set by reference - * @return - * @throws IOException - */ - private static RenderedImage getSmallestUsableImage(ImageDecoder decoder, - Dimension fullSize, - Crop crop, - Scale scale, - ReductionFactor rf) - throws IOException { - RenderedImage bestImage = null; - if (!scale.isNoOp()) { - // Pyramidal TIFFs will have > 1 "page," each half the dimensions of - // the next larger. - int numImages = decoder.getNumPages(); - if (numImages > 1) { - logger.debug("Detected multi-resolution image with {} levels", - numImages); - final Rectangle regionRect = crop.getRectangle(fullSize); - - // Loop through the tiles from smallest to largest to find the - // first one that fits the requested scale - for (int i = numImages - 1; i >= 0; i--) { - final RenderedImage tile = decoder.decodeAsRenderedImage(i); - final double tileScale = (double) tile.getWidth() / - (double) fullSize.width; - boolean fits = false; - if (scale.getMode() == Scale.Mode.ASPECT_FIT_WIDTH) { - fits = (scale.getWidth() / (float) regionRect.width <= tileScale); - } else if (scale.getMode() == Scale.Mode.ASPECT_FIT_HEIGHT) { - fits = (scale.getHeight() / (float) regionRect.height <= tileScale); - } else if (scale.getMode() == Scale.Mode.ASPECT_FIT_INSIDE) { - fits = (scale.getWidth() / (float) regionRect.width <= tileScale && - scale.getHeight() / (float) regionRect.height <= tileScale); - } else if (scale.getMode() == Scale.Mode.NON_ASPECT_FILL) { - fits = (scale.getWidth() / (float) regionRect.width <= tileScale && - scale.getHeight() / (float) regionRect.height <= tileScale); - } else if (scale.getPercent() != null) { - float pct = scale.getPercent(); - fits = ((pct * fullSize.width) / (float) regionRect.width <= tileScale && - (pct * fullSize.height) / (float) regionRect.height <= tileScale); - } - if (fits) { - rf.factor = ProcessorUtil. - getReductionFactor(tileScale, 0).factor; - logger.debug("Using a {}x{} source tile ({}x reduction factor)", - tile.getWidth(), tile.getHeight(), rf.factor); - bestImage = tile; - break; - } - } - } - } - if (bestImage == null) { - bestImage = decoder.decodeAsRenderedImage(); - } - return bestImage; - } - /** * @param inImage Image to reformat * @param tileSize JAI tile size diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java index 6c45b5f3e..8d97212f5 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java @@ -466,8 +466,8 @@ private void postProcessUsingJai(final ReadableByteChannel readableChannel, final ReductionFactor reductionFactor, final WritableByteChannel writableChannel) throws IOException, ProcessorException { - RenderedImage renderedImage = - JaiUtil.readImage(readableChannel); + RenderedImage renderedImage = new ImageIoImageReader(). + readRendered(readableChannel, SourceFormat.BMP); RenderedOp renderedOp = JaiUtil.reformatImage( RenderedOp.wrapRenderedImage(renderedImage), new Dimension(512, 512)); diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/OpenJpegProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/OpenJpegProcessor.java index 8a97afd2e..ce9f51419 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/OpenJpegProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/OpenJpegProcessor.java @@ -433,8 +433,8 @@ private void postProcessUsingJai(final ReadableByteChannel readableChannel, final ReductionFactor reductionFactor, final WritableByteChannel writableChannel) throws IOException, ProcessorException { - RenderedImage renderedImage = - JaiUtil.readImage(readableChannel); + RenderedImage renderedImage = new ImageIoImageReader(). + readRendered(readableChannel, SourceFormat.BMP); RenderedOp renderedOp = JaiUtil.reformatImage( RenderedOp.wrapRenderedImage(renderedImage), new Dimension(512, 512)); diff --git a/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java b/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java index 3da192706..5af365fb3 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java @@ -49,23 +49,13 @@ public void testFilterImage() { // TODO: write this } - @Test - public void testReadImageWithFile() { - // this will be tested in ProcessorTest - } - - @Test - public void testReadImageWithInputStream() { - // this will be tested in ProcessorTest - } - @Test public void testReformatImage() throws Exception { - final Dimension fullSize = new Dimension(100, 88); final OperationList ops = new OperationList(); final ReductionFactor reductionFactor = new ReductionFactor(); - RenderedImage image = JaiUtil.readImage( - TestUtil.getFixture("jpg"), SourceFormat.JPG, ops, fullSize, + ImageIoImageReader reader = new ImageIoImageReader(); + RenderedImage image = reader.read( + TestUtil.getFixture("jpg"), SourceFormat.JPG, ops, reductionFactor); PlanarImage planarImage = PlanarImage.wrapRenderedImage(image); RenderedOp renderedOp = JaiUtil.reformatImage(planarImage, @@ -147,11 +137,11 @@ public void testWriteImage() { } private RenderedOp getFixture(final String name) throws Exception { - final Dimension fullSize = new Dimension(100, 88); final OperationList ops = new OperationList(); final ReductionFactor reductionFactor = new ReductionFactor(); - RenderedImage image = JaiUtil.readImage( - TestUtil.getFixture(name), SourceFormat.JPG, ops, fullSize, + ImageIoImageReader reader = new ImageIoImageReader(); + RenderedImage image = reader.read( + TestUtil.getFixture(name), SourceFormat.JPG, ops, reductionFactor); PlanarImage planarImage = PlanarImage.wrapRenderedImage(image); return JaiUtil.reformatImage(planarImage, new Dimension(512, 512)); diff --git a/website/changes.html b/website/changes.html index 1e43411ac..cf1b1d9c6 100644 --- a/website/changes.html +++ b/website/changes.html @@ -4,6 +4,12 @@

Change Log

+

2.1

+ +
    +
  • Optimized JaiProcessor for tiled images.
  • +
+

2.0

    diff --git a/website/source-formats.html b/website/source-formats.html index 39dd05e06..fb7eec2bf 100644 --- a/website/source-formats.html +++ b/website/source-formats.html @@ -43,4 +43,4 @@

    Multi-Resolution (Pyramidal) TIFF

    Processor Considerations

    -

    To reiterate: most processors can "read the TIFF format," but not all can read it efficiently. Currently, Java2dProcessor and JaiProcessor both support multi-resolution TIFF, which is to say that they actually do read the embedded sub-images and choose the smallest one that can fulfill the request. Java2dProcessor additionally exploits tiled sub-images, so it should currently be the processor of choice for dealing with high-resolution TIFF images.

    +

    To reiterate: most processors can "read the TIFF format," but not all can read it efficiently. Currently, Java2dProcessor and JaiProcessor both support multi-resolution TIFF, which is to say that they actually do read the embedded sub-images and choose the smallest one that can fulfill the request. Additionally, both exploit tiled sub-images. JaiProcessor, however, is able to use the JAI processing pipeline to do this more efficiently, so it is currently the performance champ for suitably-encoded high-resolution TIFF images.

    From c2f6708855435b0f9a56fb9176458c07ab6fd96e Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Fri, 8 Jan 2016 19:57:12 -0600 Subject: [PATCH 05/86] Clarify that putDimension() needs to clean up after itself --- .../java/edu/illinois/library/cantaloupe/cache/Cache.java | 4 +++- .../illinois/library/cantaloupe/cache/FilesystemCache.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/illinois/library/cantaloupe/cache/Cache.java b/src/main/java/edu/illinois/library/cantaloupe/cache/Cache.java index 9d3217829..5606f4d60 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/cache/Cache.java +++ b/src/main/java/edu/illinois/library/cantaloupe/cache/Cache.java @@ -90,7 +90,9 @@ WritableByteChannel getImageWritableChannel(OperationList opList) void purgeExpired() throws IOException; /** - * Adds an image's dimension information to the cache. + * Adds an image's dimension information to the cache. If the writing of + * the dimension is interrupted, implementations should clean it up, if + * necessary. * * @param identifier Identifier of the image corresponding to the given * size. diff --git a/src/main/java/edu/illinois/library/cantaloupe/cache/FilesystemCache.java b/src/main/java/edu/illinois/library/cantaloupe/cache/FilesystemCache.java index 7bec9da7e..1b78f2dd4 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/cache/FilesystemCache.java +++ b/src/main/java/edu/illinois/library/cantaloupe/cache/FilesystemCache.java @@ -553,6 +553,7 @@ public void putDimension(Identifier identifier, Dimension dimension) try { infoMapper.writeValue(cacheFile, info); } catch (IOException e) { + cacheFile.delete(); throw new IOException("Unable to create " + cacheFile.getAbsolutePath(), e); } From 80998083bdee3ec2cb6e490980f64631f30ecc15 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Fri, 8 Jan 2016 22:46:45 -0600 Subject: [PATCH 06/86] Work on JAI image writing --- cantaloupe.properties.sample | 12 ++ .../processor/ImageIoImageWriter.java | 125 +++++++++++++++++- .../cantaloupe/processor/JaiProcessor.java | 25 ++-- .../library/cantaloupe/processor/JaiUtil.java | 101 -------------- .../cantaloupe/processor/JaiUtilTest.java | 5 - website/changes.html | 1 + website/getting-started.html | 8 +- 7 files changed, 159 insertions(+), 118 deletions(-) diff --git a/cantaloupe.properties.sample b/cantaloupe.properties.sample index 99391db51..d9b53014a 100644 --- a/cantaloupe.properties.sample +++ b/cantaloupe.properties.sample @@ -230,6 +230,18 @@ processor.fallback = Java2dProcessor # Optional; overrides the PATH #ImageMagickProcessor.path_to_binaries = /usr/local/bin +#---------------------------------------- +# JaiProcessor +#---------------------------------------- + +# JPEG output quality. Should be a number between 0-1 ending in "f" +JaiProcessor.jpg.quality = 0.7f + +# TIFF output compression type. Available values are `LZW`, `Deflate`, +# `ZLib`, `JPEG`, and `PackBits`. Comment out or leave blank for no +# compression. +JaiProcessor.tif.compression = LZW + #---------------------------------------- # Java2dProcessor #---------------------------------------- diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java index 85f8d5393..56340d15c 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java @@ -2,13 +2,18 @@ import edu.illinois.library.cantaloupe.Application; import edu.illinois.library.cantaloupe.image.OutputFormat; +import it.geosolutions.imageio.plugins.tiff.TIFFImageWriteParam; +import org.apache.commons.configuration.Configuration; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; +import javax.media.jai.JAI; +import javax.media.jai.RenderedOp; import java.awt.image.BufferedImage; +import java.awt.image.renderable.ParameterBlock; import java.io.IOException; import java.nio.channels.WritableByteChannel; import java.util.Iterator; @@ -19,7 +24,7 @@ class ImageIoImageWriter { /** - * Writes an image to the given byte channel. + * Writes an image to the given channel. * * @param image Image to write * @param outputFormat Format of the output image @@ -64,4 +69,122 @@ public void write(BufferedImage image, } } + /** + * Writes an image to the given channel. + * + * @param image Image to write + * @param outputFormat Format of the output image + * @param writableChannel Channel to write the image to + * @throws IOException + */ + public void write(RenderedOp image, + OutputFormat outputFormat, + WritableByteChannel writableChannel) throws IOException { + final Configuration config = Application.getConfiguration(); + switch (outputFormat) { + case GIF: + Iterator writers = ImageIO.getImageWritersByFormatName("GIF"); + if (writers.hasNext()) { + // GIFWriter can't deal with a non-0,0 origin ("coordinate + // out of bounds!") + ParameterBlock pb = new ParameterBlock(); + pb.addSource(image); + pb.add((float) -image.getMinX()); + pb.add((float) -image.getMinY()); + image = JAI.create("translate", pb); + + ImageWriter writer = writers.next(); + ImageOutputStream os = ImageIO. + createImageOutputStream(writableChannel); + writer.setOutput(os); + try { + writer.write(image); + os.flush(); // http://stackoverflow.com/a/14489406 + } finally { + writer.dispose(); + } + } + break; + case JP2: + /* + TODO: this doesn't write anything + ImageIO.write(image, outputFormat.getExtension(), + ImageIO.createImageOutputStream(writableChannel)); + // and this causes an error + writers = ImageIO.getImageWritersByFormatName("JPEG2000"); + if (writers.hasNext()) { + ImageWriter writer = writers.next(); + J2KImageWriteParam j2Param = new J2KImageWriteParam(); + j2Param.setLossless(false); + j2Param.setEncodingRate(Double.MAX_VALUE); + j2Param.setCodeBlockSize(new int[]{128, 8}); + j2Param.setTilingMode(ImageWriteParam.MODE_DISABLED); + j2Param.setProgressionType("res"); + ImageOutputStream os = ImageIO. + createImageOutputStream(writableChannel); + writer.setOutput(os); + IIOImage iioImage = new IIOImage(image, null, null); + try { + writer.write(null, iioImage, j2Param); + } finally { + writer.dispose(); + } + } */ + break; + case JPG: + Iterator iter = ImageIO.getImageWritersByFormatName("jpeg"); + ImageWriter writer = (ImageWriter) iter.next(); + try { + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(config.getFloat( + JaiProcessor.JPG_QUALITY_CONFIG_KEY, 0.7f)); + param.setCompressionType("JPEG"); + ImageOutputStream os = ImageIO.createImageOutputStream(writableChannel); + writer.setOutput(os); + // JPEGImageWriter doesn't like RenderedOps, so give it a + // BufferedImage + IIOImage iioImage = new IIOImage(image.getAsBufferedImage(), null, null); + writer.write(null, iioImage, param); + } finally { + writer.dispose(); + } + break; + case PNG: + ImageIO.write(image, outputFormat.getExtension(), + ImageIO.createImageOutputStream(writableChannel)); + break; + case TIF: + writers = ImageIO.getImageWritersByFormatName("TIFF"); + while (writers.hasNext()) { + writer = writers.next(); + if (writer instanceof it.geosolutions.imageioimpl.plugins.tiff.TIFFImageWriter) { + final String compressionType = config.getString( + JaiProcessor.TIF_COMPRESSION_CONFIG_KEY); + final TIFFImageWriteParam param = + (TIFFImageWriteParam) writer.getDefaultWriteParam(); + param.setTilingMode(ImageWriteParam.MODE_EXPLICIT); + param.setTiling(128, 128, 0, 0); + if (compressionType != null) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(compressionType); + } + + final IIOImage iioImage = new IIOImage(image, null, null); + ImageOutputStream ios = + ImageIO.createImageOutputStream(writableChannel); + writer.setOutput(ios); + try { + writer.write(null, iioImage, param); + ios.flush(); // http://stackoverflow.com/a/14489406 + } finally { + writer.dispose(); + } + } + } + break; + } + + } + } diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java index 5e7fbf311..e7c81b584 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java @@ -31,6 +31,11 @@ */ class JaiProcessor implements FileProcessor, ChannelProcessor { + public static final String JPG_QUALITY_CONFIG_KEY = + "JaiProcessor.jpg.quality"; + public static final String TIF_COMPRESSION_CONFIG_KEY = + "JaiProcessor.tif.compression"; + private static final Set SUPPORTED_FEATURES = new HashSet<>(); private static final Set @@ -86,18 +91,17 @@ public static HashMap> getFormats() { formatsMap = new HashMap<>(); for (SourceFormat sourceFormat : SourceFormat.values()) { Set outputFormats = new HashSet<>(); - for (int i = 0, length = readerMimeTypes.length; i < length; i++) { - if (sourceFormat.getMediaTypes(). - contains(new MediaType(readerMimeTypes[i].toLowerCase()))) { + for (String readerMimeType : readerMimeTypes) { + if (sourceFormat.getMediaTypes().contains( + new MediaType(readerMimeType.toLowerCase()))) { for (OutputFormat outputFormat : OutputFormat.values()) { - if (outputFormat == OutputFormat.GIF || - outputFormat == OutputFormat.JP2) { - // these currently don't work - // (see ProcessorUtil.writeImage(RenderedOp)) + if (outputFormat.equals(OutputFormat.JP2)) { + // currently doesn't work + // (see ImageIoImageWriter.write(RenderedOp...)) continue; } - for (int i2 = 0, length2 = writerMimeTypes.length; i2 < length2; i2++) { - if (outputFormat.getMediaType().equals(writerMimeTypes[i2].toLowerCase())) { + for (String writerMimeType : writerMimeTypes) { + if (outputFormat.getMediaType().equals(writerMimeType.toLowerCase())) { outputFormats.add(outputFormat); } } @@ -227,7 +231,8 @@ private void doProcess(final OperationList ops, filterImage(renderedOp, (Filter) op); } } - JaiUtil.writeImage(renderedOp, ops.getOutputFormat(), + ImageIoImageWriter writer = new ImageIoImageWriter(); + writer.write(renderedOp, ops.getOutputFormat(), writableChannel); } } catch (IOException e) { diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java index 5b9befc77..ad86ae55b 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiUtil.java @@ -1,28 +1,11 @@ package edu.illinois.library.cantaloupe.processor; -import com.sun.media.imageio.plugins.jpeg2000.J2KImageWriteParam; -import com.sun.media.jai.codec.ImageEncoder; -import com.sun.media.jai.codecimpl.TIFFImageEncoder; import edu.illinois.library.cantaloupe.image.Crop; import edu.illinois.library.cantaloupe.image.Filter; -import edu.illinois.library.cantaloupe.image.Operation; -import edu.illinois.library.cantaloupe.image.OperationList; -import edu.illinois.library.cantaloupe.image.OutputFormat; import edu.illinois.library.cantaloupe.image.Rotate; import edu.illinois.library.cantaloupe.image.Scale; -import edu.illinois.library.cantaloupe.image.SourceFormat; import edu.illinois.library.cantaloupe.image.Transpose; -import edu.illinois.library.cantaloupe.resolver.ChannelSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.imageio.IIOImage; -import javax.imageio.ImageIO; -import javax.imageio.ImageReadParam; -import javax.imageio.ImageReader; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.ImageOutputStream; import javax.media.jai.ImageLayout; import javax.media.jai.Interpolation; import javax.media.jai.JAI; @@ -32,22 +15,12 @@ import javax.media.jai.TileCache; import javax.media.jai.operator.TransposeDescriptor; import java.awt.Dimension; -import java.awt.Rectangle; import java.awt.RenderingHints; -import java.awt.image.RenderedImage; import java.awt.image.renderable.ParameterBlock; -import java.io.File; -import java.io.IOException; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; import java.util.HashMap; -import java.util.Iterator; abstract class JaiUtil { - private static Logger logger = LoggerFactory.getLogger(JaiProcessor.class); - /** * @param inImage Image to crop * @param crop Crop operation @@ -276,78 +249,4 @@ public static RenderedOp transposeImage(RenderedOp inImage, return JAI.create("transpose", pb); } - /** - * Writes an image to the given output stream. - * - * @param image Image to write - * @param outputFormat Format of the output image - * @param writableChannel Channel to write the image to - * @throws IOException - */ - public static void writeImage(RenderedOp image, - OutputFormat outputFormat, - WritableByteChannel writableChannel) - throws IOException { - switch (outputFormat) { - case GIF: - // TODO: this and ImageIO.write() frequently don't work - Iterator writers = ImageIO.getImageWritersByFormatName("GIF"); - if (writers.hasNext()) { - // GIFWriter can't deal with a non-0,0 origin - ParameterBlock pb = new ParameterBlock(); - pb.addSource(image); - pb.add((float) -image.getMinX()); - pb.add((float) -image.getMinY()); - image = JAI.create("translate", pb); - - ImageWriter writer = (ImageWriter) writers.next(); - ImageOutputStream os = ImageIO. - createImageOutputStream(writableChannel); - writer.setOutput(os); - writer.write(image); - } - break; - case JP2: - // TODO: neither this nor ImageIO.write() seem to write anything - writers = ImageIO.getImageWritersByFormatName("JPEG2000"); - if (writers.hasNext()) { - ImageWriter writer = (ImageWriter) writers.next(); - IIOImage iioImage = new IIOImage(image, null, null); - J2KImageWriteParam j2Param = new J2KImageWriteParam(); - j2Param.setLossless(false); - j2Param.setEncodingRate(Double.MAX_VALUE); - j2Param.setCodeBlockSize(new int[]{128, 8}); - j2Param.setTilingMode(ImageWriteParam.MODE_DISABLED); - j2Param.setProgressionType("res"); - ImageOutputStream os = ImageIO. - createImageOutputStream(writableChannel); - writer.setOutput(os); - writer.write(null, iioImage, j2Param); - } - break; - case JPG: - JAI.create("encode", image.getAsBufferedImage(), - Channels.newOutputStream(writableChannel), "JPEG", null); - break; - case PNG: - // ImageIO.write() seems to be more efficient than - // PNGImageEncoder - ImageIO.write(image, outputFormat.getExtension(), - ImageIO.createImageOutputStream(writableChannel)); - /* PNGEncodeParam pngParam = new PNGEncodeParam.RGB(); - ImageEncoder pngEncoder = ImageCodec.createImageEncoder("PNG", - outputStream, pngParam); - pngEncoder.encode(image); */ - break; - case TIF: - // TIFFImageEncoder seems to be more efficient than - // ImageIO.write(); - ImageEncoder tiffEnc = new TIFFImageEncoder( - Channels.newOutputStream(writableChannel), null); - tiffEnc.encode(image); - break; - } - - } - } diff --git a/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java b/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java index 5af365fb3..2c0f72e00 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/processor/JaiUtilTest.java @@ -131,11 +131,6 @@ public void testTransposeImage() throws Exception { assertEquals(88, result.getHeight()); } - @Test - public void testWriteImage() { - // TODO: write this - } - private RenderedOp getFixture(final String name) throws Exception { final OperationList ops = new OperationList(); final ReductionFactor reductionFactor = new ReductionFactor(); diff --git a/website/changes.html b/website/changes.html index f45c1723a..0667720b3 100644 --- a/website/changes.html +++ b/website/changes.html @@ -8,6 +8,7 @@

    2.1

    • Optimized JaiProcessor for tiled images.
    • +
    • Made some aspects of JaiProcessor JPEG and TIFF output configurable.

    2.0.1

    diff --git a/website/getting-started.html b/website/getting-started.html index 796275472..f5f14f135 100644 --- a/website/getting-started.html +++ b/website/getting-started.html @@ -41,7 +41,13 @@

    Running

    Upgrading

    -

    Upgrading is theoretically just a matter of downloading a new version and running it. Since instances are self-contained, new versions can run happily alongside existing ones, with each using its own configuration file. Sometimes there are backwards-incompatible changes to the file structure, though, so check below to see if there is anything more to be done.

    +

    Upgrading is theoretically just a matter of downloading a new version and running it. Since instances are self-contained, new versions can run happily alongside existing ones, with each using its own configuration file. Sometimes there are changes to the file, though, so check below to see if there is anything more to be done.

    + +

    2.0 to 2.1

    + +
      +
    • Add the JaiProcessor.* keys from the sample configuration.
    • +

    1.2 to 2.0

    From 3ef51cf358d34b3d4a34df988473e559a1ff067c Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Sat, 9 Jan 2016 20:37:05 -0600 Subject: [PATCH 07/86] Bump to 2.1-SNAPSHOT --- pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4fb591c34..1be802908 100644 --- a/pom.xml +++ b/pom.xml @@ -4,13 +4,14 @@ edu.illinois.library.cantaloupe Cantaloupe jar - 2.0-SNAPSHOT + + 2.1-SNAPSHOT Cantaloupe https://medusa-project.github.io/cantaloupe/ UTF-8 UTF-8 - 2.0-SNAPSHOT + 2.1-SNAPSHOT 1.10.43 2.4 3.4 From 74fc3f6a6763c2cd3b4028ed375a34f57cadccd6 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Sat, 9 Jan 2016 20:39:42 -0600 Subject: [PATCH 08/86] Add Java2dProcessor.tif.compression config option --- cantaloupe.properties.sample | 5 ++++ .../processor/ImageIoImageWriter.java | 28 +++++++++++++++++++ .../cantaloupe/processor/Java2dProcessor.java | 1 + website/changes.html | 3 +- website/getting-started.html | 1 + 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/cantaloupe.properties.sample b/cantaloupe.properties.sample index d9b53014a..284cf27a9 100644 --- a/cantaloupe.properties.sample +++ b/cantaloupe.properties.sample @@ -253,6 +253,11 @@ Java2dProcessor.scale_mode = speed # JPEG output quality. Should be a number between 0-1 ending in "f" Java2dProcessor.jpg.quality = 0.7f +# TIFF output compression type. Available values are `LZW`, `Deflate`, +# `ZLib`, `JPEG`, and `PackBits`. Comment out or leave blank for no +# compression. +Java2dProcessor.tif.compression = LZW + #---------------------------------------- # KakaduProcessor #---------------------------------------- diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java index 56340d15c..5034b4436 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java @@ -61,6 +61,34 @@ public void write(BufferedImage image, writer.setOutput(os); writer.write(image); break;*/ + case TIF: + Iterator writers = ImageIO.getImageWritersByFormatName("TIFF"); + while (writers.hasNext()) { + writer = writers.next(); + if (writer instanceof it.geosolutions.imageioimpl.plugins.tiff.TIFFImageWriter) { + final String compressionType = Application. + getConfiguration(). + getString(Java2dProcessor.TIF_COMPRESSION_CONFIG_KEY); + final TIFFImageWriteParam param = + (TIFFImageWriteParam) writer.getDefaultWriteParam(); + if (compressionType != null) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(compressionType); + } + + final IIOImage iioImage = new IIOImage(image, null, null); + ImageOutputStream ios = + ImageIO.createImageOutputStream(writableChannel); + writer.setOutput(ios); + try { + writer.write(null, iioImage, param); + ios.flush(); // http://stackoverflow.com/a/14489406 + } finally { + writer.dispose(); + } + } + } + break; default: // TODO: jp2 doesn't seem to work ImageIO.write(image, outputFormat.getExtension(), diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java index b86de52cb..6fccda17f 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/Java2dProcessor.java @@ -32,6 +32,7 @@ class Java2dProcessor implements ChannelProcessor, FileProcessor { public static final String JPG_QUALITY_CONFIG_KEY = "Java2dProcessor.jpg.quality"; public static final String SCALE_MODE_CONFIG_KEY = "Java2dProcessor.scale_mode"; + public static final String TIF_COMPRESSION_CONFIG_KEY = "Java2dProcessor.tif.compression"; private static final HashMap> FORMATS = getAvailableOutputFormats(); diff --git a/website/changes.html b/website/changes.html index bfd823e78..014ff95ae 100644 --- a/website/changes.html +++ b/website/changes.html @@ -8,7 +8,8 @@

    2.1

    • Optimized JaiProcessor for tiled images.
    • -
    • Made some aspects of JaiProcessor JPEG and TIFF output configurable.
    • +
    • Made some aspects of JaiProcessor's JPEG and TIFF output configurable.
    • +
    • Made Java2dProcessor's TIFF output compression configurable.

    2.0.1

    diff --git a/website/getting-started.html b/website/getting-started.html index f5f14f135..1c2e21c60 100644 --- a/website/getting-started.html +++ b/website/getting-started.html @@ -47,6 +47,7 @@

    2.0 to 2.1

    • Add the JaiProcessor.* keys from the sample configuration.
    • +
    • Add the Java2dProcessor.tif.compression key from the sample configuration.

    1.2 to 2.0

    From 128934e7ee3009f43a5f23543f2296f00caf7a63 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Sun, 10 Jan 2016 13:53:34 -0600 Subject: [PATCH 09/86] Replace JAI-EXT with Sun JAI --- pom.xml | 74 +++++++---------- .../processor/ImageIoImageWriter.java | 81 +++++++++---------- .../cantaloupe/processor/JaiProcessor.java | 4 - website/processors.html | 8 +- 4 files changed, 68 insertions(+), 99 deletions(-) diff --git a/pom.xml b/pom.xml index 1be802908..4844a6a34 100644 --- a/pom.xml +++ b/pom.xml @@ -12,48 +12,43 @@ UTF-8 UTF-8 2.1-SNAPSHOT - 1.10.43 - 2.4 - 3.4 - 1.4.190 - 2.4.1 - 1.4.0 - 1.1.12 - 2.6.0 - 1.0.6 - 2.7.8 - 9.0.4.0 - 4.12 - 1.1.3 - 2.1.3 2.3.5 - 1.7.12
    ch.qos.logback logback-classic - ${logback.version} + 1.1.3 com.amazonaws aws-java-sdk-s3 - ${amazon-s3.version} + 1.10.43 + + + com.github.jai-imageio + jai-imageio-core + 1.3.1 + + + com.github.jai-imageio + jai-imageio-jpeg2000 + 1.3.0 com.h2database h2 - ${h2.version} + 1.4.190 test com.zaxxer HikariCP - ${hikari.version} + 2.4.1 commons-configuration @@ -63,12 +58,12 @@ commons-io commons-io - ${commons-io.version} + 2.4 eu.medsea.mimeutil mime-util - ${mimeutil.version} + 2.1.3 log4j @@ -80,49 +75,40 @@ + - it.geosolutions.imageio-ext - imageio-ext-imagereadmt - ${imageio-ext.version} - - - it.geosolutions.imageio-ext - imageio-ext-tiff - ${imageio-ext.version} - - - it.geosolutions.jaiext.scale - jt-scale - ${jaiext-scale.version} + javax.media.jai + com.springsource.javax.media.jai.core + 1.1.3 org.apache.commons commons-lang3 - ${commons-lang3.version} + 3.4 junit junit - ${junit.version} + 4.12 test org.codehaus.janino janino - ${janino.version} + 2.7.8 org.im4java im4java - ${im4java.version} + 1.4.0 org.jruby jruby - ${jruby.version} + 9.0.4.0 pom @@ -162,21 +148,21 @@ org.slf4j slf4j-api - ${slf4j.version} + 1.7.12 org.slf4j jcl-over-slf4j - ${slf4j.version} + 1.7.12 - geosolutions - GeoSolutions Repository - http://maven.geo-solutions.it/ + com.springsource.repository.bundles.external + SpringSource Enterprise Bundle Repository - External Bundle Releases + http://repository.springsource.com/maven/bundles/external maven-restlet diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java index 5034b4436..98dd9639d 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java @@ -2,7 +2,6 @@ import edu.illinois.library.cantaloupe.Application; import edu.illinois.library.cantaloupe.image.OutputFormat; -import it.geosolutions.imageio.plugins.tiff.TIFFImageWriteParam; import org.apache.commons.configuration.Configuration; import javax.imageio.IIOImage; @@ -63,29 +62,26 @@ public void write(BufferedImage image, break;*/ case TIF: Iterator writers = ImageIO.getImageWritersByFormatName("TIFF"); - while (writers.hasNext()) { + if (writers.hasNext()) { writer = writers.next(); - if (writer instanceof it.geosolutions.imageioimpl.plugins.tiff.TIFFImageWriter) { - final String compressionType = Application. - getConfiguration(). - getString(Java2dProcessor.TIF_COMPRESSION_CONFIG_KEY); - final TIFFImageWriteParam param = - (TIFFImageWriteParam) writer.getDefaultWriteParam(); - if (compressionType != null) { - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionType(compressionType); - } + final String compressionType = Application. + getConfiguration(). + getString(Java2dProcessor.TIF_COMPRESSION_CONFIG_KEY); + final ImageWriteParam param = writer.getDefaultWriteParam(); + if (compressionType != null) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(compressionType); + } - final IIOImage iioImage = new IIOImage(image, null, null); - ImageOutputStream ios = - ImageIO.createImageOutputStream(writableChannel); - writer.setOutput(ios); - try { - writer.write(null, iioImage, param); - ios.flush(); // http://stackoverflow.com/a/14489406 - } finally { - writer.dispose(); - } + final IIOImage iioImage = new IIOImage(image, null, null); + ImageOutputStream ios = + ImageIO.createImageOutputStream(writableChannel); + writer.setOutput(ios); + try { + writer.write(null, iioImage, param); + ios.flush(); // http://stackoverflow.com/a/14489406 + } finally { + writer.dispose(); } } break; @@ -160,7 +156,7 @@ public void write(RenderedOp image, } */ break; case JPG: - Iterator iter = ImageIO.getImageWritersByFormatName("jpeg"); + Iterator iter = ImageIO.getImageWritersByFormatName("JPEG"); ImageWriter writer = (ImageWriter) iter.next(); try { ImageWriteParam param = writer.getDefaultWriteParam(); @@ -184,30 +180,25 @@ public void write(RenderedOp image, break; case TIF: writers = ImageIO.getImageWritersByFormatName("TIFF"); - while (writers.hasNext()) { + if (writers.hasNext()) { writer = writers.next(); - if (writer instanceof it.geosolutions.imageioimpl.plugins.tiff.TIFFImageWriter) { - final String compressionType = config.getString( - JaiProcessor.TIF_COMPRESSION_CONFIG_KEY); - final TIFFImageWriteParam param = - (TIFFImageWriteParam) writer.getDefaultWriteParam(); - param.setTilingMode(ImageWriteParam.MODE_EXPLICIT); - param.setTiling(128, 128, 0, 0); - if (compressionType != null) { - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionType(compressionType); - } + final String compressionType = config.getString( + JaiProcessor.TIF_COMPRESSION_CONFIG_KEY); + final ImageWriteParam param = writer.getDefaultWriteParam(); + if (compressionType != null) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(compressionType); + } - final IIOImage iioImage = new IIOImage(image, null, null); - ImageOutputStream ios = - ImageIO.createImageOutputStream(writableChannel); - writer.setOutput(ios); - try { - writer.write(null, iioImage, param); - ios.flush(); // http://stackoverflow.com/a/14489406 - } finally { - writer.dispose(); - } + final IIOImage iioImage = new IIOImage(image, null, null); + ImageOutputStream ios = + ImageIO.createImageOutputStream(writableChannel); + writer.setOutput(ios); + try { + writer.write(null, iioImage, param); + ios.flush(); // http://stackoverflow.com/a/14489406 + } finally { + writer.dispose(); } } break; diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java index e7c81b584..14bf5c1a7 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/JaiProcessor.java @@ -11,7 +11,6 @@ import edu.illinois.library.cantaloupe.image.Transpose; import edu.illinois.library.cantaloupe.resolver.ChannelSource; import edu.illinois.library.cantaloupe.resource.iiif.ProcessorFeature; -import it.geosolutions.jaiext.JAIExt; import org.restlet.data.MediaType; import javax.imageio.ImageIO; @@ -46,9 +45,6 @@ class JaiProcessor implements FileProcessor, ChannelProcessor { private static HashMap> formatsMap; static { - // use GeoTools JAI-EXT instead of Sun JAI - JAIExt.initJAIEXT(); - SUPPORTED_IIIF_1_1_QUALITIES.add( edu.illinois.library.cantaloupe.resource.iiif.v1.Quality.BITONAL); SUPPORTED_IIIF_1_1_QUALITIES.add( diff --git a/website/processors.html b/website/processors.html index 5eb6bda69..9530df045 100644 --- a/website/processors.html +++ b/website/processors.html @@ -120,16 +120,12 @@

    ImageMagickProcessor

    JaiProcessor

    -

    Java Advanced Imaging (JAI) is a sophisticated image processing library developed by Sun until around 2006. JaiProcessor uses an updated fork called JAI-EXT.

    +

    Java Advanced Imaging (JAI) is a sophisticated image processing library developed in the 2000s by Sun. It offers several advantages over Java 2D: a pull-based rendering pipeline that can reduce memory usage, and efficient region-of-interest decoding with some formats.

    -

    JAI offers several theoretical advantages over Java 2D for this application: a more efficient rendering pipeline that should reduce memory usage, and capability of region-of-interest decoding with some formats. Whether these advantages play out in reality is an open question; the author's own profiling seems to indicate maybe not.

    - -

    JaiProcessor can read and write the same formats as Java2dProcessor.

    +

    As JaiProcessor and Java2dProcessor use the same ImageIO readers and writers, they can read and write the same formats.

    Years ago, Sun published platform-native accelerator JARs called mediaLib for Windows, Linux, and Solaris, which improved JAI's performance. It is unknown whether these still work on modern platforms.

    -
    Note: JaiProcessor is buggy and most development effort thus far has been applied to Java2dProcessor instead. This processor should be considered experimental and Java2dProcessor should be preferred in all cases.
    -

    KakaduProcessor

    From 67234ba7ffcc6e4668e164a38be8f620faf76c85 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Mon, 11 Jan 2016 08:43:43 -0600 Subject: [PATCH 10/86] Fix write(RenderedOp) with JPEG output of source images with alpha --- .../cantaloupe/processor/ImageIoImageWriter.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java index 98dd9639d..e034822b3 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/ImageIoImageWriter.java @@ -10,6 +10,7 @@ import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; import javax.media.jai.JAI; +import javax.media.jai.OpImage; import javax.media.jai.RenderedOp; import java.awt.image.BufferedImage; import java.awt.image.renderable.ParameterBlock; @@ -101,6 +102,7 @@ public void write(BufferedImage image, * @param writableChannel Channel to write the image to * @throws IOException */ + @SuppressWarnings({ "deprecation" }) public void write(RenderedOp image, OutputFormat outputFormat, WritableByteChannel writableChannel) throws IOException { @@ -159,6 +161,16 @@ public void write(RenderedOp image, Iterator iter = ImageIO.getImageWritersByFormatName("JPEG"); ImageWriter writer = (ImageWriter) iter.next(); try { + // JPEGImageWriter will interpret a >3-band image as CMYK. + // So, select only the first 3 bands. + if (OpImage.getExpandedNumBands(image.getSampleModel(), + image.getColorModel()) == 4) { + final ParameterBlock pb = new ParameterBlock(); + pb.addSource(image); + final int[] bands = { 0, 1, 2 }; + pb.add(bands); + image = JAI.create("bandselect", pb, null); + } ImageWriteParam param = writer.getDefaultWriteParam(); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionQuality(config.getFloat( From ccb7d81fe476bef742523d16b423347a36b315b1 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Tue, 12 Jan 2016 16:47:35 -0600 Subject: [PATCH 11/86] Fix Image API 2.x link --- src/main/resources/landing.vm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/landing.vm b/src/main/resources/landing.vm index 036fafce1..1aaee5a13 100644 --- a/src/main/resources/landing.vm +++ b/src/main/resources/landing.vm @@ -28,7 +28,7 @@ - IIIF Image API 2.x + IIIF Image API 2.x #if ($iiif2EndpointEnabled) Enabled From eb7014c16cf11347931660a9f31b68d385436e86 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Wed, 13 Jan 2016 18:29:50 -0600 Subject: [PATCH 12/86] Add SyslogAppender (#26) --- cantaloupe.properties.sample | 14 ++++++++++++++ src/main/resources/logback.xml | 32 ++++++++++++++++++++++++++++++++ website/changes.html | 1 + website/getting-started.html | 1 + website/logging.html | 6 +++++- 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cantaloupe.properties.sample b/cantaloupe.properties.sample index 284cf27a9..61a1f6d50 100644 --- a/cantaloupe.properties.sample +++ b/cantaloupe.properties.sample @@ -386,6 +386,13 @@ log.application.RollingFileAppender.policy = TimeBasedRollingPolicy log.application.RollingFileAppender.TimeBasedRollingPolicy.filename_pattern = /path/to/logs/cantaloupe-%d{yyyy-MM-dd}.log log.application.RollingFileAppender.TimeBasedRollingPolicy.max_history = 30 +# See the "SyslogAppender" section for a list of facilities: +# http://logback.qos.ch/manual/appenders.html +log.application.SyslogAppender.enabled = false +log.application.SyslogAppender.host = +log.application.SyslogAppender.port = 514 +log.application.SyslogAppender.facility = LOCAL0 + #---------------------------------------- # !! Access Log #---------------------------------------- @@ -400,3 +407,10 @@ log.access.RollingFileAppender.pathname = /path/to/logs/access.log log.access.RollingFileAppender.policy = TimeBasedRollingPolicy log.access.RollingFileAppender.TimeBasedRollingPolicy.filename_pattern = /path/to/logs/access-%d{yyyy-MM-dd}.log log.access.RollingFileAppender.TimeBasedRollingPolicy.max_history = 30 + +# See the "SyslogAppender" section for a list of facilities: +# http://logback.qos.ch/manual/appenders.html +log.access.SyslogAppender.enabled = false +log.access.SyslogAppender.host = +log.access.SyslogAppender.port = 514 +log.access.SyslogAppender.facility = LOCAL0 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index ab7968890..bbac0e537 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -40,6 +40,17 @@ + + + + + ${log.application.SyslogAppender.host} + ${log.application.SyslogAppender.port} + ${log.application.SyslogAppender.facility} + + + + @@ -81,6 +92,17 @@ + + + + + ${log.access.SyslogAppender.host} + ${log.access.SyslogAppender.port} + ${log.access.SyslogAppender.facility} + + + + @@ -97,6 +119,11 @@ + + + + + @@ -119,6 +146,11 @@ + + + + + diff --git a/website/changes.html b/website/changes.html index 4f450e658..90a77af52 100644 --- a/website/changes.html +++ b/website/changes.html @@ -10,6 +10,7 @@

    2.1

  • Optimized JaiProcessor for tiled images.
  • Made some aspects of JaiProcessor's JPEG and TIFF output configurable.
  • Made Java2dProcessor's TIFF output compression configurable.
  • +
  • Added SyslogAppenders for the application and access logs.

2.0.1

diff --git a/website/getting-started.html b/website/getting-started.html index 1c2e21c60..5b0bdbe11 100644 --- a/website/getting-started.html +++ b/website/getting-started.html @@ -48,6 +48,7 @@

2.0 to 2.1

  • Add the JaiProcessor.* keys from the sample configuration.
  • Add the Java2dProcessor.tif.compression key from the sample configuration.
  • +
  • Add the log.*.SyslogAppender.* keys from the sample configuration.

1.2 to 2.0

diff --git a/website/logging.html b/website/logging.html index 9a18d1eca..ca505df48 100644 --- a/website/logging.html +++ b/website/logging.html @@ -18,7 +18,7 @@

Log Levels

Appenders

-

Appenders direct log messages to various destinations, like files and/or the console. (The default configuration file logs only to the console.) There are three available appenders — ConsoleAppender, FileAppender, and RollingFileAppender — which can be enabled or disabled in any combination.

+

Appenders direct log messages to various destinations, like files and/or the console. (The default configuration file logs only to the console.) There are several available appenders, which can be enabled or disabled in any combination.

ConsoleAppender
@@ -36,6 +36,10 @@
RollingFileAppender

Currently, the only available rolling policy (log.application.RollingFileAppender.policy) is TimeBasedRollingPolicy, which rolls over the log file based on the value of log.application.RollingFileAppender.TimeBasedRollingPolicy.filename_pattern. log.application.RollingFileAppender.TimeBasedRollingPolicy.max_history defines how many rolled-over log files will be kept.

+
SyslogAppender
+ +

SyslogAppender appends log messages to the syslog, managed by syslogd, on a local or remote host.

+

Access Log

Access logs are written in the W3C Extended Log File Format. To enable or disable the access log, set the value of log.access.ConsoleAppender.enabled to true or false.

From e1e014a811a7eb27fa3bf1dd981117dc25432e02 Mon Sep 17 00:00:00 2001 From: Alex Dolski Date: Thu, 14 Jan 2016 10:56:39 -0600 Subject: [PATCH 13/86] Fix a potential SAXParserException in getSize() caused by leading newline in kdu_jp2info output by reading into an intermediary byte array. This solution has the unfortunate effect of being less efficient. --- pom.xml | 4 ++-- .../cantaloupe/processor/KakaduProcessor.java | 14 +++++++++++++- website/changes.html | 6 ++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 44ac7c074..207459747 100644 --- a/pom.xml +++ b/pom.xml @@ -4,13 +4,13 @@ edu.illinois.library.cantaloupe Cantaloupe jar - 2.0.1 + 2.0.2-SNAPSHOT Cantaloupe https://medusa-project.github.io/cantaloupe/ UTF-8 UTF-8 - 2.0.1 + 2.0.2-SNAPSHOT 1.10.43 2.4 3.4 diff --git a/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java b/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java index 6c45b5f3e..184e7ec3b 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java +++ b/src/main/java/edu/illinois/library/cantaloupe/processor/KakaduProcessor.java @@ -17,6 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; import javax.imageio.ImageIO; @@ -33,6 +34,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.StringReader; import java.math.RoundingMode; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; @@ -195,9 +197,19 @@ public Dimension getSize(File inputFile, SourceFormat sourceFormat) logger.debug("Invoking {}", StringUtils.join(pb.command(), " ")); Process process = pb.start(); + // Ideally we could just call + // DocumentBuilder.parse(process.getInputStream()), but the XML + // output of kdu_jp2info may contain leading whitespace that + // causes a SAXParseException. So, read into a byte array in + // order to trim it, and then parse that. + ByteArrayOutputStream outputBucket = new ByteArrayOutputStream(); + org.apache.commons.io.IOUtils.copy(process.getInputStream(), + outputBucket); + final String outputXml = outputBucket.toString("UTF-8").trim(); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); - Document doc = db.parse(process.getInputStream()); + Document doc = db.parse(new InputSource(new StringReader(outputXml))); XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("//codestream/width"); diff --git a/website/changes.html b/website/changes.html index 1ad626099..98b0ba51c 100644 --- a/website/changes.html +++ b/website/changes.html @@ -4,6 +4,12 @@

Change Log

+

2.0.2

+ +
    +
  • Fixed a SAXParserException in KakaduProcessor that could occur on some platforms.
  • +
+

2.0.1