diff --git a/binaries/org.eclipse.swt.win32.win32.x86_64/META-INF/MANIFEST.MF b/binaries/org.eclipse.swt.win32.win32.x86_64/META-INF/MANIFEST.MF index 72da35e9326..d369bfb5cae 100644 --- a/binaries/org.eclipse.swt.win32.win32.x86_64/META-INF/MANIFEST.MF +++ b/binaries/org.eclipse.swt.win32.win32.x86_64/META-INF/MANIFEST.MF @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.128.0,4.0.0)" Bundle-Name: %fragmentName Bundle-Vendor: %providerName Bundle-SymbolicName: org.eclipse.swt.win32.win32.x86_64; singleton:=true -Bundle-Version: 3.129.0.qualifier +Bundle-Version: 4.0.0.qualifier Bundle-ManifestVersion: 2 Bundle-Localization: fragment Export-Package: 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.graphics.SVGRasterizer b/bundles/org.eclipse.swt.svg/META-INF/services/org.eclipse.swt.graphics.SVGRasterizer new file mode 100644 index 00000000000..bd95b6eea89 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/META-INF/services/org.eclipse.swt.graphics.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..8c7f1dadf00 --- /dev/null +++ b/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/JSVGRasterizer.java @@ -0,0 +1,278 @@ +/******************************************************************************* + * 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 javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.eclipse.swt.graphics.SVGRasterizer; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +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 ISVGRasterizer} interface. + * + * @since 1.0.0 + */ +public class JSVGRasterizer implements SVGRasterizer { + + private SVGLoader 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, float scalingFactor) throws IOException { + if (stream == null) { + throw new IllegalArgumentException("InputStream cannot be null"); + } + if(svgLoader == null) { + svgLoader = new SVGLoader(); + } + return rasterize(stream, scalingFactor); + } + + @Override + public ImageData rasterizeDisabledSVG(InputStream stream, float scalingFactor) throws IOException { + if(svgLoader == null) { + svgLoader = new SVGLoader(); + } + InputStream disabledStream = applyDisabledLook(stream); + return rasterize(disabledStream, scalingFactor); + } + + @Override + public ImageData rasterizeGraySVG(InputStream stream, float scalingFactor) throws IOException { + if(svgLoader == null) { + svgLoader = new SVGLoader(); + } + InputStream disabledStream = applyGrayLook(stream); + return rasterize(disabledStream, scalingFactor); + } + + private ImageData rasterize(InputStream stream, float scalingFactor) throws IOException { + SVGDocument svgDocument = null; + stream.mark(Integer.MAX_VALUE); + InputStream nonClosingStream = new FilterInputStream(stream) { + @Override + public void close() throws IOException { + // Do nothing to prevent closing the underlying stream + } + }; + svgDocument = svgLoader.load(nonClosingStream, null, LoaderContext.createDefault()); + stream.reset(); + if (svgDocument != null) { + 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 convertToSWT(image); + } + return null; + } + + private static InputStream applyDisabledLook(InputStream svgInputStream) throws IOException { + Document svgDocument = parseSVG(svgInputStream); + addDisabledFilter(svgDocument); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + writeSVG(svgDocument, outputStream); + return new ByteArrayInputStream(outputStream.toByteArray()); + } + } + + private static InputStream applyGrayLook(InputStream svgInputStream) throws IOException { + Document svgDocument = parseSVG(svgInputStream); + addGrayFilter(svgDocument); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + writeSVG(svgDocument, outputStream); + return new ByteArrayInputStream(outputStream.toByteArray()); + } + } + + private static Document parseSVG(InputStream inputStream) throws IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + return builder.parse(inputStream); + } catch (SAXException | IOException | ParserConfigurationException e) { + throw new IOException(e.getMessage()); + } + } + + private static void addDisabledFilter(Document document) { + addFilter(document, 0.64f, 0.4f); + } + + private static void addGrayFilter(Document document) { + addFilter(document, 0.64f, 0.1f); + } + + private static void addFilter(Document document, float slope, float intercept) { + Element defs = (Element) document.getElementsByTagName("defs").item(0); + if (defs == null) { + defs = document.createElement("defs"); + document.getDocumentElement().appendChild(defs); + } + + Element filter = document.createElement("filter"); + filter.setAttribute("id", "customizedLook"); + + Element colorMatrix = document.createElement("feColorMatrix"); + colorMatrix.setAttribute("type", "saturate"); + colorMatrix.setAttribute("values", "0"); + filter.appendChild(colorMatrix); + + Element componentTransfer = document.createElement("feComponentTransfer"); + for (String channel : new String[] { "R", "G", "B" }) { + Element func = document.createElement("feFunc" + channel); + func.setAttribute("type", "linear"); + func.setAttribute("slope", Float.toString(slope)); + func.setAttribute("intercept", Float.toString(intercept)); + componentTransfer.appendChild(func); + } + filter.appendChild(componentTransfer); + defs.appendChild(filter); + document.getDocumentElement().setAttribute("filter", "url(#customizedLook)"); + } + + private static void writeSVG(Document document, OutputStream outputStream) throws IOException { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer; + try { + transformer = transformerFactory.newTransformer(); + transformer.transform(new DOMSource(document), new StreamResult(outputStream)); + } catch (TransformerException e) { + throw new IOException(e.getMessage()); + } + } + + 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(); + //ASSUMES: 3 BYTE BGR IMAGE TYPE + PaletteData palette = new PaletteData(0x0000FF, 0x00FF00,0xFF0000); + ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(), + colorModel.getPixelSize(), palette); + //This is valid because we are using a 3-byte Data model with no transparent pixels + 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; + } + + public boolean isSVGFile(InputStream stream) throws IOException { + if (stream == null) { + throw new IllegalArgumentException("InputStream cannot be null"); + } + stream.mark(Integer.MAX_VALUE); + try { + int firstByte = stream.read(); + return firstByte == '<'; + } finally { + stream.reset(); + } + } +} 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 5b063ab2e78..f414eadf7c0 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 @@ -14,9 +14,12 @@ package org.eclipse.swt.graphics; +import java.awt.image.BufferedImage; import java.io.*; import java.util.*; +import javax.imageio.ImageIO; + import org.eclipse.swt.*; import org.eclipse.swt.internal.image.*; @@ -149,10 +152,72 @@ void reset() { * */ public ImageData[] load(InputStream stream) { + return loadDefault(stream); +} + +/** + * 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. + * 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 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. + * @return an array of ImageData objects loaded from the specified input stream + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 4.0 + */ +public ImageData[] load(InputStream stream, int zoom, int flag) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + ImageData rasterizedData = null; + SVGRasterizer rasterizer = SVGRasterizerRegistry.getRasterizer(); + if (rasterizer != null && zoom != 0) { + try { + if (rasterizer.isSVGFile(stream)) { + float scalingFactor = zoom / 100.0f; + switch(flag) { + case SWT.IMAGE_DISABLE: + rasterizedData = rasterizer.rasterizeDisabledSVG(stream, scalingFactor); + break; + case SWT.IMAGE_GRAY: + rasterizedData = rasterizer.rasterizeGraySVG(stream, scalingFactor); + break; + case SWT.IMAGE_COPY: + rasterizedData = rasterizer.rasterizeSVG(stream, scalingFactor); + break; + } + if (rasterizedData != null) { + data = new ImageData[]{rasterizedData}; + return data; + } + } + } catch (IOException e) { + //ignore. + } + } + return loadDefault(stream); +} + +private ImageData[] loadDefault(InputStream stream) { if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); reset(); data = FileFormat.load(stream, this); - return data; + return data; } /** @@ -175,18 +240,43 @@ public ImageData[] load(InputStream stream) { */ public ImageData[] load(String filename) { if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); - InputStream stream = null; - try { - stream = new FileInputStream(filename); - return load(stream); + try (InputStream stream = new FileInputStream(filename)) { + return loadDefault(stream); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + 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. + * @return an array of ImageData objects loaded from the specified file + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 4.0 + */ +public ImageData[] load(String filename, int zoom, int flag) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return load(stream, zoom, flag); } catch (IOException e) { SWT.error(SWT.ERROR_IO, e); - } finally { - try { - if (stream != null) stream.close(); - } catch (IOException e) { - // Ignore error - } } return null; } 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..842c978c63a 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 @@ -331,7 +331,39 @@ scanlinePad, checkData(data), 0, null, * @see ImageLoader#load(InputStream) */ public ImageData(InputStream stream) { - ImageData[] data = ImageDataLoader.load(stream); + this(stream, 0, SWT.IMAGE_COPY); +} + +/** + * 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. + *

+ * This constructor is provided for convenience when loading a single + * image only. If the stream contains multiple images, only the first + * one will be loaded. To load multiple images, use + * ImageLoader.load(). + *

+ * + * @param stream the input stream 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. + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @see ImageLoader#load(InputStream) + * @since 4.0 + */ +public ImageData(InputStream stream, int zoom, int flag) { + ImageData[] data = ImageDataLoader.load(stream, zoom, flag); if (data.length < 1) SWT.error(SWT.ERROR_INVALID_IMAGE); ImageData i = data[0]; setAllFields( @@ -377,7 +409,36 @@ public ImageData(InputStream stream) { * */ public ImageData(String filename) { - ImageData[] data = ImageDataLoader.load(filename); + this(filename, 0, SWT.IMAGE_COPY); +} + +/** + * 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. + *

+ * This constructor is provided for convenience when loading a single + * image only. If the file contains multiple images, only the first + * one will be loaded. To load multiple images, use + * ImageLoader.load(). + *

+ * + * @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. + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 4.0 + */ +public ImageData(String filename, int zoom, int flag) { + ImageData[] data = ImageDataLoader.load(filename, zoom, flag); if (data.length < 1) SWT.error(SWT.ERROR_INVALID_IMAGE); ImageData i = data[0]; setAllFields( 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..ebf1979e8f4 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, int flag) { + return new ImageLoader().load(stream, zoom, flag); + } + public static ImageData[] load(String filename) { return new ImageLoader().load(filename); } + public static ImageData[] load(String filename, int zoom, int flag) { + return new ImageLoader().load(filename, zoom, flag); + } + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataProvider.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataProvider.java index bfa0ec70a73..bf1c5a9f62b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataProvider.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageDataProvider.java @@ -25,7 +25,6 @@ */ public interface ImageDataProvider { - /** * Returns the image data for the given zoom level. *

@@ -43,4 +42,17 @@ public interface ImageDataProvider { */ ImageData getImageData (int zoom); + /** + * @since 4.0 + */ + default ImageData getCustomizedImageData(int zoom, int flag) { + throw new UnsupportedOperationException(); + } + + /** + * @since 4.0 + */ + default boolean supportsRasterizationFlag(int flag) { + return false; + } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizer.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizer.java new file mode 100644 index 00000000000..4774a2f2f0a --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizer.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * 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.io.*; + +/** + * Defines the interface for an SVG rasterizer, responsible for converting SVG + * data into rasterized images. + * + * @since 4.0 + */ +public interface SVGRasterizer { + + /** + * Rasterizes an SVG image from the provided byte array, using the specified + * zoom factor. + * + * @param stream the SVG image as an {@link InputStream}. + * @param scalingFactor the scaling ratio e.g. 2.0 for doubled size. + * @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, float scalingFactor) throws IOException; + + public ImageData rasterizeDisabledSVG(InputStream stream, float scalingFactor) throws IOException; + + public ImageData rasterizeGraySVG(InputStream stream, float scalingFactor) throws IOException; + + /** + * Determines whether the given {@link InputStream} contains a SVG file. + * + * @param stream the input stream to check. + * @return {@code true} if the input stream contains SVG content; {@code false} + * otherwise. + * @throws IOException if an error occurs while reading the stream. + * @throws IllegalArgumentException if the input stream is {@code null}. + */ + public boolean isSVGFile(InputStream stream) throws IOException; + //TODO: use optional above instead? +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizerRegistry.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizerRegistry.java new file mode 100644 index 00000000000..e088d1ccdd0 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizerRegistry.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * 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.*; + +/** + * A registry for managing the instance of an {@link SVGRasterizer} implementation. + * This allows for the registration and retrieval of a single rasterizer instance. + * + * @since 4.0 + */ +class SVGRasterizerRegistry { + + /** + * The instance of the registered {@link SVGRasterizer}. + */ + private static final SVGRasterizer RASTERIZER = ServiceLoader.load(SVGRasterizer.class).findFirst().orElse(null); + + /** + * Retrieves the currently registered {@link SVGRasterizer} implementation. + * + * @return the registered {@link SVGRasterizer}, or {@code null} if no implementation + * has been registered. + */ + public static SVGRasterizer getRasterizer() { + return RASTERIZER; + } +} \ No newline at end of file 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 89b97cd86e6..c7d74c0c52a 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 @@ -15,10 +15,13 @@ package org.eclipse.swt.graphics; +import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import java.util.List; +import javax.imageio.ImageIO; + import org.eclipse.swt.*; import org.eclipse.swt.internal.*; import org.eclipse.swt.internal.gtk.*; @@ -159,11 +162,72 @@ void reset() { * */ public ImageData[] load(InputStream stream) { + return loadDefault(stream); +} + +/** + * 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. + * 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 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. + * @return an array of ImageData objects loaded from the specified input stream + * + * @exception IllegalArgumentException

+ * @exception SWTException + * + * @since 4.0 + */ +public ImageData[] load(InputStream stream, int zoom, int flag) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + ImageData rasterizedData = null; + SVGRasterizer rasterizer = SVGRasterizerRegistry.getRasterizer(); + if (rasterizer != null && zoom != 0) { + try { + if (rasterizer.isSVGFile(stream)) { + float scalingFactor = zoom / 100.0f; + switch(flag) { + case SWT.IMAGE_DISABLE: + rasterizedData = rasterizer.rasterizeDisabledSVG(stream, scalingFactor); + break; + case SWT.IMAGE_GRAY: + rasterizedData = rasterizer.rasterizeGraySVG(stream, scalingFactor); + break; + case SWT.IMAGE_COPY: + rasterizedData = rasterizer.rasterizeSVG(stream, scalingFactor); + break; + } + if (rasterizedData != null) { + data = new ImageData[]{rasterizedData}; + return data; + } + } + } catch (IOException e) { + //ignore. + } + } + return loadDefault(stream); +} + +private ImageData[] loadDefault(InputStream stream) { if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); reset(); - ImageData [] imgDataArray = getImageDataArrayFromStream(stream); - data = imgDataArray; - return imgDataArray; + data = getImageDataArrayFromStream(stream); + return data; } /** @@ -294,18 +358,43 @@ boolean isInterlacedPNG(byte [] imageAsByteArray) { */ public ImageData[] load(String filename) { if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); - InputStream stream = null; - try { - stream = new FileInputStream(filename); - return load(stream); + try (InputStream stream = new FileInputStream(filename)) { + return loadDefault(stream); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + 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. + * @return an array of ImageData objects loaded from the specified file + * + * @exception IllegalArgumentException + * @exception SWTException + * + * @since 4.0 + */ +public ImageData[] load(String filename, int zoom, int flag) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return load(stream, zoom, flag); } catch (IOException e) { SWT.error(SWT.ERROR_IO, e); - } finally { - try { - if (stream != null) stream.close(); - } catch (IOException e) { - // Ignore error - } } return null; } 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 c4013a30a08..80ca08147c4 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 @@ -235,6 +235,14 @@ public Image(Device device, Image srcImage, int flag) { long srcImageHandle = win32_getHandle(srcImage, getZoom()); switch (flag) { case SWT.IMAGE_COPY: { + ImageData data = null; + Image newImage = createWithSVG(device, flag); + if(newImage != null) { + data = newImage.getImageData(newImage.getZoom()); + init(data, getZoom()); + newImage.dispose(); + break; + } switch (type) { case SWT.BITMAP: /* Get the HDC for the device */ @@ -270,13 +278,29 @@ public Image(Device device, Image srcImage, int flag) { break; } case SWT.IMAGE_DISABLE: { - ImageData data = srcImage.getImageData(srcImage.getZoom()); + ImageData data = null; + Image disabledImage = createWithSVG(device, flag); + if(disabledImage != null) { + data = disabledImage.getImageData(disabledImage.getZoom()); + init(data, getZoom()); + disabledImage.dispose(); + break; + } + data = srcImage.getImageData(srcImage.getZoom()); ImageData newData = applyDisableImageData(data, rect.height, rect.width); init (newData, getZoom()); break; } case SWT.IMAGE_GRAY: { - ImageData data = srcImage.getImageData(srcImage.getZoom()); + ImageData data = null; + Image grayImage = createWithSVG(device, flag); + if(grayImage != null) { + data = grayImage.getImageData(grayImage.getZoom()); + init(data, getZoom()); + grayImage.dispose(); + break; + } + data = srcImage.getImageData(srcImage.getZoom()); ImageData newData = applyGrayImageData(data, rect.height, rect.width); init (newData, getZoom()); break; @@ -288,6 +312,26 @@ public Image(Device device, Image srcImage, int flag) { this.device.registerResourceWithZoomSupport(this); } +private Image createWithSVG(Device device, int flag) { + Image customizedImage = null; + if (imageFileNameProvider != null) { + ElementAtZoom fileName = DPIUtil.validateAndGetImagePathAtZoom (imageFileNameProvider, getZoom()); + try (InputStream stream = new BufferedInputStream(new FileInputStream(fileName.element()))){ + SVGRasterizer rasterizer = SVGRasterizerRegistry.getRasterizer(); + if(rasterizer != null && rasterizer.isSVGFile(stream)) { + customizedImage = new Image(device, imageFileNameProvider, flag); + } + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + } else if (imageDataProvider != null) { + if(imageDataProvider.supportsRasterizationFlag(flag)) { + customizedImage = new Image(device, imageDataProvider, flag); + } + } + return customizedImage; +} + /** * Constructs an empty instance of this class with the * width and height of the specified rectangle. The result @@ -468,7 +512,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(), SWT.IMAGE_COPY), 100)); init(data, getZoom()); init(); this.device.registerResourceWithZoomSupport(this); @@ -510,7 +554,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(), SWT.IMAGE_COPY), 100)); init(data, getZoom()); init(); this.device.registerResourceWithZoomSupport(this); @@ -546,6 +590,13 @@ public Image (Device device, String filename) { * @since 3.104 */ public Image(Device device, ImageFileNameProvider imageFileNameProvider) { + this(device, imageFileNameProvider, SWT.IMAGE_COPY); +} + +/** + * @since 4.0 + */ +public Image(Device device, ImageFileNameProvider imageFileNameProvider, int flag) { super(device); this.imageFileNameProvider = imageFileNameProvider; initialNativeZoom = DPIUtil.getNativeDeviceZoom(); @@ -553,10 +604,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(), flag), getZoom()); } } else { - ImageData resizedData = DPIUtil.autoScaleImageData (device, new ImageData (fileName.element()), fileName.zoom()); + ImageData resizedData = DPIUtil.autoScaleImageData (device, new ImageData (fileName.element(), getZoom(), flag), fileName.zoom()); init(resizedData, getZoom()); } init(); @@ -603,6 +654,16 @@ public Image(Device device, ImageDataProvider imageDataProvider) { this.device.registerResourceWithZoomSupport(this); } +private Image(Device device, ImageDataProvider imageDataProvider, int flag) { + super(device); + this.imageDataProvider = imageDataProvider; + initialNativeZoom = DPIUtil.getNativeDeviceZoom(); + ImageData data = imageDataProvider.getCustomizedImageData(getZoom(), flag); + init (data, getZoom()); + init(); + this.device.registerResourceWithZoomSupport(this); +} + private ImageData adaptImageDataIfDisabledOrGray(ImageData data) { ImageData returnImageData = null; switch (this.styleFlag) { @@ -753,7 +814,7 @@ private ImageHandle getImageMetadata(int zoom) { if (imageFileNameProvider != null) { ElementAtZoom imageCandidate = DPIUtil.validateAndGetImagePathAtZoom (imageFileNameProvider, zoom); - ImageData imageData = new ImageData (imageCandidate.element()); + ImageData imageData = new ImageData (imageCandidate.element(), zoom, SWT.IMAGE_COPY); if (imageCandidate.zoom() == zoom) { /* Release current native resources */ ImageHandle imageMetadata = initNative(imageCandidate.element(), zoom); @@ -1389,7 +1450,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(), zoom, SWT.IMAGE_COPY), zoom, fileName.zoom()); } // if a GC is initialized with an Image (memGC != null), the image data must not be resized, because it would 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 a8e4c2f684c..b1b8780b419 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 @@ -149,6 +149,68 @@ void reset() { * */ public ImageData[] load(InputStream stream) { + return loadDefault(stream); +} + +/** + * 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. + * 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 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. + * @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 4.0 + */ +public ImageData[] load(InputStream stream, int zoom, int flag) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + ImageData rasterizedData = null; + SVGRasterizer rasterizer = SVGRasterizerRegistry.getRasterizer(); + if (rasterizer != null && zoom != 0) { + try { + if (rasterizer.isSVGFile(stream)) { + float scalingFactor = zoom / 100.0f; + switch(flag) { + case SWT.IMAGE_DISABLE: + rasterizedData = rasterizer.rasterizeDisabledSVG(stream, scalingFactor); + break; + case SWT.IMAGE_GRAY: + rasterizedData = rasterizer.rasterizeGraySVG(stream, scalingFactor); + break; + case SWT.IMAGE_COPY: + rasterizedData = rasterizer.rasterizeSVG(stream, scalingFactor); + break; + } + if (rasterizedData != null) { + data = new ImageData[]{rasterizedData}; + return data; + } + } + } catch (IOException e) { + //ignore. + } + } + return loadDefault(stream); +} + +private ImageData[] loadDefault(InputStream stream) { if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); reset(); data = FileFormat.load(stream, this); @@ -176,7 +238,40 @@ public ImageData[] load(InputStream stream) { public ImageData[] load(String filename) { if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); try (InputStream stream = new FileInputStream(filename)) { - return load(stream); + return loadDefault(stream); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + 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. + * @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 4.0 + */ +public ImageData[] load(String filename, int zoom, int flag) { + if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + try (InputStream stream = new FileInputStream(filename)) { + return load(stream, zoom, flag); } catch (IOException e) { SWT.error(SWT.ERROR_IO, e); }