Skip to content

Commit

Permalink
#350 #358 Implement correct non-css sizing for SVG images.
Browse files Browse the repository at this point in the history
We can now use unit values such as mm in the width/height attribute of SVG images. With tests.
  • Loading branch information
danfickle committed Jun 13, 2019
1 parent 965e7d4 commit d9cdd6a
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,21 @@ private void createReplaced(LayoutContext c) {
int cssWidth = getCSSWidth(c);
int cssHeight = getCSSHeight(c);

// Since the interface doesn't allow us to pass min-width/height
// we implement it here.
int minWidth = getCSSMinWidth(c);
int minHeight = getCSSMinHeight(c);

if (minWidth > cssWidth &&
minWidth > 0) {
cssWidth = minWidth;
}

if (minHeight > cssHeight &&
minHeight > 0) {
cssHeight = minHeight;
}

re = c.getReplacedElementFactory().createReplacedElement(
c, this, c.getUac(), cssWidth, cssHeight);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@
*/
package com.openhtmltopdf.simple.extend;

import java.awt.Point;
import java.util.logging.Level;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.openhtmltopdf.util.XRLog;


/**
* Handles xhtml documents, including presentational html attributes (see css 2.1 spec, 6.4.4).
Expand All @@ -30,6 +35,8 @@
* @author Torbjoern Gannholm
*/
public class XhtmlNamespaceHandler extends XhtmlCssOnlyNamespaceHandler {
private static final String DEFAULT_SVG_DIMS = "";

/**
* {@inheritDoc}
*/
Expand All @@ -53,25 +60,86 @@ public String getImageSourceURI(Element e) {
}

public String getNonCssStyling(Element e) {
if (e.getNodeName().equals("table")) {
return applyTableStyles(e);
} else if (e.getNodeName().equals("td") || e.getNodeName().equals("th")) {
switch(e.getNodeName()) {
case "table":
return applyTableStyles(e);
case "td": /* FALL-THRU */
case "th":
return applyTableCellStyles(e);
} else if (e.getNodeName().equals("tr")) {
case "tr":
return applyTableRowStyles(e);
} else if (e.getNodeName().equals("img")) {
case "img":
return applyImgStyles(e);
} else if (e.getNodeName().equals("p") || e.getNodeName().equals("div")) {
case "p": /* FALL-THRU */
case "div":
return applyBlockAlign(e);
} else if (e.getNodeName().equals("textarea")) {
return applyTextareaStyles(e);
} else if (e.getNodeName().equals("input")) {
return applyInputStyles(e);
case "textarea":
return applyTextareaStyles(e);
case "input":
return applyInputStyles(e);
case "svg":
return applySvgStyles(e);
}

return "";
}

private String applySvgStyles(Element e) {
String w = e.getAttribute("width");
String h = e.getAttribute("height");

if (!w.isEmpty() || !h.isEmpty()) {
StringBuilder sb = new StringBuilder();

if (!w.isEmpty()) {
sb.append("width: ");
sb.append(w);
if (isInteger(w)) {
sb.append("px");
}
sb.append(';');
}

if (!h.isEmpty()) {
sb.append("height: ");
sb.append(h);
if (isInteger(h)) {
sb.append("px");
}
sb.append(';');
}

return sb.toString();
}

String viewBoxAttr = e.getAttribute("viewBox");
String[] splitViewBox = viewBoxAttr.split("\\s+");

if (splitViewBox.length != 4) {
return DEFAULT_SVG_DIMS;
}
try {
int viewBoxWidth = Integer.parseInt(splitViewBox[2]);
int viewBoxHeight = Integer.parseInt(splitViewBox[3]);

StringBuilder sb = new StringBuilder();

sb.append("width: ");
sb.append(viewBoxWidth);
sb.append("px;");

sb.append("height: ");
sb.append(viewBoxHeight);
sb.append("px;");
} catch (NumberFormatException ex) {
XRLog.general(Level.WARNING,
"Invalid integer passed in viewBox attribute for SVG: " + viewBoxAttr);
/* FALL-THRU */
}

return DEFAULT_SVG_DIMS;
}

private String applyInputStyles(Element e) {
StringBuilder sb = new StringBuilder();

Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<html>
<head>
<style>
@page {
size: 500px 1500px;
margin: 0;
}
body {
margin: 0;
}
svg {
display: block;
border: 2px solid red;
margin: 2px;
}
</style>
</head>
<body>
<!-- width and height attributes -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 100" width="100" height="100">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>

<!-- width and height with units -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 100" width="4cm" height="4cm">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>

<!-- fallback to viewBox width and height -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 100">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>

<!-- with no width/height or viewBox fall back to default of 400px square -->
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>

<!-- unless a css width/height is provided -->
<svg xmlns="http://www.w3.org/2000/svg" style="width: 200px; height: 250px;">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>

<!-- just a css height -->
<svg xmlns="http://www.w3.org/2000/svg" style="height: 50px;">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>

<!-- css properties should take precedence over width/height attributes -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 100" width="40" height="40" style="width: 100px; min-width: 200px;">
<rect width="150" height="100" style="fill:rgb(0,0,255);" rx="15" />
</svg>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,16 @@ public void testReplacedSizingSvg() throws IOException {
assertTrue(vt.runTest("replaced-sizing-svg", WITH_SVG));
}

/**
* Tests that non-css sizing for SVG works. For example width/height
* attributes or if not present the last two values of viewBox attribute.
* Finally, if neither is present, it should default to 400px x 400px.
*/
@Test
public void testReplacedSizingSvgNonCss() throws IOException {
assertTrue(vt.runTest("replaced-sizing-svg-non-css", WITH_SVG));
}

// TODO:
// + Elements that appear just on generated overflow pages.
// + content property (page counters, etc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import com.openhtmltopdf.util.XRLog;

public class BatikSVGImage implements SVGImage {
private final static Point DEFAULT_DIMENSIONS = new Point(400, 400);
private final static int DEFAULT_SVG_WIDTH = 400;
private final static int DEFAULT_SVG_HEIGHT = 400;
private final static Point DEFAULT_DIMENSIONS = new Point(DEFAULT_SVG_WIDTH, DEFAULT_SVG_HEIGHT);

private final Element svgElement;
private final double dotsPerPixel;
Expand Down Expand Up @@ -56,18 +58,31 @@ public BatikSVGImage(Element svgElement, Box box, double cssWidth, double cssHei
}

Point dimensions = parseDimensions(svgElement);
double w;
double h;

if (dimensions == DEFAULT_DIMENSIONS &&
cssWidth >= 0 && cssHeight >= 0) {
svgElement.setAttribute("width", Integer.toString((int) (cssWidth / dotsPerPixel)));
svgElement.setAttribute("height", Integer.toString((int) (cssHeight / dotsPerPixel)));
this.pdfTranscoder.setImageSize((float) (cssWidth / dotsPerPixel), (float) (cssHeight / dotsPerPixel));
if (dimensions == DEFAULT_DIMENSIONS) {
if (cssWidth >= 0 && cssHeight >= 0) {
w = (cssWidth / dotsPerPixel);
h = (cssHeight / dotsPerPixel);
} else if (cssWidth >= 0) {
w = (cssWidth / dotsPerPixel);
h = DEFAULT_SVG_HEIGHT;
} else if (cssHeight >= 0) {
w = DEFAULT_SVG_WIDTH;
h = (cssHeight / dotsPerPixel);
} else {
w = DEFAULT_SVG_WIDTH;
h = DEFAULT_SVG_HEIGHT;
}
} else {
svgElement.setAttribute("width", Integer.toString(dimensions.x));
svgElement.setAttribute("height", Integer.toString(dimensions.y));
this.pdfTranscoder.setImageSize((float) dimensions.x,
(float) dimensions.y);
w = dimensions.x;
h = dimensions.y;
}

svgElement.setAttribute("width", Integer.toString((int) w));
svgElement.setAttribute("height", Integer.toString((int) h));
this.pdfTranscoder.setImageSize((float) w, (float) h);
}

@Override
Expand All @@ -84,31 +99,7 @@ public void setFontResolver(OpenHtmlFontResolver fontResolver) {
this.fontResolver = fontResolver;
}

public Integer parseLength(String attrValue) {
// TODO read length with units and convert to dots.
// length ::= number (~"em" | ~"ex" | ~"px" | ~"in" | ~"cm" | ~"mm" |
// ~"pt" | ~"pc")?
try {
return Integer.valueOf(attrValue);
} catch (NumberFormatException e) {
XRLog.general(Level.WARNING,
"Invalid integer passed as dimension for SVG: "
+ attrValue);
return null;
}
}

public Point parseDimensions(Element e) {
String widthAttr = e.getAttribute("width");
Integer width = widthAttr.isEmpty() ? null : parseLength(widthAttr);

String heightAttr = e.getAttribute("height");
Integer height = heightAttr.isEmpty() ? null : parseLength(heightAttr);

if (width != null && height != null) {
return new Point(width, height);
}

String viewBoxAttr = e.getAttribute("viewBox");
String[] splitViewBox = viewBoxAttr.split("\\s+");
if (splitViewBox.length != 4) {
Expand All @@ -118,17 +109,7 @@ public Point parseDimensions(Element e) {
int viewBoxWidth = Integer.parseInt(splitViewBox[2]);
int viewBoxHeight = Integer.parseInt(splitViewBox[3]);

if (width == null && height == null) {
width = viewBoxWidth;
height = viewBoxHeight;
} else if (width == null) {
width = (int) Math.round(((double) height)
* ((double) viewBoxWidth) / ((double) viewBoxHeight));
} else if (height == null) {
height = (int) Math.round(((double) width)
* ((double) viewBoxHeight) / ((double) viewBoxWidth));
}
return new Point(width, height);
return new Point(viewBoxWidth, viewBoxHeight);
} catch (NumberFormatException ex) {
return DEFAULT_DIMENSIONS;
}
Expand Down

0 comments on commit d9cdd6a

Please sign in to comment.