Home > Back-end >  Strange output when converting SVG to PDF javascript mermaid.js example
Strange output when converting SVG to PDF javascript mermaid.js example

Time:05-01

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

I found an existing 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")

CodePudding user response:

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.

CodePudding user response:

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/[email protected]/js/pdfkit.standalone.js"></script>
    <script src="https://bundle.run/[email protected]"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/source.js"></script> 
   
   <button type="button" onclick="downloadPDF('mermaid.pdf')">Download PDF</button>
    <button type="button" onclick="sanitizeSVG()">sanitize</button>

    <div >
        graph TD;
        A-->g;
        g-->l;
        B-->D;
        C-->D;
    </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: download pdf not working (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(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');
    let content = text.textContent;
    let newTextEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    newTextEl.textContent = content;
    newTextEl.setAttribute('x', '0');
    newTextEl.setAttribute('y', '0');
    newTextEl.setAttribute('dy', '15');
    //newTextEl.setAttribute('dominant-baseline', 'middle');
    newTextEl.setAttribute('style', 'font-family:"Fira-Sans-Regular"!important;');
    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/[email protected]/js/pdfkit.standalone.js"></script>
<script src="https://bundle.run/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/source.js"></script>


<button type="button" onclick="downloadPDF(svg, 'mermaid.pdf')">Convert and download PDF</button>
<button type="button" onclick="convertForeignObjects(svg)">Convert foreign objects</button>
<div >
  graph TD; A-->g; g-->l; B-->D; C-->D;
</div>

Note the hyphenated notation of font-family names.

  • Related