将 SVG 转换为 PDF 时出现奇怪的输出 javascript mermaid.js 示例

Strange output when converting SVG to PDF javascript mermaid.js example

我正在尝试将此 SVG 输出为 pdf。 SVG 由 mermaid.js

生成

我在堆栈上找到了一个现有的 answer 用于将 SVG 转换为 PDF。这个答案定义了一个函数 downloadPDF 将 SVG 转换为 PDF 并触发 PDF 下载。

当我在下面的最小示例中 运行 这个函数时,我得到了输出,但输出看起来全是灰色的。谁能帮我把给定的 SVG 正确输出为 PDF?

这是一个最小的例子:

<!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>

要生成 PDF,请将其粘贴到浏览器开发控制台中:downloadPDF(svg, "test")

这是最小示例生成的 svg 图像:

这里是downloadPDF函数的图像输出(运行downloadPDF(svg, "test")在broswer开发控制台):

注意:我在 Firefox 和 Edge 浏览器中得到了同样的输出。

更新:我编辑了函数 downloadPDF,现在我得到了正确的颜色,但是,节点文本仍然没有显示:

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();
}

没有节点文本的新 pdf 输出:

如果有人能帮助找出节点文本部分,我将不胜感激!

更新 2: 所以我注意到,如果我将 svg 节点中的 foreignObject 更改为 <text> 元素并删除 div,我会得到文本,但现在的问题是文本被偏移了我也不知道这是否将保留字体。

下面我取每个svg节点,用replaceAll修改节点的innerHTML

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")

更新

浏览器打印功能变得非常简单

我稍微修改了我在 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);
};

感谢大家的帮助!

我怀疑打印渲染模块中存在一些渲染错误,因为 SVG 调用和美人鱼响应看起来很完美,而且本机读取和打印 SVG 看起来也不错,而且合适。所以你的初始代码(带有一小段必要的@media 行看起来像这样的 PDF。

同样,Evo 示例使用相同的方法完美呈现,但在这种情况下允许系统纸张的媒体默认值,例如8.5x11(信纸)

"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

为了从 Chrome 的默认设置中减小媒体大小,我必须在您的 <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>

并且您会注意到,由于媒体数学中存在轻微的舍入误差,因此尺寸需要略大于 SVG viewBox="0 0 124.63749694824219 231.20001220703125" 奇怪的是,在这种情况下只是将宽度四舍五入,但 6 个额外的单位身高!

因此我建议将最终的打印输出方法替换为更正常的JS方法以使用浏览器原生打印功能,应该只比我的一行方法多几行。

更新:

您可以通过配置将 htmlLabels 参数设置为 false 来启用 <text> 元素标签:

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

但你仍然需要一些消毒,因为美人鱼会添加一个 xml:space 属性 svg-to-pdf-kit 不喜欢

示例:渲染 <text> 标签 (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>

转换 <foreignObject> 元素当然是个好主意,因为 svg-to-pdfkit 不支持此元素。

Unsupported

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

解决方法可能是使用可以像这样注册的网络字体:

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

关于 layout/vertical 对齐,您可以将 dy 属性添加到 <text> 替换。

示例:转换 <foreignObject>(参见 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>

注意 font-family 个名称的连字符符号。