diff --git a/bundles/org.eclipse.swt.svg.tests/.classpath b/bundles/org.eclipse.swt.svg.tests/.classpath new file mode 100644 index 00000000000..7417a95f02f --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/bundles/org.eclipse.swt.svg.tests/.project b/bundles/org.eclipse.swt.svg.tests/.project new file mode 100644 index 00000000000..6cd18c46394 --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/.project @@ -0,0 +1,28 @@ + + + org.eclipse.swt.svg.tests + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/bundles/org.eclipse.swt.svg.tests/.settings/org.eclipse.jdt.core.prefs b/bundles/org.eclipse.swt.svg.tests/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..23fa13b1705 --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=21 diff --git a/bundles/org.eclipse.swt.svg.tests/JUnit Tests/org/eclipse/swt/svg/tests/junit/Test_org_eclipse_swt_internal_SVGRasterizer.java b/bundles/org.eclipse.swt.svg.tests/JUnit Tests/org/eclipse/swt/svg/tests/junit/Test_org_eclipse_swt_internal_SVGRasterizer.java new file mode 100644 index 00000000000..bfa77054021 --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/JUnit Tests/org/eclipse/swt/svg/tests/junit/Test_org_eclipse_swt_internal_SVGRasterizer.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * 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.svg.tests.junit; + +import static org.eclipse.swt.tests.junit.SwtTestUtil.assertSWTProblem; +import static org.junit.Assert.fail; + +import java.io.File; +import java.nio.file.Path; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageDataProvider; +import org.eclipse.swt.graphics.ImageFileNameProvider; +import org.eclipse.swt.tests.junit.SwtTestUtil; +import org.eclipse.swt.widgets.Display; +import org.junit.Before; +import org.junit.Test; + +public class Test_org_eclipse_swt_internal_SVGRasterizer { + + Display display; + + ImageFileNameProvider imageFileNameProvider = zoom -> { + String fileName = "collapseall.svg"; + return getPath(fileName); + }; + + ImageDataProvider imageDataProvider = zoom -> { + String fileName = "collapseall.svg"; + return new ImageData(getPath(fileName), zoom); + }; + + @Before + public void setUp() { + display = Display.getDefault(); + } + + String getPath(String fileName) { + String urlPath = ""; + String pluginPath = System.getProperty("PLUGIN_PATH"); + if (pluginPath == null) { + urlPath = Path.of("data/" + fileName).toAbsolutePath().toString(); + } else { + urlPath = pluginPath + "/data/" + fileName; + } + if (File.separatorChar != '/') + urlPath = urlPath.replace('/', File.separatorChar); + if (SwtTestUtil.isWindows && urlPath.indexOf(File.separatorChar) == 0) + urlPath = urlPath.substring(1); + urlPath = urlPath.replaceAll("%20", " "); + return urlPath; + } + + @Test + public void test_ConstructorLorg_eclipse_swt_graphics_Device_ImageFileNameProvider() { + // Valid provider + Image image = new Image(display, imageFileNameProvider); + image.dispose(); + // Corrupt Image provider + ImageFileNameProvider provider = zoom -> { + String fileName = "corrupt.svg"; + return getPath(fileName); + }; + try { + image = new Image(display, provider); + image.dispose(); + fail("No exception thrown for corrupt image file."); + } catch (SWTException e) { + assertSWTProblem("Incorrect exception thrown for provider with corrupt images", SWT.ERROR_INVALID_IMAGE, e); + } + } + + @Test + public void test_ConstructorLorg_eclipse_swt_graphics_Device_ImageDataProvider() { + // Valid provider + Image image = new Image(display, imageDataProvider); + image.dispose(); + // Corrupt Image provider + ImageDataProvider provider = zoom -> { + String fileName = "corrupt.svg"; + return new ImageData(getPath(fileName), zoom); + }; + try { + image = new Image(display, provider); + image.dispose(); + fail("No exception thrown for corrupt image file."); + } catch (SWTException e) { + assertSWTProblem("Incorrect exception thrown for provider with corrupt images", SWT.ERROR_INVALID_IMAGE, e); + } + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.svg.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.swt.svg.tests/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..c33b7d7f436 --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Tests +Bundle-SymbolicName: org.eclipse.swt.svg.tests +Bundle-Version: 1.0.0.qualifier +Require-Bundle: org.eclipse.swt, + org.eclipse.jface, + org.eclipse.swt.tests, + org.junit, + org.eclipse.core.runtime +Automatic-Module-Name: org.eclipse.swt.svg.tests +Bundle-ClassPath: . +Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/bundles/org.eclipse.swt.svg.tests/build.properties b/bundles/org.eclipse.swt.svg.tests/build.properties new file mode 100644 index 00000000000..4763cdab3fc --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/build.properties @@ -0,0 +1,4 @@ +source.. = JUnit Tests/, +output.. = bin/ +bin.includes = .,\ + META-INF/ diff --git a/bundles/org.eclipse.swt.svg.tests/data/collapseall.svg b/bundles/org.eclipse.swt.svg.tests/data/collapseall.svg new file mode 100644 index 00000000000..587c3c3497e --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/data/collapseall.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.eclipse.swt.svg.tests/data/corrupt.svg b/bundles/org.eclipse.swt.svg.tests/data/corrupt.svg new file mode 100644 index 00000000000..edac6e7e9f2 --- /dev/null +++ b/bundles/org.eclipse.swt.svg.tests/data/corrupt.svg @@ -0,0 +1,210 @@ + + + +< inkscape:version="0.91 r13725" + sodipodi:docname="collapseall.svg"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.eclipse.swt.svg/.classpath b/bundles/org.eclipse.swt.svg/.classpath new file mode 100644 index 00000000000..7e80fa54e17 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/.classpath @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/bundles/org.eclipse.swt.svg/.project b/bundles/org.eclipse.swt.svg/.project new file mode 100644 index 00000000000..587ce542c7c --- /dev/null +++ b/bundles/org.eclipse.swt.svg/.project @@ -0,0 +1,28 @@ + + + org.eclipse.swt.svg + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/bundles/org.eclipse.swt.svg/.settings/org.eclipse.core.resources.prefs b/bundles/org.eclipse.swt.svg/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/bundles/org.eclipse.swt.svg/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/bundles/org.eclipse.swt.svg/.settings/org.eclipse.jdt.core.prefs b/bundles/org.eclipse.swt.svg/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..4b76983c5f1 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.incompleteClasspath=warning diff --git a/bundles/org.eclipse.swt.svg/META-INF/MANIFEST.MF b/bundles/org.eclipse.swt.svg/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..46eab990081 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/META-INF/MANIFEST.MF @@ -0,0 +1,10 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: SvgPlugin +Bundle-SymbolicName: org.eclipse.swt.svg +Bundle-Version: 1.0.0.qualifier +Automatic-Module-Name: org.eclipse.swt.svgPlugin +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Fragment-Host: org.eclipse.swt +Export-Package: org.eclipse.swt.svg +Bundle-ClassPath: ., libs/jsvg-1.6.1.jar diff --git a/bundles/org.eclipse.swt.svg/META-INF/services/org.eclipse.swt.internal.SVGRasterizer b/bundles/org.eclipse.swt.svg/META-INF/services/org.eclipse.swt.internal.SVGRasterizer new file mode 100644 index 00000000000..bd95b6eea89 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/META-INF/services/org.eclipse.swt.internal.SVGRasterizer @@ -0,0 +1 @@ +org.eclipse.swt.svg.JSVGRasterizer diff --git a/bundles/org.eclipse.swt.svg/build.properties b/bundles/org.eclipse.swt.svg/build.properties new file mode 100644 index 00000000000..3c7f762fff8 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + libs/jsvg-1.6.1.jar diff --git a/bundles/org.eclipse.swt.svg/libs/jsvg-1.6.1.jar b/bundles/org.eclipse.swt.svg/libs/jsvg-1.6.1.jar new file mode 100644 index 00000000000..3095291a853 Binary files /dev/null and b/bundles/org.eclipse.swt.svg/libs/jsvg-1.6.1.jar differ 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 new file mode 100644 index 00000000000..42617c97f18 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/JSVGRasterizer.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * 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.svg; + +import static java.awt.RenderingHints.*; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.util.*; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.internal.SVGRasterizer; +import com.github.weisj.jsvg.*; +import com.github.weisj.jsvg.geometry.size.*; +import com.github.weisj.jsvg.parser.*; + +/** + * A rasterizer implementation for converting SVG data into rasterized images. + * This class implements the {@code SVGRasterizer} interface. + * + * @since 1.0.0 + */ +public class JSVGRasterizer implements SVGRasterizer { + + private static final SVGLoader SVG_LOADER = new SVGLoader(); + + private final static Map RENDERING_HINTS = Map.of( + KEY_ANTIALIASING, VALUE_ANTIALIAS_ON, // + KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY, // + KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY, // + KEY_DITHERING, VALUE_DITHER_DISABLE, // + KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON, // + KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC, // + KEY_RENDERING, VALUE_RENDER_QUALITY, // + KEY_STROKE_CONTROL, VALUE_STROKE_PURE, // + KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON // + ); + + @Override + public ImageData[] rasterizeSVG(InputStream stream, int zoom) throws IOException { + SVGDocument svgDocument = null; + svgDocument = SVG_LOADER.load(stream, null, LoaderContext.createDefault()); + if (svgDocument != null) { + float scalingFactor = zoom / 100.0f; + FloatSize size = svgDocument.size(); + double originalWidth = size.getWidth(); + double originalHeight = size.getHeight(); + int scaledWidth = (int) Math.round(originalWidth * scalingFactor); + int scaledHeight = (int) Math.round(originalHeight * scalingFactor); + BufferedImage image = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.setRenderingHints(RENDERING_HINTS); + g.scale(scalingFactor, scalingFactor); + svgDocument.render(null, g); + g.dispose(); + return new ImageData[] { convertToSWT(image) }; + } else { + SWT.error(SWT.ERROR_INVALID_IMAGE); + } + return null; + } + + private ImageData convertToSWT(BufferedImage bufferedImage) { + if (bufferedImage.getColorModel() instanceof DirectColorModel) { + DirectColorModel colorModel = (DirectColorModel) bufferedImage.getColorModel(); + PaletteData palette = new PaletteData(colorModel.getRedMask(), colorModel.getGreenMask(), + colorModel.getBlueMask()); + ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), + colorModel.getPixelSize(), palette); + for (int y = 0; y < data.height; y++) { + for (int x = 0; x < data.width; x++) { + int rgb = bufferedImage.getRGB(x, y); + int pixel = palette.getPixel(new RGB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF)); + data.setPixel(x, y, pixel); + if (colorModel.hasAlpha()) { + data.setAlpha(x, y, (rgb >> 24) & 0xFF); + } + } + } + return data; + } else if (bufferedImage.getColorModel() instanceof IndexColorModel) { + IndexColorModel colorModel = (IndexColorModel) bufferedImage.getColorModel(); + int size = colorModel.getMapSize(); + byte[] reds = new byte[size]; + byte[] greens = new byte[size]; + byte[] blues = new byte[size]; + colorModel.getReds(reds); + colorModel.getGreens(greens); + colorModel.getBlues(blues); + RGB[] rgbs = new RGB[size]; + for (int i = 0; i < rgbs.length; i++) { + rgbs[i] = new RGB(reds[i] & 0xFF, greens[i] & 0xFF, blues[i] & 0xFF); + } + PaletteData palette = new PaletteData(rgbs); + ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), + colorModel.getPixelSize(), palette); + data.transparentPixel = colorModel.getTransparentPixel(); + WritableRaster raster = bufferedImage.getRaster(); + int[] pixelArray = new int[1]; + for (int y = 0; y < data.height; y++) { + for (int x = 0; x < data.width; x++) { + raster.getPixel(x, y, pixelArray); + data.setPixel(x, y, pixelArray[0]); + } + } + return data; + } else if (bufferedImage.getColorModel() instanceof ComponentColorModel) { + ComponentColorModel colorModel = (ComponentColorModel) bufferedImage.getColorModel(); + PaletteData palette = new PaletteData(0x0000FF, 0x00FF00, 0xFF0000); + ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), + colorModel.getPixelSize(), palette); + data.transparentPixel = -1; + WritableRaster raster = bufferedImage.getRaster(); + int[] pixelArray = new int[3]; + for (int y = 0; y < data.height; y++) { + for (int x = 0; x < data.width; x++) { + raster.getPixel(x, y, pixelArray); + int pixel = palette.getPixel(new RGB(pixelArray[0], pixelArray[1], pixelArray[2])); + data.setPixel(x, y, pixel); + } + } + return data; + } + return null; + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java index f86c862acd9..9229f7b96ae 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java @@ -689,10 +689,11 @@ public Image(Device device, ImageData source, ImageData mask) { */ public Image(Device device, InputStream stream) { super(device); + NSAutoreleasePool pool = null; if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); try { - init(new ImageData(stream)); + init(new ImageData(stream, DPIUtil.getDeviceZoom())); init(); } finally { if (pool != null) pool.release(); @@ -738,7 +739,7 @@ public Image(Device device, String filename) { try { if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); initNative(filename); - if (this.handle == null) init(new ImageData(filename)); + if (this.handle == null) init(new ImageData(filename, DPIUtil.getDeviceZoom())); init(); } finally { if (pool != null) pool.release(); @@ -784,7 +785,8 @@ public Image(Device device, ImageFileNameProvider imageFileNameProvider) { if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init(); try { initNative(filename); - if (this.handle == null) init(new ImageData(filename)); + //TODO: is using DPIUtil.getDeviceZoom() possible here? + if (this.handle == null) init(new ImageData(filename, DPIUtil.getDeviceZoom())); init(); String filename2x = imageFileNameProvider.getImagePath(200); if (filename2x != null) { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/ImageLoader.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/ImageLoader.java index 376d18316f0..8d6cfc3a289 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/ImageLoader.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/ImageLoader.java @@ -155,6 +155,44 @@ public ImageData[] load(InputStream stream) { return data; } +/** + * Loads an array of ImageData objects from the + * specified input stream. If the stream is a SVG File and zoom is not 0, + * this method will try to rasterize the SVG. + * + * @param stream the input stream to load the images from + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(InputStream stream)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + * + * The scaling is applied uniformly to both width and height. + * + * @return an array of ImageData objects loaded from the specified input stream + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 3.129 + */ +public ImageData[] load(InputStream stream, int zoom) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + data = FileFormat.load(stream, zoom, this); + return data; +} + /** * Loads an array of ImageData objects from the * file with the specified name. Throws an error if either @@ -183,6 +221,49 @@ public ImageData[] load(String filename) { return null; } +/** + * Loads an array of ImageData objects from the + * file with the specified name. If the filename is a SVG File and zoom is not 0, + * this method will try to rasterize the SVG. Throws an error if either + * an error occurs while loading the images, or if the images are + * not of a supported type. Returns the loaded image data array. + * + * @param filename the name of the file to load the images from + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(String)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + * + * The scaling is applied uniformly to both width and height. + * + * @return an array of ImageData objects loaded from the specified file + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 3.129 + */ +public ImageData[] load(String filename, int zoom) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return load(stream, zoom); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; +} + /** * Saves the image data in this ImageLoader to the specified stream. * The format parameter can have one of the following values: diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageData.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageData.java index c268e5caaed..a032624f27c 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageData.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageData.java @@ -354,6 +354,62 @@ public ImageData(InputStream stream) { i.delayTime); } +/** + * Constructs an ImageData loaded from the specified + * input stream. Throws an error if an error occurs while loading + * the image, or if the image has an unsupported type. Application + * code is still responsible for closing the input stream. + * + * @param stream the input stream to load the image from (must not be null) + * @param zoom the zoom factor to apply when rasterizing an SVG. + * + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(InputStream)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + * + * The scaling is applied uniformly to both width and height. + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @see ImageLoader#load(InputStream) + * @since 3.129 + */ +public ImageData(InputStream stream, int zoom) { + ImageData[] data = ImageDataLoader.load(stream, zoom); + if (data.length < 1) SWT.error(SWT.ERROR_INVALID_IMAGE); + ImageData i = data[0]; + setAllFields( + i.width, + i.height, + i.depth, + i.scanlinePad, + i.bytesPerLine, + i.data, + i.palette, + i.transparentPixel, + i.maskData, + i.maskPad, + i.alphaData, + i.alpha, + i.type, + i.x, + i.y, + i.disposalMethod, + i.delayTime); +} + /** * Constructs an ImageData loaded from a file with the * specified name. Throws an error if an error occurs loading the @@ -400,6 +456,59 @@ public ImageData(String filename) { i.delayTime); } +/** + * Constructs an ImageData loaded from a file with the + * specified name. Throws an error if an error occurs loading the + * image, or if the image has an unsupported type. + * + * @param filename the name of the file to load the image from (must not be null) + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(String)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + * + * The scaling is applied uniformly to both width and height. + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 3.129 + */ +public ImageData(String filename, int zoom) { + ImageData[] data = ImageDataLoader.load(filename, zoom); + if (data.length < 1) SWT.error(SWT.ERROR_INVALID_IMAGE); + ImageData i = data[0]; + setAllFields( + i.width, + i.height, + i.depth, + i.scanlinePad, + i.bytesPerLine, + i.data, + i.palette, + i.transparentPixel, + i.maskData, + i.maskPad, + i.alphaData, + i.alpha, + i.type, + i.x, + i.y, + i.disposalMethod, + i.delayTime); +} + /** * Prevents uninitialized instances from being created outside the package. */ 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 b1fe23d2472..5cd05692c78 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 @@ -25,8 +25,16 @@ public static ImageData[] load(InputStream stream) { return new ImageLoader().load(stream); } + public static ImageData[] load(InputStream stream, int zoom) { + return new ImageLoader().load(stream, zoom); + } + public static ImageData[] load(String filename) { return new ImageLoader().load(filename); } + public static ImageData[] load(String filename, int zoom) { + return new ImageLoader().load(filename, zoom); + } + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/SVGRasterizer.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/SVGRasterizer.java new file mode 100644 index 00000000000..db29ea35cec --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/SVGRasterizer.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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.internal; + +import java.io.*; + +import org.eclipse.swt.graphics.*; + +/** + * Defines the interface for an SVG rasterizer, responsible for converting SVG + * data into rasterized images. + * + * @since 3.129 + */ +public interface SVGRasterizer { + /** + * Rasterizes an SVG image from the provided {@code InputStream} using the specified + * zoom factor. + * + * @param stream the SVG image as an {@link InputStream}. + * @param zoom the scaling factor e.g. 200 for doubled size. This value must no be 0. + * @return the {@link ImageData} for the rasterized image, or {@code null} if + * the input is not a valid SVG file or cannot be processed. + * @throws IOException if an error occurs while reading the SVG data. + */ + public ImageData[] rasterizeSVG(InputStream stream, int zoom) throws IOException; +} \ No newline at end of file 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 943cddc9fd4..55b8c2a8a96 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 @@ -15,9 +15,11 @@ import java.io.*; +import java.util.*; import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.*; /** * Abstract factory class for loading/unloading images from files or streams @@ -33,6 +35,11 @@ public abstract class FileFormat { ImageLoader loader; int compression; + /** + * The instance of the registered {@link SVGRasterizer}. + */ + private static final SVGRasterizer RASTERIZER = ServiceLoader.load(SVGRasterizer.class).findFirst().orElse(null); + static FileFormat getFileFormat (LEDataInputStream stream, String format) throws Exception { Class clazz = Class.forName(FORMAT_PACKAGE + '.' + format + FORMAT_SUFFIX); FileFormat fileFormat = (FileFormat) clazz.getDeclaredConstructor().newInstance(); @@ -89,6 +96,70 @@ public static ImageData[] load(InputStream is, ImageLoader loader) { return fileFormat.loadFromStream(stream); } +/** + * Loads an array of ImageData objects from the specified input stream. + * Depending on the file type and zoom factor, this method either rasterizes the image + * (for SVG files) or uses the provided {@link ImageLoader} to load the image data. + * + *

If the input stream is an SVG file and the specified zoom factor is not 0, + * the method attempts to rasterize the SVG using the configured rasterizer. + * Otherwise, it delegates the loading process to the {@link FileFormat#load(InputStream, ImageLoader)} method. + * + * @param stream the input stream to load the images from. The stream cannot be null. + * If the stream does not support marking, it is wrapped in a + * {@link BufferedInputStream}. + * @param zoom the zoom factor to apply when rasterizing an SVG. A value of 0 uses + * the standard image loading method. A positive value specifies a scaling + * factor for the output image: + *

+ * The scaling factor is applied uniformly to both width and height. + * @param loader the {@link ImageLoader} instance used to load the image if the + * stream is not an SVG or if the zoom factor is 0. + * + * @return an array of ImageData objects loaded from the specified + * input stream, or null if an error occurs during loading. + * + * @exception IllegalArgumentException if the input stream is null. + * @exception SWTException if an error occurs while loading the image data. + * + * @since 3.129 + */ +public static ImageData[] load(InputStream stream, int zoom, ImageLoader loader) { + if (stream == null) { + throw new IllegalArgumentException("InputStream cannot be null"); + } + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + try { + if (RASTERIZER != null && zoom != 0 && isSVGFile(stream)) { + return RASTERIZER.rasterizeSVG(stream, zoom); + } else { + return load(stream, loader); + } + } catch (IOException e) { + SWT.error(SWT.ERROR_INVALID_IMAGE, e); + } + return null; +} + +private static boolean isSVGFile(InputStream stream) throws IOException { + if (stream == null) { + throw new IllegalArgumentException("InputStream cannot be null"); + } + stream.mark(1); + try { + int firstByte = stream.read(); + return firstByte == '<'; + } finally { + stream.reset(); + } +} + /** * Write the device independent image array stored in the specified loader * to the specified output stream using the specified file format. diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java index e518cfda153..5dbb3a5a3cb 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java @@ -530,8 +530,8 @@ public Image(Device device, ImageData source, ImageData mask) { */ public Image(Device device, InputStream stream) { super(device); - ImageData data = new ImageData(stream); currentDeviceZoom = DPIUtil.getDeviceZoom(); + ImageData data = new ImageData(stream, currentDeviceZoom); data = DPIUtil.autoScaleUp (device, data); init(data); init(); @@ -572,9 +572,8 @@ public Image(Device device, InputStream stream) { public Image(Device device, String filename) { super(device); if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); - - ImageData data = new ImageData(filename); currentDeviceZoom = DPIUtil.getDeviceZoom(); + ImageData data = new ImageData(filename, currentDeviceZoom); data = DPIUtil.autoScaleUp (device, data); init(data); init(); @@ -618,11 +617,11 @@ public Image(Device device, ImageFileNameProvider imageFileNameProvider) { initNative (filename.element()); if (this.surface == 0) { - ImageData data = new ImageData(filename.element()); + ImageData data = new ImageData(filename.element(), currentDeviceZoom); init(data); } } else { - ImageData imageData = new ImageData (filename.element()); + ImageData imageData = new ImageData (filename.element(), currentDeviceZoom); ImageData resizedData = DPIUtil.autoScaleImageData (device, imageData, filename.zoom()); init(resizedData); } @@ -731,7 +730,7 @@ boolean refreshImageForZoom () { destroy (); initNative(filename.element()); if (this.surface == 0) { - ImageData data = new ImageData(filename.element()); + ImageData data = new ImageData(filename.element(), currentDeviceZoom); init(data); } init (); @@ -739,7 +738,7 @@ boolean refreshImageForZoom () { } else { /* Release current native resources */ destroy (); - ImageData imageData = new ImageData (filename.element()); + ImageData imageData = new ImageData (filename.element(), currentDeviceZoom); ImageData resizedData = DPIUtil.autoScaleImageData (device, imageData, filename.zoom()); init(resizedData); init (); @@ -1160,7 +1159,7 @@ public ImageData getImageData (int zoom) { return DPIUtil.scaleImageData (device, data.element(), zoom, data.zoom()); } else if (imageFileNameProvider != null) { ElementAtZoom fileName = DPIUtil.validateAndGetImagePathAtZoom (imageFileNameProvider, zoom); - return DPIUtil.scaleImageData (device, new ImageData (fileName.element()), zoom, fileName.zoom()); + return DPIUtil.scaleImageData (device, new ImageData (fileName.element(), currentDeviceZoom), zoom, fileName.zoom()); } else if (imageGcDrawer != null) { return drawWithImageGcDrawer(width, height, zoom); } else { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/ImageLoader.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/ImageLoader.java index 67336de12bc..d8a0da76911 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/ImageLoader.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/ImageLoader.java @@ -166,6 +166,44 @@ public ImageData[] load(InputStream stream) { return imgDataArray; } +/** + * Loads an array of ImageData objects from the + * specified input stream. If the stream is a SVG File and zoom is not 0, + * this method will try to rasterize the SVG. + * + * @param stream the input stream to load the images from + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(InputStream stream)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + *
    + *
  • A value of 100 maintains the original size of the SVG when rasterized.
  • + *
  • A value of 200 doubles the size of the rasterized image.
  • + *
  • A value of 50 reduces the size of the rasterized image to half.
  • + *
+ * The scaling is applied uniformly to both width and height. + * + * @return an array of ImageData objects loaded from the specified input stream + * + * @exception IllegalArgumentException
    + *
  • ERROR_NULL_ARGUMENT - if the stream is null
  • + *
+ * @exception SWTException
    + *
  • ERROR_IO - if an IO error occurs while reading from the stream
  • + *
  • ERROR_INVALID_IMAGE - if the image stream contains invalid data
  • + *
  • ERROR_UNSUPPORTED_FORMAT - if the image stream contains an unrecognized format
  • + *
+ * + * @since 3.129 + */ +public ImageData[] load(InputStream stream, int zoom) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + data = FileFormat.load(stream, zoom, this); + return data; +} + /** * Return true if the image is an interlaced PNG file. * This is used to check whether ImageLoaderEvent should be fired when loading images. @@ -299,6 +337,49 @@ public ImageData[] load(String filename) { return null; } +/** + * Loads an array of ImageData objects from the + * file with the specified name. If the filename is a SVG File and zoom is not 0, + * this method will try to rasterize the SVG. Throws an error if either + * an error occurs while loading the images, or if the images are + * not of a supported type. Returns the loaded image data array. + * + * @param filename the name of the file to load the images from + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(String)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + *
    + *
  • A value of 100 maintains the original size of the SVG when rasterized.
  • + *
  • A value of 200 doubles the size of the rasterized image.
  • + *
  • A value of 50 reduces the size of the rasterized image to half.
  • + *
+ * The scaling is applied uniformly to both width and height. + * + * @return an array of ImageData objects loaded from the specified file + * + * @exception IllegalArgumentException
    + *
  • ERROR_NULL_ARGUMENT - if the file name is null
  • + *
+ * @exception SWTException
    + *
  • ERROR_IO - if an IO error occurs while reading from the file
  • + *
  • ERROR_INVALID_IMAGE - if the image file contains invalid data
  • + *
  • ERROR_UNSUPPORTED_FORMAT - if the image file contains an unrecognized format
  • + *
+ * + * @since 3.129 + */ +public ImageData[] load(String filename, int zoom) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return load(stream, zoom); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; +} + /** * Load GdkPixbuf directly using gdk_pixbuf_new_from_file, * without FileInputStream. 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 9f9181909ca..448974252fa 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 @@ -471,7 +471,7 @@ public Image(Device device, ImageData source, ImageData mask) { public Image (Device device, InputStream stream) { super(device); initialNativeZoom = DPIUtil.getNativeDeviceZoom(); - ImageData data = DPIUtil.autoScaleUp(device, new ElementAtZoom<>(new ImageData (stream), 100)); + ImageData data = DPIUtil.autoScaleUp(device, new ElementAtZoom<>(new ImageData (stream, getZoom()), 100)); init(data, getZoom()); init(); this.device.registerResourceWithZoomSupport(this); @@ -513,7 +513,7 @@ public Image (Device device, String filename) { super(device); if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); initialNativeZoom = DPIUtil.getNativeDeviceZoom(); - ImageData data = DPIUtil.autoScaleUp(device, new ElementAtZoom<>(new ImageData (filename), 100)); + ImageData data = DPIUtil.autoScaleUp(device, new ElementAtZoom<>(new ImageData (filename, getZoom()), 100)); init(data, getZoom()); init(); this.device.registerResourceWithZoomSupport(this); @@ -556,10 +556,10 @@ public Image(Device device, ImageFileNameProvider imageFileNameProvider) { if (fileName.zoom() == getZoom()) { ImageHandle imageMetadata = initNative (fileName.element(), getZoom()); if (imageMetadata == null) { - init(new ImageData (fileName.element()), getZoom()); + init(new ImageData (fileName.element(), getZoom()), getZoom()); } } else { - ImageData resizedData = DPIUtil.autoScaleImageData (device, new ImageData (fileName.element()), fileName.zoom()); + ImageData resizedData = DPIUtil.autoScaleImageData (device, new ImageData (fileName.element(), getZoom()), fileName.zoom()); init(resizedData, getZoom()); } init(); @@ -2130,13 +2130,13 @@ protected Rectangle getBounds(int zoom) { @Override ImageData getImageData(int zoom) { ElementAtZoom fileName = DPIUtil.validateAndGetImagePathAtZoom (provider, zoom); - return DPIUtil.scaleImageData (device, new ImageData (fileName.element()), zoom, fileName.zoom()); + return DPIUtil.scaleImageData (device, new ImageData (fileName.element(), zoom), zoom, fileName.zoom()); } @Override ImageHandle getImageMetadata(int zoom) { ElementAtZoom imageCandidate = DPIUtil.validateAndGetImagePathAtZoom (provider, zoom); - ImageData imageData = new ImageData (imageCandidate.element()); + ImageData imageData = new ImageData (imageCandidate.element(), zoom); if (imageCandidate.zoom() == zoom) { /* Release current native resources */ ImageHandle imageMetadata = initNative(imageCandidate.element(), zoom); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/ImageLoader.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/ImageLoader.java index da78f52d244..088489e9774 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/ImageLoader.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/ImageLoader.java @@ -155,6 +155,44 @@ public ImageData[] load(InputStream stream) { return data; } +/** + * Loads an array of ImageData objects from the + * specified input stream. If the stream is a SVG File and zoom is not 0, + * this method will try to rasterize the SVG. + * + * @param stream the input stream to load the images from + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(InputStream stream)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + *
    + *
  • A value of 100 maintains the original size of the SVG when rasterized.
  • + *
  • A value of 200 doubles the size of the rasterized image.
  • + *
  • A value of 50 reduces the size of the rasterized image to half.
  • + *
+ * The scaling is applied uniformly to both width and height. + * + * @return an array of ImageData objects loaded from the specified input stream + * + * @exception IllegalArgumentException
    + *
  • ERROR_NULL_ARGUMENT - if the stream is null
  • + *
+ * @exception SWTException
    + *
  • ERROR_IO - if an IO error occurs while reading from the stream
  • + *
  • ERROR_INVALID_IMAGE - if the image stream contains invalid data
  • + *
  • ERROR_UNSUPPORTED_FORMAT - if the image stream contains an unrecognized format
  • + *
+ * + * @since 3.129 + */ +public ImageData[] load(InputStream stream, int zoom) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + data = FileFormat.load(stream, zoom, this); + return data; +} + /** * Loads an array of ImageData objects from the * file with the specified name. Throws an error if either @@ -183,6 +221,49 @@ public ImageData[] load(String filename) { return null; } +/** + * Loads an array of ImageData objects from the + * file with the specified name. If the filename is a SVG File and zoom is not 0, + * this method will try to rasterize the SVG. Throws an error if either + * an error occurs while loading the images, or if the images are + * not of a supported type. Returns the loaded image data array. + * + * @param filename the name of the file to load the images from + * @param zoom the zoom factor to apply when rasterizing a SVG. + * A value of 0 means that the standard method for loading should be used. + * This case is equivalent to calling {@link ImageLoader#load(String)}. + * + * A value above 0 specifies a scaling factor for the output image. For example: + *
    + *
  • A value of 100 maintains the original size of the SVG when rasterized.
  • + *
  • A value of 200 doubles the size of the rasterized image.
  • + *
  • A value of 50 reduces the size of the rasterized image to half.
  • + *
+ * The scaling is applied uniformly to both width and height. + * + * @return an array of ImageData objects loaded from the specified file + * + * @exception IllegalArgumentException
    + *
  • ERROR_NULL_ARGUMENT - if the file name is null
  • + *
+ * @exception SWTException
    + *
  • ERROR_IO - if an IO error occurs while reading from the file
  • + *
  • ERROR_INVALID_IMAGE - if the image file contains invalid data
  • + *
  • ERROR_UNSUPPORTED_FORMAT - if the image file contains an unrecognized format
  • + *
+ * + * @since 3.129 + */ +public ImageData[] load(String filename, int zoom) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return load(stream, zoom); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; +} + /** * Saves the image data in this ImageLoader to the specified stream. * The format parameter can have one of the following values: