diff --git a/js/msr_character.js b/js/msr_character.js index 7663159..a55f918 100644 --- a/js/msr_character.js +++ b/js/msr_character.js @@ -57,6 +57,57 @@ function resolveCandidateImageUrls(originNode) { return urls; } +async function blobToOptimizedDataUrl(blob, maxDim = 1024, quality = 0.82) { + const objectUrl = URL.createObjectURL(blob); + try { + const image = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error("Failed to decode image blob")); + img.src = objectUrl; + }); + + const width = image.naturalWidth || image.width || 0; + const height = image.naturalHeight || image.height || 0; + const scale = width > 0 && height > 0 ? Math.min(1, maxDim / Math.max(width, height)) : 1; + const targetWidth = Math.max(1, Math.round(width * scale)) || width || 1; + const targetHeight = Math.max(1, Math.round(height * scale)) || height || 1; + + const canvas = document.createElement("canvas"); + canvas.width = targetWidth; + canvas.height = targetHeight; + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error("Could not acquire canvas context"); + } + ctx.drawImage(image, 0, 0, targetWidth, targetHeight); + + const optimizedBlob = await new Promise((resolve, reject) => { + canvas.toBlob((result) => { + if (result) resolve(result); + else reject(new Error("Canvas toBlob failed")); + }, "image/jpeg", quality); + }); + + const dataUrl = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(optimizedBlob); + }); + + return { + dataUrl, + originalWidth: width, + originalHeight: height, + outputWidth: targetWidth, + outputHeight: targetHeight, + outputBytes: optimizedBlob.size || 0, + }; + } finally { + URL.revokeObjectURL(objectUrl); + } +} + async function imageInputToDataUrl(node, inputName) { const originNode = getOriginNodeForInput(node, inputName); const candidateUrls = resolveCandidateImageUrls(originNode); @@ -72,6 +123,11 @@ async function imageInputToDataUrl(node, inputName) { mimeType: null, blobBytes: 0, dataUrlLength: 0, + originalWidth: 0, + originalHeight: 0, + outputWidth: 0, + outputHeight: 0, + outputBytes: 0, error: "No candidate image URLs found", }, }; @@ -87,11 +143,8 @@ async function imageInputToDataUrl(node, inputName) { if (!blob.type.startsWith("image/")) { throw new Error(`Unexpected blob type: ${blob.type || "unknown"}`); } - const dataUrl = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.readAsDataURL(blob); - }); + const optimized = await blobToOptimizedDataUrl(blob); + const dataUrl = optimized.dataUrl; return { dataUrl, debug: { @@ -103,6 +156,11 @@ async function imageInputToDataUrl(node, inputName) { mimeType: blob.type || null, blobBytes: blob.size || 0, dataUrlLength: typeof dataUrl === "string" ? dataUrl.length : 0, + originalWidth: optimized.originalWidth, + originalHeight: optimized.originalHeight, + outputWidth: optimized.outputWidth, + outputHeight: optimized.outputHeight, + outputBytes: optimized.outputBytes, error: null, }, }; @@ -122,6 +180,11 @@ async function imageInputToDataUrl(node, inputName) { mimeType: null, blobBytes: 0, dataUrlLength: 0, + originalWidth: 0, + originalHeight: 0, + outputWidth: 0, + outputHeight: 0, + outputBytes: 0, error: "All candidate image URLs failed", }, };