From 73d7ed4a4c12cadf40074a5d4ad088c158a654fc Mon Sep 17 00:00:00 2001 From: Michael Bangas Date: Thu, 3 Jul 2025 10:26:26 +0200 Subject: [PATCH] Support rendering images at custom sizes This commit introduces support for drawing images at arbitrary sizes. This enables flexible rendering in scenarios where image size is determined by UI requirements rather than screen zoom, such as: - Scalable images in diagrams (e.g., GEF-based tools) - Resizable UI components that embed images - Annotation rulers that scale with editor font size (#3044) The new API allows consumers to obtain image data at a specific target size, enabling better integration in custom UI scaling contexts. --- .../org/eclipse/swt/svg/JSVGRasterizer.java | 46 ++-- .../swt/graphics/ImageDataAtSizeProvider.java | 24 ++ .../eclipse/swt/graphics/ImageDataLoader.java | 28 ++- .../graphics/ImageFileNameAtSizeProvider.java | 28 +++ .../org/eclipse/swt/graphics/ImageLoader.java | 28 ++- .../org/eclipse/swt/internal/DPIUtil.java | 47 ++-- .../swt/internal/image/FileFormat.java | 31 +++ .../swt/internal/image/SVGFileFormat.java | 20 ++ .../swt/internal/image/SVGRasterizer.java | 2 + .../win32/org/eclipse/swt/graphics/GC.java | 75 ++++-- .../win32/org/eclipse/swt/graphics/Image.java | 154 +++++++++++- .../swt/internal/NativeImageLoader.java | 4 + .../resources/Snippet386/collapseall.png | Bin 0 -> 360 bytes .../resources/Snippet386/collapseall.svg | 223 ++++++++++++++++++ .../org/eclipse/swt/snippets/Snippet386.java | 100 ++++++++ 15 files changed, 732 insertions(+), 78 deletions(-) create mode 100644 bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataAtSizeProvider.java create mode 100644 bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageFileNameAtSizeProvider.java create mode 100644 examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.png create mode 100644 examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.svg create mode 100644 examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet386.java diff --git a/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/JSVGRasterizer.java b/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/JSVGRasterizer.java index cee41f23d9a..a2a5da280dc 100644 --- a/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/JSVGRasterizer.java +++ b/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/JSVGRasterizer.java @@ -72,34 +72,44 @@ public class JSVGRasterizer implements SVGRasterizer { @Override public ImageData rasterizeSVG(InputStream inputStream, int zoom) throws IOException { - SVGDocument svgDocument = loadSVG(inputStream); - if (svgDocument == null) { - SWT.error(SWT.ERROR_INVALID_IMAGE); - } + SVGDocument svgDocument = loadAndValidateSVG(inputStream); BufferedImage rasterizedImage = renderSVG(svgDocument, zoom); return convertToSWTImageData(rasterizedImage); } - - private SVGDocument loadSVG(InputStream inputStream) { - return SVG_LOADER.load(inputStream, null, LoaderContext.createDefault()); + + @Override + public ImageData rasterizeSVG(InputStream inputStream, int width, int height) throws IOException { + SVGDocument svgDocument = loadAndValidateSVG(inputStream); + BufferedImage rasterizedImage = renderSVG(svgDocument, width, height); + return convertToSWTImageData(rasterizedImage); + } + + private SVGDocument loadAndValidateSVG(InputStream inputStream) throws IOException { + SVGDocument svgDocument = SVG_LOADER.load(inputStream, null, LoaderContext.createDefault()); + if (svgDocument == null) { + SWT.error(SWT.ERROR_INVALID_IMAGE); + } + return svgDocument; } private BufferedImage renderSVG(SVGDocument svgDocument, int zoom) { + FloatSize sourceImageSize = svgDocument.size(); float scalingFactor = zoom / 100.0f; - BufferedImage image = createImageBase(svgDocument, scalingFactor); - Graphics2D g = configureRenderingOptions(scalingFactor, image); + int targetImageWidth = calculateTargetWidth(scalingFactor, sourceImageSize); + int targetImageHeight = calculateTargetHeight(scalingFactor, sourceImageSize); + return renderSVG(svgDocument, targetImageWidth, targetImageHeight); + } + + private BufferedImage renderSVG(SVGDocument svgDocument, int width, int height) { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + float widthScalingFactor = width / svgDocument.size().width; + float heightScalingFactor = height / svgDocument.size().height; + Graphics2D g = configureRenderingOptions(widthScalingFactor, heightScalingFactor, image); svgDocument.render(null, g); g.dispose(); return image; } - private BufferedImage createImageBase(SVGDocument svgDocument, float scalingFactor) { - FloatSize sourceImageSize = svgDocument.size(); - int targetImageWidth = calculateTargetWidth(scalingFactor, sourceImageSize); - int targetImageHeight = calculateTargetHeight(scalingFactor, sourceImageSize); - return new BufferedImage(targetImageWidth, targetImageHeight, BufferedImage.TYPE_INT_ARGB); - } - private int calculateTargetWidth(float scalingFactor, FloatSize sourceImageSize) { double sourceImageWidth = sourceImageSize.getWidth(); return (int) Math.round(sourceImageWidth * scalingFactor); @@ -110,10 +120,10 @@ private int calculateTargetHeight(float scalingFactor, FloatSize sourceImageSize return (int) Math.round(sourceImageHeight * scalingFactor); } - private Graphics2D configureRenderingOptions(float scalingFactor, BufferedImage image) { + private Graphics2D configureRenderingOptions(float widthScalingFactor, float heightScalingFactor, BufferedImage image) { Graphics2D g = image.createGraphics(); g.setRenderingHints(RENDERING_HINTS); - g.scale(scalingFactor, scalingFactor); + g.scale(widthScalingFactor, heightScalingFactor); return g; } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataAtSizeProvider.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataAtSizeProvider.java new file mode 100644 index 00000000000..a3ae805394a --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataAtSizeProvider.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2025 Vector Informatik GmbH and others. + * + * This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Michael Bangas (Vector Informatik GmbH) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.graphics; + +import java.util.*; + +/** + * @since 3.131 + */ +public interface ImageDataAtSizeProvider extends ImageDataProvider { + + Optional getImageData(int targetWidth, int targetHeight); + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataLoader.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataLoader.java index 29f517b45c0..8b2d1dc36a7 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataLoader.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataLoader.java @@ -41,20 +41,32 @@ public static boolean canLoadAtZoom(InputStream stream, int fileZoom, int target return ImageLoader.canLoadAtZoom(stream, fileZoom, targetZoom); } - public static ElementAtZoom load(InputStream stream, int fileZoom, int targetZoom) { - List> data = new ImageLoader().load(stream, fileZoom, targetZoom); - if (data.isEmpty()) SWT.error(SWT.ERROR_INVALID_IMAGE); - return data.get(0); - } - public static boolean canLoadAtZoom(String filename, int fileZoom, int targetZoom) { return ImageLoader.canLoadAtZoom(filename, fileZoom, targetZoom); } - public static ElementAtZoom load(String filename, int fileZoom, int targetZoom) { - List> data = new ImageLoader().load(filename, fileZoom, targetZoom); + public static ElementAtZoom loadByZoom(InputStream stream, int fileZoom, int targetZoom) { + List> data = new ImageLoader().loadByZoom(stream, fileZoom, targetZoom); + if (data.isEmpty()) SWT.error(SWT.ERROR_INVALID_IMAGE); + return data.get(0); + } + + public static ElementAtZoom loadByZoom(String filename, int fileZoom, int targetZoom) { + List> data = new ImageLoader().loadByZoom(filename, fileZoom, targetZoom); if (data.isEmpty()) SWT.error(SWT.ERROR_INVALID_IMAGE); return data.get(0); } + public static ImageData loadByTargetSize(InputStream stream, int targetWidth, int targetHeight) { + ImageData data = new ImageLoader().loadByTargetSize(stream, targetWidth, targetHeight); + if (data == null) SWT.error(SWT.ERROR_INVALID_IMAGE); + return data; + } + + public static ImageData loadByTargetSize(String filename, int targetWidth, int targetHeight) { + ImageData data = new ImageLoader().loadByTargetSize(filename, targetWidth, targetHeight); + if (data == null) SWT.error(SWT.ERROR_INVALID_IMAGE); + return data; + } + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageFileNameAtSizeProvider.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageFileNameAtSizeProvider.java new file mode 100644 index 00000000000..63ec3330879 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageFileNameAtSizeProvider.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 Vector Informatik GmbH and others. + * + * This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Michael Bangas (Vector Informatik GmbH) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.graphics; + +import java.util.*; + +/** + * @since 3.131 + */ +public interface ImageFileNameAtSizeProvider extends ImageFileNameProvider { + + /** + * Returns the image file path most suitable for rendering at the given target size. + * + */ + Optional getImagePath(int targetWidth, int targetHeight); + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageLoader.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageLoader.java index 9973b8f06e3..8cb312fcc33 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageLoader.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageLoader.java @@ -151,11 +151,11 @@ void reset() { * */ public ImageData[] load(InputStream stream) { - load(stream, FileFormat.DEFAULT_ZOOM, FileFormat.DEFAULT_ZOOM); + loadByZoom(stream, FileFormat.DEFAULT_ZOOM, FileFormat.DEFAULT_ZOOM); return data; } -List> load(InputStream stream, int fileZoom, int targetZoom) { +List> loadByZoom(InputStream stream, int fileZoom, int targetZoom) { if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); reset(); List> images = NativeImageLoader.load(new ElementAtZoom<>(stream, fileZoom), this, targetZoom); @@ -163,6 +163,14 @@ List> load(InputStream stream, int fileZoom, int target return images; } +ImageData loadByTargetSize(InputStream stream, int targetWidth, int targetHeight) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + ImageData image = NativeImageLoader.load(stream, this, targetWidth, targetHeight); + data = new ImageData[] {image}; + return image; +} + static boolean canLoadAtZoom(InputStream stream, int fileZoom, int targetZoom) { if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); return FileFormat.canLoadAtZoom(new ElementAtZoom<>(stream, fileZoom), targetZoom); @@ -187,14 +195,24 @@ static boolean canLoadAtZoom(InputStream stream, int fileZoom, int targetZoom) { * */ public ImageData[] load(String filename) { - load(filename, FileFormat.DEFAULT_ZOOM, FileFormat.DEFAULT_ZOOM); + loadByZoom(filename, FileFormat.DEFAULT_ZOOM, FileFormat.DEFAULT_ZOOM); return data; } -List> load(String filename, int fileZoom, int targetZoom) { +List> loadByZoom(String filename, int fileZoom, int targetZoom) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return loadByZoom(stream, fileZoom, targetZoom); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; +} + +ImageData loadByTargetSize(String filename, int targetWidth, int targetHeight) { if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); try (InputStream stream = new FileInputStream(filename)) { - return load(stream, fileZoom, targetZoom); + return loadByTargetSize(stream, targetWidth, targetHeight); } catch (IOException e) { SWT.error(SWT.ERROR_IO, e); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/DPIUtil.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/DPIUtil.java index 510bcb33490..79b3caa34ec 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/DPIUtil.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/DPIUtil.java @@ -144,34 +144,49 @@ public static ImageData scaleImageData (Device device, final ElementAtZoom imageData); - ImageGcDrawer drawer = new ImageGcDrawer() { + Image original = new Image(device, (ImageDataProvider) zoom -> imageData); + + ImageGcDrawer drawer = new ImageGcDrawer() { @Override public void drawOn(GC gc, int imageWidth, int imageHeight) { - gc.setAntialias (SWT.ON); - Image.drawScaled(gc, original, width, height, scaleFactor); - }; + gc.setAntialias(SWT.ON); + drawFunction.draw(gc, original, originalWidth, originalHeight, targetWidth, targetHeight); + } @Override public int getGcStyle() { return SWT.TRANSPARENT; } }; - Image resultImage = new Image (device, drawer, scaledWidth, scaledHeight); - ImageData result = resultImage.getImageData (100); - original.dispose (); - resultImage.dispose (); + + Image resultImage = new Image(device, drawer, targetWidth, targetHeight); + ImageData result = resultImage.getImageData(100); + original.dispose(); + resultImage.dispose(); return result; } else { - return imageData.scaledTo (scaledWidth, scaledHeight); + return imageData.scaledTo(targetWidth, targetHeight); } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/FileFormat.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/FileFormat.java index 8232d71023d..6e5da08b774 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/FileFormat.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/FileFormat.java @@ -85,6 +85,11 @@ static abstract class StaticImageFileFormat extends FileFormat { List> loadFromByteStream(int fileZoom, int targetZoom) { return Arrays.stream(loadFromByteStream()).map(d -> new ElementAtZoom<>(d, fileZoom)).toList(); } + + @Override + ImageData loadFromByteStreamByTargetSize(int targetWidth, int targetHeight) { + return loadFromByteStream()[0]; + } } LEDataInputStream inputStream; @@ -104,6 +109,8 @@ List> loadFromByteStream(int fileZoom, int targetZoom) */ abstract List> loadFromByteStream(int fileZoom, int targetZoom); + abstract ImageData loadFromByteStreamByTargetSize(int targetWidth, int targetHeight); + /** * Read the specified input stream, and return the * device independent image array represented by the stream. @@ -122,6 +129,20 @@ public List> loadFromStream(LEDataInputStream stream, i } } +public ImageData loadFromStreamByTargetSize(LEDataInputStream stream, int targetWidth, int targetHeight) { + try { + inputStream = stream; + return loadFromByteStreamByTargetSize(targetWidth, targetHeight); + } catch (Exception e) { + if (e instanceof IOException) { + SWT.error(SWT.ERROR_IO, e); + } else { + SWT.error(SWT.ERROR_INVALID_IMAGE, e); + } + return null; + } +} + /** * Read the specified input stream using the specified loader, and * return the device independent image array represented by the stream. @@ -136,6 +157,16 @@ public static List> load(ElementAtZoom is, return fileFormat.loadFromStream(stream, is.zoom(), targetZoom); } +public static ImageData load(InputStream is, ImageLoader loader, int targetWidth, int targetHeight) { + LEDataInputStream stream = new LEDataInputStream(is); + FileFormat fileFormat = determineFileFormat(stream).orElseGet(() -> { + SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT); + return null; + }); + fileFormat.loader = loader; + return fileFormat.loadFromStreamByTargetSize(stream, targetWidth, targetHeight); +} + public static boolean canLoadAtZoom(ElementAtZoom is, int targetZoom) { return is.zoom() == targetZoom || isDynamicallySizableFormat(is.element()); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGFileFormat.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGFileFormat.java index 78d8abebcb6..777e7373ae4 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGFileFormat.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGFileFormat.java @@ -60,6 +60,26 @@ List> loadFromByteStream(int fileZoom, int targetZoom) } } + @Override + ImageData loadFromByteStreamByTargetSize(int targetWidth, int targetHeight) { + if (RASTERIZER == null) { + SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT, null, " [No SVG rasterizer found]"); + } + if (targetWidth <= 0 || targetHeight <= 0) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT, null, " [Cannot rasterize SVG for targetWidth or targetHeight <= 0]"); + } + if (targetHeight <= 0 || targetHeight <= 0) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT, null, " [Cannot rasterize SVG for targetHeight or targetHeight <= 0]"); + } + try { + ImageData rasterizedImageData = RASTERIZER.rasterizeSVG(inputStream, targetWidth, targetHeight); + return rasterizedImageData; + } catch (IOException e) { + SWT.error(SWT.ERROR_INVALID_IMAGE, e); + return null; + } + } + @Override void unloadIntoByteStream(ImageLoader loader) { throw new UnsupportedOperationException(); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGRasterizer.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGRasterizer.java index 9586abfb5c6..79eae39be1c 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGRasterizer.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/image/SVGRasterizer.java @@ -32,4 +32,6 @@ public interface SVGRasterizer { * the input is not a valid SVG file or cannot be processed. */ public ImageData rasterizeSVG(InputStream stream, int zoom) throws IOException; + + public ImageData rasterizeSVG(InputStream stream, int width, int height) throws IOException; } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java index ebbbfbd1b0d..4e33ffd4b7b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java @@ -1132,22 +1132,29 @@ private int calculateZoomForImage(int gcZoom, int srcWidth, int srcHeight, int d private void drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, int destWidth, int destHeight, int imageZoom, int scaledImageZoom) { - Rectangle src = Win32DPIUtils.pointToPixel(drawable, new Rectangle(srcX, srcY, srcWidth, srcHeight), scaledImageZoom); - Rectangle dest = Win32DPIUtils.pointToPixel(drawable, new Rectangle(destX, destY, destWidth, destHeight), imageZoom); - if (scaledImageZoom != 100) { - /* - * This is a HACK! Due to rounding errors at fractional scale factors, - * the coordinates may be slightly off. The workaround is to restrict - * coordinates to the allowed bounds. - */ - Rectangle b = image.getBounds(scaledImageZoom); - int errX = src.x + src.width - b.width; - int errY = src.y + src.height - b.height; - if (errX != 0 || errY != 0) { - if (errX <= scaledImageZoom / 100 && errY <= scaledImageZoom / 100) { - src.intersect(b); - } else { - SWT.error (SWT.ERROR_INVALID_ARGUMENT); + Rectangle src; + Rectangle dest; + if (image.createdWithTargetSize()) { + dest = new Rectangle(destX, destY, destWidth, destHeight); + src = dest; + } else { + src = Win32DPIUtils.pointToPixel(drawable, new Rectangle(srcX, srcY, srcWidth, srcHeight), scaledImageZoom); + dest = Win32DPIUtils.pointToPixel(drawable, new Rectangle(destX, destY, destWidth, destHeight), imageZoom); + if (scaledImageZoom != 100) { + /* + * This is a HACK! Due to rounding errors at fractional scale factors, + * the coordinates may be slightly off. The workaround is to restrict + * coordinates to the allowed bounds. + */ + Rectangle b = image.getBounds(scaledImageZoom); + int errX = src.x + src.width - b.width; + int errY = src.y + src.height - b.height; + if (errX != 0 || errY != 0) { + if (errX <= scaledImageZoom / 100 && errY <= scaledImageZoom / 100) { + src.intersect(b); + } else { + SWT.error (SWT.ERROR_INVALID_ARGUMENT); + } } } } @@ -1231,17 +1238,37 @@ private void drawImage(Image srcImage, int srcX, int srcY, int srcWidth, int src } return; } - long imageHandle = Image.win32_getHandle(srcImage, imageZoom); - switch (srcImage.type) { - case SWT.BITMAP: - drawBitmap(srcImage, imageHandle, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, simple); - break; - case SWT.ICON: - drawIcon(imageHandle, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, simple); - break; + if (srcImage.createdWithTargetSize()) { + drawWithTempHandle(srcImage, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, simple); + } else { + long imageHandle = Image.win32_getHandle(srcImage, imageZoom); + switch (srcImage.type) { + case SWT.BITMAP: + drawBitmap(srcImage, imageHandle, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, + simple); + break; + case SWT.ICON: + drawIcon(imageHandle, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, simple); + break; + } } } +private void drawWithTempHandle(Image srcImage, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, + int destWidth, int destHeight, boolean simple) { + srcImage.executeOnImageHandleAtSize(tempImageHandle -> { + switch (srcImage.type) { + case SWT.BITMAP: + drawBitmap(srcImage, tempImageHandle, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, + simple); + break; + case SWT.ICON: + drawIcon(tempImageHandle, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight, simple); + break; + } + }, destWidth, destHeight); +} + private void drawIcon(long imageHandle, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, int destWidth, int destHeight, boolean simple) { int technology = OS.GetDeviceCaps(handle, OS.TECHNOLOGY); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java index 9530d94fb4c..37297ef0bc0 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java @@ -20,6 +20,7 @@ import java.util.*; import java.util.Map.*; import java.util.function.*; +import java.util.stream.*; import org.eclipse.swt.*; import org.eclipse.swt.internal.*; @@ -27,6 +28,7 @@ import org.eclipse.swt.internal.gdip.*; import org.eclipse.swt.internal.image.*; import org.eclipse.swt.internal.win32.*; +import org.eclipse.swt.widgets.*; /** * Instances of this class are graphics which have been prepared @@ -809,6 +811,25 @@ public static long win32_getHandle (Image image, int zoom) { return image.getImageMetadata(zoom).handle; } +void executeOnImageHandleAtSize(Consumer drawFunction, int targetWidth, int targetHeight) { + ImageData imageData = this.imageProvider.newImageData(targetWidth, targetHeight); + HandleForImageDataContainer handleContainer = init(device, imageData); + long tempHandle = handleContainer.handles()[0]; + drawFunction.accept(tempHandle); + if (handleContainer.type == SWT.ICON) { + OS.DestroyIcon (tempHandle); + } else { + OS.DeleteObject (tempHandle); + } +} + +/** + * @since 3.131 + */ +public boolean createdWithTargetSize() { + return this.imageProvider.isSizeAware(); +} + /** * IMPORTANT: This method is not part of the public * API for Image. It is marked public only so that it @@ -825,8 +846,17 @@ public static long win32_getHandle (Image image, int zoom) { * @noreference This method is not intended to be referenced by clients. */ public static void drawScaled(GC gc, Image original, int width, int height, float scaleFactor) { + int targetWidth = Math.round(width * scaleFactor); + int targetHeight = Math.round(height * scaleFactor); + drawAtTargetSize(gc, original, width, height, targetWidth, targetHeight); +} + +/** + * @since 3.131 + */ +public static void drawAtTargetSize(GC gc, Image original, int width, int height, int targetWidth, int targetHeight) { gc.drawImage (original, 0, 0, width, height, - 0, 0, Math.round (width * scaleFactor), Math.round (height * scaleFactor), false); + 0, 0, targetWidth, targetHeight, false); } long [] createGdipImage(Integer zoom) { @@ -1223,7 +1253,6 @@ public ImageData getImageData (int zoom) { return this.imageProvider.newImageData(zoom); } - /** * Returns an ImageData based on the receiver. * Modifications made to this ImageData will not @@ -1868,8 +1897,35 @@ public static Image win32_new(Device device, int type, long handle, int nativeZo return new Image(device, type, handle, nativeZoom); } +@FunctionalInterface +interface ElementAtSizeFunction { + Optional getForSize(int width, int height); +} + +private Optional getElementAtTargetSize(ElementAtSizeFunction elementForSizeProvider, int targetWidth, + int targetHeight) { + Optional dataAtOriginalSize = elementForSizeProvider.getForSize(targetWidth, targetHeight); + if (dataAtOriginalSize.isPresent()) { + T data = dataAtOriginalSize.get(); + if (data instanceof String fileName) { + ImageData imageData = ImageDataLoader.loadByTargetSize(fileName, targetWidth, targetHeight); + if (imageData.width == targetWidth && imageData.height == targetHeight) { + return dataAtOriginalSize; + } + } else if (data instanceof ImageData) + return dataAtOriginalSize; + } + return Optional.empty(); +} + private abstract class AbstractImageProviderWrapper { + protected boolean sizeAware = false; + + public boolean isSizeAware (){ + return this.sizeAware; + } + protected abstract Rectangle getBounds(int zoom); protected long configureGCData(GCData data) { @@ -1882,6 +1938,38 @@ public Collection getPreservedZoomLevels() { abstract ImageData newImageData(int zoom); + final ImageData newImageData(int targetWidth, int targetHeight) { + ImageData imageData = loadImageData(targetWidth, targetHeight); + imageData = adaptImageDataIfDisabledOrGray(imageData); + return DPIUtil.autoScaleImageData(device, imageData, targetWidth, targetHeight); + }; + + protected ImageData loadImageData(int targetWidth, int targetHeight) { + ImageData imageData; + Rectangle bounds = getBounds(100); + int imageZoomForWidth = Math.round(1f * targetWidth / bounds.width); + int imageZoomForHeight = Math.round(1f * targetHeight / bounds.height); + int imageZoom = Math.max(imageZoomForWidth, imageZoomForHeight); + if (getAllCurrentMonitorZooms().contains(imageZoom)) { + return newImageData(imageZoom); + } + if (imageZoom > 150) { + imageData = newImageData(200); + } else { + imageData = newImageData(100); + } + return imageData; + }; + + private Collection getAllCurrentMonitorZooms() { + if (device instanceof Display display) { + return Arrays.stream(display.getMonitors()) + .map(Monitor::getZoom) + .collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + abstract AbstractImageProviderWrapper createCopy(Image image); ImageData getScaledImageData (int zoom) { @@ -1984,7 +2072,6 @@ private ImageHandle initializeHandleFromSource(int zoom) { imageData = adaptImageDataIfDisabledOrGray(imageData); return newImageHandle(imageData, zoom); } - } private class PlainImageDataProviderWrapper extends ImageFromImageDataProviderWrapper { @@ -2068,7 +2155,7 @@ private ImageDataLoaderStreamProviderWrapper(byte[] inputStreamData) { @Override protected ElementAtZoom loadImageData(int zoom) { - return ImageDataLoader.load(new ByteArrayInputStream(inputStreamData), FileFormat.DEFAULT_ZOOM, zoom); + return ImageDataLoader.loadByZoom(new ByteArrayInputStream(inputStreamData), FileFormat.DEFAULT_ZOOM, zoom); } @Override @@ -2237,7 +2324,6 @@ ImageData newImageData(int zoom) { return cachedImageData.computeIfAbsent(zoom, imageDataRetrival); } - @Override protected ImageHandle newImageHandle(int zoom) { ImageData cachedData = cachedImageData.remove(zoom); @@ -2266,6 +2352,9 @@ protected Rectangle getBounds(int zoom) { private class ImageFileNameProviderWrapper extends BaseImageProviderWrapper { ImageFileNameProviderWrapper(ImageFileNameProvider provider) { super(provider, ImageFileNameProvider.class); + if (provider instanceof ImageFileNameAtSizeProvider) { + this.sizeAware = true; + } // Checks for the contract of the passed provider require // checking for valid image data creation newImageData(DPIUtil.getDeviceZoom()); @@ -2277,7 +2366,7 @@ protected ElementAtZoom loadImageData(int zoom) { // Load at appropriate zoom via loader if (fileForZoom.zoom() != zoom && ImageDataLoader.canLoadAtZoom(fileForZoom.element(), fileForZoom.zoom(), zoom)) { - ElementAtZoom imageDataAtZoom = ImageDataLoader.load(fileForZoom.element(), fileForZoom.zoom(), zoom); + ElementAtZoom imageDataAtZoom = ImageDataLoader.loadByZoom(fileForZoom.element(), fileForZoom.zoom(), zoom); return new ElementAtZoom<>(imageDataAtZoom.element(), zoom); } @@ -2290,7 +2379,7 @@ protected ElementAtZoom loadImageData(int zoom) { } ElementAtZoom imageDataAtZoom; if (nativeInitializedImage == null) { - imageDataAtZoom = ImageDataLoader.load(fileForZoom.element(), fileForZoom.zoom(), zoom); + imageDataAtZoom = ImageDataLoader.loadByZoom(fileForZoom.element(), fileForZoom.zoom(), zoom); } else { imageDataAtZoom = new ElementAtZoom<>(nativeInitializedImage.getImageData(), fileForZoom.zoom()); nativeInitializedImage.destroy(); @@ -2298,6 +2387,35 @@ protected ElementAtZoom loadImageData(int zoom) { return imageDataAtZoom; } + @Override + protected ImageData loadImageData(int targetWidth, int targetHeight) { + Optional fileForTargetSize = validateAndGetImagePathAtTargetSize(provider, targetWidth, targetHeight); + if(fileForTargetSize.isPresent()) { + ImageData imageDataAtZoom = ImageDataLoader.loadByTargetSize(fileForTargetSize.get(), targetWidth, targetWidth); + return imageDataAtZoom; + } + return null; + } + + private Optional validateAndGetImagePathAtTargetSize(ImageFileNameProvider provider, int targetWidth, int targetHeight) { + if (provider == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (provider instanceof ImageFileNameAtSizeProvider sizeAwareProvider) { + Optional imagePathAtTargetSize = getElementAtTargetSize(sizeAwareProvider::getImagePath, + targetWidth, targetHeight); + if (!imagePathAtTargetSize.isEmpty()) { + return imagePathAtTargetSize; + } + } + Rectangle originalSize = getBounds(100); + int imageZoomForWidth = Math.round(1f * targetWidth / originalSize.width); + int imageZoomForHeight = Math.round(1f * targetHeight / originalSize.height); + int imageZoom = Math.max(imageZoomForWidth, imageZoomForHeight); + String imagePath = DPIUtil.validateAndGetImagePathAtZoom(provider, imageZoom).element(); + return Optional.of(imagePath); + } + @Override public int hashCode() { return Objects.hash(provider, styleFlag, transparentPixel); @@ -2505,6 +2623,11 @@ private long extractHandleForPixelFormat(int width, int height, int pixelFormat) private class ImageDataProviderWrapper extends BaseImageProviderWrapper { ImageDataProviderWrapper(ImageDataProvider provider) { super(provider, ImageDataProvider.class); + // Checks for the contract of the passed provider require + // checking for valid image data creation + if (provider instanceof ImageDataAtSizeProvider) { + this.sizeAware = true; + } } @Override @@ -2512,6 +2635,23 @@ protected ElementAtZoom loadImageData(int zoom) { return DPIUtil.validateAndGetImageDataAtZoom (provider, zoom); } + @Override + protected ImageData loadImageData(int targetWidth, int targetHeight) { + return validateAndGetImageDataAtTargetSize(targetWidth, targetHeight).get(); + } + + private Optional validateAndGetImageDataAtTargetSize(int targetWidth, int targetHeight) { + if (provider == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + Optional imageDataAtTargetSize = getElementAtTargetSize((x, z) -> ((ImageDataAtSizeProvider) provider).getImageData(x, z), targetWidth, targetHeight); + if (imageDataAtTargetSize == null) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT, null, + ": ImageDataProvider [" + provider + "] returns null ImageData at 100% zoom."); + } + return imageDataAtTargetSize; + } + @Override ImageDataProviderWrapper createCopy(Image image) { return image.new ImageDataProviderWrapper(provider); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/NativeImageLoader.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/NativeImageLoader.java index 09b39591368..3d122179db7 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/NativeImageLoader.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/NativeImageLoader.java @@ -26,6 +26,10 @@ public static List> load(ElementAtZoom str return FileFormat.load(streamAtZoom, imageLoader, targetZoom); } + public static ImageData load(InputStream streamAtZoom, ImageLoader imageLoader, int targetWidth, int targetHeight) { + return FileFormat.load(streamAtZoom, imageLoader, targetWidth, targetHeight); + } + public static void save(OutputStream stream, int format, ImageLoader imageLoader) { FileFormat.save(stream, format, imageLoader); } diff --git a/examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.png b/examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.png new file mode 100644 index 0000000000000000000000000000000000000000..0ac25a9247ba9cf9b9a9916cbbcf782c21daaf26 GIT binary patch literal 360 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR44*VqH_sb~aQnMsrKE9!xl< zUGmWIkMvPf-NG~H>xr-yeP>%bYjNpozZ{%PH;+Lt1ta_bOBl$b=95aU-K1>T?or+TFrlrofD z48B?H=g!RjuHJg{`qwKw0*#-uPW|Ov9&J`vIVC{B;d0mAl=z0j?`&11RI zR9Yk(ZKtfYLV%g;&YzaEhxGUT4)?nH{twgoN{iFiID~D19%k@#^>bP0l+XkK#)yWF literal 0 HcmV?d00001 diff --git a/examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.svg b/examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.svg new file mode 100644 index 00000000000..587c3c3497e --- /dev/null +++ b/examples/org.eclipse.swt.snippets/resources/Snippet386/collapseall.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet386.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet386.java new file mode 100644 index 00000000000..a44ac5e8569 --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet386.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2025 Vector Informatik GmbH and others. + * + * This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Michael Bangas (Vector Informatik GmbH) - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.snippets; + +import java.io.*; +import java.util.*; + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.*; +import org.eclipse.swt.internal.DPIUtil.*; +import org.eclipse.swt.widgets.*; + +public class Snippet386 { + + public static void main(String[] args) { + drawWithImageFileNameProvider(); + drawWithImageDataProvider(); + } + + private static void drawWithImageFileNameProvider() { + ImageFileNameProvider provider = createImageFileNameProvider(); + Image image = new Image(Display.getDefault(), provider); + createShellWithImage(image, "Snippet 386 - ImageFileNameProvider"); + } + + private static void drawWithImageDataProvider() { + ImageDataProvider provider = createImageDataProvider(); + Image image = new Image(Display.getDefault(), provider); + createShellWithImage(image, "Snippet 386 - ImageDataProvider"); + } + + private static ImageFileNameProvider createImageFileNameProvider() { + return new ImageFileNameAtSizeProvider() { + @Override + public Optional getImagePath(int targetWidth, int targetHeight) { + return Optional.of("resources/Snippet386/collapseall.svg"); + } + + @Override + public String getImagePath(int zoom) { + return "resources/Snippet386/collapseall.svg"; + } + }; + } + + private static ImageDataProvider createImageDataProvider() { + return new ImageDataAtSizeProvider() { + @SuppressWarnings("restriction") + @Override + public Optional getImageData(int targetWidth, int targetHeight) { + try (InputStream stream = new FileInputStream("resources/Snippet386/collapseall.svg")) { + return Optional.of(NativeImageLoader.load(stream, new ImageLoader(), targetWidth, targetHeight)); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; + } + + @SuppressWarnings("restriction") + @Override + public ImageData getImageData(int zoom) { + try (InputStream stream = new FileInputStream("resources/Snippet386/collapseall.svg")) { + return NativeImageLoader.load(new ElementAtZoom<>(stream, 100), new ImageLoader(), 100).get(0).element(); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; + } + }; + } + + private static void createShellWithImage(Image image, String title) { + Shell shell = new Shell(Display.getDefault(), SWT.SHELL_TRIM | SWT.DOUBLE_BUFFERED); + shell.setText(title); + + shell.addListener(SWT.Paint, e -> { + Rectangle rect = image.getBounds(); + e.gc.drawImage(image, 0, 0, rect.width, rect.height, 0, 0, 100, 200); + }); + + shell.setSize(600, 400); + shell.open(); + while (!shell.isDisposed()) { + if (!Display.getDefault().readAndDispatch()) + Display.getDefault().sleep(); + } + Display.getDefault().dispose(); + } +}