yWorks / svg2pdf.js

A javascript-only SVG to PDF conversion utility that runs in the browser. Brought to you by yWorks - the diagramming experts
MIT License
667 stars 104 forks source link

Unexpected Behavior when rendering the same Svg-image on two different Pdf-pages #166

Closed FHoffmannSopra closed 3 years ago

FHoffmannSopra commented 3 years ago

Hey guys,

I tried to render the same SVG image in my pdf with the svg2pdf tool on two different pages in the document.

For most of the Svg-files I have that works perfectly.

But for some Svg's I get the following behavior:

jsPdf-Error

Unfortunately I could not isolate the problem better, except for the fact that when I remove all xlink:href attributes it works fine. Since xlink:href is deprecated in the Svg-standard, I did also try href instead, but got no positive effects. Not to use href is not an option for me.

I have seen that the error is thrown by the jsPdf library, but I am not sure if its a problem with the preprocessing of svg2pdf or indeed a problem with jsPdf.

If I can provide more information just let me know.

The Svg's are created with InkScape.

I am using svg2pdf 2.0.0 and jsPdf 2.0.0.

Thanks for your help Frederic

HackbrettXXX commented 3 years ago

From this information alone I cannot reproduce the issue. I can guess that the exception might originate from here: https://github.com/MrRio/jsPDF/blob/d8bb3b39efcd129994f7a3b01b632164144ec43e/src/modules/annotations.js#L84-L88

However, this would mean that adding pages is completely broken in jsPDF, which it isn't. Do you call any other jsPDF methods besides the svg() call (e.g. link() or textWithLink())?

In order to help you we need to be able to reproduce the issue. Could you share a small example with two SVGs that cause the exception and the code that produces the PDF?

FHoffmannSopra commented 3 years ago

I can give you the library specific code and two example Svg's for now. I hope it helps

    /** renders multiPage Pdf
     * @param {Array} _canvasObjects all svgCanvasObjects that should be rendered on pdf
     * @param {String} _fileName name of output file
    * @memberof PdfController
    */
        function _renderMultiPagePdf(_canvasObjects, _fileName) {
            const canvas = _canvasObjects[0];
            _canvasObjects.shift();
            const width = canvas.width + 2*canvas.padding.horizontal;
            const height = canvas.height + 2*canvas.padding.vertical;
            const svgElement = document.getElementById(canvas.id);
            let doc;
            if (width > height) {doc = new jsPDF("1", "pt", [width, height]);} else {doc = new jsPDF("0", "pt", [width, height]);}
            doc
            .svg(svgElement, {
                x: 0 + canvas.padding.horizontal,
                y: 0 + canvas.padding.vertical,
                width: width,
                height: height,
            })
            .then(() => {
                const start = async () => {
                    await renderAllPagesBesidesFirst(_canvasObjects, doc);
                    doc.save(`${_fileName}`);
                    const div = document.getElementById("pdfExportWrapper");
                    while (div.firstChild) {
                        div.removeChild(div.firstChild);
                    }
                };
                start();
            });
            /** renders all remaining pages
         * @param {Array} _array remaining pages
         * @param {*} _doc  jsPdf document
        */
            async function renderAllPagesBesidesFirst(_array, _doc) {
                for (let index = 0; index < _array.length; index++) {
                    const canvas = _array[index];
                    const width = canvas.width + 2*canvas.padding.horizontal;
                    const height = canvas.height + 2*canvas.padding.vertical;
                    const svgElement = document.getElementById(canvas.id);
                    if (width > height) {
                        _doc.addPage([width, height, "1"]);
                    } else {
                        _doc.addPage([width, height], "0");
                    }
                    await _doc
                    .svg(svgElement, {
                        x: 0 + canvas.padding.horizontal,
                        y: 0 + canvas.padding.vertical,
                        width: width,
                        height: height,
                    });
                }
            }
        }

example2.zip

HackbrettXXX commented 3 years ago

Thanks for sharing your code with us. When I run it with the two SVGs, I get a different error:

Uncaught (in promise) TypeError: Cannot read property 'offset' of undefined

That's the same bug you reported in #165. However, I could not reproduce your exception.

FHoffmannSopra commented 3 years ago

Thanks for your research first of all. You are absolutely right I am fixing the gradients without stops dynamically and forgot to also send that workaround.

Back to the topic of this thread:

I did analyze my Svg's in detail and detected that the def-Block is quite messy (around 2000 lines of code). I tried to localize this, which also include a huge amount of href-Statements and it turned out that a specific part of the Svg causes the majority of statements in there. I guess that is caused by copy and pasting things in InkScape multiple times (maybe 50 times or sth like that). When I removed that specific parts and transfered the rest to a clean file, I could not reproduce the error.

So I encourage everyone that detects such cross page errors to clean up their Svg files, in case their def-Block seems too heavy for the things they have drawn.

I will let you know if I can detect what exactly crashes the library.

FHoffmannSopra commented 3 years ago

Hey guys,

After more research I found a case for which I could isolate the error better. I also fixed the gradients for you, so testing should be possible now.

The two svgs I will attach are just multiple gradient colored lines. They differ in these aspects:

1) pathObjects.svg

2) useObjects.svg

Examples.zip

Thanks for your help Frederic

FHoffmannSopra commented 3 years ago
  • but not appearing on the PDF, which is most likely caused by another problem or an unsupported feature

I tested also with non gradient colored lines, the lines are rendered correctly

HackbrettXXX commented 3 years ago

Thanks for narrowing it down. The lines don't appear in the PDF because gradients are not supported on strokes.

I could also reproduce the PubSub error now. There are two bugs, one in jsPDF and one in svg2pdf:

jsPDF

https://github.com/MrRio/jsPDF/blob/e988a043bc6a26957798ce48e6f12e0b96b8b803/src/jspdf.js#L5690-L5707

  var endFormObject = function(key) {
    // only add it if it is not already present (the keys provided by the user must be unique!)
    if (renderTargetMap[key]) return;

    // save the created xObject
    var newXObject = new RenderTarget();

    var xObjectId = "Xo" + (Object.keys(renderTargets).length + 1).toString(10);
    newXObject.id = xObjectId;

    renderTargetMap[key] = xObjectId;
    renderTargets[xObjectId] = newXObject;

    events.publish("addFormObject", newXObject);

    // restore state from stack
    renderTargetStack.pop().restore();
  };

The early exit leaves jsPDF in a bad state, resulting in the exception when writing the pages. The early exit is missing the renderTargetStack.pop().restore(); line. I've created an issue for that: https://github.com/MrRio/jsPDF/issues/3127

svg2pdf

We are running into this case, because we render the same SVG twice, resulting in the path with id path853 drawn twice. The second time will not be written to PDF because of above early exit and there is already an form object with the given id. While in this particular case this actually benefits the file size, in general we should write all form objects from different SVGs even if they have the same id. I suggest we prefix the ids passed to jsPDF. Either keep a counter for the current svg2pdf invocation or generate a hash from the SVG. We can probably just extend this function:

https://github.com/yWorks/svg2pdf.js/blob/d6dd5f105df37a6a4593dcddca6d22216a1a570d/src/context/referenceshandler.ts#L36-L38