diff --git a/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/SVGAbstractTranscoder.java b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/SVGAbstractTranscoder.java index 88b93a3e4..d21b69cc4 100644 --- a/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/SVGAbstractTranscoder.java +++ b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/SVGAbstractTranscoder.java @@ -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; @@ -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); } } } @@ -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; } diff --git a/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/impl/SizingHelper.java b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/impl/SizingHelper.java new file mode 100644 index 000000000..9b8203b9f --- /dev/null +++ b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/impl/SizingHelper.java @@ -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. + *

+ * See Sizing SVG + * content in CSS context. + *

+ *

+ * To resolve 'auto' value on ‘svg’ element if the ‘viewBox’ attribute is not + * specified: + *

+ * + *

+ * To resolve 'auto' value on ‘svg’ element if the ‘viewBox’ attribute is + * specified: + *

+ * + * + * @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); + } + +} diff --git a/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/impl/package-info.java b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/impl/package-info.java new file mode 100644 index 000000000..baced173b --- /dev/null +++ b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/impl/package-info.java @@ -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; diff --git a/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/util/CSSTranscodingHelper.java b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/util/CSSTranscodingHelper.java index 9eab8ad58..7531b9c77 100644 --- a/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/util/CSSTranscodingHelper.java +++ b/echosvg-transcoder/src/main/java/io/sf/carte/echosvg/transcoder/util/CSSTranscodingHelper.java @@ -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; @@ -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) { diff --git a/samples/tests/spec/styling/css2.html b/samples/tests/spec/styling/css2.html index 8ec378be4..f7685cfeb 100644 --- a/samples/tests/spec/styling/css2.html +++ b/samples/tests/spec/styling/css2.html @@ -34,7 +34,7 @@ - +