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(); + } + } + } +}