/* * 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.Dimension; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.fourthline.cling.support.model.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.pms.dlna.protocolinfo.DLNAOrgProfileName; import net.pms.dlna.protocolinfo.KnownDLNAOrgProfileName; import net.pms.dlna.protocolinfo.MimeType; import net.pms.dlna.protocolinfo.ProtocolInfo; import net.pms.image.ColorSpaceType; import net.pms.image.GIFInfo; import net.pms.image.ImageFormat; import net.pms.image.ImageInfo; import net.pms.image.ImagesUtil; import net.pms.image.ImagesUtil.ScaleType; import net.pms.image.JPEGInfo; import net.pms.image.JPEGInfo.CompressionType; import net.pms.image.JPEGSubsamplingNotation; import net.pms.image.PNGInfo; import net.pms.image.ExifInfo.ExifColorSpace; import net.pms.image.PNGInfo.InterlaceMethod; /** * Definition and validation of the different DLNA media profiles for images. * If more are added, corresponding changes need to be made in * {@link ImageFormat#fromImageProfile}. * * Please note: Only the profile constant (e.g {@code JPEG_TN} is taken into * consideration in {@link #equals(Object)} and {@link #hashCode()}, metadata * like {@code H} and {@code V} aren't compared. This means that: * <p> * {@code DLNAImageProfile.JPEG_RES_H_V.equals(DLNAImageProfile.JPEG_RES_H_V)} * <p> * is true even if {@code H} and {@code V} are different in the two profiles. */ @SuppressWarnings({"checkstyle:MethodName", "checkstyle:ParameterName"}) public class DLNAImageProfile implements Comparable<DLNAImageProfile>, Serializable { private static final long serialVersionUID = 1L; private static final Logger LOGGER = LoggerFactory.getLogger(DLNAImageProfile.class); /** Standard GIF mime-type */ public static final MimeType MIMETYPE_GIF = new MimeType("image", "gif"); /** Standard JPEG mime-type */ public static final MimeType MIMETYPE_JPEG = new MimeType("image", "jpeg"); /** Standard PNG mime-type */ public static final MimeType MIMETYPE_PNG = new MimeType("image", "png"); /** Integer representation of GIF_LRG. */ public static final int GIF_LRG_INT = 1; /** Integer representation of JPEG_LRG. */ public static final int JPEG_LRG_INT = 2; /** Integer representation of JPEG_MED. */ public static final int JPEG_MED_INT = 3; /** Integer representation of JPEG_RES_H_V. */ public static final int JPEG_RES_H_V_INT = 4; /** Integer representation of JPEG_SM. */ public static final int JPEG_SM_INT = 5; /** Integer representation of JPEG_TN. */ public static final int JPEG_TN_INT = 6; /** Integer representation of PNG_LRG. */ public static final int PNG_LRG_INT = 7; /** Integer representation of PNG_TN. */ public static final int PNG_TN_INT = 8; /** String representation of GIF_LRG. */ public static final String GIF_LRG_STRING = "GIF_LRG"; /** String representation of JPEG_LRG. */ public static final String JPEG_LRG_STRING = "JPEG_LRG"; /** String representation of JPEG_MED. */ public static final String JPEG_MED_STRING = "JPEG_MED"; /** String representation of JPEG_RES_H_V. */ public static final String JPEG_RES_H_V_STRING = "JPEG_RES_H_V"; /** String representation of JPEG_SM. */ public static final String JPEG_SM_STRING = "JPEG_SM"; /** String representation of JPEG_TN. */ public static final String JPEG_TN_STRING = "JPEG_TN"; /** String representation of PNG_LRG. */ public static final String PNG_LRG_STRING = "PNG_LRG"; /** String representation of PNG_TN. */ public static final String PNG_TN_STRING = "PNG_TN"; /** * {@code GIF_LRG} maximum resolution 1600 x 1200. */ public static final DLNAImageProfile GIF_LRG = new DLNAImageProfile(GIF_LRG_INT, GIF_LRG_STRING, 1600, 1200, null); /** * {@code JPEG_LRG} maximum resolution 4096 x 4096. */ public static final DLNAImageProfile JPEG_LRG = new DLNAImageProfile(JPEG_LRG_INT, JPEG_LRG_STRING, 4096, 4096, null); /** * {@code JPEG_MED} maximum resolution 1024 x 768. */ public static final DLNAImageProfile JPEG_MED = new DLNAImageProfile(JPEG_MED_INT, JPEG_MED_STRING, 1024, 768, null); /** * {@code JPEG_RES_H_V} exact resolution H x V.<br> * <br> * <b>This constant creates an invalid profile, only for use with * {@link #equals(Object)}.</b><br> * <br> * To create a valid profile, use {@link #createJPEG_RES_H_V(int, int)} instead. */ public static final DLNAImageProfile JPEG_RES_H_V = new DLNAImageProfile(JPEG_RES_H_V_INT, JPEG_RES_H_V_STRING, -1, -1, null); /** * {@code JPEG_SM} maximum resolution 640 x 480. */ public static final DLNAImageProfile JPEG_SM = new DLNAImageProfile(JPEG_SM_INT, JPEG_SM_STRING, 640, 480, null); /** * {@code JPEG_TN} maximum resolution 160 x 160. */ public static final DLNAImageProfile JPEG_TN = new DLNAImageProfile(JPEG_TN_INT, JPEG_TN_STRING, 160, 160, null); /** * {@code PNG_LRG} maximum resolution 4096 x 4096. */ public static final DLNAImageProfile PNG_LRG = new DLNAImageProfile(PNG_LRG_INT, PNG_LRG_STRING, 4096, 4096, null); /** * {@code PNG_TN} maximum resolution 160 x 160. */ public static final DLNAImageProfile PNG_TN = new DLNAImageProfile(PNG_TN_INT, PNG_TN_STRING, 160, 160, null); /** * Creates a new {@link #JPEG_RES_H_V} profile instance with the exact * resolution H x V. Set {@code H} and {@code V} for this instance. The * default mime-type {@code image/jpeg} is used. Not applicable for other * profiles. * * @param horizontal the {@code H} value. * @param vertical the {@code V} value. * @return The new {@link #JPEG_RES_H_V} instance. */ public static DLNAImageProfile createJPEG_RES_H_V(int horizontal, int vertical) { return new DLNAImageProfile( JPEG_RES_H_V_INT, "JPEG_RES_" + Integer.toString(horizontal) + "_" + Integer.toString(vertical), horizontal, vertical, null ); } /** * Creates a new {@link #JPEG_RES_H_V} profile instance with the exact * resolution H x V. Set {@code H} and {@code V} for this instance. Not * applicable for other profiles. * * @param horizontal the {@code H} value. * @param vertical the {@code V} value. * @param mimeType the {@link MimeType} for this profile. * @return The new {@link #JPEG_RES_H_V} instance. */ public static DLNAImageProfile createJPEG_RES_H_V(int horizontal, int vertical, MimeType mimeType) { return new DLNAImageProfile( JPEG_RES_H_V_INT, "JPEG_RES_" + Integer.toString(horizontal) + "_" + Integer.toString(vertical), horizontal, vertical, mimeType ); } /** This profile's integer value */ protected final int imageProfileInt; /** This profile's string value */ protected final String imageProfileStr; /** This profile's mime type value */ protected final MimeType mimeType; private final int horizontal; private final int vertical; /** * Instantiate a {@link DLNAImageProfile} object. */ private DLNAImageProfile( int imageProfileInt, String imageProfileStr, int horizontal, int vertical, MimeType mimeType ) { this.imageProfileInt = imageProfileInt; this.imageProfileStr = imageProfileStr; this.horizontal = horizontal; this.vertical = vertical; this.mimeType = mimeType; } /** * @return The integer representation of this {@link DLNAImageProfile}. */ public int toInt() { return imageProfileInt; } /** * Returns the string representation of this {@link DLNAImageProfile}. */ @Override public String toString() { return imageProfileStr; } /** * Gets the "default" extension for files of this {@link DLNAImageProfile} * in lower case. This is used for appending the correct extension to a * generated image. * * @return The default file extension. */ public String getDefaultExtension() { switch (imageProfileInt) { case GIF_LRG_INT: return "gif"; case JPEG_LRG_INT: case JPEG_MED_INT: case JPEG_RES_H_V_INT: case JPEG_SM_INT: case JPEG_TN_INT: return "jpg"; case PNG_LRG_INT: case PNG_TN_INT: return "png"; default: throw new IllegalStateException( "Profile value " + imageProfileInt + "is not implemented in DLNAImageProfile.getDefaultExtension()"); } } /** * Converts the integer passed as argument to a {@link DLNAImageProfile} * using the default mime-type for the profile. If the conversion fails, * this method returns {@code null}. * * @param value the integer value to convert to a {@link DLNAImageProfile}. * @return The resulting {@link DLNAImageProfile} or {@code null} if the * conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(int value) { return toDLNAImageProfile(value, null, null); } /** * Converts the integer passed as argument to a {@link DLNAImageProfile}. If * the conversion fails, this method returns {@code null}. * * @param value the integer value to convert to a {@link DLNAImageProfile}. * @param mimeType the {@link MimeType} to use for the resulting * {@link DLNAImageProfile}. * @return The resulting {@link DLNAImageProfile} or {@code null} if the * conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(int value, MimeType mimeType) { return toDLNAImageProfile(value, null, mimeType); } /** * Converts the integer passed as argument to a {@link DLNAImageProfile} * using the default mime-type for the profile. If the conversion fails, * this method returns the specified default. * * @param value the integer value to convert to a {@link DLNAImageProfile}. * @param defaultImageProfile the {@link DLNAImageProfile} to return if the * conversion fails. * @return The resulting {@link DLNAImageProfile} or * {@code defaultImageProfile} if the conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(int value, DLNAImageProfile defaultImageProfile) { return toDLNAImageProfile(value, defaultImageProfile, null); } /** * Converts the integer passed as argument to a {@link DLNAImageProfile}. If * the conversion fails, this method returns the specified default. * * @param value the integer value to convert to a {@link DLNAImageProfile}. * @param defaultImageProfile the {@link DLNAImageProfile} to return if the * conversion fails. * @param mimeType the {@link MimeType} to use for the resulting * {@link DLNAImageProfile}. * @return The resulting {@link DLNAImageProfile} or * {@code defaultImageProfile} if the conversion failed. */ public static DLNAImageProfile toDLNAImageProfile( int value, DLNAImageProfile defaultImageProfile, MimeType mimeType ) { /* * Note: Even though this will work without checking if the mimeType is * blank and just sending it as a parameter, doing it this way allows * reuse of the default instances. */ switch (value) { case GIF_LRG_INT: return mimeType == null ? DLNAImageProfile.GIF_LRG : new DLNAImageProfile(GIF_LRG_INT, GIF_LRG_STRING, 1600, 1200, mimeType); case JPEG_LRG_INT: return mimeType == null ? DLNAImageProfile.JPEG_LRG : new DLNAImageProfile(JPEG_LRG_INT, JPEG_LRG_STRING, 4096, 4096, mimeType); case JPEG_MED_INT: return mimeType == null ? DLNAImageProfile.JPEG_MED : new DLNAImageProfile(JPEG_MED_INT, JPEG_MED_STRING, 1024, 768, mimeType); case JPEG_RES_H_V_INT: return mimeType == null ? DLNAImageProfile.JPEG_RES_H_V : new DLNAImageProfile(JPEG_RES_H_V_INT, JPEG_RES_H_V_STRING, -1, -1, mimeType); case JPEG_SM_INT: return mimeType == null ? DLNAImageProfile.JPEG_SM : new DLNAImageProfile(JPEG_SM_INT, JPEG_SM_STRING, 640, 480, mimeType); case JPEG_TN_INT: return mimeType == null ? DLNAImageProfile.JPEG_TN : new DLNAImageProfile(JPEG_TN_INT, JPEG_TN_STRING, 160, 160, mimeType); case PNG_LRG_INT: return mimeType == null ? DLNAImageProfile.PNG_LRG : new DLNAImageProfile(PNG_LRG_INT, PNG_LRG_STRING, 4096, 4096, mimeType); case PNG_TN_INT: return mimeType == null ? DLNAImageProfile.PNG_TN : new DLNAImageProfile(PNG_TN_INT, PNG_TN_STRING, 160, 160, mimeType); default: return defaultImageProfile; } } /** * Converts the {@link String} passed as argument to a * {@link DLNAImageProfile} using the default mime-type for the profile. If * the conversion fails, this method returns {@code null}. * * @param argument the {@link String} to convert to a * {@link DLNAImageProfile}. * @return The resulting {@link DLNAImageProfile} or {@code null} if the * conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(String argument) { return toDLNAImageProfile(argument, null, null); } /** * Converts the {@link String} passed as argument to a * {@link DLNAImageProfile}. If the conversion fails, this method returns * {@code null}. * * @param argument the {@link String} to convert to a * {@link DLNAImageProfile}. * @param mimeType the {@link MimeType} to use for the resulting * {@link DLNAImageProfile}. * @return The resulting {@link DLNAImageProfile} or {@code null} if the * conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(String argument, MimeType mimeType) { return toDLNAImageProfile(argument, null, mimeType); } /** * Converts the {@link String} passed as argument to a * {@link DLNAImageProfile} using the default mime-type for the profile. If * the conversion fails, this method returns the specified default. * * @param argument the {@link String} to convert to a * {@link DLNAImageProfile}. * @param defaultImageProfile the {@link DLNAImageProfile} to return if the * conversion fails. * @return The resulting {@link DLNAImageProfile} or * {@code defaultImageProfile} if the conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(String argument, DLNAImageProfile defaultImageProfile) { if (argument == null) { return defaultImageProfile; } return toDLNAImageProfile(argument, defaultImageProfile, null); } /** * Converts the {@link String} passed as argument to a * {@link DLNAImageProfile}. If the conversion fails, this method returns * the specified default. * * @param argument the {@link String} to convert to a * {@link DLNAImageProfile}. * @param defaultImageProfile the {@link DLNAImageProfile} to return if the * conversion fails. * @param mimeType the {@link MimeType} to use for the resulting * {@link DLNAImageProfile}. * @return The resulting {@link DLNAImageProfile} or * {@code defaultImageProfile} if the conversion failed. */ public static DLNAImageProfile toDLNAImageProfile(String argument, DLNAImageProfile defaultImageProfile, MimeType mimeType) { if (argument == null) { return defaultImageProfile; } argument = argument.toUpperCase(Locale.ROOT); /* * Note: Even though this will work without checking if the mimeType is * blank and just sending it as a parameter, doing it this way allows * reuse of the default instances. */ switch (argument) { case GIF_LRG_STRING: return mimeType == null ? DLNAImageProfile.GIF_LRG : new DLNAImageProfile(GIF_LRG_INT, GIF_LRG_STRING, 1600, 1200, mimeType); case JPEG_LRG_STRING: return mimeType == null ? DLNAImageProfile.JPEG_LRG : new DLNAImageProfile(JPEG_LRG_INT, JPEG_LRG_STRING, 4096, 4096, mimeType); case JPEG_MED_STRING: return mimeType == null ? DLNAImageProfile.JPEG_MED : new DLNAImageProfile(JPEG_MED_INT, JPEG_MED_STRING, 1024, 768, mimeType); case JPEG_RES_H_V_STRING: return mimeType == null ? DLNAImageProfile.JPEG_RES_H_V : new DLNAImageProfile(JPEG_RES_H_V_INT, JPEG_RES_H_V_STRING, -1, -1, mimeType); case JPEG_SM_STRING: return mimeType == null ? DLNAImageProfile.JPEG_SM : new DLNAImageProfile(JPEG_SM_INT, JPEG_SM_STRING, 640, 480, mimeType); case JPEG_TN_STRING: return mimeType == null ? DLNAImageProfile.JPEG_TN : new DLNAImageProfile(JPEG_TN_INT, JPEG_TN_STRING, 160, 160, mimeType); case PNG_LRG_STRING: return mimeType == null ? DLNAImageProfile.PNG_LRG : new DLNAImageProfile(PNG_LRG_INT, PNG_LRG_STRING, 4096, 4096, mimeType); case PNG_TN_STRING: return mimeType == null ? DLNAImageProfile.PNG_TN : new DLNAImageProfile(PNG_TN_INT, PNG_TN_STRING, 160, 160, mimeType); default: if (argument.startsWith("JPEG_RES")) { Matcher matcher = Pattern.compile("^JPEG_RES_?(\\d+)[X_](\\d+)").matcher(argument); if (matcher.find()) { return new DLNAImageProfile( JPEG_RES_H_V_INT, "JPEG_RES_" + matcher.group(1) + "_" + matcher.group(2), Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), mimeType ); } } return defaultImageProfile; } } /** * Parses and converts a {@link ProtocolInfo} to a one or more * {@link DLNAImageProfile}(s). If the conversion fails, this method returns * {@code null}. * * @param protocolInfo the {@link ProtocolInfo} instance to parse. * @return The resulting {@link DLNAImageProfile}(s) or {@code null} if the * conversion failed. */ public static Set<DLNAImageProfile> toDLNAImageProfiles(ProtocolInfo protocolInfo) { Set<DLNAImageProfile> result = new HashSet<>(); if ( protocolInfo != null && ( protocolInfo.getProtocol() == Protocol.HTTP_GET || protocolInfo.getProtocol() == Protocol.ALL ) && protocolInfo.getMimeType() != null && ( protocolInfo.getMimeType().isAnyType() || "image".equals(protocolInfo.getMimeType().getType().trim().toLowerCase(Locale.ROOT)) ) ) { DLNAOrgProfileName dlnaProfileName = protocolInfo.getDLNAProfileName(); if (dlnaProfileName != null) { if (dlnaProfileName instanceof KnownDLNAOrgProfileName) { switch ((KnownDLNAOrgProfileName) dlnaProfileName) { case GIF_LRG: result.add(GIF_LRG); break; case JPEG_LRG: result.add(JPEG_LRG); break; case JPEG_MED: result.add(JPEG_MED); break; case JPEG_RES_H_V: result.add(JPEG_RES_H_V); break; case JPEG_SM: result.add(JPEG_SM); break; case JPEG_TN: result.add(JPEG_TN); break; case PNG_LRG: result.add(PNG_LRG); break; case PNG_TN: result.add(PNG_TN); break; case JPEG_SM_ICO: case JPEG_LRG_ICO: case PNG_SM_ICO: case PNG_LRG_ICO: default: break; } } else { // Handles specified JPEG_RES_H_V profiles and any other "unknowns" DLNAImageProfile profile = toDLNAImageProfile(dlnaProfileName.getValue(), null, protocolInfo.getMimeType()); if (profile != null) { result.add(profile); LOGGER.trace( "DLNAImageProfile parsed \"{}\" as {}", protocolInfo, profile ); } else if (LOGGER.isDebugEnabled()) { LOGGER.debug( "DLNAImageProfile was unable to parse DLNA profile from \"{}\"", protocolInfo ); } } return result; } if (protocolInfo.getProfileName() != null) { // If we find a profile name that isn't a DLNA profile name, we're not interested return result; } MimeType mimeType = protocolInfo.getMimeType(); if ( mimeType == null || mimeType.isAnyType() || mimeType.isAnySubtype() ) { result.add(JPEG_TN); result.add(JPEG_SM); result.add(JPEG_MED); result.add(JPEG_LRG); result.add(JPEG_RES_H_V); result.add(PNG_TN); result.add(PNG_LRG); result.add(GIF_LRG); LOGGER.trace( "DLNAImageProfile parsed \"{}\" as all image profiles", protocolInfo ); } else if (MIMETYPE_GIF.equals(mimeType)) { result.add(new DLNAImageProfile(GIF_LRG_INT, GIF_LRG_STRING, 1600, 1200, mimeType)); LOGGER.trace("DLNAImageProfile parsed \"{}\" as all GIF profiles", protocolInfo); } else if (MIMETYPE_JPEG.equals(mimeType)) { result.add(new DLNAImageProfile(JPEG_LRG_INT, JPEG_LRG_STRING, 4096, 4096, mimeType)); result.add(new DLNAImageProfile(JPEG_MED_INT, JPEG_MED_STRING, 1024, 768, mimeType)); result.add(new DLNAImageProfile(JPEG_RES_H_V_INT, JPEG_RES_H_V_STRING, -1, -1, mimeType)); result.add(new DLNAImageProfile(JPEG_SM_INT, JPEG_SM_STRING, 640, 480, mimeType)); result.add(new DLNAImageProfile(JPEG_TN_INT, JPEG_TN_STRING, 160, 160, mimeType)); LOGGER.trace("DLNAImageProfile parsed \"{}\" as all JPEG profiles", protocolInfo); } else if (MIMETYPE_PNG.equals(mimeType)) { result.add(new DLNAImageProfile(PNG_LRG_INT, PNG_LRG_STRING, 4096, 4096, mimeType)); result.add(new DLNAImageProfile(PNG_TN_INT, PNG_TN_STRING, 160, 160, mimeType)); LOGGER.trace("DLNAImageProfile parsed \"{}\" as all PNG profiles", protocolInfo); } } return result; } /** * @return The {@link ImageFormat} for this {@link DLNAImageProfile}. */ public ImageFormat getFormat() { switch (imageProfileInt) { case GIF_LRG_INT: return ImageFormat.GIF; case JPEG_LRG_INT: case JPEG_MED_INT: case JPEG_RES_H_V_INT: case JPEG_SM_INT: case JPEG_TN_INT: return ImageFormat.JPEG; case PNG_LRG_INT: case PNG_TN_INT: return ImageFormat.PNG; default: throw new IllegalStateException("Profile is missing from switch statement"); } } /** * @return The {@link MimeType} for this {@link DLNAImageProfile}. */ public MimeType getMimeType() { if (mimeType != null) { return mimeType; } return getDefaultMimeType(); } /** * @return The default {@link MimeType} for this * {@link DLNAImageProfile}. */ public MimeType getDefaultMimeType() { switch (getFormat()) { case GIF: return MIMETYPE_GIF; case JPEG: return MIMETYPE_JPEG; case PNG: return MIMETYPE_PNG; default: throw new IllegalStateException("Format is missing from switch statement"); } } /** * @return The formatted mime-type {@link String} for this * {@link DLNAImageProfile}. */ public String getMimeTypeString() { return getMimeType().toStringWithoutParameters(); } /** * Gets the maximum image width for the given {@link DLNAImageProfile}. * * @return The value in pixels. */ public int getMaxWidth() { return horizontal; } /** * Get {@code H} for the {@code JPEG_RES_H_V} profile. * * @return The {@code H} value in pixels. */ public int getH() { return horizontal; } /** * Gets the maximum image height for the given {@link DLNAImageProfile}. * * @return The value in pixels. */ public int getMaxHeight() { return vertical; } /** * Get {@code V} for the {@code JPEG_RES_H_V} profile. * * @return The {@code V} value in pixels. */ public int getV() { return vertical; } /** * Get the {@link Dimension} of the maximum image resolution for the given * {@link DLNAImageProfile}. * * @return The {@link Dimension}. */ public Dimension getMaxResolution() { if (horizontal >= 0 && vertical >= 0) { return new Dimension(horizontal, vertical); } return new Dimension(); } /** * Evaluates whether the thumbnail or the image itself should be used as * the source for the given profile when two sources are available. This is * typically only relevant for image resources. * * @param imageInfo the {@link ImageInfo} for the non-thumbnail source. * @param thumbnailImageInfo the {@link ImageInfo} for the thumbnail source. * @return The evaluation result. */ public boolean useThumbnailSource(ImageInfo imageInfo, ImageInfo thumbnailImageInfo) { if (DLNAImageProfile.JPEG_TN.equals(this) || DLNAImageProfile.PNG_TN.equals(this)) { // These should always use thumbnail as the source return true; } if ( imageInfo == null || imageInfo.getWidth() < 1 || imageInfo.getHeight() < 1 || thumbnailImageInfo == null || thumbnailImageInfo.getWidth() < 1 || thumbnailImageInfo.getHeight() < 1 ) { // Impossible to do a valid evaluation under these circumstances return false; } boolean thumbIsSmaller = thumbnailImageInfo.getWidth() < imageInfo.getWidth() || thumbnailImageInfo.getHeight() < imageInfo.getHeight(); if (!thumbIsSmaller) { // Thumbnail has as good a resolution as the source, we might as // well use the thumbnail if the format matches return getFormat() == thumbnailImageInfo.getFormat() || getFormat() != imageInfo.getFormat(); } // At this point we know that the thumbnail is smaller than the source. // Only use the thumbnail if it's bigger or equal in size to this profile. return getMaxWidth() <= thumbnailImageInfo.getWidth() && getMaxHeight() <= thumbnailImageInfo.getHeight(); } /** * Performs GIF compliance checks. The result is stored in * {@code complianceResult}. * * @param imageInfo the {@link ImageInfo} instance to check. * @param complianceResult the {@link InternalComplianceResult} where the * results are added. */ protected static void checkGIF( ImageInfo imageInfo, InternalComplianceResult complianceResult ) { if (imageInfo == null || imageInfo.getFormat() != ImageFormat.GIF || !(imageInfo instanceof GIFInfo)) { return; } complianceResult.colorsCorrect = true; GIFInfo gifInfo = (GIFInfo) imageInfo; complianceResult.formatCorrect = "89a".equals(gifInfo.getFormatVersion()); if (!complianceResult.formatCorrect) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "GIF DLNA compliance failed with wrong GIF version \"%s\"", gifInfo.getFormatVersion() )); } else { complianceResult.failures.add("GIF version"); } } } /** * Performs JPEG compliance checks. The result is stored in * {@code complianceResult}. * * @param imageInfo the {@link ImageInfo} instance to check. * @param complianceResult the {@link InternalComplianceResult} where the * results are added. */ protected static void checkJPEG( ImageInfo imageInfo, InternalComplianceResult complianceResult ) { if (imageInfo == null || imageInfo.getFormat() != ImageFormat.JPEG || !(imageInfo instanceof JPEGInfo)) { return; } JPEGInfo jpegInfo = (JPEGInfo) imageInfo; complianceResult.colorsCorrect = jpegInfo.getColorSpaceType() == ColorSpaceType.TYPE_GRAY || jpegInfo.getColorSpaceType() == ColorSpaceType.TYPE_RGB || jpegInfo.getColorSpaceType() == ColorSpaceType.TYPE_YCbCr; if (!complianceResult.colorsCorrect) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with illegal color space \"%s\"", jpegInfo.getColorSpaceType() )); } else { complianceResult.failures.add("color space"); } } if ( jpegInfo.getExifColorSpace() != null && jpegInfo.getExifColorSpace() != ExifColorSpace.SRGB && jpegInfo.getExifColorSpace() != ExifColorSpace.UNCALIBRATED ) { complianceResult.colorsCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with illegal Exif color space \"%s\"", jpegInfo.getExifColorSpace() )); } else { complianceResult.failures.add("Exif color space"); } } complianceResult.formatCorrect = jpegInfo.getJFIFVersion() >= 0x102 || jpegInfo.getExifVersion() >= 100; if (!complianceResult.formatCorrect && LOGGER.isTraceEnabled()) { String jfifVersion = jpegInfo.getJFIFVersion() == ImageInfo.UNKNOWN ? null : Double.toHexString((double) jpegInfo.getJFIFVersion() / 0x100); if (jfifVersion != null) { jfifVersion = jfifVersion.replaceFirst("0x", "").replaceFirst("p-?\\d*", ""); } String exifVersion = jpegInfo.getExifVersion() == ImageInfo.UNKNOWN ? null : Double.toString((double) jpegInfo.getExifVersion() / 100); if (jfifVersion == null && exifVersion == null) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add("JPEG DLNA compliance failed with missing Exif- and JFIF version"); } else { complianceResult.failures.add("missing version information"); } } else if (jfifVersion == null) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with too low Exif version \"%s\"", exifVersion )); } else { complianceResult.failures.add("Exif version"); } } else if (exifVersion == null) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with too low JFIF version \"%s\"", jfifVersion )); } else { complianceResult.failures.add("JFIF version"); } } else { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with too low Exif version \"%s\" and JFIF version \"%s\"", exifVersion, jfifVersion )); } else { complianceResult.failures.add("Exif and JFIF version"); } } } if (jpegInfo.getCompressionType() != CompressionType.BASELINE_HUFFMAN) { complianceResult.formatCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with illegal compression type \"%s\"", jpegInfo.getCompressionType() )); } else { complianceResult.failures.add("compression type"); } } if (jpegInfo.getBitDepth() != 8) { complianceResult.formatCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with illegal bit depth \"%d\"", jpegInfo.getBitDepth() )); } else { complianceResult.failures.add("bit depth"); } } if (jpegInfo.getNumComponents() != 3 && jpegInfo.getNumComponents() != 1) { complianceResult.formatCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with illegal number of components \"%d\"", jpegInfo.getNumComponents() )); } else { complianceResult.failures.add("number of components"); } } if (!jpegInfo.isTypicalHuffman()) { complianceResult.formatCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add("JPEG DLNA compliance failed with non-typical Huffman tables"); } else { complianceResult.failures.add("optimized"); } } if ( jpegInfo.getNumComponents() == 3 && !(new JPEGSubsamplingNotation(4, 2, 2)).equals(jpegInfo.getChromaSubsampling()) && !(new JPEGSubsamplingNotation(4, 2, 0)).equals(jpegInfo.getChromaSubsampling()) ) { complianceResult.formatCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "JPEG DLNA compliance failed with illegal chroma subsampling \"%s\"", jpegInfo.getChromaSubsampling() )); } else { complianceResult.failures.add("chroma subsampling"); } } } /** * Performs PNG compliance checks. The result is stored in * {@code complianceResult}. * * @param imageInfo the {@link ImageInfo} instance to check. * @param complianceResult the {@link InternalComplianceResult} where the * results are added. */ protected static void checkPNG( ImageInfo imageInfo, InternalComplianceResult complianceResult ) { if (imageInfo == null || imageInfo.getFormat() != ImageFormat.PNG || !(imageInfo instanceof PNGInfo)) { return; } PNGInfo pngInfo = (PNGInfo) imageInfo; if (pngInfo.getColorType() != null) { switch (pngInfo.getColorType()) { case Greyscale: case GreyscaleWithAlpha: if (pngInfo.getBitDepth() == 8 || pngInfo.getBitDepth() == 16) { complianceResult.colorsCorrect = true; } else { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "PNG DLNA compliance failed with illegal bit depth \"%d\"", pngInfo.getBitDepth() )); } else { complianceResult.failures.add("bit depth"); } } break; case IndexedColor: case TrueColor: case TrueColorWithAlpha: if (pngInfo.getBitDepth() == 8) { complianceResult.colorsCorrect = true; } else { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "PNG DLNA compliance failed with illegal bit depth \"%d\"", pngInfo.getBitDepth() )); } else { complianceResult.failures.add("bit depth"); } } break; default: if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "PNG DLNA compliance failed with illegal color type \"%s\"", pngInfo.getColorType() )); } else { complianceResult.failures.add("color type"); } } } else if (LOGGER.isTraceEnabled()) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add("PNG DLNA compliance failed with missing color type information"); } else { complianceResult.failures.add("missing color type"); } } if (pngInfo.isModifiedBitDepth()) { complianceResult.colorsCorrect = false; if (LOGGER.isTraceEnabled()) { complianceResult.failures.add("PNG DLNA compliance failed with non-standard bit depth"); } else { complianceResult.failures.add("non-standard bit depth"); } } complianceResult.formatCorrect = pngInfo.getInterlaceMethod() == InterlaceMethod.NONE; if (!complianceResult.formatCorrect && LOGGER.isTraceEnabled()) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "PNG DLNA compliance failed with illegal interlace method \"%s\"", pngInfo.getInterlaceMethod() )); } else { complianceResult.failures.add("interlace method"); } } } /** * Finds the best fitting {@link DLNAImageProfile} for an image of the given * resolution and {@link ImageFormat}. * * @param resolution the image resolution. * @param format the image {@link ImageFormat}. * @param allowJPEG_RES_H_V Whether or not * {@link DLNAImageProfile#JPEG_RES_H_V} is allowed. When this is * true, all JPEG images will use this profile. * @return The best fitting {@link DLNAImageProfile}. */ public static DLNAImageProfile getClosestDLNAProfile( Dimension resolution, ImageFormat format, boolean allowJPEG_RES_H_V ) { return getClosestDLNAProfile(resolution.width, resolution.height, format, allowJPEG_RES_H_V); } /** * Finds the best fitting {@link DLNAImageProfile} for an image of the given * resolution and {@link ImageFormat}. * * @param width the image width. * @param height the image height. * @param format the image {@link ImageFormat}. * @param allowJPEG_RES_H_V Whether or not * {@link DLNAImageProfile#JPEG_RES_H_V} is allowed. When this is * true, all JPEG images will use this profile. * @return The best fitting {@link DLNAImageProfile}. */ public static DLNAImageProfile getClosestDLNAProfile( int width, int height, ImageFormat format, boolean allowJPEG_RES_H_V ) { if (format == null) { throw new NullPointerException("format cannot be null"); } if (width < 1 || height < 1) { throw new IllegalArgumentException(String.format( "Cannot find DLNA media format profile for %d x %d", width, height )); } switch (format) { case GIF: return GIF_LRG; case JPEG: if (allowJPEG_RES_H_V) { return createJPEG_RES_H_V(width, height); } if (JPEG_TN.isResolutionCorrect(width, height)) { return JPEG_TN; } if (JPEG_SM.isResolutionCorrect(width, height)) { return JPEG_SM; } if (JPEG_MED.isResolutionCorrect(width, height)) { return JPEG_MED; } return JPEG_LRG; case PNG: if (PNG_TN.isResolutionCorrect(width, height)) { return PNG_TN; } return PNG_LRG; default: throw new IllegalArgumentException( "Invalid format " + format + " for which to find a DLNA media format profile" ); } } /** * Validates DLNA compliance for {@code imageInfo} against the largest * {@link DLNAImageProfile} for the given {@link ImageFormat}. Validation * is performed on resolution, format, format version, format compression * and color coding. The validation result is returned in a * {@link DLNAComplianceResult} data structure. * * @param imageInfo the {@link ImageInfo} for the image to check for DLNA * compliance. * @param format the {@link ImageFormat} to check for DLNA compliance * against. * @return The validation result. */ public static DLNAComplianceResult checkCompliance(ImageInfo imageInfo, ImageFormat format) { if (imageInfo == null) { throw new NullPointerException("imageInfo cannot be null"); } if (format == null) { throw new NullPointerException("format cannot be null"); } InternalComplianceResult complianceResult = new InternalComplianceResult(); DLNAImageProfile largestProfile; switch (format) { case JPEG: // Since JPEG_RES_H_V has no upper limit, any resolution is ok complianceResult.maxWidth = Integer.MAX_VALUE; complianceResult.maxHeight = Integer.MAX_VALUE; complianceResult.resolutionCorrect = true; checkJPEG(imageInfo, complianceResult); break; case GIF: largestProfile = DLNAImageProfile.GIF_LRG; complianceResult.maxWidth = largestProfile.getMaxWidth(); complianceResult.maxHeight = largestProfile.getMaxHeight(); complianceResult.resolutionCorrect = largestProfile.isResolutionCorrect(imageInfo); if (!complianceResult.resolutionCorrect) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "GIF DLNA compliance failed with wrong resolution %d x %d (limits are %d x %d)", imageInfo.getWidth(), imageInfo.getHeight(), largestProfile.getMaxWidth(), largestProfile.getMaxHeight() )); } else { complianceResult.failures.add("resolution"); } } checkGIF(imageInfo, complianceResult); break; case PNG: largestProfile = DLNAImageProfile.PNG_LRG; complianceResult.maxWidth = largestProfile.getMaxWidth(); complianceResult.maxHeight = largestProfile.getMaxHeight(); complianceResult.resolutionCorrect = largestProfile.isResolutionCorrect(imageInfo); if (!complianceResult.resolutionCorrect) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "PNG DLNA compliance failed with wrong resolution %d x %d (limits are %d x %d)", imageInfo.getWidth(), imageInfo.getHeight(), largestProfile.getMaxWidth(), largestProfile.getMaxHeight() )); } else { complianceResult.failures.add("resolution"); } } checkPNG(imageInfo, complianceResult); break; default: if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "DLNA compliance failed with illegal image format \"%s\"", format )); } else { complianceResult.failures.add("illegal format"); } } return DLNAComplianceResult.toDLNAComplianceResult(complianceResult); } /** * Validates DLNA compliance for {@code imageInfo} against a specific * {@link DLNAImageProfile}. Validation is performed on resolution, format, * format version, format compression and color coding. The validation * result is returned in a {@link DLNAComplianceResult} data structure. * * @param imageInfo the {@link ImageInfo} for the image to check for DLNA * compliance. * @param profile the {@link DLNAImageProfile} to check for DLNA compliance * against. * @return The validation result. */ public static DLNAComplianceResult checkCompliance(ImageInfo imageInfo, DLNAImageProfile profile) { if (imageInfo == null || profile == null) { return DLNAComplianceResult.toDLNAComplianceResult(new InternalComplianceResult()); } return profile.checkCompliance(imageInfo); } /** * Determines whether the resolution specified in {@code imageInfo} is valid * for this {@link DLNAImageProfile}. * * @param imageInfo the {@link ImageInfo} to evaluate. * @return {@code true} if the resolution is valid, {@code false} otherwise. */ public boolean isResolutionCorrect(ImageInfo imageInfo) { if (imageInfo == null) { return false; } return isResolutionCorrect(imageInfo.getWidth(), imageInfo.getHeight()); } /** * Determines whether the resolution specified in {@code width} and * {@code height} is valid for this {@link DLNAImageProfile}. * * @param width the number of horizontal pixels for this resolution. * @param height the number of vertical pixels for this resolution. * @return {@code true} if the resolution is valid, {@code false} otherwise. */ public boolean isResolutionCorrect(int width, int height) { if (width < 1 || height < 1) { return false; } if (equals(DLNAImageProfile.JPEG_RES_H_V)) { return width == horizontal && height == vertical; } return width <= horizontal && height <= vertical; } /** * Validates DLNA compliance for {@code image} against this * {@link DLNAImageProfile}. Validation is performed on resolution, format, * format version, format compression and color coding. The validation * result is returned in a {@link DLNAComplianceResult} data structure. * * @param imageInfo the {@link ImageInfo} for the image to check for DLNA * compliance. * @return The validation result. */ public DLNAComplianceResult checkCompliance(ImageInfo imageInfo) { InternalComplianceResult complianceResult = new InternalComplianceResult(); complianceResult.maxWidth = horizontal; complianceResult.maxHeight = vertical; if (imageInfo == null) { return DLNAComplianceResult.toDLNAComplianceResult(complianceResult); } complianceResult.resolutionCorrect = isResolutionCorrect(imageInfo); if (!complianceResult.resolutionCorrect) { if (LOGGER.isTraceEnabled()) { complianceResult.failures.add(String.format( "%s DLNA compliance failed with wrong resolution %d x %d (limits are %d x %d)", toString(), imageInfo.getWidth(), imageInfo.getHeight(), horizontal, vertical )); } else { complianceResult.failures.add("resolution"); } } switch (this.toInt()) { case DLNAImageProfile.GIF_LRG_INT: checkGIF(imageInfo, complianceResult); break; case DLNAImageProfile.JPEG_LRG_INT: case DLNAImageProfile.JPEG_MED_INT: case DLNAImageProfile.JPEG_RES_H_V_INT: case DLNAImageProfile.JPEG_SM_INT: case DLNAImageProfile.JPEG_TN_INT: checkJPEG(imageInfo, complianceResult); break; case DLNAImageProfile.PNG_LRG_INT: case DLNAImageProfile.PNG_TN_INT: checkPNG(imageInfo, complianceResult); break; default: throw new IllegalStateException("Illegal DLNA media profile"); } return DLNAComplianceResult.toDLNAComplianceResult(complianceResult); } /** * Calculates the would-be resolution and size of {@code imageInfo} if that * image were to be converted into this profile. Size cannot be calculated * if any actual conversion is needed (finding that would require an actual * encoding to be done), and will return {@code null} unless the image can * be used as-is. * * @param imageInfo the {@link ImageInfo} for the image in question. * * @return The result in a {@link HypotheticalResult} data structure. */ public HypotheticalResult calculateHypotheticalProperties(ImageInfo imageInfo) { if (imageInfo == null) { throw new IllegalArgumentException("calculateHypotheticalProperties: imageInfo cannot be null"); } if (imageInfo.getWidth() < 1 || imageInfo.getHeight() < 1) { return new HypotheticalResult(ImageInfo.UNKNOWN, ImageInfo.UNKNOWN, null, true); } DLNAComplianceResult complianceResult = checkCompliance(imageInfo); if (complianceResult.isAllCorrect()) { return new HypotheticalResult( imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getSize(), false ); } if (complianceResult.resolutionCorrect) { return new HypotheticalResult(imageInfo.getWidth(), imageInfo.getHeight(), null, true); } Dimension scaledResolution = ImagesUtil.calculateScaledResolution( imageInfo, this.equals(JPEG_RES_H_V) ? ScaleType.EXACT : ScaleType.MAX, horizontal, vertical ); return new HypotheticalResult( scaledResolution.width, scaledResolution.height, null, true ); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + imageProfileInt; return result; } /** * Only the profile constant is considered when comparing, metadata like * {@code H} and {@code V} aren't taken into account. */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof DLNAImageProfile)) { return false; } DLNAImageProfile other = (DLNAImageProfile) obj; if (imageProfileInt != other.imageProfileInt) { return false; } return true; } @Override public int compareTo(DLNAImageProfile o) { if (o == null) { return -1; } int result = Integer.compare(imageProfileInt, o.imageProfileInt); if (result != 0) { return result; } if (imageProfileStr == null && o.imageProfileStr == null) { return 0; } if (imageProfileStr == null) { return 1; } if (o.imageProfileStr == null) { return -1; } return imageProfileStr.compareTo(o.imageProfileStr); } /** * Immutable {@link #calculateHypotheticalProperties(ImageInfo)} * result data structure. Please note that {@code size} might be {@code null}. */ public static class HypotheticalResult { /** * The calculated width or {@link ImageInfo#UNKNOWN} if unknown. */ public final int width; /** * The calculated height or {@link ImageInfo#UNKNOWN} if unknown. */ public final int height; /** * The size or {@code null} if unknown. */ public final Long size; /** * Whether conversion is needed. */ public final boolean conversionNeeded; /** * Instantiates a new hypothetical result. * * @param width the estimated image width. * @param height the estimated image height. * @param size the estimated image size. * @param conversionNeeded the "conversion needed" evaluation result. */ public HypotheticalResult(int width, int height, Long size, boolean conversionNeeded) { if (width < 1 || height < 1) { // Use ImageInfo.UNKNOWN for unknown this.width = ImageInfo.UNKNOWN; this.height = ImageInfo.UNKNOWN; } else { this.width = width; this.height = height; } this.size = size; this.conversionNeeded = conversionNeeded; } @Override public String toString() { return "HypotheticalResult [width=" + width + ", height=" + height + ", size=" + size + ", conversionNeeded=" + conversionNeeded + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (conversionNeeded ? 1231 : 1237); result = prime * result + height; result = prime * result + ((size == null) ? 0 : size.hashCode()); result = prime * result + width; return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof HypotheticalResult)) { return false; } HypotheticalResult other = (HypotheticalResult) obj; if (conversionNeeded != other.conversionNeeded) { return false; } if (height != other.height) { return false; } if (size == null) { if (other.size != null) { return false; } } else if (!size.equals(other.size)) { return false; } if (width != other.width) { return false; } return true; } } /** * Internal mutable compliance result data structure. */ @SuppressWarnings({"checkstyle:VisibilityModifier", "JavadocVariable"}) protected static class InternalComplianceResult { public boolean resolutionCorrect = false; public boolean formatCorrect = false; public boolean colorsCorrect = false; public int maxWidth; public int maxHeight; public List<String> failures = new ArrayList<>(); } /** * Immutable compliance result data structure. */ public static class DLNAComplianceResult { private final boolean resolutionCorrect; private final boolean formatCorrect; private final boolean colorsCorrect; private final boolean allCorrect; private final int maxWidth; private final int maxHeight; private final List<String> failures; /** * Instantiates a new DLNA compliance result. * * @param resolutionCorrect the is resolution correct flag. * @param formatCorrect the is format correct flag. * @param colorsCorrect the are colors correct flag. * @param maxWidth the maximum image width. * @param maxHeight the maximum image height. * @param failures the {@link List} of failures. */ public DLNAComplianceResult( boolean resolutionCorrect, boolean formatCorrect, boolean colorsCorrect, int maxWidth, int maxHeight, List<String> failures ) { this.resolutionCorrect = resolutionCorrect; this.formatCorrect = formatCorrect; this.colorsCorrect = colorsCorrect; this.allCorrect = resolutionCorrect && formatCorrect && colorsCorrect; this.maxWidth = maxWidth; this.maxHeight = maxHeight; this.failures = failures; } /** * Instantiates a new {@link DLNAComplianceResult} from an * {@link InternalComplianceResult}. * * @param result the source {@link InternalComplianceResult}. * @return The resulting {@link DLNAComplianceResult}. */ protected static DLNAComplianceResult toDLNAComplianceResult(InternalComplianceResult result) { return new DLNAComplianceResult( result.resolutionCorrect, result.formatCorrect, result.colorsCorrect, result.maxWidth, result.maxHeight, result.failures ); } /** * @return Whether the resolution is within DLNA limits for the given * profile. */ public boolean isResolutionCorrect() { return resolutionCorrect; } /** * @return Whether the format/compression is compliant for the given * profile. */ public boolean isFormatCorrect() { return formatCorrect; } /** * @return Whether the color coding is compliant for the given profile. */ public boolean isColorsCorrect() { return colorsCorrect; } /** * @return Whether all tests show compliance. */ public boolean isAllCorrect() { return allCorrect; } /** * @return The maximum width. */ public int getMaxWidth() { return maxWidth; } /** * @return the maximum height. */ public int getMaxHeight() { return maxHeight; } /** * @return The {@link List} of failure messages. */ public List<String> getFailures() { return Collections.unmodifiableList(this.failures); } } }