Canvas to PDF — Export HTML5 Canvas as PDF with JavaScript
Export any HTML5 Canvas content to a PDF document using JavaScript. This demo shows how to convert a Konva stage into a downloadable PDF file using jsPDF, with support for high-quality rendering and selectable text.
The approach works in four steps: generate your canvas content, export the canvas as an image, insert that image into a PDF document, and save. Two tips to get the best results:
High-quality export: Use the pixelRatio attribute when converting canvas to an image — see the High Quality Export guide for details.
Selectable text in PDF: Even though the canvas is added as an image, you can insert text nodes manually into the PDF layer beneath it. The text won't be visible (it sits behind the image) but remains selectable and searchable. Text rendering in PDF differs from Konva's, so complex styles may need adjustment.
Instructions: view the canvas below, then click the button to save it as a PDF.
- Vanilla
- React
- Vue
import Konva from 'konva';
import { jsPDF } from 'jspdf';
// Create a button for PDF export
const saveButton = document.createElement('button');
saveButton.textContent = 'Save as PDF';
saveButton.style.position = 'absolute';
saveButton.style.top = '5px';
saveButton.style.left = '5px';
document.body.appendChild(saveButton);
// Create a stage
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);
// Add background
const back = new Konva.Rect({
width: stage.width(),
height: stage.height(),
fill: 'rgba(200, 200, 200)',
});
layer.add(back);
// Add text with blur effect
const text = new Konva.Text({
text: 'This is the Darth Vader',
x: 15,
y: 40,
rotation: -10,
filters: [Konva.Filters.Blur],
blurRadius: 4,
fontSize: 18,
});
text.cache();
layer.add(text);
// Add arrow
const arrow = new Konva.Arrow({
points: [70, 50, 100, 80, 150, 100, 190, 100],
tension: 0.5,
stroke: 'black',
fill: 'black',
});
layer.add(arrow);
// Add image
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('Failed to load image');
}
);
// Handle PDF export
saveButton.addEventListener('click', function () {
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [stage.width(), stage.height()],
hotfixes: ['px_scaling'],
});
pdf.setTextColor('#000000');
// First add texts
stage.find('Text').forEach((text) => {
const size = text.fontSize() * 0.75; // convert pixels to points
pdf.setFontSize(size);
pdf.text(text.text(), text.x(), text.y(), {
baseline: 'top',
angle: -text.getAbsoluteRotation(),
});
});
// Then put image on top of texts (so texts are not visible)
pdf.addImage(
stage.toDataURL({ pixelRatio: 2 }),
0,
0,
stage.width(),
stage.height()
);
pdf.save('canvas.pdf');
});
import Konva from 'konva';
import { useEffect, useRef } from 'react';
import { Stage, Layer, Rect, Text, Arrow, Image } from 'react-konva';
import useImage from 'use-image';
import { jsPDF } from 'jspdf';
const App = () => {
const stageRef = useRef(null);
const textRef = useRef(null);
const [darthVaderImage] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const width = window.innerWidth;
const height = window.innerHeight;
useEffect(() => {
textRef.current?.cache();
}, []);
// Handle PDF export
const handleExport = () => {
if (stageRef.current) {
const stage = stageRef.current;
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [width, height],
hotfixes: ['px_scaling'],
});
pdf.setTextColor('#000000');
// First add texts
stage.find('Text').forEach((text) => {
const size = text.fontSize() * 0.75; // convert pixels to points
pdf.setFontSize(size);
pdf.text(text.text(), text.x(), text.y(), {
baseline: 'top',
angle: -text.getAbsoluteRotation(),
});
});
// Then put image on top of texts (so texts are not visible)
pdf.addImage(
stage.toDataURL({ pixelRatio: 2 }),
0,
0,
width,
height
);
pdf.save('canvas.pdf');
} else {
console.error('Stage is not available');
}
};
return (
<div style={{ position: 'relative' }}>
<button
style={{ position: 'absolute', top: '5px', left: '5px', zIndex: 10 }}
onClick={handleExport}
>
Save as PDF
</button>
<Stage width={width} height={height} ref={stageRef}>
<Layer>
<Rect
width={width}
height={height}
fill="rgba(200, 200, 200)"
/>
<Text
ref={textRef}
text="This is the Darth Vader"
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"
>
Save as 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, computed } from 'vue';
import Konva from 'konva';
import { useImage } from 'vue-konva';
import { jsPDF } from 'jspdf';
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;
// Stage configuration
const stageConfig = {
width,
height
};
// Background configuration
const backgroundConfig = {
width,
height,
fill: 'rgba(200, 200, 200)'
};
// Text configuration
const textConfig = {
text: 'This is the Darth Vader',
x: 15,
y: 40,
rotation: -10,
fontSize: 18,
filters: [Konva.Filters.Blur],
blurRadius: 4
};
// Arrow configuration
const arrowConfig = {
points: [70, 50, 100, 80, 150, 100, 190, 100],
tension: 0.5,
stroke: 'black',
fill: 'black'
};
// Image configuration
const imageConfig = computed(() => ({
image: darthVaderImage.value,
x: 200,
y: 50,
scaleX: 0.5,
scaleY: 0.5
}));
onMounted(() => {
// Cache text for blur filter to work
if (textRef.value) {
textRef.value.getNode().cache();
}
});
// Handle PDF export
const handleExport = () => {
if (stageRef.value) {
const stage = stageRef.value.getNode();
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [width, height],
hotfixes: ['px_scaling'],
});
pdf.setTextColor('#000000');
// First add texts
stage.find('Text').forEach((text) => {
const size = text.fontSize() * 0.75; // convert pixels to points
pdf.setFontSize(size);
pdf.text(text.text(), text.x(), text.y(), {
baseline: 'top',
angle: -text.getAbsoluteRotation(),
});
});
// Then put image on top of texts (so texts are not visible)
pdf.addImage(
stage.toDataURL({ pixelRatio: 2 }),
0,
0,
width,
height
);
pdf.save('canvas.pdf');
} else {
console.error('Stage is not available');
}
};
</script>
Print output
The method above places a raster image into an RGB document. A commercial printer usually asks for CMYK separation, PDF/X compliance, bleed, and crop marks, and a browser canvas produces none of them — that needs a render step outside the browser. Polotno is a commercial design editor SDK built with Konva that covers the editor and its export pipeline.