mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-18 04:43:55 +00:00
204 lines
6 KiB
TypeScript
204 lines
6 KiB
TypeScript
import DOMPurify from 'dompurify';
|
|
import { toast } from 'svelte-sonner';
|
|
|
|
import { createNewNote } from '$lib/apis/notes';
|
|
import { safeImageUrl } from '$lib/utils/safeImageUrl';
|
|
|
|
export const downloadPdf = async (note) => {
|
|
const [{ default: jsPDF }, { default: html2canvas }] = await Promise.all([
|
|
import('jspdf'),
|
|
import('html2canvas-pro')
|
|
]);
|
|
|
|
// Define a fixed virtual screen size
|
|
const virtualWidth = 1024; // Fixed width (adjust as needed)
|
|
const virtualHeight = 1400; // Fixed height (adjust as needed)
|
|
|
|
// STEP 1. Get a DOM node to render
|
|
const html = DOMPurify.sanitize(note.data?.content?.html ?? '');
|
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
|
|
|
let node;
|
|
if (html instanceof HTMLElement) {
|
|
node = html;
|
|
} else {
|
|
const virtualWidth = 800; // px, fixed width for cloned element
|
|
|
|
// Clone and style
|
|
node = document.createElement('div');
|
|
|
|
// title node
|
|
const titleNode = document.createElement('div');
|
|
titleNode.textContent = note.title;
|
|
titleNode.style.fontSize = '24px';
|
|
titleNode.style.fontWeight = 'medium';
|
|
titleNode.style.paddingBottom = '20px';
|
|
titleNode.style.color = isDarkMode ? 'white' : 'black';
|
|
node.appendChild(titleNode);
|
|
|
|
const contentNode = document.createElement('div');
|
|
|
|
contentNode.innerHTML = html;
|
|
|
|
for (const img of contentNode.querySelectorAll('img[src^="data://"]')) {
|
|
const fileId = img.getAttribute('src').replace('data://', '');
|
|
const file = (note.data?.files ?? []).find((f) => f.id === fileId);
|
|
if (file) {
|
|
img.setAttribute('src', safeImageUrl(file.url || ''));
|
|
}
|
|
}
|
|
|
|
node.appendChild(contentNode);
|
|
|
|
node.classList.add('text-black');
|
|
node.classList.add('dark:text-white');
|
|
node.style.width = `${virtualWidth}px`;
|
|
node.style.position = 'absolute';
|
|
node.style.left = '-9999px';
|
|
node.style.height = 'auto';
|
|
node.style.padding = '40px 40px';
|
|
|
|
console.log(node);
|
|
document.body.appendChild(node);
|
|
}
|
|
|
|
await Promise.all(
|
|
Array.from(node.querySelectorAll('img')).map((img) => img.decode().catch(() => {}))
|
|
);
|
|
|
|
// A4 page settings
|
|
const pdf = new jsPDF('p', 'mm', 'a4');
|
|
const pageWidthMM = 210; // A4 width in mm
|
|
const pageHeightMM = 297; // A4 height in mm
|
|
|
|
const pagePixelHeight = Math.floor((virtualWidth * pageHeightMM) / pageWidthMM);
|
|
const rootTop = node.getBoundingClientRect().top;
|
|
const contentHeight = Math.ceil(node.getBoundingClientRect().height);
|
|
|
|
const blocks = [];
|
|
const trackedElements = new Set();
|
|
const collectBlocks = (element) => {
|
|
const children = Array.from(element.children);
|
|
const splittable =
|
|
children.length > 0 &&
|
|
(getComputedStyle(element).display === 'contents' ||
|
|
(element.getBoundingClientRect().height > pagePixelHeight / 4 &&
|
|
!Array.from(element.childNodes).some(
|
|
(childNode) => childNode.nodeType === Node.TEXT_NODE && childNode.textContent?.trim()
|
|
) &&
|
|
children.every((child) => !getComputedStyle(child).display.startsWith('inline'))));
|
|
|
|
if (element !== node) trackedElements.add(element);
|
|
|
|
if (splittable) {
|
|
children.forEach(collectBlocks);
|
|
} else {
|
|
const { top, bottom } = element.getBoundingClientRect();
|
|
blocks.push({ element, top: top - rootTop, bottom: bottom - rootTop });
|
|
}
|
|
};
|
|
collectBlocks(node);
|
|
|
|
node.setAttribute('data-pdf-root', '');
|
|
|
|
let offsetY = 0;
|
|
let page = 0;
|
|
|
|
while (offsetY < contentHeight) {
|
|
const sliceHeight = Math.min(pagePixelHeight, contentHeight - offsetY);
|
|
const sliceBlocks = blocks.filter(
|
|
(block) => block.bottom > offsetY && block.top < offsetY + sliceHeight
|
|
);
|
|
|
|
const keptElements = new Set();
|
|
for (const { element } of sliceBlocks) {
|
|
for (let current = element; current && current !== node; current = current.parentElement) {
|
|
keptElements.add(current);
|
|
}
|
|
}
|
|
|
|
const anchor = sliceBlocks.find((block) => block.bottom > block.top);
|
|
anchor?.element.setAttribute('data-pdf-anchor', '');
|
|
|
|
const canvas = await html2canvas(node, {
|
|
useCORS: true,
|
|
logging: false,
|
|
backgroundColor: isDarkMode ? '#000' : '#fff',
|
|
scale: 2, // Keep at 1x to avoid unexpected enlargements
|
|
width: virtualWidth, // Set fixed virtual screen width
|
|
windowWidth: virtualWidth, // Ensure consistent rendering
|
|
windowHeight: virtualHeight,
|
|
height: sliceHeight,
|
|
y: offsetY,
|
|
ignoreElements: (element) =>
|
|
(element.parentElement === document.body &&
|
|
element !== node &&
|
|
!['STYLE', 'LINK'].includes(element.tagName)) ||
|
|
(trackedElements.has(element) && !keptElements.has(element)),
|
|
onclone: (clonedDocument) => {
|
|
const clonedRoot = clonedDocument.querySelector('[data-pdf-root]');
|
|
const clonedAnchor = clonedDocument.querySelector('[data-pdf-anchor]');
|
|
if (!clonedRoot || !clonedAnchor) return;
|
|
|
|
const spacer = clonedDocument.createElement('div');
|
|
clonedRoot.prepend(spacer);
|
|
|
|
let spacerHeight = 0;
|
|
for (let pass = 0; pass < 2; pass++) {
|
|
spacerHeight +=
|
|
anchor.top -
|
|
(clonedAnchor.getBoundingClientRect().top - clonedRoot.getBoundingClientRect().top);
|
|
spacer.style.height = `${spacerHeight}px`;
|
|
}
|
|
}
|
|
});
|
|
|
|
anchor?.element.removeAttribute('data-pdf-anchor');
|
|
|
|
const imgData = canvas.toDataURL('image/jpeg', 0.7);
|
|
const imgHeightMM = (sliceHeight * pageWidthMM) / virtualWidth;
|
|
|
|
if (page > 0) pdf.addPage();
|
|
|
|
if (isDarkMode) {
|
|
pdf.setFillColor(0, 0, 0);
|
|
pdf.rect(0, 0, pageWidthMM, pageHeightMM, 'F'); // black bg
|
|
}
|
|
|
|
pdf.addImage(imgData, 'JPEG', 0, 0, pageWidthMM, imgHeightMM);
|
|
|
|
offsetY += sliceHeight;
|
|
page++;
|
|
}
|
|
|
|
// Remove hidden node if needed
|
|
if (!(html instanceof HTMLElement)) {
|
|
document.body.removeChild(node);
|
|
}
|
|
|
|
pdf.save(`${note.title}.pdf`);
|
|
};
|
|
|
|
export const createNoteHandler = async (title: string, md?: string, html?: string) => {
|
|
// $i18n.t('New Note'),
|
|
const res = await createNewNote(localStorage.token, {
|
|
// YYYY-MM-DD
|
|
title: title,
|
|
data: {
|
|
content: {
|
|
json: null,
|
|
html: html || md || '',
|
|
md: md || ''
|
|
}
|
|
},
|
|
meta: null,
|
|
access_grants: []
|
|
}).catch((error) => {
|
|
toast.error(`${error}`);
|
|
return null;
|
|
});
|
|
|
|
if (res) {
|
|
return res;
|
|
}
|
|
};
|