package net.pms.image;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.filters.Canvas;
import net.coobird.thumbnailator.geometry.Positions;
import net.pms.dlna.DLNAImage;
import net.pms.dlna.DLNAImageProfile;
import net.pms.dlna.DLNAImageProfile.DLNAComplianceResult;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.DLNAThumbnail;
import net.pms.image.ImageIOTools.ImageReaderResult;
import net.pms.image.thumbnailator.ExifFilterUtils;
import net.pms.util.BufferedImageType;
import net.pms.util.InvalidStateException;
import net.pms.util.ParseException;
import net.pms.util.ResettableInputStream;
import net.pms.util.UnknownFormatException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.drew.imaging.FileType;
import com.drew.imaging.FileTypeDetector;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.imaging.bmp.BmpMetadataReader;
import com.drew.imaging.gif.GifMetadataReader;
import com.drew.imaging.ico.IcoMetadataReader;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.pcx.PcxMetadataReader;
import com.drew.imaging.png.PngMetadataReader;
import com.drew.imaging.psd.PsdMetadataReader;
import com.drew.imaging.raf.RafMetadataReader;
import com.drew.imaging.tiff.TiffMetadataReader;
import com.drew.imaging.webp.WebpMetadataReader;
import com.drew.lang.RandomAccessReader;
import com.drew.lang.RandomAccessStreamReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
public class ImagesUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ImagesUtil.class);
/**
* Parses an image file and stores the results in the given
* {@link DLNAMediaInfo}. Parsing is performed using both
* <a href=https://github.com/drewnoakes/metadata-extractor>Metadata Extractor</a>
* and {@link ImageIO}. While Metadata Extractor offers more detailed
* information, {@link ImageIO} offers information that is convenient for
* image transformation with {@link ImageIO}. Parsing will be performed if
* just one of the two methods produces results, but some details will be
* missing if either one failed.
* <p><b>
* This method consumes and closes {@code inputStream}.
* </b>
* @param file the {@link File} to parse.
* @param media the {@link DLNAMediaInfo} instance to store the parsing
* results to.
* @throws IOException if an IO error occurs or no information can be parsed.
*
*/
public static void parseImage(File file, DLNAMediaInfo media) throws IOException {
final int MAX_BUFFER = 1048576; // 1 MB
if (file == null) {
throw new IllegalArgumentException("parseImage: file cannot be null");
}
if (media == null) {
throw new IllegalArgumentException("parseImage: media cannot be null");
}
boolean trace = LOGGER.isTraceEnabled();
if (trace) {
LOGGER.trace("Parsing image file \"{}\"", file.getAbsolutePath());
}
long size = file.length();
ResettableInputStream inputStream = new ResettableInputStream(Files.newInputStream(file.toPath()), MAX_BUFFER);
try {
Metadata metadata = null;
FileType fileType = null;
try {
fileType = FileTypeDetector.detectFileType(inputStream);
metadata = getMetadata(inputStream, fileType);
} catch (IOException e) {
metadata = new Metadata();
LOGGER.debug("Error reading \"{}\": {}", file.getAbsolutePath(), e.getMessage());
LOGGER.trace("", e);
} catch (ImageProcessingException e) {
metadata = new Metadata();
LOGGER.debug(
"Error parsing {} metadata for \"{}\": {}",
fileType.toString().toUpperCase(Locale.ROOT),
file.getAbsolutePath(),
e.getMessage()
);
LOGGER.trace("", e);
}
ImageFormat format = ImageFormat.toImageFormat(fileType);
if (format == null || format == ImageFormat.TIFF) {
ImageFormat tmpformat = ImageFormat.toImageFormat(metadata);
if (tmpformat != null) {
format = tmpformat;
}
}
if (inputStream.isFullResetAvailable()) {
inputStream.fullReset();
} else {
// If we can't reset it, close it and create a new
inputStream.close();
inputStream = new ResettableInputStream(Files.newInputStream(file.toPath()), MAX_BUFFER);
}
ImageInfo imageInfo = null;
try {
imageInfo = ImageIOTools.readImageInfo(inputStream, size , metadata, false);
} catch (UnknownFormatException | IIOException | ParseException e) {
if (format == null) {
throw new UnknownFormatException(
"Unable to recognize image format for \"" + file.getAbsolutePath() + "\" - parsing failed",
e
);
}
LOGGER.debug(
"Unable to parse \"{}\" with ImageIO because the format is unsupported, image information will be limited",
file.getAbsolutePath()
);
LOGGER.trace("ImageIO parse failure reason: {}", e.getMessage());
// Gather basic information from the data we have
if (metadata != null) {
try {
imageInfo = ImageInfo.create(metadata, format, size, true, true);
} catch (ParseException pe) {
imageInfo = null;
LOGGER.debug("Unable to parse metadata for \"{}\": {}", file.getAbsolutePath(), pe.getMessage());
LOGGER.trace("", pe);
}
}
}
if (imageInfo == null && format == null) {
throw new ParseException("Parsing of \"" + file.getAbsolutePath() + "\" failed");
}
if (format == null) {
format = imageInfo.getFormat();
} else if (imageInfo != null && imageInfo.getFormat() != null && format != imageInfo.getFormat()) {
if (imageInfo.getFormat() == ImageFormat.TIFF && format.isRaw()) {
if (format == ImageFormat.ARW && !isARW(metadata)) {
// XXX Remove this if https://github.com/drewnoakes/metadata-extractor/issues/217 is fixed
// Metadata extractor misidentifies some Photoshop created TIFFs for ARW, correct it
format = ImageFormat.toImageFormat(metadata);
if (format == null) {
format = ImageFormat.TIFF;
}
LOGGER.trace(
"Correcting misidentified image format ARW to {} for \"{}\"",
format,
file.getAbsolutePath()
);
} else {
/*
* ImageIO recognizes many RAW formats as TIFF because
* of their close relationship let's treat them as what
* they really are.
*/
imageInfo = ImageInfo.create(
imageInfo.getWidth(),
imageInfo.getHeight(),
format,
size,
imageInfo.getBitDepth(),
imageInfo.getNumComponents(),
imageInfo.getColorSpace(),
imageInfo.getColorSpaceType(),
metadata,
false,
imageInfo.isImageIOSupported()
);
LOGGER.trace(
"Correcting misidentified image format TIFF to {} for \"{}\"",
format.toString(),
file.getAbsolutePath()
);
}
} else {
LOGGER.debug(
"Image parsing for \"{}\" was inconclusive, metadata parsing " +
"detected {} format while ImageIO detected {}. Choosing {}.",
file.getAbsolutePath(),
format,
imageInfo.getFormat(),
imageInfo.getFormat()
);
format = imageInfo.getFormat();
}
}
media.setImageInfo(imageInfo);
if (format != null) {
media.setCodecV(format.toFormatConfiguration());
media.setContainer(format.toFormatConfiguration());
}
if (trace) {
LOGGER.trace("Parsing of image \"{}\" completed", file.getName());
}
} finally {
inputStream.close();
}
}
/**
* There is a bug in Metadata Extractor that misidentifies some TIFF files
* as ARW files. This method is here to verify if such a misidentification
* has taken place or not.
*
* XXX This method can be removed if https://github.com/drewnoakes/metadata-extractor/issues/217 is fixed
*/
public static boolean isARW(Metadata metadata) {
if (metadata == null) {
return false;
}
Collection<ExifSubIFDDirectory> directories = metadata.getDirectoriesOfType(ExifSubIFDDirectory.class);
for (ExifSubIFDDirectory directory : directories) {
if (
directory.containsTag(ExifSubIFDDirectory.TAG_COMPRESSION) &&
directory.getInteger(ExifSubIFDDirectory.TAG_COMPRESSION) != null &&
directory.getInteger(ExifSubIFDDirectory.TAG_COMPRESSION) == 32767
) {
return true;
}
}
return false;
}
/**
* @return The version number multiplied with 100 (last two digits are decimals).
*/
public static int parseExifVersion(byte[] bytes) {
if (bytes == null) {
return ImageInfo.UNKNOWN;
}
StringBuilder stringBuilder = new StringBuilder(4);
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] > 47 && bytes[i] < 58) {
stringBuilder.append((char) bytes[i]);
} else if (bytes[i] == 0 && i == bytes.length - 1) {
// Some buggy C/C++ software doesn't properly format the string
// so we end up with a null-terminated string without the leading zero.
stringBuilder.insert(0, '0');
}
}
while (stringBuilder.length() < 4) {
stringBuilder.append("0");
}
try {
return Integer.parseInt(stringBuilder.toString());
} catch (NumberFormatException e) {
LOGGER.debug("Failed to parse Exif version number from: {}", Arrays.toString(bytes));
return 0;
}
}
/**
* Tries to parse {@link ExifOrientation} from the given metadata. If it
* fails, {@link ExifOrientation#TOP_LEFT} is returned.
*
* @param metadata the {@link Metadata} to parse.
* @return The parsed {@link ExifOrientation} or
* {@link ExifOrientation#TOP_LEFT}.
*/
public static ExifOrientation parseExifOrientation(Metadata metadata) {
return parseExifOrientation(metadata, ExifOrientation.TOP_LEFT);
}
/**
* Tries to parse {@link ExifOrientation} from the given metadata. If it
* fails, {@code defaultOrientation} is returned.
*
* @param metadata the {@link Metadata} to parse.
* @param defaultOrientation the default to return if parsing fails.
* @return The parsed {@link ExifOrientation} or {@code defaultOrientation}.
*/
public static ExifOrientation parseExifOrientation(Metadata metadata, ExifOrientation defaultOrientation) {
if (metadata == null) {
return defaultOrientation;
}
try {
for (Directory directory : metadata.getDirectories()) {
if (directory instanceof ExifIFD0Directory) {
if (((ExifIFD0Directory) directory).containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
return ExifOrientation.typeOf(((ExifIFD0Directory) directory).getInt(ExifIFD0Directory.TAG_ORIENTATION));
}
}
}
} catch (MetadataException e) {
return defaultOrientation;
}
return defaultOrientation;
}
/**
* Checks if the resolution axes must be swapped if the image is rotated
* according to the given Exif orientation.
*
* @param imageInfo the {@link ImageInfo} whose Exif orientation to evaluate.
* @return {@code true} if the axes should be swapped, {@code false}
* otherwise.
*/
public static boolean isExifAxesSwapNeeded(ImageInfo imageInfo) {
return imageInfo != null && isExifAxesSwapNeeded(imageInfo.getExifOrientation());
}
/**
* Checks if the resolution axes must be swapped if the image is rotated
* according to the given Exif orientation.
*
* @param orientation the Exif orientation to evaluate.
* @return {@code true} if the axes should be swapped, {@code false}
* otherwise.
*/
public static boolean isExifAxesSwapNeeded(ExifOrientation orientation) {
if (orientation == null) {
return false;
}
switch (orientation) {
case LEFT_TOP:
case RIGHT_TOP:
case RIGHT_BOTTOM:
case LEFT_BOTTOM:
return true;
default:
return false;
}
}
/**
* Calculates the resolution for the image described by {@code imageInfo} if
* it is scaled to {@code scaleWidth} width and {@code scaleHeight} height
* while preserving aspect ratio.
*
* @param actualWidth the width of the source image.
* @param actualHeight the height of the source image.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param scaleWidth the width to scale to.
* @param scaleHeight the height to scale to.
* @return A {@link Dimension} with the resulting resolution.
*/
public static Dimension calculateScaledResolution(
ImageInfo imageInfo,
ScaleType scaleType,
int scaleWidth,
int scaleHeight
) {
return calculateScaledResolution(
imageInfo.getWidth(),
imageInfo.getHeight(),
scaleType,
scaleWidth,
scaleHeight
);
}
/**
* Calculates the resolution for the image with {@code actualWidth} width
* and {@code actualHeight}) height if it is scaled to {@code scaleWidth}
* width and {@code scaleHeight} height while preserving aspect ratio.
*
* @param actualWidth the width of the source image.
* @param actualHeight the height of the source image.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param scaleWidth the width to scale to.
* @param scaleHeight the height to scale to.
* @return A {@link Dimension} with the resulting resolution.
*/
public static Dimension calculateScaledResolution(
int actualWidth,
int actualHeight,
ScaleType scaleType,
int scaleWidth,
int scaleHeight
) {
if (scaleType == null) {
throw new NullPointerException("scaleType cannot be null");
}
if (actualWidth < 1 || actualHeight < 1) {
throw new IllegalArgumentException(String.format(
"actualWidth (%d) and actualHeight (%d) must be positive", actualWidth, actualHeight
));
}
if (scaleWidth < 1 || scaleHeight < 1) {
throw new IllegalArgumentException(String.format(
"scaleWidth (%d) and scaleHeight (%d) must be positive", scaleWidth, scaleHeight
));
}
double scale = Math.min((double) scaleWidth / actualWidth, (double) scaleHeight / actualHeight);
if (scaleType == ScaleType.MAX && scale > 1) {
// Never scale up for ScaleType.MAX
scale = 1;
}
return new Dimension(
(int) Math.max(Math.round(actualWidth * scale), 1),
(int) Math.max(Math.round(actualHeight * scale), 1)
);
}
/**
* Retrieves a reference to the underlying byte array from a
* {@link ByteArrayInputStream} using reflection. Keep in mind that this
* this byte array is shared with the {@link ByteArrayInputStream}.
*
* @param inputStream the {@link ByteArrayInputStream} whose buffer to retrieve.
* @return The byte array or {@code null} if retrieval failed.
*/
public static byte[] retrieveByteArray(ByteArrayInputStream inputStream) {
Field f;
try {
f = ByteArrayInputStream.class.getDeclaredField("buf");
f.setAccessible(true);
return (byte[]) f.get(inputStream);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
LOGGER.debug("Unexpected reflection failure in retrieveByteArray(): {}", e.getMessage());
LOGGER.trace("", e);
return null;
}
}
/**
* This attempts to get the underlying byte array directly from the
* {@link InputStream} if it is backed by a byte array, otherwise the
* {@link InputStream} is copied into a new byte array with
* {@link IOUtils#toByteArray(InputStream)}.
* <p><b>
* This method consumes and closes {@code inputStream}.
* </b>
* @param inputStream the <code>InputStream</code> to read.
* @return The resulting byte array.
* @throws IOException if an I/O error occurs
*/
public static byte[] toByteArray(InputStream inputStream) throws IOException {
if (inputStream == null) {
return null;
}
// Avoid copying the data if it's already a byte array
if (inputStream instanceof ByteArrayInputStream) {
byte[] bytes = retrieveByteArray((ByteArrayInputStream) inputStream);
if (bytes != null) {
return bytes;
}
// Reflection failed, use IOUtils to read the stream instead
}
try {
return IOUtils.toByteArray(inputStream);
} finally {
inputStream.close();
}
}
/**
* Converts an image to a different {@link ImageFormat}. Rotates/flips the
* image according to Exif orientation. Format support is limited to that of
* {@link ImageIO}.
*
* @param inputImage the source {@link Image}.
* @param outputFormat the {@link ImageFormat} to convert to. If this is
* {@link ImageFormat#SOURCE} or {@code null} this has no effect.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @return The converted image or {@code null} if the source is {@code null}
* .
* @throws IOException if the operation fails.
*/
public static Image convertImage(
Image inputImage,
ImageFormat outputFormat,
boolean dlnaCompliant,
boolean dlnaThumbnail
) throws IOException {
return transcodeImage(
inputImage,
0,
0,
null,
outputFormat,
dlnaCompliant,
dlnaThumbnail,
false
);
}
/**
* Converts an image to a different {@link ImageFormat}. Rotates/flips the
* image according to Exif orientation. Format support is limited to that of
* {@link ImageIO}.
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputStream the source image in a supported format.
* @param outputFormat the {@link ImageFormat} to convert to. If this is
* {@link ImageFormat#SOURCE} or {@code null} this has no effect.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @return The converted image or {@code null} if the source is {@code null}
* .
* @throws IOException if the operation fails.
*/
public static Image convertImage(
InputStream inputStream,
ImageFormat outputFormat,
boolean dlnaCompliant,
boolean dlnaThumbnail
) throws IOException {
return transcodeImage(
inputStream,
0,
0,
null,
outputFormat,
dlnaCompliant,
dlnaThumbnail,
false
);
}
/**
* Converts an image to a different {@link ImageFormat}. Rotates/flips the
* image according to Exif orientation. Format support is limited to that of
* {@link ImageIO}.
*
* @param inputByteArray the source image in a supported format.
* @param outputFormat the {@link ImageFormat} to convert to. If this is
* {@link ImageFormat#SOURCE} or {@code null} this has no effect.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @return The converted image or {@code null} if the source is {@code null}
* .
* @throws IOException if the operation fails.
*/
public static Image convertImage(
byte[] inputByteArray,
ImageFormat outputFormat,
boolean dlnaCompliant,
boolean dlnaThumbnail
) throws IOException {
return transcodeImage(
inputByteArray,
0,
0,
null,
outputFormat,
dlnaCompliant,
dlnaThumbnail,
false
);
}
/**
* Scales an image to the given dimensions. Scaling can be with or without
* padding. Preserves aspect ratio and rotates/flips the image according to
* Exif orientation. Format support is limited to that of {@link ImageIO}.
*
* @param inputImage the source {@link Image}.
* @param width the new width.
* @param height the new height.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize Whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled image or {@code null} if the source is {@code null}.
* @throws IOException if the operation fails.
*/
public Image scaleImage(
Image inputImage,
int width,
int height,
ScaleType scaleType,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
inputImage,
width,
height,
scaleType,
ImageFormat.SOURCE,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Scales an image to the given dimensions. Scaling can be with or without
* padding. Preserves aspect ratio and rotates/flips the image according to
* Exif orientation. Format support is limited to that of {@link ImageIO}.
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputStream the source image in a supported format.
* @param width the new width.
* @param height the new height.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize Whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled image or {@code null} if the source is {@code null}.
* @throws IOException if the operation fails.
*/
public Image scaleImage(
InputStream inputStream,
int width,
int height,
ScaleType scaleType,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
inputStream,
width,
height,
scaleType,
ImageFormat.SOURCE,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Scales an image to the given dimensions. Scaling can be with or without
* padding. Preserves aspect ratio and rotates/flips the image according to
* Exif orientation. Format support is limited to that of {@link ImageIO}.
*
* @param inputByteArray the source image in a supported format.
* @param width the new width.
* @param height the new height.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize Whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled image or {@code null} if the source is {@code null}.
* @throws IOException if the operation fails.
*/
public Image scaleImage(
byte[] inputByteArray,
int width,
int height,
ScaleType scaleType,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
inputByteArray,
width,
height,
scaleType,
ImageFormat.SOURCE,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and if necessary scales an image to comply with a
* {@link DLNAImageProfile}. Preserves aspect ratio and rotates/flips the
* image according to Exif orientation. Format support is limited to that of
* {@link ImageIO}.
*
* @param inputImage the source {@link Image}.
* @param outputProfile the {@link DLNAImageProfile} to convert to.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The converted image or {@code null} if the source is {@code null}
* .
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
Image inputImage,
DLNAImageProfile outputProfile,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(inputImage, 0, 0, null, outputProfile, true, dlnaThumbnail, padToSize);
}
/**
* Converts and if necessary scales an image to comply with a
* {@link DLNAImageProfile}. Preserves aspect ratio and rotates/flips the
* image according to Exif orientation. Format support is limited to that of
* {@link ImageIO}.
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputStream the source image in a supported format.
* @param outputProfile the {@link DLNAImageProfile} to convert to.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The converted image or {@code null} if the source is {@code null}
* .
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
InputStream inputStream,
DLNAImageProfile outputProfile,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(inputStream, 0, 0, null, outputProfile, true, dlnaThumbnail, padToSize);
}
/**
* Converts and if necessary scales an image to comply with a
* {@link DLNAImageProfile}. Preserves aspect ratio and rotates/flips the
* image according to Exif orientation. Format support is limited to that of
* {@link ImageIO}.
*
* @param inputByteArray the source image in a supported format.
* @param outputProfile the {@link DLNAImageProfile} to convert to.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The converted image or {@code null} if the source is {@code null}
* .
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
byte[] inputByteArray,
DLNAImageProfile outputProfile,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(inputByteArray, 0, 0, null, outputProfile, true, dlnaThumbnail, padToSize);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
*
* @param inputImage the source {@link Image}.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputFormat the {@link ImageFormat} to convert to or
* {@link ImageFormat#SOURCE} to preserve source format.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
Image inputImage,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
null,
inputImage,
null,
width,
height,
scaleType,
outputFormat,
null,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputStream the source image in a supported format.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputFormat the {@link ImageFormat} to convert to or
* {@link ImageFormat#SOURCE} to preserve source format.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
InputStream inputStream,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
null,
null,
inputStream,
width,
height,
scaleType,
outputFormat,
null,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
*
* @param inputByteArray the source image in a supported format.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputFormat the {@link ImageFormat} to convert to or
* {@link ImageFormat#SOURCE} to preserve source format.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
byte[] inputByteArray,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
inputByteArray,
null,
null,
width,
height,
scaleType,
outputFormat,
null,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
*
* @param inputImage the source {@link Image}.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputProfile the {@link DLNAImageProfile} to convert to.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
Image inputImage,
int width,
int height,
ScaleType scaleType,
DLNAImageProfile outputProfile,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
null,
inputImage,
null,
width,
height,
scaleType,
null,
outputProfile,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputStream the source image in a supported format.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputProfile the {@link DLNAImageProfile} to convert to.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
InputStream inputStream,
int width,
int height,
ScaleType scaleType,
DLNAImageProfile outputProfile,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
null,
null,
inputStream,
width,
height,
scaleType,
null,
outputProfile,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
*
* @param inputByteArray the source image in a supported format.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputProfile the {@link DLNAImageProfile} to convert to.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
public static Image transcodeImage(
byte[] inputByteArray,
int width,
int height,
ScaleType scaleType,
DLNAImageProfile outputProfile,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return transcodeImage(
inputByteArray,
null,
null,
width,
height,
scaleType,
null,
outputProfile,
dlnaCompliant,
dlnaThumbnail,
padToSize
);
}
/**
* Converts and scales an image in one operation. Scaling can be with or
* without padding. Preserves aspect ratio and rotates/flips the image
* according to Exif orientation. Format support is limited to that of
* {@link ImageIO}. Only one of the three input arguments may be used in any
* given call. Note that {@code outputProfile} overrides
* {@code outputFormat}.
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputByteArray the source image in a supported format.
* @param inputImage the source {@link Image}.
* @param inputStream the source image in a supported format.
* @param width the new width or 0 to disable scaling.
* @param height the new height or 0 to disable scaling.
* @param scaleType the {@link ScaleType} to use when scaling.
* @param outputFormat the {@link ImageFormat} to convert to or
* {@link ImageFormat#SOURCE} to preserve source format.
* Overridden by {@code outputProfile}.
* @param outputProfile the {@link DLNAImageProfile} to convert to. This
* overrides {@code outputFormat}.
* @param dlnaCompliant whether or not the output image should be restricted
* to DLNA compliance. This also means that the output can be
* safely cast to {@link DLNAImage}.
* @param dlnaThumbnail whether or not the output image should be restricted
* to DLNA thumbnail compliance. This also means that the output
* can be safely cast to {@link DLNAThumbnail}.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The scaled and/or converted image or {@code null} if the source
* is {@code null}.
* @throws IOException if the operation fails.
*/
protected static Image transcodeImage(
byte[] inputByteArray,
Image inputImage,
InputStream inputStream,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
DLNAImageProfile outputProfile,
boolean dlnaCompliant,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
if (inputByteArray == null && inputStream == null && inputImage == null) {
return null;
}
if (
(inputByteArray != null & inputImage != null) ||
(inputByteArray != null & inputStream != null) ||
(inputImage != null & inputStream != null)
) {
throw new IllegalArgumentException("Use either inputByteArray, inputImage or inputStream");
}
boolean trace = LOGGER.isTraceEnabled();
if (trace) {
StringBuilder sb = new StringBuilder();
if (scaleType != null) {
sb.append("ScaleType = ").append(scaleType);
}
if (width > 0 && height > 0) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("Width = ").append(width).append(", Height = ").append(height);
}
if (sb.length() > 0) {
sb.append(", ");
}
sb.append("PadToSize = ").append(padToSize ? "True" : "False");
LOGGER.trace(
"Converting {} image source to {} format and type {} using the following parameters: {}",
inputByteArray != null ? "byte array" : inputImage != null ? "Image" : "input stream",
outputProfile != null ? outputProfile : outputFormat,
dlnaThumbnail ? "DLNAThumbnail" : dlnaCompliant ? "DLNAImage" : "Image",
sb
);
}
ImageIO.setUseCache(false);
dlnaCompliant = dlnaCompliant || dlnaThumbnail;
if (inputImage != null) {
inputByteArray = inputImage.getBytes(false);
} else if (inputStream != null) {
inputByteArray = ImagesUtil.toByteArray(inputStream);
}
// outputProfile overrides outputFormat
if (outputProfile != null) {
if (dlnaThumbnail && outputProfile.equals(DLNAImageProfile.GIF_LRG)) {
outputProfile = DLNAImageProfile.JPEG_LRG;
}
// Default to correct ScaleType for the profile
if (scaleType == null) {
if (DLNAImageProfile.JPEG_RES_H_V.equals(outputProfile)) {
scaleType = ScaleType.EXACT;
} else {
scaleType = ScaleType.MAX;
}
}
outputFormat = ImageFormat.toImageFormat(outputProfile);
width = width > 0 ? width : outputProfile.getMaxWidth();
height = height > 0 ? height : outputProfile.getMaxHeight();
} else if (scaleType == null) {
scaleType = ScaleType.MAX;
}
ImageReaderResult inputResult;
try {
inputResult = ImageIOTools.read(new ByteArrayInputStream(inputByteArray));
} catch (IIOException e) {
throw new UnknownFormatException("Unable to read image format", e);
}
if (inputResult.bufferedImage == null || inputResult.imageFormat == null) { // ImageIO doesn't support the image format
throw new UnknownFormatException("Failed to transform image because the source format is unknown");
}
if (outputFormat == null || outputFormat == ImageFormat.SOURCE) {
outputFormat = inputResult.imageFormat;
}
BufferedImage bufferedImage = inputResult.bufferedImage;
boolean reencode = false;
if (outputProfile == null && dlnaCompliant) {
// Override output format to one valid for DLNA, defaulting to PNG
// if the source image has alpha and JPEG if not.
switch (outputFormat) {
case GIF:
if (dlnaThumbnail) {
outputFormat = ImageFormat.JPEG;
}
break;
case JPEG:
case PNG:
break;
default:
if (bufferedImage.getColorModel().hasAlpha()) {
outputFormat = ImageFormat.PNG;
} else {
outputFormat = ImageFormat.JPEG;
}
}
}
Metadata metadata = null;
ExifOrientation orientation;
if (inputImage != null && inputImage.getImageInfo() != null) {
orientation = inputImage.getImageInfo().getExifOrientation();
} else {
try {
metadata = getMetadata(inputByteArray, inputResult.imageFormat);
} catch (IOException | ImageProcessingException e) {
LOGGER.error("Failed to read input image metadata: {}", e.getMessage());
LOGGER.trace("", e);
metadata = new Metadata();
}
if (metadata == null) {
metadata = new Metadata();
}
orientation = parseExifOrientation(metadata);
}
if (orientation != ExifOrientation.TOP_LEFT) {
// Rotate the image before doing all the other checks
BufferedImage oldBufferedImage = bufferedImage;
bufferedImage = Thumbnails.of(bufferedImage)
.scale(1.0d)
.addFilter(ExifFilterUtils.getFilterForOrientation(orientation.getThumbnailatorOrientation()))
.asBufferedImage();
oldBufferedImage.flush();
// Re-parse the metadata after rotation as these are newly generated.
ByteArrayOutputStream tmpOutputStream = new ByteArrayOutputStream(inputByteArray.length);
Thumbnails.of(bufferedImage).scale(1.0d).outputFormat(outputFormat.toString()).toOutputStream(tmpOutputStream);
try {
metadata = getMetadata(tmpOutputStream.toByteArray(), outputFormat);
} catch (IOException | ImageProcessingException e) {
LOGGER.debug("Failed to read rotated image metadata: {}", e.getMessage());
LOGGER.trace("", e);
metadata = new Metadata();
}
if (metadata == null) {
metadata = new Metadata();
}
reencode = true;
}
if (outputProfile == null && dlnaCompliant) {
// Set a suitable output profile.
if (width < 1 || height < 1) {
outputProfile = DLNAImageProfile.getClosestDLNAProfile(
bufferedImage.getWidth(),
bufferedImage.getHeight(),
outputFormat,
true
);
width = outputProfile.getMaxWidth();
height = outputProfile.getMaxHeight();
} else {
outputProfile = DLNAImageProfile.getClosestDLNAProfile(
calculateScaledResolution(
bufferedImage.getWidth(),
bufferedImage.getHeight(),
scaleType,
width,
height
),
outputFormat,
true
);
width = Math.min(width, outputProfile.getMaxWidth());
height = Math.min(height, outputProfile.getMaxHeight());
}
if (DLNAImageProfile.JPEG_RES_H_V.equals(outputProfile)) {
scaleType = ScaleType.EXACT;
}
}
boolean convertColors =
bufferedImage.getType() == BufferedImageType.TYPE_CUSTOM.getTypeId() ||
bufferedImage.getType() == BufferedImageType.TYPE_BYTE_BINARY.getTypeId() ||
bufferedImage.getColorModel().getColorSpace().getType() != ColorSpaceType.TYPE_RGB.getTypeId();
// Impose DLNA format restrictions
if (!reencode && outputFormat == inputResult.imageFormat && outputProfile != null) {
DLNAComplianceResult complianceResult;
switch (outputFormat) {
case GIF:
case JPEG:
case PNG:
ImageInfo imageInfo;
// metadata is only null at this stage if inputImage != null and no rotation was necessary
if (metadata == null) {
imageInfo = inputImage.getImageInfo();
}
imageInfo = ImageInfo.create(
bufferedImage.getWidth(),
bufferedImage.getHeight(),
inputResult.imageFormat,
ImageInfo.SIZE_UNKNOWN,
bufferedImage.getColorModel(),
metadata,
false,
true
);
complianceResult = DLNAImageProfile.checkCompliance(imageInfo, outputProfile);
break;
default:
throw new IllegalStateException("Unexpected image format: " + outputFormat);
}
reencode = reencode || convertColors || !complianceResult.isFormatCorrect() || !complianceResult.isColorsCorrect();;
if (!complianceResult.isResolutionCorrect()) {
width = width > 0 && complianceResult.getMaxWidth() > 0 ?
Math.min(width, complianceResult.getMaxWidth()) :
width > 0 ? width : complianceResult.getMaxWidth();
height = height > 0 && complianceResult.getMaxHeight() > 0 ?
Math.min(height, complianceResult.getMaxHeight()) :
height > 0 ? height : complianceResult.getMaxHeight();
}
if (trace) {
if (complianceResult.isAllCorrect()) {
LOGGER.trace("Image conversion DLNA compliance check: The source image is compliant");
} else {
LOGGER.trace(
"Image conversion DLNA compliance check for {}: " +
"The source image colors are {}, format is {} and resolution ({} x {}) is {}",
outputProfile,
complianceResult.isColorsCorrect() ? "compliant" : "non-compliant",
complianceResult.isFormatCorrect() ? "compliant" : "non-compliant",
bufferedImage.getWidth(),
bufferedImage.getHeight(),
complianceResult.isResolutionCorrect() ? "compliant" : "non-compliant"
);
}
}
}
if (convertColors) {
// Preserve alpha channel if the output format supports it
BufferedImageType outputImageType;
if (
(outputFormat == ImageFormat.PNG || outputFormat == ImageFormat.PSD) &&
bufferedImage.getColorModel().getNumComponents() == 4
) {
outputImageType = bufferedImage.isAlphaPremultiplied() ?
BufferedImageType.TYPE_4BYTE_ABGR_PRE :
BufferedImageType.TYPE_4BYTE_ABGR;
} else {
outputImageType = BufferedImageType.TYPE_3BYTE_BGR;
}
BufferedImage convertedImage = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), outputImageType.getTypeId());
ColorConvertOp colorConvertOp = new ColorConvertOp(null);
colorConvertOp.filter(bufferedImage, convertedImage);
bufferedImage.flush();
bufferedImage = convertedImage;
reencode = true;
}
if (width < 1 ||
height < 1 ||
(
scaleType == ScaleType.MAX &&
bufferedImage.getWidth() <= width &&
bufferedImage.getHeight() <= height
) ||
(
scaleType == ScaleType.EXACT &&
bufferedImage.getWidth() == width &&
bufferedImage.getHeight() == height
)
) {
//No resize, just convert
if (!reencode && inputResult.imageFormat == outputFormat) {
// Nothing to do, just return source
// metadata is only null at this stage if inputImage != null
Image result;
if (dlnaThumbnail) {
result = metadata == null ?
new DLNAThumbnail(inputImage, outputProfile, false) :
new DLNAThumbnail(inputByteArray, outputFormat, bufferedImage, metadata, outputProfile, false);
} else if (dlnaCompliant) {
result = metadata == null ?
new DLNAImage(inputImage, outputProfile, false) :
new DLNAImage(inputByteArray, outputFormat, bufferedImage, metadata, outputProfile, false);
} else {
result = metadata == null ?
new Image(inputImage, false) :
new Image(inputByteArray, outputFormat, bufferedImage, metadata, false);
}
bufferedImage.flush();
if (trace) {
LOGGER.trace(
"No conversion is needed, returning source image with width = {}, height = {} and output {}.",
bufferedImage.getWidth(),
bufferedImage.getHeight(),
dlnaCompliant && outputProfile != null ? "profile: " + outputProfile : "format: " + outputFormat
);
}
return result;
}
} else {
boolean force = DLNAImageProfile.JPEG_RES_H_V.equals(outputProfile);
BufferedImage oldBufferedImage = bufferedImage;
if (padToSize && force) {
bufferedImage = Thumbnails.of(bufferedImage)
.forceSize(width, height)
.addFilter(new Canvas(width, height, Positions.CENTER, Color.BLACK))
.asBufferedImage();
} else if (padToSize) {
bufferedImage = Thumbnails.of(bufferedImage)
.size(width, height)
.addFilter(new Canvas(width, height, Positions.CENTER, Color.BLACK))
.asBufferedImage();
} else if (force) {
bufferedImage = Thumbnails.of(bufferedImage)
.forceSize(width, height)
.asBufferedImage();
} else {
bufferedImage = Thumbnails.of(bufferedImage)
.size(width, height)
.asBufferedImage();
}
oldBufferedImage.flush();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Thumbnails.of(bufferedImage)
.scale(1.0d)
.outputFormat(outputFormat.toString())
.outputQuality(1.0f)
.toOutputStream(outputStream);
byte[] outputByteArray = outputStream.toByteArray();
Image result;
if (dlnaThumbnail) {
result = new DLNAThumbnail(outputByteArray, bufferedImage.getWidth(), bufferedImage.getHeight(), outputFormat, null, null, outputProfile, false);
} else if (dlnaCompliant) {
result = new DLNAImage(outputByteArray, bufferedImage.getWidth(), bufferedImage.getHeight(), outputFormat, null, null, outputProfile, false);
} else {
result = new Image(outputByteArray, bufferedImage.getWidth(), bufferedImage.getHeight(), outputFormat, null, null, true, false);
}
if (trace) {
StringBuilder sb = new StringBuilder();
sb.append("Convert colors = ").append(convertColors ? "True" : "False")
.append(", Re-encode = ").append(reencode ? "True" : "False");
LOGGER.trace(
"Finished converting {} {} image{}. " +
"Output image resolution: {}, {}. Flags: {}",
inputResult.width + "×" + inputResult.height,
inputResult.imageFormat,
orientation != ExifOrientation.TOP_LEFT ? " with orientation " + orientation : "",
bufferedImage.getWidth() + "×" + bufferedImage.getHeight(),
dlnaCompliant && outputProfile != null ? "profile: " + outputProfile : "format: " + outputFormat,
sb
);
}
bufferedImage.flush();
return result;
}
/**
* Extracts an embedded Exif thumbnail from a {@link Metadata} instance.
*
* @param file the {@link File} to read the thumbnail from
* @param metadata the {@link Metadata} collected on the {@link File}
* previously.
* @return A byte array containing the thumbnail or {@code null} if no
* thumbnail was found/could be extracted.
*/
public static byte[] getThumbnailFromMetadata(File file, Metadata metadata) {
if (metadata == null) {
return null;
}
/*
* XXX Extraction of thumbnails was removed in version
* 2.10.0 of metadata-extractor because of a bug in
* related code. This section is deactivated while
* waiting for this to be made available again.
*
* Images supported by ImageIO or DCRaw aren't affected,
* so this only applied to very few images anyway.
* It could extract thumbnails for some "raw" images
* if DCRaw was disabled.
*
// First check if there is a ExifThumbnailDirectory
Collection<ExifThumbnailDirectory> directories = metadata.getDirectoriesOfType(ExifThumbnailDirectory.class);
if (directories.isEmpty()) {
return null;
}
// Now get the thumbnail data if they're there
directories = metadata.getDirectoriesOfType(ExifThumbnailDirectory.class);
for (ExifThumbnailDirectory directory : directories) {
if (directory.hasThumbnailData()) {
return directory.getThumbnailData();
}
}*/
return null;
}
/**
* Reads image metadata for supported format.
*
* @param bytes the image for which to read metadata.
* @param format the {@link ImageFormat} of the image.
* @return The {@link Metadata} or {@code null} if {@code bytes} is
* {@code null}.
* @throws ImageProcessingException
* @throws IOException
*/
public static Metadata getMetadata(byte[] bytes, ImageFormat format) throws ImageProcessingException, IOException {
return getMetadata(bytes, format, null);
}
/**
* Reads image metadata for supported format.
*
* @param bytes the image for which to read metadata.
* @param fileType the {@link FileType} of the image.
* @return The {@link Metadata} or {@code null} if {@code bytes} is
* {@code null}.
* @throws ImageProcessingException
* @throws IOException
*/
public static Metadata getMetadata(byte[] bytes, FileType fileType) throws ImageProcessingException, IOException {
return getMetadata(bytes, null, fileType);
}
/**
* Reads image metadata for supported format. Either {@code format} or
* {@code FileType} must be non-null.
*
* @param bytes the image for which to read metadata.
* @param format the {@link ImageFormat} of the image.
* @param fileType the {@link FileType} of the image.
* @return The {@link Metadata} or {@code null} if {@code bytes} is
* {@code null}.
* @throws ImageProcessingException
* @throws IOException
*/
public static Metadata getMetadata(byte[] bytes, ImageFormat format, FileType fileType) throws ImageProcessingException, IOException {
if (bytes == null) {
return null;
}
return getMetadata(new ByteArrayInputStream(bytes), format, fileType);
}
/**
* Reads image metadata for supported formats.
*
* @param inputStream the image for which to read metadata.
* @param format the {@link ImageFormat} of the image.
* @return The {@link Metadata} or {@code null} if {@code bytes} is
* {@code null}.
* @throws ImageProcessingException
* @throws IOException
*/
public static Metadata getMetadata(InputStream inputStream, ImageFormat format) throws ImageProcessingException, IOException {
return getMetadata(inputStream, format, null);
}
/**
* Reads image metadata for supported formats.
*
* @param inputStream the image for which to read metadata.
* @param fileType the {@link FileType} of the image.
* @return The {@link Metadata} or {@code null} if {@code bytes} is
* {@code null}.
* @throws ImageProcessingException
* @throws IOException
*/
public static Metadata getMetadata(InputStream inputStream, FileType fileType) throws ImageProcessingException, IOException {
return getMetadata(inputStream, null, fileType);
}
/**
* Reads image metadata for supported formats. Either {@code format} or
* {@code FileType} must be non-null.
*
* @param inputStream the image for which to read metadata.
* @param format the {@link ImageFormat} of the image.
* @param fileType the {@link FileType} of the image.
* @return The {@link Metadata} or {@code null} if {@code bytes} is
* {@code null}.
* @throws ImageProcessingException
* @throws IOException
*/
public static Metadata getMetadata(InputStream inputStream, ImageFormat format, FileType fileType) throws ImageProcessingException, IOException {
if (inputStream == null) {
return null;
}
if (format == null && fileType == null) {
throw new IllegalArgumentException("Either format or fileType must be non-null");
}
Metadata metadata = null;
if (fileType != null) {
switch (fileType) {
case Bmp:
metadata = BmpMetadataReader.readMetadata(inputStream);
break;
case Gif:
metadata = GifMetadataReader.readMetadata(inputStream);
break;
case Ico:
metadata = IcoMetadataReader.readMetadata(inputStream);
break;
case Jpeg:
metadata = JpegMetadataReader.readMetadata(inputStream);
break;
case Pcx:
metadata = PcxMetadataReader.readMetadata(inputStream);
break;
case Png:
metadata = PngMetadataReader.readMetadata(inputStream);
break;
case Psd:
metadata = PsdMetadataReader.readMetadata(inputStream);
break;
case Raf:
metadata = RafMetadataReader.readMetadata(inputStream);
break;
case Riff:
metadata = WebpMetadataReader.readMetadata(inputStream);
break;
case Tiff:
case Arw:
case Cr2:
case Nef:
case Orf:
case Rw2:
metadata = TiffMetadataReader.readMetadata(new RandomAccessStreamReader(
inputStream, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, -1
));
break;
case Crw:
case Unknown:
default:
// Return an empty Metadata instance for unsupported formats
metadata = new Metadata();
}
} else {
switch (format) {
case BMP:
metadata = BmpMetadataReader.readMetadata(inputStream);
break;
case GIF:
metadata = GifMetadataReader.readMetadata(inputStream);
break;
case ICO:
metadata = IcoMetadataReader.readMetadata(inputStream);
break;
case JPEG:
metadata = JpegMetadataReader.readMetadata(inputStream);
break;
case DCX:
case PCX:
metadata = PcxMetadataReader.readMetadata(inputStream);
break;
case PNG:
metadata = PngMetadataReader.readMetadata(inputStream);
break;
case PSD:
metadata = PsdMetadataReader.readMetadata(inputStream);
break;
case RAF:
metadata = RafMetadataReader.readMetadata(inputStream);
break;
case TIFF:
case ARW:
case CR2:
case NEF:
case ORF:
case RW2:
metadata = TiffMetadataReader.readMetadata(new RandomAccessStreamReader(
inputStream, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, -1
));
break;
case WEBP:
metadata = WebpMetadataReader.readMetadata(inputStream);
break;
case SOURCE:
metadata = ImageMetadataReader.readMetadata(inputStream);
break;
// Return an empty Metadata instance for unsupported formats
case CRW:
case CUR:
case ICNS:
case PNM:
case WBMP:
default:
metadata = new Metadata();
}
}
return metadata;
}
/**
* @param fileName the "file name" part of the HTTP request.
* @return The "decoded" {@link ImageProfile} or
* {@link ImageProfile#JPEG_TN} if the parsing fails.
*/
public static DLNAImageProfile parseThumbRequest(String fileName) {
if (fileName.startsWith("thumbnail0000")) {
fileName = fileName.substring(13);
return parseImageRequest(fileName, DLNAImageProfile.JPEG_TN);
} else {
LOGGER.warn("Could not parse thumbnail DLNAImageProfile from \"{}\"");
return DLNAImageProfile.JPEG_TN;
}
}
/**
* @param fileName the "file name" part of the HTTP request.
* @param defaultProfile the {@link DLNAImageProfile} to return if parsing
* fails.
* @return The "decoded" {@link ImageProfile} or {@code defaultProfile} if
* the parsing fails.
*/
public static DLNAImageProfile parseImageRequest(String fileName, DLNAImageProfile defaultProfile) {
Matcher matcher = Pattern.compile("^([A-Z]+_(?:(?!R)[A-Z]+|RES_?\\d+[Xx_]\\d+))_").matcher(fileName);
if (matcher.find()) {
DLNAImageProfile imageProfile = DLNAImageProfile.toDLNAImageProfile(matcher.group(1));
if (imageProfile == null) {
LOGGER.warn("Could not parse DLNAImageProfile from \"{}\"", matcher.group(1));
} else {
return imageProfile;
}
} else {
LOGGER.debug("Embedded DLNAImageProfile not found in \"{}\"", fileName);
}
return defaultProfile;
}
/**
* Retrieves the bit depth from an array of bit depths for all components.
* The last component's bit depth is allowed to be different from the rest,
* but the others must be equal or an {@link InvalidStateException} is
* thrown.
*
* @param bitDepthArray the array of bit depths for all components.
* @return The bit depth value if it's constant for all but the last
* component.
* @throws InvalidStateException If the bit depth values vary or
* {@code bitDepthArray} is null or empty.
*/
public static int getBitDepthFromArray(int[] bitDepthArray) throws InvalidStateException {
if (bitDepthArray == null || bitDepthArray.length == 0) {
throw new InvalidStateException("The bit depth array cannot be null or empty");
}
try {
return getConstantIntArrayValue(bitDepthArray);
} catch (InvalidStateException e) {
// Allow the last value to be different in it's the alpha
if (bitDepthArray.length > 1) {
int[] tmpArray = new int[bitDepthArray.length - 1];
System.arraycopy(bitDepthArray, 0, tmpArray, 0, bitDepthArray.length - 1);
return getConstantIntArrayValue(tmpArray);
} else {
throw e;
}
}
}
/**
* Retrieves the bit depth from an array of bit depths for all components.
* The last component's bit depth is allowed to be different from the rest,
* but the others must be equal or an {@link InvalidStateException} is
* thrown.
*
* @param bitDepthArray the array of bit depths for all components.
* @return The bit depth value if it's constant for all but the last
* component.
* @throws InvalidStateException If the bit depth values vary or
* {@code bitDepthArray} is null or empty.
*/
public static byte getBitDepthFromArray(byte[] bitDepthArray) throws InvalidStateException {
if (bitDepthArray == null || bitDepthArray.length == 0) {
throw new InvalidStateException("The bit depth array cannot be null or empty");
}
try {
return getConstantByteArrayValue(bitDepthArray);
} catch (InvalidStateException e) {
// Allow the last value to be different in it's the alpha
if (bitDepthArray.length > 1) {
byte[] tmpArray = new byte[bitDepthArray.length - 1];
System.arraycopy(bitDepthArray, 0, tmpArray, 0, bitDepthArray.length - 1);
return getConstantByteArrayValue(tmpArray);
} else {
throw e;
}
}
}
/**
* Verifies and retrieves a constant integer value from an integer array. If
* the {@code intArray}'s values aren't all the same or {@code intArray} is
* {@code null} or empty, an {@link InvalidStateException} is thrown.
*
* @param intArray the integer array from which to extract the constant
* integer value.
* @return The constant integer value.
* @throws InvalidStateException if the values aren't equal or the array is
* {@code null} or empty.
*/
public static int getConstantIntArrayValue(int[] intArray) throws InvalidStateException {
if (intArray == null || intArray.length == 0) {
throw new InvalidStateException("The array cannot be null or empty");
}
if (intArray.length == 1) {
return intArray[0];
}
Integer result = null;
for (int i : intArray) {
if (result == null) {
result = Integer.valueOf(i);
} else {
if (result.intValue() != i) {
throw new InvalidStateException("The array doesn't have a constant value: " + Arrays.toString(intArray));
}
}
}
return result.intValue();
}
/**
* Verifies and retrieves a constant byte value from a byte array. If
* the {@code byteArray}'s values aren't all the same or {@code byteArray} is
* {@code null} or empty, an {@link InvalidStateException} is thrown.
*
* @param byteArray the byte array from which to extract the constant
* byte value.
* @return The constant byte value.
* @throws InvalidStateException if the values aren't equal or the array is
* {@code null} or empty.
*/
public static byte getConstantByteArrayValue(byte[] byteArray) throws InvalidStateException {
if (byteArray == null || byteArray.length == 0) {
throw new InvalidStateException("The array cannot be null or empty");
}
if (byteArray.length == 1) {
return byteArray[0];
}
Byte result = null;
for (byte b : byteArray) {
if (result == null) {
result = Byte.valueOf(b);
} else {
if (result.byteValue() != b) {
throw new InvalidStateException("The array doesn't have a constant value: " + Arrays.toString(byteArray));
}
}
}
return result.byteValue();
}
/**
* Finds the offset of the given Exif tag. Only the first IFD is searched.
*
* @param tagId the tag id to look for.
* @param reader a {@link RandomAccessReader} with a JPEG image.
* @return the offset of the given tag's value, or -1 if not found.
* @throws UnknownFormatException if the content isn't a JPEG.
* @throws IOException if any error occurs while reading.
*/
public static int getJPEGExifIFDTagOffset(int tagId, RandomAccessReader reader) throws UnknownFormatException, IOException {
reader.setMotorolaByteOrder(true);
if (reader.getUInt16(0) != 0xFFD8) {
throw new UnknownFormatException("Content isn't JPEG");
}
byte SEGMENT_IDENTIFIER = (byte) 0xFF;
byte MARKER_EOI = (byte) 0xD9;
byte APP1 = (byte) 0xE1;
final String EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
byte segmentIdentifier = reader.getInt8(2);
byte segmentType = reader.getInt8(3);
int pos = 4;
while (
segmentIdentifier != SEGMENT_IDENTIFIER ||
segmentType != APP1 &&
segmentType != MARKER_EOI ||
segmentType == APP1 &&
!EXIF_SEGMENT_PREAMBLE.equals(new String(
reader.getBytes(pos + 2, EXIF_SEGMENT_PREAMBLE.length()),
0,
EXIF_SEGMENT_PREAMBLE.length(),
StandardCharsets.US_ASCII)
)
) {
segmentIdentifier = segmentType;
segmentType = reader.getInt8(pos++);
}
if (segmentType == MARKER_EOI) {
// Reached the end of the image without finding an Exif segment
return -1;
}
int segmentLength = reader.getUInt16(pos) - 2;
pos += 2 + EXIF_SEGMENT_PREAMBLE.length();
if (segmentLength < EXIF_SEGMENT_PREAMBLE.length()) {
throw new ParseException("Exif segment is too small");
}
int exifHeaderOffset = pos;
short byteOrderIdentifier = reader.getInt16(pos);
pos += 4; // Skip TIFF marker
if (byteOrderIdentifier == 0x4d4d) { // "MM"
reader.setMotorolaByteOrder(true);
} else if (byteOrderIdentifier == 0x4949) { // "II"
reader.setMotorolaByteOrder(false);
} else {
throw new ParseException("Can't determine Exif endianness from: 0x" + Integer.toHexString(byteOrderIdentifier));
}
pos = reader.getInt32(pos) + exifHeaderOffset;
int tagCount = reader.getUInt16(pos);
for (int tagNumber = 0; tagNumber < tagCount; tagNumber++) {
int tagOffset = pos + 2 + (12 * tagNumber);
int curTagId = reader.getUInt16(tagOffset);
if (curTagId == tagId) {
// tag found
return tagOffset + 8;
}
}
return -1;
}
/**
* Reads the resolution specified in the JPEG SOF header.
*
* @param reader a {@link RandomAccessReader} with a JPEG image.
* @return The JPEG SOF specified resolution or {@code null} if no SOF is
* found.
* @throws UnknownFormatException if the content isn't a JPEG.
* @throws IOException if any error occurs while reading.
*/
public static Dimension getJPEGResolution(RandomAccessReader reader) throws UnknownFormatException, IOException {
reader.setMotorolaByteOrder(true);
if (reader.getUInt16(0) != 0xFFD8) {
throw new UnknownFormatException("Content isn't JPEG");
}
byte SEGMENT_IDENTIFIER = (byte) 0xFF;
byte MARKER_EOI = (byte) 0xD9;
Set<Byte> SOFS = new HashSet<>(Arrays.asList(
(byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC5,
(byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA,
(byte) 0xCB, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF
));
byte segmentIdentifier = reader.getInt8(2);
byte segmentType = reader.getInt8(3);
int pos = 4;
while (
segmentIdentifier != SEGMENT_IDENTIFIER ||
!SOFS.contains(segmentType) &&
segmentType != MARKER_EOI
) {
segmentIdentifier = segmentType;
segmentType = reader.getInt8(pos++);
}
if (segmentType == MARKER_EOI) {
// Reached the end of the image without finding the SOF segment
return null;
}
int segmentLength = reader.getUInt16(pos) - 2;
pos += 3;
if (segmentLength < 5) {
throw new ParseException("SOF segment is too small");
}
return new Dimension(reader.getUInt16(pos + 2), reader.getUInt16(pos));
}
/**
* Defines how image scaling is done. {@link #EXACT} will scale the image
* to the given resolution if the aspect ratio is the same, or scale the
* image so that it fills the given resolution as much as possible while
* keeping the aspect ratio if the aspect radio doesn't match. {@link #MAX}
* will make sure that the output image doesn't exceed the given
* resolution. {@link #MAX} will never upscale, only downscale if required.
*/
public enum ScaleType {
/**
* Scale the image to the given resolution while keeping the aspect
* ratio. If the aspect ratio mismatch, one axis will be scaled to the
* given resolution while the other will be smaller.
*/
EXACT,
/**
* Scale the image so that it is no bigger than the given resolution
* while keeping aspect ratio. Will never upscale.
*/
MAX
}
}