Skip to content

Commit

Permalink
transcoder: compute image sizes in CSS context where appropriate
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosame committed Aug 5, 2024
1 parent e3b97a3 commit 78a4545
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import io.sf.carte.echosvg.gvt.CanvasGraphicsNode;
import io.sf.carte.echosvg.gvt.CompositeGraphicsNode;
import io.sf.carte.echosvg.gvt.GraphicsNode;
import io.sf.carte.echosvg.transcoder.impl.SizingHelper;
import io.sf.carte.echosvg.transcoder.keys.BooleanKey;
import io.sf.carte.echosvg.transcoder.keys.FloatKey;
import io.sf.carte.echosvg.transcoder.keys.LengthKey;
Expand Down Expand Up @@ -389,6 +390,9 @@ private SVGOMDocument importAsSVGDocument(Document document, String uri)
}
// Set the right namespace
docElm = replaceSVGRoot(docElm);
} else {
// We are in CSS context
SizingHelper.defaultDimensions(docElm);
}
}
}
Expand Down Expand Up @@ -421,6 +425,11 @@ private Element replaceSVGRoot(Element docElm) {
XMLConstants.XMLNS_ATTRIBUTE, SVGConstants.SVG_NAMESPACE_URI);
Element newRoot = replaceNamespace(docElm);
docElm.getParentNode().replaceChild(newRoot, docElm);

// We are in CSS context and need to apply some rules
// see https://svgwg.org/specs/integration/#svg-css-sizing
SizingHelper.defaultDimensions(newRoot);

return newRoot;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
See the NOTICE file distributed with this work for additional
information regarding copyright ownership.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package io.sf.carte.echosvg.transcoder.impl;

import org.w3c.dom.DOMException;
import org.w3c.dom.Element;

import io.sf.carte.doc.style.css.CSSUnit;
import io.sf.carte.doc.style.css.CSSValue.CssType;
import io.sf.carte.doc.style.css.property.ExpressionValue;
import io.sf.carte.doc.style.css.property.PercentageEvaluator;
import io.sf.carte.doc.style.css.property.StyleValue;
import io.sf.carte.doc.style.css.property.TypedValue;
import io.sf.carte.doc.style.css.property.ValueFactory;
import io.sf.carte.doc.style.css.property.ValueList;
import io.sf.carte.echosvg.transcoder.TranscoderException;
import io.sf.carte.echosvg.util.CSSConstants;

/**
* Helper methods for SVG sizing.
*/
public class SizingHelper {

// Prevent instantiation
SizingHelper() {
super();
}

/**
* Apply the CSS context rules.
* <p>
* See <a href="https://svgwg.org/specs/integration/#svg-css-sizing">Sizing SVG
* content in CSS context</a>.
* </p>
* <p>
* To resolve 'auto' value on ‘svg’ element if the ‘viewBox’ attribute is not
* specified:
* </p>
* <ul>
* <li>Use author specified width and height ‘svg’ element attributes.</li>
* <li>If any of the sizing attributes are missing, resolve the missing ‘svg’
* element width to '300px' and missing height to '150px' (using CSS 2.1
* replaced elements size calculation).</li>
* </ul>
* <p>
* To resolve 'auto' value on ‘svg’ element if the ‘viewBox’ attribute is
* specified:
* </p>
* <ul>
* <li>Use the ‘viewBox’ attribute to calculate the intrinsic aspect ratio of
* the ‘svg’ element.</li>
* <li>If the width or height attributes are not specified - keep not specified
* width or height as 'auto'.</li>
* </ul>
*
* @param svgRoot the outer svg element.
*/
public static void defaultDimensions(Element svgRoot) {
String width = svgRoot.getAttribute("width").trim();
String height = svgRoot.getAttribute("height").trim();

if (width.isEmpty() || CSSConstants.CSS_AUTO_VALUE.equalsIgnoreCase(width)) {
defaultWidth(svgRoot, height);
}

if (height.isEmpty() || CSSConstants.CSS_AUTO_VALUE.equalsIgnoreCase(height)) {
String viewBox = svgRoot.getAttribute("viewBox").trim();
if (viewBox.isEmpty()) {
svgRoot.setAttribute("height", "150px");
}
}
}

private static void defaultWidth(Element svgRoot, String height) {
String viewBox = svgRoot.getAttribute("viewBox").trim();
String width;
if (viewBox.isEmpty()) {
width = "300px";
} else if (!height.isEmpty() && !CSSConstants.CSS_AUTO_VALUE.equalsIgnoreCase(height)) {
try {
float ratio = computeAspectRatio(viewBox);
float fh = floatValue(height);
if (!Float.isNaN(ratio) && !Float.isNaN(fh)) {
width = Float.toString(ratio * fh);
} else {
width = "300px";
}
} catch (TranscoderException e) {
// Leave as it is, maybe the value has to be computed
return;
} catch (Exception e) {
// Any other exception: set to 300
width = "300px";
}
} else {
return;
}

svgRoot.setAttribute("width", width);
}

static float computeAspectRatio(String viewBox) throws DOMException {
ValueFactory factory = new ValueFactory();
StyleValue value = factory.parseProperty(viewBox);
float[] numbers = new float[4];
if (!computeRectangle(value, numbers)) {
throw new DOMException(DOMException.SYNTAX_ERR, "Wrong viewBox attribute.");
}
return numbers[2] / numbers[3];
}

static boolean computeRectangle(StyleValue value, float[] numbers) throws DOMException {
if (value.getCssValueType() != CssType.LIST) {
return false;
}
ValueList list = (ValueList) value;
if (list.getLength() != 4) {
return false;
}

for (int i = 0; i < 4; i++) {
StyleValue item = list.item(i);
if (item.getCssValueType() != CssType.TYPED) {
return false;
}
TypedValue typed;
switch (item.getPrimitiveType()) {
case NUMERIC:
typed = (TypedValue) item;
if (typed.getUnitType() != CSSUnit.CSS_NUMBER) {
return false;
}
break;
case EXPRESSION:
PercentageEvaluator eval = new PercentageEvaluator();
typed = eval.evaluateExpression((ExpressionValue) item);
if (typed.getUnitType() != CSSUnit.CSS_NUMBER) {
return false;
}
break;
default:
return false;
}
numbers[i] = typed.getFloatValue(CSSUnit.CSS_NUMBER);
}
return true;
}

static float floatValue(String number) throws TranscoderException, DOMException {
ValueFactory factory = new ValueFactory();
StyleValue value = factory.parseProperty(number);

if (value.getCssValueType() != CssType.TYPED) {
throw new TranscoderException("Leave value unchanged.");
}

TypedValue typed;
switch (value.getPrimitiveType()) {
case NUMERIC:
typed = (TypedValue) value;
if (typed.getUnitType() != CSSUnit.CSS_NUMBER) {
if (CSSUnit.isRelativeLengthUnitType(typed.getUnitType())) {
throw new TranscoderException("Leave value unchanged.");
}
// Either an absolute length or a wrong value
return typed.getFloatValue(CSSUnit.CSS_PX);
}
break;
case EXPRESSION:
PercentageEvaluator eval = new PercentageEvaluator();
typed = eval.evaluateExpression((ExpressionValue) value);
if (typed.getUnitType() != CSSUnit.CSS_NUMBER) {
if (CSSUnit.isRelativeLengthUnitType(typed.getUnitType())) {
throw new TranscoderException("Leave value unchanged.");
}
// Either an absolute length or a wrong value
return typed.getFloatValue(CSSUnit.CSS_PX);
}
break;
default:
throw new DOMException(DOMException.SYNTAX_ERR, "Wrong dimension.");
}

return typed.getFloatValue(CSSUnit.CSS_NUMBER);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
See the NOTICE file distributed with this work for additional
information regarding copyright ownership.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
* Implementation classes, do not use outside of EchoSVG.
*/
package io.sf.carte.echosvg.transcoder.impl;
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
import io.sf.carte.echosvg.transcoder.TranscodingHints;
import io.sf.carte.echosvg.transcoder.image.ImageTranscoder;
import io.sf.carte.echosvg.transcoder.image.PNGTranscoder;
import io.sf.carte.echosvg.transcoder.impl.SizingHelper;
import io.sf.carte.echosvg.util.ParsedURL;
import io.sf.carte.echosvg.util.SVGConstants;
import io.sf.carte.util.agent.AgentUtil;
Expand Down Expand Up @@ -721,6 +722,10 @@ private void transcodeDOMDocument(DOMDocument document, TranscoderOutput output,
}
}

// We are in CSS context, need to apply some rules
// see https://svgwg.org/specs/integration/#svg-css-sizing
SizingHelper.defaultDimensions(svgRoot);

boolean isSVG12;
String version = svgRoot.getAttribute("version");
if (version.length() == 0) {
Expand Down
2 changes: 1 addition & 1 deletion samples/tests/spec/styling/css2.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
</head>
<body>
<!-- svg elements without a namespace declaration are legal in plain HTML -->
<svg width="500" height="500" id="theSVG">
<svg height="500" viewBox="0 0 450 500" id="theSVG">
<style>
/*<![CDATA[*/

Expand Down
2 changes: 1 addition & 1 deletion samples/tests/spec/styling/css2.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ text.itemdesc {
</style>
</head>
<body>
<s:svg xmlns:s="http://www.w3.org/2000/svg" width="500" height="500">
<s:svg xmlns:s="http://www.w3.org/2000/svg" height="500" viewBox="0 0 450 500">
<s:style>
/*<![CDATA[*/

Expand Down
2 changes: 1 addition & 1 deletion samples/tests/spec2/styling/css3.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<link rel="alternate stylesheet" type="text/css" title="Gray" href="../../resources/style/gray.css"/>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" id="theSVG">
<svg xmlns="http://www.w3.org/2000/svg" height="500" viewBox="0 0 450 500" id="theSVG">
<style>
/*<![CDATA[*/

Expand Down
2 changes: 1 addition & 1 deletion samples/tests/spec2/styling/mermaid-color4.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test-references/samples/tests/spec/styling/css2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test-references/samples/tests/spec2/styling/css3-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test-references/samples/tests/spec2/styling/css3-print.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test-references/samples/tests/spec2/styling/css3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test-references/samples/tests/spec2/styling/css3_Gray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test-references/samples/tests/spec2/styling/mermaid-color4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 78a4545

Please sign in to comment.