4

I'm trying to output this SVG to a pdf. The SVG is generated by mermaid.js

I found an existing answer on stack for converting SVG to PDF. This answer defined a function downloadPDF which converts SVG to PDF and triggers the PDF download.

When I run this function in the below minimal example, I get output, but the output looks all grayed-out. Can someone help me output the given SVG to PDF correctly?

Here is a minimal example:

<!DOCTYPE html>
<html lang="en" style="height: auto;">
<head>
</head>

<body>
    <div class="mermaid">
        graph TD;
        A-->B;
        A-->C;
        B-->D;
        C-->D;
        C[Action 1 </br> Can code preserve line breaks?];
    </div>
</body>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.13.4/mermaid.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/pdfkit@0.10.0/js/pdfkit.standalone.js"></script>
<script src="https://bundle.run/blob-stream@0.1.3"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-to-pdfkit@0.1.8/source.js"></script>

<script>
mermaid.init({
    flowchart: { useMaxWidth: false },
    "theme": "default",
    "themeVariables": {
        "fontFamily": "Helvetica"
    }
}, document.querySelectorAll(".mermaid"));
</script>

<script>
function downloadPDF(svg, outFileName) {
    let doc = new PDFDocument({compress: false});
    SVGtoPDF(doc, svg, 0, 0);   
    let stream = doc.pipe(blobStream());
    stream.on('finish', () => {
      let blob = stream.toBlob('application/pdf');
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = outFileName + ".pdf";
      link.click();
    });
    doc.end();
}
var svg = document.querySelector('svg');
</script>

To produce the PDF, paste this into the browser development console: downloadPDF(svg, "test")

Here is the svg image produced by minimal example:
Original SVG

Here is the image output from the downloadPDF function (run downloadPDF(svg, "test") in broswer development console):

pdf made from SVG

Note: I'm getting this same output with Firefox and Edge browsers.

Update: I edited the function downloadPDF and now I get the colors correctly, however, node text is still not displayed:

function downloadPDF(svg, outFileName) {
    let doc = new PDFDocument({compress: false});    
    SVGtoPDF(doc, svg, 0, 0, {useCSS:true});
    let stream = doc.pipe(blobStream());
    stream.on('finish', () => {
      let blob = stream.toBlob('application/pdf');
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = outFileName + ".pdf";
      link.click();
    });
    doc.end();
}

New pdf output without node text:

enter image description here

If anyone could help figure out the node text portion I would really appreciate it!

Update 2: So I noticed that if I change the foreignObject's in the svg nodes to <text> elements and remove the div, I get the text, but now the issue is the text is offset also I have no idea if this will preserve fonts.

Below I take each svg node and use replaceAll to modify the innerHTML of the nodes

for(i = 0; i< svg.getElementsByClassName("node").length; i++){
        svg.getElementsByClassName("node")[i].innerHTML = svg.getElementsByClassName("node")[i].innerHTML.replaceAll("<div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: inline-block; white-space: nowrap;\">", "").replaceAll("</div>", "").replaceAll("foreignObject", "text");
    }
    downloadPDF(svg, "test")

Update

It became really easy with browser print functions

I slightly modified this function which I found in this answer.

 var printSVG = function()
{
    var popUpAndPrint = function()
    {
        var container = $('.mermaid')[0];
        var width = Math.max(1000, parseFloat(svg.getAttribute("width")))
        var height = Math.max(1000, parseFloat(svg.getAttribute("height")))
        
        var printWindow = window.open('', 'PrintMap',
        'width=' + width + ',height=' + height);
        printWindow.document.writeln($(container).html());
        printWindow.document.close();
        printWindow.print();
        printWindow.close();
    };
    setTimeout(popUpAndPrint, 500);
};

Thanks to everyone for the help!

Frank
  • 952
  • 1
  • 9
  • 23

2 Answers2

2

I suspect there is some rendering bug in the print rendering module since the SVG call and Mermaid response appears perfect, plus native read and print SVG also looks good, and proper. So your initial code (with a small necessary @media line looks like this as a PDF.

enter image description here

Likewise the Evo sample is perfectly rendered using the same method but allowing in that case the media default of system paper e.g. 8.5x11(Letter)

"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --headless --run-all-compositor-stages-before-draw --print-to-pdf-no-header --print-to-pdf="C:\Users\WDAGUtilityAccount\desktop\svg-example.pdf" https://www.evopdf.com/DemoAppFiles/HTML_Files/SVG_Examples.html & timeout 5 & svg-example.pdf

enter image description here

To reduce the media size from Chrome's default I had to add in your <head>

<head>
<meta http-equiv="Content-Style-Type" content="text/css">
<style>@media print { @page { margin: 0; size: 125px 238px ; } body { margin: 0; } }</style>
</head>

and you will note due to a slight rounding error in the media maths the size needs to be slightly larger than the SVG viewBox="0 0 124.63749694824219 231.20001220703125" oddly on this occasion just round up width but 6 extra units in height !

Thus I suggest replace the final printout method to the more normal JS methods to use the browsers native print function, should be only a few lines more than my one line method.

Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
K J
  • 8,045
  • 3
  • 14
  • 36
1

Update:

You could enable <text> element labels via config set htmlLabels param to false:

var config = { 
    startOnLoad:true, 
    flowchart:{ 
        useMaxWidth:false, 
        htmlLabels:false
     } };
mermaid.initialize(config);

But you still need some sanitizing, since mermaid will add a xml:space property svg-to-pdf-kit doesn't like

Example: render <text> labels (codepen)

var config = {
  startOnLoad: true,
  flowchart: {
    useMaxWidth: false,
    htmlLabels: false
  }
};
mermaid.initialize(config);

//example font fira Sans
let fontUrl = 'https://fonts.gstatic.com/s/firasans/v16/va9E4kDNxMZdWfMOD5Vvl4jO.ttf';

async function downloadPDF(outFileName) {
  let svg = document.querySelector('.mermaid svg');

  // sanitize
  sanitizeSVG();

  // load font and register
  const font = await fetch(fontUrl)
  const arrayBuffer = await font.arrayBuffer()
  let doc = new PDFDocument({
    compress: false
  });
  doc.registerFont('Fira-Sans-Regular', arrayBuffer)
  doc.font('Fira-Sans-Regular')

  SVGtoPDF(doc, svg, 0, 0, {
    useCSS: true
  });
  let stream = doc.pipe(blobStream());
  stream.on('finish', () => {
    let blob = stream.toBlob('application/pdf');
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = outFileName + ".pdf";
    link.click();
  });
  doc.end();

}


function sanitizeSVG() {
  let svg = document.querySelector('.mermaid svg');
  let tspans = svg.querySelectorAll('tspan');
  tspans.forEach(function(tspan, i) {
    tspan.removeAttribute('xml:space');
  });
}
@font-face {
  font-family: 'Fira-Sans-Regular';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/firasans/v16/va9E4kDNxMZdWfMOD5Vvl4jO.ttf) format('truetype');
}

text {
  font-family: 'Fira-Sans-Regular';
  line-height: 18px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.13.4/mermaid.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfkit@0.10.0/js/pdfkit.standalone.js"></script>
<script src="https://bundle.run/blob-stream@0.1.3"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-to-pdfkit@0.1.8/source.js"></script>

<button type="button" onclick="downloadPDF( 'mermaid')">Download PDF</button>
<button type="button" onclick="sanitizeSVG()">sanitize</button>

<div class="mermaid">
  graph TD; A-->B; A-->C; B-->D; C-->D; C[Action 1 </br> Can code preserve line breaks?];
</div>

Converting <foreignObject> elements is certainly a good idea since svg-to-pdfkit doesn't support this element.

Unsupported

  • filters
  • text attributes: font-variant, writing-mode, unicode-bidi
  • vector-effect (#113)
  • foreignObject (#37)
  • other things I don't even know they exist

A workaround might be to use a webfont that could be registered like so:

    doc.registerFont('Fira-Sans-Regular', arrayBuffer)
    doc.font('Fira-Sans-Regular')

Regarding layout/vertical alignment you could add a dy attribute to your <text> replacement.

Example: Convert <foreignObject> (see codepen)

mermaid.init({
  flowchart: {
    useMaxWidth: false
  },
  "theme": "default",
  "themeVariables": {
    "fontFamily": "Fira-Sans-Regular",
  }

}, document.querySelectorAll(".mermaid"));


const svg = document.querySelector('.mermaid svg');


//example font fira Sans
let fontUrl = 'https://fonts.gstatic.com/s/firasans/v16/va9E4kDNxMZdWfMOD5Vvl4jO.ttf';

async function downloadPDF(svg, outFileName) {

  // convert foreignObjects
  convertForeignObjects(svg);

  // load font and register
  const font = await fetch(fontUrl)
  const arrayBuffer = await font.arrayBuffer()
  let doc = new PDFDocument({
    compress: false
  });
  doc.registerFont('Fira-Sans-Regular', arrayBuffer)
  doc.font('Fira-Sans-Regular')

  SVGtoPDF(doc, svg, 0, 0, {
    useCSS: true
  });
  let stream = doc.pipe(blobStream());
  stream.on('finish', () => {
    let blob = stream.toBlob('application/pdf');
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = outFileName + ".pdf";
    link.click();
  });
  doc.end();

}




function convertForeignObjects(svg) {
  //replace font-family in css
  svg.innerHTML = svg.innerHTML.replaceAll('"trebuchet ms"', 'Fira-Sans-Regular')
  let foreignObjects = svg.querySelectorAll('foreignObject');
  foreignObjects.forEach(function(el, i) {
    let text = el.querySelector('div');

    //split newlines
    let contentHTML = text.innerHTML;
    contentHTML = contentHTML.replaceAll('</br>', ' || ').replaceAll('<br>', ' || ');
    text.innerHTML = contentHTML;
    let content = text.textContent;
    let contentLines = content.split(' || ');

    let newTextEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    newTextEl.setAttribute('x', '0');
    newTextEl.setAttribute('y', '-0.2em');
    newTextEl.setAttribute('style', 'font-family:"Fira-Sans-Regular"!important;');

    if(contentLines.length){
        contentLines.forEach(function(line){
            let newLine = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
            newLine.textContent = line;
            newLine.setAttribute('x', '0');
            newLine.setAttribute('dy', '1.2em');
            newTextEl.appendChild(newLine);
        });
    }

    el.parentNode.appendChild(newTextEl);
    el.remove();
  })
}
@font-face {
  font-family: 'Fira-Sans-Regular';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/firasans/v16/va9E4kDNxMZdWfMOD5Vvl4jO.ttf) format('truetype');
}

text {
  font-family: 'Fira-Sans-Regular';
  line-height: 18px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.13.4/mermaid.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfkit@0.10.0/js/pdfkit.standalone.js"></script>
<script src="https://bundle.run/blob-stream@0.1.3"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-to-pdfkit@0.1.8/source.js"></script>

<button type="button" onclick="downloadPDF(svg, 'mermaid')">Convert and download PDF</button>
<button type="button" onclick="convertForeignObjects(svg)">Convert foreign objects</button>

<div class="mermaid">
        graph TD;
        A-->B;
        A-->C;
        B-->D;
        C-->D;
        C[Action 1 <br> Can code preserve line breaks?];
 </div>

Note the hyphenated notation of font-family names.

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
  • I really like your answer. Although there is an issue, the code doesn't preserve line breaks. I edited the graph in my original post with an added line ```C[Action 1 Can code preserve line breaks?]```. I'm working on the issue today, if I find a solution I'll update – Frank May 03 '22 at 12:36
  • @Frank: using `` elements by default will split your lines into `` – but box sizing is rather quirky for long texts. 2nd approach (converting foreignObject before pdf generating) will work with some additional sanitizinh (see updated snippet). – herrstrietzel May 03 '22 at 14:37