Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow-Up Feature: Icon-Enablement with Rasterization of SVGs #1647

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Fragment-Host: org.eclipse.swt;bundle-version="[3.128.0,4.0.0)"
Bundle-Name: %fragmentName
Bundle-Vendor: %providerName
Bundle-SymbolicName: org.eclipse.swt.win32.win32.x86_64; singleton:=true
Bundle-Version: 3.129.0.qualifier
Bundle-Version: 4.0.0.qualifier
Bundle-ManifestVersion: 2
Bundle-Localization: fragment
Export-Package:
Expand Down
8 changes: 8 additions & 0 deletions bundles/org.eclipse.swt.svg/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="lib" path="libs/jsvg-1.6.1.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
28 changes: 28 additions & 0 deletions bundles/org.eclipse.swt.svg/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.eclipse.swt.svg</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17
11 changes: 11 additions & 0 deletions bundles/org.eclipse.swt.svg/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SvgPlugin
Bundle-SymbolicName: org.eclipse.swt.svg
Bundle-Version: 1.0.0.qualifier
Automatic-Module-Name: org.eclipse.swt.svgPlugin
Bundle-RequiredExecutionEnvironment: JavaSE-17
Export-Package: org.eclipse.swt.svg
Import-Package: org.eclipse.swt,
org.eclipse.swt.graphics
Bundle-ClassPath: ., libs/jsvg-1.6.1.jar
5 changes: 5 additions & 0 deletions bundles/org.eclipse.swt.svg/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.,\
libs/jsvg-1.6.1.jar
Binary file added bundles/org.eclipse.swt.svg/libs/jsvg-1.6.1.jar
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*******************************************************************************
* Copyright (c) 2025 Vector Informatik GmbH 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: Michael Bangas (Vector Informatik GmbH) - initial API and implementation
*******************************************************************************/
package org.eclipse.swt.svg;

import static java.awt.RenderingHints.*;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.swt.graphics.SVGRasterizer;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.SVGRasterizerRegistry;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import com.github.weisj.jsvg.*;
import com.github.weisj.jsvg.geometry.size.*;
import com.github.weisj.jsvg.parser.*;

/**
* A rasterizer implementation for converting SVG data into rasterized images.
* This class implements the {@code ISVGRasterizer} interface.
*
* @since 1.0.0
*/
public class JSVGRasterizer implements SVGRasterizer {

private SVGLoader svgLoader;

/**
* Initializes the SVG rasterizer by registering an instance of this rasterizer
* with the {@link SVGRasterizerRegistry}.
*/
public static void intializeJSVGRasterizer() {
SVGRasterizerRegistry.register(new JSVGRasterizer());
}

private final static Map<Key, Object> RENDERING_HINTS = Map.of(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON, //
KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY, //
KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY, //
KEY_DITHERING, VALUE_DITHER_DISABLE, //
KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON, //
KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC, //
KEY_RENDERING, VALUE_RENDER_QUALITY, //
KEY_STROKE_CONTROL, VALUE_STROKE_PURE, //
KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON //
);

@Override
public ImageData rasterizeSVG(InputStream stream, float scalingFactor) throws IOException {
if (stream == null) {
throw new IllegalArgumentException("InputStream cannot be null");
}
if(svgLoader == null) {
svgLoader = new SVGLoader();
}
return rasterize(stream, scalingFactor);
}

@Override
public ImageData rasterizeDisabledSVG(InputStream stream, float scalingFactor) throws IOException {
if(svgLoader == null) {
svgLoader = new SVGLoader();
}
InputStream disabledStream = applyDisabledLook(stream);
return rasterize(disabledStream, scalingFactor);
}

@Override
public ImageData rasterizeGraySVG(InputStream stream, float scalingFactor) throws IOException {
if(svgLoader == null) {
svgLoader = new SVGLoader();
}
InputStream disabledStream = applyGrayLook(stream);
return rasterize(disabledStream, scalingFactor);
}

private ImageData rasterize(InputStream stream, float scalingFactor) throws IOException {
SVGDocument svgDocument = null;
stream.mark(Integer.MAX_VALUE);
InputStream nonClosingStream = new FilterInputStream(stream) {
@Override
public void close() throws IOException {
// Do nothing to prevent closing the underlying stream
}
};
svgDocument = svgLoader.load(nonClosingStream, null, LoaderContext.createDefault());
stream.reset();
if (svgDocument != null) {
FloatSize size = svgDocument.size();
double originalWidth = size.getWidth();
double originalHeight = size.getHeight();
int scaledWidth = (int) Math.round(originalWidth * scalingFactor);
int scaledHeight = (int) Math.round(originalHeight * scalingFactor);
BufferedImage image = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setRenderingHints(RENDERING_HINTS);
g.scale(scalingFactor, scalingFactor);
svgDocument.render(null, g);
g.dispose();
return convertToSWT(image);
}
return null;
}

private static InputStream applyDisabledLook(InputStream svgInputStream) throws IOException {
Document svgDocument = parseSVG(svgInputStream);
addDisabledFilter(svgDocument);
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
writeSVG(svgDocument, outputStream);
return new ByteArrayInputStream(outputStream.toByteArray());
}
}

private static InputStream applyGrayLook(InputStream svgInputStream) throws IOException {
Document svgDocument = parseSVG(svgInputStream);
addGrayFilter(svgDocument);
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
writeSVG(svgDocument, outputStream);
return new ByteArrayInputStream(outputStream.toByteArray());
}
}

private static Document parseSVG(InputStream inputStream) throws IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
return builder.parse(inputStream);
} catch (SAXException | IOException | ParserConfigurationException e) {
throw new IOException(e.getMessage());
}
}

private static void addDisabledFilter(Document document) {
addFilter(document, 0.64f, 0.4f);
}

private static void addGrayFilter(Document document) {
addFilter(document, 0.64f, 0.1f);
}

private static void addFilter(Document document, float slope, float intercept) {
Element defs = (Element) document.getElementsByTagName("defs").item(0);
if (defs == null) {
defs = document.createElement("defs");
document.getDocumentElement().appendChild(defs);
}

Element filter = document.createElement("filter");
filter.setAttribute("id", "customizedLook");

Element colorMatrix = document.createElement("feColorMatrix");
colorMatrix.setAttribute("type", "saturate");
colorMatrix.setAttribute("values", "0");
filter.appendChild(colorMatrix);

Element componentTransfer = document.createElement("feComponentTransfer");
for (String channel : new String[] { "R", "G", "B" }) {
Element func = document.createElement("feFunc" + channel);
func.setAttribute("type", "linear");
func.setAttribute("slope", Float.toString(slope));
func.setAttribute("intercept", Float.toString(intercept));
componentTransfer.appendChild(func);
}
filter.appendChild(componentTransfer);
defs.appendChild(filter);
document.getDocumentElement().setAttribute("filter", "url(#customizedLook)");
}

private static void writeSVG(Document document, OutputStream outputStream) throws IOException {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer;
try {
transformer = transformerFactory.newTransformer();
transformer.transform(new DOMSource(document), new StreamResult(outputStream));
} catch (TransformerException e) {
throw new IOException(e.getMessage());
}
}

private ImageData convertToSWT(BufferedImage bufferedImage) {
if (bufferedImage.getColorModel() instanceof DirectColorModel) {
DirectColorModel colorModel = (DirectColorModel)bufferedImage.getColorModel();
PaletteData palette = new PaletteData(
colorModel.getRedMask(),
colorModel.getGreenMask(),
colorModel.getBlueMask());
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(),
colorModel.getPixelSize(), palette);
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
int rgb = bufferedImage.getRGB(x, y);
int pixel = palette.getPixel(new RGB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF));
data.setPixel(x, y, pixel);
if (colorModel.hasAlpha()) {
data.setAlpha(x, y, (rgb >> 24) & 0xFF);
}
}
}
return data;
}
else if (bufferedImage.getColorModel() instanceof IndexColorModel) {
IndexColorModel colorModel = (IndexColorModel)bufferedImage.getColorModel();
int size = colorModel.getMapSize();
byte[] reds = new byte[size];
byte[] greens = new byte[size];
byte[] blues = new byte[size];
colorModel.getReds(reds);
colorModel.getGreens(greens);
colorModel.getBlues(blues);
RGB[] rgbs = new RGB[size];
for (int i = 0; i < rgbs.length; i++) {
rgbs[i] = new RGB(reds[i] & 0xFF, greens[i] & 0xFF, blues[i] & 0xFF);
}
PaletteData palette = new PaletteData(rgbs);
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(),
colorModel.getPixelSize(), palette);
data.transparentPixel = colorModel.getTransparentPixel();
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[1];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
raster.getPixel(x, y, pixelArray);
data.setPixel(x, y, pixelArray[0]);
}
}
return data;
}
else if (bufferedImage.getColorModel() instanceof ComponentColorModel) {
ComponentColorModel colorModel = (ComponentColorModel)bufferedImage.getColorModel();
//ASSUMES: 3 BYTE BGR IMAGE TYPE
PaletteData palette = new PaletteData(0x0000FF, 0x00FF00,0xFF0000);
ImageData data = new ImageData(bufferedImage.getWidth(), bufferedImage.getHeight(),
colorModel.getPixelSize(), palette);
//This is valid because we are using a 3-byte Data model with no transparent pixels
data.transparentPixel = -1;
WritableRaster raster = bufferedImage.getRaster();
int[] pixelArray = new int[3];
for (int y = 0; y < data.height; y++) {
for (int x = 0; x < data.width; x++) {
raster.getPixel(x, y, pixelArray);
int pixel = palette.getPixel(new RGB(pixelArray[0], pixelArray[1], pixelArray[2]));
data.setPixel(x, y, pixel);
}
}
return data;
}
return null;
}

public boolean isSVGFile(InputStream stream) throws IOException {
if (stream == null) {
throw new IllegalArgumentException("InputStream cannot be null");
}
stream.mark(Integer.MAX_VALUE);
try {
int firstByte = stream.read();
return firstByte == '<';
} finally {
stream.reset();
}
}
}
Loading