diff --git a/src/common/util/canvasUtils.ts b/src/common/util/canvasUtils.ts index 717e46e8d..7f3258dde 100644 --- a/src/common/util/canvasUtils.ts +++ b/src/common/util/canvasUtils.ts @@ -1,10 +1,49 @@ +/** + * Converts a canvas to a data URL and extracts the MIME type and Base64 data + * @param canvas The canvas element to convert + * @param requestedMimeType The desired MIME type for the image + * @param imageQuality A number between 0 and 1 indicating image quality for lossy formats + * @param userLogLabel A label to use in console warnings + */ +export function canvasToDataURLAndMimeType( + canvas: HTMLCanvasElement, + requestedMimeType: string, + imageQuality: number | undefined, + userLogLabel: string, +): { mimeType: string; base64Data: string } { + + // Extract the actual MIME type and Base64 data efficiently + const dataUrl = canvas.toDataURL(requestedMimeType, imageQuality); + + const colonIndex = dataUrl.indexOf(':'); + const semicolonIndex = dataUrl.indexOf(';', colonIndex); + if (colonIndex === -1 || semicolonIndex === -1) + throw new Error('canvasToDataURLAndMimeType: Invalid data URL format.'); + const actualMimeType = dataUrl.slice(colonIndex + 1, semicolonIndex); + + const commaIndex = dataUrl.indexOf(','); + if (commaIndex === -1) + throw new Error('canvasToDataURLAndMimeType: Invalid data URL comma.'); + const base64Data = dataUrl.slice(commaIndex + 1); + + // Warn if the actual MIME type differs from the requested one + if (actualMimeType !== requestedMimeType) + console.warn(`${userLogLabel}: requested MIME type "${requestedMimeType}" was not used. Actual MIME type is "${actualMimeType}".`); + + return { mimeType: actualMimeType, base64Data }; +} + /** * Creates a Blob object representing the image contained in the canvas * @param canvas The canvas element to convert to a Blob. * @param imageFormat Browsers are required to support image/png; many will support additional formats including image/jpeg and image/webp. * @param imageQuality A Number between 0 and 1 indicating image quality if the requested type is image/jpeg or image/webp. */ -export async function asyncCanvasToBlob(canvas: HTMLCanvasElement, imageFormat: 'image/png' | 'image/jpeg', imageQuality?: number): Promise { +export async function asyncCanvasToBlob( + canvas: HTMLCanvasElement, + imageFormat: 'image/png' | 'image/jpeg', + imageQuality?: number +): Promise { return new Promise((resolve) => canvas.toBlob(resolve, imageFormat, imageQuality)); } diff --git a/src/common/util/imageUtils.ts b/src/common/util/imageUtils.ts index 51fc6df8d..0dcb842b2 100644 --- a/src/common/util/imageUtils.ts +++ b/src/common/util/imageUtils.ts @@ -5,6 +5,7 @@ * Also see videoUtils.ts for more image-related functions. */ +import { canvasToDataURLAndMimeType } from './canvasUtils'; import { createBlobURLFromDataURL } from './urlUtils'; @@ -67,11 +68,18 @@ export async function convertBase64Image(base64DataUrl: string, destMimeType: st return; } ctx.drawImage(image, 0, 0); - const dataUrl = canvas.toDataURL(destMimeType, destQuality); - resolve({ - mimeType: destMimeType, - base64: dataUrl.split(',')[1], - }); + + // Convert canvas image to a DataURL string + try { + const { mimeType: actualMimeType, base64Data } = canvasToDataURLAndMimeType(canvas, destMimeType, destQuality, 'image-convert'); + resolve({ + mimeType: actualMimeType, + base64: base64Data, + }); + } catch (error) { + console.warn(`imageUtils: failed to convert image to ${destMimeType}.`, { error }); + reject(new Error(`Failed to convert image to '${destMimeType}'.`)); + } }; image.onerror = (error) => { console.warn('Failed to load image for conversion.', error); @@ -222,11 +230,18 @@ export async function resizeBase64ImageIfNeeded(inputMimeType: string, inputBase canvas.width = newWidth; canvas.height = newHeight; ctx.drawImage(image, 0, 0, newWidth, newHeight); - const resizedDataUrl = canvas.toDataURL(destMimeType, destQuality); - resolve({ - mimeType: destMimeType, - base64: resizedDataUrl.split(',')[1], // Return base64 part only - }); + + // Convert canvas image to a DataURL string + try { + const { mimeType: actualMimeType, base64Data } = canvasToDataURLAndMimeType(canvas, destMimeType, destQuality, 'image-resize'); + resolve({ + mimeType: actualMimeType, + base64: base64Data, + }); + } catch (error) { + console.warn(`imageUtils: failed to resize image to '${resizeMode}' as ${destMimeType}.`, { error }); + reject(new Error(`Failed to resize image to '${resizeMode}' as '${destMimeType}'.`)); + } }; image.onerror = (error) => { diff --git a/src/common/util/pdfUtils.ts b/src/common/util/pdfUtils.ts index 3bb35e6f4..18478ac9e 100644 --- a/src/common/util/pdfUtils.ts +++ b/src/common/util/pdfUtils.ts @@ -1,3 +1,5 @@ +import { canvasToDataURLAndMimeType } from './canvasUtils'; + // configuration const SKIP_LOADING_IN_DEV = false; @@ -97,26 +99,29 @@ export async function pdfToImageDataURLs(pdfBuffer: ArrayBuffer, imageMimeType: const viewport = page.getViewport({ scale }); const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); + const context = canvas.getContext('2d')!; canvas.height = viewport.height; canvas.width = viewport.width; onProgress((i * 3 + 1) / (pdf.numPages * 3)); await page.render({ - canvasContext: context!, + canvasContext: context, viewport, }).promise; - const base64DataUrl = canvas.toDataURL(imageMimeType, imageQuality); - const base64Data = base64DataUrl.slice(`data:${imageMimeType};base64,`.length); - - images.push({ - mimeType: imageMimeType, - base64Data, - scale, - width: viewport.width, - height: viewport.height, - }); + // Convert canvas image to a DataURL string + try { + const { mimeType: actualMimeType, base64Data } = canvasToDataURLAndMimeType(canvas, imageMimeType, imageQuality, 'pdf-to-image'); + images.push({ + mimeType: actualMimeType, + base64Data, + scale, + width: viewport.width, + height: viewport.height, + }); + } catch (error) { + console.warn(`pdfToImageDataURLs: failed to convert image to ${imageMimeType}.`, { error }); + } onProgress((i * 3 + 2) / (pdf.numPages * 3)); }