zenozeng / svgcanvas

Draw on SVG using Canvas's 2D Context API
MIT License
66 stars 19 forks source link

Canvas calls that include NaN parameters aren't treated properly #33

Open mgirolet-gl opened 8 months ago

mgirolet-gl commented 8 months ago

Situation

Sometimes (most often mistakenly), CanvasRenderingContext2D API functions may be called with NaN passed as one or more of the parameters.

Although it is not mentionned in the MDN documentation as far as I could find, it turns out that calls to such functions with NaN as one of the parameters are simply ignored with no errors returned.

As such, the following code here will have three of its lines completely ignored:

let canvas = document.getElementById('test');
let ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(NaN, 70); // Gets ignored
ctx.lineTo(70, NaN); // Gets ignored
ctx.lineTo(NaN, NaN); // Gets ignored
ctx.lineTo(100, 100);
ctx.stroke();

See for yourself: https://jsfiddle.net/d0bmx5nt/1/

The problem

It however seems that svgcanvas does not acknowledge this, and therefore encodes the NaN values into the exported svg.

Therefore, the following code:

import svgcanvas from 'https://cdn.jsdelivr.net/npm/svgcanvas@2.5.0/+esm'

let ctx = new svgcanvas.Context(250, 250);

ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(NaN, 70); // Gets ignored
ctx.lineTo(70, NaN); // Gets ignored
ctx.lineTo(NaN, NaN); // Gets ignored
ctx.lineTo(100, 100);
ctx.stroke();

saveAs(new Blob([ctx.getSerializedSvg()], { type: 'image/svg+xml' }), 'test.svg');

Exports the following SVG file:

<svg version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500">
    <defs/>
    <g>
        <path fill="none" stroke="#000000" paint-order="fill stroke markers" d=" M 50 50 L NaN NaN L NaN NaN L NaN NaN L 100 100" stroke-miterlimit="10" stroke-dasharray=""/>
    </g>
</svg>

See for yourself: https://jsfiddle.net/pcrn6471/1/

While some browsers might correctly ignore the commands that involve those NaN values, it is ultimately up to the software you are opening it with to decide what to do with it.

Inkscape, for instance, considers those NaNs as 0s, while Firefox seems to sometimes ignore them, or sometimes invalidate the whole path.

Practical use case

You might think that if there are NaNs in paths, then it's up to the programmer who has put them to fix it.

However, in practice, this very likely won't ever happen.

The reason I met this problem in the first place is when trying to export charts rendered by Chart.JS as SVG files. And even though Chart.JS is a 63.2k stars JS module, there are some draw calls that end up passing NaN to CanvasRenderingContext2D functions. And while yes I suppose an issue could be opened there, why would they bother fixing something that does not trigger any error in the first place nor cause any problem to any Chart.JS user and only affects one module that intercepts function calls that weren't meant to be intercepted in the first place?

Therefore, I think it would be more reasonable to implement a fix for this within svgcanvas, as it would make it more accurate to browsers' behaviour and would avoid opening issues for every module that uses canvas but has such calls happening in the background.

Workaround

In the meantime of this issue being resolved, you can work around the problem with one of these solutions: