Search code examples
javascripthtmlpdf-generationjspdfhtml2canvas

Is there a way to convert an SVG element to a PDF while maintaining its CSS properties?


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.


Solution

  • 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');