Skip to content

Commit

Permalink
svggen: support colors in non-sRGB color spaces, via color() function
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosame committed Jul 11, 2024
1 parent 12e0235 commit 331be27
Show file tree
Hide file tree
Showing 11 changed files with 541 additions and 20 deletions.
112 changes: 104 additions & 8 deletions echosvg-svggen/src/main/java/io/sf/carte/echosvg/svggen/SVGColor.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,19 @@

import java.awt.Color;
import java.awt.Paint;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import io.sf.carte.echosvg.ext.awt.g2d.GraphicContext;

Expand Down Expand Up @@ -56,6 +67,8 @@ public class SVGColor extends AbstractSVGConverter {
public static final Color white = Color.white;
public static final Color yellow = Color.yellow;

private static final Set<String> cssProfileNames;

/**
* Color map maps Color values to HTML 4.0 color names
*/
Expand All @@ -78,6 +91,15 @@ public class SVGColor extends AbstractSVGConverter {
colorMap.put(blue, "blue");
colorMap.put(teal, "teal");
colorMap.put(aqua, "aqua");

/*
* CSS standard color spaces. A98-rgb and Prophoto-rgb are never going to match
* in practice and probably should be removed; they are left for now, just in
* case they are useful.
*/
String[] knownProfiles = { "display-p3", "a98-rgb", "prophoto-rgb", "rec2020" };
cssProfileNames = new HashSet<>(knownProfiles.length);
Collections.addAll(cssProfileNames, knownProfiles);
}

/**
Expand Down Expand Up @@ -114,14 +136,7 @@ public static SVGPaintDescriptor toSVG(Color color, SVGGeneratorContext gc) {
String cssColor = colorMap.get(color);
if (cssColor == null) {
// color is not one of the predefined colors
StringBuilder cssColorBuffer = new StringBuilder(RGB_PREFIX);
cssColorBuffer.append(color.getRed());
cssColorBuffer.append(COMMA);
cssColorBuffer.append(color.getGreen());
cssColorBuffer.append(COMMA);
cssColorBuffer.append(color.getBlue());
cssColorBuffer.append(RGB_SUFFIX);
cssColor = cssColorBuffer.toString();
cssColor = serializeColor(color);
}

//
Expand All @@ -134,4 +149,85 @@ public static SVGPaintDescriptor toSVG(Color color, SVGGeneratorContext gc) {
return new SVGPaintDescriptor(cssColor, alphaString);
}

private static String serializeColor(Color color) {
StringBuilder cssColorBuffer;
ColorSpace cs = color.getColorSpace();
if (!cs.isCS_sRGB()) {
float[] comps = color.getColorComponents(null);
String csName;
if (!(cs instanceof ICC_ColorSpace) || (csName = lcColorProfileName((ICC_ColorSpace) cs)) == null
|| !cssProfileNames.contains(csName)) {
// Not a known CSS color profile, let's use XYZ
csName = "xyz-d50";
comps = cs.toCIEXYZ(comps);
}
DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.ROOT);
DecimalFormat df = new DecimalFormat("#.#", dfs);
df.setMaximumFractionDigits(6);
cssColorBuffer = new StringBuilder(csName.length() + 34);
cssColorBuffer.append("color(").append(csName);
for (float comp : comps) {
cssColorBuffer.append(' ').append(df.format(comp));
}
cssColorBuffer.append(')');
} else {
cssColorBuffer = new StringBuilder(RGB_PREFIX);
cssColorBuffer.append(color.getRed());
cssColorBuffer.append(COMMA);
cssColorBuffer.append(color.getGreen());
cssColorBuffer.append(COMMA);
cssColorBuffer.append(color.getBlue());
cssColorBuffer.append(RGB_SUFFIX);
}
return cssColorBuffer.toString();
}

private static String lcColorProfileName(ICC_ColorSpace cs) {
ICC_Profile profile = cs.getProfile();
byte[] bdesc = profile.getData(ICC_Profile.icSigProfileDescriptionTag);
/*
* The profile description tag is of type multiLocalizedUnicodeType which starts
* with a 'mluc' (see paragraph 10.15 of ICC specification
* https://www.color.org/specification/ICC.1-2022-05.pdf).
*/
final byte[] mluc = { 'm', 'l', 'u', 'c' };
String iccProfileName = null;
if (bdesc != null && Arrays.equals(bdesc, 0, 4, mluc, 0, 4)) {
int numrec = uInt32Number(bdesc, 8);
if (numrec > 0) {
int len = uInt32Number(bdesc, 20);
int offset = uInt32Number(bdesc, 24);
int maxlen = bdesc.length - offset;
if (maxlen > 0) {
len = Math.min(len, maxlen);
// This isn't always the name of the color space, but let's try
iccProfileName = new String(bdesc, offset, len, StandardCharsets.UTF_16BE).trim();
iccProfileName = iccProfileName.toLowerCase(Locale.ROOT).replace(' ', '-');
if (iccProfileName.contains("bt.2020")) {
// recommendation 2020
iccProfileName = "rec2020";
} else if ("adobe-rgb-(1998)".equals(iccProfileName)) {
// A98
iccProfileName = "a98-rgb";
}
}
}
}
return iccProfileName;
}

/**
* Convert four bytes into a big-endian unsigned 32-bit integer.
*
* @param bytes the array of bytes.
* @param offset the offset at which to start the conversion.
* @return the 32-bit integer.
*/
private static int uInt32Number(byte[] bytes, int offset) {
// Computation is carried out as a long integer, to avoid potential overflows
long value = (bytes[offset + 3] & 0xFF) | ((bytes[offset + 2] & 0xFF) << 8)
| ((bytes[offset + 1] & 0xFF) << 16) | ((long) (bytes[offset] & 0xFF) << 24);
return (int) value;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.font.TextAttribute;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import io.sf.carte.echosvg.test.TestFonts;

/**
* This test validates the convertion of Java 2D text into SVG Shapes, one of
* the options of the SVGGraphics2D constructor.
* This test validates the convertion of Java 2D text with profiled colors into
* SVG Shapes, one of the capabilities of the SVGGraphics2D.
*
* @author See Git history.
* @version $Id$
Expand Down Expand Up @@ -63,14 +69,61 @@ public void paint(Graphics2D g) {
attributes2.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
Font fontUL = new Font(attributes2);

// Prepare a color
Color stColor;
// Load a color profile that does not match a known space (although it is).
try (InputStream is = getClass().getResourceAsStream(
"/io/sf/carte/echosvg/css/color/profiles/LargeRGB-elle-V4-g18.icc")) {
ICC_Profile profile = ICC_Profile.getInstance(is);
ICC_ColorSpace cs = new ICC_ColorSpace(profile);
float[] comps = { 0.33f, 0.34f, 0.43f };
stColor = new Color(cs, comps , 1f);
} catch (IOException e) {
stColor = new Color(0x666699);
}
// The color will be translated to XYZ (D50)

// Set the STRIKETHROUGH font and a color
g.setFont(fontST);
g.setPaint(new Color(0x666699));
g.setPaint(stColor);
// Draw a string
g.drawString("Strike Through", 10, 40);

// Now draw with a different color and the UNDERLINE font
g.setPaint(Color.black);
/*
* Draw some figure with a non-sRGB color
*/
Color lColor;
// Load another color profile
try (InputStream is = getClass().getResourceAsStream(
"/io/sf/carte/echosvg/css/color/profiles/ITU-R_BT2020.icc")) {
ICC_Profile profile = ICC_Profile.getInstance(is);
ICC_ColorSpace cs = new ICC_ColorSpace(profile);
float[] comps = { .55f, .6f, .34f };
lColor = new Color(cs, comps , 1f);
} catch (IOException e) {
lColor = Color.magenta;
}

// Now draw with the new color
g.setPaint(lColor);
g.draw(new Line2D.Float(60, 46, 60, 80));
g.fill(new Ellipse2D.Float(56, 53, 8, 20));

// Prepare a new color
Color ulColor;
// Load a color profile
try (InputStream is = getClass().getResourceAsStream(
"/io/sf/carte/echosvg/css/color/profiles/Display P3.icc")) {
ICC_Profile profile = ICC_Profile.getInstance(is);
ICC_ColorSpace cs = new ICC_ColorSpace(profile);
float[] comps = { .36f, .35f, .33f };
ulColor = new Color(cs, comps , 1f);
} catch (IOException e) {
ulColor = Color.black;
}

// Now draw with the new color and the UNDERLINE font
g.setPaint(ulColor);
g.setFont(fontUL);
g.translate(0, 30);
// Draw a new string
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions test-references/io/sf/carte/echosvg/svggen/FontDecoration.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading

0 comments on commit 331be27

Please sign in to comment.