From e1a4a314585986d92be6d6550fbaee8becfd30be Mon Sep 17 00:00:00 2001 From: alam00000 Date: Fri, 14 Aug 2026 14:56:23 +0530 Subject: [PATCH] Add out of memory error handling for PDF to TIFF conversion --- public/locales/en/tools.json | 3 +- src/js/logic/pdf-to-tiff-page.ts | 235 ++++++++++++++++++------------- 2 files changed, 140 insertions(+), 98 deletions(-) diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 68f32fbd..aa10e729 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -400,7 +400,8 @@ "noFile": "No File", "noFileExplanation": "Please upload a PDF file first.", "conversionSuccess": "PDF converted to TIFFs successfully!", - "conversionError": "Failed to convert PDF to TIFF. The file might be corrupted." + "conversionError": "Failed to convert PDF to TIFF. The file might be corrupted.", + "conversionOutOfMemory": "The browser ran out of memory while converting. Try a lower DPI, or uncheck the multi-page TIFF option to convert pages one at a time." } }, "pdfToCbz": { diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index 7d713dba..15a73fbe 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -36,6 +36,7 @@ async function getVips(): Promise { return fileName; }, }); + vipsInstance.Cache.max(0); return vipsInstance; } @@ -145,7 +146,11 @@ async function renderPageToRgba( }).promise; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - return { rgba: imageData.data, width: canvas.width, height: canvas.height }; + const { width, height } = canvas; + page.cleanup(); + canvas.width = 0; + canvas.height = 0; + return { rgba: imageData.data, width, height }; } function encodePageToTiff( @@ -155,52 +160,60 @@ function encodePageToTiff( height: number, options: TiffOptions ): Uint8Array { - let image = vips.Image.newFromMemory( - new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength), - width, - height, - 4, - vips.BandFormat.uchar - ); - - image = image.copy(); - const pixelsPerMm = options.dpi / 25.4; - image.setDouble('xres', pixelsPerMm); - image.setDouble('yres', pixelsPerMm); - - if (options.colorMode === 'greyscale' || options.colorMode === 'bw') { - if (image.bands === 4) { - image = image.flatten({ background: [255, 255, 255] }); - } - image = image.colourspace(vips.Interpretation.b_w); - } else { - if (image.bands === 4) { - image = image.flatten({ background: [255, 255, 255] }); - } - } - - const tiffOptions: Parameters[0] = { - compression: options.compression as Vips.Enum, - resunit: vips.ForeignTiffResunit.inch, - xres: options.dpi / 25.4, - yres: options.dpi / 25.4, - predictor: - options.compression === 'lzw' || options.compression === 'deflate' - ? vips.ForeignTiffPredictor.horizontal - : vips.ForeignTiffPredictor.none, + const intermediates: Vips.Image[] = []; + const track = (img: Vips.Image): Vips.Image => { + intermediates.push(img); + return img; }; - if (options.colorMode === 'bw') { - tiffOptions.bitdepth = 1; - } + try { + let image = track( + vips.Image.newFromMemory( + new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength), + width, + height, + 4, + vips.BandFormat.uchar + ) + ); - if (options.compression === 'jpeg') { - tiffOptions.Q = 85; - } + image = track(image.copy()); + const pixelsPerMm = options.dpi / 25.4; + image.setDouble('xres', pixelsPerMm); + image.setDouble('yres', pixelsPerMm); - const buffer = image.tiffsaveBuffer(tiffOptions); - image.delete(); - return buffer; + if (image.bands === 4) { + image = track(image.flatten({ background: [255, 255, 255] })); + } + if (options.colorMode === 'greyscale' || options.colorMode === 'bw') { + image = track(image.colourspace(vips.Interpretation.b_w)); + } + + const tiffOptions: Parameters[0] = { + compression: options.compression as Vips.Enum, + resunit: vips.ForeignTiffResunit.inch, + xres: options.dpi / 25.4, + yres: options.dpi / 25.4, + predictor: + options.compression === 'lzw' || options.compression === 'deflate' + ? vips.ForeignTiffPredictor.horizontal + : vips.ForeignTiffPredictor.none, + }; + + if (options.colorMode === 'bw') { + tiffOptions.bitdepth = 1; + } + + if (options.compression === 'jpeg') { + tiffOptions.Q = 85; + } + + return image.tiffsaveBuffer(tiffOptions); + } finally { + for (const img of intermediates) { + if (!img.isDeleted()) img.delete(); + } + } } async function convert() { @@ -239,68 +252,88 @@ async function convert() { if (options.multiPage && pdf.numPages > 1) { const pages: Vips.Image[] = []; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const { rgba, width, height } = await renderPageToRgba( - page, - options.dpi - ); + try { + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const { rgba, width, height } = await renderPageToRgba( + page, + options.dpi + ); - let img = vips.Image.newFromMemory( - new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength), - width, - height, - 4, - vips.BandFormat.uchar - ); + const intermediates: Vips.Image[] = []; + const track = (image: Vips.Image): Vips.Image => { + intermediates.push(image); + return image; + }; - if (options.colorMode === 'greyscale' || options.colorMode === 'bw') { - if (img.bands === 4) { - img = img.flatten({ background: [255, 255, 255] }); - } - img = img.colourspace(vips.Interpretation.b_w); - } else { - if (img.bands === 4) { - img = img.flatten({ background: [255, 255, 255] }); + try { + let img = track( + vips.Image.newFromMemory( + new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength), + width, + height, + 4, + vips.BandFormat.uchar + ) + ); + + if (img.bands === 4) { + img = track(img.flatten({ background: [255, 255, 255] })); + } + if ( + options.colorMode === 'greyscale' || + options.colorMode === 'bw' + ) { + img = track(img.colourspace(vips.Interpretation.b_w)); + } + + pages.push(img.copyMemory()); + } finally { + for (const img of intermediates) { + if (!img.isDeleted()) img.delete(); + } } } - pages.push(img); - } + const firstPage = pages[0]; + let joined = firstPage; + if (pages.length > 1) { + joined = vips.Image.arrayjoin(pages, { across: 1 }); + } - const firstPage = pages[0]; - let joined = firstPage; - if (pages.length > 1) { - joined = vips.Image.arrayjoin(pages, { across: 1 }); - } + try { + const tiffOptions: Parameters[0] = { + compression: options.compression as Vips.Enum, + resunit: vips.ForeignTiffResunit.inch, + xres: options.dpi / 25.4, + yres: options.dpi / 25.4, + page_height: firstPage.height, + predictor: + options.compression === 'lzw' || options.compression === 'deflate' + ? vips.ForeignTiffPredictor.horizontal + : vips.ForeignTiffPredictor.none, + }; - const tiffOptions: Parameters[0] = { - compression: options.compression as Vips.Enum, - resunit: vips.ForeignTiffResunit.inch, - xres: options.dpi / 25.4, - yres: options.dpi / 25.4, - page_height: firstPage.height, - predictor: - options.compression === 'lzw' || options.compression === 'deflate' - ? vips.ForeignTiffPredictor.horizontal - : vips.ForeignTiffPredictor.none, - }; + if (options.colorMode === 'bw') { + tiffOptions.bitdepth = 1; + } - if (options.colorMode === 'bw') { - tiffOptions.bitdepth = 1; - } + if (options.compression === 'jpeg') { + tiffOptions.Q = 85; + } - if (options.compression === 'jpeg') { - tiffOptions.Q = 85; - } - - const buffer = joined.tiffsaveBuffer(tiffOptions); - const blob = new Blob([new Uint8Array(buffer)], { type: 'image/tiff' }); - downloadFile(blob, getCleanPdfFilename(files[0].name) + '.tiff'); - - joined.delete(); - for (const p of pages) { - if (!p.isDeleted()) p.delete(); + const buffer = joined.tiffsaveBuffer(tiffOptions); + const blob = new Blob([new Uint8Array(buffer)], { + type: 'image/tiff', + }); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.tiff'); + } finally { + if (joined !== firstPage && !joined.isDeleted()) joined.delete(); + } + } finally { + for (const p of pages) { + if (!p.isDeleted()) p.delete(); + } } } else if (pdf.numPages === 1) { const page = await pdf.getPage(1); @@ -334,7 +367,15 @@ async function convert() { ); } catch (e) { console.error(e); - showAlert(t('common.error'), t('tools:pdfToTiff.alert.conversionError')); + const message = e instanceof Error ? e.message : String(e); + if (/memory/i.test(message)) { + showAlert( + t('common.error'), + t('tools:pdfToTiff.alert.conversionOutOfMemory') + ); + } else { + showAlert(t('common.error'), t('tools:pdfToTiff.alert.conversionError')); + } } finally { hideLoader(); }