Skip to content

Commit 7a4c784

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 02fc196 commit 7a4c784

File tree

12 files changed

+456
-10
lines changed

12 files changed

+456
-10
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: %pluginName
44
Bundle-SymbolicName: org.eclipse.jface;singleton:=true
5-
Bundle-Version: 3.38.100.qualifier
5+
Bundle-Version: 3.39.0
66
Bundle-Vendor: %providerName
77
Bundle-Localization: plugin
88
Export-Package: org.eclipse.jface,
@@ -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: 11 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
}
@@ -171,6 +179,7 @@ private static InputStream getStream(URL url) {
171179
}
172180
return url.openStream();
173181
} catch (IOException e) {
182+
e.printStackTrace();
174183
if (InternalPolicy.DEBUG_LOG_URL_IMAGE_DESCRIPTOR_MISSING_2x) {
175184
String path = url.getPath();
176185
if (path.endsWith("@2x.png") || path.endsWith("@1.5x.png")) { //$NON-NLS-1$ //$NON-NLS-2$

examples/org.eclipse.jface.snippets/.classpath

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<classpath>
3-
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
3+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21"/>
44
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
55
<classpathentry kind="src" path="Eclipse JFace Snippets"/>
66
<classpathentry kind="output" path="bin"/>

examples/org.eclipse.jface.snippets/.settings/org.eclipse.jdt.core.prefs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annota
88
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
99
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
1010
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
11-
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
11+
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
1212
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
13-
org.eclipse.jdt.core.compiler.compliance=17
13+
org.eclipse.jdt.core.compiler.compliance=21
1414
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
1515
org.eclipse.jdt.core.compiler.debug.localVariable=generate
1616
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -118,7 +118,7 @@ org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
118118
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
119119
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
120120
org.eclipse.jdt.core.compiler.release=enabled
121-
org.eclipse.jdt.core.compiler.source=17
121+
org.eclipse.jdt.core.compiler.source=21
122122
org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
123123
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
124124
org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16
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+
}

examples/org.eclipse.jface.snippets/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ Export-Package: org.eclipse.jface.snippets.dialogs,
1515
org.eclipse.jface.snippets.viewers,
1616
org.eclipse.jface.snippets.window,
1717
org.eclipse.jface.snippets.wizard
18-
Bundle-RequiredExecutionEnvironment: JavaSE-17
18+
Bundle-RequiredExecutionEnvironment: JavaSE-21
1919
Automatic-Module-Name: org.eclipse.jface.snippets
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)