From f60541efc3acae18baa4108454e8a0bab1695042 Mon Sep 17 00:00:00 2001 From: Dan Fickle Date: Sat, 12 Nov 2016 17:12:36 +1100 Subject: [PATCH] For #38 - Implements transform-origin and the infrastructure for transform. [ci skip] Next step is to create a list of AffineTransform objects in the property builder for transform. --- .../openhtmltopdf/css/constants/CSSName.java | 47 +++++--- .../property/PrimitivePropertyBuilders.java | 71 +++++++++++- .../css/style/CalculatedStyle.java | 4 + .../openhtmltopdf/extend/OutputDevice.java | 18 ++- .../java/com/openhtmltopdf/layout/Layer.java | 59 +++++++--- .../openhtmltopdf/render/InlineLayoutBox.java | 12 -- .../swing/Java2DOutputDevice.java | 20 +++- .../pdfboxout/PdfBoxOutputDevice.java | 106 ++++++++++++++++-- 8 files changed, 281 insertions(+), 56 deletions(-) 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 0396cee53..6d1359ffc 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 @@ -1129,20 +1129,27 @@ public final class CSSName implements Comparable { NOT_INHERITED, new PrimitivePropertyBuilders.TransformPropertyBuilder() ); - - /** - * Unique CSSName instance for CSS3 property. - */ - public final static CSSName TRANSFORM_ORIGIN = - addProperty( - "transform-origin", - PRIMITIVE, - "50% 50%", - NOT_INHERITED, - new PrimitivePropertyBuilders.TransformOriginPropertyBuilder() - ); - - + + public final static CSSName FS_TRANSFORM_ORIGIN_X = + addProperty( + "-fs-transform-origin-x", + PRIMITIVE, + "50%", + NOT_INHERITED, + new PrimitivePropertyBuilders.TransformOriginX() + ); + + public final static CSSName FS_TRANSFORM_ORIGIN_Y = + addProperty( + "-fs-transform-origin-y", + PRIMITIVE, + "50%", + NOT_INHERITED, + new PrimitivePropertyBuilders.TransformOriginY() + ); + + + /** * Unique CSSName instance for CSS2 property. */ @@ -1648,6 +1655,18 @@ public final class CSSName implements Comparable { NOT_INHERITED, new SizePropertyBuilder() ); + + /** + * Unique CSSName instance for CSS3 property. + */ + public final static CSSName TRANSFORM_ORIGIN_SHORTHAND = + addProperty( + "transform-origin", + SHORTHAND, + "50% 50%", + NOT_INHERITED, + new PrimitivePropertyBuilders.TransformOriginPropertyBuilder() + ); public final static CSSSideProperties MARGIN_SIDE_PROPERTIES = new CSSSideProperties( 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 80ab1f674..189f0adc0 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 @@ -20,6 +20,7 @@ package com.openhtmltopdf.css.parser.property; import java.util.ArrayList; +import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.Iterator; @@ -1637,13 +1638,81 @@ public List buildDeclarations(CSSName cssName, List values, int origin, boolean important, origin)); } } + + // 0 | left | right | center | length | percentage + public static class TransformOriginX extends AbstractPropertyBuilder { + private static final BitSet ALLOWED = setFor(new IdentValue[] { IdentValue.LEFT, IdentValue.CENTER, IdentValue.RIGHT }); + + @Override + public List buildDeclarations(CSSName cssName, List values, int origin, boolean important, boolean inheritAllowed) { + checkValueCount(cssName, 1, values.size()); + CSSPrimitiveValue value = (CSSPrimitiveValue) values.get(0); + checkInheritAllowed(value, inheritAllowed); + + if (value.getCssValueType() == CSSPrimitiveValue.CSS_INHERIT) { + return Collections.singletonList(new PropertyDeclaration(cssName, value, important, origin)); + } + + if (value.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) { + IdentValue ident = checkIdent(cssName, value); + checkValidity(cssName, ALLOWED, ident); + if (ident == IdentValue.LEFT) { + return Collections.singletonList(new PropertyDeclaration(cssName, new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 0f, "0%"), important, origin)); + } else if (ident == IdentValue.CENTER) { + return Collections.singletonList(new PropertyDeclaration(cssName, new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 50f, "50%"), important, origin)); + } else { // if (ident == IdentValue.RIGHT) + return Collections.singletonList(new PropertyDeclaration(cssName, new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 100f, "100%"), important, origin)); + } + } else { + checkLengthOrPercentType(cssName, value); + return Collections.singletonList(new PropertyDeclaration(cssName, value, important, origin)); + } + } + } + + // 0 | top | bottom | center | length | percentage + public static class TransformOriginY extends AbstractPropertyBuilder { + private static final BitSet ALLOWED = setFor(new IdentValue[] { IdentValue.TOP, IdentValue.CENTER, IdentValue.BOTTOM }); + + @Override + public List buildDeclarations(CSSName cssName, List values, int origin, boolean important, boolean inheritAllowed) { + checkValueCount(cssName, 1, values.size()); + CSSPrimitiveValue value = (CSSPrimitiveValue) values.get(0); + checkInheritAllowed(value, inheritAllowed); + + if (value.getCssValueType() == CSSPrimitiveValue.CSS_INHERIT) { + return Collections.singletonList(new PropertyDeclaration(cssName, value, important, origin)); + } + + if (value.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) { + IdentValue ident = checkIdent(cssName, value); + checkValidity(cssName, ALLOWED, ident); + if (ident == IdentValue.TOP) { + return Collections.singletonList(new PropertyDeclaration(cssName, new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 0f, "0%"), important, origin)); + } else if (ident == IdentValue.CENTER) { + return Collections.singletonList(new PropertyDeclaration(cssName, new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 50f, "50%"), important, origin)); + } else { // if (ident == IdentValue.BOTTOM) + return Collections.singletonList(new PropertyDeclaration(cssName, new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 100f, "100%"), important, origin)); + } + } else { + checkLengthOrPercentType(cssName, value); + return Collections.singletonList(new PropertyDeclaration(cssName, value, important, origin)); + } + } + } public static class TransformOriginPropertyBuilder extends AbstractPropertyBuilder { @Override public List buildDeclarations(CSSName cssName, List values, int origin, boolean important, boolean inheritAllowed) { checkValueCount(cssName, 2, 3, values.size()); - return Collections.singletonList(new PropertyDeclaration(CSSName.TRANSFORM_ORIGIN, new PropertyValue(values), important, origin)); + CSSPrimitiveValue x = (CSSPrimitiveValue) values.get(0); + CSSPrimitiveValue y = (CSSPrimitiveValue) values.get(1); + + return Arrays.asList( + new TransformOriginX().buildDeclarations(CSSName.FS_TRANSFORM_ORIGIN_X, Collections.singletonList(x), origin, important).get(0), + new TransformOriginY().buildDeclarations(CSSName.FS_TRANSFORM_ORIGIN_Y, Collections.singletonList(y), origin, important).get(0) + ); } } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java index 9654e22dd..bebada315 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java @@ -959,6 +959,10 @@ public boolean establishesBFC() { } public boolean requiresLayer() { + if (!isIdent(CSSName.TRANSFORM, IdentValue.NONE)) { + return true; + } + FSDerivedValue value = valueByName(CSSName.POSITION); if (value instanceof FunctionValue) { // running(header) diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java index 51ec4b962..4c3030634 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java @@ -33,12 +33,6 @@ public interface OutputDevice { // Required for SVG output. public void saveState(); public void restoreState(); - - /** - * Apply the given transform on top of the current one. You should - * use saveState() and restoreState() to restore the transform after you are done with whatever you would like to do here - */ - public void applyTransform(AffineTransform transform); public void setPaint(Paint paint); public void setAlpha(int alpha); @@ -46,6 +40,18 @@ public interface OutputDevice { public void setRawClip(Shape s); public void rawClip(Shape s); public Shape getRawClip(); + + // Required for CSS transforms. + + /** + * Apply the given transform on top of the current one in the PDF graphics stream. + * This is a cumulative operation. You should popTransform after the box and children are painted. + */ + public void pushTransform(AffineTransform transform); + public void popTransform(); + + public AffineTransform translateTransform(float translateX, float translateY); + public void translateTransform(AffineTransform inverse); // And the rest. public void drawText(RenderingContext c, InlineText inlineText); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Layer.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Layer.java index 0a52c9339..03bd8b82e 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Layer.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Layer.java @@ -20,15 +20,18 @@ package com.openhtmltopdf.layout; import com.openhtmltopdf.css.constants.CSSName; +import com.openhtmltopdf.css.constants.IdentValue; import com.openhtmltopdf.css.constants.PageElementPosition; import com.openhtmltopdf.css.newmatch.PageInfo; import com.openhtmltopdf.css.style.CalculatedStyle; import com.openhtmltopdf.css.style.CssContext; import com.openhtmltopdf.css.style.EmptyStyle; +import com.openhtmltopdf.css.style.FSDerivedValue; import com.openhtmltopdf.newtable.TableCellBox; import com.openhtmltopdf.render.*; import java.awt.*; +import java.awt.geom.AffineTransform; import java.util.*; import java.util.List; @@ -87,7 +90,8 @@ public Layer(Layer parent, Box master) { _parent = parent; _master = master; setStackingContext( - master.getStyle().isPositioned() && ! master.getStyle().isAutoZIndex()); + (master.getStyle().isPositioned() && !master.getStyle().isAutoZIndex()) || + (!master.getStyle().isIdent(CSSName.TRANSFORM, IdentValue.NONE))); master.setLayer(this); master.setContainingLayer(this); } @@ -108,6 +112,10 @@ public int getZIndex() { return (int) _master.getStyle().asFloat(CSSName.Z_INDEX); } + public boolean isZIndexAuto() { + return _master.getStyle().isIdent(CSSName.Z_INDEX, IdentValue.AUTO); + } + public Box getMaster() { return _master; } @@ -186,14 +194,18 @@ private List getStackingContextLayers(int which) { Layer target = (Layer)children.get(i); if (target.isStackingContext()) { - int zIndex = target.getZIndex(); - if (which == NEGATIVE && zIndex < 0) { - result.add(target); - } else if (which == POSITIVE && zIndex > 0) { - result.add(target); - } else if (which == ZERO && zIndex == 0) { - result.add(target); - } + if (!target.isZIndexAuto()) { + int zIndex = target.getZIndex(); + if (which == NEGATIVE && zIndex < 0) { + result.add(target); + } else if (which == POSITIVE && zIndex > 0) { + result.add(target); + } else if (which == ZERO && zIndex == 0) { + result.add(target); + } + } else if (which == ZERO) { + result.add(target); + } } } @@ -225,6 +237,7 @@ private void paintBackgroundsAndBorders( helper.popClipRegions(c, i); BlockBox box = (BlockBox)blocks.get(i); + box.paintBackground(c); box.paintBorder(c); if (c.debugDrawBoxes()) { @@ -240,7 +253,7 @@ private void paintBackgroundsAndBorders( } } } - + helper.pushClipRegion(c, i); } @@ -282,6 +295,8 @@ public void paint(RenderingContext c) { positionFixedLayer(c); } + boolean transformed = applyTranform(c, getMaster()); + if (isRootLayer()) { getMaster().paintRootElementBackground(c); } @@ -325,6 +340,25 @@ public void paint(RenderingContext c) { paintLayers(c, getSortedLayers(POSITIVE)); } } + + if (transformed) { + c.getOutputDevice().popTransform(); + } + } + + protected boolean applyTranform(RenderingContext c, Box box) { + FSDerivedValue transform = box.getStyle().valueByName(CSSName.TRANSFORM); + if (transform.isIdent() && transform.asIdentValue() == IdentValue.NONE) + return false; + + // By default the transform point is the lower left of the page, so we need to translate to correctly apply transform. + float translateX = box.getStyle().getFloatPropertyProportionalWidth(CSSName.FS_TRANSFORM_ORIGIN_X, box.getWidth(), c) + box.getAbsX(); + float translateY = box.getStyle().getFloatPropertyProportionalHeight(CSSName.FS_TRANSFORM_ORIGIN_Y, box.getHeight(), c) + box.getAbsY(); + + AffineTransform translate = c.getOutputDevice().translateTransform(translateX, translateY); + c.getOutputDevice().pushTransform(AffineTransform.getRotateInstance(Math.toRadians(-70))); + c.getOutputDevice().translateTransform(translate); + return true; } private List getFloats() { @@ -448,15 +482,14 @@ public void paintAsLayer(RenderingContext c, BlockBox startingPoint) { this, startingPoint, blocks, lines, rangeLists); Map collapsedTableBorders = collectCollapsedTableBorders(c, blocks); - + paintBackgroundsAndBorders(c, blocks, collapsedTableBorders, rangeLists); paintListMarkers(c, blocks, rangeLists); paintInlineContent(c, lines, rangeLists); paintSelection(c, lines); // XXX only do when there is a selection paintReplacedElements(c, blocks, rangeLists); } - - + private void paintListMarkers(RenderingContext c, List blocks, BoxRangeLists rangeLists) { BoxRangeHelper helper = new BoxRangeHelper(c.getOutputDevice(), rangeLists.getBlock()); diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java index dc73d5f38..8b0956bd9 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/InlineLayoutBox.java @@ -248,9 +248,6 @@ public void paintInline(RenderingContext c) { return; } - //c.getOutputDevice().saveState(); - //applyTranform(c); - paintBackground(c); paintBorder(c); @@ -285,8 +282,6 @@ public void paintInline(RenderingContext c) { } } } - - //c.getOutputDevice().restoreState(); } public int getBorderSides() { @@ -937,11 +932,4 @@ public float adjustHorizontalPosition(JustificationInfo info, float adjust) { public int getEffectiveWidth() { return getInlineWidth(); } - - protected void applyTranform(RenderingContext c) { - FSDerivedValue transform = getStyle().valueByName(CSSName.TRANSFORM); - if (transform.isIdent() && transform.asIdentValue() == IdentValue.NONE) - return; - FSDerivedValue transformOrigin = getStyle().valueByName(CSSName.TRANSFORM_ORIGIN); - } } diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java index 3cd714897..c548017bd 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java @@ -323,9 +323,27 @@ public Shape getRawClip() { } @Override - public void applyTransform(AffineTransform transform) { + public void pushTransform(AffineTransform transform) { AffineTransform currentTransform = _graphics.getTransform(); currentTransform.concatenate(transform); _graphics.setTransform(currentTransform); } + + @Override + public void popTransform() { + // TODO Auto-generated method stub + + } + + @Override + public AffineTransform translateTransform(float translateX, float translateY) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void translateTransform(AffineTransform inverse) { + // TODO Auto-generated method stub + + } } diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxOutputDevice.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxOutputDevice.java index 64d33d3d1..f62ba78f5 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxOutputDevice.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxOutputDevice.java @@ -35,6 +35,7 @@ import com.openhtmltopdf.render.*; import com.openhtmltopdf.util.Configuration; import com.openhtmltopdf.util.XRLog; + import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -58,6 +59,7 @@ import org.w3c.dom.Node; import javax.imageio.ImageIO; + import java.awt.*; import java.awt.RenderingHints.Key; import java.awt.geom.*; @@ -113,8 +115,23 @@ public class PdfBoxOutputDevice extends AbstractOutputDevice implements OutputDe // TODO: Make this work. private AffineTransform _currentTransform = new AffineTransform(); + + // A stack of currently in force transforms on the PDF graphics state. + // NOTE: Transforms are cumulative and order is important. + // After the graphics state is restored in setClip we must appropriately reapply the transforms + // that should be in effect. private final Deque transformStack = new ArrayDeque(); + // An index into the transformStack. When we save state we set this to the length of transformStack + // then we know we have to reapply those transforms set after saving state upon restoring state. + private int clipTransformIndex; + + // We use these to know how far (in PDF points) we have been translated. + // We are translated to implement page margins. + // We need these values for calculating transform-origins. + private double _translateX = 0; + private double _translateY = 0; + // The desired color as set by setColor. // To make sure this color is set on the PDF graphics stream call ensureFillColor or ensureStrokeColor. private FSColor _color = FSRGBColor.BLACK; @@ -233,6 +250,8 @@ public void initializePage(PDPageContentStream currentPage, PDPage page, float h _transform = new AffineTransform(); _transform.scale(1.0d / _dotsPerPoint, 1.0d / _dotsPerPoint); + _translateX = 0; + _translateY = 0; _stroke = transformStroke(STROKE_ONE); _originalStroke = _stroke; @@ -443,6 +462,8 @@ public void fillOval(int x, int y, int width, int height) { public void translate(double tx, double ty) { _transform.translate(tx, ty); + _translateX += tx; + _translateY += ty; } public Object getRenderingHint(Key key) { @@ -858,8 +879,19 @@ public Shape getClip() { } public void setClip(Shape s) { + // Restore graphics to get back to a no-clip situation. _cp.restoreGraphics(); + + // Reapply the transforms that are in effect. + reapplyTransforms(); + + // Save graphics so we can do this again. _cp.saveGraphics(); + + // Set the index so we know which transforms have to be reapplied + // when we next restore graphics. + clipTransformIndex = transformStack.size(); + if (s != null) s = _transform.createTransformedShape(s); if (s == null) { @@ -1348,14 +1380,75 @@ public void setRenderingContext(RenderingContext result) { public void setBidiReorderer(BidiReorderer reorderer) { _reorderer = reorderer; } + + @Override + public void popTransform() { + AffineTransform transform = transformStack.pop(); + System.out.println("popped transform: " + transform.toString()); + try { + AffineTransform inverse = transform.createInverse(); + _cp.setPdfMatrix(inverse); + } catch (NoninvertibleTransformException e) { + // Shouldn't happen. + } + } + + @Override + public void pushTransform(AffineTransform transform) { + try { + transform.createInverse(); + transformStack.push(transform); + System.out.println("pushed transform: " + transform.toString()); + _cp.setPdfMatrix(transform); + } catch (NoninvertibleTransformException e) { + XRLog.render(Level.WARNING, "Tried to set a non-invertible CSS transform. Ignored."); + } + } + + private void reapplyTransforms() { + int idx = 0; + + for (Iterator iter = transformStack.descendingIterator(); iter.hasNext(); ) { + AffineTransform transform = iter.next(); + if (idx >= clipTransformIndex) { + System.out.println("reapplied transform: " + transform.toString()); + _cp.setPdfMatrix(transform); + } + idx++; + } + } + + @Override + public AffineTransform translateTransform(float tx, float ty) { + float y = normalizeY((ty / _dotsPerPoint) + (float) _translateY / _dotsPerPoint); + float x = (tx / _dotsPerPoint) + (float) _translateX / _dotsPerPoint; + AffineTransform transform = AffineTransform.getTranslateInstance(x, y); + _cp.setPdfMatrix(transform); + return transform; + } + + @Override + public void translateTransform(AffineTransform token) { + try { + _cp.setPdfMatrix(token.createInverse()); + } catch (NoninvertibleTransformException e) { + // Shouldn't happen. + e.printStackTrace(); + } + } + + // The below methods are for the experimental SVG code and should not be used for other uses. + @Override + @Deprecated public void saveState() { _cp.saveGraphics(); - transformStack.push(_currentTransform); + } @Override + @Deprecated public void restoreState() { _cp.restoreGraphics(); _currentTransform = transformStack.pop(); @@ -1377,12 +1470,14 @@ public void setAlpha(int alpha) { } @Override + @Deprecated public void setRawClip(Shape s) { _clip = new Area(s); followPath(s, CLIP); } @Override + @Deprecated public void rawClip(Shape s) { if (_clip == null) _clip = new Area(s); @@ -1392,15 +1487,8 @@ public void rawClip(Shape s) { } @Override + @Deprecated public Shape getRawClip() { return _clip; } - - @Override - public void applyTransform(AffineTransform transform) { - AffineTransform calculatedTransform = new AffineTransform(transform); - _currentTransform.concatenate(transform); - calculatedTransform.concatenate(_currentTransform); - _cp.setPdfMatrix(calculatedTransform); - } }