From 072e6f4cbedd3fbe413dae5f72578564947d0fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 22 Oct 2025 18:46:14 +0200 Subject: [PATCH] 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 --- .../org.eclipse.jface/META-INF/MANIFEST.MF | 2 +- .../jface/resource/URLHintProvider.java | 50 +++ .../jface/resource/URLImageDescriptor.java | 23 +- .../Snippet083SVGImageSizeHints.java | 333 ++++++++++++++++ .../jface/snippets/resources/demo-icon.svg | 19 + .../icons/imagetests/16x16/test-icon.svg | 6 + .../icons/imagetests/32x32/test-icon.svg | 6 + .../icons/imagetests/test-icon.svg | 6 + .../jface/tests/images/AllImagesTests.java | 3 +- .../tests/images/URLHintProviderTest.java | 354 ++++++++++++++++++ 10 files changed, 798 insertions(+), 4 deletions(-) create mode 100644 bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLHintProvider.java create mode 100644 examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/Snippet083SVGImageSizeHints.java create mode 100644 examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/demo-icon.svg create mode 100644 tests/org.eclipse.jface.tests/icons/imagetests/16x16/test-icon.svg create mode 100644 tests/org.eclipse.jface.tests/icons/imagetests/32x32/test-icon.svg create mode 100644 tests/org.eclipse.jface.tests/icons/imagetests/test-icon.svg create mode 100644 tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/URLHintProviderTest.java diff --git a/bundles/org.eclipse.jface/META-INF/MANIFEST.MF b/bundles/org.eclipse.jface/META-INF/MANIFEST.MF index 0a2620fd711..90a269afeef 100644 --- a/bundles/org.eclipse.jface/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.jface/META-INF/MANIFEST.MF @@ -34,7 +34,7 @@ Export-Package: org.eclipse.jface, org.eclipse.jface.window, org.eclipse.jface.wizard, org.eclipse.jface.wizard.images -Require-Bundle: org.eclipse.swt;bundle-version="[3.126.0,4.0.0)";visibility:=reexport, +Require-Bundle: org.eclipse.swt;bundle-version="[3.132.0,3.134.0)";visibility:=reexport, org.eclipse.core.commands;bundle-version="[3.4.0,4.0.0)";visibility:=reexport, org.eclipse.equinox.common;bundle-version="[3.18.0,4.0.0)", org.eclipse.equinox.bidi;bundle-version="[0.10.0,2.0.0)";resolution:=optional diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLHintProvider.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLHintProvider.java new file mode 100644 index 00000000000..4d5c176cf32 --- /dev/null +++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLHintProvider.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich 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: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.jface.resource; + +import java.net.URL; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.swt.graphics.Point; + +class URLHintProvider implements Supplier { + + private static final Pattern QUERY_PATTERN = Pattern.compile("&size=(\\d+)x(\\d+)"); //$NON-NLS-1$ + private static final Pattern PATH_PATTERN = Pattern.compile("/(\\d+)x(\\d+)/"); //$NON-NLS-1$ + + private URL url; + + public URLHintProvider(URL url) { + this.url = url; + } + + @Override + public Point get() { + String query = url.getQuery(); + Matcher matcher; + if (query != null && !query.isEmpty()) { + matcher = QUERY_PATTERN.matcher("&" + query); //$NON-NLS-1$ + } else { + String path = url.getPath(); + matcher = PATH_PATTERN.matcher(path); + } + if (matcher.find()) { + return new Point(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))); + } + return null; + } + +} diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java index 646ca5cb90f..a45d192892b 100644 --- a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java +++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java @@ -25,6 +25,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,6 +43,7 @@ import org.eclipse.swt.graphics.ImageDataProvider; import org.eclipse.swt.graphics.ImageFileNameProvider; import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.graphics.Point; import org.eclipse.swt.internal.DPIUtil.ElementAtZoom; import org.eclipse.swt.internal.NativeImageLoader; import org.eclipse.swt.internal.image.FileFormat; @@ -59,6 +61,11 @@ private ImageFileNameProvider createURLImageFileNameProvider() { if (tempURL != null) { final boolean logIOException = zoom == 100; if (zoom == 100) { + // Do not use file path if URL has query parameters (e.g., size hints) + // because getFilePath() strips the query and size hints would be lost + if (tempURL.getQuery() != null) { + return null; + } // Do not take this path if the image file can be scaled up dynamically. // The calling image will do that itself! return getFilePath(tempURL, logIOException); @@ -133,7 +140,7 @@ private static R getZoomedImageSource(URL url, String urlString, int zoom, F private static ImageData getImageData(URL url, int fileZoom, int targetZoom) { try (InputStream in = getStream(url)) { if (in != null) { - return loadImageFromStream(new BufferedInputStream(in), fileZoom, targetZoom); + return loadImageFromStream(new BufferedInputStream(in), fileZoom, targetZoom, new URLHintProvider(url)); } } catch (SWTException e) { if (e.code != SWT.ERROR_INVALID_IMAGE) { @@ -147,7 +154,13 @@ private static ImageData getImageData(URL url, int fileZoom, int targetZoom) { } @SuppressWarnings("restriction") - private static ImageData loadImageFromStream(InputStream stream, int fileZoom, int targetZoom) { + private static ImageData loadImageFromStream(InputStream stream, int fileZoom, int targetZoom, + Supplier hintProvider) { + Point hintSize = hintProvider.get(); + if (hintSize != null) { + return NativeImageLoader.load(stream, new ImageLoader(), hintSize.x * targetZoom / fileZoom, + hintSize.y * targetZoom / fileZoom); + } return NativeImageLoader.load(new ElementAtZoom<>(stream, fileZoom), new ImageLoader(), targetZoom).get(0) .element(); } @@ -169,8 +182,14 @@ private static InputStream getStream(URL url) { if (InternalPolicy.OSGI_AVAILABLE) { url = resolvePathVariables(url); } + // For file: URLs, strip query parameters before opening the stream + // Query parameters are used for size hints but are not valid in file paths + if (FILE_PROTOCOL.equalsIgnoreCase(url.getProtocol()) && url.getQuery() != null) { + url = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); + } return url.openStream(); } catch (IOException e) { + e.printStackTrace(); if (InternalPolicy.DEBUG_LOG_URL_IMAGE_DESCRIPTOR_MISSING_2x) { String path = url.getPath(); if (path.endsWith("@2x.png") || path.endsWith("@1.5x.png")) { //$NON-NLS-1$ //$NON-NLS-2$ diff --git a/examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/Snippet083SVGImageSizeHints.java b/examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/Snippet083SVGImageSizeHints.java new file mode 100644 index 00000000000..3cfb6c22077 --- /dev/null +++ b/examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/Snippet083SVGImageSizeHints.java @@ -0,0 +1,333 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich 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: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.jface.snippets.resources; + +import java.net.URI; +import java.net.URL; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CTabFolder; +import org.eclipse.swt.custom.CTabItem; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * A snippet to demonstrate SVG image size hints using path-based and + * query-parameter-based size detection. + * + *

+ * This demonstrates two ways to control SVG rendering size: + *

+ *
    + *
  1. Path-based hints: Place SVG in folders like /icons/16x16/ or + * /icons/32x32/
  2. + *
  3. Query parameter hints: Add ?size=WIDTHxHEIGHT to the URL
  4. + *
+ * + *

+ * This allows using a single SVG file at different sizes without creating + * multiple scaled versions or restricting the SVG design size. + *

+ */ +public class Snippet083SVGImageSizeHints { + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("SVG Image Size Hints Demo"); + GridLayoutFactory.fillDefaults().margins(10, 10).applyTo(shell); + + CTabFolder tabFolder = new CTabFolder(shell, SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, true).hint(700, 500).applyTo(tabFolder); + + createOverviewTab(tabFolder); + createVisualDemoTab(tabFolder, display); + createCodeExamplesTab(tabFolder); + createUseCasesTab(tabFolder); + + tabFolder.setSelection(0); + + shell.pack(); + shell.open(); + + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + private static void createOverviewTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText("Overview"); + + Composite content = new Composite(folder, SWT.NONE); + GridLayoutFactory.fillDefaults().margins(10, 10).applyTo(content); + tab.setControl(content); + + Text text = new Text(content, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + GridDataFactory.fillDefaults().grab(true, true).applyTo(text); + + text.setText(""" + SVG Image Size Hints + ==================== + + JFace now supports automatic size detection for SVG images through two mechanisms: + + 1. PATH-BASED SIZE HINTS + SVGs placed in folders with size patterns (e.g., /icons/16x16/, /icons/32x32/) + are automatically rendered at that size. + + Example: bundle://plugin.id/icons/16x16/icon.svg → renders at 16x16 pixels + + 2. QUERY PARAMETER SIZE HINTS + You can specify size using query parameters for maximum flexibility. + This allows using the same SVG at different sizes. + + Example: bundle://plugin.id/icons/icon.svg?size=16x16 + Example: bundle://plugin.id/icons/icon.svg?size=128x128 + + HIGH-DPI SUPPORT + Both methods work with high-DPI displays. The hint specifies the base size, + and JFace automatically scales for display zoom levels: + + 16x16 at 100% zoom → 16x16 pixels + 16x16 at 150% zoom → 24x24 pixels + 16x16 at 200% zoom → 32x32 pixels + + PRECEDENCE + Query parameters take precedence over path-based hints, so you can override + the path-based size if needed. + """); + } + + private static void createVisualDemoTab(CTabFolder folder, Display display) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText("Visual Demo"); + + Composite content = new Composite(folder, SWT.NONE); + GridLayoutFactory.fillDefaults().margins(10, 10).spacing(10, 10).numColumns(3).applyTo(content); + tab.setControl(content); + + // Try to load demo SVG + URL svgUrl = Snippet083SVGImageSizeHints.class.getResource("demo-icon.svg"); + + if (svgUrl == null) { + Label error = new Label(content, SWT.WRAP); + error.setText("ERROR: Could not find demo-icon.svg resource.\n\n" + + "Expected location: /org/eclipse/jface/snippets/resources/demo-icon.svg\n\n" + + "Please ensure the SVG file is present in the resources folder."); + error.setForeground(display.getSystemColor(SWT.COLOR_RED)); + GridDataFactory.fillDefaults().span(3, 1).grab(true, true).applyTo(error); + return; + } + + Label info = new Label(content, SWT.WRAP); + info.setText("The same SVG file rendered at different sizes using query parameter hints:"); + GridDataFactory.fillDefaults().span(3, 1).grab(true, false).applyTo(info); + + // Demo various sizes + addImageDemo(content, "16x16 pixels", svgUrl, "?size=16x16", display); + addImageDemo(content, "32x32 pixels", svgUrl, "?size=32x32", display); + addImageDemo(content, "48x48 pixels", svgUrl, "?size=48x48", display); + addImageDemo(content, "64x64 pixels", svgUrl, "?size=64x64", display); + addImageDemo(content, "96x96 pixels", svgUrl, "?size=96x96", display); + addImageDemo(content, "128x128 pixels (native)", svgUrl, "", display); + + Label note = new Label(content, SWT.WRAP); + note.setText("\nNote: All images above are from the same SVG file. " + + "The size hints control the rendering size without modifying the source file."); + GridDataFactory.fillDefaults().span(3, 1).grab(true, false).applyTo(note); + } + + private static void createCodeExamplesTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText("Code Examples"); + + Composite content = new Composite(folder, SWT.NONE); + GridLayoutFactory.fillDefaults().margins(10, 10).applyTo(content); + tab.setControl(content); + + Text text = new Text(content, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL | SWT.H_SCROLL); + GridDataFactory.fillDefaults().grab(true, true).applyTo(text); + text.setFont(org.eclipse.jface.resource.JFaceResources.getTextFont()); + + text.setText(""" + // EXAMPLE 1: Using query parameter size hints + URL iconUrl = URI.create("bundle://my.plugin/icons/search.svg?size=16x16").toURL(); + ImageDescriptor desc = ImageDescriptor.createFromURL(iconUrl); + Image icon = desc.createImage(); + + // EXAMPLE 2: Using path-based size hints + URL iconUrl = FileLocator.find(bundle, new Path("icons/16x16/search.svg")); + ImageDescriptor desc = ImageDescriptor.createFromURL(iconUrl); + Image icon = desc.createImage(); + + // EXAMPLE 3: Dynamic sizing at runtime + String baseUrl = "bundle://my.plugin/icons/icon.svg"; + String sizeParam = "?size=" + desiredWidth + "x" + desiredHeight; + URL iconUrl = URI.create(baseUrl + sizeParam).toURL(); + ImageDescriptor desc = ImageDescriptor.createFromURL(iconUrl); + + // EXAMPLE 4: Using with ImageDescriptor registry + ImageDescriptor desc = ImageDescriptor.createFromURL( + URI.create("bundle://my.plugin/icons/toolbar.svg?size=16x16").toURL() + ); + JFaceResources.getImageRegistry().put("toolbar.icon", desc); + + // EXAMPLE 5: File URLs with query parameters + File svgFile = new File("/path/to/icon.svg"); + URL fileUrl = svgFile.toURI().toURL(); + String urlWithSize = fileUrl.toExternalForm() + "?size=32x32"; + URL sizedUrl = URI.create(urlWithSize).toURL(); + ImageDescriptor desc = ImageDescriptor.createFromURL(sizedUrl); + + // EXAMPLE 6: Multiple query parameters + URL url = URI.create("bundle://my.plugin/icons/icon.svg?theme=dark&size=24x24").toURL(); + // Note: Only 'size' parameter is used for size hints + + // IMPORTANT: Use URI.create().toURL() instead of new URL(String) + // The URL(String) constructor is deprecated in Java 21 + """); + } + + private static void createUseCasesTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText("Use Cases"); + + Composite content = new Composite(folder, SWT.NONE); + GridLayoutFactory.fillDefaults().margins(10, 10).applyTo(content); + tab.setControl(content); + + Text text = new Text(content, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + GridDataFactory.fillDefaults().grab(true, true).applyTo(text); + + text.setText(""" + Use Cases for SVG Size Hints + ============================= + + 1. TOOLBAR ICONS + Use ?size=16x16 for consistent toolbar icon sizing across different themes + and plugins, without requiring each plugin to scale their SVGs. + + Example: Command contributions can use a single SVG at toolbar size. + + 2. WIZARD IMAGES + Use ?size=128x128 or larger for wizard header graphics, while using the + same SVG at smaller sizes in tree views or preference pages. + + Example: One brand SVG for all sizes in a feature. + + 3. VIEW ICONS + Place SVGs in /icons/16x16/ folder structure for automatic sizing in + Eclipse view tabs, tree items, and list items. + + Example: Package Explorer, Project Explorer use consistent 16x16 icons. + + 4. MULTI-RESOLUTION SUPPORT + One SVG file serves all sizes without duplication. No need to maintain + separate PNG files for 16x16, 32x32, 48x48, etc. + + Example: Plugin can ship one SVG instead of 5+ PNG files. + + 5. DYNAMIC UI SCALING + Support user preferences for icon sizes or accessibility features by + dynamically adjusting query parameters at runtime. + + Example: Large icon mode in toolbars for accessibility. + + 6. THEME VARIATIONS + Combine with path-based loading to support light/dark themes while + controlling size independently. + + Example: icons/dark/16x16/icon.svg and ?size= parameter + + 7. RESPONSIVE LAYOUTS + Adjust icon sizes based on available space or zoom level without + creating multiple image files. + + Example: Sidebar icons that scale with panel width. + + 8. RETINA/HIGH-DPI DISPLAYS + Automatic scaling to 150% and 200% zoom ensures crisp rendering on + high-DPI displays without manual intervention. + + Example: MacBook Retina displays automatically get 2x scaled icons. + """); + } + + private static void addImageDemo(Composite parent, String description, URL baseUrl, String queryParam, + Display display) { + Composite demoComposite = new Composite(parent, SWT.NONE); + GridLayoutFactory.fillDefaults().applyTo(demoComposite); + GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(demoComposite); + + try { + String urlString = baseUrl.toExternalForm() + queryParam; + URL url = URI.create(urlString).toURL(); + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + Image image = descriptor.createImage(display); + + if (image == null) { + createErrorLabel(demoComposite, description, "Failed to create image from descriptor"); + return; + } + + Canvas canvas = new Canvas(demoComposite, SWT.BORDER); + int width = image.getBounds().width; + int height = image.getBounds().height; + GridDataFactory.swtDefaults().hint(width + 4, height + 4).align(SWT.CENTER, SWT.CENTER).applyTo(canvas); + + canvas.addPaintListener(e -> { + if (!image.isDisposed()) { + e.gc.drawImage(image, 2, 2); + } + }); + + Label label = new Label(demoComposite, SWT.CENTER); + label.setText(description); + GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(label); + + Label sizeLabel = new Label(demoComposite, SWT.CENTER); + sizeLabel.setText("(" + width + "×" + height + ")"); + sizeLabel.setForeground(display.getSystemColor(SWT.COLOR_DARK_GRAY)); + GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(sizeLabel); + + canvas.addDisposeListener(e -> { + if (!image.isDisposed()) { + image.dispose(); + } + }); + } catch (Exception e) { + createErrorLabel(demoComposite, description, + "Error: " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + private static void createErrorLabel(Composite parent, String description, String errorMsg) { + Label error = new Label(parent, SWT.WRAP); + error.setText(description + "\n" + errorMsg); + error.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_RED)); + GridDataFactory.fillDefaults().hint(150, SWT.DEFAULT).applyTo(error); + } +} diff --git a/examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/demo-icon.svg b/examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/demo-icon.svg new file mode 100644 index 00000000000..78c63c010f9 --- /dev/null +++ b/examples/org.eclipse.jface.snippets/Eclipse JFace Snippets/org/eclipse/jface/snippets/resources/demo-icon.svg @@ -0,0 +1,19 @@ + + + Demo Icon + A simple icon for demonstrating SVG size hints + + + + + + + + + + + + DEMO + diff --git a/tests/org.eclipse.jface.tests/icons/imagetests/16x16/test-icon.svg b/tests/org.eclipse.jface.tests/icons/imagetests/16x16/test-icon.svg new file mode 100644 index 00000000000..61178e6910f --- /dev/null +++ b/tests/org.eclipse.jface.tests/icons/imagetests/16x16/test-icon.svg @@ -0,0 +1,6 @@ + + + + + S + diff --git a/tests/org.eclipse.jface.tests/icons/imagetests/32x32/test-icon.svg b/tests/org.eclipse.jface.tests/icons/imagetests/32x32/test-icon.svg new file mode 100644 index 00000000000..61178e6910f --- /dev/null +++ b/tests/org.eclipse.jface.tests/icons/imagetests/32x32/test-icon.svg @@ -0,0 +1,6 @@ + + + + + S + diff --git a/tests/org.eclipse.jface.tests/icons/imagetests/test-icon.svg b/tests/org.eclipse.jface.tests/icons/imagetests/test-icon.svg new file mode 100644 index 00000000000..4c8ecc392a1 --- /dev/null +++ b/tests/org.eclipse.jface.tests/icons/imagetests/test-icon.svg @@ -0,0 +1,6 @@ + + + + + S + diff --git a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/AllImagesTests.java b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/AllImagesTests.java index 7761d2aa846..d4fb86cacd9 100644 --- a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/AllImagesTests.java +++ b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/AllImagesTests.java @@ -21,7 +21,8 @@ @Suite @SelectClasses({ ImageRegistryTest.class, ResourceManagerTest.class, FileImageDescriptorTest.class, - UrlImageDescriptorTest.class, DecorationOverlayIconTest.class, DeferredImageDescriptorTest.class }) + UrlImageDescriptorTest.class, URLHintProviderTest.class, DecorationOverlayIconTest.class, + DeferredImageDescriptorTest.class }) public class AllImagesTests { public static void main(String[] args) { diff --git a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/URLHintProviderTest.java b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/URLHintProviderTest.java new file mode 100644 index 00000000000..c96b89a5e18 --- /dev/null +++ b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/URLHintProviderTest.java @@ -0,0 +1,354 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich 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: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.jface.tests.images; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.widgets.Display; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for URLHintProvider functionality that detects desired image sizes from + * URL paths and query parameters. + * + *

+ * Note: SVG size hint support requires SWT 3.132+ with native SVG loading + * capabilities. Tests verify that hints are detected and passed to the loader, + * but actual rendering depends on platform SVG support. + *

+ */ +public class URLHintProviderTest { + + private Display display; + + @BeforeEach + public void setUp() { + display = Display.getDefault(); + } + + @AfterEach + public void tearDown() { + // Display is shared, don't dispose + } + + @Test + public void testPathBasedHintDetection16x16() throws Exception { + URL url = URLHintProviderTest.class.getResource("/icons/imagetests/16x16/test-icon.svg"); + assertNotNull(url, "Test SVG not found"); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + // If SVG size hints are supported, should be 16x16, otherwise native 128x128 + assertEquals(16, imageData.width, "Width should be 16 based on path hint"); + assertEquals(16, imageData.height, "Height should be 16 based on path hint"); + } + + @Test + public void testPathBasedHintDetection32x32() throws Exception { + URL url = URLHintProviderTest.class.getResource("/icons/imagetests/32x32/test-icon.svg"); + assertNotNull(url, "Test SVG not found"); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(32, imageData.width, "Width should be 32 based on path hint"); + assertEquals(32, imageData.height, "Height should be 32 based on path hint"); + } + + @Test + public void testPathBasedHintDetectionZoom200() throws Exception { + URL url = URLHintProviderTest.class.getResource("/icons/imagetests/16x16/test-icon.svg"); + assertNotNull(url, "Test SVG not found"); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(200); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(32, imageData.width, "Width should be 32 (16*2) at 200% zoom"); + assertEquals(32, imageData.height, "Height should be 32 (16*2) at 200% zoom"); + } + + @Test + public void testPathBasedHintDetectionZoom150() throws Exception { + URL url = URLHintProviderTest.class.getResource("/icons/imagetests/16x16/test-icon.svg"); + assertNotNull(url, "Test SVG not found"); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(150); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(24, imageData.width, "Width should be 24 (16*1.5) at 150% zoom"); + assertEquals(24, imageData.height, "Height should be 24 (16*1.5) at 150% zoom"); + } + + @Test + public void testQueryParameterHintDetection() throws Exception { + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + // Create URL with query parameter - using jar: protocol from class loader + String urlString = baseUrl.toExternalForm(); + URL url = URI.create(urlString + "?size=16x16").toURL(); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(16, imageData.width, "Width should be 16 based on query parameter"); + assertEquals(16, imageData.height, "Height should be 16 based on query parameter"); + } + + @Test + public void testQueryParameterHintDetection64x64() throws Exception { + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + String urlString = baseUrl.toExternalForm(); + URL url = URI.create(urlString + "?size=64x64").toURL(); + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(64, imageData.width, "Width should be 64 based on query parameter"); + assertEquals(64, imageData.height, "Height should be 64 based on query parameter"); + } + + @Test + public void testQueryParameterWithZoom200() throws Exception { + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + String urlString = baseUrl.toExternalForm(); + URL url = URI.create(urlString + "?size=16x16").toURL(); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(200); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(32, imageData.width, "Width should be 32 (16*2) at 200% zoom"); + assertEquals(32, imageData.height, "Height should be 32 (16*2) at 200% zoom"); + } + + @Test + public void testQueryParameterRectangularSize() throws Exception { + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + String urlString = baseUrl.toExternalForm(); + URL url = URI.create(urlString + "?size=48x32").toURL(); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(48, imageData.width, "Width should be 48 based on query parameter"); + assertEquals(32, imageData.height, "Height should be 32 based on query parameter"); + } + + @Test + public void testNoHintDefaultSize() throws Exception { + URL url = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(url, "Test SVG not found"); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + // Without hint, SVG should render at its native viewBox size (128x128) + assertEquals(128, imageData.width, "Width should be 128 (native SVG size)"); + assertEquals(128, imageData.height, "Height should be 128 (native SVG size)"); + } + + @Test + public void testQueryParameterPrecedenceOverPath() throws Exception { + // Query parameter should take precedence over path hint + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/16x16/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + String urlString = baseUrl.toExternalForm(); + URL url = URI.create(urlString + "?size=64x64").toURL(); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(64, imageData.width, "Width should be 64 from query parameter, not 16 from path"); + assertEquals(64, imageData.height, "Height should be 64 from query parameter, not 16 from path"); + } + + @Test + public void testQueryParameterWithMultipleParams() throws Exception { + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + String urlString = baseUrl.toExternalForm(); + // Test with multiple query parameters + URL url = URI.create(urlString + "?foo=bar&size=24x24&other=value").toURL(); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(24, imageData.width, "Width should be 24 from query parameter"); + assertEquals(24, imageData.height, "Height should be 24 from query parameter"); + } + + /** + * Tests that file: URLs can have query parameters for size hints. This is + * important because file URLs need special handling - the query parameter + * should be used for size detection but stripped when accessing the actual + * file. + */ + @Test + public void testFileURLWithQueryParameter() throws IOException { + // Copy test SVG to a temporary file + URL resourceUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(resourceUrl, "Test SVG not found"); + + File tempSvg = File.createTempFile("test-icon", ".svg"); + try (InputStream openStream = resourceUrl.openStream()) { + Files.copy(openStream, tempSvg.toPath(), StandardCopyOption.REPLACE_EXISTING); + + // Create file: URL with query parameter + URL fileUrl = tempSvg.toURI().toURL(); + String fileUrlWithQuery = fileUrl.toExternalForm() + "?size=16x16"; + URL fileUrlQuery = URI.create(fileUrlWithQuery).toURL(); + + // Verify URL has file protocol and query parameter + assertEquals("file", fileUrlQuery.getProtocol(), "Should be a file URL"); + assertEquals("size=16x16", fileUrlQuery.getQuery(), "Query parameter should be preserved"); + + // Test that ImageDescriptor can load the image with query parameter + ImageDescriptor descriptor = ImageDescriptor.createFromURL(fileUrlQuery); + ImageData imageData = descriptor.getImageData(100); + + assertNotNull(imageData, "ImageData should not be null for file URL with query parameter"); + assertEquals(16, imageData.width, "Width should be 16 based on query parameter"); + assertEquals(16, imageData.height, "Height should be 16 based on query parameter"); + } finally { + tempSvg.delete(); + } + } + + /** + * Tests that file: URLs with query parameters work at different zoom levels. + */ + @Test + public void testFileURLWithQueryParameterZoom() throws IOException { + // Copy test SVG to a temporary file + URL resourceUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(resourceUrl, "Test SVG not found"); + + File tempSvg = File.createTempFile("test-icon", ".svg"); + try (InputStream openStream = resourceUrl.openStream()) { + Files.copy(openStream, tempSvg.toPath(), StandardCopyOption.REPLACE_EXISTING); + + // Create file: URL with query parameter + URL fileUrl = tempSvg.toURI().toURL(); + String fileUrlWithQuery = fileUrl.toExternalForm() + "?size=16x16"; + URL fileUrlQuery = URI.create(fileUrlWithQuery).toURL(); + + // Test at 200% zoom + ImageDescriptor descriptor = ImageDescriptor.createFromURL(fileUrlQuery); + ImageData imageData = descriptor.getImageData(200); + + assertNotNull(imageData, "ImageData should not be null"); + assertEquals(32, imageData.width, "Width should be 32 (16*2) at 200% zoom"); + assertEquals(32, imageData.height, "Height should be 32 (16*2) at 200% zoom"); + } finally { + tempSvg.delete(); + } + } + + /** + * Tests that file: URLs with query parameters work when creating Image objects + * (not just ImageData). This tests the ImageFileNameProvider code path. + */ + @Test + public void testFileURLWithQueryParameterCreateImage() throws IOException { + // Copy test SVG to a temporary file + URL resourceUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(resourceUrl, "Test SVG not found"); + + File tempSvg = File.createTempFile("test-icon", ".svg"); + try (InputStream openStream = resourceUrl.openStream()) { + Files.copy(openStream, tempSvg.toPath(), StandardCopyOption.REPLACE_EXISTING); + + // Create file: URL with query parameter + URL fileUrl = tempSvg.toURI().toURL(); + String fileUrlWithQuery = fileUrl.toExternalForm() + "?size=32x32"; + URL fileUrlQuery = URI.create(fileUrlWithQuery).toURL(); + + // Test that ImageDescriptor can create Image (not just ImageData) with query parameter + ImageDescriptor descriptor = ImageDescriptor.createFromURL(fileUrlQuery); + Image image = descriptor.createImage(display); + + try { + assertNotNull(image, "Image should not be null for file URL with query parameter"); + assertEquals(32, image.getBounds().width, "Width should be 32 based on query parameter"); + assertEquals(32, image.getBounds().height, "Height should be 32 based on query parameter"); + } finally { + if (image != null) { + image.dispose(); + } + } + } finally { + tempSvg.delete(); + } + } + + /** + * Tests that jar: URLs with query parameters work when creating Image objects. + * This is the most common use case in Eclipse plugins. + */ + @Test + public void testJarURLWithQueryParameterCreateImage() throws Exception { + URL baseUrl = URLHintProviderTest.class.getResource("/icons/imagetests/test-icon.svg"); + assertNotNull(baseUrl, "Test SVG not found"); + + // Add query parameter to jar: URL + String urlString = baseUrl.toExternalForm(); + URL url = URI.create(urlString + "?size=48x48").toURL(); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); + Image image = descriptor.createImage(display); + + try { + assertNotNull(image, "Image should not be null"); + assertEquals(48, image.getBounds().width, "Width should be 48 based on query parameter"); + assertEquals(48, image.getBounds().height, "Height should be 48 based on query parameter"); + } finally { + if (image != null) { + image.dispose(); + } + } + } +}