Bill's notes/research as this plugin is developed start here.


Example of inserting image via jsPDF suggests that I don't want to get mixed up rendering images into jsPDF. Need to get a canvas-based solution working.

html2canvas continues to be the phind-recommended solution most often recommended, but it seems to need the canvas to be non-zero sized, thus the forays below into OffscreenCanvas

OffscreenCanvas seems like it will work, but still getting exceptions trying to fetch the images to be inserted from plugin subdomain without running afoul of CSP restrictions

linkCurrent problem might suggest a path to get past offscreen canvas throwing exception about drawImage

Later todo

Swap back to using actual note content instead of sample content with unicode & image (two functionalities not natively easy with PDFs)



Note2PDF Experimental


Download a note as a PDF




This plugin provides an alternative to extracting PDFs from notes via the "Print method." Read more about exporting notes to PDFs at our help page.

As of May 2023 this plugin is still experimental and does not work. Please check it again in June. In the meantime, you can save a note to PDF format using your browser's print faculty, read more here.

"I don't always generate PDFs, but when I do, I prefer Note2PDF." --anon

// -----------------------------------------------------------------------
async noteOption(app, noteUUID) {
await this._initLibraries();
console.log("Initialized PDF libraries, now preparing PDF for download...");
await this._downloadPDF(app, noteUUID);
// -----------------------------------------------------------------------
async _initLibraries() {
if (typeof showdown === "undefined") {
console.log("Loading showdown from CDN...")
await this._loadScript("");
if (typeof jsPDF === "undefined") {
console.log("Loading jsPDF");
await this._loadScript("");
if (typeof html2canvas === "undefined") {
console.log("Loading html2canvas");
// Phind thinks that jsPDF needs this, tho jsPDF's docs suggest it gets loaded dynamically
await this._loadScript("");
// -----------------------------------------------------------------------
async _downloadPDF(app, noteUUID) {
const note = await app.notes.find(noteUUID);
const markdownText = await note.content();
// const html = this._markdownToHTML(markdownText);
const html = `

Some Unicode text: ©

console.log("Translated to HTML as", html);
// await this._htmlToPdf(app, html);
const offscreenCanvas = await this._htmlToCanvas(html);
console.log("offscreenCanvas", offscreenCanvas)
const imageBitmap = await offscreenCanvas.transferToImageBitmap();
console.log("Got bitmap", imageBitmap);
const imageDataURL = this._canvasToImage(imageBitmap);
console.log("Got imageDataURL", imageDataURL)
const pdf = new jsPDF("p", "mm", "a4");
if (pdf) {
console.log("Saving pdf", pdf);
pdf.addImage(imageDataURL, "JPEG", 10, 10);
const pdfContent = pdf.output(); // Generate PDF content
const blob = new Blob([pdfContent], { type: "application/pdf" });
await app.saveFile(blob, `note-${ noteUUID }.pdf`);
// -----------------------------------------------------------------------
_canvasToImage(imageBitmap) {
console.log("Rendering to canvas", imageBitmap, "width", imageBitmap.width)
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
return canvas.toDataURL('image/png');
// -----------------------------------------------------------------------
// Convert HTML to PDF using jsPDF
async _htmlToPdf(app, html) {
const contentEl = document.querySelector("#content") || document.createElement("div");
contentEl.setAttribute("id", "content");
contentEl.innerHTML = html;
const docBody = document.body;
console.log("Added", contentEl, "to document, resulting in", docBody, "thus width", docBody.innerWidth, "height", docBody.innerHeight);
const offscreenCanvas = new OffscreenCanvas(150, 150);
const ctx = offscreenCanvas.getContext("2d");
ctx.clearRect(0, 0, 150, 150);
ctx.drawImage(contentEl, 0, 0, 150, 150);
const imageData = offscreenCanvas.toDataURL("image/png");
console.log("imageData", imageData)
const doc = new jsPDF();
const pdf = doc.addImage(imageData, "PNG", 10, 10, 150, 50);
// const pdf = doc.fromHTML(html, 15, 15, { width: 170 });
return pdf;
// -----------------------------------------------------------------------
async _htmlToCanvas(html) {
const canvas = await html2canvas(html, {
ignoreElements: node => {
console.log("Whether to ignore", node)
return['background-image'].indexOf('image/svg') !== -1;;
proxy: "",
useCORS: true,
console.log("Initialized canvas", canvas, "from html")
const offscreenCanvas = new OffscreenCanvas(canvas.width, canvas.height);
const offscreenCtx = offscreenCanvas.getContext('2d');
console.log("Initialized offscreenCanvas", offscreenCanvas)
offscreenCtx.drawImage(canvas, 0, 0);
return offscreenCanvas;
// -----------------------------------------------------------------------
_markdownToHTML(markdownText) {
const converter = new showdown.Converter();
return converter.makeHtml(markdownText);
// -----------------------------------------------------------------------
async _loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;


May 18. v1.0 Initial implementation. Download still throwing Javascript errors that appear to need relaxed domain permissions.