/* * 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.BufferedImage; import java.awt.image.ColorModel; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import javax.imageio.ImageIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.drew.imaging.ImageProcessingException; import com.drew.metadata.Metadata; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.pms.dlna.DLNAImage; import net.pms.dlna.DLNAImageProfile; import net.pms.dlna.DLNAThumbnail; import net.pms.image.ImagesUtil.ScaleType; import net.pms.util.ParseException; /** * This class is simply a byte array for holding an {@link ImageIO} supported * image with some additional metadata. * * @author Nadahar */ public class Image implements Serializable { private static final long serialVersionUID = 6878185988106188499L; private static final Logger LOGGER = LoggerFactory.getLogger(Image.class); protected final byte[] bytes; protected final ImageInfo imageInfo; /** * Creates a new {@link Image} instance. * * @param image the source {@link Image} in a supported format. Format * support is limited to that of {@link ImageIO}. * @param copy whether this instance should be copied or shared. */ @SuppressFBWarnings("EI_EXPOSE_REP2") public Image(Image image, boolean copy) { this.bytes = image.getBytes(copy); this.imageInfo = copy && image.getImageInfo() != null ? image.getImageInfo().copy() : image.getImageInfo(); } /** * Creates a new {@link Image} instance. * * @param bytes the source image in a supported format. Format support is * limited to that of {@link ImageIO}. * @param imageInfo the {@link ImageInfo} to store with this {@link Image}. * @param copy whether this instance should be copied or shared. */ @SuppressFBWarnings("EI_EXPOSE_REP2") public Image(byte[] bytes, ImageInfo imageInfo, boolean copy) { if (copy && bytes != null) { this.bytes = new byte[bytes.length]; System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); } else { this.bytes = bytes; } this.imageInfo = copy && imageInfo != null ? imageInfo.copy() : imageInfo; } /** * Creates a new {@link Image} instance. * * @param bytes the image in a supported format. Format support is limited * to that of {@link ImageIO}. * @param width the width of the image. * @param height the height of the image. * @param format the {@link ImageFormat} of the image. * @param colorModel the {@link ColorModel} of the image. * @param metadata the {@link Metadata} instance describing the image. Will * be created if {@code null}. * @param imageIOSupport whether or not {@link ImageIO} can read/parse this * image. * @param copy whether this instance should be copied or shared. * @throws ParseException if {@code format} is {@code null} and parsing the * format from {@code metadata} fails. */ @SuppressFBWarnings("EI_EXPOSE_REP2") public Image( byte[] bytes, int width, int height, ImageFormat format, ColorModel colorModel, Metadata metadata, boolean imageIOSupport, boolean copy ) throws ParseException { if (copy && bytes != null) { this.bytes = new byte[bytes.length]; System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); } else { this.bytes = bytes; } if (metadata == null) { try { metadata = ImagesUtil.getMetadata(this.bytes, format); } catch (ImageProcessingException | IOException e) { LOGGER.error("Error reading image metadata: {}", e.getMessage()); LOGGER.trace("", e); metadata = new Metadata(); } } this.imageInfo = ImageInfo.create( width, height, format, bytes != null ? bytes.length : 0, colorModel, metadata, false, imageIOSupport ); } /** * Creates a new {@link Image} instance. * * @param bytes the image in a supported format. Format support is limited * to that of {@link ImageIO}. * @param format the {@link ImageFormat} of the image. * @param bufferedImage the {@link BufferedImage} to get non- * {@link Metadata} metadata from. * @param metadata the {@link Metadata} instance describing the image. Will * be created if {@code null}. * @param copy whether this instance should be copied or shared. * @throws ParseException if {@code format} is {@code null} and parsing the * format from {@code metadata} fails. */ @SuppressFBWarnings("EI_EXPOSE_REP2") public Image( byte[] bytes, ImageFormat format, BufferedImage bufferedImage, Metadata metadata, boolean copy ) throws ParseException { if (bufferedImage == null) { throw new IllegalArgumentException("bufferedImage cannot be null"); } if (copy && bytes != null) { this.bytes = new byte[bytes.length]; System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); } else { this.bytes = bytes; } if (metadata == null) { try { metadata = ImagesUtil.getMetadata(this.bytes, format); } catch (ImageProcessingException | IOException e) { LOGGER.error("Error while reading image metadata: {}", e.getMessage()); LOGGER.trace("", e); metadata = new Metadata(); } } this.imageInfo = ImageInfo.create( bufferedImage.getWidth(), bufferedImage.getHeight(), format, bytes != null ? bytes.length : 0, bufferedImage.getColorModel(), metadata, false, true ); } /** * Converts an image to an {@link Image}. 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. * @return The populated {@link Image} or {@code null} if the source image * is {@code null}. * @throws IOException */ public static Image toImage(InputStream inputStream) throws IOException { return toImage(inputStream, 0, 0, null, ImageFormat.SOURCE, false); } /** * Converts an image to an {@link Image}. Preserves aspect ratio and * rotates/flips the image according to Exif orientation. Format support is * limited to that of {@link ImageIO}. * * @param imageByteArray the source image in a supported format. * @return The populated {@link Image} or {@code null} if the source image * is {@code null}. * @throws IOException */ public static Image toImage(byte[] imageByteArray) throws IOException { return toImage(imageByteArray, 0, 0, null, ImageFormat.SOURCE, false); } /** * Converts an {@link Image} to another {@link Image}. 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 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 generate or * {@link ImageFormat#SOURCE} to preserve source format. * @param padToSize whether padding should be used if source aspect doesn't * match target aspect. * @return The populated {@link Image} or {@code null} if the source image * is {@code null}. * @throws IOException */ public static Image toImage( Image inputImage, int width, int height, ScaleType scaleType, ImageFormat outputFormat, boolean padToSize ) throws IOException { return ImagesUtil.transcodeImage( inputImage, width, height, scaleType, outputFormat, false, false, padToSize ); } /** * Converts an image to an {@link Image}. 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 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 generate or * {@link ImageFormat#SOURCE} to preserve source format. * @param padToSize whether padding should be used if source aspect doesn't * match target aspect. * @return The populated {@link Image} or {@code null} if the source image * is {@code null}. * @throws IOException */ public static Image toImage( InputStream inputStream, int width, int height, ScaleType scaleType, ImageFormat outputFormat, boolean padToSize ) throws IOException { return ImagesUtil.transcodeImage( inputStream, width, height, scaleType, outputFormat, false, false, padToSize ); } /** * Converts an image to an {@link Image}. Preserves aspect ratio and * rotates/flips the image according to Exif orientation. Format support is * limited to that of {@link ImageIO}. * * @param imageByteArray 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 generate or * {@link ImageFormat#SOURCE} to preserve source format. * @param padToSize whether padding should be used if source aspect doesn't * match target aspect. * @return The populated {@link Image} or {@code null} if the source image * is {@code null}. * @throws IOException */ public static Image toImage( byte[] imageByteArray, int width, int height, ScaleType scaleType, ImageFormat outputFormat, boolean padToSize ) throws IOException { return ImagesUtil.transcodeImage( imageByteArray, width, height, scaleType, outputFormat, false, false, padToSize); } /** * Scales the {@link Image}. Preserves aspect ratio and rotates/flips the * image according to Exif orientation. Format support is limited to that of * {@link ImageIO}. * * @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 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 {@link Image} or {@code null} if the source is * {@code null}. * @throws IOException if the operation fails. */ public Image scale( int width, int height, ScaleType scaleType, boolean updateMetadata, boolean dlnaCompliant, boolean dlnaThumbnail, boolean padToSize ) throws IOException { return transcode( width, height, scaleType, ImageFormat.SOURCE, dlnaCompliant, dlnaThumbnail, padToSize ); } /** * Converts the {@link Image}. Preserves aspect ratio and rotates/flips the * image according to Exif orientation. Format support is limited to that of * {@link ImageIO}. * * @param outputFormat the {@link ImageFormat} to convert to or * {@link ImageFormat#SOURCE} to preserve source format. * Overridden by {@code outputProfile}. * @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 {@link Image} or {@code null} if the source is * {@code null}. * @throws IOException if the operation fails. */ public Image transcode( ImageFormat outputFormat, boolean dlnaCompliant, boolean dlnaThumbnail ) throws IOException { return transcode( 0, 0, null, outputFormat, dlnaCompliant, dlnaThumbnail, false ); } /** * Converts and scales the {@link Image}. Preserves aspect ratio and * rotates/flips the image according to Exif orientation. Format support is * limited to that of {@link ImageIO}. * * @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 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 {@link Image} or {@code null} if the * source is {@code null}. * @throws IOException if the operation fails. */ public Image transcode( int width, int height, ScaleType scaleType, ImageFormat outputFormat, boolean dlnaCompliant, boolean dlnaThumbnail, boolean padToSize ) throws IOException { return ImagesUtil.transcodeImage( this.getBytes(false), width, height, scaleType, outputFormat, dlnaCompliant, dlnaThumbnail, padToSize ); } /** * Converts and scales the {@link Image} according to the given * {@link DLNAImageProfile}. Preserves aspect ratio and rotates/flips the * image according to Exif orientation. Format support is limited to that of * {@link ImageIO}. * * @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 {@link Image} or {@code null} if the * source is {@code null}. * @throws IOException if the operation fails. */ public Image transcode( DLNAImageProfile outputProfile, boolean dlnaCompliant, boolean dlnaThumbnail, boolean padToSize ) throws IOException { return ImagesUtil.transcodeImage( this.getBytes(false), 0, 0, ScaleType.MAX, outputProfile, dlnaCompliant, dlnaThumbnail, padToSize ); } /** * @param copy whether or not a new array or a reference to the underlying * buffer should be returned. If a reference is returned, * <b>NO MODIFICATIONS must be done to the array!</b> * @return The bytes of this image. */ @SuppressFBWarnings("EI_EXPOSE_REP") public byte[] getBytes(boolean copy) { if (copy) { byte[] result = new byte[bytes.length]; System.arraycopy(bytes, 0, result, 0, bytes.length); return result; } return bytes; } /** * @return The {@link ImageInfo} for this image. */ public ImageInfo getImageInfo() { return imageInfo; } /** * @return The width of this image. */ public int getWidth() { return imageInfo != null ? imageInfo.getWidth() : -1; } /** * @return The height of this image. */ public int getHeight() { return imageInfo != null ? imageInfo.getHeight() : -1; } /** * @return The {@link ImageFormat} for this image. */ public ImageFormat getFormat() { return imageInfo != null ? imageInfo.getFormat() : null; } /** * @return The size of this image in bytes. */ public long getSize() { return bytes != null ? bytes.length : 0; } /** * @return The {@link ColorSpace} for this image. */ public ColorSpace getColorSpace() { return imageInfo != null ? imageInfo.getColorSpace() : null; } /** * @return The {@link ColorSpaceType} for this image. */ public ColorSpaceType getColorSpaceType() { return imageInfo != null ? imageInfo.getColorSpaceType() : null; } /** * @return The bits per pixel for this image. * * @see #getBitDepth() */ public int getBitPerPixel() { return imageInfo != null ? imageInfo.getBitsPerPixel() : -1; } /** * The number of color 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 color components for this image. */ public int getNumComponents() { return imageInfo != null ? imageInfo.getNumComponents() : -1; } /** * @return The number of bits per color "channel" for this image. * * @see #getBitPerPixel() * @see #getNumColorComponents() */ public int getBitDepth() { return imageInfo != null ? imageInfo.getBitDepth() : -1; } /** * @return Whether or not {@link ImageIO} can read/parse this image. */ public boolean isImageIOSupported() { return imageInfo != null ? imageInfo.isImageIOSupported() : false; } /** * @return A copy of this image. The buffer is copied and the metadata recreated. */ public Image copy() { return new Image(bytes, imageInfo, true); } /** * Override this to add information to {@link #toString} from subclasses. * * @param sb the {@link StringBuilder} to add information to. */ protected void buildToString(StringBuilder sb) { } @Override public String toString() { StringBuilder sb = new StringBuilder(80); sb.append(getClass().getSimpleName()) .append(": [Format = ").append(imageInfo.getFormat()) .append(", Resolution = ").append(imageInfo.getWidth()) .append("×").append(imageInfo.getHeight()); if (getSize() > 0) { sb.append(", Size = ").append(bytes != null ? bytes.length : 0); } buildToString(sb); sb.append("]"); return sb.toString(); } }