From 3b289921f77d4c6ffdbfd0536b3765551907c2d2 Mon Sep 17 00:00:00 2001 From: danfickle Date: Fri, 17 Jul 2020 16:45:55 +1000 Subject: [PATCH 1/7] #493 Failing test for CSS not passed to SVG renderer. --- .../visualtest/html/issue-493-svg-styles.html | 56 +++++++++++++++++++ .../VisualRegressionTest.java | 12 +++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html new file mode 100644 index 000000000..4946d0860 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java index 1bff25d15..cdb6b2e65 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java @@ -728,7 +728,17 @@ public void testPaddingPercentage() throws IOException { public void testSvgInWrapperWithNamedPage() throws IOException { assertTrue(vt.runTest("svg-in-wrapper-with-named-page", TestSupport.WITH_SVG)); } - + + /** + * Tests that styles applying to internal SVG objects such as rect + * are passed to Batik for rendering. + */ + @Test + @Ignore // SVG styles not being passed to Batik. + public void testIssue493SVGStyles() throws IOException { + assertTrue(vt.runTest("issue-493-svg-styles", TestSupport.WITH_SVG)); + } + /** * Tests that a broken image inside a table cell renders with a zero sized image rather * than crashing with a NPE. See issue 336. From 3bd9343dc7a0e6d0fa67b9ad71709ec407b16373 Mon Sep 17 00:00:00 2001 From: danfickle Date: Fri, 17 Jul 2020 17:55:11 +1000 Subject: [PATCH 2/7] #493 Clean up mapper while understanding how it works. Need to write a new mapper to retrieve styles for SVGs. --- .../openhtmltopdf/css/newmatch/Matcher.java | 164 +++++++++++------- 1 file changed, 103 insertions(+), 61 deletions(-) diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java index 0038be132..cc4e40dee 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java @@ -21,6 +21,7 @@ package com.openhtmltopdf.css.newmatch; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -260,16 +261,25 @@ private com.openhtmltopdf.css.sheet.Ruleset getNonCssStyle(Object e) { * @author Torbjoern Gannholm */ class Mapper { - java.util.List axes; - private HashMap> pseudoSelectors; - private List mappedSelectors; - private Map children; + private final List axes; + private final Map> pseudoSelectors; + private final List mappedSelectors; - Mapper(java.util.Collection selectors) { - axes = new java.util.ArrayList(selectors); + private Map children; + + Mapper(Collection selectors) { + this.axes = new ArrayList<>(selectors); + this.pseudoSelectors = Collections.emptyMap(); + this.mappedSelectors = Collections.emptyList(); } - private Mapper() { + private Mapper( + List axes, + List mappedSelectors, + Map> pseudoSelectors) { + this.axes = axes; + this.mappedSelectors = mappedSelectors; + this.pseudoSelectors = pseudoSelectors; } /** @@ -280,33 +290,44 @@ private Mapper() { * (more correct: preserves the sort order from Matcher creation) */ Mapper mapChild(Object e) { - //Mapper childMapper = new Mapper(); - java.util.List childAxes = new ArrayList(axes.size() + 10); - java.util.HashMap> pseudoSelectors = new java.util.HashMap>(); - java.util.List mappedSelectors = new java.util.ArrayList(); + List childAxes = null; + List mappedSelectors = null; + Map> pseudoSelectors = null; + StringBuilder key = new StringBuilder(); + for (Selector sel : axes) { if (sel.getAxis() == Selector.DESCENDANT_AXIS) { - //carry it forward to other descendants + if (childAxes == null) { + childAxes = new ArrayList<>(); + } + + // Carry it forward to other descendants childAxes.add(sel); } else if (sel.getAxis() == Selector.IMMEDIATE_SIBLING_AXIS) { throw new RuntimeException(); } + if (!sel.matches(e, _attRes, _treeRes)) { continue; } - //Assumption: if it is a pseudo-element, it does not also have dynamic pseudo-class + + // Assumption: if it is a pseudo-element, it does not also have dynamic pseudo-class String pseudoElement = sel.getPseudoElement(); + if (pseudoElement != null) { - List l = pseudoSelectors.get(pseudoElement); - if (l == null) { - l = new ArrayList(); - pseudoSelectors.put(pseudoElement, l); + if (pseudoSelectors == null) { + pseudoSelectors = new HashMap<>(); } + + List l = pseudoSelectors.computeIfAbsent(pseudoElement, kee -> new ArrayList<>()); l.add(sel); + key.append(sel.getSelectorID()).append(":"); + continue; } + if (sel.isPseudoClass(Selector.VISITED_PSEUDOCLASS)) { _visitElements.add(e); } @@ -319,60 +340,78 @@ Mapper mapChild(Object e) { if (sel.isPseudoClass(Selector.FOCUS_PSEUDOCLASS)) { _focusElements.add(e); } + if (!sel.matchesDynamic(e, _attRes, _treeRes)) { continue; } + key.append(sel.getSelectorID()).append(":"); + Selector chain = sel.getChainedSelector(); + if (chain == null) { + if (mappedSelectors == null) { + mappedSelectors = new ArrayList<>(); + } + mappedSelectors.add(sel); } else if (chain.getAxis() == Selector.IMMEDIATE_SIBLING_AXIS) { throw new RuntimeException(); } else { + if (childAxes == null) { + childAxes = new ArrayList<>(); + } + childAxes.add(chain); } } - if (children == null) children = new HashMap(); - Mapper childMapper = children.get(key.toString()); - if (childMapper == null) { - childMapper = new Mapper(); - childMapper.axes = childAxes; - childMapper.pseudoSelectors = pseudoSelectors; - childMapper.mappedSelectors = mappedSelectors; - children.put(key.toString(), childMapper); + + if (children == null) { + children = new HashMap<>(); } + + List normalisedChildAxes = childAxes == null ? Collections.emptyList() : childAxes; + List normalisedMappedSelectors = mappedSelectors == null ? Collections.emptyList() : mappedSelectors; + Map> normalisedPseudoSelectors = pseudoSelectors == null ? Collections.emptyMap() : pseudoSelectors; + + Mapper childMapper = children.computeIfAbsent( + key.toString(), + kee -> new Mapper( + normalisedChildAxes, + normalisedMappedSelectors, + normalisedPseudoSelectors)); + link(e, childMapper); + return childMapper; } CascadedStyle getCascadedStyle(Object e) { - CascadedStyle result; - - CascadedStyle cs = null; - com.openhtmltopdf.css.sheet.Ruleset elementStyling = getElementStyle(e); - com.openhtmltopdf.css.sheet.Ruleset nonCssStyling = getNonCssStyle(e); - List propList = new ArrayList(); - //specificity 0,0,0,0 - if (nonCssStyling != null) { - propList.addAll(nonCssStyling.getPropertyDeclarations()); - } - //these should have been returned in order of specificity - for (Selector sel : mappedSelectors) { - propList.addAll(sel.getRuleset().getPropertyDeclarations()); - } - //specificity 1,0,0,0 - if (elementStyling != null) { - propList.addAll(elementStyling.getPropertyDeclarations()); - } - if (propList.size() == 0) - cs = CascadedStyle.emptyCascadedStyle; - else { - cs = new CascadedStyle(propList.iterator()); - } + Ruleset elementStyling = getElementStyle(e); + Ruleset nonCssStyling = getNonCssStyle(e); + + List propList = new ArrayList(); + + // Specificity 0,0,0,0 + if (nonCssStyling != null) { + propList.addAll(nonCssStyling.getPropertyDeclarations()); + } + + // These should have been returned in order of specificity + for (Selector sel : mappedSelectors) { + propList.addAll(sel.getRuleset().getPropertyDeclarations()); + } - result = cs; + // Specificity 1,0,0,0 + if (elementStyling != null) { + propList.addAll(elementStyling.getPropertyDeclarations()); + } - return result; + if (propList.isEmpty()) { + return CascadedStyle.emptyCascadedStyle; + } else { + return new CascadedStyle(propList.iterator()); + } } /** @@ -380,24 +419,27 @@ CascadedStyle getCascadedStyle(Object e) { * We assume that restyle has already been done by a getCascadedStyle if necessary. */ public CascadedStyle getPECascadedStyle(Object e, String pseudoElement) { - java.util.Iterator>> si = pseudoSelectors.entrySet().iterator(); - if (!si.hasNext()) { + if (pseudoSelectors.isEmpty()) { return null; } - CascadedStyle cs = null; - java.util.List pe = pseudoSelectors.get(pseudoElement); - if (pe == null) return null; - java.util.List propList = new java.util.ArrayList(); + List pe = pseudoSelectors.get(pseudoElement); + + if (pe == null) { + return null; + } + + List propList = new ArrayList<>(); + for (Selector sel : pe) { propList.addAll(sel.getRuleset().getPropertyDeclarations()); } - if (propList.size() == 0) - cs = CascadedStyle.emptyCascadedStyle;//already internalized - else { - cs = new CascadedStyle(propList.iterator()); + + if (propList.isEmpty()) { + return CascadedStyle.emptyCascadedStyle; + } else { + return new CascadedStyle(propList.iterator()); } - return cs; } } } From f1284f41d5f6bfa062a0c793b895746f0a1d82ef Mon Sep 17 00:00:00 2001 From: danfickle Date: Fri, 17 Jul 2020 22:02:59 +1000 Subject: [PATCH 3/7] #493 Semi working proof of concept on extracting SVG CSS styles. Still needs work on passing through CSS properties tht are not valid in HTML but valid in SVG such as the fill property. --- .../openhtmltopdf/context/StyleReference.java | 21 ++-- .../openhtmltopdf/css/newmatch/Condition.java | 119 +++++++++++++++++- .../openhtmltopdf/css/newmatch/Matcher.java | 102 +++++++++++---- .../openhtmltopdf/css/newmatch/Selector.java | 69 +++++++++- .../openhtmltopdf/css/parser/CSSParser.java | 1 + .../css/sheet/PropertyDeclaration.java | 13 ++ .../com/openhtmltopdf/css/sheet/Ruleset.java | 11 ++ .../svgsupport/BatikSVGImage.java | 14 +++ 8 files changed, 307 insertions(+), 43 deletions(-) diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/context/StyleReference.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/context/StyleReference.java index 1d3fa5c29..24207db3e 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/context/StyleReference.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/context/StyleReference.java @@ -36,6 +36,7 @@ import com.openhtmltopdf.css.extend.lib.DOMTreeResolver; import com.openhtmltopdf.css.newmatch.CascadedStyle; import com.openhtmltopdf.css.newmatch.PageInfo; +import com.openhtmltopdf.css.newmatch.Selector; import com.openhtmltopdf.css.parser.CSSPrimitiveValue; import com.openhtmltopdf.css.sheet.PropertyDeclaration; import com.openhtmltopdf.css.sheet.Stylesheet; @@ -196,7 +197,16 @@ public CascadedStyle getCascadedStyle(Element e, boolean restyle) { if (e == null) return CascadedStyle.emptyCascadedStyle; return _matcher.getCascadedStyle(e, restyle); } - + + /** + * Given an element, returns all selectors and their rulesets + * for its descendants. Useful for getting the styles that should be + * applied to SVG, etc. + */ + public String getCSSForAllDescendants(Element e) { + return _matcher.getCSSForAllDescendants(e); + } + public PageInfo getPageStyle(String pageName, String pseudoPage) { return _matcher.getPageCascadedStyle(pageName, pseudoPage); } @@ -268,14 +278,7 @@ private List getStylesheets() { return infos; } - - @Deprecated - public void removeStyle(Element e) { - if (_matcher != null) { - _matcher.removeStyle(e); - } - } - + public List getFontFaceRules() { return _matcher.getFontFaceRules(); } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Condition.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Condition.java index 47e3205a6..ab06fcd30 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Condition.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Condition.java @@ -37,6 +37,7 @@ abstract class Condition { abstract boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes); + abstract void toCSS(StringBuilder sb); /** * the CSS condition [attribute] @@ -217,6 +218,17 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { return compare(val, _value); } + + protected void toCSS(StringBuilder sb, String type) { + sb.append('['); + sb.append(_name); + sb.append(type); + sb.append('='); + sb.append('\"'); + sb.append(_value); + sb.append('\"'); + sb.append(']'); + } } private static class AttributeExistsCondition extends AttributeCompareCondition { @@ -241,6 +253,13 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { protected boolean compare(String attrValue, String conditionValue) { throw new UnsupportedOperationException(); } + + @Override + void toCSS(StringBuilder sb) { + sb.append('['); + sb.append(_name); + sb.append(']'); + } } private static class AttributeEqualsCondition extends AttributeCompareCondition { @@ -252,6 +271,11 @@ private static class AttributeEqualsCondition extends AttributeCompareCondition protected boolean compare(String attrValue, String conditionValue) { return attrValue.equals(conditionValue); } + + @Override + void toCSS(StringBuilder sb) { + toCSS(sb, ""); + } } private static class AttributePrefixCondition extends AttributeCompareCondition { @@ -263,6 +287,11 @@ private static class AttributePrefixCondition extends AttributeCompareCondition protected boolean compare(String attrValue, String conditionValue) { return attrValue.startsWith(conditionValue); } + + @Override + void toCSS(StringBuilder sb) { + toCSS(sb, "^"); + } } private static class AttributeSuffixCondition extends AttributeCompareCondition { @@ -274,6 +303,11 @@ private static class AttributeSuffixCondition extends AttributeCompareCondition protected boolean compare(String attrValue, String conditionValue) { return attrValue.endsWith(conditionValue); } + + @Override + void toCSS(StringBuilder sb) { + toCSS(sb, "$"); + } } private static class AttributeSubstringCondition extends AttributeCompareCondition { @@ -285,8 +319,13 @@ private static class AttributeSubstringCondition extends AttributeCompareConditi protected boolean compare(String attrValue, String conditionValue) { return attrValue.indexOf(conditionValue) > -1; } + + @Override + void toCSS(StringBuilder sb) { + toCSS(sb, "*"); + } } - + private static class AttributeMatchesListCondition extends AttributeCompareCondition { AttributeMatchesListCondition(String namespaceURI, String name, String value) { super(namespaceURI, name, value); @@ -303,6 +342,11 @@ protected boolean compare(String attrValue, String conditionValue) { } return matched; } + + @Override + void toCSS(StringBuilder sb) { + toCSS(sb, "~"); + } } private static class AttributeMatchesFirstPartCondition extends AttributeCompareCondition { @@ -318,6 +362,11 @@ protected boolean compare(String attrValue, String conditionValue) { } return false; } + + @Override + void toCSS(StringBuilder sb) { + toCSS(sb, "|"); + } } private static class ClassCondition extends Condition { @@ -343,6 +392,12 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { // in an XML DOM, space normalization in attributes is supposed to have happened already. return (" " + c + " ").indexOf(_paddedClassName) != -1; } + + @Override + public void toCSS(StringBuilder sb) { + sb.append('.'); + sb.append(_paddedClassName.substring(1, _paddedClassName.length() - 1)); + } } private static class IDCondition extends Condition { @@ -363,6 +418,12 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { } return true; } + + @Override + void toCSS(StringBuilder sb) { + sb.append('#'); + sb.append(_id); + } } private static class LangCondition extends Condition { @@ -390,6 +451,13 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { } return false; } + + @Override + void toCSS(StringBuilder sb) { + sb.append(":lang("); + sb.append(_lang); + sb.append(')'); + } } private static class FirstChildCondition extends Condition { @@ -401,6 +469,11 @@ private static class FirstChildCondition extends Condition { boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { return treeRes.isFirstChildElement(e); } + + @Override + void toCSS(StringBuilder sb) { + sb.append(":first-child"); + } } private static class LastChildCondition extends Condition { @@ -412,6 +485,11 @@ private static class LastChildCondition extends Condition { boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { return treeRes.isLastChildElement(e); } + + @Override + void toCSS(StringBuilder sb) { + sb.append(":last-child"); + } } private static class NthChildCondition extends Condition { @@ -420,10 +498,12 @@ private static class NthChildCondition extends Condition { private final int a; private final int b; + private final String input; - NthChildCondition(int a, int b) { + NthChildCondition(int a, int b, String input) { this.a = a; this.b = b; + this.input = input; } @Override @@ -442,16 +522,23 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { } } + @Override + void toCSS(StringBuilder sb) { + sb.append(":nth-child("); + sb.append(input); + sb.append(')'); + } + static NthChildCondition fromString(String number) { number = number.trim().toLowerCase(); if ("even".equals(number)) { - return new NthChildCondition(2, 0); + return new NthChildCondition(2, 0, number); } else if ("odd".equals(number)) { - return new NthChildCondition(2, 1); + return new NthChildCondition(2, 1, number); } else { try { - return new NthChildCondition(0, Integer.parseInt(number)); + return new NthChildCondition(0, Integer.parseInt(number), number); } catch (NumberFormatException e) { Matcher m = pattern.matcher(number); @@ -467,7 +554,7 @@ static NthChildCondition fromString(String number) { b *= -1; } - return new NthChildCondition(a, b); + return new NthChildCondition(a, b, number); } } } @@ -484,6 +571,11 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { int position = treeRes.getPositionOfElement(e); return position >= 0 && position % 2 == 0; } + + @Override + void toCSS(StringBuilder sb) { + sb.append(":nth-child(even)"); + } } private static class OddChildCondition extends Condition { @@ -496,6 +588,11 @@ boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { int position = treeRes.getPositionOfElement(e); return position >= 0 && position % 2 == 1; } + + @Override + void toCSS(StringBuilder sb) { + sb.append(":nth-child(odd)"); + } } private static class LinkCondition extends Condition { @@ -507,6 +604,11 @@ private static class LinkCondition extends Condition { boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { return attRes.isLink(e); } + + @Override + void toCSS(StringBuilder sb) { + sb.append(":link"); + } } /** @@ -521,6 +623,11 @@ private static class UnsupportedCondition extends Condition { boolean matches(Object e, AttributeResolver attRes, TreeResolver treeRes) { return false; } + + @Override + void toCSS(StringBuilder sb) { + // Nothing we can do... + } } private static String[] split(String s, char ch) { diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java index cc4e40dee..58c26b94e 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java @@ -20,11 +20,14 @@ */ package com.openhtmltopdf.css.newmatch; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.Deque; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -44,36 +47,29 @@ * @author Torbjoern Gannholm */ public class Matcher { + private final Mapper docMapper; + private final AttributeResolver _attRes; + private final TreeResolver _treeRes; + private final StylesheetFactory _styleFactory; - private Mapper docMapper; - private com.openhtmltopdf.css.extend.AttributeResolver _attRes; - private com.openhtmltopdf.css.extend.TreeResolver _treeRes; - private com.openhtmltopdf.css.extend.StylesheetFactory _styleFactory; + private final Map _map = new HashMap<>(); - private java.util.Map _map; + private final Set _hoverElements = new HashSet<>(); + private final Set _activeElements = new HashSet<>(); + private final Set _focusElements = new HashSet<>(); + private final Set _visitElements = new HashSet<>(); + + private final List _pageRules = new ArrayList<>(); + private final List _fontFaceRules = new ArrayList<>(); - //handle dynamic - private Set _hoverElements; - private Set _activeElements; - private Set _focusElements; - private Set _visitElements; - - private final List _pageRules = new ArrayList(); - private final List _fontFaceRules = new ArrayList(); - public Matcher( TreeResolver tr, AttributeResolver ar, StylesheetFactory factory, List stylesheets, String medium) { - newMaps(); _treeRes = tr; _attRes = ar; _styleFactory = factory; docMapper = createDocumentMapper(stylesheets, medium); } - - public void removeStyle(Object e) { - _map.remove(e); - } public CascadedStyle getCascadedStyle(Object e, boolean restyle) { Mapper em; @@ -85,6 +81,15 @@ public CascadedStyle getCascadedStyle(Object e, boolean restyle) { return em.getCascadedStyle(e); } + public String getCSSForAllDescendants(Object e) { + Mapper child = getMapper(e); + + AllDescendantMapper descendants = new AllDescendantMapper(child.axes, _attRes, _treeRes); + descendants.map(e); + + return descendants.toCSS(); + } + /** * May return null. * We assume that restyle has already been done by a getCascadedStyle if necessary. @@ -205,14 +210,6 @@ private void link(Object e, Mapper m) { _map.put(e, m); } - private void newMaps() { - _map = new java.util.HashMap(); - _hoverElements = new java.util.HashSet(); - _activeElements = new java.util.HashSet(); - _focusElements = new java.util.HashSet(); - _visitElements = new java.util.HashSet(); - } - private Mapper getMapper(Object e) { Mapper m = _map.get(e); if (m != null) { @@ -442,5 +439,56 @@ public CascadedStyle getPECascadedStyle(Object e, String pseudoElement) { } } } + + public static class AllDescendantMapper { + private final List axes; + private final List mappedSelectors = new ArrayList<>(); + private final Set topSelectors = new HashSet<>(); + private final AttributeResolver attRes; + private final TreeResolver treeRes; + + AllDescendantMapper(List axes, AttributeResolver attRes, TreeResolver treeRes) { + this.axes = axes; + this.attRes = attRes; + this.treeRes = treeRes; + } + + public String toCSS() { + StringBuilder sb = new StringBuilder(); + + for (Selector sel : mappedSelectors) { + sel.toCSS(sb, topSelectors); + sel.getRuleset().toCSS(sb); + } + + return sb.toString(); + } + + void map(Object e) { + Deque queue = new ArrayDeque<>(); + + for (Selector sel : axes) { + if (!sel.matches(e, attRes, treeRes) || + sel.getChainedSelector() == null) { + continue; + } + + queue.addLast(sel); + this.topSelectors.add(sel); + } + + while (!queue.isEmpty()) { + Selector current = queue.removeFirst(); + + Selector chain = current.getChainedSelector(); + + if (chain == null) { + this.mappedSelectors.add(current); + } else { + queue.addLast(chain); + } + } + } + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java index 05992e63c..e513fadeb 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java @@ -25,6 +25,8 @@ import com.openhtmltopdf.util.LogMessageId; import com.openhtmltopdf.util.XRLog; +import java.util.List; +import java.util.Set; import java.util.logging.Level; @@ -52,7 +54,7 @@ public class Selector { private int _pos;//to distinguish between selectors of same specificity - private java.util.List conditions; + private List conditions; public final static int DESCENDANT_AXIS = 0; public final static int CHILD_AXIS = 1; @@ -67,6 +69,7 @@ public class Selector { * Give each a unique ID to be able to create a key to internalize Matcher.Mappers */ private int selectorID; + private Selector _ancestorSelector; private static int selectorCount = 0; public Selector() { @@ -430,6 +433,62 @@ private void addCondition(Condition c) { conditions.add(c); } + public void toCSS(StringBuilder sb, Set stopAt) { + if (stopAt.contains(this)) { + return; + } + + Selector ancestor = this; + + while (ancestor != null) { + Selector current = ancestor.getAncestorSelector(); + + if (current == null || stopAt.contains(current)) { + break; + } + + ancestor = current; + } + + Selector chained = ancestor == null ? this : ancestor; + + if (chained._name != null) { + sb.append(chained._name); + } + + if (chained.conditions != null) { + for (Condition condition : chained.conditions) { + condition.toCSS(sb); + } + } + + sb.append(' '); + + Selector next = chained.getChainedSelector(); + + while (next != null) { + if (next.getAxis() == Selector.CHILD_AXIS) { + sb.append('>'); + sb.append(' '); + } else if (next.getAxis() == Selector.DESCENDANT_AXIS) { + // Do nothing, already have a space. + } + + if (next._name != null) { + sb.append(next._name); + } + + if (next.conditions != null) { + for (Condition condition : next.conditions) { + condition.toCSS(sb); + } + } + sb.append(' '); + + next = next.getChainedSelector(); + } + } + /** * Gets the elementStylingOrder attribute of the Selector class * @@ -489,5 +548,13 @@ public void setSiblingSelector(Selector selector) { public void setNamespaceURI(String namespaceURI) { _namespaceURI = namespaceURI; } + + public void setAncestorSelector(Selector ancestor) { + _ancestorSelector = ancestor; + } + + public Selector getAncestorSelector() { + return _ancestorSelector; + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java index b3e26da98..8cee35a58 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java @@ -854,6 +854,7 @@ private Selector mergeSimpleSelectors(List selectors, List comb result = first; } first.setChainedSelector(second); + second.setAncestorSelector(first); } else { second.setSiblingSelector(first); if (result == null || result == first) { diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java index 5de44581b..39fde41a1 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java @@ -231,6 +231,19 @@ public boolean isImportant() { public int getOrigin() { return origin; } + + public void toCSS(StringBuilder sb) { + sb.append(getPropertyName()); + sb.append(':'); + sb.append(' '); + sb.append(getValue().toString()); + + if (isImportant()) { + sb.append(" !important;"); + } else { + sb.append(';'); + } + } }// end class /* diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java index 2a5ae87bb..5f5e51301 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java @@ -71,6 +71,17 @@ public int getOrigin() { return _origin; } + public void toCSS(StringBuilder sb) { + sb.append('{'); + sb.append('\n'); + for (PropertyDeclaration decl : _props) { + decl.toCSS(sb); + sb.append('\n'); + } + sb.append('}'); + sb.append('\n'); + } + }// end class /* diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java index 9b8a1c13c..f0a196ec5 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java @@ -1,6 +1,7 @@ package com.openhtmltopdf.svgsupport; import java.awt.Point; +import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -14,7 +15,10 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.Text; +import com.openhtmltopdf.css.newmatch.Selector; +import com.openhtmltopdf.css.sheet.PropertyDeclaration; import com.openhtmltopdf.extend.OutputDevice; import com.openhtmltopdf.extend.SVGDrawer.SVGImage; import com.openhtmltopdf.render.Box; @@ -166,12 +170,22 @@ public void drawSVG(OutputDevice outputDevice, RenderingContext ctx, pdfTranscoder.setRenderingParameters(outputDevice, ctx, x, y, fontResolver, userAgentCallback); + + String styles = ctx.getCss().getCSSForAllDescendants(svgElement); + try { DOMImplementation impl = SVGDOMImplementation .getDOMImplementation(); Document newDocument = impl.createDocument( SVGDOMImplementation.SVG_NAMESPACE_URI, "svg", null); + if (styles != null) { + Element styleElem = newDocument.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, "style"); + Text styleText = newDocument.createTextNode(styles); + styleElem.appendChild(styleText); + newDocument.getDocumentElement().appendChild(styleElem); + } + for (int i = 0; i < svgElement.getChildNodes().getLength(); i++) { Node importedNode = newDocument .importNode(svgElement.getChildNodes().item(i), true); From 4a2cf8ed6ecce8db34070315c20a67b79d39cece Mon Sep 17 00:00:00 2001 From: danfickle Date: Sun, 19 Jul 2020 17:48:08 +1000 Subject: [PATCH 4/7] #493 Keep invalid property declarations in the CSS parser So they can be passed to SVG plugin. Test is now working well, but proof will be provided in a separate commit. --- .../openhtmltopdf/css/newmatch/Matcher.java | 16 +++- .../openhtmltopdf/css/newmatch/Selector.java | 61 +++++++++++- .../openhtmltopdf/css/parser/CSSParser.java | 9 +- .../openhtmltopdf/css/parser/FSFunction.java | 14 +-- .../css/sheet/InvalidPropertyDeclaration.java | 65 +++++++++++++ .../css/sheet/PropertyDeclaration.java | 8 +- .../com/openhtmltopdf/css/sheet/Ruleset.java | 92 +++++-------------- .../visualtest/html/issue-493-svg-styles.html | 8 +- .../svgsupport/BatikSVGImage.java | 5 +- 9 files changed, 187 insertions(+), 91 deletions(-) create mode 100644 openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/InvalidPropertyDeclaration.java diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java index 58c26b94e..1674470d1 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Matcher.java @@ -33,7 +33,6 @@ import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; - import com.openhtmltopdf.css.constants.MarginBoxName; import com.openhtmltopdf.css.extend.AttributeResolver; import com.openhtmltopdf.css.extend.StylesheetFactory; @@ -81,8 +80,18 @@ public CascadedStyle getCascadedStyle(Object e, boolean restyle) { return em.getCascadedStyle(e); } + /** + * Returns CSS rulesets for descendants of e. + * For example, if e is an svg element and we have the ruleset + * 'svg rect { .. }' then the string returned will be 'rect { .. }'. + * + * FIXME: Does not correctly handle sibling selectors. + */ public String getCSSForAllDescendants(Object e) { - Mapper child = getMapper(e); + // We must use the parent mapper as a starting point + // to correctly handle direct child selectors such as 'body > svg rect'. + Object parent = _treeRes.getParentElement(e); + Mapper child = parent != null ? getMapper(parent) : docMapper; AllDescendantMapper descendants = new AllDescendantMapper(child.axes, _attRes, _treeRes); descendants.map(e); @@ -321,7 +330,6 @@ Mapper mapChild(Object e) { l.add(sel); key.append(sel.getSelectorID()).append(":"); - continue; } @@ -453,7 +461,7 @@ public static class AllDescendantMapper { this.treeRes = treeRes; } - public String toCSS() { + String toCSS() { StringBuilder sb = new StringBuilder(); for (Selector sel : mappedSelectors) { diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java index e513fadeb..bd5cefc49 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/newmatch/Selector.java @@ -433,6 +433,18 @@ private void addCondition(Condition c) { conditions.add(c); } + /** + * Prints the selector chain to a StringBuilder, stopping + * when it hits a selector in the stopAt set. + * + * For example, given the selector 'body svg rect' and the stop + * set contains 'svg' then this will print 'rect' to the builder. + * + * This method is used to recreate CSS selectors to pass to SVG or + * other plugins. + * + * FIXME: Does not handle sibling selector. + */ public void toCSS(StringBuilder sb, Set stopAt) { if (stopAt.contains(this)) { return; @@ -450,7 +462,12 @@ public void toCSS(StringBuilder sb, Set stopAt) { ancestor = current; } - Selector chained = ancestor == null ? this : ancestor; + Selector chained = ancestor; + + if (chained.getAxis() == Selector.CHILD_AXIS) { + sb.append('>'); + sb.append(' '); + } if (chained._name != null) { sb.append(chained._name); @@ -556,5 +573,47 @@ public void setAncestorSelector(Selector ancestor) { public Selector getAncestorSelector() { return _ancestorSelector; } + + /** + * For debugging, prints the entire selector chain. + * FIXME: Does not handle sibling selectors. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + Selector current = this; + Selector ancestor = this; + + while (current != null) { + current = current.getAncestorSelector(); + if (current != null) { + ancestor = current; + } + } + + current = ancestor; + + while (current != null) { + if (current.getAxis() == Selector.CHILD_AXIS) { + sb.append(" > "); + } else { + sb.append(' '); + } + + if (current._name != null) { + sb.append(current._name); + } + + if (current.conditions != null) { + for (Condition cond : current.conditions) { + cond.toCSS(sb); + } + } + + current = current.getChainedSelector(); + } + + return sb.toString(); + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java index 8cee35a58..dca1e90a3 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java @@ -766,7 +766,8 @@ private void ruleset(RulesetContainer container) throws IOException { t, new Token[] { Token.TK_COMMA, Token.TK_LBRACE }, getCurrentLine()); } - if (ruleset.getPropertyDeclarations().size() > 0) { + if (!ruleset.getPropertyDeclarations().isEmpty() || + !ruleset.getInvalidPropertyDeclarations().isEmpty()) { container.addContent(ruleset); } } catch (CSSParseException e) { @@ -1314,6 +1315,12 @@ private void declaration(Ruleset ruleset, boolean inFontFace) throws IOException e.setLine(getCurrentLine()); error(e, "declaration", true); } + } else { + // We need to keep invalid properties in case they are used by SVG, etc. + ruleset.addInvalidProperty( + new InvalidPropertyDeclaration( + propertyName, values, ruleset.getOrigin(), important, + ruleset.getPropertyDeclarations().size() + ruleset.getInvalidPropertyDeclarations().size())); } } else { push(t); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/FSFunction.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/FSFunction.java index 9b71017ef..2c3bd4aeb 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/FSFunction.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/FSFunction.java @@ -22,9 +22,9 @@ import java.util.List; public class FSFunction { - private String _name; - private List _parameters; - + private final String _name; + private final List _parameters; + public FSFunction(String name, List parameters) { _name = name; _parameters = parameters; @@ -43,9 +43,11 @@ public String toString() { StringBuilder result = new StringBuilder(); result.append(_name); result.append('('); - for (PropertyValue _parameter : _parameters) { - result.append(_parameter); // HACK - result.append(','); + for (int i = 0; i < getParameters().size(); i++) { + result.append(getParameters().get(i)); + if (i < getParameters().size() - 1) { + result.append(','); + } } result.append(')'); return result.toString(); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/InvalidPropertyDeclaration.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/InvalidPropertyDeclaration.java new file mode 100644 index 000000000..b1de08807 --- /dev/null +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/InvalidPropertyDeclaration.java @@ -0,0 +1,65 @@ +package com.openhtmltopdf.css.sheet; + +import java.util.List; + +import com.openhtmltopdf.css.parser.PropertyValue; +import com.openhtmltopdf.css.parser.Token; + +/** + * Holds an invalid property declaration (ie. one not understood by + * this project). Useful for passing to plugins such as SVG. + * + * WARNING: This is not a general subclass of PropertyDeclaration, the only + * method which should be used is toCSS. + */ +public class InvalidPropertyDeclaration extends PropertyDeclaration { + + private final String propertyName; + private final List values; + private final int order; + + public InvalidPropertyDeclaration( + String propertyName, + List values, + int origin, + boolean important, + int order) { + super(null, null, important, origin); + this.propertyName = propertyName; + this.values = values; + this.order = order; + } + + @Override + public void toCSS(StringBuilder sb) { + sb.append(this.propertyName); + sb.append(':'); + for (PropertyValue value : this.values) { + if (value.getOperator() == Token.TK_COMMA) { + sb.append(','); + } else { + sb.append(' '); + } + sb.append(value.getCssText()); + } + if (this.isImportant()) { + sb.append(" !important;\n"); + } else { + sb.append(';'); + sb.append('\n'); + } + } + + @Override + public String getPropertyName() { + return this.propertyName; + } + + /** + * Holds the order so as to recreate a list of invalid and valid + * properties in their original order. + */ + public int getOrder() { + return this.order; + } +} diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java index 39fde41a1..fcb35bba4 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/PropertyDeclaration.java @@ -38,11 +38,6 @@ * @author Patrick Wright */ public class PropertyDeclaration { - /** - * Description of the Field - */ - private String propName; - /** * Description of the Field */ @@ -122,7 +117,6 @@ public PropertyDeclaration(CSSName cssName, CSSPrimitiveValue value, boolean imp, int orig) { - this.propName = cssName.toString(); this.cssName = cssName; this.cssPrimitiveValue = value; this.important = imp; @@ -200,7 +194,7 @@ public int getImportanceAndOrigin() { * @return See desc. */ public String getPropertyName() { - return propName; + return this.cssName.toString(); } /** diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java index 5f5e51301..32c56bd4a 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/sheet/Ruleset.java @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - import com.openhtmltopdf.css.newmatch.Selector; @@ -34,6 +33,7 @@ public class Ruleset { private final int _origin; private final List _props; private final List _fsSelectors; + private List _invalidProperties; public Ruleset(int orig) { _origin = orig; @@ -72,9 +72,22 @@ public int getOrigin() { } public void toCSS(StringBuilder sb) { + List decls; + + if (_invalidProperties != null) { + // Create a list of declarations in their proper order. + // Not efficient, but there should be few of them. + decls = new ArrayList<>(_props); + for (InvalidPropertyDeclaration decl : _invalidProperties) { + decls.add(decl.getOrder(), decl); + } + } else { + decls = _props; + } + sb.append('{'); sb.append('\n'); - for (PropertyDeclaration decl : _props) { + for (PropertyDeclaration decl : decls) { decl.toCSS(sb); sb.append('\n'); } @@ -82,69 +95,14 @@ public void toCSS(StringBuilder sb) { sb.append('\n'); } -}// end class - -/* - * $Id$ - * - * $Log$ - * Revision 1.17 2007/08/19 22:22:54 peterbrant - * Merge R8pbrant changes to HEAD - * - * Revision 1.16.2.1 2007/07/09 22:18:02 peterbrant - * Begin work on running headers and footers and named pages - * - * Revision 1.16 2007/02/20 01:17:11 peterbrant - * Start CSS parser cleanup - * - * Revision 1.15 2007/02/19 14:53:38 peterbrant - * Integrate new CSS parser - * - * Revision 1.14 2006/07/26 18:05:05 pdoubleya - * Clean exception throw. - * - * Revision 1.13 2006/05/08 21:36:03 pdoubleya - * Log and skip properties we can't parse into declarations... - * - * Revision 1.12 2005/12/30 01:32:41 peterbrant - * First merge of parts of pagination work - * - * Revision 1.11 2005/10/20 20:48:05 pdoubleya - * Updates for refactoring to style classes. CalculatedStyle now has lookup methods to cover all general cases, so propertyByName() is private, which means the backing classes for styling were able to be replaced. - * - * Revision 1.10 2005/10/15 23:39:15 tobega - * patch from Peter Brant - * - * Revision 1.9 2005/07/14 17:43:39 joshy - * fixes for parser access exceptions when running in a sandbox (webstart basically) - * - * Revision 1.8 2005/06/16 07:24:46 tobega - * Fixed background image bug. - * Caching images in browser. - * Enhanced LinkListener. - * Some house-cleaning, playing with Idea's code inspection utility. - * - * Revision 1.7 2005/01/29 20:19:21 pdoubleya - * Clean/reformat code. Removed commented blocks, checked copyright. - * - * Revision 1.6 2005/01/29 12:08:23 pdoubleya - * Added constructor for SelectorList/PD List, for possible use of our own SAC DocumentHandler in the future. - * - * Revision 1.5 2005/01/24 19:01:08 pdoubleya - * Mass checkin. Changed to use references to CSSName, which now has a Singleton instance for each property, everywhere property names were being used before. Removed commented code. Cascaded and Calculated style now store properties in arrays rather than maps, for optimization. - * - * Revision 1.4 2005/01/24 14:36:30 pdoubleya - * Mass commit, includes: updated for changes to property declaration instantiation, and new use of DerivedValue. Removed any references to older XR... classes (e.g. XRProperty). Cleaned imports. - * - * Revision 1.3 2004/11/15 12:42:23 pdoubleya - * Across this checkin (all may not apply to this particular file) - * Changed default/package-access members to private. - * Changed to use XRRuntimeException where appropriate. - * Began move from System.err.println to std logging. - * Standard code reformat. - * Removed some unnecessary SAC member variables that were only used in initialization. - * CVS log section. - * - * - */ + public void addInvalidProperty(InvalidPropertyDeclaration invalidPropertyDeclaration) { + if (_invalidProperties == null) { + _invalidProperties = new ArrayList<>(); + } + _invalidProperties.add(invalidPropertyDeclaration); + } + public List getInvalidPropertyDeclarations() { + return _invalidProperties == null ? Collections.emptyList() : _invalidProperties; + } +} diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html index 4946d0860..90bcb1653 100644 --- a/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html @@ -22,6 +22,11 @@ .hl rect { /* To SVGs with class hl. */ fill: rgb(255,0,0); } +body > svg.hl > rect { + stroke: black; + stroke-width: 4px; + stroke-dasharray: 10 5; +} div { height: 20px; background-color: orange; @@ -45,11 +50,12 @@ - + +
diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java index f0a196ec5..b15a949e8 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java @@ -1,7 +1,6 @@ package com.openhtmltopdf.svgsupport; import java.awt.Point; -import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -17,8 +16,6 @@ import org.w3c.dom.Node; import org.w3c.dom.Text; -import com.openhtmltopdf.css.newmatch.Selector; -import com.openhtmltopdf.css.sheet.PropertyDeclaration; import com.openhtmltopdf.extend.OutputDevice; import com.openhtmltopdf.extend.SVGDrawer.SVGImage; import com.openhtmltopdf.render.Box; @@ -179,7 +176,7 @@ public void drawSVG(OutputDevice outputDevice, RenderingContext ctx, Document newDocument = impl.createDocument( SVGDOMImplementation.SVG_NAMESPACE_URI, "svg", null); - if (styles != null) { + if (styles != null && !styles.isEmpty()) { Element styleElem = newDocument.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, "style"); Text styleText = newDocument.createTextNode(styles); styleElem.appendChild(styleText); From 62ada2eec5f9ac437655fef60c465ce19b2635dd Mon Sep 17 00:00:00 2001 From: danfickle Date: Sun, 19 Jul 2020 18:14:38 +1000 Subject: [PATCH 5/7] #493 Suppress warnings about SVG properties not being implemented. --- .../css/constants/SVGProperty.java | 48 +++++++++++++++++++ .../openhtmltopdf/css/parser/CSSParser.java | 5 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/SVGProperty.java diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/SVGProperty.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/SVGProperty.java new file mode 100644 index 000000000..f39e42249 --- /dev/null +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/SVGProperty.java @@ -0,0 +1,48 @@ +package com.openhtmltopdf.css.constants; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * This is a partial list of common SVG properties that are not present in + * the HTML renderer of this project. This list is here so we can suppress + * warnings for these properties. + * + * List from: + * https://css-tricks.com/svg-properties-and-css/ + */ +public enum SVGProperty { + CLIP, + CLIP_PATH, + CLIP_RULE, + MASK, + FILTER, + STOP_COLOR, + STOP_OPACITY, + FILL, + FILL_RULE, + FILL_OPACITY, + MARKER, + MARKER_START, + MARKER_MID, + MARKER_END, + STROKE, + STROKE_DASHARRAY, + STROKE_DASHOFFSET, + STROKE_LINECAP, + STROKE_LINEJOIN, + STROKE_MITERLIMIT, + STROKE_OPACITY, + STROKE_WIDTH; + + private static final Set _set = + Arrays.stream(values()) + .map(v -> v.name().toLowerCase(Locale.US).replace('_', '-')) + .collect(Collectors.toSet()); + + public static Set properties() { + return _set; + } +} diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java index dca1e90a3..7d5958b80 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/CSSParser.java @@ -21,6 +21,7 @@ import com.openhtmltopdf.css.constants.CSSName; import com.openhtmltopdf.css.constants.MarginBoxName; +import com.openhtmltopdf.css.constants.SVGProperty; import com.openhtmltopdf.css.extend.TreeResolver; import com.openhtmltopdf.css.newmatch.Selector; import com.openhtmltopdf.css.parser.property.PropertyBuilder; @@ -1242,10 +1243,12 @@ private void pseudo(Selector selector) throws IOException { private boolean checkCSSName(CSSName cssName, String propertyName) { if (cssName == null) { - _errorHandler.error( + if (!SVGProperty.properties().contains(propertyName)) { + _errorHandler.error( _URI, propertyName + " is an unrecognized CSS property at line " + getCurrentLine() + ". Ignoring declaration."); + } return false; } From f038dd087785937666f84253e0dc8c83452643e1 Mon Sep 17 00:00:00 2001 From: danfickle Date: Sun, 19 Jul 2020 18:19:20 +1000 Subject: [PATCH 6/7] #493 Test proof that auto extraction of SVG styles is working. --- .../visualtest/expected/issue-493-svg-styles.pdf | Bin 0 -> 4199 bytes .../VisualRegressionTest.java | 1 - 2 files changed, 1 deletion(-) create mode 100644 openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles.pdf diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5a74150133e79f83393c4b6d29028cc0f67d909d GIT binary patch literal 4199 zcmeHKYj9IV6b5DxSSc0}2c*b~l&9q0y|3hA1>1zu(pE}a9$Jc|$!(jGr0R;wh3XZ$?odid5&|hYzY0lp7JLlW8 zd(YWT&G%#uL=Fz5Ui<#WNq`^?<C2 z0T}UerEs}Y0TGK>qA051oP?494!}BjI0-OtIG|xAKs3w)#U+^O2hZRUtO14q)}#Ik zs=qW6fR~7oDP~i(;Qe9VaK3#vKrb7f`i3!Y?=&#Ry?B7f}4u1Sez!tWa_pqj?t!} z={5~>o_Gg3XoeF-eWI347)Znn^{ML3ZW{3|f{`gRKAEQ(G1i$GpW$NVTkaAfs);cD zneknCLG=l!c|_)b=E3FCC#X)yW|!)8$(cDYZI!}e2kKMJpUpHim;@~cb_sai zwwTz8%SJ9G`5EQ4u?$Oe~>oQFj#(?gE)hDS<%CgKnxEKeGcY=`y(L*h&mN~%T~t~aFUcW?s7yH$LeL|FkuezSl`ufeEkNd)8z43jWaiYs za2jSLNFohItCmJevybm6A_^q)TZko%kuP%EVxx%Q9FfS_TJkoz)wbRj&!v zaBS3T0e3j9{pq0qRajlBs11?Cu-AVgVrY#rtWY714v}Q4Es@HIZ7!s(KtWJ@OJ*Rb z;EqRS1~JDY`DDL@8m$c!wQ0KvJk_^l!zF+2jNG={x}KOi&0pKqaf2H)TZRKqr=EUyadh96tLs1gd1klV zd+WtDXwB03T`CVP{OIVK&!&Eu8JIXH`P$-L{U_!g6_3n&Bqw@b->r?}-n#qWqzhYn zzxbqwujPd8$5uVWJXig|;0D*YhG9)pY7b`}ozz^NE4|gT$k#Ka(>K3;eEDW8VbhSD zTdX8mo;#Ru^9=HT!v7@UXtRPW{#`k?D8wqnvay$>`dz2?s&?Nk1XHt(IVnD5=ReB!V-R*7Iy$&V>#^XKL)S^NA3>E!&4EjFBJ+MnEY z$DpKl%2VDy_wciiB@d}++IrKqnt7t*I@EDqyNfr9C(mdpY`^6WSIbOX#u=Lb$2CPg z+>)37U*F>2+H1=BUZMTCrleQZm26HPvvKs$Sp|(39?hQ7dBlX_-|>?#{gPL?#=B=( zW0!3&tc2n8FSEM%3Vv@sxT~tRds;?*Gpy^3)=zk8Xzy%a&xPD5chS6o9h&O*SJo~5 zpwqd=Q__*9R*Np4f8}aj`)0cE*v=oO5A?O!Ii-H~U7!E@^@?fZ$89Ouhp5LdxAra= zF~8(McV~wc>lTdVt9}}PfvR17(tY51TDNNYt`qhB!|C^=h#9*Y&g^iu zTr&CfTT~vkI2-Fy9!2=XZ|r6L0)y<=3>6W7yhrvsj*8HFkJV6+{l=jpw1#9u*zX8h zpD{ZKUgJD`>ykrN`1Vv9lx#N{Wbc8uL9=(b+o0L&^xvn|M5Ho&1VJL8%@00-@GK9@ z@Xbg6ki?S@44Y#}z;!BajK)Ww_%VjTX^6wIYyu8B>4X@_DdM$>E Date: Wed, 22 Jul 2020 01:03:53 +1000 Subject: [PATCH 7/7] #493 Test applying styles to external SVG linked via img tag. Copy the class attribute from img tag to svg tag so it can be better targetted. --- .../issue-493-svg-styles-linked-image.pdf | Bin 0 -> 2441 bytes .../issue-493-svg-styles-linked-image.html | 29 ++++++++++++++++++ .../VisualRegressionTest.java | 9 ++++++ .../PdfBoxReplacedElementFactory.java | 13 ++++++-- 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles-linked-image.pdf create mode 100644 openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles-linked-image.html diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles-linked-image.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles-linked-image.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2a560e47dde0880f0a0d99cea94d57c88d3e97ca GIT binary patch literal 2441 zcmd5;Yitx%6b=%bcm-n-N&3TjmGlAX&b>1`GrLflbRRX?R!U1yTQ_#PGi|r-?kuy@ z2PCE>h!&d&4J8s>G|YJwQwi&hh})_e@sJ+H z9CgTut5h1bIOf5;3KbV4Evhm%!RGEYG{po5pJHmb(6^2!JRvd<57EW*k5N)UNWmJ2 zWaw&3P&06$9@C;tW+ZItF}1-F)FW$bp;@Z=m02Yqt}LX+ajlb85cCr$Ej zHaOy{Tk1`VnJlWk5iG60-F&jzWQ+q$XSvCF7U;6oYv^)mC~N}vxY95<`Lv*}YCsLe zG(r|J09?k2#GP6M?CosttJqtDc9Hx2f6lCo-*xGeZ-!3pK6#=mx9(I;)4eNxoa)_^ z)BQ`q=)lY?>Zh+gGMfFt>&0Ij-|@r#8(ZbJ;iVHjW8?R=zaR$2v!^THoVxnJ@YC*+ z!V`V(e>;Mz?%92+&&VGxCbh4+#yju+z2)zZ)Uy+gD|@HjD-U*@&Rew}O}hC@*`NRR zVxS}IkaPV4En>JYBCd>L);ItE!8L#JaIL%CcJX#o+2gG%uf)|utNX8woxPnkxFD{K z!s)n5<6nfa`4tme^&a?V}rIeKf-b^hm~{^_Ny zyUwjWeDSUEk>}5EQ;xm8BDOYm;NiCO> zPzjN_3rnIXh`0d;%j`uVIJrbaj*WsVR z;uN>)G5&WG_;jm7J76eD ViNT>5rqxO6mSmKd=igL{{sFeq+Rgv~ literal 0 HcmV?d00001 diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles-linked-image.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles-linked-image.html new file mode 100644 index 000000000..574463156 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles-linked-image.html @@ -0,0 +1,29 @@ + + + + + + + Circle + + + Circle + + + Circle + + diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java index e368c9dd1..b4f4549de 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java @@ -738,6 +738,15 @@ public void testIssue493SVGStyles() throws IOException { assertTrue(vt.runTest("issue-493-svg-styles", TestSupport.WITH_SVG)); } + /** + * Tests that styles applying to internal svg objects such as circle + * in a linked img tag are applied and passed to Batik for rendering. + */ + @Test + public void testIssue493SVGStylesLinkedImage() throws IOException { + assertTrue(vt.runTest("issue-493-svg-styles-linked-image", TestSupport.WITH_SVG)); + } + /** * Tests that a broken image inside a table cell renders with a zero sized image rather * than crashing with a NPE. See issue 336. diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxReplacedElementFactory.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxReplacedElementFactory.java index 3083c66da..eaaf3c09e 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxReplacedElementFactory.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxReplacedElementFactory.java @@ -63,11 +63,18 @@ public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, boolean isDataImageSvg = false; if (_svgImpl != null && (srcAttr.endsWith(".svg") || (isDataImageSvg = srcAttr.startsWith("data:image/svg+xml;base64,")))) { XMLResource xml = isDataImageSvg ? XMLResource.load(new ByteArrayInputStream(ImageUtil.getEmbeddedBase64Image(srcAttr))) : uac.getXMLResource(srcAttr); - + if (xml != null) { - return new PdfBoxSVGReplacedElement(xml.getDocument().getDocumentElement(), _svgImpl, cssWidth, cssHeight, box, c, c.getSharedContext()); + Element svg = xml.getDocument().getDocumentElement(); + + // Copy across the class attribute so it can be targetted with CSS. + if (!e.getAttribute("class").isEmpty()) { + svg.setAttribute("class", e.getAttribute("class")); + } + + return new PdfBoxSVGReplacedElement(svg, _svgImpl, cssWidth, cssHeight, box, c, c.getSharedContext()); } - + return null; } else if (srcAttr.endsWith(".pdf")) { byte[] pdfBytes = uac.getBinaryResource(srcAttr);