/*
* Universal Media Server, for streaming any media to DLNA
* compatible renderers based on the http://www.ps3mediaserver.org.
* Copyright (C) 2012 UMS developers.
*
* This program is a free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.stream.ImageInputStream;
import net.pms.image.ImageIORuntimeException;
import net.pms.util.UnknownFormatException;
import com.drew.metadata.Metadata;
/**
* This is a utility class for use with {@link ImageIO}, which mainly contains
* modified versions of static {@link ImageIO} methods.
*
* @author Nadahar
*/
public class ImageIOTools {
protected static final IIORegistry theRegistry = IIORegistry.getDefaultInstance();
// Not to be instantiated
private ImageIOTools() {
}
/**
* A copy of {@link ImageIO#read(InputStream)} that calls
* {{@link #read(ImageInputStream)} instead of
* {@link ImageIO#read(ImageInputStream)} and that returns
* {@link ImageReaderResult} instead of {@link BufferedImage}. This lets
* information about the detected format be retained.
*
* <p><b>
* This method consumes and closes {@code inputStream}.
* </b>
*
* @param inputStream an {@link InputStream} to read from.
*
* @see ImageIO#read(InputStream)
*/
public static ImageReaderResult read(InputStream inputStream) throws IOException {
if (inputStream == null) {
throw new IllegalArgumentException("input == null!");
}
ImageInputStream stream = createImageInputStream(inputStream);
try {
ImageReaderResult result = read(stream);
if (result == null) {
inputStream.close();
}
return result;
} catch (RuntimeException | IOException e) {
try {
inputStream.close();
} catch (Exception e2) {
//Do nothing
}
if (e instanceof RuntimeException) {
throw new ImageIORuntimeException(
"An error occurred while trying to read image: " + e.getMessage(),
(RuntimeException) e
);
}
throw e;
}
}
/**
* A copy of {@link ImageIO#read(ImageInputStream)} that returns
* {@link ImageReaderResult} instead of {@link BufferedImage}. This lets
* information about the detected format be retained.
*
* <b>
* This method consumes and closes {@code stream}.
* </b>
*
* @param stream an {@link ImageInputStream} to read from.
*
* @see ImageIO#read(ImageInputStream)
*/
public static ImageReaderResult read(ImageInputStream stream) throws IOException {
if (stream == null) {
throw new IllegalArgumentException("stream == null!");
}
try {
Iterator<?> iter = ImageIO.getImageReaders(stream);
if (!iter.hasNext()) {
throw new UnknownFormatException("Unable to find a suitable image reader");
}
ImageFormat inputFormat = null;
BufferedImage bufferedImage = null;
ImageReader reader = (ImageReader) iter.next();
try {
// Store the parsing result
inputFormat = ImageFormat.toImageFormat(reader.getFormatName());
reader.setInput(stream, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return bufferedImage != null ? new ImageReaderResult(bufferedImage, inputFormat) : null;
} catch (RuntimeException e) {
throw new ImageIORuntimeException("An error occurred while trying to read image: " + e.getMessage(), e);
} finally {
stream.close();
}
}
/**
* Tries to detect the input image file format using {@link ImageIO} and
* returns the result.
* <p>
* This method does not close {@code inputStream}.
*
* @param inputStream the image whose format to detect.
* @return The {@link ImageFormat} for the input.
* @throws UnknownFormatException if the format could not be determined.
* @throws IOException if an IO error occurred.
*/
public static ImageFormat detectFileFormat(InputStream inputStream) throws IOException {
if (inputStream == null) {
throw new IllegalArgumentException("input == null!");
}
try (ImageInputStream stream = createImageInputStream(inputStream)) {
Iterator<?> iter = ImageIO.getImageReaders(stream);
if (!iter.hasNext()) {
throw new UnknownFormatException("Unable to find a suitable image reader");
}
ImageReader reader = (ImageReader) iter.next();
ImageFormat format = ImageFormat.toImageFormat(reader.getFormatName());
if (format == null) {
throw new UnknownFormatException("Unable to determine image format");
}
return format;
} catch (RuntimeException e) {
throw new ImageIORuntimeException("An error occurred while trying to detect image format: " + e.getMessage(), e);
}
}
/**
* Tries to gather the data needed to populate a {@link ImageInfo} instance
* describing the input image.
*
* <p>
* This method does not close {@code inputStream}.
*
* @param inputStream the image whose information to gather.
* @param size the size of the image in bytes or
* {@link ImageInfo#SIZE_UNKNOWN} if it can't be determined.
* @param metadata the {@link Metadata} instance to embed in the resulting
* {@link ImageInfo} instance.
* @param applyExifOrientation whether or not Exif orientation should be
* compensated for when setting width and height. This will also
* reset the Exif orientation information. <b>Changes will be
* applied to the {@code metadata} argument instance</b>.
* @return An {@link ImageInfo} instance describing the input image.
* @throws UnknownFormatException if the format could not be determined.
* @throws IOException if an IO error occurred.
*/
public static ImageInfo readImageInfo(InputStream inputStream, long size, Metadata metadata, boolean applyExifOrientation) throws IOException {
if (inputStream == null) {
throw new IllegalArgumentException("input == null!");
}
try (ImageInputStream stream = createImageInputStream(inputStream)) {
Iterator<?> iter = ImageIO.getImageReaders(stream);
if (!iter.hasNext()) {
throw new UnknownFormatException("Unable to find a suitable image reader");
}
ImageReader reader = (ImageReader) iter.next();
try {
int width = -1;
int height = -1;
ImageFormat format = ImageFormat.toImageFormat(reader.getFormatName());
if (format == null) {
throw new UnknownFormatException("Unable to determine image format");
}
ColorModel colorModel = null;
try {
reader.setInput(stream, true, true);
Iterator<ImageTypeSpecifier> iterator = reader.getImageTypes(0);
if (iterator.hasNext()) {
colorModel = iterator.next().getColorModel();
}
width = reader.getWidth(0);
height = reader.getHeight(0);
} catch (RuntimeException e) {
throw new ImageIORuntimeException("Error reading image information: " + e.getMessage(), e);
}
boolean imageIOSupport;
if (format == ImageFormat.TIFF) {
// ImageIO thinks that it can read some "TIFF like" RAW formats,
// but fails when it actually tries, so we have to test it.
try {
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(1, 1));
reader.read(0, param);
imageIOSupport = true;
} catch (Exception e) {
// Catch anything here, we simply want to test if it fails.
imageIOSupport = false;
}
} else {
imageIOSupport = true;
}
ImageInfo imageInfo = ImageInfo.create(
width,
height,
format,
size,
colorModel,
metadata,
applyExifOrientation,
imageIOSupport
);
return imageInfo;
} finally {
reader.dispose();
}
}
}
/**
* A copy of {@link ImageIO#createImageInputStream(Object)} that ignores
* {@link ImageIO} configuration and never caches to disk. This is intended
* used on relatively small images and caching to disk is very expensive
* compared to keeping a copy in memory while doing the source analysis.
*
* @see ImageIO#createImageInputStream(Object)
*/
public static ImageInputStream createImageInputStream(Object input)
throws IOException {
if (input == null) {
throw new IllegalArgumentException("input == null!");
}
Iterator<ImageInputStreamSpi> iter;
// Ensure category is present
try {
iter = theRegistry.getServiceProviders(ImageInputStreamSpi.class, true);
} catch (IllegalArgumentException e) {
return null;
}
while (iter.hasNext()) {
ImageInputStreamSpi spi = (ImageInputStreamSpi)iter.next();
if (spi.getInputClass().isInstance(input)) {
try {
return spi.createInputStreamInstance(input, false, null);
} catch (IOException e) {
throw new IIOException("Can't create cache file!", e);
}
}
}
return null;
}
/**
* This is a wrapper around
* {@link ImageIO#write(RenderedImage, String, OutputStream)}
* that translate any thrown {@link RuntimeException} to an
* {@link ImageIORuntimeException} because {@link ImageIO} has the nasty
* habit of throwing {@link RuntimeException}s when something goes wrong.
*
* @see ImageIO#write(RenderedImage, String, OutputStream)
*/
public static boolean imageIOWrite(RenderedImage im, String formatName, OutputStream output) throws IOException {
try {
return ImageIO.write(im, formatName, output);
} catch (RuntimeException e) {
throw new ImageIORuntimeException(e.getMessage(), e);
}
}
/**
* A simple container for more than one return value.
*/
public static class ImageReaderResult {
public final BufferedImage bufferedImage;
public final ImageFormat imageFormat;
public final int width;
public final int height;
public ImageReaderResult(BufferedImage bufferedImage, ImageFormat imageFormat) {
this.bufferedImage = bufferedImage;
this.imageFormat = imageFormat;
this.width = bufferedImage == null ? -1 : bufferedImage.getWidth();
this.height = bufferedImage == null ? -1 : bufferedImage.getHeight();
}
}
}