node-mapnik
node-mapnik copied to clipboard
SVGs with percentages for `width` and `height` result in inconsistent output
If an SVG buffer with percentage-based width and height attributes is passed to mapnik.fromSVGBytes (or I assume any of the other fromSVG methods), the callback receives no error and a non-null image result.
However, the image result is inconsistent. If the SVG has width="100%" height="100%", we get back an image that, when encoded to PNG, is 1px x 1px.
If the SVG has non-100% values, like width="90%" height="90%", we get back an image, that when encoded to PNG, throws an error: Could not write to empty stream.
Desired behavior: Since it seems like percentage-based width and height values do not really produce a usable image in any case, I think the callback to fromSVGBytes should instead receive a error (maybe like "svg must have pixel-based width and height values") and null result.
Example test cases:
test('SVG with 100% width and height', t => {
const percent = '100%';
const svgPercentageWidthHeight = Buffer.from(
`<svg xmlns='http://www.w3.org/2000/svg' width='${percent}' height='${percent}'><rect fill='#f0f' width='24' height='24'/></svg>`
);
const options = {
max_size: 512,
scale: 1
};
mapnik.Image.fromSVGBytes(svgPercentageWidthHeight, options, (err, image) => {
// These two tests fail, but we would like to have have an error and not get an image
t.ok(err, 'should have an error');
t.notOk(image, 'should not have an image');
// The image when encoded to PNG produces a 1px x 1px image
const buffer = image.encodeSync('png');
fs.writeFileSync('./100percentage_image.png', buffer);
t.end();
});
});
test('SVG with other percentage width and height', t => {
const percent = '90%';
const svgPercentageWidthHeight = Buffer.from(
`<svg xmlns='http://www.w3.org/2000/svg' width='${percent}' height='${percent}'><rect fill='#f0f' width='24' height='24'/></svg>`
);
const options = {
max_size: 512,
scale: 1
};
mapnik.Image.fromSVGBytes(svgPercentageWidthHeight, options, (err, image) => {
// These two tests fail, but we would like to have have an error and not get an image
t.ok(err, 'should have an error');
t.notOk(image, 'should not have an image');
// The image when encoded to PNG throws an error:
// Error: Could not write to empty stream
const buffer = image.encodeSync('png');
fs.writeFileSync('./percentage_image.png', buffer);
t.end();
});
});
test('SVG with no width and height', t => {
const svgNoHeightWidth = Buffer.from(
"<svg xmlns='http://www.w3.org/2000/svg'><rect fill='#f0f' width='24' height='24'/></svg>"
);
const options = {
max_size: 512,
scale: 1
};
mapnik.Image.fromSVGBytes(svgNoHeightWidth, options, (err, image) => {
// These tests pass, as expected
t.ok(err, 'should have an error');
t.equal(
err.message,
'image created from svg must have a width and height greater then zero',
'has expected message'
);
t.notOk(image, 'should not have an image');
t.end();
});
});
cc @artemp
@jseppi - looks inconsistent indeed. I'll take a look, thanks.
@artemp what is the status of this ticket?
I'm working to address this issue as part of https://github.com/mapnik/mapnik/pull/4225
Throwing an exception when meaningful image dimensions can't be deduced (e.g width and height defined using % and no viewBox attribute) would be reasonable approach but I think we can do better. I'm planning to add "fallback" width and height as optional parameters. When specified these params will allow interpreting % values as relative to them. I'll post detailed explanation once implementation is in place, but from user point of view following SVG
<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'> <!-- NOTE: no "viewBox"-->
<rect fill='#f0f' width='100%' height='100%'/>
</svg>
and
mapnik.Image.fromSVGBytes(svg_bytes, {width:32, height:24} , (err, image) => {
}
will result in 32x24 image
/cc @springmeyer @jseppi
https://github.com/mapnik/mapnik/pull/4225/commits/654a3c1f9f03a36fc5237c79b4cc87f7d4638319
improved viewport logic [WIP]
/cc @jseppi @springmeyer
Initial implementation : https://github.com/mapnik/mapnik/tree/svg-viewport
svg2png --scale-factor= 4 --width=25 --height=20 // overwrite "width" and "height" outermost `<svg>` element
Resulting png is 100x80px. I'm still wondering if this logic should only apply to width and height expressed in % (?)
There is also interesting effects when SVG paths are using absolute coordinates..
/cc @springmeyer @jseppi