/*
* 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.dlna;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.imageio.ImageIO;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.drew.metadata.Metadata;
import net.pms.dlna.DLNAImageProfile.DLNAComplianceResult;
import net.pms.image.Image;
import net.pms.image.ImageFormat;
import net.pms.image.ImageInfo;
import net.pms.image.ImagesUtil;
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 restricted to valid DLNA image media
* format profiles.
*
* @see DLNAThumbnail
*
* @author Nadahar
*/
public class DLNAImage extends Image {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LoggerFactory.getLogger(DLNAImage.class);
protected final DLNAImageProfile profile;
/**
* Creates a new {@link DLNAImage} instance.
*
* @param image the source {@link Image} in either GIF, JPEG or PNG format
* adhering to the DLNA restrictions for color space and
* compression.
* @param profile the {@link DLNAImageProfile} this {@link DLNAImage}
* adheres to.
* @param copy whether this instance should be copied or shared.
* @throws DLNAProfileException if the profile compliance check fails.
*/
public DLNAImage(
Image image,
DLNAImageProfile profile,
boolean copy
) throws DLNAProfileException {
super(image, copy);
this.profile = profile != null ? profile : findMatchingProfile(this instanceof DLNAThumbnail);
if (this.profile == null) {
throw new NullPointerException("DLNAImage: profile cannot be null");
}
checkCompliance();
}
/**
* Creates a new {@link DLNAImage} instance.
*
* @param bytes the source image in either GIF, JPEG or PNG format adhering
* to the DLNA restrictions for color space and compression.
* @param imageInfo the {@link ImageInfo} to store with this
* {@link DLNAImage}.
* @param profile the {@link DLNAImageProfile} this {@link DLNAImage}
* adheres to.
* @param copy whether this instance should be copied or shared.
* @throws DLNAProfileException if the profile compliance check fails.
*/
public DLNAImage(
byte[] bytes,
ImageInfo imageInfo,
DLNAImageProfile profile,
boolean copy
) throws DLNAProfileException {
super(bytes, imageInfo, copy);
this.profile = profile != null ? profile : findMatchingProfile(this instanceof DLNAThumbnail);
if (this.profile == null) {
throw new NullPointerException("DLNAImage: profile cannot be null");
}
checkCompliance();
}
/**
* Creates a new {@link DLNAImage} instance.
*
* @param bytes the source image in either GIF, JPEG or PNG format adhering
* to the DLNA restrictions for color space and compression.
* @param width the width of the source image.
* @param height the height of the source image.
* @param format the {@link ImageFormat} of the source image.
* @param colorModel the {@link ColorModel} of the image.
* @param metadata the {@link Metadata} instance describing the image.
* @param profile the {@link DLNAImageProfile} this {@link DLNAImage}
* adheres to.
* @param copy whether this instance should be copied or shared.
* @throws DLNAProfileException if the profile compliance check fails.
* @throws ParseException if {@code format} is {@code null} and parsing the
* format from {@code metadata} fails.
*/
public DLNAImage(
byte[] bytes,
int width,
int height,
ImageFormat format,
ColorModel colorModel,
Metadata metadata,
DLNAImageProfile profile,
boolean copy
) throws DLNAProfileException, ParseException {
super(bytes, width, height, format, colorModel, metadata, true, copy);
this.profile = profile != null ? profile : findMatchingProfile(this instanceof DLNAThumbnail);
if (this.profile == null) {
throw new NullPointerException("DLNAImage: profile cannot be null");
}
checkCompliance();
}
/**
* Creates a new {@link DLNAImage} instance.
*
* @param inputByteArray the source image in either GIF, JPEG or PNG format
* adhering to the DLNA restrictions for color space and
* compression.
* @param format the {@link ImageFormat} the source image is in.
* @param bufferedImage the {@link BufferedImage} to get non-
* {@link Metadata} metadata from.
* @param metadata the {@link Metadata} instance describing the source
* image.
* @param copy whether this instance should be copied or shared.
* @throws DLNAProfileException if the profile compliance check fails.
* @throws ParseException if {@code format} is {@code null} and parsing the
* format from {@code metadata} fails.
*/
public DLNAImage(
byte[] bytes,
ImageFormat format,
BufferedImage bufferedImage,
Metadata metadata,
DLNAImageProfile profile,
boolean copy
) throws DLNAProfileException, ParseException {
super(bytes, format, bufferedImage, metadata, copy);
this.profile = profile != null ? profile : findMatchingProfile(this instanceof DLNAThumbnail);
if (this.profile == null) {
throw new NullPointerException("DLNAImage: profile cannot be null");
}
checkCompliance();
}
/**
* Converts an {@link Image} to a {@link DLNAImage}. Output format will be
* the same as the source if the source is either GIF, JPEG or PNG. Further
* restrictions on color space and compression is imposed and conversion
* done if necessary. All other formats will be converted to a DLNA
* compliant JPEG.
*
* @param inputImage the source {@link Image}.
* @return The populated {@link DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(Image inputImage) throws IOException {
return toDLNAImage(inputImage, 0, 0, null, ImageFormat.SOURCE, false);
}
/**
* Converts an image to a {@link DLNAImage}. Format support is limited to
* that of {@link ImageIO}. Output format will be the same as the source if
* the source is either GIF, JPEG or PNG. Further restrictions on color
* space and compression is imposed and conversion done if necessary. All
* other formats will be converted to a DLNA compliant JPEG. Preserves
* aspect ratio and rotates/flips the image according to Exif orientation.
*
* <p>
* <b> This method consumes and closes {@code inputStream}. </b>
*
* @param inputStream the source image in a supported format.
* @return The populated {@link DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(InputStream inputStream) throws IOException {
return toDLNAImage(inputStream, 0, 0, null, ImageFormat.SOURCE, false);
}
/**
* Converts an image to a {@link DLNAImage}. Format support is limited to
* that of {@link ImageIO}. Output format will be the same as the source if
* the source is either GIF, JPEG or PNG. Further restrictions on color
* space and compression is imposed and conversion done if necessary. All
* other formats will be converted to a DLNA compliant JPEG. Preserves
* aspect ratio and rotates/flips the image according to Exif orientation.
*
* @param sourceByteArray the source image in a supported format.
* @return The populated {@link DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(byte[] sourceByteArray) throws IOException {
return toDLNAImage(sourceByteArray, 0, 0, null, ImageFormat.SOURCE, false);
}
/**
* Converts an {@link Image} to a {@link DLNAThumbnail} adhering to
* {@code outputProfile}. Output format will be the same as the source if
* the source is either GIF, JPEG or PNG. Further restrictions on color
* space and compression is imposed and conversion done if necessary. All
* other formats will be converted to a DLNA compliant JPEG.
*
* @param inputImage the source image in a supported format.
* @param outputProfile the {@link DLNAImageProfile} to adhere to for the
* output.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The populated {@link DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(
Image inputImage,
DLNAImageProfile outputProfile,
boolean padToSize
) throws IOException {
if (inputImage == null) {
return null;
}
return (DLNAImage) ImagesUtil.transcodeImage(
inputImage,
outputProfile,
false,
padToSize
);
}
/**
* Converts an image to a {@link DLNAThumbnail} adhering to
* {@code outputProfile}. Format support is limited to that of
* {@link ImageIO}. Output format will be the same as the source if the
* source is either GIF, JPEG or PNG. Further restrictions on color space
* and compression is imposed and conversion done if necessary. All other
* formats will be converted to a DLNA compliant JPEG. Preserves aspect
* ratio and rotates/flips the image according to Exif orientation.
*
* <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 adhere to for the
* output.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The populated {@link DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(
InputStream inputStream,
DLNAImageProfile outputProfile,
boolean padToSize
) throws IOException {
if (inputStream == null) {
return null;
}
return (DLNAImage) ImagesUtil.transcodeImage(
inputStream,
outputProfile,
false,
padToSize
);
}
/**
* Converts an image to a {@link DLNAThumbnail} adhering to
* {@code outputProfile}. Format support is limited to that of
* {@link ImageIO}. Output format will be the same as the source if the
* source is either GIF, JPEG or PNG. Further restrictions on color space
* and compression is imposed and conversion done if necessary. All other
* formats will be converted to a DLNA compliant JPEG. Preserves aspect
* ratio and rotates/flips the image according to Exif orientation.
*
* @param inputByteArray the source image in a supported format.
* @param outputProfile the {@link DLNAImageProfile} to adhere to for the
* output.
* @param padToSize whether padding should be used if source aspect doesn't
* match target aspect.
* @return The populated {@link DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(
byte[] inputByteArray,
DLNAImageProfile outputProfile,
boolean padToSize
) throws IOException {
if (inputByteArray == null) {
return null;
}
return (DLNAImage) ImagesUtil.transcodeImage(
inputByteArray,
outputProfile,
false,
padToSize
);
}
/**
* Converts an {@link Image} to a {@link DLNAImage}. Output format will be
* the same as the source if the source is either GIF, JPEG or PNG. Further
* restrictions on color space and compression is imposed and conversion
* done if necessary. All other formats will be converted to a DLNA
* compliant JPEG.
*
* @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 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 DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(
Image inputImage,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
boolean padToSize
) throws IOException {
if (inputImage == null) {
return null;
}
return (DLNAImage) ImagesUtil.transcodeImage(
inputImage,
width,
height,
scaleType,
outputFormat,
true,
false,
padToSize
);
}
/**
* Converts an image to a {@link DLNAImage}. Format support is limited to
* that of {@link ImageIO}. Output format will be the same as the source if
* the source is either GIF, JPEG or PNG. Further restrictions on color
* space and compression is imposed and conversion done if necessary. All
* other formats will be converted to a DLNA compliant JPEG. Preserves
* aspect ratio and rotates/flips the image according to Exif orientation.
*
* <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 DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(
InputStream inputStream,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
boolean padToSize
) throws IOException {
if (inputStream == null) {
return null;
}
return (DLNAImage) ImagesUtil.transcodeImage(
inputStream,
width,
height,
scaleType,
outputFormat,
true,
false,
padToSize
);
}
/**
* Converts an image to a {@link DLNAImage}. Format support is limited to
* that of {@link ImageIO}. Output format will be the same as the source if
* the source is either GIF, JPEG or PNG. Further restrictions on color
* space and compression is imposed and conversion done if necessary. All
* other formats will be converted to a DLNA compliant JPEG. Preserves
* aspect ratio and rotates/flips the image according to Exif orientation.
*
* @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 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 DLNAImage} or {@code null} if the source
* image is {@code null}.
* @throws IOException if the operation fails.
*/
public static DLNAImage toDLNAImage(
byte[] inputByteArray,
int width,
int height,
ScaleType scaleType,
ImageFormat outputFormat,
boolean padToSize) throws IOException {
return (DLNAImage) ImagesUtil.transcodeImage(
inputByteArray,
width,
height,
scaleType,
outputFormat,
true,
false,
padToSize
);
}
/**
* Converts and scales the image according to the given
* {@link DLNAImageProfile}. Preserves aspect ratio. Format support is
* limited to that of {@link ImageIO}.
*
* @param outputProfile the {@link DLNAImageProfile} to adhere to for the
* output.
* @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 thumbnail, {@code null} if the
* source is {@code null}.
* @exception IOException if the operation fails.
*/
public DLNAImage transcode(
DLNAImageProfile outputProfile,
boolean dlnaThumbnail,
boolean padToSize
) throws IOException {
return (DLNAImage) ImagesUtil.transcodeImage(
this,
outputProfile,
dlnaThumbnail,
padToSize
);
}
/**
* @return The {@link DLNAImageProfile} this {@link DLNAImage} adheres to.
*/
public DLNAImageProfile getDLNAImageProfile() {
return profile;
}
@Override
public DLNAImage copy() {
try {
return new DLNAImage(bytes, imageInfo, profile, true);
} catch (DLNAProfileException e) {
// Should be impossible
LOGGER.error("Impossible situation in DLNAImage.copy(): {}", e.getMessage());
LOGGER.trace("", e);
return null;
}
}
protected DLNAImageProfile findMatchingProfile(boolean dlnaThumbnail) {
if (
imageInfo == null || imageInfo.getFormat() == null ||
imageInfo.getWidth() < 1 || imageInfo.getHeight() < 1
) {
return null;
}
DLNAComplianceResult result;
switch (imageInfo.getFormat()) {
case GIF:
if (dlnaThumbnail) {
return null;
}
if (DLNAImageProfile.GIF_LRG.checkCompliance(imageInfo).isAllCorrect()) {
return DLNAImageProfile.GIF_LRG;
}
return null;
case JPEG:
result = DLNAImageProfile.JPEG_TN.checkCompliance(imageInfo);
if (result.isAllCorrect()) {
return DLNAImageProfile.JPEG_TN;
} else if (result.isColorsCorrect() && result.isFormatCorrect()) {
if (DLNAImageProfile.JPEG_SM.isResolutionCorrect(imageInfo)) {
return DLNAImageProfile.JPEG_SM;
}
if (DLNAImageProfile.JPEG_MED.isResolutionCorrect(imageInfo)) {
return DLNAImageProfile.JPEG_MED;
}
if (DLNAImageProfile.JPEG_LRG.isResolutionCorrect(imageInfo)) {
return DLNAImageProfile.JPEG_LRG;
}
return DLNAImageProfile.createJPEG_RES_H_V(imageInfo.getWidth(), imageInfo.getHeight());
}
return null;
case PNG:
result = DLNAImageProfile.PNG_TN.checkCompliance(imageInfo);
if (result.isAllCorrect()) {
return DLNAImageProfile.PNG_TN;
} else if (
result.isColorsCorrect() &&
result.isFormatCorrect() &&
DLNAImageProfile.PNG_LRG.isResolutionCorrect(imageInfo)
) {
return DLNAImageProfile.PNG_LRG;
}
return null;
default:
}
return null;
}
protected void checkCompliance() throws DLNAProfileException {
DLNAComplianceResult result = profile.checkCompliance(imageInfo);
if (result.isAllCorrect()) {
return;
}
StringBuilder sb = new StringBuilder(150);
sb.append(this.getClass().getSimpleName()).
append(": Compliance check failed for ").append(profile);
List<String> failures = result.getFailures();
if (!failures.isEmpty()) {
if (LOGGER.isDebugEnabled()) {
sb.append(" with the following failures:\n").
append(StringUtils.join(failures, "\n"));
} else {
sb.append(" for: ").
append(StringUtils.join(failures, ", "));
}
}
throw new DLNAProfileException(sb.toString());
}
@Override
protected void buildToString(StringBuilder sb) {
if (profile != null) {
sb.append(", DLNA Profile = ").append(profile);
}
}
}