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 9d54b1b86..b9ae52b87 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; @@ -85,6 +86,10 @@ public CalculatedStyle getRootElementStyle() { } } + public List getSelectors() { + return _matcher.getSelectors(); + } + /** * Sets the documentContext attribute of the StyleReference object * diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java index 2a5336944..b62c15089 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/CSSName.java @@ -302,6 +302,87 @@ public final class CSSName implements Comparable { new PrimitivePropertyBuilders.FSKeepWithInline() ); + /** + * Layer identifier (document-scoped). + */ + public final static CSSName FS_OCG_ID = + addProperty( + "-fs-ocg-id", + PRIMITIVE, + IdentValue.NONE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcId() + ); + /** + * Layer name, suitable for UI presentation (see {@code Name} entry in optional content group + * dictionary [ISO:32000-1:8.11.2.1]). + */ + public final static CSSName FS_OCG_LABEL = + addProperty( + "-fs-ocg-label", + PRIMITIVE, + IdentValue.NONE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcgLabel() + ); + /** + * Layer parent {@link #FS_OCG_ID reference}. + */ + public final static CSSName FS_OCG_PARENT = + addProperty( + "-fs-ocg-parent", + PRIMITIVE, + IdentValue.NONE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcId() + ); + /** + * Layer visibility. + */ + public final static CSSName FS_OCG_VISIBILITY = + addProperty( + "-fs-ocg-visibility", + PRIMITIVE, + IdentValue.VISIBLE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcgVisibility() + ); + /** + * Layer membership identifier (document-scoped). + */ + public final static CSSName FS_OCM_ID = + addProperty( + "-fs-ocm-id", + PRIMITIVE, + IdentValue.NONE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcId() + ); + /** + * Layer visibility policy (see {@code BaseState}, {@code ON}, {@code OFF} entries in {@code D} + * entry in optional content configuration dictionary [ISO:32000-1:8.11.4.3]). + */ + public final static CSSName FS_OCM_VISIBLE = + addProperty( + "-fs-ocm-visible", + PRIMITIVE, + IdentValue.ANY_VISIBLE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcmVisible() + ); + /** + * Layers belonging to the membership. + * + * Value: list of {@link #FS_OCG_ID layer references}. + */ + public final static CSSName FS_OCM_OCGS = + addProperty( + "-fs-ocm-ocgs", + PRIMITIVE, + IdentValue.NONE.asString(), + NOT_INHERITED, + new PrimitivePropertyBuilders.FSOcIds()); + /** * Unique CSSName instance for CSS2 property. */ diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java index ca62c822a..48dbfeec4 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/constants/IdentValue.java @@ -60,7 +60,11 @@ public class IdentValue implements FSDerivedValue { public final int FS_ID; public final static IdentValue ABSOLUTE = addValue("absolute"); + public final static IdentValue ALL_HIDDEN = addValue("all-hidden"); + public final static IdentValue ALL_VISIBLE = addValue("all-visible"); public final static IdentValue ALWAYS = addValue("always"); + public final static IdentValue ANY_HIDDEN = addValue("any-hidden"); + public final static IdentValue ANY_VISIBLE = addValue("any-visible"); public final static IdentValue ARMENIAN = addValue("armenian"); public final static IdentValue AUTO = addValue("auto"); public final static IdentValue AVOID = addValue("avoid"); 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 c7343e953..e9e2a9bf1 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 @@ -139,6 +139,10 @@ public PageInfo getPageCascadedStyle(String pageName, String pseudoPage) { public List getFontFaceRules() { return _fontFaceRules; } + + public List getSelectors() { + return docMapper.axes; + } public boolean isVisitedStyled(Object e) { return _visitElements.contains(e); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java index 46717cc58..167c817bb 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java @@ -184,7 +184,51 @@ protected BitSet getAllowed() { return BORDER_STYLES; } } - + + private static class GenericString extends AbstractPropertyBuilder { + private static final BitSet ALLOWED = setFor( + new IdentValue[] { IdentValue.NONE }); + + @Override + public List buildDeclarations( + CSSName cssName, List values, int origin, boolean important, boolean inheritAllowed) { + checkValueCount(cssName, 1, values.size()); + return Collections.singletonList( + checkDeclaration(cssName, values.get(0), origin, important, inheritAllowed)); + } + + protected PropertyDeclaration checkDeclaration( + CSSName cssName, CSSPrimitiveValue value, int origin, boolean important, boolean inheritAllowed) { + checkInheritAllowed(value, inheritAllowed); + if (value.getCssValueType() != CSSValue.CSS_INHERIT) { + switch (value.getPrimitiveType()) { + case CSSPrimitiveValue.CSS_STRING: + /* NOOP: Any custom string value accepted. */ + break; + case CSSPrimitiveValue.CSS_IDENT: + IdentValue ident = checkIdent(cssName, value); + checkValidity(cssName, ALLOWED, ident); + break; + default: + throw new CSSParseException("Value '" + value + "' is invalid for " + cssName, -1); + } + } + return new PropertyDeclaration(cssName, value, important, origin); + } + } + + private static class GenericStrings extends GenericString { + @Override + public List buildDeclarations( + CSSName cssName, List values, int origin, boolean important, boolean inheritAllowed) { + List declarations = new ArrayList<>(); + for (PropertyValue value : values) { + declarations.add(checkDeclaration(cssName, value, origin, important, inheritAllowed)); + } + return Collections.unmodifiableList(declarations); + } + } + public static class Direction extends SingleIdent { @Override protected BitSet getAllowed() { @@ -1014,6 +1058,38 @@ protected BitSet getAllowed() { } } + public static class FSOcgLabel extends GenericString { + } + + public static class FSOcgVisibility extends SingleIdent { + private static final BitSet ALLOWED = setFor( + new IdentValue[] { + IdentValue.VISIBLE, IdentValue.HIDDEN }); + + @Override + protected BitSet getAllowed() { + return ALLOWED; + } + } + + public static class FSOcId extends GenericString { + } + + public static class FSOcIds extends GenericStrings { + }; + + public static class FSOcmVisible extends SingleIdent { + private static final BitSet ALLOWED = setFor( + new IdentValue[] { + IdentValue.ALL_HIDDEN, IdentValue.ALL_VISIBLE, IdentValue.ANY_HIDDEN, + IdentValue.ANY_VISIBLE }); + + @Override + protected BitSet getAllowed() { + return ALLOWED; + } + } + public static class Left extends LengthLikeWithAuto { } 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 32c56bd4a..dbec8b512 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 @@ -105,4 +105,11 @@ public void addInvalidProperty(InvalidPropertyDeclaration invalidPropertyDeclara public List getInvalidPropertyDeclarations() { return _invalidProperties == null ? Collections.emptyList() : _invalidProperties; } + + @Override + public String toString() { + StringBuilder b=new StringBuilder(); + toCSS(b); + return b.toString(); + } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java index 43c1439cd..edd66e865 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java @@ -231,6 +231,11 @@ public void paintBackground(RenderingContext c, Box box) { paintBackground0(c, box.getStyle(), backgroundBounds, backgroundBounds, border); } + protected void onBackgroundPaint(RenderingContext c, CalculatedStyle style, + Rectangle backgroundBounds, Rectangle bgImageContainer, + BorderPropertySet border) { + } + private void paintBackground0( RenderingContext c, CalculatedStyle style, Rectangle backgroundBounds, Rectangle bgImageContainer, @@ -240,6 +245,8 @@ private void paintBackground0( return; } + onBackgroundPaint(c, style, backgroundBounds, bgImageContainer, border); + FSColor backgroundColor = style.getBackgroundColor(); List bgImages = style.getBackgroundImages(); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/displaylist/DisplayListPainter.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/displaylist/DisplayListPainter.java index 1176e3a44..cf42deb53 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/displaylist/DisplayListPainter.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/displaylist/DisplayListPainter.java @@ -75,6 +75,9 @@ private void paintBackgroundAndBorders(RenderingContext c, List box.paintBackground(c); box.paintBorder(c); + c.getOutputDevice().endStructure(innerToken); + c.getOutputDevice().endStructure(outerToken); + if (collapsedTableBorders != null && box instanceof TableCellBox) { TableCellBox cell = (TableCellBox) box; @@ -88,9 +91,6 @@ private void paintBackgroundAndBorders(RenderingContext c, List } } } - - c.getOutputDevice().endStructure(innerToken); - c.getOutputDevice().endStructure(outerToken); } } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LogMessageId.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LogMessageId.java index c4eab9b6d..792af132d 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LogMessageId.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/util/LogMessageId.java @@ -144,6 +144,7 @@ enum LogMessageId1Param implements LogMessageId { GENERAL_PDF_ACCESSIBILITY_NO_TITLE_TEXT_PROVIDED_FOR(XRLog.GENERAL, "PDF/UA - No title text provided for {}."), GENERAL_PDF_COULD_NOT_FIND_VALID_TARGET_FOR_BOOKMARK(XRLog.GENERAL, "Could not find valid target for bookmark. Bookmark href = {}"), GENERAL_PDF_COULD_NOT_FIND_VALID_TARGET_FOR_LINK(XRLog.GENERAL, "Could not find valid target for link. Link href = {}"), + GENERAL_PDF_LAYER_END(XRLog.GENERAL, "OC: {} END"), GENERAL_PDF_URI_IN_HREF_IS_NOT_A_VALID_URI(XRLog.GENERAL, "'{}' in href is not a valid URI, will be skipped"), GENERAL_PDF_FOUND_FORM_CONTROL_WITH_NO_ENCLOSING_FORM(XRLog.GENERAL, "Found form control ({}) with no enclosing form. Ignoring."), GENERAL_PDF_A_ELEMENT_DOES_NOT_HAVE_OPTION_CHILDREN(XRLog.GENERAL, "A <{}> element does not have children"), @@ -278,6 +279,12 @@ enum LogMessageId3Param implements LogMessageId { ", parent type={}" + ", expected child type={}" + ". Document will not be PDF/UA compliant."), + GENERAL_PDF_LAYER_BEGIN(XRLog.GENERAL, "OC: {} BEGIN (" + + "box type={}" + + ", element={})"), + GENERAL_PDF_LAYER_CONTINUE(XRLog.GENERAL, "OC: {} CONTINUE (" + + "box type={}" + + ", element={})"), EXCEPTION_URI_WITH_BASE_URI_INVALID(XRLog.EXCEPTION, "When trying to load uri({}) with base {} URI({}), one or both were invalid URIs."), EXCEPTION_SVG_SCRIPT_NOT_ALLOWED(XRLog.EXCEPTION, "Tried to run script inside SVG. Refusing. Details: {}, {}, {}"); diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java index 61f144704..f298b631f 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java @@ -21,12 +21,18 @@ import com.openhtmltopdf.bidi.BidiReorderer; import com.openhtmltopdf.bidi.SimpleBidiReorderer; +import com.openhtmltopdf.css.constants.CSSName; import com.openhtmltopdf.css.constants.IdentValue; +import com.openhtmltopdf.css.newmatch.Selector; +import com.openhtmltopdf.css.parser.CSSPrimitiveValue; import com.openhtmltopdf.css.parser.FSCMYKColor; import com.openhtmltopdf.css.parser.FSColor; import com.openhtmltopdf.css.parser.FSRGBColor; +import com.openhtmltopdf.css.sheet.PropertyDeclaration; import com.openhtmltopdf.css.style.CalculatedStyle; import com.openhtmltopdf.css.style.CssContext; +import com.openhtmltopdf.css.style.FSDerivedValue; +import com.openhtmltopdf.css.style.derived.BorderPropertySet; import com.openhtmltopdf.css.style.derived.FSLinearGradient; import com.openhtmltopdf.css.value.FontSpecification; import com.openhtmltopdf.extend.FSImage; @@ -51,11 +57,16 @@ import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentMembershipDictionary; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode; import org.w3c.dom.Document; @@ -90,6 +101,529 @@ private enum GraphicsOperation { CLIP; } + /** + * Optional content manager [ISO:32000-1:8.11]. + * + * Optional content rendering is CSS-driven. In the source HTML document, each content + * fragment can be marked by a CSS class defining a layer through the following + * properties: + * + * group: + * + * {@link CSSName#FS_OCG_ID} + * {@link CSSName#FS_OCG_LABEL} + * {@link CSSName#FS_OCG_PARENT} + * {@link CSSName#FS_OCG_VISIBILITY} + * + * + * membership: + * + * {@link CSSName#FS_OCM_ID} + * {@link CSSName#FS_OCM_OCGS} + * {@link CSSName#FS_OCM_VISIBLE} + * + * + * + * + * During page painting, structural operations on layer-marked contents (represented by + * {@link Box} objects) are wrapped inside possibly-nested layer fragments, keeping track + * of them in a stack. While inside the stack, layer fragments are alive (open/begun); + * conversely, once popped out of the stack, they are dead (closed/ended). + * Whenever possible (that is, when PDF/UA is disabled and no out-of-sequence operation occurs), + * dying fragments are kept alive until the next box is evaluated for contiguity, thus avoiding + * unnecessary fragmentation. Furthermore, new layer fragments are kept pending until + * {@link #onContentRender()} is called. This lazy mechanism ensures that layers are rendered + * inside the content stream only when effectively needed (otherwise, the content stream would + * be polluted by tons of empty layer fragments!). To recap, layer fragments may be in any of + * the following states: + * + * alive: inside the stack + * dead: removed from the stack + * dying: inside the stack, but candidate to removal + * pending: outside the stack, but candidate to insertion + * + * + * This class is NOT thread-safe. + * + * @apiNote For example, consider we want to define two layers ("OCG 1" and "OCG2"): + * +<style> + .ocg1 { + -fs-ocg-id: "ocg1"; + -fs-ocg-label: "OCG 1"; + } + + .ocg2 { + -fs-ocg-id: "ocg2"; + -fs-ocg-label: "OCG 2"; + -fs-ocg-parent: "ocg1"; + -fs-ocg-visibility: hidden; + } +</style> + * + * and then use them to mark our contents: + * +<body> + <p class="ocg1">(OCG 1) This is a layered content block.</p> + <p class="ocg2">(OCG 2) This is another layered content block.</p> +</body> + * + */ + private static final class OptionalContentManager { + private static final class GroupDeclaration extends LayerDeclaration { + public final String label; + @SuppressWarnings("unused") + public final String parent; + public final boolean visible; + + public GroupDeclaration(String id, String label, String parent, boolean visible) { + super(id); + this.label = label; + this.parent = parent; + this.visible = visible; + } + + @Override + public PDOptionalContentGroup build(OptionalContentManager manager) { + PDOptionalContentProperties configuration = manager.getConfiguration(); + if (configuration.hasGroup(label)) { + return configuration.getGroup(label); + } else { + PDOptionalContentGroup group = new PDOptionalContentGroup(label); + { + configuration.setGroupEnabled(group, visible); + /*- + * TODO: configure parent (hierarchical UI presentation)! + * + * Should map parent property to configuration (that is, Order entry of D + * entry of PDOptionalContentProperties) but, unfortunately, arbitrary group + * nesting seems not to be natively supported by currently-used PDFBox + * version (2.0), as adding a group via + * PDOptionalContentProperties.addGroup(..) automatically builds a flat list + * inside Order entry instead. + */ + } + configuration.addGroup(group); + return group; + } + } + } + + private static abstract class LayerDeclaration { + @SuppressWarnings("unused") + public final String id; + + public LayerDeclaration(String id) { + this.id = id; + } + + public abstract T build(OptionalContentManager manager); + } + + private static final class MembershipDeclaration extends LayerDeclaration { + public final List ocgs; + public final COSName visibilityPolicy; + + public MembershipDeclaration(String id, List ocgs, COSName visibilityPolicy) { + super(id); + this.ocgs = Collections.unmodifiableList(ocgs); + this.visibilityPolicy = visibilityPolicy; + } + + @Override + public PDOptionalContentMembershipDictionary build(OptionalContentManager manager) { + PDOptionalContentMembershipDictionary membership = new PDOptionalContentMembershipDictionary(); + { + List ocgs = new ArrayList<>(); + for (String ocg : this.ocgs) { + ocgs.add(manager.getLayer(ocg)); + } + membership.setOCGs(ocgs); + membership.setVisibilityPolicy(visibilityPolicy); + } + return membership; + } + } + + /** + * Layer fragment. + */ + private static final class StackLayer { + public final PDPropertyList base; + /** + * Whether this layer fragment is at block level. + */ + public final boolean blocked; + + /** + * Structural depth (non-positive values mean this layer fragment ended). + */ + private int level; + + public StackLayer(PDPropertyList base, boolean blocked) { + this.base = base; + this.blocked = blocked; + } + + public boolean out() { + return --level <= 0; + } + + public void in() { + level++; + } + + @Override + public String toString() { + return OptionalContentManager.toString(base); + } + } + + private static final CSSPrimitiveValue CSSEmptyValue = new CSSPrimitiveValue() { + @Override public short getCssValueType() { return 0; } + @Override public String getCssText() { return null; } + @Override public String getStringValue() { return null; } + @Override public short getPrimitiveType() { return 0; } + @Override public float getFloatValue(short unitType) { return 0; } + }; + private static final PropertyDeclaration EmptyPropertyDeclaration = new PropertyDeclaration(null, CSSEmptyValue, false, 0); + + private static String toString(PDPropertyList layer) { + StringBuilder b = new StringBuilder(); + toString(layer, b); + return b.toString(); + } + + private static void toString(PDPropertyList layer, StringBuilder b) { + if (layer instanceof PDOptionalContentGroup) { + b.append("Layer '").append(((PDOptionalContentGroup)layer).getName()).append("'"); + } else if (layer instanceof PDOptionalContentMembershipDictionary) { + b.append("Membership ["); + List ocgs = ((PDOptionalContentMembershipDictionary)layer).getOCGs(); + for (int i = 0; i < ocgs.size(); i++) { + if (i > 0) { + b.append(", "); + } + toString(ocgs.get(i), b); + } + b.append("]"); + } else + throw new UnsupportedOperationException(layer.toString()); + } + + private static String toString(Box box) { + Object obj = box instanceof InlineLayoutBox ? ((InlineLayoutBox)box).getInlineChildren() : box.getElement(); + return obj != null ? obj.toString() : null; + } + + /** + * Current box. + */ + private Box _box; + /** + * Current box's layers. + */ + private LinkedList _boxLayers = new LinkedList<>(); + /** + * Current structure. + */ + private StructureType _structureType; + + private PDOptionalContentProperties _configuration; + /** + * Layers mapped in the output document. + */ + private Map _layers = new HashMap<>(); + /** + * Layer definitions (CSS-based). + */ + private Map> _layerDeclarations; + + /** + * Layer fragments currently open in content stream. + */ + private LinkedList _stack = new LinkedList<>(); + /** + * Whether there are layers associated to the {@link #box current box} which are waiting to + * be opened in content stream in case of actual content fragments. + */ + private boolean _pending; + + private final PdfBoxFastOutputDevice _od; + + public OptionalContentManager(PdfBoxFastOutputDevice od){ + _od = od; + } + + /** + * Closes any layer fragment up to the given one inside the content stream. + * + * @param layer + * Upper limit (inclusive) of layer fragments stack closure ({@code null}, to end + * all layer fragments). + */ + public void end(PDPropertyList layer) { + while (!_stack.isEmpty()) { + XRLog.log(Level.FINE, LogMessageId.LogMessageId1Param.GENERAL_PDF_LAYER_END, _stack.peek()); + + _od._cp.endMarkedContent(); + + if (_stack.pop().base == layer) { + break; + } + } + _pending = false; + } + + /** + * Notifies content rendering. + */ + public void onContentRender() { + if (_pending) { + // Open pending layers inside the content stream! + start(); + } + } + + /** + * Notifies the end of current structure. + * + * In case of an unstructured document, dying layer fragments are kept alive to possibly + * merge with contiguous fragments. + * + * To ensure proper nesting, it MUST be called BEFORE closing the current structure. + */ + public void onStructureEnd() { + /* + * NOTE: As recommended by the PDF spec [ISO:32000-1:8.11.3.2], layer fragments SHOULD + * be nested inside other marked content; therefore, if PDF/UA is active, current layer + * fragments are immediately ended. Otherwise, layer fragments can continue across + * contiguous inline boxes only; on the contrary, layer fragments at block level are + * always immediately ended, because block-painting operations may be intermingled with + * interfering out-of-sequence graphical operations (for example, table border is + * painted between cells' background and cells' content drawing). + */ + if(!_stack.isEmpty() && ( + (_stack.peek().out() && _stack.peek().blocked) + || _od._pdfUa != null)) { + end(null); + } + } + + /** + * Notifies the start of pending structure. + * + * Dead layer fragments are closed, while new layer fragments are kept pending until + * {@link #onContentRender()} is called. This lazy mechanism ensures that layers are + * rendered inside the content stream only when effectively needed (otherwise, the content + * stream would be polluted by tons of empty layer fragments!). + * + * To ensure proper nesting, it MUST be called BEFORE opening the pending structure. + */ + public void onStructureStart(StructureType type, Box box) { + /*- + * NOTE: For optional content [ISO:32000-1:8.11.3.2], to avoid conflict with other + * features that used marked content (such as logical structure), the following strategy + * is recommended: + * - where content is to be tagged with optional content markers as well as other + * markers, the optional content markers should be nested INSIDE the other marked + * content. + * - where optional content and the other markers would overlap but there is not strict + * containment, the optional content should be BROKEN UP into two or more BDC/EMC + * sections, nesting the optional content sections inside the others as necessary. + * Breaking up optional content spans does not damage the nature of the visibility of + * the content, whereas the same guarantee cannot be made for all other uses of marked + * content. + * + * Since the painting phase deals with imperative operations instead of declarative + * boxes, a box associated to a layer may be rendered through multiple (possibly + * non-contiguous) operations, each one described by one or more nested structures. In + * order to merge contiguous contents belonging to the same layer, current layer stack + * is compared to the layer hierarchy of the next box, closing their non-matching + * levels. + */ + // Extracting box layer hierarchy... + _boxLayers.clear(); + Box parentBox = box; + while (parentBox != null) { + PDPropertyList layer = getLayer(parentBox); + if (layer != null && (_boxLayers.isEmpty() || _boxLayers.getLast() != layer)) { + _boxLayers.add(layer); + } + parentBox = parentBox.getParent(); + } + // Closing dead layer fragments... + for (int i = 0; i < _stack.size(); i++) { + if (i >= _boxLayers.size() || _stack.get(i).base != _boxLayers.get(i)) { + end(_stack.get(i).base); + break; + } + } + if (!_stack.isEmpty()) { + XRLog.log(Level.FINE, LogMessageId.LogMessageId3Param.GENERAL_PDF_LAYER_CONTINUE, _stack, _structureType, toString(box)); + } + if (_pending = (_stack.size() != _boxLayers.size())) { + _box = box; + _structureType = type; + } + } + + /** + * Layer configuration in the output document. + * + * Created on the fly if missing. + */ + private PDOptionalContentProperties getConfiguration() { + if (_configuration == null) { + PDDocumentCatalog catalog = _od._writer.getDocumentCatalog(); + _configuration = catalog.getOCProperties(); + if (_configuration == null) { + catalog.setOCProperties(_configuration = new PDOptionalContentProperties()); + } + } + return _configuration; + } + + /** + * Gets the CSS-based declaration associated to the given ID. + * + * Layer declarations are lazily harvested from the CSS rulesets available in the + * current context. + * + * @param id + * Layer ID (either {@link CSSName#FS_OCG_ID group} or + * {@link CSSName#FS_OCM_ID membership}). + * @return + * {@code null}, if no match found. + */ + @SuppressWarnings("unchecked") + private > T getDeclaration(String id) { + if (_layerDeclarations == null) { + /*- + * TODO: This could be strengthened if extension at-rules were implemented (@-fs-ocg + * for optional content groups and @-fs-ocm for optional content memberships) beside + * existing standard at-rules (such as fontFaceRules) in + * com.openhtmltopdf.css.sheet.Stylesheet). + */ + _layerDeclarations = new HashMap<>(); + Map layerProperties = new HashMap<>(); + for (Selector selector : _od._sharedContext.getCss().getSelectors()) { + for (PropertyDeclaration property : selector.getRuleset().getPropertyDeclarations()) { + CSSName propertyName = property.getCSSName(); + if (propertyName == CSSName.FS_OCG_ID + || propertyName == CSSName.FS_OCG_LABEL + || propertyName == CSSName.FS_OCG_PARENT + || propertyName == CSSName.FS_OCG_VISIBILITY + || propertyName == CSSName.FS_OCM_ID + || propertyName == CSSName.FS_OCM_OCGS + || propertyName == CSSName.FS_OCM_VISIBLE) { + layerProperties.put(propertyName, property); + } + } + if (layerProperties.isEmpty()) + continue; + + PropertyDeclaration layerIdProperty; + LayerDeclaration> layerDeclaration; + if ((layerIdProperty = layerProperties.get(CSSName.FS_OCG_ID)) != null) { + layerDeclaration = new GroupDeclaration(layerIdProperty.getValue().getStringValue(), + layerProperties.get(CSSName.FS_OCG_LABEL).getValue().getStringValue(), + layerProperties.getOrDefault(CSSName.FS_OCG_PARENT, EmptyPropertyDeclaration).getValue().getStringValue(), + getPropertyValue(layerProperties, CSSName.FS_OCG_VISIBILITY).equals(IdentValue.VISIBLE)); + } else if ((layerIdProperty = layerProperties.get(CSSName.FS_OCM_ID)) != null) { + List ocgs = new ArrayList<>(); + { + PropertyDeclaration ocgsProperty = layerProperties.get(CSSName.FS_OCM_OCGS); + if(ocgsProperty == null) + throw new RuntimeException(CSSName.FS_OCM_OCGS + " property undefined (" + layerIdProperty + ")"); + + ocgs = Arrays.asList(ocgsProperty.getValue().getStringValue().split(" ")); + } + COSName visibilityPolicy; + { + IdentValue rawVisibilityPolicy = getPropertyValue(layerProperties, CSSName.FS_OCM_VISIBLE); + if (rawVisibilityPolicy == IdentValue.ALL_HIDDEN) { + visibilityPolicy = COSName.ALL_OFF; + } else if (rawVisibilityPolicy == IdentValue.ALL_VISIBLE) { + visibilityPolicy = COSName.ALL_ON; + } else if (rawVisibilityPolicy == IdentValue.ANY_HIDDEN) { + visibilityPolicy = COSName.ANY_OFF; + } else { + visibilityPolicy = COSName.ANY_ON; + } + } + layerDeclaration = new MembershipDeclaration(layerIdProperty.getValue().getStringValue(), + ocgs, + visibilityPolicy); + } else + throw new UnsupportedOperationException("Broken layer property set: " + layerProperties); + + _layerDeclarations.put(layerIdProperty.getValue().getStringValue(), layerDeclaration); + layerProperties.clear(); + } + } + return (T)_layerDeclarations.get(id); + } + + /** + * Gets the layer associated to the given ID, ensuring its existence in the output + * document. + * + * @throws NullPointerException if no match found. + */ + private PDPropertyList getLayer(String id) { + return _layers.computeIfAbsent(id, k -> getDeclaration(k).build(this)); + } + + /** + * Gets the layer directly associated to the given box, ensuring its existence in the output + * document. + * + * Boxes can be directly associated to at most one layer through CSS class mechanism; + * they also inherit any layer associated to their ancestor boxes. + */ + private PDPropertyList getLayer(Box box) { + FSDerivedValue layerIdObj; + { + CalculatedStyle style = box.getStyle(); + layerIdObj = style.valueByName(CSSName.FS_OCG_ID); + if(layerIdObj.isIdent()) { + layerIdObj = style.valueByName(CSSName.FS_OCM_ID); + } + } + return !layerIdObj.isIdent() ? getLayer(layerIdObj.asString()) : null; + } + + private static IdentValue getPropertyValue(Map properties, CSSName propertyName) { + return properties.containsKey(propertyName) + ? properties.get(propertyName).asIdentValue() + : IdentValue.getByIdentString(CSSName.initialValue(propertyName)); + } + + /** + * Opens pending layers inside the content stream. + * + * In case of multiple layers, they are recursively nested. + */ + private void start() { + if (!_pending) + return; + + _pending = false; + + for (int i = _stack.size(); i < _boxLayers.size(); i++) { + PDPropertyList layer = _boxLayers.get(i); + + XRLog.log(Level.FINE, LogMessageId.LogMessageId3Param.GENERAL_PDF_LAYER_BEGIN, toString(layer), _structureType, toString(_box)); + + _od._cp.beginMarkedContent(COSName.OC, layer); + + _stack.push(new StackLayer(layer, _box instanceof BlockBox)); + _stack.peek().in(); + } + } + } + private static class PageState { // The actual fill and stroke colors set on the PDF graphics stream. // We keep these so we don't bloat the PDF with unneeded color calls. @@ -198,7 +732,9 @@ private PageState copy() { private final boolean _pdfUaConform; private final boolean _pdfAConform; - + + private OptionalContentManager ocManager = new OptionalContentManager(this); + public PdfBoxFastOutputDevice(float dotsPerPoint, boolean testMode, boolean pdfUaConform, boolean pdfAConform) { _dotsPerPoint = dotsPerPoint; _testMode = testMode; @@ -259,6 +795,8 @@ private PageState popState() { @Override public void finishPage() { + ocManager.end(null) /* Ensures that any pending layer is closed */; + _cp.closeContent(); popState(); @@ -273,6 +811,12 @@ public void paintReplacedElement(RenderingContext c, BlockBox box) { element.paint(c, this, box); } + @Override + protected void onBackgroundPaint(RenderingContext c, CalculatedStyle style, + Rectangle backgroundBounds, Rectangle bgImageContainer, BorderPropertySet border) { + ocManager.onContentRender(); + } + /** * We use paintBackground to do extra stuff such as processing links, forms and form controls. */ @@ -443,6 +987,8 @@ public void drawStringFast(String s, float x, float y, JustificationInfo info, F if (s.length() == 0) return; + ocManager.onContentRender(); + ensureFillColor(); AffineTransform at = new AffineTransform(getTransform()); at.translate(x, y); @@ -577,7 +1123,9 @@ public PDPage getPage(){ private void followPath(Shape s, GraphicsOperation drawType) { if (s == null) return; - + + ocManager.onContentRender(); + if (drawType == GraphicsOperation.STROKE) { if (!(_stroke instanceof BasicStroke)) { s = _stroke.createStrokedShape(s); @@ -795,12 +1343,16 @@ public void realizeImage(PdfBoxImage img) { @Override public void drawLinearGradient(FSLinearGradient backgroundLinearGradient, Shape bounds) { + ocManager.onContentRender(); + PDShading shading = GradientHelper.createLinearGradient(this, getTransform(), backgroundLinearGradient, bounds); _cp.paintGradient(shading); } @Override public void drawImage(FSImage fsImage, int x, int y, boolean interpolate) { + ocManager.onContentRender(); + PdfBoxImage img = (PdfBoxImage) fsImage; PDImageXObject xobject = img.getXObject(); @@ -845,6 +1397,8 @@ public void drawImage(FSImage fsImage, int x, int y, boolean interpolate) { @Override public void drawPdfAsImage(PDFormXObject _srcObject, Rectangle contentBounds, float intrinsicWidth, float intrinsicHeight) { + ocManager.onContentRender(); + // We start with the page margins... AffineTransform af = AffineTransform.getTranslateInstance( getTransform().getTranslateX(), @@ -1065,6 +1619,8 @@ public boolean isSupportsCMYKColors() { @Override public void drawWithGraphics(float x, float y, float width, float height, OutputDeviceGraphicsDrawer renderer) { + ocManager.onContentRender(); + try { PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(_writer, (int) width, (int) height); /* @@ -1250,14 +1806,15 @@ private void clearPageState() { @Override public Object startStructure(StructureType type, Box box) { - if (_pdfUa != null) { - return _pdfUa.startStructure(type, box); - } - return null; + ocManager.onStructureStart(type, box); + + return _pdfUa != null ? _pdfUa.startStructure(type, box) : null; } @Override public void endStructure(Object token) { + ocManager.onStructureEnd(); + if (_pdfUa != null) { _pdfUa.endStructure(token); } diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java index 664e1cbbf..82bbf9f41 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java @@ -356,8 +356,12 @@ public void placeXForm(float x, float y, PDFormXObject xFormObject) { } public void beginMarkedContent(COSName tag, COSDictionary dict) { + beginMarkedContent(tag, PDPropertyList.create(dict)); + } + + public void beginMarkedContent(COSName tag, PDPropertyList propertyList) { try { - cs.beginMarkedContent(tag, PDPropertyList.create(dict)); + cs.beginMarkedContent(tag, propertyList); } catch (IOException e) { logAndThrow("beginMarkedContent", e); }
Value: list of {@link #FS_OCG_ID layer references}.
Optional content rendering is CSS-driven. In the source HTML document, each content + * fragment can be marked by a CSS class defining a layer through the following + * properties:
During page painting, structural operations on layer-marked contents (represented by + * {@link Box} objects) are wrapped inside possibly-nested layer fragments, keeping track + * of them in a stack. While inside the stack, layer fragments are alive (open/begun); + * conversely, once popped out of the stack, they are dead (closed/ended). + * Whenever possible (that is, when PDF/UA is disabled and no out-of-sequence operation occurs), + * dying fragments are kept alive until the next box is evaluated for contiguity, thus avoiding + * unnecessary fragmentation. Furthermore, new layer fragments are kept pending until + * {@link #onContentRender()} is called. This lazy mechanism ensures that layers are rendered + * inside the content stream only when effectively needed (otherwise, the content stream would + * be polluted by tons of empty layer fragments!). To recap, layer fragments may be in any of + * the following states:
This class is NOT thread-safe.
+<style> + .ocg1 { + -fs-ocg-id: "ocg1"; + -fs-ocg-label: "OCG 1"; + } + + .ocg2 { + -fs-ocg-id: "ocg2"; + -fs-ocg-label: "OCG 2"; + -fs-ocg-parent: "ocg1"; + -fs-ocg-visibility: hidden; + } +</style> + *
+<body> + <p class="ocg1">(OCG 1) This is a layered content block.</p> + <p class="ocg2">(OCG 2) This is another layered content block.</p> +</body> + *
In case of an unstructured document, dying layer fragments are kept alive to possibly + * merge with contiguous fragments.
To ensure proper nesting, it MUST be called BEFORE closing the current structure.
Dead layer fragments are closed, while new layer fragments are kept pending until + * {@link #onContentRender()} is called. This lazy mechanism ensures that layers are + * rendered inside the content stream only when effectively needed (otherwise, the content + * stream would be polluted by tons of empty layer fragments!).
To ensure proper nesting, it MUST be called BEFORE opening the pending structure.
Created on the fly if missing.
Layer declarations are lazily harvested from the CSS rulesets available in the + * current context.
Boxes can be directly associated to at most one layer through CSS class mechanism; + * they also inherit any layer associated to their ancestor boxes.
In case of multiple layers, they are recursively nested.