From 9bba9c0e1dd948a187376848029333d2154106c2 Mon Sep 17 00:00:00 2001 From: Xinchen Liu Date: Thu, 1 Oct 2026 18:33:12 -0400 Subject: [PATCH 1/2] Preserve HiDPI detail in ROI and overlay double buffering --- ij/gui/ImageCanvas.java | 61 ++++++++++------ tests/ij/gui/ImageCanvasHiDpiTest.java | 97 ++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 tests/ij/gui/ImageCanvasHiDpiTest.java diff --git a/ij/gui/ImageCanvas.java b/ij/gui/ImageCanvas.java index 747915ea9..a68dc7a4a 100644 --- a/ij/gui/ImageCanvas.java +++ b/ij/gui/ImageCanvas.java @@ -78,6 +78,7 @@ public class ImageCanvas extends Canvas implements MouseListener, MouseMotionLis private Image offScreenImage; private int offScreenWidth = 0; private int offScreenHeight = 0; + private double offScreenScaleX = 1.0, offScreenScaleY = 1.0; private boolean mouseExited = true; private boolean customRoi; private boolean drawNames; @@ -538,10 +539,23 @@ void drawZoomIndicator(Graphics g) { void paintDoubleBuffered(Graphics g) { final int srcRectWidthMag = (int)(srcRect.width*magnification+0.5); final int srcRectHeightMag = (int)(srcRect.height*magnification+0.5); - if (offScreenImage==null || offScreenWidth!=srcRectWidthMag || offScreenHeight!=srcRectHeightMag) { - offScreenImage = createImage(srcRectWidthMag, srcRectHeightMag); - offScreenWidth = srcRectWidthMag; - offScreenHeight = srcRectHeightMag; + AffineTransform transform = ((Graphics2D)g).getTransform(); + double scaleX = Math.max(1.0, Math.hypot(transform.getScaleX(), transform.getShearY())); + double scaleY = Math.max(1.0, Math.hypot(transform.getShearX(), transform.getScaleY())); + final int bufferWidth = (int)Math.ceil(srcRectWidthMag*scaleX); + final int bufferHeight = (int)Math.ceil(srcRectHeightMag*scaleY); + if (offScreenImage==null || offScreenWidth!=bufferWidth || offScreenHeight!=bufferHeight + || offScreenScaleX!=scaleX || offScreenScaleY!=scaleY) { + if (offScreenImage!=null) + offScreenImage.flush(); + // Explicit raster dimensions avoid depending on a platform Image's backing scale. + offScreenImage = scaleX==1.0 && scaleY==1.0 + ? createImage(bufferWidth, bufferHeight) + : new BufferedImage(bufferWidth, bufferHeight, BufferedImage.TYPE_INT_RGB); + offScreenWidth = bufferWidth; + offScreenHeight = bufferHeight; + offScreenScaleX = scaleX; + offScreenScaleY = scaleY; } Roi roi = imp.getRoi(); try { @@ -550,22 +564,29 @@ void paintDoubleBuffered(Graphics g) { imp.updateImage(); } Graphics offScreenGraphics = offScreenImage.getGraphics(); - setInterpolation(offScreenGraphics, Prefs.interpolateScaledImages); - Image img = imp.getImage(); - if (img!=null) - offScreenGraphics.drawImage(img, 0, 0, srcRectWidthMag, srcRectHeightMag, - srcRect.x, srcRect.y, srcRect.x+srcRect.width, srcRect.y+srcRect.height, null); - Overlay overlay = imp.getOverlay(); - if (overlay!=null) - drawOverlay(overlay, offScreenGraphics); - if (showAllOverlay!=null) - drawOverlay(showAllOverlay, offScreenGraphics); - if (roi!=null) - drawRoi(roi, offScreenGraphics); - if (srcRect.width Date: Thu, 1 Oct 2026 18:54:30 -0400 Subject: [PATCH 2/2] Preserve fractional device pixel phase when presenting buffers --- ij/gui/ImageCanvas.java | 54 +++++++++++++++++++------- tests/ij/gui/ImageCanvasHiDpiTest.java | 33 +++++++++++----- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/ij/gui/ImageCanvas.java b/ij/gui/ImageCanvas.java index a68dc7a4a..896a00692 100644 --- a/ij/gui/ImageCanvas.java +++ b/ij/gui/ImageCanvas.java @@ -78,7 +78,7 @@ public class ImageCanvas extends Canvas implements MouseListener, MouseMotionLis private Image offScreenImage; private int offScreenWidth = 0; private int offScreenHeight = 0; - private double offScreenScaleX = 1.0, offScreenScaleY = 1.0; + private boolean offScreenDeviceBuffer; private boolean mouseExited = true; private boolean customRoi; private boolean drawNames; @@ -540,22 +540,35 @@ void paintDoubleBuffered(Graphics g) { final int srcRectWidthMag = (int)(srcRect.width*magnification+0.5); final int srcRectHeightMag = (int)(srcRect.height*magnification+0.5); AffineTransform transform = ((Graphics2D)g).getTransform(); - double scaleX = Math.max(1.0, Math.hypot(transform.getScaleX(), transform.getShearY())); - double scaleY = Math.max(1.0, Math.hypot(transform.getShearX(), transform.getScaleY())); - final int bufferWidth = (int)Math.ceil(srcRectWidthMag*scaleX); - final int bufferHeight = (int)Math.ceil(srcRectHeightMag*scaleY); + boolean deviceBuffer = transform.getScaleX()!=1.0 || transform.getScaleY()!=1.0 + || transform.getShearX()!=0.0 || transform.getShearY()!=0.0 + || transform.getTranslateX()!=Math.floor(transform.getTranslateX()) + || transform.getTranslateY()!=Math.floor(transform.getTranslateY()); + Rectangle bufferBounds = deviceBuffer + ? transform.createTransformedShape(new Rectangle(0, 0, srcRectWidthMag, srcRectHeightMag)).getBounds() + : new Rectangle(0, 0, srcRectWidthMag, srcRectHeightMag); + final int bufferWidth = bufferBounds.width; + final int bufferHeight = bufferBounds.height; + if (bufferWidth<=0 || bufferHeight<=0) + return; + AffineTransform imageTransform = null; + if (deviceBuffer) { + try { + imageTransform = transform.createInverse(); + imageTransform.translate(bufferBounds.x, bufferBounds.y); + } catch (NoninvertibleTransformException e) { return; } + } if (offScreenImage==null || offScreenWidth!=bufferWidth || offScreenHeight!=bufferHeight - || offScreenScaleX!=scaleX || offScreenScaleY!=scaleY) { + || offScreenDeviceBuffer!=deviceBuffer) { if (offScreenImage!=null) offScreenImage.flush(); // Explicit raster dimensions avoid depending on a platform Image's backing scale. - offScreenImage = scaleX==1.0 && scaleY==1.0 + offScreenImage = !deviceBuffer ? createImage(bufferWidth, bufferHeight) - : new BufferedImage(bufferWidth, bufferHeight, BufferedImage.TYPE_INT_RGB); + : new BufferedImage(bufferWidth, bufferHeight, BufferedImage.TYPE_INT_ARGB); offScreenWidth = bufferWidth; offScreenHeight = bufferHeight; - offScreenScaleX = scaleX; - offScreenScaleY = scaleY; + offScreenDeviceBuffer = deviceBuffer; } Roi roi = imp.getRoi(); try { @@ -565,9 +578,15 @@ void paintDoubleBuffered(Graphics g) { } Graphics offScreenGraphics = offScreenImage.getGraphics(); try { - if (scaleX!=1.0 || scaleY!=1.0) - ((Graphics2D)offScreenGraphics).scale((double)bufferWidth/srcRectWidthMag, - (double)bufferHeight/srcRectHeightMag); + if (deviceBuffer) { + Graphics2D bufferGraphics = (Graphics2D)offScreenGraphics; + bufferGraphics.setComposite(AlphaComposite.Clear); + bufferGraphics.fillRect(0, 0, bufferWidth, bufferHeight); + bufferGraphics.setComposite(AlphaComposite.SrcOver); + AffineTransform bufferTransform = AffineTransform.getTranslateInstance(-bufferBounds.x, -bufferBounds.y); + bufferTransform.concatenate(transform); + bufferGraphics.setTransform(bufferTransform); + } setInterpolation(offScreenGraphics, Prefs.interpolateScaledImages); Image img = imp.getImage(); if (img!=null) @@ -586,7 +605,14 @@ void paintDoubleBuffered(Graphics g) { } finally { offScreenGraphics.dispose(); } - g.drawImage(offScreenImage, 0, 0, srcRectWidthMag, srcRectHeightMag, null); + if (deviceBuffer) { + // Present at an integer device-pixel origin without a second resampling. + Graphics2D displayGraphics = (Graphics2D)g.create(); + try { + displayGraphics.drawImage(offScreenImage, imageTransform, null); + } finally { displayGraphics.dispose(); } + } else + g.drawImage(offScreenImage, 0, 0, null); } catch(OutOfMemoryError e) {IJ.outOfMemory("Paint");} } diff --git a/tests/ij/gui/ImageCanvasHiDpiTest.java b/tests/ij/gui/ImageCanvasHiDpiTest.java index a9d5f733b..c4a5e9bf1 100644 --- a/tests/ij/gui/ImageCanvasHiDpiTest.java +++ b/tests/ij/gui/ImageCanvasHiDpiTest.java @@ -24,17 +24,22 @@ private static class SyntheticImage extends ImagePlus { public ImageCanvas getCanvas() { return canvas; } } - private SyntheticImage image() { - int[] pixels = new int[128*128]; - for (int y=0; y<128; y++) - for (int x=0; x<128; x++) - pixels[y*128+x] = ((x+y)%2==0) ? 0xffffff : 0; - return new SyntheticImage(new ColorProcessor(128,128,pixels)); + private SyntheticImage image() { return image(128); } + + private SyntheticImage image(int size) { + int[] pixels = new int[size*size]; + for (int y=0; y