ColorSpaces.java

/*
 * Copyright (c) 2011, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.imageio.color;

import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.util.LRUHashMap;

import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Map;

import static com.twelvemonkeys.imageio.color.ColorProfiles.*;

/**
 * A helper class for working with ICC color profiles and color spaces.
 * <p>
 * Standard ICC color profiles are read from system-specific locations
 * for known operating systems.
 * </p>
 * <p>
 * Color profiles may be configured by placing a property-file
 * {@code com/twelvemonkeys/imageio/color/icc_profiles.properties}
 * on the classpath, specifying the full path to the profiles.
 * ICC color profiles are probably already present on your system, or
 * can be downloaded from
 * <a href="http://www.color.org/profiles2.xalter">ICC</a>,
 * <a href="http://www.adobe.com/downloads/">Adobe</a> or other places.
 *  * </p>
 * <p>
 * Example property file:
 * </p>
 * <pre>
 * # icc_profiles.properties
 * ADOBE_RGB_1998=/path/to/Adobe RGB 1998.icc
 * GENERIC_CMYK=/path/to/Generic CMYK.icc
 * </pre>
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haraldk$
 * @version $Id: ColorSpaces.java,v 1.0 24.01.11 17.51 haraldk Exp$
 */
public final class ColorSpaces {
    // TODO: Consider creating our own ICC profile class, which just wraps the byte array,
    // for easier access and manipulation until creating a "real" ICC_Profile/ColorSpace.
    // This will also let us work around the issues in the LCMS implementation.

    final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.color.debug"));

    // NOTE: java.awt.color.ColorSpace.CS_* uses 1000-1004, we'll use 5000+ to not interfere with future additions

    /** The Adobe RGB 1998 (or compatible) color space. Either read from disk or built-in. */
    @SuppressWarnings("WeakerAccess")
    public static final int CS_ADOBE_RGB_1998 = 5000;

    /** A best-effort "generic" CMYK color space. Either read from disk or built-in. */
    @SuppressWarnings("WeakerAccess")
    public static final int CS_GENERIC_CMYK = 5001;

    // TODO: Move to ColorProfiles OR cache ICC_ColorSpace instead?
    // Weak references to hold the color spaces while cached
    private static WeakReference<ICC_Profile> adobeRGB1998 = new WeakReference<>(null);
    private static WeakReference<ICC_Profile> genericCMYK = new WeakReference<>(null);

    // Cache for the latest used color spaces
    private static final Map<Key, ICC_ColorSpace> cache = new LRUHashMap<>(16);

    static {
        // In case we didn't activate through SPI already
        ProfileDeferralActivator.activateProfiles();
    }

    private ColorSpaces() {}

    /**
     * Creates an ICC color space from the given ICC color profile.
     * <p>
     * For standard Java color spaces, the built-in instance is returned.
     * Otherwise, color spaces are looked up from cache and created on demand.
     * </p>
     *
     * @param profile the ICC color profile. May not be {@code null}.
     * @return an ICC color space
     * @throws IllegalArgumentException if {@code profile} is {@code null}.
     * @throws java.awt.color.CMMException if {@code profile} is invalid.
     */
    public static ICC_ColorSpace createColorSpace(final ICC_Profile profile) {
        Validate.notNull(profile, "profile");

        // Fix profile before lookup/create
        fixProfile(profile);

        byte[] profileHeader = getProfileHeaderWithProfileId(profile);

        ICC_ColorSpace cs = getInternalCS(profile.getColorSpaceType(), profileHeader);
        if (cs != null) {
            return cs;
        }

        return getCachedOrCreateCS(profile, profileHeader);
    }

    static ICC_ColorSpace getInternalCS(final int profileCSType, final byte[] profileHeader) {
        if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, ColorProfiles.sRGB.header)) {
            return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_sRGB);
        }
        else if (profileCSType == ColorSpace.TYPE_GRAY && Arrays.equals(profileHeader, ColorProfiles.GRAY.header)) {
            return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_GRAY);
        }
        else if (profileCSType == ColorSpace.TYPE_3CLR && Arrays.equals(profileHeader, ColorProfiles.PYCC.header)) {
            return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_PYCC);
        }
        else if (profileCSType == ColorSpace.TYPE_RGB && Arrays.equals(profileHeader, ColorProfiles.LINEAR_RGB.header)) {
            return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
        }
        else if (profileCSType == ColorSpace.TYPE_XYZ && Arrays.equals(profileHeader, ColorProfiles.CIEXYZ.header)) {
            return (ICC_ColorSpace) ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
        }

        return null;
    }

    private static ICC_ColorSpace getCachedOrCreateCS(final ICC_Profile profile, final byte[] profileHeader) {
        Key key = new Key(profileHeader);

        synchronized (cache) {
            ICC_ColorSpace cs = getCachedCS(key);

            if (cs == null) {
                cs = new ICC_ColorSpace(profile);

                validateColorSpace(cs);

                cache.put(key, cs);

                // On LCMS, validation *alters* the profile header, need to re-generate key
                if (ColorProfiles.validationAltersProfileHeader()) {
                    cache.put(new Key(getProfileHeaderWithProfileId(cs.getProfile())), cs);
                }
            }

            return cs;
        }
    }

    private static ICC_ColorSpace getCachedCS(Key profileKey) {
        synchronized (cache) {
            return cache.get(profileKey);
        }
    }

    static ICC_ColorSpace getCachedCS(final byte[] profileHeader) {
        return getCachedCS(new Key(profileHeader));
    }

    static void validateColorSpace(final ICC_ColorSpace cs) {
        // Validate the color space, to avoid caching bad profiles/color spaces
        // Will throw IllegalArgumentException or CMMException if the profile is bad
        cs.fromRGB(new float[] {0.999f, 0.5f, 0.001f});

        // This breaks *sometimes* after validation of bad profiles,
        // we'll let it blow up early in this case
        cs.getProfile().getData();
    }

    /**
     * @deprecated Use {@link ColorProfiles#isCS_sRGB(ICC_Profile)} instead.
     */
    @Deprecated
    public static boolean isCS_sRGB(final ICC_Profile profile) {
        return ColorProfiles.isCS_sRGB(profile);
    }

    /**
     * @deprecated Use {@link ColorProfiles#isCS_GRAY(ICC_Profile)} instead.
     */
    @Deprecated
    public static boolean isCS_GRAY(final ICC_Profile profile) {
        return ColorProfiles.isCS_GRAY(profile);
    }

    /**
     * @deprecated Use {@link ColorProfiles#validateProfile(ICC_Profile)} instead.
     */
    @Deprecated
    public static ICC_Profile validateProfile(final ICC_Profile profile) {
        return ColorProfiles.validateProfile(profile);
    }

    /**
     * Returns the color space specified by the given color space constant.
     * <p>
     * For standard Java color spaces, the built-in instance is returned.
     * Otherwise, color spaces are looked up from cache and created on demand.
     * </p>
     *
     * @param colorSpace the color space constant.
     * @return the {@link ColorSpace} specified by the color space constant.
     * @throws IllegalArgumentException if {@code colorSpace} is not one of the defined color spaces ({@code CS_*}).
     * @see ColorSpace
     * @see ColorSpaces#CS_ADOBE_RGB_1998
     * @see ColorSpaces#CS_GENERIC_CMYK
     */
    public static ColorSpace getColorSpace(int colorSpace) {
        ICC_Profile profile;

        switch (colorSpace) {
            case CS_ADOBE_RGB_1998:
                synchronized (ColorSpaces.class) {
                    profile = adobeRGB1998.get();

                    if (profile == null) {
                        // Try to get system default or user-defined profile
                        profile = readProfileFromPath(Profiles.getPath("ADOBE_RGB_1998"));

                        if (profile == null) {
                            // Fall back to the bundled ClayRGB1998 public domain Adobe RGB 1998 compatible profile,
                            // which is identical for all practical purposes
                            profile = readProfileFromClasspathResource("/profiles/ClayRGB1998.icc");

                            if (profile == null) {
                                // Should never happen given we now bundle fallback profile...
                                throw new IllegalStateException("Could not read AdobeRGB1998 profile");
                            }
                        }

                        if (profile.getColorSpaceType() != ColorSpace.TYPE_RGB) {
                            throw new IllegalStateException("Configured AdobeRGB1998 profile is not TYPE_RGB");
                        }

                        adobeRGB1998 = new WeakReference<>(profile);
                    }
                }

                return createColorSpace(profile);

            case CS_GENERIC_CMYK:
                synchronized (ColorSpaces.class) {
                    profile = genericCMYK.get();

                    if (profile == null) {
                        // Try to get system default or user-defined profile
                        profile = readProfileFromPath(Profiles.getPath("GENERIC_CMYK"));

                        if (profile == null) {
                            if (DEBUG) {
                                System.out.println("Using fallback profile");
                            }

                            // Fall back to generic CMYK ColorSpace, which is *insanely slow* using ColorConvertOp... :-P
                            return CMYKColorSpace.getInstance();
                        }

                        if (profile.getColorSpaceType() != ColorSpace.TYPE_CMYK) {
                            throw new IllegalStateException("Configured Generic CMYK profile is not TYPE_CMYK");
                        }

                        genericCMYK = new WeakReference<>(profile);
                    }
                }

                return createColorSpace(profile);

            default:
                // Default cases for convenience
                return ColorSpace.getInstance(colorSpace);
        }
    }

    private static final class Key {
        private final byte[] data;

        Key(byte[] data) {
            this.data = data;
        }

        @Override
        public boolean equals(Object other) {
            return other instanceof Key && Arrays.equals(data, ((Key) other).data);
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(data);
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
        }
    }
}