mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 03:55:28 -04:00
#631 New way of forcing profile activation + guarding all invocations of ICC_Profile.getInstance()
This commit is contained in:
parent
3911191b04
commit
b2c5915db8
@ -39,6 +39,7 @@ import com.twelvemonkeys.util.LRUHashMap;
|
|||||||
import java.awt.color.ColorSpace;
|
import java.awt.color.ColorSpace;
|
||||||
import java.awt.color.ICC_ColorSpace;
|
import java.awt.color.ICC_ColorSpace;
|
||||||
import java.awt.color.ICC_Profile;
|
import java.awt.color.ICC_Profile;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
@ -96,6 +97,8 @@ public final class ColorSpaces {
|
|||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public static final int CS_GENERIC_CMYK = 5001;
|
public static final int CS_GENERIC_CMYK = 5001;
|
||||||
|
|
||||||
|
static final int ICC_PROFILE_HEADER_SIZE = 128;
|
||||||
|
|
||||||
// Weak references to hold the color spaces while cached
|
// Weak references to hold the color spaces while cached
|
||||||
private static WeakReference<ICC_Profile> adobeRGB1998 = new WeakReference<>(null);
|
private static WeakReference<ICC_Profile> adobeRGB1998 = new WeakReference<>(null);
|
||||||
private static WeakReference<ICC_Profile> genericCMYK = new WeakReference<>(null);
|
private static WeakReference<ICC_Profile> genericCMYK = new WeakReference<>(null);
|
||||||
@ -104,16 +107,8 @@ public final class ColorSpaces {
|
|||||||
private static final Map<Key, ICC_ColorSpace> cache = new LRUHashMap<>(10);
|
private static final Map<Key, ICC_ColorSpace> cache = new LRUHashMap<>(10);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
// In case we didn't activate through SPI already
|
||||||
// Force invocation of ProfileDeferralMgr.activateProfiles() to avoid JDK-6986863
|
ProfileDeferralActivator.activateProfiles();
|
||||||
ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData();
|
|
||||||
}
|
|
||||||
catch (Throwable disasters) {
|
|
||||||
System.err.println("ICC Color Profile not properly activated due to the exception below.");
|
|
||||||
System.err.println("Expect to see JDK-6986863 in action, and consider filing a bug report to your JRE provider.");
|
|
||||||
|
|
||||||
disasters.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ColorSpaces() {}
|
private ColorSpaces() {}
|
||||||
@ -148,9 +143,11 @@ public final class ColorSpaces {
|
|||||||
|
|
||||||
private static byte[] getProfileHeaderWithProfileId(final ICC_Profile profile) {
|
private static byte[] getProfileHeaderWithProfileId(final ICC_Profile profile) {
|
||||||
// Get *entire profile data*... :-/
|
// Get *entire profile data*... :-/
|
||||||
byte[] data = profile.getData();
|
return getProfileHeaderWithProfileId(profile.getData());
|
||||||
|
}
|
||||||
|
|
||||||
// Clear out preferred CMM, platform & creator, as these does not affect the profile in any way
|
private static byte[] getProfileHeaderWithProfileId(byte[] data) {
|
||||||
|
// Clear out preferred CMM, platform & creator, as these don't affect the profile in any way
|
||||||
// - LCMS updates CMM + creator to "lcms" and platform to current platform
|
// - LCMS updates CMM + creator to "lcms" and platform to current platform
|
||||||
// - KCMS keeps the values in the file...
|
// - KCMS keeps the values in the file...
|
||||||
Arrays.fill(data, ICC_Profile.icHdrCmmId, ICC_Profile.icHdrCmmId + 4, (byte) 0);
|
Arrays.fill(data, ICC_Profile.icHdrCmmId, ICC_Profile.icHdrCmmId + 4, (byte) 0);
|
||||||
@ -167,7 +164,7 @@ public final class ColorSpaces {
|
|||||||
System.arraycopy(md5, 0, data, ICC_Profile.icHdrProfileID, md5.length);
|
System.arraycopy(md5, 0, data, ICC_Profile.icHdrProfileID, md5.length);
|
||||||
|
|
||||||
// ICC profile header is the first 128 bytes
|
// ICC profile header is the first 128 bytes
|
||||||
return Arrays.copyOf(data, 128);
|
return Arrays.copyOf(data, ICC_PROFILE_HEADER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] computeMD5(byte[] data) {
|
private static byte[] computeMD5(byte[] data) {
|
||||||
@ -203,25 +200,35 @@ public final class ColorSpaces {
|
|||||||
Key key = new Key(profileHeader);
|
Key key = new Key(profileHeader);
|
||||||
|
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
ICC_ColorSpace cs = cache.get(key);
|
ICC_ColorSpace cs = getCachedCS(key);
|
||||||
|
|
||||||
if (cs == null) {
|
if (cs == null) {
|
||||||
cs = new ICC_ColorSpace(profile);
|
cs = new ICC_ColorSpace(profile);
|
||||||
|
|
||||||
validateColorSpace(cs);
|
validateColorSpace(cs);
|
||||||
|
|
||||||
// On LCMS, validation *alters* the profile header, need to re-generate key
|
|
||||||
key = profileCleaner.validationAltersProfileHeader()
|
|
||||||
? new Key(getProfileHeaderWithProfileId(cs.getProfile()))
|
|
||||||
: key;
|
|
||||||
|
|
||||||
cache.put(key, cs);
|
cache.put(key, cs);
|
||||||
|
|
||||||
|
// On LCMS, validation *alters* the profile header, need to re-generate key
|
||||||
|
if (profileCleaner.validationAltersProfileHeader()) {
|
||||||
|
cache.put(new Key(getProfileHeaderWithProfileId(cs.getProfile())), cs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cs;
|
return cs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ICC_ColorSpace getCachedCS(Key profileKey) {
|
||||||
|
synchronized (cache) {
|
||||||
|
return cache.get(profileKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ICC_ColorSpace getCachedCS(final byte[] profileHeader) {
|
||||||
|
return getCachedCS(new Key(profileHeader));
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateColorSpace(final ICC_ColorSpace cs) {
|
private static void validateColorSpace(final ICC_ColorSpace cs) {
|
||||||
// Validate the color space, to avoid caching bad profiles/color spaces
|
// Validate the color space, to avoid caching bad profiles/color spaces
|
||||||
// Will throw IllegalArgumentException or CMMException if the profile is bad
|
// Will throw IllegalArgumentException or CMMException if the profile is bad
|
||||||
@ -318,6 +325,137 @@ public final class ColorSpaces {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ICC_Profile readProfileRaw(final InputStream input) throws IOException {
|
||||||
|
return ICC_Profile.getInstance(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ICC_Profile readProfile(final InputStream input) throws IOException {
|
||||||
|
// TODO: Implement this smarter?
|
||||||
|
// Could read the header 128 bytes, get size + magic, then read read rest into array and feed the byte[] method...
|
||||||
|
ICC_Profile profile = ICC_Profile.getInstance(input);
|
||||||
|
|
||||||
|
if (profile == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid ICC Profile Data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return createProfile(profile.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ICC_Profile createProfileRaw(final byte[] input) {
|
||||||
|
try {
|
||||||
|
return readProfileRaw(new ByteArrayInputStream(input));
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Invalid ICC Profile Data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ICC_Profile createProfile(final byte[] input) {
|
||||||
|
Validate.notNull(input, "input");
|
||||||
|
|
||||||
|
if (input.length < ICC_PROFILE_HEADER_SIZE) { // Can't be less than size of ICC header
|
||||||
|
throw new IllegalArgumentException("Truncated ICC profile, length < 128: " + input.length);
|
||||||
|
}
|
||||||
|
int size = intBigEndian(input, 0);
|
||||||
|
if (size < 0 || size > input.length) {
|
||||||
|
throw new IllegalArgumentException("Truncated ICC profile, length < " + size + ": " + input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input[36] != 'a' || input[37] != 'c' || input[38] != 's' || input[39] != 'p') {
|
||||||
|
throw new IllegalArgumentException("Not an ICC profile, missing file signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up in cache before returning, these are already validated
|
||||||
|
byte[] profileHeader = getProfileHeaderWithProfileId(input);
|
||||||
|
int csType = getCsType(profileHeader);
|
||||||
|
|
||||||
|
ICC_ColorSpace internal = getInternalCS(csType, profileHeader);
|
||||||
|
if (internal != null) {
|
||||||
|
return internal.getProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
ICC_ColorSpace cached = getCachedCS(profileHeader);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached.getProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEIRDNESS: Unlike the InputStream version, the byte version
|
||||||
|
// of ICC_Profile.getInstance() does not discard extra bytes at the end.
|
||||||
|
// We'll chop them off here for convenience
|
||||||
|
byte[] profileBytes = input.length == size ? input : Arrays.copyOf(input, size);
|
||||||
|
ICC_Profile profile = ICC_Profile.getInstance(profileBytes);
|
||||||
|
|
||||||
|
// We'll validate & cache by creating a color space and returning its profile...
|
||||||
|
// TODO: Rewrite with separate cache for profiles...
|
||||||
|
return createColorSpace(profile).getProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intBigEndian(byte[] data, int index) {
|
||||||
|
return (data[index] & 0xff) << 24 | (data[index + 1] & 0xff) << 16 | (data[index + 2] & 0xff) << 8 | (data[index + 3] & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getCsType(byte[] profileHeader) {
|
||||||
|
int csSig = intBigEndian(profileHeader, ICC_Profile.icHdrColorSpace);
|
||||||
|
|
||||||
|
// TODO: Wonder why they didn't just use the sig as type, when there is obviously a 1:1 mapping...
|
||||||
|
|
||||||
|
switch (csSig) {
|
||||||
|
case ICC_Profile.icSigXYZData:
|
||||||
|
return ColorSpace.TYPE_XYZ;
|
||||||
|
case ICC_Profile.icSigLabData:
|
||||||
|
return ColorSpace.TYPE_Lab;
|
||||||
|
case ICC_Profile.icSigLuvData:
|
||||||
|
return ColorSpace.TYPE_Luv;
|
||||||
|
case ICC_Profile.icSigYCbCrData:
|
||||||
|
return ColorSpace.TYPE_YCbCr;
|
||||||
|
case ICC_Profile.icSigYxyData:
|
||||||
|
return ColorSpace.TYPE_Yxy;
|
||||||
|
case ICC_Profile.icSigRgbData:
|
||||||
|
return ColorSpace.TYPE_RGB;
|
||||||
|
case ICC_Profile.icSigGrayData:
|
||||||
|
return ColorSpace.TYPE_GRAY;
|
||||||
|
case ICC_Profile.icSigHsvData:
|
||||||
|
return ColorSpace.TYPE_HSV;
|
||||||
|
case ICC_Profile.icSigHlsData:
|
||||||
|
return ColorSpace.TYPE_HLS;
|
||||||
|
case ICC_Profile.icSigCmykData:
|
||||||
|
return ColorSpace.TYPE_CMYK;
|
||||||
|
// Note: There is no TYPE_* 10...
|
||||||
|
case ICC_Profile.icSigCmyData:
|
||||||
|
return ColorSpace.TYPE_CMY;
|
||||||
|
case ICC_Profile.icSigSpace2CLR:
|
||||||
|
return ColorSpace.TYPE_2CLR;
|
||||||
|
case ICC_Profile.icSigSpace3CLR:
|
||||||
|
return ColorSpace.TYPE_3CLR;
|
||||||
|
case ICC_Profile.icSigSpace4CLR:
|
||||||
|
return ColorSpace.TYPE_4CLR;
|
||||||
|
case ICC_Profile.icSigSpace5CLR:
|
||||||
|
return ColorSpace.TYPE_5CLR;
|
||||||
|
case ICC_Profile.icSigSpace6CLR:
|
||||||
|
return ColorSpace.TYPE_6CLR;
|
||||||
|
case ICC_Profile.icSigSpace7CLR:
|
||||||
|
return ColorSpace.TYPE_7CLR;
|
||||||
|
case ICC_Profile.icSigSpace8CLR:
|
||||||
|
return ColorSpace.TYPE_8CLR;
|
||||||
|
case ICC_Profile.icSigSpace9CLR:
|
||||||
|
return ColorSpace.TYPE_9CLR;
|
||||||
|
case ICC_Profile.icSigSpaceACLR:
|
||||||
|
return ColorSpace.TYPE_ACLR;
|
||||||
|
case ICC_Profile.icSigSpaceBCLR:
|
||||||
|
return ColorSpace.TYPE_BCLR;
|
||||||
|
case ICC_Profile.icSigSpaceCCLR:
|
||||||
|
return ColorSpace.TYPE_CCLR;
|
||||||
|
case ICC_Profile.icSigSpaceDCLR:
|
||||||
|
return ColorSpace.TYPE_DCLR;
|
||||||
|
case ICC_Profile.icSigSpaceECLR:
|
||||||
|
return ColorSpace.TYPE_ECLR;
|
||||||
|
case ICC_Profile.icSigSpaceFCLR:
|
||||||
|
return ColorSpace.TYPE_FCLR;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid ICC color space signature: " + csSig); // TODO: fourCC?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the color space specified by the given color space constant.
|
* Returns the color space specified by the given color space constant.
|
||||||
* <p>
|
* <p>
|
||||||
@ -465,7 +603,7 @@ public final class ColorSpaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache header profile data to avoid excessive array creation/copying in static inner class for on-demand lazy init
|
// Cache header profile data to avoid excessive array creation/copying. Use static inner class for on-demand lazy init
|
||||||
private static class sRGB {
|
private static class sRGB {
|
||||||
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_sRGB));
|
private static final byte[] header = getProfileHeaderWithProfileId(ICC_Profile.getInstance(ColorSpace.CS_sRGB));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.twelvemonkeys.imageio.color;
|
||||||
|
|
||||||
|
import javax.imageio.spi.ImageInputStreamSpi;
|
||||||
|
import javax.imageio.spi.ServiceRegistry;
|
||||||
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static com.twelvemonkeys.imageio.util.IIOUtil.deregisterProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class exists to force early invocation of {@code ProfileDeferralMgr.activateProfiles()},
|
||||||
|
* in an attempt to avoid JDK-6986863 and related bugs in Java < 17.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-6986863">JDK-6986863</a>
|
||||||
|
*/
|
||||||
|
final class ProfileDeferralActivator {
|
||||||
|
|
||||||
|
static {
|
||||||
|
activateProfilesInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void activateProfilesInternal() {
|
||||||
|
try {
|
||||||
|
// Force invocation of ProfileDeferralMgr.activateProfiles() to avoid JDK-6986863 and friends.
|
||||||
|
// Relies on static initializer in ColorConvertOp to actually invoke ProfileDeferralMgr.activateProfiles()
|
||||||
|
Class.forName("java.awt.image.ColorConvertOp");
|
||||||
|
}
|
||||||
|
catch (Throwable disasters) {
|
||||||
|
System.err.println("ProfileDeferralMgr.activateProfiles() failed. ICC Color Profiles may not work properly, see stack trace below.");
|
||||||
|
System.err.println("For more information, see https://bugs.openjdk.java.net/browse/JDK-6986863");
|
||||||
|
System.err.println("Please upgrade to Java 17 or later where this bug is fixed, or ask your JRE provider to backport the fix.");
|
||||||
|
System.err.println();
|
||||||
|
System.err.println("If you can't update to Java 17, a possible workaround is to add");
|
||||||
|
System.err.println("\tClass.forName(\"java.awt.image.ColorConvertOp\");");
|
||||||
|
System.err.println("*early* in your application startup code, to force profile activation before profiles are accessed.");
|
||||||
|
System.err.println();
|
||||||
|
|
||||||
|
disasters.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void activateProfiles() {
|
||||||
|
// This method exists for other classes in the package to
|
||||||
|
// ensure this class' static initializer is run.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is not a service provider, but exploits the SPI mechanism as a hook to force early profile activation.
|
||||||
|
*/
|
||||||
|
public static final class Spi extends ImageInputStreamSpi {
|
||||||
|
@Override public void onRegistration(ServiceRegistry registry, Class<?> category) {
|
||||||
|
activateProfiles();
|
||||||
|
|
||||||
|
deregisterProvider(registry, this, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getDescription(Locale locale) {
|
||||||
|
return getClass().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ImageInputStream createInputStreamInstance(Object input, boolean useCache, File cacheDir) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -158,19 +158,18 @@ public final class IIOUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* THIS METHOD WILL ME MOVED/RENAMED, DO NOT USE.
|
* THIS METHOD WILL BE MOVED/RENAMED, DO NOT USE.
|
||||||
*
|
*
|
||||||
* @param registry the registry to unregister from.
|
* @param registry the registry to unregister from.
|
||||||
* @param provider the provider to unregister.
|
* @param provider the provider to unregister.
|
||||||
* @param category the category to unregister from.
|
* @param category the category to unregister from.
|
||||||
*/
|
*/
|
||||||
public static <T> void deregisterProvider(final ServiceRegistry registry, final IIOServiceProvider provider, final Class<T> category) {
|
public static <T> void deregisterProvider(final ServiceRegistry registry, final IIOServiceProvider provider, final Class<T> category) {
|
||||||
// http://www.ibm.com/developerworks/java/library/j-jtp04298.html
|
|
||||||
registry.deregisterServiceProvider(category.cast(provider), category);
|
registry.deregisterServiceProvider(category.cast(provider), category);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* THIS METHOD WILL ME MOVED/RENAMED, DO NOT USE.
|
* THIS METHOD WILL BE MOVED/RENAMED, DO NOT USE.
|
||||||
*
|
*
|
||||||
* @param registry the registry to lookup from.
|
* @param registry the registry to lookup from.
|
||||||
* @param providerClassName name of the provider class.
|
* @param providerClassName name of the provider class.
|
||||||
|
@ -35,6 +35,7 @@ import org.junit.Test;
|
|||||||
import java.awt.color.ColorSpace;
|
import java.awt.color.ColorSpace;
|
||||||
import java.awt.color.ICC_ColorSpace;
|
import java.awt.color.ICC_ColorSpace;
|
||||||
import java.awt.color.ICC_Profile;
|
import java.awt.color.ICC_Profile;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
@ -204,7 +205,7 @@ public class ColorSpacesTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEqualHeadersDifferentProfile() throws IOException {
|
public void testEqualHeadersDifferentProfile() throws IOException {
|
||||||
// These profiles are extracted from various JPEGs, and have the exact same profile header...
|
// These profiles are extracted from various JPEGs, and have the exact same profile header (but are different)...
|
||||||
ICC_Profile profile1 = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
ICC_Profile profile1 = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
ICC_Profile profile2 = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/color_match_rgb.icc"));
|
ICC_Profile profile2 = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/color_match_rgb.icc"));
|
||||||
|
|
||||||
@ -215,4 +216,69 @@ public class ColorSpacesTest {
|
|||||||
|
|
||||||
assertNotSame(cs1, cs2);
|
assertNotSame(cs1, cs2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileBytesSame() throws IOException {
|
||||||
|
ICC_Profile profile = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
|
ICC_Profile profile1 = ColorSpaces.createProfile(profile.getData());
|
||||||
|
ICC_Profile profile2 = ColorSpaces.createProfile(profile.getData());
|
||||||
|
|
||||||
|
assertEquals(profile1, profile2);
|
||||||
|
assertSame(profile1, profile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileInputStreamSame() throws IOException {
|
||||||
|
ICC_Profile profile1 = ColorSpaces.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
|
ICC_Profile profile2 = ColorSpaces.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
|
|
||||||
|
assertEquals(profile1, profile2);
|
||||||
|
assertSame(profile1, profile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileDifferent() throws IOException {
|
||||||
|
// These profiles are extracted from various JPEGs, and have the exact same profile header (but are different profiles)...
|
||||||
|
ICC_Profile profile1 = ColorSpaces.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
|
ICC_Profile profile2 = ColorSpaces.readProfile(getClass().getResourceAsStream("/profiles/color_match_rgb.icc"));
|
||||||
|
|
||||||
|
assertNotSame(profile1, profile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileBytesSameAsCached() throws IOException {
|
||||||
|
ICC_Profile profile = ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
|
ICC_ColorSpace cs1 = ColorSpaces.createColorSpace(profile);
|
||||||
|
ICC_Profile profile2 = ColorSpaces.createProfile(profile.getData());
|
||||||
|
|
||||||
|
assertEquals(cs1.getProfile(), profile2);
|
||||||
|
assertSame(cs1.getProfile(), profile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileInputStreamSameAsCached() throws IOException {
|
||||||
|
ICC_ColorSpace cs1 = ColorSpaces.createColorSpace(ICC_Profile.getInstance(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc")));
|
||||||
|
ICC_Profile profile2 = ColorSpaces.readProfile(getClass().getResourceAsStream("/profiles/adobe_rgb_1998.icc"));
|
||||||
|
|
||||||
|
assertEquals(cs1.getProfile(), profile2);
|
||||||
|
assertSame(cs1.getProfile(), profile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileBytesSameAsInternal() {
|
||||||
|
ICC_Profile profile1 = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
|
||||||
|
ICC_Profile profile2 = ColorSpaces.createProfile(ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData());
|
||||||
|
|
||||||
|
assertEquals(profile1, profile2);
|
||||||
|
assertSame(profile1, profile2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadProfileInputStreamSameAsInternal() throws IOException {
|
||||||
|
ICC_Profile profile1 = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
|
||||||
|
ICC_Profile profile2 = ColorSpaces.readProfile(new ByteArrayInputStream(ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData()));
|
||||||
|
|
||||||
|
assertEquals(profile1, profile2);
|
||||||
|
assertSame(profile1, profile2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -614,7 +614,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) {
|
private ICC_Profile ensureDisplayProfile(final ICC_Profile profile) throws IOException {
|
||||||
// NOTE: This is probably not the right way to do it... :-P
|
// NOTE: This is probably not the right way to do it... :-P
|
||||||
// TODO: Consider moving method to ColorSpaces class or new class in imageio.color package
|
// TODO: Consider moving method to ColorSpaces class or new class in imageio.color package
|
||||||
|
|
||||||
@ -630,7 +630,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first
|
intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first
|
||||||
|
|
||||||
return ICC_Profile.getInstance(profileData);
|
return ColorSpaces.createProfile(profileData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -954,7 +954,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
int iccChunkDataSize = segment.data.length - segmentDataStart;
|
int iccChunkDataSize = segment.data.length - segmentDataStart;
|
||||||
int iccSize = segment.data.length < segmentDataStart + 4 ? 0 : intFromBigEndian(segment.data, segmentDataStart);
|
int iccSize = segment.data.length < segmentDataStart + 4 ? 0 : intFromBigEndian(segment.data, segmentDataStart);
|
||||||
|
|
||||||
return readICCProfileSafe(stream, allowBadIndexes, iccSize, iccChunkDataSize);
|
return readICCProfileSafe(stream, allowBadIndexes);
|
||||||
}
|
}
|
||||||
else if (!segments.isEmpty()) {
|
else if (!segments.isEmpty()) {
|
||||||
// NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order...
|
// NOTE: This is probably over-complicated, as I've never encountered ICC_PROFILE chunks out of order...
|
||||||
@ -1012,25 +1012,20 @@ public final class JPEGImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))), allowBadIndexes, iccSize, iccChunkDataSize);
|
return readICCProfileSafe(new SequenceInputStream(Collections.enumeration(Arrays.asList(streams))), allowBadIndexes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ICC_Profile readICCProfileSafe(final InputStream stream, final boolean allowBadProfile, final int iccSize, final int iccChunkDataSize) throws IOException {
|
private ICC_Profile readICCProfileSafe(final InputStream stream, final boolean allowBadProfile) {
|
||||||
if (iccSize < 0 || iccSize > iccChunkDataSize) {
|
|
||||||
processWarningOccurred(String.format("Truncated 'ICC_PROFILE' chunk(s), size: %d. Ignoring ICC profile.", iccSize));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ICC_Profile profile = ICC_Profile.getInstance(stream);
|
ICC_Profile profile = ColorSpaces.readProfileRaw(stream);
|
||||||
|
|
||||||
// NOTE: Need to ensure we have a display profile *before* validating, for the caching to work
|
// NOTE: Need to ensure we have a display profile *before* validating, for the caching to work
|
||||||
return allowBadProfile ? profile : ColorSpaces.validateProfile(ensureDisplayProfile(profile));
|
return allowBadProfile ? profile : ColorSpaces.validateProfile(ensureDisplayProfile(profile));
|
||||||
}
|
}
|
||||||
catch (RuntimeException e) {
|
catch (IOException | RuntimeException e) {
|
||||||
// NOTE: Throws either IllegalArgumentException or CMMException, depending on platform.
|
// NOTE: Throws either IllegalArgumentException or CMMException, depending on platform.
|
||||||
// Usual reason: Broken tools store truncated ICC profiles in a single ICC_PROFILE chunk...
|
// Usual reason: Broken tools store truncated ICC profiles in a single ICC_PROFILE chunk...
|
||||||
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk(s): %s. Ignoring ICC profile.", e.getMessage()));
|
processWarningOccurred(String.format("Bad 'ICC_PROFILE' chunk(s): %s. Ignoring ICC profile.", e.getMessage()));
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.metadata.jpeg;
|
package com.twelvemonkeys.imageio.metadata.jpeg;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||||
import com.twelvemonkeys.imageio.metadata.psd.PSD;
|
import com.twelvemonkeys.imageio.metadata.psd.PSD;
|
||||||
@ -42,7 +43,6 @@ import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
|||||||
import javax.imageio.IIOException;
|
import javax.imageio.IIOException;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.color.ICC_ColorSpace;
|
|
||||||
import java.awt.color.ICC_Profile;
|
import java.awt.color.ICC_Profile;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
@ -353,8 +353,8 @@ public final class JPEGSegmentUtil {
|
|||||||
Directory psd = new PSDReader().read(stream);
|
Directory psd = new PSDReader().read(stream);
|
||||||
Entry iccEntry = psd.getEntryById(PSD.RES_ICC_PROFILE);
|
Entry iccEntry = psd.getEntryById(PSD.RES_ICC_PROFILE);
|
||||||
if (iccEntry != null) {
|
if (iccEntry != null) {
|
||||||
ICC_ColorSpace colorSpace = new ICC_ColorSpace(ICC_Profile.getInstance((byte[]) iccEntry.getValue()));
|
ICC_Profile profile = ColorSpaces.createProfileRaw((byte[]) iccEntry.getValue());
|
||||||
System.err.println("colorSpace: " + colorSpace);
|
System.err.println("ICC Profile: " + profile);
|
||||||
}
|
}
|
||||||
System.err.println("PSD: " + psd);
|
System.err.println("PSD: " + psd);
|
||||||
System.err.println(TIFFReader.HexDump.dump(segment.data));
|
System.err.println(TIFFReader.HexDump.dump(segment.data));
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.psd;
|
package com.twelvemonkeys.imageio.plugins.psd;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||||
import com.twelvemonkeys.imageio.util.IIOUtil;
|
import com.twelvemonkeys.imageio.util.IIOUtil;
|
||||||
|
|
||||||
import javax.imageio.stream.ImageInputStream;
|
import javax.imageio.stream.ImageInputStream;
|
||||||
@ -55,7 +56,7 @@ final class ICCProfile extends PSDImageResource {
|
|||||||
@Override
|
@Override
|
||||||
protected void readData(final ImageInputStream pInput) throws IOException {
|
protected void readData(final ImageInputStream pInput) throws IOException {
|
||||||
try (InputStream stream = IIOUtil.createStreamAdapter(pInput, size)) {
|
try (InputStream stream = IIOUtil.createStreamAdapter(pInput, size)) {
|
||||||
profile = ICC_Profile.getInstance(stream);
|
profile = ColorSpaces.readProfile(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,45 +30,6 @@
|
|||||||
|
|
||||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||||
|
|
||||||
import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter;
|
|
||||||
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.color.CMMException;
|
|
||||||
import java.awt.color.ColorSpace;
|
|
||||||
import java.awt.color.ICC_Profile;
|
|
||||||
import java.awt.image.*;
|
|
||||||
import java.io.*;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.zip.Inflater;
|
|
||||||
import java.util.zip.InflaterInputStream;
|
|
||||||
|
|
||||||
import javax.imageio.IIOException;
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import javax.imageio.ImageReadParam;
|
|
||||||
import javax.imageio.ImageReader;
|
|
||||||
import javax.imageio.ImageTypeSpecifier;
|
|
||||||
import javax.imageio.event.IIOReadWarningListener;
|
|
||||||
import javax.imageio.metadata.IIOMetadata;
|
|
||||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
|
||||||
import javax.imageio.metadata.IIOMetadataNode;
|
|
||||||
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
|
||||||
import javax.imageio.spi.ImageReaderSpi;
|
|
||||||
import javax.imageio.stream.ImageInputStream;
|
|
||||||
|
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
|
|
||||||
import com.twelvemonkeys.imageio.ImageReaderBase;
|
import com.twelvemonkeys.imageio.ImageReaderBase;
|
||||||
import com.twelvemonkeys.imageio.color.CIELabColorConverter;
|
import com.twelvemonkeys.imageio.color.CIELabColorConverter;
|
||||||
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
|
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
|
||||||
@ -99,6 +60,35 @@ import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
|||||||
import com.twelvemonkeys.lang.StringUtil;
|
import com.twelvemonkeys.lang.StringUtil;
|
||||||
import com.twelvemonkeys.xml.XMLSerializer;
|
import com.twelvemonkeys.xml.XMLSerializer;
|
||||||
|
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.imageio.*;
|
||||||
|
import javax.imageio.event.IIOReadWarningListener;
|
||||||
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
|
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||||
|
import javax.imageio.metadata.IIOMetadataNode;
|
||||||
|
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
||||||
|
import javax.imageio.spi.ImageReaderSpi;
|
||||||
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.color.CMMException;
|
||||||
|
import java.awt.color.ColorSpace;
|
||||||
|
import java.awt.color.ICC_Profile;
|
||||||
|
import java.awt.image.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.zip.Inflater;
|
||||||
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
|
import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter;
|
||||||
|
import static com.twelvemonkeys.imageio.util.IIOUtil.subsampleRow;
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageReader implementation for Aldus/Adobe Tagged Image File Format (TIFF).
|
* ImageReader implementation for Aldus/Adobe Tagged Image File Format (TIFF).
|
||||||
* <p>
|
* <p>
|
||||||
@ -2507,20 +2497,8 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE);
|
Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE);
|
||||||
|
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
byte[] value = (byte[]) entry.getValue();
|
|
||||||
|
|
||||||
// Validate ICC profile size vs actual value size
|
|
||||||
int size = (value[0] & 0xff) << 24 | (value[1] & 0xff) << 16 | (value[2] & 0xff) << 8 | (value[3] & 0xff);
|
|
||||||
if (size < 0 || size > value.length) {
|
|
||||||
processWarningOccurred("Ignoring truncated ICC profile: Bad ICC profile size (" + size + ")");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// WEIRDNESS: Reading profile from InputStream is somehow more compatible
|
return ColorSpaces.createProfile((byte[]) entry.getValue());
|
||||||
// than reading from byte array (chops off extra bytes + validates profile).
|
|
||||||
ICC_Profile profile = ICC_Profile.getInstance(new ByteArrayInputStream(value));
|
|
||||||
return ColorSpaces.validateProfile(profile);
|
|
||||||
}
|
}
|
||||||
catch (CMMException | IllegalArgumentException e) {
|
catch (CMMException | IllegalArgumentException e) {
|
||||||
processWarningOccurred("Ignoring broken/incompatible ICC profile: " + e.getMessage());
|
processWarningOccurred("Ignoring broken/incompatible ICC profile: " + e.getMessage());
|
||||||
|
@ -287,7 +287,7 @@ final class WebPImageReader extends ImageReaderBase {
|
|||||||
long chunkStart = imageInput.getStreamPosition();
|
long chunkStart = imageInput.getStreamPosition();
|
||||||
|
|
||||||
if (nextChunk == WebP.CHUNK_ICCP) {
|
if (nextChunk == WebP.CHUNK_ICCP) {
|
||||||
iccProfile = ICC_Profile.getInstance(IIOUtil.createStreamAdapter(imageInput, chunkLength));
|
iccProfile = ColorSpaces.readProfile(IIOUtil.createStreamAdapter(imageInput, chunkLength));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
processWarningOccurred(String.format("Expected 'ICCP' chunk, '%s' chunk encountered", fourCC(nextChunk)));
|
processWarningOccurred(String.format("Expected 'ICCP' chunk, '%s' chunk encountered", fourCC(nextChunk)));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user