diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/extend/ReplacedElementScaleHelper.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/extend/ReplacedElementScaleHelper.java new file mode 100644 index 000000000..3cd490f3f --- /dev/null +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/extend/ReplacedElementScaleHelper.java @@ -0,0 +1,58 @@ +package com.openhtmltopdf.simple.extend; + +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; + +public class ReplacedElementScaleHelper { + /** + * Creates a scale AffineTransform to scale a given replaced element to the desired size. + * @param dotsPerPixel + * @param contentBounds the desired size + * @param width the intrinsic width + * @param height the intrinsic height + * @return AffineTransform or null if not available. + */ + public static AffineTransform createScaleTransform(double dotsPerPixel, Rectangle contentBounds, float width, float height) { + int intrinsicWidth = (int) width; + int intrinsicHeight = (int) height; + + int desiredWidth = (int) (contentBounds.width / dotsPerPixel); + int desiredHeight = (int) (contentBounds.height / dotsPerPixel); + + AffineTransform scale = null; + + if (width == 0 || height == 0) { + // Do nothing... + } + else if (desiredWidth > intrinsicWidth && + desiredHeight > intrinsicHeight) { + + double rw = (double) desiredWidth / width; + double rh = (double) desiredHeight / height; + + double factor = Math.min(rw, rh); + scale = AffineTransform.getScaleInstance(factor, factor); + } else if (desiredWidth < intrinsicWidth && + desiredHeight < intrinsicHeight) { + double rw = (double) desiredWidth / width; + double rh = (double) desiredHeight / height; + + double factor = Math.max(rw, rh); + scale = AffineTransform.getScaleInstance(factor, factor); + } + + return scale; + } + + public static AffineTransform inverseOrNull(AffineTransform in) { + if (in == null) { + return null; + } + try { + return in.createInverse(); + } catch (NoninvertibleTransformException e) { + return null; + } + } +} diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/replaced-sizing-mathml.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/replaced-sizing-mathml.pdf new file mode 100644 index 000000000..c8d54165f Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/replaced-sizing-mathml.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/replaced-sizing-mathml.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/replaced-sizing-mathml.html new file mode 100644 index 000000000..49a45fdb7 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/replaced-sizing-mathml.html @@ -0,0 +1,206 @@ + + + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + + + + + + a + + + b + + + 2 + + + + + + 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 306d4551f..c6ec4b968 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java @@ -8,6 +8,7 @@ import org.junit.Ignore; import org.junit.Test; +import com.openhtmltopdf.mathmlsupport.MathMLDrawer; import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; import com.openhtmltopdf.svgsupport.BatikSVGDrawer; import com.openhtmltopdf.visualtest.VisualTester; @@ -763,6 +764,16 @@ public void testReplacedSizingSvgNonCss() throws IOException { assertTrue(vt.runTest("replaced-sizing-svg-non-css", WITH_SVG)); } + /** + * Tests all the CSS sizing properties for MathML elements. + */ + @Test + public void testReplacedSizingMathMl() throws IOException { + assertTrue(vt.runTest("replaced-sizing-mathml", (builder) -> { + builder.useMathMLDrawer(new MathMLDrawer()); + })); + } + // TODO: // + Elements that appear just on generated overflow pages. // + content property (page counters, etc) diff --git a/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLDrawer.java b/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLDrawer.java index cd411f4b3..032b98b77 100644 --- a/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLDrawer.java +++ b/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLDrawer.java @@ -117,7 +117,7 @@ public SVGImage buildSVGImage(Element mathMlElement, Box box, CssContext c, doub double cssMaxHeight = CalculatedStyle.getCSSMaxHeight(c, box); List fontList = Arrays.asList(fonts); - MathMLImage img = new MathMLImage(mathMlElement, cssWidth, cssHeight, cssMaxWidth, cssMaxHeight, dotsPerPixel, fontList); + MathMLImage img = new MathMLImage(mathMlElement, box, cssWidth, cssHeight, cssMaxWidth, cssMaxHeight, dotsPerPixel, fontList); return img; } diff --git a/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLImage.java b/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLImage.java index 67e562b03..1eac2d1c4 100644 --- a/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLImage.java +++ b/openhtmltopdf-mathml-support/src/main/java/com/openhtmltopdf/mathmlsupport/MathMLImage.java @@ -1,6 +1,8 @@ package com.openhtmltopdf.mathmlsupport; import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; import java.util.List; import net.sourceforge.jeuclid.DOMBuilder; @@ -14,37 +16,25 @@ import com.openhtmltopdf.extend.OutputDevice; import com.openhtmltopdf.extend.OutputDeviceGraphicsDrawer; import com.openhtmltopdf.extend.SVGDrawer.SVGImage; +import com.openhtmltopdf.render.Box; import com.openhtmltopdf.render.RenderingContext; +import com.openhtmltopdf.simple.extend.ReplacedElementScaleHelper; public class MathMLImage implements SVGImage { private final JEuclidView _view; private final DocumentElement _mathDoc; private final double _dotsPerPixel; + private final Box _box; private final MathLayoutContext _context = new MathLayoutContext(); - private final double _scaledWidthInOutputDeviceDots; - private final double _scaledHeightInOutputDeviceDots; - private double _sx = 1; - private double _sy = 1; public static class MathLayoutContext extends LayoutContextImpl { private static final long serialVersionUID = 1; } - - private boolean haveValue(double val) { - return val >= 0; - } - - private double minIgnoringNoValues(double spec, double max) { - if (haveValue(spec) && haveValue(max)) { - return Math.min(spec, max); - } else { - return haveValue(spec) ? spec : max; - } - } - - public MathMLImage(Element mathMlElement, double cssWidth, + + public MathMLImage(Element mathMlElement, Box box, double cssWidth, double cssHeight, double cssMaxWidth, double cssMaxHeight, double dotsPerPixel, List fonts) { + this._box = box; this._dotsPerPixel = dotsPerPixel; this._mathDoc = DOMBuilder.getInstance().createJeuclidDom(mathMlElement); @@ -58,65 +48,6 @@ public MathMLImage(Element mathMlElement, double cssWidth, this._context.setParameter(Parameter.MATHSIZE, 16f); // TODO: Proper font size pickup from CSS. this._view = new JEuclidView(this._mathDoc, this._context, null); - - if (this.getViewWidthInOutputDeviceDots() <= 0 || this.getViewHeightInOutputDeviceDots() <= 0) { - this._scaledWidthInOutputDeviceDots = 0; - this._scaledHeightInOutputDeviceDots = 0; - return; - } - - boolean haveBoth = haveValue(cssWidth) && haveValue(cssHeight); - boolean haveNone = !haveValue(cssWidth) && !haveValue(cssHeight); - boolean haveOne = !haveBoth && !haveNone; - - double w = -1f; - double h = -1f; - - if (haveBoth) { - // Have both from CSS, use them constrained by max-xxx values, don't keep aspect ratio. - w = minIgnoringNoValues(cssWidth, cssMaxWidth); - h = minIgnoringNoValues(cssHeight, cssMaxHeight); - } else if (haveNone) { - // Use rendered view size with max-xxx constraints, keeping aspect ratio. - double prelimW = minIgnoringNoValues(this.getViewWidthInOutputDeviceDots(), cssMaxWidth); - double prelimH = minIgnoringNoValues(this.getViewHeightInOutputDeviceDots(), cssMaxHeight); - - double prelimScaleX = prelimW / this.getViewWidthInOutputDeviceDots(); - double prelimScaleY = prelimH / this.getViewHeightInOutputDeviceDots(); - - double scale = Math.min(prelimScaleX, prelimScaleY); - - w = scale * this.getViewWidthInOutputDeviceDots(); - h = scale * this.getViewHeightInOutputDeviceDots(); - } else if (haveOne) { - if (haveValue(cssWidth)) { - // Keep aspect ratio, if we have both a width and a conflicting max-height, the max-height wins out. - double prelimW = minIgnoringNoValues(cssWidth, cssMaxWidth); - double prelimScale = prelimW / this.getViewWidthInOutputDeviceDots(); - double prelimH = this.getViewHeightInOutputDeviceDots() * prelimScale; - - double scale = (haveValue(cssMaxHeight) && prelimH > cssMaxHeight) ? cssMaxHeight / this.getViewHeightInOutputDeviceDots() : prelimScale; - - w = scale * this.getViewWidthInOutputDeviceDots(); - h = scale * this.getViewHeightInOutputDeviceDots(); - } else if (haveValue(cssHeight)) { - // Keep aspect ratio, if we have both a height and a conflicting max-width, the max-width wins out. - double prelimH = minIgnoringNoValues(cssHeight, cssMaxHeight); - double prelimScale = prelimH / this.getViewHeightInOutputDeviceDots(); - double prelimW = this.getViewWidthInOutputDeviceDots() * prelimScale; - - double scale = (haveValue(cssMaxWidth) && prelimW > cssMaxWidth) ? cssMaxWidth / this.getViewWidthInOutputDeviceDots() : prelimScale; - - w = scale * this.getViewWidthInOutputDeviceDots(); - h = scale * this.getViewHeightInOutputDeviceDots(); - } - } - - _sx = w / this.getViewWidthInOutputDeviceDots(); - _sy = h / this.getViewHeightInOutputDeviceDots(); - - this._scaledWidthInOutputDeviceDots = w; - this._scaledHeightInOutputDeviceDots = h; } private double getViewWidthInOutputDeviceDots() { @@ -129,23 +60,36 @@ private double getViewHeightInOutputDeviceDots() { @Override public int getIntrinsicWidth() { - return (int) this._scaledWidthInOutputDeviceDots; + return (int) this.getViewWidthInOutputDeviceDots(); } @Override public int getIntrinsicHeight() { - return (int) this._scaledHeightInOutputDeviceDots; + return (int) this.getViewHeightInOutputDeviceDots(); } - @Override - public void drawSVG(OutputDevice outputDevice, RenderingContext ctx, - double x, double y) { - outputDevice.drawWithGraphics((float) x, (float) y, (float) this._scaledWidthInOutputDeviceDots, (float) this._scaledHeightInOutputDeviceDots, new OutputDeviceGraphicsDrawer() { - @Override - public void render(Graphics2D g2d) { - g2d.scale(_sx, _sy); - _view.draw(g2d, 0, _view.getAscentHeight()); - } + @Override + public void drawSVG(OutputDevice outputDevice, RenderingContext ctx, double x, double y) { + Rectangle contentBounds = _box.getContentAreaEdge(_box.getAbsX(), _box.getAbsY(), ctx); + + final AffineTransform scale2 = ReplacedElementScaleHelper.createScaleTransform(_dotsPerPixel, contentBounds, this._view.getWidth(), this._view.getAscentHeight() + this._view.getDescentHeight()); + final AffineTransform inverse2 = ReplacedElementScaleHelper.inverseOrNull(scale2); + final boolean transformed2 = scale2 != null && inverse2 != null; + + outputDevice.drawWithGraphics((float) x, (float) y, + (float) (contentBounds.width / _dotsPerPixel), + (float) (contentBounds.height / _dotsPerPixel), + new OutputDeviceGraphicsDrawer() { + @Override + public void render(Graphics2D g2d) { + if (transformed2) { + g2d.transform(scale2); + } + _view.draw(g2d, 0, _view.getAscentHeight()); + if (transformed2) { + g2d.transform(inverse2); + } + } }); } diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java index c638e8dd3..22ddb7bab 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java @@ -10,6 +10,7 @@ import com.openhtmltopdf.layout.SharedContext; import com.openhtmltopdf.render.Box; import com.openhtmltopdf.render.RenderingContext; +import com.openhtmltopdf.simple.extend.ReplacedElementScaleHelper; import com.openhtmltopdf.util.XRLog; import org.apache.batik.bridge.FontFace; import org.apache.batik.bridge.FontFamilyResolver; @@ -23,7 +24,6 @@ import java.awt.*; import java.awt.font.TextAttribute; import java.awt.geom.AffineTransform; -import java.awt.geom.NoninvertibleTransformException; import java.io.InputStream; import java.util.HashMap; import java.util.List; @@ -168,7 +168,7 @@ public void importFontFaces(List fontFaces, SharedContext ctx) { continue; } - byte[] font1 = ctx.getUac().getBinaryResource(src.asString()); + byte[] font1 = ctx.getUserAgentCallback().getBinaryResource(src.asString()); if (font1 == null) { XRLog.exception("Could not load font " + src.asString()); continue; @@ -208,50 +208,11 @@ protected void transcode(Document svg, String uri, TranscoderOutput out) throws this.userAgent = new OpenHtmlUserAgent(this.fontResolver); super.transcode(svg, uri, out); - int intrinsicWidth = (int) width; - int intrinsicHeight = (int) height; - Rectangle contentBounds = box.getContentAreaEdge(box.getAbsX(), box.getAbsY(), ctx); - - int desiredWidth = (int) (contentBounds.width / this.dotsPerPixel); - int desiredHeight = (int) (contentBounds.height / this.dotsPerPixel); - - boolean transformed = false; - AffineTransform scale = null; - - if (width == 0 || height == 0) { - // Do nothing... - } - else if (desiredWidth > intrinsicWidth && - desiredHeight > intrinsicHeight) { - - double rw = (double) desiredWidth / width; - double rh = (double) desiredHeight / height; - - double factor = Math.min(rw, rh); - scale = AffineTransform.getScaleInstance(factor, factor); - transformed = true; - } else if (desiredWidth < intrinsicWidth && - desiredHeight < intrinsicHeight) { - double rw = (double) desiredWidth / width; - double rh = (double) desiredHeight / height; - - double factor = Math.max(rw, rh); - scale = AffineTransform.getScaleInstance(factor, factor); - transformed = true; - } - - AffineTransform inverseScale = null; - try { - if (transformed) { - inverseScale = scale.createInverse(); - } - } catch (NoninvertibleTransformException e) { - transformed = false; - } - final AffineTransform scale2 = scale; - final AffineTransform inverse2 = inverseScale; - final boolean transformed2 = transformed; + + final AffineTransform scale2 = ReplacedElementScaleHelper.createScaleTransform(this.dotsPerPixel, contentBounds, width, height); + final AffineTransform inverse2 = ReplacedElementScaleHelper.inverseOrNull(scale2); + final boolean transformed2 = scale2 != null && inverse2 != null; outputDevice.drawWithGraphics( (float) x,