From 745bf64462182f0b487d4197dbcbfdf93db535b5 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 15 Apr 2024 13:00:37 +0900 Subject: [PATCH 1/3] Add helper methods for creating/populating BufferedImage Signed-off-by: Benjamin Gilbert --- org/openslide/OpenSlide.java | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/org/openslide/OpenSlide.java b/org/openslide/OpenSlide.java index 4f7ce26..7cf3b02 100644 --- a/org/openslide/OpenSlide.java +++ b/org/openslide/OpenSlide.java @@ -288,11 +288,8 @@ public void paintRegion(Graphics2D g, int dx, int dy, long sx, long sy, return; } - BufferedImage img = new BufferedImage(levelW, levelH, - BufferedImage.TYPE_INT_ARGB_PRE); - - int data[] = ((DataBufferInt) img.getRaster().getDataBuffer()) - .getData(); + BufferedImage img = createARGBBufferedImage(levelW, levelH); + int data[] = getARGBPixels(img); paintRegionARGB(data, baseX, baseY, level, img.getWidth(), img .getHeight()); @@ -396,11 +393,8 @@ BufferedImage getAssociatedImage(String name) throws IOException { throw new IOException("Failure reading associated image"); } - BufferedImage img = new BufferedImage((int) dim[0], (int) dim[1], - BufferedImage.TYPE_INT_ARGB_PRE); - - int data[] = ((DataBufferInt) img.getRaster().getDataBuffer()) - .getData(); + BufferedImage img = createARGBBufferedImage((int) dim[0], (int) dim[1]); + int data[] = getARGBPixels(img); try (errorCtx) { OpenSlideFFM.openslide_read_associated_image(errorCtx.getOsr(), @@ -451,4 +445,13 @@ 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 int[] getARGBPixels(BufferedImage img) { + DataBufferInt buf = (DataBufferInt) img.getRaster().getDataBuffer(); + return buf.getData(); + } } From 7846fcbb2d4e9a5759acb1aa84eafe6f9d306889 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 15 Apr 2024 14:47:26 +0900 Subject: [PATCH 2/3] Add OpenSlide.readRegion() API method that returns a BufferedImage We have paintRegionARGB() which writes directly into an int array, and paintRegion() which scales and draws into a Graphics2D, but we could use an intermediate-level API that returns an image object directly from OpenSlide. Signed-off-by: Benjamin Gilbert --- org/openslide/OpenSlide.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/org/openslide/OpenSlide.java b/org/openslide/OpenSlide.java index 7cf3b02..573b819 100644 --- a/org/openslide/OpenSlide.java +++ b/org/openslide/OpenSlide.java @@ -246,6 +246,14 @@ 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); + int data[] = getARGBPixels(img); + paintRegionARGB(data, x, y, level, w, h); + return img; + } + public void paintRegion(Graphics2D g, int dx, int dy, long sx, long sy, int w, int h, double downsample) throws IOException { if (downsample < 1.0) { @@ -288,11 +296,7 @@ public void paintRegion(Graphics2D g, int dx, int dy, long sx, long sy, return; } - BufferedImage img = createARGBBufferedImage(levelW, levelH); - int data[] = getARGBPixels(img); - - paintRegionARGB(data, baseX, baseY, level, img.getWidth(), img - .getHeight()); + BufferedImage img = readRegion(baseX, baseY, level, levelW, levelH); // g.scale(1.0 / relativeDS, 1.0 / relativeDS); g.drawImage(img, dx, dy, w, h, null); From a9c59ac722b01008d8cfff8791cf26d35e58f7b8 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 15 Apr 2024 14:39:42 +0900 Subject: [PATCH 3/3] Support reading ICC color profiles 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: https://github.com/openslide/openslide-java/issues/53 Signed-off-by: Benjamin Gilbert --- org/openslide/OpenSlide.java | 70 +++++++++++++++++++++++++++++++-- org/openslide/OpenSlideFFM.java | 64 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/org/openslide/OpenSlide.java b/org/openslide/OpenSlide.java index 573b819..588be77 100644 --- a/org/openslide/OpenSlide.java +++ b/org/openslide/OpenSlide.java @@ -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; @@ -113,6 +121,8 @@ public void close() throws IOException { final private Map associatedImages; + final private ColorModel colorModel; + final private File canonicalFile; final private int hashCodeVal; @@ -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); @@ -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]); @@ -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; @@ -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) { @@ -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); + } + } } diff --git a/org/openslide/OpenSlideFFM.java b/org/openslide/OpenSlideFFM.java index ec66829..20e9651 100644 --- a/org/openslide/OpenSlideFFM.java +++ b/org/openslide/OpenSlideFFM.java @@ -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); @@ -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);