0

I have a svg graph that is generated using the d3 library and styled using css. I would like to convert this graph to a PDF while maintaining its CSS properties.

I have tried various different approaches but none seem to capture the svg with its style attributes. Currently, I'm using html2canvas and jsPDF for creating a canvas and saving it as a PDF. I have included the code for a minimal reproducible example of the styled graph + the code I currently have to generate a PDF.

<script src="https://d3js.org/d3.v5.js"></script>

<!-- jQuery CDN -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript">
</script>

<!-- html2canvas CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js" type="text/javascript">

<!-- jsPDF CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js"
    integrity="sha384-NaWTHo/8YCBYJ59830LTz/P4aQZK1sS0SneOgAvhsIl3zBu8r9RevNg5lHCHAuQ/" crossorigin="anonymous">
</script>

<script>
    function genPDF() {
        // gets the quality based on the value that is input in the field
        if (quality = document.getElementById("quality").value) {
            // window.devicePixelRatio = 2; 
            html2canvas(document.getElementById("svg1"), {
                // scale based on quality value input
                scale: quality,
                // logging for debugging
                logging: true,
                letterRendering: 1,
                // allows for cross origin images
                allowTaint: true,
                useCORS: true
            }).then(function (canvas) {
                // creating a canvas with page data 
                var img = canvas.toDataURL('image/png');
                // creating a portrait a4 page
                var doc = new jsPDF('l', 'mm', 'a4');
                // setting the width of the page to auto
                const imgProps = doc.getImageProperties(img);
                const pdfWidth = doc.internal.pageSize.getWidth();
                const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
                // adding the image canvas to the pdf and saving
                doc.addImage(img, 'PNG', 2, 2, pdfWidth, pdfHeight);
                doc.save('generatedPDF.pdf');
            });
        } else {
            // throw if no scale value is entered
            throw "Please enter a scale value!";
        }
    }
</script>
<style>

    rect {
        fill: #868e96;
        opacity: 0.3;
    }
</style>

<body>
<form>
    Enter scale quality:<br>
    <input type="text" id="quality" name="quality" placeholder="1 - 5">
</form>
<button onclick="genPDF()">Generate PDF</button>
    <svg id="svg1" width="1000" height="700">
        <g id="elementsContainer">

            <rect x="25" y="25" width="240" height="240" />
            <rect x="275" y="25" width="240" height="240" />
            <rect x="25" y="275" width="240" height="240" />
            <rect x="275" y="275" width="240" height="240" />
   </g>
</svg>
</body>

I just get a black non-styled graph.

Edit: I tried adding a timeout function which still does not seem to solve the issue:

function genPDF() {
        // gets the quality based on the value that is input in the field
        if (quality = document.getElementById("quality").value) {
            // window.devicePixelRatio = 2; 
            html2canvas(document.getElementById("apply"), {
                // scale based on quality value input
                scale: quality,
                // logging for debugging
                logging: true,
                letterRendering: 1,
                // allows for cross origin images
                allowTaint: true,
                useCORS: true
            }).then(function (canvas) {
                setTimeout(function () {
                    var img = canvas.toDataURL('image/png');
                    var doc = new jsPDF('l', 'mm', 'a4');
                    const imgProps = doc.getImageProperties(img);
                    const pdfWidth = doc.internal.pageSize.getWidth();
                    const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
                    doc.addImage(img, 'PNG', 2, 2, pdfWidth, pdfHeight);
                    doc.save('generatedPDF.pdf');
                }, 3000);
            });
        } else {
            // throw if no scale value is entered
            throw "Please enter a scale value!";
        }
    }

Edit: The solution that I found works the best is just to simply add the style properties inline to the SVG tags.

2 Answers2

0

You may want to consider a server-side solution. cairosvg (https://cairosvg.org/) sounds like it should be able to handle it. It does support css styling of svg. While I get trying to avoid server-side solutions, it is common to have server endpoints for handling image formatting, conversion and printing. This also has the advantage that it doesn't require much of the browser so you shouldn't run into browser compatibility or memory usage issues.

0

You need to add the CSS styles on the SVG container:

SVG to Canvas with d3.js

    function getGraphImage(container) {
        addCssStyles(container);
        var svgChart = d3.select("#" + container + " svg");
        svgChart.attr('width', svgChart.node().clientWidth);
        svgChart.attr('height', svgChart.node().clientHeight);

        //Get svg markup as string
        var svg = document.getElementById(container).innerHTML;

        if (svg)
            svg = svg.replace(/\r?\n|\r/g, '').trim();

        var canvas = document.createElement('canvas');
        var context = canvas.getContext('2d');

        context.clearRect(0, 0, canvas.width, canvas.height);
        canvg(canvas, svg);

        return canvas.toDataURL('image/png');
    }

    function addCssStyles(container) {
        // get styles from all required stylesheets
        // http://www.coffeegnome.net/converting-svg-to-png-with-canvg/
        // https://stackoverflow.com/questions/11567668/svg-to-canvas-with-d3-js
        var style = "\n";
        var requiredSheets = ['nv.d3.css']; // list of required CSS
        for (var i = 0; i < document.styleSheets.length; i++) {
            var sheet = document.styleSheets[i];
            if (sheet.href) {
                var sheetName = sheet.href.split('/').pop();
                if (requiredSheets.indexOf(sheetName) !== -1) {
                    var rules = sheet.rules;
                    if (rules) {
                        for (var j = 0; j < rules.length; j++) {
                            style += (rules[j].cssText + '\n');
                        }
                    }
                }
            }
        }

        d3.select("#" + container + " svg")
                .insert('defs', ":first-child");
        d3.select("#" + container + " svg defs")
                .append('style')
                .attr('type', 'text/css')
                .html(style);
    }

then you just add the image using addImage():

    var doc = new jsPDF('p', 'mm', 'letter');
    doc.addImage(getGraphImage(containerID), 'PNG', x, y, w, h);
    doc.output('dataurlnewwindow');
Chico3001
  • 1,853
  • 1
  • 22
  • 43