如何将画布转换为 PDF
你想将 Konva 阶段保存为 PDF 文件吗?
PDF 是一种复杂的格式。因此,我们需要使用一个外部库,如 jsPDF。
将画布保存为PDF的思路很简单:
- 生成画布内容
- 将画布导出为图像
- 将图像添加到使用PDF库创建的PDF文档中
- 保存PDF文件
我还有两个小提示:
-
借助于 高质量导出,你可以在将节点转换为图像时使用
pixelRatio
属性来提高 PDF 的质量。 -
可以使文本在 PDF 中可选择。即使我们将画布作为图像添加到 PDF 中,我们也可以手动插入文本。那并不简单,如果你有复杂样式可能会很困难。此外,PDF 上的文本渲染与
Konva
的文本渲染不同。但是我们可以尽量使之接近。为了演示,我们将在 PDF 文件中绘制“隐藏”的文本。文本将放置在图像下方,因此不会可见。但它仍然是可选择的。作为“复杂样式”的演示,我将模糊文本。
说明:查看画布。然后尝试将其保存为 PDF。
- Vanilla
- React
- Vue
import Konva from 'konva'; // 创建用于 PDF 导出的按钮 const saveButton = document.createElement('button'); saveButton.textContent = '保存为 PDF'; saveButton.style.position = 'absolute'; saveButton.style.top = '5px'; saveButton.style.left = '5px'; document.body.appendChild(saveButton); // 创建舞台 const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); // 添加背景 const back = new Konva.Rect({ width: stage.width(), height: stage.height(), fill: 'rgba(200, 200, 200)', }); layer.add(back); // 添加带模糊效 果的文本 const text = new Konva.Text({ text: '这是达斯·维达', x: 15, y: 40, rotation: -10, filters: [Konva.Filters.Blur], blurRadius: 4, fontSize: 18, }); text.cache(); layer.add(text); // 添加箭头 const arrow = new Konva.Arrow({ points: [70, 50, 100, 80, 150, 100, 190, 100], tension: 0.5, stroke: 'black', fill: 'black', }); layer.add(arrow); // 添加图像 const imageUrl = 'https://konvajs.org/assets/darth-vader.jpg'; Konva.Image.fromURL( imageUrl, function (darthNode) { darthNode.setAttrs({ x: 200, y: 50, scaleX: 0.5, scaleY: 0.5, }); layer.add(darthNode); }, function () { console.error('加载图像失败'); } ); // 处理 PDF 导出 saveButton.addEventListener('click', function () { // 我们需要检查 jsPDF 是否已加载 if (typeof jsPDF !== 'undefined') { const pdf = new jsPDF('l', 'px', [stage.width(), stage.height()]); pdf.setTextColor('#000000'); // 首先添加文本 stage.find('Text').forEach((text) => { const size = text.fontSize() / 0.75; // 将像素转换为点 pdf.setFontSize(size); pdf.text(text.text(), text.x(), text.y(), { baseline: 'top', angle: -text.getAbsoluteRotation(), }); }); // 然后在文本上绘制图像(使文本不可见) pdf.addImage( stage.toDataURL({ pixelRatio: 2 }), 0, 0, stage.width(), stage.height() ); pdf.save('canvas.pdf'); } else { console.error('jsPDF 库未加载。请将其包含在你的项目中。'); alert('jsPDF 库未加载。在真实项目中,你需要将其包含进来。'); } }); // 动态加载 jsPDF 库以供演示 const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js'; script.integrity = 'sha384-NaWTHo/8YCBYJ59830LTz/P4aQZK1sS0SneOgAvhsIl3zBu8r9RevNg5lHCHAuQ/'; script.crossOrigin = 'anonymous'; document.head.appendChild(script);
import { useRef, useEffect, useState } from 'react'; import { Stage, Layer, Rect, Text, Arrow, Image } from 'react-konva'; import useImage from 'use-image'; const App = () => { const stageRef = useRef(null); const [darthVaderImage] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const width = window.innerWidth; const height = window.innerHeight; // 动态加载 jsPDF 库 useEffect(() => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js'; script.integrity = 'sha384-NaWTHo/8YCBYJ59830LTz/P4aQZK1sS0SneOgAvhsIl3zBu8r9RevNg5lHCHAuQ/'; script.crossOrigin = 'anonymous'; document.head.appendChild(script); return () => { document.head.removeChild(script); }; }, []); // 处理 PDF 导出 const handleExport = () => { if (stageRef.current && typeof window.jsPDF !== 'undefined') { const stage = stageRef.current; const pdf = new window.jsPDF('l', 'px', [width, height]); pdf.setTextColor('#000000'); // 首先添加文本 stage.find('Text').forEach((text) => { const size = text.fontSize() / 0.75; // 将像素转换为点 pdf.setFontSize(size); pdf.text(text.text(), text.x(), text.y(), { baseline: 'top', angle: -text.getAbsoluteRotation(), }); }); // 然后在文本上绘制图像(使文本不可见) pdf.addImage( stage.toDataURL({ pixelRatio: 2 }), 0, 0, width, height ); pdf.save('canvas.pdf'); } else { console.error('jsPDF 库未加载或舞台不可用'); alert('jsPDF 库未加载。在真实项目中,你需要将其包含进来。'); } }; return ( <div style={{ position: 'relative' }}> <button style={{ position: 'absolute', top: '5px', left: '5px', zIndex: 10 }} onClick={handleExport} > 保存为 PDF </button> <Stage width={width} height={height} ref={stageRef}> <Layer> <Rect width={width} height={height} fill="rgba(200, 200, 200)" /> <Text text="这是达斯·维达" x={15} y={40} rotation={-10} fontSize={18} filters={[Konva.Filters.Blur]} blurRadius={4} /> <Arrow points={[70, 50, 100, 80, 150, 100, 190, 100]} tension={0.5} stroke="black" fill="black" /> {darthVaderImage && ( <Image image={darthVaderImage} x={200} y={50} scaleX={0.5} scaleY={0.5} /> )} </Layer> </Stage> </div> ); }; export default App;
<template> <div style="position: relative"> <button style="position: absolute; top: 5px; left: 5px; z-index: 10" @click="handleExport" > 保存为 PDF </button> <v-stage ref="stageRef" :config="stageConfig"> <v-layer> <v-rect :config="backgroundConfig" /> <v-text ref="textRef" :config="textConfig" /> <v-arrow :config="arrowConfig" /> <v-image v-if="darthVaderImage" :config="imageConfig" /> </v-layer> </v-stage> </div> </template> <script setup> import { ref, onMounted, onUnmounted, computed } from 'vue'; import Konva from 'konva'; import { useImage } from 'vue-konva'; const stageRef = ref(null); const textRef = ref(null); const [darthVaderImage] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous'); const width = window.innerWidth; const height = window.innerHeight; // 舞台配置 const stageConfig = { width, height }; // 背景配置 const backgroundConfig = { width, height, fill: 'rgba(200, 200, 200)' }; // 文本配置 const textConfig = { text: '这是达斯·维达', x: 15, y: 40, rotation: -10, fontSize: 18, filters: [Konva.Filters.Blur], blurRadius: 4 }; // 箭头配置 const arrowConfig = { points: [70, 50, 100, 80, 150, 100, 190, 100], tension: 0.5, stroke: 'black', fill: 'black' }; // 图像配置 const imageConfig = computed(() => ({ image: darthVaderImage.value, x: 200, y: 50, scaleX: 0.5, scaleY: 0.5 })); // 加载 jsPDF 库 onMounted(() => { // 加载 jsPDF 库 const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.debug.js'; script.integrity = 'sha384-NaWTHo/8YCBYJ59830LTz/P4aQZK1sS0SneOgAvhsIl3zBu8r9RevNg5lHCHAuQ/'; script.crossOrigin = 'anonymous'; document.head.appendChild(script); // 缓存文本以便模糊过滤器工作 if (textRef.value) { textRef.value.getNode().cache(); } }); onUnmounted(() => { // 清理脚本 const script = document.querySelector('script[src*="jspdf"]'); if (script) { document.head.removeChild(script); } }); // 处理 PDF 导出 const handleExport = () => { if (stageRef.value && typeof window.jsPDF !== 'undefined') { const stage = stageRef.value.getNode(); const pdf = new window.jsPDF('l', 'px', [width, height]); pdf.setTextColor('#000000'); // 首先添加文本 stage.find('Text').forEach((text) => { const size = text.fontSize() / 0.75; // 将像素转换为点 pdf.setFontSize(size); pdf.text(text.text(), text.x(), text.y(), { baseline: 'top', angle: -text.getAbsoluteRotation(), }); }); // 然后在文本上绘制图像(使文本不可见) pdf.addImage( stage.toDataURL({ pixelRatio: 2 }), 0, 0, width, height ); pdf.save('canvas.pdf'); } else { console.error('jsPDF 库未加载或舞台不可用'); alert('jsPDF 库未加载。在真实项目中,你需要将其包含进来。'); } }; </script>