diff --git a/bundles/org.eclipse.swt.svg/.classpath b/bundles/org.eclipse.swt.svg/.classpath new file mode 100644 index 0000000000..d16b87e8ee --- /dev/null +++ b/bundles/org.eclipse.swt.svg/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/bundles/org.eclipse.swt.svg/.project b/bundles/org.eclipse.swt.svg/.project new file mode 100644 index 0000000000..587ce542c7 --- /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 0000000000..99f26c0203 --- /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 0000000000..62ef3488cc --- /dev/null +++ b/bundles/org.eclipse.swt.svg/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +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 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 0000000000..82af1dad47 --- /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 +Export-Package: org.eclipse.swt.svg +Import-Package: org.eclipse.swt.graphics +Bundle-ClassPath: ., libs/jsvg-1.6.1.jar diff --git a/bundles/org.eclipse.swt.svg/build.properties b/bundles/org.eclipse.swt.svg/build.properties new file mode 100644 index 0000000000..3c7f762fff --- /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 0000000000..3095291a85 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/SVGRasterizer.java b/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/SVGRasterizer.java new file mode 100644 index 0000000000..555bc02a6d --- /dev/null +++ b/bundles/org.eclipse.swt.svg/src/org/eclipse/swt/svg/SVGRasterizer.java @@ -0,0 +1,69 @@ +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.graphics.ISVGRasterizer; +import org.eclipse.swt.graphics.SVGRasterizerRegistry; +import org.eclipse.swt.graphics.SVGUtil; + +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 SVGRasterizer implements ISVGRasterizer { + + /** + * Initializes the SVG rasterizer by registering an instance of this rasterizer + * with the {@link SVGRasterizerRegistry}. + */ + public static void intializeSVGRasterizer() { + SVGRasterizerRegistry.register(new SVGRasterizer()); + } + + 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 BufferedImage rasterizeSVG(byte[] bytes, float scalingFactor) throws IOException { + SVGLoader loader = new SVGLoader(); + SVGDocument svgDocument = null; + if (SVGUtil.isSVGFile(bytes)) { + try (InputStream stream = new ByteArrayInputStream(bytes)) { + svgDocument = loader.load(stream, null, LoaderContext.createDefault()); + } + 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 image; + } + } + return null; + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ISVGRasterizer.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ISVGRasterizer.java new file mode 100644 index 0000000000..117a840fdf --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ISVGRasterizer.java @@ -0,0 +1,27 @@ +package org.eclipse.swt.graphics; + +import java.awt.image.*; +import java.io.*; + +/** + * Defines the interface for an SVG rasterizer, responsible for converting SVG + * data into rasterized images. + * + * @since 3.129 + */ +public interface ISVGRasterizer { + + /** + * Rasterizes an SVG image from the provided byte array, using the specified + * zoom factor. + * + * @param bytes the SVG image as a byte array. + * @param scalingFactor the scaling ratio e.g. 2.0 for doubled size. + * @return a {@link BufferedImage} containing 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 BufferedImage rasterizeSVG(byte[] bytes, float scalingFactor) throws IOException; + +} 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 c268e5caae..a0d03843db 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); +} + +/** + * 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 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( @@ -377,7 +409,36 @@ public ImageData(InputStream stream) { * */ public ImageData(String filename) { - ImageData[] data = ImageDataLoader.load(filename); + this(filename, 0); +} + +/** + * 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 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( 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 b1fe23d247..9c9da788bf 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(String filename) { + 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/graphics/SVGRasterizerRegistry.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizerRegistry.java new file mode 100644 index 0000000000..d1fd380927 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGRasterizerRegistry.java @@ -0,0 +1,38 @@ +package org.eclipse.swt.graphics; + +/** + * A registry for managing the instance of an {@link ISVGRasterizer} implementation. + * This allows for the registration and retrieval of a single rasterizer instance. + * + * @since 3.129 + */ +public class SVGRasterizerRegistry { + + /** + * The instance of the registered {@link ISVGRasterizer}. + */ + private static ISVGRasterizer rasterizer; + + /** + * Registers the provided implementation of {@link ISVGRasterizer}. + * If a rasterizer has already been registered, subsequent calls to this method + * will have no effect. + * + * @param implementation the {@link ISVGRasterizer} implementation to register. + */ + public static void register(ISVGRasterizer implementation) { + if (rasterizer == null) { + rasterizer = implementation; + } + } + + /** + * Retrieves the currently registered {@link ISVGRasterizer} implementation. + * + * @return the registered {@link ISVGRasterizer}, or {@code null} if no implementation + * has been registered. + */ + public static ISVGRasterizer getRasterizer() { + return rasterizer; + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGUtil.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGUtil.java new file mode 100644 index 0000000000..939e475200 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/SVGUtil.java @@ -0,0 +1,43 @@ +package org.eclipse.swt.graphics; + +import java.io.*; +import java.nio.charset.*; + +/** + * Utility class for handling SVG-related operations. + * + * @since 3.129 + */ +public class SVGUtil { + + /** + * Determines whether the given {@link InputStream} contains a SVG file. + * + * @param data byte array 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 static boolean isSVGFile(byte[] data) throws IOException { + String content = new String(data, 0, Math.min(data.length, 512), StandardCharsets.UTF_8); + return content.contains("(new ImageData (stream), 100)); + ImageData data = DPIUtil.autoScaleUp(device, new ElementAtZoom<>(new ImageData (stream, getZoom()), 100)); init(data, getZoom()); init(); this.device.registerResourceWithZoomSupport(this); @@ -510,7 +510,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); @@ -553,10 +553,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(); @@ -753,7 +753,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); if (imageCandidate.zoom() == zoom) { /* Release current native resources */ ImageHandle imageMetadata = initNative(imageCandidate.element(), zoom); @@ -1389,7 +1389,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), 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 a8e4c2f684..59bbd40a33 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 @@ -14,9 +14,12 @@ package org.eclipse.swt.graphics; +import java.awt.image.*; import java.io.*; import java.util.*; +import javax.imageio.*; + import org.eclipse.swt.*; import org.eclipse.swt.internal.image.*; @@ -149,10 +152,75 @@ 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 3.129 + */ +public ImageData[] load(InputStream stream, int zoom) { + if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + reset(); + byte[] bytes = null; + try { + bytes = stream.readAllBytes(); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + ISVGRasterizer rasterizer = SVGRasterizerRegistry.getRasterizer(); + if (rasterizer != null && zoom != 0) { + try { + float scalingFactor = zoom / 100.0f; + BufferedImage image = rasterizer.rasterizeSVG(bytes, scalingFactor); + if(image != null) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", baos); + try (InputStream in = new ByteArrayInputStream(baos.toByteArray())) { + data = FileFormat.load(in, this); + return data; + } + } + } + } catch (IOException e) { + // try standard method + } + } + try (InputStream fallbackStream = new ByteArrayInputStream(bytes)) { + return loadDefault(fallbackStream); + } catch (IOException e) { + SWT.error(SWT.ERROR_IO, e); + } + return null; +} + +/** + * @since 3.129 + */ +public ImageData[] loadDefault(InputStream stream) { if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); reset(); data = FileFormat.load(stream, this); - return data; + return data; } /** @@ -176,7 +244,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 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); }