Skip to content

Commit

Permalink
Support reading ICC color profiles
Browse files Browse the repository at this point in the history
BufferedImage uses an sRGB ColorModel by default.  If a slide pyramid or
associated image has an ICC profile, attach a ColorModel containing that
profile to all BufferedImages produced from that image.

Callers of paintRegionARGB() don't receive a BufferedImage.  For those
users, add OpenSlide.getColorModel() so the profile can be read
separately.

Graphics2D's documentation suggests that it handles color management, but
its drawImage() method apparently does not.  If we therefore manually
invoke a ColorConvertOp in paintRegion(), the DICOM/Leica-4 test slide
throws an exception:

    Exception in thread "AWT-EventQueue-0" java.awt.color.CMMException: LCMS error 13: Couldn't link the profiles
        at java.desktop/sun.java2d.cmm.lcms.LCMS.createNativeTransform(Native Method)
        at java.desktop/sun.java2d.cmm.lcms.LCMS.createTransform(LCMS.java:113)
        at java.desktop/sun.java2d.cmm.lcms.LCMSTransform.doTransform(LCMSTransform.java:114)
        at java.desktop/sun.java2d.cmm.lcms.LCMSTransform.colorConvert(LCMSTransform.java:149)
        at java.desktop/java.awt.image.ColorConvertOp.ICCBIFilter(ColorConvertOp.java:350)
        at java.desktop/java.awt.image.ColorConvertOp.filter(ColorConvertOp.java:277)
        at org.openslide.OpenSlide.paintRegion(OpenSlide.java:326)
        [...]

It seems that ColorConvertOp always uses perceptual rendering intent, not
the default intent encoded in the profile:

    https://bugs.openjdk.org/browse/JDK-8216369

Not all profiles support perceptual rendering, which may be the cause of
the exception.  If we're going to automatically perform color conversion,
we should do it predictably, not just when the Java CMS glue happens to do
the right thing.  For now, don't try to do color conversion, either in
paintRegion() or any of the GUI code.

Closes: #53
Signed-off-by: Benjamin Gilbert <[email protected]>
  • Loading branch information
bgilbert committed May 3, 2024
1 parent 7846fcb commit a9c59ac
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 4 deletions.
70 changes: 66 additions & 4 deletions org/openslide/OpenSlide.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@
package org.openslide;

import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
Expand Down Expand Up @@ -113,6 +121,8 @@ public void close() throws IOException {

final private Map<String, AssociatedImage> associatedImages;

final private ColorModel colorModel;

final private File canonicalFile;

final private int hashCodeVal;
Expand Down Expand Up @@ -178,6 +188,8 @@ public OpenSlide(File file) throws IOException {

associatedImages = Collections.unmodifiableMap(associated);

colorModel = readColorModel(null);

// store info for hash and equals
canonicalFile = file.getCanonicalFile();
String quickhash1 = getProperties().get(PROPERTY_NAME_QUICKHASH1);
Expand Down Expand Up @@ -224,6 +236,10 @@ public long getLevelHeight(int level) {
return levelHeights[level];
}

public ColorModel getColorModel() {
return colorModel;
}

public void paintRegionOfLevel(Graphics2D g, int dx, int dy, int sx,
int sy, int w, int h, int level) throws IOException {
paintRegion(g, dx, dy, sx, sy, w, h, levelDownsamples[level]);
Expand All @@ -248,7 +264,7 @@ public void paintRegionARGB(int dest[], long x, long y, int level, int w,

public BufferedImage readRegion(long x, long y, int level, int w, int h)
throws IOException {
BufferedImage img = createARGBBufferedImage(w, h);
BufferedImage img = createARGBBufferedImage(colorModel, w, h);
int data[] = getARGBPixels(img);
paintRegionARGB(data, x, y, level, w, h);
return img;
Expand Down Expand Up @@ -397,7 +413,9 @@ BufferedImage getAssociatedImage(String name) throws IOException {
throw new IOException("Failure reading associated image");
}

BufferedImage img = createARGBBufferedImage((int) dim[0], (int) dim[1]);
ColorModel cm = readColorModel(name);
BufferedImage img = createARGBBufferedImage(cm, (int) dim[0],
(int) dim[1]);
int data[] = getARGBPixels(img);

try (errorCtx) {
Expand Down Expand Up @@ -450,12 +468,56 @@ public boolean equals(Object obj) {
return false;
}

private static BufferedImage createARGBBufferedImage(int w, int h) {
return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
private static BufferedImage createARGBBufferedImage(ColorModel cm, int w,
int h) {
WritableRaster raster = Raster.createWritableRaster(
cm.createCompatibleSampleModel(w, h), null);
return new BufferedImage(cm, raster, true, null);
}

private static int[] getARGBPixels(BufferedImage img) {
DataBufferInt buf = (DataBufferInt) img.getRaster().getDataBuffer();
return buf.getData();
}

private ColorModel readColorModel(String associated) throws IOException {
ColorSpace space = readColorSpace(associated);
return new DirectColorModel(space, 32,
0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000, true,
DataBuffer.TYPE_INT);
}

private ColorSpace readColorSpace(String associated) throws IOException {
long size;
try (errorCtx) {
if (associated != null) {
size = OpenSlideFFM.openslide_get_associated_image_icc_profile_size(
errorCtx.getOsr(), associated);
} else {
size = OpenSlideFFM.openslide_get_icc_profile_size(
errorCtx.getOsr());
}
}
if (size <= 0) {
return ColorSpace.getInstance(ColorSpace.CS_sRGB);
} else if (size > Integer.MAX_VALUE) {
throw new IOException("ICC profile too large");
}

byte[] data = new byte[(int) size];
try (errorCtx) {
if (associated != null) {
OpenSlideFFM.openslide_read_associated_image_icc_profile(
errorCtx.getOsr(), associated, data);
} else {
OpenSlideFFM.openslide_read_icc_profile(errorCtx.getOsr(),
data);
}
}
try {
return new ICC_ColorSpace(ICC_Profile.getInstance(data));
} catch (IllegalArgumentException ex) {
throw new IOException("Invalid ICC profile", ex);
}
}
}
64 changes: 64 additions & 0 deletions org/openslide/OpenSlideFFM.java
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,32 @@ static void openslide_read_region(OpenSlideRef osr, int dest[],
}
}

private static final MethodHandle get_icc_profile_size = function(
JAVA_LONG, "openslide_get_icc_profile_size", C_POINTER);

static long openslide_get_icc_profile_size(OpenSlideRef osr) {
try (Ref.ScopedLock l = osr.lock()) {
return (long) get_icc_profile_size.invokeExact(osr.getSegment());
} catch (Throwable ex) {
throw wrapException(ex);
}
}

private static final MethodHandle read_icc_profile = function(
null, "openslide_read_icc_profile", C_POINTER, C_POINTER);

static void openslide_read_icc_profile(OpenSlideRef osr, byte dest[]) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment buf = arena.allocate(JAVA_BYTE, dest.length);
try (Ref.ScopedLock l = osr.lock()) {
read_icc_profile.invokeExact(osr.getSegment(), buf);
} catch (Throwable ex) {
throw wrapException(ex);
}
MemorySegment.copy(buf, JAVA_BYTE, 0, dest, 0, dest.length);
}
}

private static final MethodHandle get_error = function(
C_POINTER, "openslide_get_error", C_POINTER);

Expand Down Expand Up @@ -438,6 +464,44 @@ static void openslide_read_associated_image(OpenSlideRef osr, String name,
}
}

private static final MethodHandle get_associated_image_icc_profile_size = function(
JAVA_LONG, "openslide_get_associated_image_icc_profile_size",
C_POINTER, C_POINTER);

static long openslide_get_associated_image_icc_profile_size(
OpenSlideRef osr, String name) {
if (name == null) {
return -1;
}
try (Arena arena = Arena.ofConfined(); Ref.ScopedLock l = osr.lock()) {
return (long) get_associated_image_icc_profile_size.invokeExact(
osr.getSegment(), arena.allocateFrom(name));
} catch (Throwable ex) {
throw wrapException(ex);
}
}

private static final MethodHandle read_associated_image_icc_profile = function(
null, "openslide_read_associated_image_icc_profile", C_POINTER,
C_POINTER, C_POINTER);

static void openslide_read_associated_image_icc_profile(OpenSlideRef osr,
String name, byte dest[]) {
if (name == null) {
return;
}
try (Arena arena = Arena.ofConfined()) {
MemorySegment buf = arena.allocate(JAVA_BYTE, dest.length);
try (Ref.ScopedLock l = osr.lock()) {
read_associated_image_icc_profile.invokeExact(osr.getSegment(),
arena.allocateFrom(name), buf);
} catch (Throwable ex) {
throw wrapException(ex);
}
MemorySegment.copy(buf, JAVA_BYTE, 0, dest, 0, dest.length);
}
}

private static final MethodHandle cache_create = function(
C_POINTER, "openslide_cache_create", SIZE_T);

Expand Down

0 comments on commit a9c59ac

Please sign in to comment.