Skip to content

Commit 0459b4b

Browse files
committed
Introduce SVG Rasterization for Icons
Feature Proposal: Rasterization of SVGs at Runtime for Eclipse Icons Fixes #1438 Eclipse currently loads icons exclusively as raster graphics (e.g., `.png`), without support for vector formats like `.svg`. A major drawback of raster graphics is their inability to scale without degrading image quality. Additionally, generating icons of different sizes requires manually rasterizing SVGs outside Eclipse, leading to unnecessary effort and many icon files. This PR introduces support for vector graphics in Eclipse, enabling SVGs to be used for icons. Existing PNG icons will continue to be loaded alongside SVGs, allowing the use of the new functionality without the need to replace all PNG files at once. --- - **How It Works**: - To use SVG icons, simply place the SVG file in the bundle and reference it in the `plugin.xml` and other necessary locations, as is done for PNGs. No additional configuration is required. - At runtime, Eclipse uses the library JSVG to rasterize the SVG into a raster image of the desired size, eliminating the need for scaling. My analysis shows that JSVG is the most suitable Java library for this purpose. - You need to write the flag `-Dswt.autoScale=quarter` into your `eclipse.ini` file or into the run arguments of a new configuration.
1 parent 632681f commit 0459b4b

File tree

25 files changed

+1155
-12
lines changed

25 files changed

+1155
-12
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<classpath>
3+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
4+
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
5+
<classpathentry kind="src" path="src"/>
6+
<classpathentry kind="output" path="bin"/>
7+
</classpath>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>org.eclipse.swt.svg</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
<buildCommand>
14+
<name>org.eclipse.pde.ManifestBuilder</name>
15+
<arguments>
16+
</arguments>
17+
</buildCommand>
18+
<buildCommand>
19+
<name>org.eclipse.pde.SchemaBuilder</name>
20+
<arguments>
21+
</arguments>
22+
</buildCommand>
23+
</buildSpec>
24+
<natures>
25+
<nature>org.eclipse.pde.PluginNature</nature>
26+
<nature>org.eclipse.jdt.core.javanature</nature>
27+
</natures>
28+
</projectDescription>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
eclipse.preferences.version=1
2+
encoding/<project>=UTF-8
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
eclipse.preferences.version=1
2+
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
3+
org.eclipse.jdt.core.compiler.compliance=17
4+
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
5+
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
6+
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
7+
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
8+
org.eclipse.jdt.core.compiler.release=enabled
9+
org.eclipse.jdt.core.compiler.source=17
10+
org.eclipse.jdt.core.incompleteClasspath=warning
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Manifest-Version: 1.0
2+
Bundle-ManifestVersion: 2
3+
Bundle-Name: SWT SVG Rendering Support
4+
Bundle-SymbolicName: org.eclipse.swt.svg
5+
Bundle-Version: 1.0.0.qualifier
6+
Automatic-Module-Name: org.eclipse.swt.svgPlugin
7+
Bundle-RequiredExecutionEnvironment: JavaSE-17
8+
Fragment-Host: org.eclipse.swt
9+
Import-Package: com.github.weisj.jsvg;version="[1.7.0,2.0.0)",
10+
com.github.weisj.jsvg.geometry.size;version="[1.7.0,2.0.0)",
11+
com.github.weisj.jsvg.parser;version="[1.7.0,2.0.0)"
12+
Export-Package: org.eclipse.swt.svg
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.eclipse.swt.svg.JSVGRasterizer
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
source.. = src/
2+
output.. = bin/
3+
bin.includes = META-INF/,\
4+
.
5+
tycho.pomless.parent = ../../local-build/local-build-parent
6+
jars.extra.classpath = platform:/plugin/org.eclipse.swt.cocoa.macosx.aarch64,\
7+
platform:/plugin/org.eclipse.swt.cocoa.macosx.x86_64,\
8+
platform:/plugin/org.eclipse.swt.gtk.linux.aarch64,\
9+
platform:/plugin/org.eclipse.swt.gtk.linux.ppc64le,\
10+
platform:/plugin/org.eclipse.swt.gtk.linux.riscv64,\
11+
platform:/plugin/org.eclipse.swt.gtk.linux.x86_64,\
12+
platform:/plugin/org.eclipse.swt.win32.win32.aarch64,\
13+
platform:/plugin/org.eclipse.swt.win32.win32.x86_64
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vector Informatik GmbH and others.
3+
*
4+
* This program and the accompanying materials are made available under the terms of the Eclipse
5+
* Public License 2.0 which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Michael Bangas (Vector Informatik GmbH) - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.swt.svg;
14+
15+
import static java.awt.RenderingHints.KEY_ALPHA_INTERPOLATION;
16+
import static java.awt.RenderingHints.KEY_ANTIALIASING;
17+
import static java.awt.RenderingHints.KEY_COLOR_RENDERING;
18+
import static java.awt.RenderingHints.KEY_DITHERING;
19+
import static java.awt.RenderingHints.KEY_FRACTIONALMETRICS;
20+
import static java.awt.RenderingHints.KEY_INTERPOLATION;
21+
import static java.awt.RenderingHints.KEY_RENDERING;
22+
import static java.awt.RenderingHints.KEY_STROKE_CONTROL;
23+
import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
24+
import static java.awt.RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY;
25+
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
26+
import static java.awt.RenderingHints.VALUE_COLOR_RENDER_QUALITY;
27+
import static java.awt.RenderingHints.VALUE_DITHER_DISABLE;
28+
import static java.awt.RenderingHints.VALUE_FRACTIONALMETRICS_ON;
29+
import static java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC;
30+
import static java.awt.RenderingHints.VALUE_RENDER_QUALITY;
31+
import static java.awt.RenderingHints.VALUE_STROKE_PURE;
32+
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
33+
34+
import java.awt.Graphics2D;
35+
import java.awt.RenderingHints.Key;
36+
import java.awt.image.BufferedImage;
37+
import java.awt.image.ComponentColorModel;
38+
import java.awt.image.DirectColorModel;
39+
import java.awt.image.IndexColorModel;
40+
import java.awt.image.WritableRaster;
41+
import java.io.IOException;
42+
import java.io.InputStream;
43+
import java.util.Map;
44+
45+
import org.eclipse.swt.SWT;
46+
import org.eclipse.swt.graphics.ImageData;
47+
import org.eclipse.swt.graphics.PaletteData;
48+
import org.eclipse.swt.graphics.RGB;
49+
import org.eclipse.swt.internal.SVGRasterizer;
50+
51+
import com.github.weisj.jsvg.SVGDocument;
52+
import com.github.weisj.jsvg.geometry.size.FloatSize;
53+
import com.github.weisj.jsvg.parser.LoaderContext;
54+
import com.github.weisj.jsvg.parser.SVGLoader;
55+
56+
/**
57+
* A rasterizer implementation for converting SVG data into rasterized images.
58+
* This class implements the {@code SVGRasterizer} interface.
59+
*
60+
* @since 1.0.0
61+
*/
62+
public class JSVGRasterizer implements SVGRasterizer {
63+
64+
private static final SVGLoader SVG_LOADER = new SVGLoader();
65+
66+
private final static Map<Key, Object> RENDERING_HINTS = Map.of( //
67+
KEY_ANTIALIASING, VALUE_ANTIALIAS_ON, //
68+
KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY, //
69+
KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY, //
70+
KEY_DITHERING, VALUE_DITHER_DISABLE, //
71+
KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON, //
72+
KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC, //
73+
KEY_RENDERING, VALUE_RENDER_QUALITY, //
74+
KEY_STROKE_CONTROL, VALUE_STROKE_PURE, //
75+
KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON //
76+
);
77+
78+
@Override
79+
public ImageData[] rasterizeSVG(InputStream stream, int zoom) throws IOException {
80+
SVGDocument svgDocument = null;
81+
svgDocument = SVG_LOADER.load(stream, null, LoaderContext.createDefault());
82+
if (svgDocument != null) {
83+
float scalingFactor = zoom / 100.0f;
84+
FloatSize size = svgDocument.size();
85+
double originalWidth = size.getWidth();
86+
double originalHeight = size.getHeight();
87+
int scaledWidth = (int) Math.round(originalWidth * scalingFactor);
88+
int scaledHeight = (int) Math.round(originalHeight * scalingFactor);
89+
BufferedImage image = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB);
90+
Graphics2D g = image.createGraphics();
91+
g.setRenderingHints(RENDERING_HINTS);
92+
g.scale(scalingFactor, scalingFactor);
93+
svgDocument.render(null, g);
94+
g.dispose();
95+
return new ImageData[] { convertToSWT(image) };
96+
} else {
97+
SWT.error(SWT.ERROR_INVALID_IMAGE);
98+
}
99+
return null;
100+
}
101+
102+
private ImageData convertToSWT(BufferedImage bufferedImage) {
103+
if (bufferedImage.getColorModel() instanceof DirectColorModel) {
104+
DirectColorModel colorModel = (DirectColorModel) bufferedImage.getColorModel();
105+
PaletteData palette = new PaletteData(colorModel.getRedMask(), colorModel.getGreenMask(),
106+
colorModel.getBlueMask());
107+
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(),
108+
colorModel.getPixelSize(), palette);
109+
for (int y = 0; y < data.height; y++) {
110+
for (int x = 0; x < data.width; x++) {
111+
int rgb = bufferedImage.getRGB(x, y);
112+
int pixel = palette.getPixel(new RGB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF));
113+
data.setPixel(x, y, pixel);
114+
if (colorModel.hasAlpha()) {
115+
data.setAlpha(x, y, (rgb >> 24) & 0xFF);
116+
}
117+
}
118+
}
119+
return data;
120+
} else if (bufferedImage.getColorModel() instanceof IndexColorModel) {
121+
IndexColorModel colorModel = (IndexColorModel) bufferedImage.getColorModel();
122+
int size = colorModel.getMapSize();
123+
byte[] reds = new byte[size];
124+
byte[] greens = new byte[size];
125+
byte[] blues = new byte[size];
126+
colorModel.getReds(reds);
127+
colorModel.getGreens(greens);
128+
colorModel.getBlues(blues);
129+
RGB[] rgbs = new RGB[size];
130+
for (int i = 0; i < rgbs.length; i++) {
131+
rgbs[i] = new RGB(reds[i] & 0xFF, greens[i] & 0xFF, blues[i] & 0xFF);
132+
}
133+
PaletteData palette = new PaletteData(rgbs);
134+
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(),
135+
colorModel.getPixelSize(), palette);
136+
data.transparentPixel = colorModel.getTransparentPixel();
137+
WritableRaster raster = bufferedImage.getRaster();
138+
int[] pixelArray = new int[1];
139+
for (int y = 0; y < data.height; y++) {
140+
for (int x = 0; x < data.width; x++) {
141+
raster.getPixel(x, y, pixelArray);
142+
data.setPixel(x, y, pixelArray[0]);
143+
}
144+
}
145+
return data;
146+
} else if (bufferedImage.getColorModel() instanceof ComponentColorModel) {
147+
ComponentColorModel colorModel = (ComponentColorModel) bufferedImage.getColorModel();
148+
PaletteData palette = new PaletteData(0x0000FF, 0x00FF00, 0xFF0000);
149+
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(),
150+
colorModel.getPixelSize(), palette);
151+
data.transparentPixel = -1;
152+
WritableRaster raster = bufferedImage.getRaster();
153+
int[] pixelArray = new int[3];
154+
for (int y = 0; y < data.height; y++) {
155+
for (int x = 0; x < data.width; x++) {
156+
raster.getPixel(x, y, pixelArray);
157+
int pixel = palette.getPixel(new RGB(pixelArray[0], pixelArray[1], pixelArray[2]));
158+
data.setPixel(x, y, pixel);
159+
}
160+
}
161+
return data;
162+
}
163+
return null;
164+
}
165+
}

bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ public Image(Device device, InputStream stream) {
692692
NSAutoreleasePool pool = null;
693693
if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init();
694694
try {
695-
init(new ImageData(stream));
695+
init(new ImageData(stream, DPIUtil.getDeviceZoom()));
696696
init();
697697
} finally {
698698
if (pool != null) pool.release();
@@ -738,7 +738,7 @@ public Image(Device device, String filename) {
738738
try {
739739
if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
740740
initNative(filename);
741-
if (this.handle == null) init(new ImageData(filename));
741+
if (this.handle == null) init(new ImageData(filename, DPIUtil.getDeviceZoom()));
742742
init();
743743
} finally {
744744
if (pool != null) pool.release();
@@ -784,7 +784,7 @@ public Image(Device device, ImageFileNameProvider imageFileNameProvider) {
784784
if (!NSThread.isMainThread()) pool = (NSAutoreleasePool) new NSAutoreleasePool().alloc().init();
785785
try {
786786
initNative(filename);
787-
if (this.handle == null) init(new ImageData(filename));
787+
if (this.handle == null) init(new ImageData(filename, DPIUtil.getDeviceZoom()));
788788
init();
789789
String filename2x = imageFileNameProvider.getImagePath(200);
790790
if (filename2x != null) {

bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageData.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,62 @@ public ImageData(InputStream stream) {
354354
i.delayTime);
355355
}
356356

357+
/**
358+
* Constructs an <code>ImageData</code> loaded from the specified
359+
* input stream. Throws an error if an error occurs while loading
360+
* the image, or if the image has an unsupported type. Application
361+
* code is still responsible for closing the input stream.
362+
*
363+
* @param stream the input stream to load the image from (must not be null)
364+
* @param zoom the zoom factor to apply when rasterizing an SVG.
365+
*
366+
* A value of 0 means that the standard method for loading should be used.
367+
* This case is equivalent to calling {@link ImageLoader#load(InputStream)}.
368+
*
369+
* A value above 0 specifies a scaling factor for the output image. For example:
370+
* <ul>
371+
* <li>A value of 100 maintains the original size of the SVG when rasterized.</li>
372+
* <li>A value of 200 doubles the size of the rasterized image.</li>
373+
* <li>A value of 50 reduces the size of the rasterized image to half.</li>
374+
* </ul>
375+
* The scaling is applied uniformly to both width and height.
376+
*
377+
* @exception IllegalArgumentException <ul>
378+
* <li>ERROR_NULL_ARGUMENT - if the stream is null</li>
379+
* </ul>
380+
* @exception SWTException <ul>
381+
* <li>ERROR_IO - if an IO error occurs while reading from the stream</li>
382+
* <li>ERROR_INVALID_IMAGE - if the image stream contains invalid data</li>
383+
* <li>ERROR_UNSUPPORTED_FORMAT - if the image stream contains an unrecognized format</li>
384+
* </ul>
385+
*
386+
* @see ImageLoader#load(InputStream)
387+
* @since 3.130
388+
*/
389+
public ImageData(InputStream stream, int zoom) {
390+
ImageData[] data = ImageDataLoader.load(stream, zoom);
391+
if (data.length < 1) SWT.error(SWT.ERROR_INVALID_IMAGE);
392+
ImageData i = data[0];
393+
setAllFields(
394+
i.width,
395+
i.height,
396+
i.depth,
397+
i.scanlinePad,
398+
i.bytesPerLine,
399+
i.data,
400+
i.palette,
401+
i.transparentPixel,
402+
i.maskData,
403+
i.maskPad,
404+
i.alphaData,
405+
i.alpha,
406+
i.type,
407+
i.x,
408+
i.y,
409+
i.disposalMethod,
410+
i.delayTime);
411+
}
412+
357413
/**
358414
* Constructs an <code>ImageData</code> loaded from a file with the
359415
* specified name. Throws an error if an error occurs loading the
@@ -400,6 +456,59 @@ public ImageData(String filename) {
400456
i.delayTime);
401457
}
402458

459+
/**
460+
* Constructs an <code>ImageData</code> loaded from a file with the
461+
* specified name. Throws an error if an error occurs loading the
462+
* image, or if the image has an unsupported type.
463+
*
464+
* @param filename the name of the file to load the image from (must not be null)
465+
* @param zoom the zoom factor to apply when rasterizing a SVG.
466+
* A value of 0 means that the standard method for loading should be used.
467+
* This case is equivalent to calling {@link ImageLoader#load(String)}.
468+
*
469+
* A value above 0 specifies a scaling factor for the output image. For example:
470+
* <ul>
471+
* <li>A value of 100 maintains the original size of the SVG when rasterized.</li>
472+
* <li>A value of 200 doubles the size of the rasterized image.</li>
473+
* <li>A value of 50 reduces the size of the rasterized image to half.</li>
474+
* </ul>
475+
* The scaling is applied uniformly to both width and height.
476+
*
477+
* @exception IllegalArgumentException <ul>
478+
* <li>ERROR_NULL_ARGUMENT - if the file name is null</li>
479+
* </ul>
480+
* @exception SWTException <ul>
481+
* <li>ERROR_IO - if an IO error occurs while reading from the file</li>
482+
* <li>ERROR_INVALID_IMAGE - if the image file contains invalid data</li>
483+
* <li>ERROR_UNSUPPORTED_FORMAT - if the image file contains an unrecognized format</li>
484+
* </ul>
485+
*
486+
* @since 3.130
487+
*/
488+
public ImageData(String filename, int zoom) {
489+
ImageData[] data = ImageDataLoader.load(filename, zoom);
490+
if (data.length < 1) SWT.error(SWT.ERROR_INVALID_IMAGE);
491+
ImageData i = data[0];
492+
setAllFields(
493+
i.width,
494+
i.height,
495+
i.depth,
496+
i.scanlinePad,
497+
i.bytesPerLine,
498+
i.data,
499+
i.palette,
500+
i.transparentPixel,
501+
i.maskData,
502+
i.maskPad,
503+
i.alphaData,
504+
i.alpha,
505+
i.type,
506+
i.x,
507+
i.y,
508+
i.disposalMethod,
509+
i.delayTime);
510+
}
511+
403512
/**
404513
* Prevents uninitialized instances from being created outside the package.
405514
*/

0 commit comments

Comments
 (0)