Merge pull request #32459 from silentoplayz/fix/long-pdf-export-sliced

fix: long notes and chats download as PDF instead of failing
This commit is contained in:
G30 2026-10-15 05:11:40 -04:00 • committed by GitHub
parent fb5431a09c
commit f78e46ef44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 269 additions and 124 deletions

View file

@ -111,63 +111,102 @@
// Let the browser compute layout for the cloned element
await new Promise((r) => requestAnimationFrame(r));
// Render entire content once
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
scale: 2, // increase resolution
width: virtualWidth
});
document.body.removeChild(clonedElement);
const pdf = new jsPDF('p', 'mm', 'a4');
const pageWidthMM = 210;
const pageHeightMM = 297;
// Convert page height in mm to px on canvas scale for cropping
// Get canvas DPI scale:
const pxPerMM = canvas.width / virtualWidth; // width in px / width in px?
// Since 1 page width is 210 mm, but canvas width is 800 px at scale 2
// Assume 1 mm = px / (pageWidthMM scaled)
// Actually better: Calculate scale factor from px/mm:
// virtualWidth px corresponds directly to 210mm in PDF, so pxPerMM:
const pxPerPDFMM = canvas.width / pageWidthMM; // canvas px per PDF mm
const pagePixelHeight = Math.floor((virtualWidth * pageHeightMM) / pageWidthMM);
const rootTop = clonedElement.getBoundingClientRect().top;
const contentHeight = Math.ceil(clonedElement.getBoundingClientRect().height);
// Height in px for one page slice:
const pagePixelHeight = Math.floor(pxPerPDFMM * pageHeightMM);
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 !== clonedElement) trackedElements.add(element);
if (splittable) {
children.forEach(collectBlocks);
} else {
const { top, bottom } = element.getBoundingClientRect();
blocks.push({ element, top: top - rootTop, bottom: bottom - rootTop });
}
};
collectBlocks(clonedElement);
clonedElement.setAttribute('data-pdf-root', '');
let offsetY = 0;
let page = 0;
while (offsetY < canvas.height) {
// Height of slice
const sliceHeight = Math.min(pagePixelHeight, canvas.height - offsetY);
// Create temp canvas for slice
const pageCanvas = document.createElement('canvas');
pageCanvas.width = canvas.width;
pageCanvas.height = sliceHeight;
const ctx = pageCanvas.getContext('2d');
// Draw the slice of original canvas onto pageCanvas
ctx.drawImage(
canvas,
0,
offsetY,
canvas.width,
sliceHeight,
0,
0,
canvas.width,
sliceHeight
while (offsetY < contentHeight) {
const sliceHeight = Math.min(pagePixelHeight, contentHeight - offsetY);
const sliceBlocks = blocks.filter(
(block) => block.bottom > offsetY && block.top < offsetY + sliceHeight
);
const imgData = pageCanvas.toDataURL('image/jpeg', 0.7);
const keptElements = new Set();
for (const { element } of sliceBlocks) {
for (
let current = element;
current && current !== clonedElement;
current = current.parentElement
) {
keptElements.add(current);
}
}
// Calculate image height in PDF units keeping aspect ratio
const imgHeightMM = (sliceHeight * pageWidthMM) / canvas.width;
const anchor = sliceBlocks.find((block) => block.bottom > block.top);
anchor?.element.setAttribute('data-pdf-anchor', '');
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
logging: false,
scale: 2, // increase resolution
width: virtualWidth,
height: sliceHeight,
y: offsetY,
ignoreElements: (element) =>
(element.parentElement === document.body &&
element !== clonedElement &&
!['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();
@ -182,6 +221,8 @@
page++;
}
document.body.removeChild(clonedElement);
pdf.save(`chat-${chat.chat.title}.pdf`);
showFullMessages = false;

View file

@ -126,63 +126,102 @@
// Let the browser compute layout for the cloned element
await new Promise((r) => requestAnimationFrame(r));
// Render entire content once
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
scale: 2, // increase resolution
width: virtualWidth
});
document.body.removeChild(clonedElement);
const pdf = new jsPDF('p', 'mm', 'a4');
const pageWidthMM = 210;
const pageHeightMM = 297;
// Convert page height in mm to px on canvas scale for cropping
// Get canvas DPI scale:
const pxPerMM = canvas.width / virtualWidth; // width in px / width in px?
// Since 1 page width is 210 mm, but canvas width is 800 px at scale 2
// Assume 1 mm = px / (pageWidthMM scaled)
// Actually better: Calculate scale factor from px/mm:
// virtualWidth px corresponds directly to 210mm in PDF, so pxPerMM:
const pxPerPDFMM = canvas.width / pageWidthMM; // canvas px per PDF mm
const pagePixelHeight = Math.floor((virtualWidth * pageHeightMM) / pageWidthMM);
const rootTop = clonedElement.getBoundingClientRect().top;
const contentHeight = Math.ceil(clonedElement.getBoundingClientRect().height);
// Height in px for one page slice:
const pagePixelHeight = Math.floor(pxPerPDFMM * pageHeightMM);
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 !== clonedElement) trackedElements.add(element);
if (splittable) {
children.forEach(collectBlocks);
} else {
const { top, bottom } = element.getBoundingClientRect();
blocks.push({ element, top: top - rootTop, bottom: bottom - rootTop });
}
};
collectBlocks(clonedElement);
clonedElement.setAttribute('data-pdf-root', '');
let offsetY = 0;
let page = 0;
while (offsetY < canvas.height) {
// Height of slice
const sliceHeight = Math.min(pagePixelHeight, canvas.height - offsetY);
// Create temp canvas for slice
const pageCanvas = document.createElement('canvas');
pageCanvas.width = canvas.width;
pageCanvas.height = sliceHeight;
const ctx = pageCanvas.getContext('2d');
// Draw the slice of original canvas onto pageCanvas
ctx.drawImage(
canvas,
0,
offsetY,
canvas.width,
sliceHeight,
0,
0,
canvas.width,
sliceHeight
while (offsetY < contentHeight) {
const sliceHeight = Math.min(pagePixelHeight, contentHeight - offsetY);
const sliceBlocks = blocks.filter(
(block) => block.bottom > offsetY && block.top < offsetY + sliceHeight
);
const imgData = pageCanvas.toDataURL('image/jpeg', 0.7);
const keptElements = new Set();
for (const { element } of sliceBlocks) {
for (
let current = element;
current && current !== clonedElement;
current = current.parentElement
) {
keptElements.add(current);
}
}
// Calculate image height in PDF units keeping aspect ratio
const imgHeightMM = (sliceHeight * pageWidthMM) / canvas.width;
const anchor = sliceBlocks.find((block) => block.bottom > block.top);
anchor?.element.setAttribute('data-pdf-anchor', '');
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
logging: false,
scale: 2, // increase resolution
width: virtualWidth,
height: sliceHeight,
y: offsetY,
ignoreElements: (element) =>
(element.parentElement === document.body &&
element !== clonedElement &&
!['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();
@ -197,6 +236,8 @@
page++;
}
document.body.removeChild(clonedElement);
pdf.save(`chat-${chat.chat.title}.pdf`);
showFullMessages = false;

View file

@ -62,55 +62,118 @@ export const downloadPdf = async (note) => {
document.body.appendChild(node);
}
// Render to canvas with predefined width
const canvas = await html2canvas(node, {
useCORS: true,
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
});
// Remove hidden node if needed
if (!(html instanceof HTMLElement)) {
document.body.removeChild(node);
}
const imgData = canvas.toDataURL('image/jpeg', 0.7);
await Promise.all(
Array.from(node.querySelectorAll('img')).map((img) => img.decode().catch(() => {}))
);
// A4 page settings
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 width in mm
const pageWidthMM = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
const pageHeightMM = 297; // A4 height in mm
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, pageWidthMM, pageHeightMM, 'F'); // black bg
}
const pagePixelHeight = Math.floor((virtualWidth * pageHeightMM) / pageWidthMM);
const rootTop = node.getBoundingClientRect().top;
const contentHeight = Math.ceil(node.getBoundingClientRect().height);
// Maintain aspect ratio
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
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'))));
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
if (element !== node) trackedElements.add(element);
// Handle additional pages
while (heightLeft > 0) {
position -= pageHeight;
pdf.addPage();
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, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
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`);