Skip to content

Commit 8c8e0a8

Browse files
committed
Detect the desired size from the path or Query parameters.
Currently Icons can be organized in folders like /icons/16x16, /icons/32x32 and so on for high resolution support. Currently if one want to replace such structure with SVG it is required to scale down the SVG to 16x16 pixel document size as otherwise they get rendered at there native size (what usually is much larger). As it is not really desirable to restrict the size of the SVG design for technical reasons, JFace now can detect two cases: 1) the SVG is places in a folder with "classic" folder layout the size is extracted and passed down as a hint for dynamic sizable icons 2) one can additionally add a query parameter, e.g. if I have an icon like /icons/obj16/search.svg and I have two places where I want to use it one for a toolbar (16x16) and once for a Wizard Images (usually 128x128) I can use the url bundle:/example.id/icons/obj16/search.svg?size=16x16 and bundle:/example.id/icons/obj16/search.svg?size=128x128 to accomplish this task without the need to even store two SVGs
1 parent 40552f3 commit 8c8e0a8

File tree

9 files changed

+527
-4
lines changed

9 files changed

+527
-4
lines changed

bundles/org.eclipse.jface/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Export-Package: org.eclipse.jface,
3434
org.eclipse.jface.window,
3535
org.eclipse.jface.wizard,
3636
org.eclipse.jface.wizard.images
37-
Require-Bundle: org.eclipse.swt;bundle-version="[3.126.0,4.0.0)";visibility:=reexport,
37+
Require-Bundle: org.eclipse.swt;bundle-version="[3.132.0,3.134.0)";visibility:=reexport,
3838
org.eclipse.core.commands;bundle-version="[3.4.0,4.0.0)";visibility:=reexport,
3939
org.eclipse.equinox.common;bundle-version="[3.18.0,4.0.0)",
4040
org.eclipse.equinox.bidi;bundle-version="[0.10.0,2.0.0)";resolution:=optional
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Christoph Läubrich - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jface.resource;
15+
16+
import java.net.URL;
17+
import java.util.function.Supplier;
18+
import java.util.regex.Matcher;
19+
import java.util.regex.Pattern;
20+
21+
import org.eclipse.swt.graphics.Point;
22+
23+
class URLHintProvider implements Supplier<Point> {
24+
25+
private static final Pattern QUERY_PATTERN = Pattern.compile("&size=(\\d+)x(\\d+)"); //$NON-NLS-1$
26+
private static final Pattern PATH_PATTERN = Pattern.compile("/(\\d+)x(\\d+)/"); //$NON-NLS-1$
27+
28+
private URL url;
29+
30+
public URLHintProvider(URL url) {
31+
this.url = url;
32+
}
33+
34+
@Override
35+
public Point get() {
36+
String query = url.getQuery();
37+
Matcher matcher;
38+
if (query != null && !query.isEmpty()) {
39+
matcher = QUERY_PATTERN.matcher("&" + query); //$NON-NLS-1$
40+
} else {
41+
String path = url.getPath();
42+
matcher = PATH_PATTERN.matcher(path);
43+
}
44+
if (matcher.find()) {
45+
return new Point(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
46+
}
47+
return null;
48+
}
49+
50+
}

bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.net.MalformedURLException;
2626
import java.net.URL;
2727
import java.util.function.Function;
28+
import java.util.function.Supplier;
2829
import java.util.regex.Matcher;
2930
import java.util.regex.Pattern;
3031

@@ -42,6 +43,7 @@
4243
import org.eclipse.swt.graphics.ImageDataProvider;
4344
import org.eclipse.swt.graphics.ImageFileNameProvider;
4445
import org.eclipse.swt.graphics.ImageLoader;
46+
import org.eclipse.swt.graphics.Point;
4547
import org.eclipse.swt.internal.DPIUtil.ElementAtZoom;
4648
import org.eclipse.swt.internal.NativeImageLoader;
4749
import org.eclipse.swt.internal.image.FileFormat;
@@ -133,7 +135,7 @@ private static <R> R getZoomedImageSource(URL url, String urlString, int zoom, F
133135
private static ImageData getImageData(URL url, int fileZoom, int targetZoom) {
134136
try (InputStream in = getStream(url)) {
135137
if (in != null) {
136-
return loadImageFromStream(new BufferedInputStream(in), fileZoom, targetZoom);
138+
return loadImageFromStream(new BufferedInputStream(in), fileZoom, targetZoom, new URLHintProvider(url));
137139
}
138140
} catch (SWTException e) {
139141
if (e.code != SWT.ERROR_INVALID_IMAGE) {
@@ -147,7 +149,13 @@ private static ImageData getImageData(URL url, int fileZoom, int targetZoom) {
147149
}
148150

149151
@SuppressWarnings("restriction")
150-
private static ImageData loadImageFromStream(InputStream stream, int fileZoom, int targetZoom) {
152+
private static ImageData loadImageFromStream(InputStream stream, int fileZoom, int targetZoom,
153+
Supplier<Point> hintProvider) {
154+
Point hintSize = hintProvider.get();
155+
if (hintSize != null) {
156+
return NativeImageLoader.load(stream, new ImageLoader(), hintSize.x * targetZoom / fileZoom,
157+
hintSize.y * targetZoom / fileZoom);
158+
}
151159
return NativeImageLoader.load(new ElementAtZoom<>(stream, fileZoom), new ImageLoader(), targetZoom).get(0)
152160
.element();
153161
}
@@ -169,8 +177,14 @@ private static InputStream getStream(URL url) {
169177
if (InternalPolicy.OSGI_AVAILABLE) {
170178
url = resolvePathVariables(url);
171179
}
180+
// For file: URLs, strip query parameters before opening the stream
181+
// Query parameters are used for size hints but are not valid in file paths
182+
if (FILE_PROTOCOL.equalsIgnoreCase(url.getProtocol()) && url.getQuery() != null) {
183+
url = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath());
184+
}
172185
return url.openStream();
173186
} catch (IOException e) {
187+
e.printStackTrace();
174188
if (InternalPolicy.DEBUG_LOG_URL_IMAGE_DESCRIPTOR_MISSING_2x) {
175189
String path = url.getPath();
176190
if (path.endsWith("@2x.png") || path.endsWith("@1.5x.png")) { //$NON-NLS-1$ //$NON-NLS-2$
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Christoph Läubrich - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jface.snippets.resources;
15+
16+
import java.net.URL;
17+
18+
import org.eclipse.jface.layout.GridDataFactory;
19+
import org.eclipse.jface.layout.GridLayoutFactory;
20+
import org.eclipse.jface.resource.ImageDescriptor;
21+
import org.eclipse.swt.SWT;
22+
import org.eclipse.swt.graphics.Image;
23+
import org.eclipse.swt.widgets.Canvas;
24+
import org.eclipse.swt.widgets.Display;
25+
import org.eclipse.swt.widgets.Label;
26+
import org.eclipse.swt.widgets.Shell;
27+
28+
/**
29+
* A snippet to demonstrate SVG image size hints using path-based and
30+
* query-parameter-based size detection.
31+
*
32+
* <p>
33+
* This demonstrates two ways to control SVG rendering size:
34+
* <ol>
35+
* <li><b>Path-based hints:</b> Place SVG in folders like /icons/16x16/ or
36+
* /icons/32x32/</li>
37+
* <li><b>Query parameter hints:</b> Add ?size=WIDTHxHEIGHT to the URL</li>
38+
* </ol>
39+
* </p>
40+
*
41+
* <p>
42+
* This allows using a single SVG file at different sizes without creating
43+
* multiple scaled versions or restricting the SVG design size.
44+
* </p>
45+
*/
46+
public class Snippet083SVGImageSizeHints {
47+
48+
public static void main(String[] args) {
49+
Display display = new Display();
50+
Shell shell = new Shell(display);
51+
shell.setText("SVG Image Size Hints Demo");
52+
GridLayoutFactory.fillDefaults().numColumns(2).margins(10, 10).spacing(10, 10).applyTo(shell);
53+
54+
addSection(shell, "Path-Based Size Hints", """
55+
SVGs placed in folders with size patterns (e.g., /icons/16x16/, /icons/32x32/)
56+
are automatically rendered at that size.
57+
58+
Example: bundle://plugin.id/icons/16x16/icon.svg
59+
""");
60+
61+
addSection(shell, "Query Parameter Size Hints", """
62+
You can specify size using query parameters for maximum flexibility.
63+
This allows using the same SVG at different sizes.
64+
65+
Example: bundle://plugin.id/icons/icon.svg?size=16x16
66+
Example: bundle://plugin.id/icons/icon.svg?size=128x128
67+
""");
68+
69+
addSection(shell, "Zoom Support", """
70+
Both methods work with high-DPI displays. The hint specifies the base size,
71+
and JFace automatically scales for 150% and 200% zoom levels.
72+
73+
16x16 at 100% zoom → 16x16 pixels
74+
16x16 at 150% zoom → 24x24 pixels
75+
16x16 at 200% zoom → 32x32 pixels
76+
""");
77+
78+
// Demonstrate with actual images from test resources if available
79+
try {
80+
URL svgUrl = Snippet083SVGImageSizeHints.class
81+
.getResource("/org/eclipse/jface/snippets/resources/test-demo.svg");
82+
if (svgUrl != null) {
83+
addImageDemo(shell, "Default (no hint)", svgUrl.toString(), display);
84+
addImageDemo(shell, "With ?size=32x32", svgUrl.toString() + "?size=32x32", display);
85+
addImageDemo(shell, "With ?size=64x64", svgUrl.toString() + "?size=64x64", display);
86+
}
87+
} catch (Exception e) {
88+
// Demo images not available, that's okay
89+
Label note = new Label(shell, SWT.WRAP);
90+
note.setText("Note: Visual demo requires test SVG files. See URLHintProviderTest for examples.");
91+
GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(note);
92+
}
93+
94+
addSection(shell, "Use Cases", """
95+
1. Toolbar icons: Use ?size=16x16 for consistent toolbar sizing
96+
2. Wizard images: Use ?size=128x128 for large wizard graphics
97+
3. View icons: Place in /icons/16x16/ folder structure
98+
4. Multi-resolution: One SVG serves all sizes without duplication
99+
""");
100+
101+
addSection(shell, "Code Example", """
102+
// Using query parameter
103+
URL iconUrl = new URL("bundle://my.plugin/icons/search.svg?size=16x16");
104+
ImageDescriptor desc = ImageDescriptor.createFromURL(iconUrl);
105+
Image icon = desc.createImage();
106+
107+
// Using path-based hint
108+
URL iconUrl = FileLocator.find(bundle, new Path("icons/16x16/search.svg"));
109+
ImageDescriptor desc = ImageDescriptor.createFromURL(iconUrl);
110+
""");
111+
112+
shell.pack();
113+
shell.open();
114+
115+
while (!shell.isDisposed()) {
116+
if (!display.readAndDispatch()) {
117+
display.sleep();
118+
}
119+
}
120+
display.dispose();
121+
}
122+
123+
private static void addSection(Shell shell, String title, String text) {
124+
Label titleLabel = new Label(shell, SWT.BOLD);
125+
titleLabel.setText(title);
126+
GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(titleLabel);
127+
128+
Label textLabel = new Label(shell, SWT.WRAP);
129+
textLabel.setText(text.trim());
130+
GridDataFactory.fillDefaults().span(2, 1).grab(true, false).hint(600, SWT.DEFAULT).applyTo(textLabel);
131+
132+
Label separator = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL);
133+
GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(separator);
134+
}
135+
136+
private static void addImageDemo(Shell shell, String description, String urlString, Display display) {
137+
try {
138+
URL url = new URL(urlString);
139+
ImageDescriptor descriptor = ImageDescriptor.createFromURL(url);
140+
Image image = descriptor.createImage(display);
141+
142+
Label label = new Label(shell, SWT.NONE);
143+
label.setText(description + ":");
144+
GridDataFactory.fillDefaults().applyTo(label);
145+
146+
Canvas canvas = new Canvas(shell, SWT.BORDER);
147+
canvas.addPaintListener(e -> {
148+
if (!image.isDisposed()) {
149+
e.gc.drawImage(image, 0, 0);
150+
}
151+
});
152+
GridDataFactory.swtDefaults().hint(image.getBounds().width + 2, image.getBounds().height + 2)
153+
.applyTo(canvas);
154+
155+
canvas.addDisposeListener(e -> {
156+
if (!image.isDisposed()) {
157+
image.dispose();
158+
}
159+
});
160+
} catch (Exception e) {
161+
Label error = new Label(shell, SWT.NONE);
162+
error.setText(description + ": Error loading image");
163+
GridDataFactory.fillDefaults().span(2, 1).applyTo(error);
164+
}
165+
}
166+
}
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading

tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/AllImagesTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
@Suite
2323
@SelectClasses({ ImageRegistryTest.class, ResourceManagerTest.class, FileImageDescriptorTest.class,
24-
UrlImageDescriptorTest.class, DecorationOverlayIconTest.class, DeferredImageDescriptorTest.class })
24+
UrlImageDescriptorTest.class, URLHintProviderTest.class, DecorationOverlayIconTest.class,
25+
DeferredImageDescriptorTest.class })
2526
public class AllImagesTests {
2627

2728
public static void main(String[] args) {

0 commit comments

Comments
 (0)