/*
* 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.color.ColorSpace;
import java.awt.image.ColorModel;
import java.io.Serializable;
import java.util.Arrays;
import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.drew.metadata.Metadata;
import net.pms.dlna.DLNAImage;
import net.pms.dlna.DLNAImageInputStream;
import net.pms.dlna.DLNAResource;
import net.pms.dlna.DLNAThumbnail;
import net.pms.dlna.DLNAThumbnailInputStream;
import net.pms.image.ExifInfo.ExifParseInfo;
import net.pms.util.InvalidStateException;
import net.pms.util.ParseException;
/**
* This holds information about a given image, and is used as a standard image
* information container in {@link DLNAResource}, {@link DLNAThumbnail},
* {@link DLNAThumbnailInputStream}, {@link DLNAImageInputStream},
* {@link DLNAImage} and {@link Image}.
*
* Size might not always be available (live- or web streams for example), and
* will in those cases be and be set to {@link #SIZE_UNKNOWN}. Unknown integer
* values are and should be set to {@link #UNKNOWN}.
*
* The class itself is immutable although some of the objects it or it's
* subclasses references might not be. The definition of immutable requires any
* immutable class to also be {@code final] because otherwise a subclass could
* break the immutability. Instead, any subclasses must make sure not to break
* the immutability.
*
* @author Nadahar
*/
@SuppressWarnings("serial")
public abstract class ImageInfo implements Serializable {
/*
* Please note: This class is packed and stored in the database. Any changes
* to the data structure (fields) will invalidate any instances already
* stored.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ImageInfo.class);
/**
* Used to symbolize an unknown image size.
*/
public static final long SIZE_UNKNOWN = Long.MIN_VALUE;
/**
* Used to symbolize unknown int values.
*/
public static final int UNKNOWN = Integer.MIN_VALUE;
protected final int width;
protected final int height;
protected final ImageFormat format;
protected final long size;
protected final int bitDepth;
protected final int numComponents;
protected final ColorSpace colorSpace;
protected final ColorSpaceType colorSpaceType;
protected final boolean imageIOSupport;
/**
* This field is used to make the parsing result available to subclass
* constructors so that they can initialize their final fields. It is only
* used during construction, and isn't kept when serialized.
*/
protected transient ParseInfo parsedInfo = null;
/**
* This method is responsible for creating a {@link ParseInfo} instance of
* the correct type for that subclass.
*
* @return A new {@link ParseInfo} instance of the correct type.
*/
protected abstract ParseInfo createParseInfo();
/**
* This method is responsible for parsing a {@link Metadata} instance and
* storing it in a {@link ParseInfo} instance.
*
* @param metadata the {@link Metadata} instance to parse.
* @throws ParseException if the parsing fails.
*/
protected abstract void parseMetadata(Metadata metadata) throws ParseException;
/**
* Creates a new {@link ImageInfo} instance populated with the information
* in the parameters.
*
* @param width the width of the image in pixels.
* @param height the height of the image in pixels.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @param colorModel the {@link ColorModel} used in the image or
* {@code null} if unknown.
* @param metadata the {@link Metadata} describing the image.
* @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>.
* @param imageIOSupport whether or not {@link ImageIO} can read/parse this
* image.
* @throws ParseException if {@code format} is {@code null} and parsing the
* format from {@code metadata} fails or parsing of
* {@code metadata} fails.
*/
public static ImageInfo create(
int width,
int height,
ImageFormat format,
long size,
ColorModel colorModel,
Metadata metadata,
boolean applyExifOrientation,
boolean imageIOSupport
) throws ParseException {
if (format == null && metadata != null) {
format = ImageFormat.toImageFormat(metadata);
if (format == null) {
throw new ParseException("Unable to determine image format from metadata");
}
}
if (format == null) {
throw new IllegalArgumentException("Both format and metadata cannot be null");
}
if (format.isRaw()) {
return new RAWInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
}
switch (format) {
case ICNS:
case IFF:
case PICT:
case PNM:
case RGBE:
case SGI:
case TGA:
case WBMP:
return new GenericImageInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case BMP:
return new BMPInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case CUR:
return new CURInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case DCX:
return new PCXInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case GIF:
return new GIFInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case ICO:
return new ICOInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case JPEG:
return new JPEGInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case PCX:
return new PCXInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case PNG:
return new PNGInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case PSD:
return new PSDInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case TIFF:
return new TIFFInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
case WEBP:
return new WebPInfo(
width, height, format, size, colorModel, metadata,
applyExifOrientation, imageIOSupport
);
default:
throw new IllegalStateException("Format " + format + " is unknown for ImageInfo.create()");
}
}
/**
* Creates a new {@link ImageInfo} instance populated with the information in
* the parameters.
*
* @param width the width of the image in pixels.
* @param height the height of the image in pixels.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @param colorModel the {@link ColorModel} used in the image or
* {@code null} if unknown.
* @param metadata the {@link Metadata} describing the image.
* @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>.
* @param imageIOSupport whether or not {@link ImageIO} can read/parse this
* image.
* @throws ParseException if parsing of {@code metadata} fails.
*/
protected ImageInfo(
int width,
int height,
ImageFormat format,
long size,
ColorModel colorModel,
Metadata metadata,
boolean applyExifOrientation,
boolean imageIOSupport
) throws ParseException {
parsedInfo = createParseInfo();
parseMetadata(metadata);
compareResolution(width, height, parsedInfo);
if (
(width < 1 || height < 1) &&
parsedInfo.width != null && parsedInfo.height != null
) {
width = parsedInfo.width.intValue();
height = parsedInfo.height.intValue();
}
if (
applyExifOrientation &&
parsedInfo instanceof ExifParseInfo &&
((ExifParseInfo) parsedInfo).exifOrientation != null &&
ImagesUtil.isExifAxesSwapNeeded(((ExifParseInfo) parsedInfo).exifOrientation)
) {
this.width = height;
this.height = width;
} else {
this.width = width;
this.height = height;
}
compareFormat(format, parsedInfo);
this.format = format != null ? format : parsedInfo.format;
this.size =
!applyExifOrientation ||
!(parsedInfo instanceof ExifParseInfo) ||
((ExifParseInfo) parsedInfo).exifOrientation == ExifOrientation.TOP_LEFT ?
size :
SIZE_UNKNOWN;
if (colorModel == null ||
// ImageIO will parse CMYK JPEGs with a RGB color model.
// Prefer the parsed info in that situation
this instanceof JPEGInfo && parsedInfo.colorSpaceType == ColorSpaceType.TYPE_CMYK
) {
this.bitDepth = parsedInfo.bitDepth != null ? parsedInfo.bitDepth : UNKNOWN;
this.numComponents = parsedInfo.numComponents != null ? parsedInfo.numComponents : UNKNOWN;
this.colorSpace = null;
this.colorSpaceType = parsedInfo.colorSpaceType;
} else {
int bitDepth = UNKNOWN;
if (colorModel.getNumComponents() > 0) {
try {
bitDepth = ImagesUtil.getBitDepthFromArray(colorModel.getComponentSize());
} catch (InvalidStateException e) {
LOGGER.trace(
"Unexpected bit depth array retrieved from ColorModel: {}",
Arrays.toString(colorModel.getComponentSize())
);
}
}
int numComponents = colorModel.getNumComponents();
ColorSpaceType colorSpaceType = ColorSpaceType.toColorSpaceType(colorModel.getColorSpace().getType());
compareColorModel(bitDepth, numComponents, colorSpaceType, parsedInfo);
this.bitDepth =
bitDepth == UNKNOWN && parsedInfo.bitDepth != null ?
parsedInfo.bitDepth.intValue() :
bitDepth;
this.numComponents =
numComponents == UNKNOWN && parsedInfo.numComponents != null ?
parsedInfo.numComponents.intValue() :
numComponents;
this.colorSpace = colorModel.getColorSpace();
this.colorSpaceType =
colorSpaceType == null && parsedInfo.colorSpaceType != null ?
parsedInfo.colorSpaceType :
colorSpaceType;
}
this.imageIOSupport = imageIOSupport;
}
/**
* Creates a new {@link ImageInfo} instance populated with the information
* in the parameters.
*
* @param width the width of the image in pixels.
* @param height the height of the image in pixels.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @param bitDepth the number of bits per color model component for this
* image.
* @param numComponents the number of color model components for this image.
* @param colorSpace the {@link ColorSpace} of this image.
* @param colorSpaceType the {@link ColorSpaceType} of this image. Only used
* if {@code ColorSpace} is {@code null}.
* @param metadata the {@link Metadata} describing the image.
* @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>.
* @param imageIOSupport whether or not {@link ImageIO} can read/parse this
* image.
* @throws ParseException if {@code format} is {@code null} and parsing the
* format from {@code metadata} fails or parsing of
* {@code metadata} fails.
*/
public static ImageInfo create(
int width,
int height,
ImageFormat format,
long size,
int bitDepth,
int numComponents,
ColorSpace colorSpace,
ColorSpaceType colorSpaceType,
Metadata metadata,
boolean applyExifOrientation,
boolean imageIOSupport
) throws ParseException {
if (format == null && metadata != null) {
format = ImageFormat.toImageFormat(metadata);
if (format == null) {
throw new ParseException("Unable to determine image format from metadata");
}
}
if (format == null) {
throw new IllegalArgumentException("Both format and metadata cannot be null");
}
if (format.isRaw()) {
return new RAWInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
}
switch (format) {
case ICNS:
case IFF:
case PICT:
case PNM:
case RGBE:
case SGI:
case TGA:
case WBMP:
return new GenericImageInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case BMP:
return new BMPInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case CUR:
return new CURInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case DCX:
return new PCXInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case GIF:
return new GIFInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case ICO:
return new ICOInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case JPEG:
return new JPEGInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case PCX:
return new PCXInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case PNG:
return new PNGInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case PSD:
return new PSDInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case TIFF:
return new TIFFInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
case WEBP:
return new WebPInfo(
width, height, format, size, bitDepth, numComponents,
colorSpace, colorSpaceType, metadata, applyExifOrientation,
imageIOSupport
);
default:
throw new IllegalStateException("Format " + format + " is unknown for this ImageInfo.create()");
}
}
/**
* Creates a new {@link ImageInfo} instance populated with the information
* in the parameters.
*
* @param width the width of the image in pixels.
* @param height the height of the image in pixels.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @param bitDepth the number of bits per color model component for this
* image.
* @param numComponents the number of color model components for this
* image.
* @param colorSpace the {@link ColorSpace} of this image.
* @param colorSpaceType the {@link ColorSpaceType} of this image. Only
* used if {@code ColorSpace} is {@code null}.
* @param metadata the {@link Metadata} describing the image.
* @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>.
* @param imageIOSupport whether or not {@link ImageIO} can read/parse this
* image.
* @throws ParseException if parsing of {@code metadata} fails.
*/
protected ImageInfo(
int width,
int height,
ImageFormat format,
long size,
int bitDepth,
int numComponents,
ColorSpace colorSpace,
ColorSpaceType colorSpaceType,
Metadata metadata,
boolean applyExifOrientation,
boolean imageIOSupport
) throws ParseException {
parsedInfo = createParseInfo();
parseMetadata(metadata);
compareResolution(width, height, parsedInfo);
if (
(width < 1 || height < 1) &&
parsedInfo.width != null && parsedInfo.height != null
) {
width = parsedInfo.width.intValue();
height = parsedInfo.height.intValue();
}
if (
applyExifOrientation &&
parsedInfo instanceof ExifParseInfo &&
((ExifParseInfo) parsedInfo).exifOrientation != null &&
ImagesUtil.isExifAxesSwapNeeded(((ExifParseInfo) parsedInfo).exifOrientation)
) {
this.width = height;
this.height = width;
} else {
this.width = width;
this.height = height;
}
compareFormat(format, parsedInfo);
this.format = format != null ? format : parsedInfo.format;
this.size =
!applyExifOrientation ||
!(parsedInfo instanceof ExifParseInfo) ||
((ExifParseInfo) parsedInfo).exifOrientation == ExifOrientation.TOP_LEFT ?
size :
SIZE_UNKNOWN;
if (this instanceof JPEGInfo && parsedInfo.colorSpaceType == ColorSpaceType.TYPE_CMYK) {
// ImageIO will parse CMYK JPEGs with a RGB color model.
// Prefer the parsed info in that situation
this.bitDepth = parsedInfo.bitDepth != null ? parsedInfo.bitDepth.intValue() : bitDepth;
this.numComponents = parsedInfo.numComponents != null ? parsedInfo.numComponents.intValue() : numComponents;
this.colorSpace = null;
this.colorSpaceType = parsedInfo.colorSpaceType;
} else {
colorSpaceType = colorSpace != null ? ColorSpaceType.toColorSpaceType(colorSpace.getType()) : colorSpaceType;
compareColorModel(bitDepth, numComponents, colorSpaceType, parsedInfo);
this.bitDepth =
bitDepth == UNKNOWN && parsedInfo.bitDepth != null ?
parsedInfo.bitDepth.intValue() :
bitDepth;
this.numComponents =
numComponents == UNKNOWN && parsedInfo.numComponents != null ?
parsedInfo.numComponents.intValue() :
numComponents;
this.colorSpace = colorSpace;
this.colorSpaceType =
colorSpaceType == null && parsedInfo.colorSpaceType != null ?
parsedInfo.colorSpaceType :
colorSpaceType;
}
this.imageIOSupport = imageIOSupport;
}
/**
* Tries to create an {@link ImageInfo} instance from {@link Metadata}. If
* {@code metadata} is null or can't be parsed, an instance with invalid
* values is created or an {@link ParseException} is thrown depending on
* {@code throwOnParseFailure}. The {@link ColorModel} in this instance will
* be {@code null}. This constructor should only be used if {@link ImageIO}
* can't parse the source. Instances created with this constructor will have
* {@code isImageIOSupport()} set to false.
*
* @param metadata the {@link Metadata} describing the image.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @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>.
* @param throwOnParseFailure if a {@link ParseException} should be thrown
* instead of returning an instance with invalid resolution if
* parsing of resolution fails.
* @throws ParseException if {@code format} is {@code null} and parsing the
* format from {@code metadata} fails or parsing of
* {@code metadata} fails.
*/
public static ImageInfo create(
Metadata metadata,
ImageFormat format,
long size,
boolean applyExifOrientation,
boolean throwOnParseFailure
) throws ParseException {
return create(UNKNOWN, UNKNOWN, metadata, format, size, applyExifOrientation, throwOnParseFailure);
}
/**
* Tries to create an {@link ImageInfo} instance from {@link Metadata}. If
* {@code metadata} is null or can't be parsed, an instance with invalid
* values is created or an {@link ParseException} is thrown depending on
* {@code throwOnParseFailure}. The {@link ColorModel} in this instance will
* be {@code null}. This constructor should only be used if {@link ImageIO}
* can't parse the source. Instances created with this constructor will have
* {@code isImageIOSupport()} set to false.
*
* @param width the width of the image in pixels.
* @param height the height of the image in pixels.
* @param metadata the {@link Metadata} describing the image.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @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>.
* @param throwOnParseFailure if a {@link ParseException} should be thrown
* instead of returning an instance with invalid resolution if
* parsing of resolution fails.
* @throws ParseException if {@code format} is {@code null} and parsing the
* format from {@code metadata} fails or parsing of
* {@code metadata} fails.
*/
public static ImageInfo create(
int width,
int height,
Metadata metadata,
ImageFormat format,
long size,
boolean applyExifOrientation,
boolean throwOnParseFailure
) throws ParseException {
if (format == null && metadata != null) {
format = ImageFormat.toImageFormat(metadata);
if (format == null) {
throw new ParseException("Unable to determine image format from metadata");
}
}
if (format == null) {
throw new IllegalArgumentException("Both format and metadata cannot be null");
}
if (format.isRaw()) {
return new RAWInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
}
switch (format) {
case ICNS:
case IFF:
case PICT:
case PNM:
case RGBE:
case SGI:
case TGA:
case WBMP:
return new GenericImageInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case BMP:
return new BMPInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case CUR:
return new CURInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case DCX:
return new PCXInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case GIF:
return new GIFInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case ICO:
return new ICOInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case JPEG:
return new JPEGInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case PCX:
return new PCXInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case PNG:
return new PNGInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case PSD:
return new PSDInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case TIFF:
return new TIFFInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
case WEBP:
return new WebPInfo(
width, height, metadata, format, size,
applyExifOrientation, throwOnParseFailure
);
default:
throw new IllegalStateException("Format " + format + " is unknown for ImageInfo.create()");
}
}
/**
* Tries to create an {@link ImageInfo} instance from {@link Metadata}. If
* {@code metadata} is null or can't be parsed, an instance with invalid
* values is created or an {@link ParseException} is thrown depending on
* {@code throwOnParseFailure}. The {@link ColorModel} in this instance will
* be {@code null}. This constructor should only be used if {@link ImageIO}
* can't parse the source. Instances created with this constructor will have
* {@code isImageIOSupport()} set to false.
*
* @param width the width of the image in pixels.
* @param height the height of the image in pixels.
* @param metadata the {@link Metadata} describing the image.
* @param format the {@link ImageFormat} for the image.
* @param size the size of the image in bytes or {@link #SIZE_UNKNOWN}.
* @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>.
* @param throwOnParseFailure if a {@link ParseException} should be thrown
* instead of returning an instance with invalid resolution if
* parsing of resolution fails.
* @throws ParseException if a parsing error occurs or parsing fails.
*/
protected ImageInfo(
int width,
int height,
Metadata metadata,
ImageFormat format,
long size,
boolean applyExifOrientation,
boolean throwOnParseFailure
) throws ParseException {
parsedInfo = createParseInfo();
parseMetadata(metadata);
compareResolution(width, height, parsedInfo);
if (
(width < 1 || height < 1) &&
parsedInfo.width != null && parsedInfo.height != null
) {
width = parsedInfo.width.intValue();
height = parsedInfo.height.intValue();
}
if (throwOnParseFailure && (width < 0 || height < 0)) {
throw new ParseException("Failed to parse image resolution from metadata");
}
if (
applyExifOrientation &&
parsedInfo instanceof ExifParseInfo &&
((ExifParseInfo) parsedInfo).exifOrientation != null &&
ImagesUtil.isExifAxesSwapNeeded(((ExifParseInfo) parsedInfo).exifOrientation)
) {
this.width = height;
this.height = width;
} else {
this.width = width;
this.height = height;
}
compareFormat(format, parsedInfo);
this.format = format != null ? format : parsedInfo.format;
this.size =
!applyExifOrientation ||
!(parsedInfo instanceof ExifParseInfo) ||
((ExifParseInfo) parsedInfo).exifOrientation == ExifOrientation.TOP_LEFT ?
size :
SIZE_UNKNOWN;
this.bitDepth = parsedInfo.bitDepth != null ? parsedInfo.bitDepth.intValue() : UNKNOWN;
this.numComponents = parsedInfo.numComponents != null ? parsedInfo.numComponents.intValue() : UNKNOWN;
this.colorSpace = null;
this.colorSpaceType = parsedInfo.colorSpaceType;
this.imageIOSupport = false; // Implied by calling this constructor
}
/**
* Copy constructor
*/
protected ImageInfo(
int width,
int height,
ImageFormat format,
long size,
int bitDepth,
int numComponents,
ColorSpace colorSpace,
ColorSpaceType colorSpaceType,
boolean imageIOSupport
) {
this.width = width;
this.height = height;
this.format = format;
this.size = size;
this.bitDepth = bitDepth;
this.numComponents = numComponents;
this.colorSpace = colorSpace;
this.colorSpaceType = colorSpaceType;
this.imageIOSupport = imageIOSupport;
}
/**
* @return The image width or {@link #UNKNOWN} if unknown.
*/
public int getWidth() {
return width;
}
/**
* @return The image height or {@link #UNKNOWN} if unknown.
*/
public int getHeight() {
return height;
}
/**
* @return The {@link ImageFormat} or {@code null} if unknown.
*/
public ImageFormat getFormat() {
return format;
}
/**
* @return The image size in bytes or {@link #SIZE_UNKNOWN}
* if the size is unknown.
*/
public long getSize() {
return size;
}
/**
* @return The {@link ColorSpace} or {@code null} if unknown.
*/
public ColorSpace getColorSpace() {
return colorSpace;
}
/**
* @return The {@link ColorSpaceType} or {@code null} if unknown.
*/
public ColorSpaceType getColorSpaceType() {
if (colorSpace != null) {
return ColorSpaceType.toColorSpaceType(colorSpace.getType());
}
return colorSpaceType;
}
/**
* @return The number of bits per pixel or {@link #UNKNOWN} if unknown.
*/
public int getBitsPerPixel() {
return bitDepth < 0 || numComponents < 0 ? UNKNOWN : bitDepth * numComponents;
}
/**
* The number of components describe how many "channels" the color model
* has. A grayscale image without alpha has 1, a RGB image without alpha has
* 3, a RGB image with alpha has 4 etc.
*
* @return The number of components in the {@link ColorModel} or
* {@link #UNKNOWN} if unknown.
*/
public int getNumComponents() {
return numComponents;
}
/**
* @return The number of bits per color "channel" or {@link #UNKNOWN} if
* unknown.
*
* @see #getBitPerPixel()
* @see #getNumColorComponents()
*/
public int getBitDepth() {
return bitDepth;
}
/**
* @return Whether or not {@link ImageIO} can read/parse this image.
*/
public boolean isImageIOSupported() {
return imageIOSupport;
}
/**
* @return The {@link ExifOrientation} or {@link ExifOrientation#TOP_LEFT} if unknown.
*/
public ExifOrientation getExifOrientation() {
if (!(this instanceof ExifInfo)) {
return ExifOrientation.TOP_LEFT;
}
return ((ExifInfo) this).exifOrientation != null ?
((ExifInfo) this).exifOrientation :
ExifOrientation.TOP_LEFT
;
}
/**
* @return A copy of this {@link ImageInfo} instance.
*/
public abstract ImageInfo copy();
protected abstract void buildToString(StringBuilder sb);
@Override
public String toString() {
StringBuilder sb = new StringBuilder(80);
sb.append(getClass().getSimpleName())
.append(": [Format = ").append(format)
.append(", Resolution = ").append(width == UNKNOWN ? "Unknown" : width)
.append("×").append(height == UNKNOWN ? "Unknown" : height)
.append(", Size = ").append(size == SIZE_UNKNOWN ? "Unknown" : size)
.append(", Bit Depth = ").append(bitDepth == UNKNOWN ? "Unknown" : bitDepth)
.append(", Number of Components = ").append(numComponents == UNKNOWN ? "Unknown" : numComponents);
if (colorSpace != null) {
sb.append(", Color Space = [");
for (int i = 0; i < colorSpace.getNumComponents(); i++) {
if (i != 0) {
sb.append(", ");
}
sb.append(colorSpace.getName(i));
}
sb.append("]");
}
if (colorSpaceType != null) {
sb.append(", Color Space Type = ").append(colorSpaceType);
}
sb.append(", ImageIO Support = ").append(imageIOSupport ? "True" : "False");
buildToString(sb);
sb.append("]");
return sb.toString();
}
/**
* Compares the parsed and the given resolution and logs a warning if they
* mismatch.
*/
protected void compareResolution(int width, int height, ParseInfo parsedInfo) {
if (parsedInfo == null) {
return;
}
int parsedWidth = parsedInfo.width != null ? parsedInfo.width.intValue() : UNKNOWN;
int parsedHeight = parsedInfo.height!= null ? parsedInfo.height.intValue() : UNKNOWN;
if (this instanceof RAWInfo) {
// DCRaw decodes pixels that's normally hidden because they are too
// expensive to decode for CPU restrained devices, so the resolution
// might be some pixels larger.
if (parsedInfo.width != null && width >= parsedInfo.width.intValue() && width <= (parsedInfo.width.intValue() + 40)) {
parsedWidth = Integer.valueOf(width);
}
if (parsedInfo.height != null && height >= parsedInfo.height.intValue() && height <= (parsedInfo.height.intValue() + 40)) {
parsedHeight = Integer.valueOf(height);
}
}
if (
width > 0 && parsedInfo.width != null && width != parsedWidth ||
height > 0 && parsedInfo.height != null && height != parsedHeight
) {
LOGGER.debug(
"Warning: Parsed image resolution ({} x {}) mismatches given image " +
"resolution ({} x {}) - using given resolution",
parsedInfo.width,
parsedInfo.height,
width,
height
);
}
}
/**
* Compares the parsed and the given {@link ImageFormat} and logs a warning
* if they mismatch.
*/
protected void compareFormat(ImageFormat format, ParseInfo parsedInfo) {
if (parsedInfo == null) {
return;
}
if (parsedInfo.format != null && format != null && format != parsedInfo.format) {
LOGGER.debug(
"Warning: Parsed image format ({}) mismatches given image " +
"format ({}) - using given format",
parsedInfo.format,
format
);
}
}
/**
* Compares the parsed and the given color model information and logs a
* warning if any information mismatch.
*/
protected void compareColorModel(int bitDepth, int numComponents, ColorSpaceType colorSpaceType, ParseInfo parsedInfo) {
if (parsedInfo == null) {
return;
}
if (bitDepth != UNKNOWN && parsedInfo.bitDepth != null && bitDepth != parsedInfo.bitDepth.intValue()) {
if (
!(this instanceof GIFInfo) ||
parsedInfo.bitDepth >= 8
) {
// It seems like ImageIO will parse GIFs with any bit depth as 8, no reason to log that
LOGGER.debug(
"Warning: Parsed image bit depth ({}) mismatches given color model " +
"bit depth ({}) - using given color model bit depth",
parsedInfo.bitDepth,
bitDepth
);
}
}
if (numComponents != UNKNOWN && parsedInfo.numComponents != null && numComponents != parsedInfo.numComponents.intValue()) {
LOGGER.debug(
"Warning: Parsed image number of components ({}) mismatches given color model " +
"number of components ({}) - using given color model number of components",
parsedInfo.numComponents,
numComponents
);
}
if (colorSpaceType != null && parsedInfo.colorSpaceType != null && colorSpaceType != parsedInfo.colorSpaceType) {
if (
!(this instanceof JPEGInfo) ||
parsedInfo.colorSpaceType != ColorSpaceType.TYPE_YCbCr &&
colorSpaceType != ColorSpaceType.TYPE_RGB
) {
// ImageIO (TwelveMonkeys) will convert YCbCr to RGB when reading JPEGs, no reason to log that
LOGGER.debug(
"Warning: Parsed image color space type ({}) mismatches given color model " +
"color space type ({}) - using given color model color space type",
parsedInfo.colorSpaceType,
colorSpaceType
);
}
}
}
protected static class ParseInfo {
Integer width;
Integer height;
ImageFormat format;
Integer bitDepth;
Integer numComponents;
ColorSpaceType colorSpaceType;
}
}