diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js index ebafa41..438eab5 100644 --- a/desktop-app/resources/js/script.js +++ b/desktop-app/resources/js/script.js @@ -2023,6 +2023,14 @@ This is a fully client-side application. Your content never leaves your browser windowWidth: 1000, // html2canvas config scale: 2 // html2canvas scale factor }; + // Browser canvas implementations commonly fail/blank around 32,767 px in either dimension. + // Keep a margin under that practical limit for cross-browser stability. + const MAX_PDF_CANVAS_DIMENSION = 32000; + // Very large total pixel areas can also fail on some engines even below dimension limits. + // 250M px is a conservative cap to avoid blank exports on large markdown documents. + const MAX_PDF_CANVAS_AREA = 250000000; + // Keep readability by not scaling below 50% for PDF capture. + const MIN_READABLE_PDF_SCALE = 0.5; /** * Task 1: Identifies all graphic elements that may need page-break handling @@ -2586,35 +2594,128 @@ This is a fully client-side application. Your content never leaves your browser const margin = 15; const contentWidth = pageWidth - (margin * 2); - const canvas = await html2canvas(tempElement, { - scale: 2, - useCORS: true, - allowTaint: true, - logging: false, - windowWidth: 1000, - windowHeight: tempElement.scrollHeight + // Prevent oversized canvases for very large documents (can produce empty PDF output) + const actualElementWidth = Math.max(tempElement.offsetWidth || 0, 1); + const actualElementHeight = Math.max(tempElement.scrollHeight || 0, 1); + const pageContentHeightPx = actualElementWidth * (PAGE_CONFIG.contentHeight / PAGE_CONFIG.contentWidth); + const desiredScale = PAGE_CONFIG.scale; + const dimensionLimitedScale = Math.min( + MAX_PDF_CANVAS_DIMENSION / actualElementWidth, + MAX_PDF_CANVAS_DIMENSION / actualElementHeight + ); + const elementArea = actualElementWidth * actualElementHeight; + const areaLimitedScale = Number.isFinite(elementArea) && elementArea > 0 + ? Math.sqrt(MAX_PDF_CANVAS_AREA / elementArea) + // Fallback to 1 (no extra area-based reduction) when area is invalid. + : 1; + const safeScale = Math.max( + MIN_READABLE_PDF_SCALE, + Math.min(desiredScale, dimensionLimitedScale, areaLimitedScale) + ); + + if (safeScale < desiredScale) { + console.warn( + `Reducing PDF render scale from ${desiredScale} to ${safeScale.toFixed(2)} ` + + `to avoid browser canvas limits for large content.` + ); + } + + const totalPages = Math.max(1, Math.ceil(actualElementHeight / pageContentHeightPx)); + const shouldRenderInSlices = totalPages > 1; + const yieldToBrowser = () => new Promise(resolve => { + requestAnimationFrame(() => { + setTimeout(resolve, 0); + }); }); - const scaleFactor = canvas.width / contentWidth; - const imgHeight = canvas.height / scaleFactor; - const pagesCount = Math.ceil(imgHeight / (pageHeight - margin * 2)); + if (shouldRenderInSlices) { + statusText.textContent = `Rendering ${totalPages} pages...`; + await yieldToBrowser(); + + const sliceHeight = Math.max(1, Math.min(pageContentHeightPx, actualElementHeight)); + const sliceDimensionLimitedScale = Math.min( + MAX_PDF_CANVAS_DIMENSION / actualElementWidth, + MAX_PDF_CANVAS_DIMENSION / sliceHeight + ); + const sliceArea = actualElementWidth * sliceHeight; + const sliceAreaLimitedScale = Number.isFinite(sliceArea) && sliceArea > 0 + ? Math.sqrt(MAX_PDF_CANVAS_AREA / sliceArea) + // Fallback to 1 (no extra area-based reduction) when area is invalid. + : 1; + const sliceScale = Math.max( + MIN_READABLE_PDF_SCALE, + Math.min(desiredScale, sliceDimensionLimitedScale, sliceAreaLimitedScale) + ); + + if (sliceScale < desiredScale) { + console.warn( + `Reducing PDF slice scale from ${desiredScale} to ${sliceScale.toFixed(2)} ` + + `to stay within browser canvas limits.` + ); + } - for (let page = 0; page < pagesCount; page++) { - if (page > 0) pdf.addPage(); + for (let page = 0; page < totalPages; page++) { + statusText.textContent = `Rendering page ${page + 1} of ${totalPages}...`; + await yieldToBrowser(); - const sourceY = page * (pageHeight - margin * 2) * scaleFactor; - const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor); - const destHeight = sourceHeight / scaleFactor; + if (page > 0) pdf.addPage(); - const pageCanvas = document.createElement('canvas'); - pageCanvas.width = canvas.width; - pageCanvas.height = sourceHeight; + const sliceY = page * pageContentHeightPx; + const sliceHeightForPage = Math.min(actualElementHeight - sliceY, pageContentHeightPx); - const ctx = pageCanvas.getContext('2d'); - ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight); + const sliceCanvas = await html2canvas(tempElement, { + scale: sliceScale, + useCORS: true, + allowTaint: true, + logging: false, + windowWidth: Math.ceil(actualElementWidth), + windowHeight: Math.ceil(sliceHeightForPage), + y: Math.floor(sliceY), + height: Math.ceil(sliceHeightForPage) + }); - const imgData = pageCanvas.toDataURL('image/png'); - pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); + const sliceScaleFactor = sliceCanvas.width / contentWidth; + const destHeight = sliceCanvas.height / sliceScaleFactor; + const imgData = sliceCanvas.toDataURL('image/png'); + pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); + } + } else { + statusText.textContent = 'Rendering PDF...'; + await yieldToBrowser(); + + const canvas = await html2canvas(tempElement, { + scale: safeScale, + useCORS: true, + allowTaint: true, + logging: false, + windowWidth: Math.ceil(actualElementWidth), + windowHeight: Math.ceil(actualElementHeight) + }); + + const scaleFactor = canvas.width / contentWidth; + const imgHeight = canvas.height / scaleFactor; + const pagesCount = Math.ceil(imgHeight / (pageHeight - margin * 2)); + + for (let page = 0; page < pagesCount; page++) { + statusText.textContent = `Composing page ${page + 1} of ${pagesCount}...`; + await yieldToBrowser(); + + if (page > 0) pdf.addPage(); + + const sourceY = page * (pageHeight - margin * 2) * scaleFactor; + const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor); + const destHeight = sourceHeight / scaleFactor; + + const pageCanvas = document.createElement('canvas'); + pageCanvas.width = canvas.width; + pageCanvas.height = sourceHeight; + + const ctx = pageCanvas.getContext('2d'); + ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight); + + const imgData = pageCanvas.toDataURL('image/png'); + pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); + } } pdf.save("document.pdf"); diff --git a/script.js b/script.js index ebafa41..438eab5 100644 --- a/script.js +++ b/script.js @@ -2023,6 +2023,14 @@ This is a fully client-side application. Your content never leaves your browser windowWidth: 1000, // html2canvas config scale: 2 // html2canvas scale factor }; + // Browser canvas implementations commonly fail/blank around 32,767 px in either dimension. + // Keep a margin under that practical limit for cross-browser stability. + const MAX_PDF_CANVAS_DIMENSION = 32000; + // Very large total pixel areas can also fail on some engines even below dimension limits. + // 250M px is a conservative cap to avoid blank exports on large markdown documents. + const MAX_PDF_CANVAS_AREA = 250000000; + // Keep readability by not scaling below 50% for PDF capture. + const MIN_READABLE_PDF_SCALE = 0.5; /** * Task 1: Identifies all graphic elements that may need page-break handling @@ -2586,35 +2594,128 @@ This is a fully client-side application. Your content never leaves your browser const margin = 15; const contentWidth = pageWidth - (margin * 2); - const canvas = await html2canvas(tempElement, { - scale: 2, - useCORS: true, - allowTaint: true, - logging: false, - windowWidth: 1000, - windowHeight: tempElement.scrollHeight + // Prevent oversized canvases for very large documents (can produce empty PDF output) + const actualElementWidth = Math.max(tempElement.offsetWidth || 0, 1); + const actualElementHeight = Math.max(tempElement.scrollHeight || 0, 1); + const pageContentHeightPx = actualElementWidth * (PAGE_CONFIG.contentHeight / PAGE_CONFIG.contentWidth); + const desiredScale = PAGE_CONFIG.scale; + const dimensionLimitedScale = Math.min( + MAX_PDF_CANVAS_DIMENSION / actualElementWidth, + MAX_PDF_CANVAS_DIMENSION / actualElementHeight + ); + const elementArea = actualElementWidth * actualElementHeight; + const areaLimitedScale = Number.isFinite(elementArea) && elementArea > 0 + ? Math.sqrt(MAX_PDF_CANVAS_AREA / elementArea) + // Fallback to 1 (no extra area-based reduction) when area is invalid. + : 1; + const safeScale = Math.max( + MIN_READABLE_PDF_SCALE, + Math.min(desiredScale, dimensionLimitedScale, areaLimitedScale) + ); + + if (safeScale < desiredScale) { + console.warn( + `Reducing PDF render scale from ${desiredScale} to ${safeScale.toFixed(2)} ` + + `to avoid browser canvas limits for large content.` + ); + } + + const totalPages = Math.max(1, Math.ceil(actualElementHeight / pageContentHeightPx)); + const shouldRenderInSlices = totalPages > 1; + const yieldToBrowser = () => new Promise(resolve => { + requestAnimationFrame(() => { + setTimeout(resolve, 0); + }); }); - const scaleFactor = canvas.width / contentWidth; - const imgHeight = canvas.height / scaleFactor; - const pagesCount = Math.ceil(imgHeight / (pageHeight - margin * 2)); + if (shouldRenderInSlices) { + statusText.textContent = `Rendering ${totalPages} pages...`; + await yieldToBrowser(); + + const sliceHeight = Math.max(1, Math.min(pageContentHeightPx, actualElementHeight)); + const sliceDimensionLimitedScale = Math.min( + MAX_PDF_CANVAS_DIMENSION / actualElementWidth, + MAX_PDF_CANVAS_DIMENSION / sliceHeight + ); + const sliceArea = actualElementWidth * sliceHeight; + const sliceAreaLimitedScale = Number.isFinite(sliceArea) && sliceArea > 0 + ? Math.sqrt(MAX_PDF_CANVAS_AREA / sliceArea) + // Fallback to 1 (no extra area-based reduction) when area is invalid. + : 1; + const sliceScale = Math.max( + MIN_READABLE_PDF_SCALE, + Math.min(desiredScale, sliceDimensionLimitedScale, sliceAreaLimitedScale) + ); + + if (sliceScale < desiredScale) { + console.warn( + `Reducing PDF slice scale from ${desiredScale} to ${sliceScale.toFixed(2)} ` + + `to stay within browser canvas limits.` + ); + } - for (let page = 0; page < pagesCount; page++) { - if (page > 0) pdf.addPage(); + for (let page = 0; page < totalPages; page++) { + statusText.textContent = `Rendering page ${page + 1} of ${totalPages}...`; + await yieldToBrowser(); - const sourceY = page * (pageHeight - margin * 2) * scaleFactor; - const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor); - const destHeight = sourceHeight / scaleFactor; + if (page > 0) pdf.addPage(); - const pageCanvas = document.createElement('canvas'); - pageCanvas.width = canvas.width; - pageCanvas.height = sourceHeight; + const sliceY = page * pageContentHeightPx; + const sliceHeightForPage = Math.min(actualElementHeight - sliceY, pageContentHeightPx); - const ctx = pageCanvas.getContext('2d'); - ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight); + const sliceCanvas = await html2canvas(tempElement, { + scale: sliceScale, + useCORS: true, + allowTaint: true, + logging: false, + windowWidth: Math.ceil(actualElementWidth), + windowHeight: Math.ceil(sliceHeightForPage), + y: Math.floor(sliceY), + height: Math.ceil(sliceHeightForPage) + }); - const imgData = pageCanvas.toDataURL('image/png'); - pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); + const sliceScaleFactor = sliceCanvas.width / contentWidth; + const destHeight = sliceCanvas.height / sliceScaleFactor; + const imgData = sliceCanvas.toDataURL('image/png'); + pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); + } + } else { + statusText.textContent = 'Rendering PDF...'; + await yieldToBrowser(); + + const canvas = await html2canvas(tempElement, { + scale: safeScale, + useCORS: true, + allowTaint: true, + logging: false, + windowWidth: Math.ceil(actualElementWidth), + windowHeight: Math.ceil(actualElementHeight) + }); + + const scaleFactor = canvas.width / contentWidth; + const imgHeight = canvas.height / scaleFactor; + const pagesCount = Math.ceil(imgHeight / (pageHeight - margin * 2)); + + for (let page = 0; page < pagesCount; page++) { + statusText.textContent = `Composing page ${page + 1} of ${pagesCount}...`; + await yieldToBrowser(); + + if (page > 0) pdf.addPage(); + + const sourceY = page * (pageHeight - margin * 2) * scaleFactor; + const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor); + const destHeight = sourceHeight / scaleFactor; + + const pageCanvas = document.createElement('canvas'); + pageCanvas.width = canvas.width; + pageCanvas.height = sourceHeight; + + const ctx = pageCanvas.getContext('2d'); + ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight); + + const imgData = pageCanvas.toDataURL('image/png'); + pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight); + } } pdf.save("document.pdf");