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/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/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 0038be132..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 @@ -20,16 +20,19 @@ */ 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; 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; @@ -43,36 +46,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; @@ -84,6 +80,25 @@ 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) { + // 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); + + return descendants.toCSS(); + } + /** * May return null. * We assume that restyle has already been done by a getCascadedStyle if necessary. @@ -204,14 +219,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) { @@ -260,16 +267,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 +296,43 @@ 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 +345,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 +424,78 @@ 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()); + } + } + } + + 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; + } + + 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); + } } - return cs; } } } 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..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 @@ -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,79 @@ 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; + } + + Selector ancestor = this; + + while (ancestor != null) { + Selector current = ancestor.getAncestorSelector(); + + if (current == null || stopAt.contains(current)) { + break; + } + + ancestor = current; + } + + Selector chained = ancestor; + + if (chained.getAxis() == Selector.CHILD_AXIS) { + sb.append('>'); + sb.append(' '); + } + + 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 +565,55 @@ public void setSiblingSelector(Selector selector) { public void setNamespaceURI(String namespaceURI) { _namespaceURI = namespaceURI; } + + public void setAncestorSelector(Selector ancestor) { + _ancestorSelector = 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 b3e26da98..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; @@ -766,7 +767,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) { @@ -854,6 +856,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) { @@ -1240,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; } @@ -1313,6 +1318,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 5de44581b..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(); } /** @@ -231,6 +225,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..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; @@ -71,69 +71,38 @@ public int getOrigin() { return _origin; } -}// end class + public void toCSS(StringBuilder sb) { + List decls; -/* - * $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. - * - * - */ + 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 : decls) { + decl.toCSS(sb); + sb.append('\n'); + } + sb.append('}'); + sb.append('\n'); + } + 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/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 000000000..2a560e47d Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles-linked-image.pdf differ 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 000000000..5a7415013 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-493-svg-styles.pdf differ 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/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..90bcb1653 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-493-svg-styles.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + 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..b4f4549de 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,25 @@ 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 + 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); 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..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 @@ -14,6 +14,7 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.Text; import com.openhtmltopdf.extend.OutputDevice; import com.openhtmltopdf.extend.SVGDrawer.SVGImage; @@ -166,12 +167,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 && !styles.isEmpty()) { + 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);