From 6cfffeb0318201462bb7cc3c48ae6c4cce07c9fe Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 28 Aug 2026 23:32:58 -0400 Subject: [PATCH] refactor(core): avoid encoding rejected image candidates (#46073) --- packages/core/src/image/photon.ts | 12 ++++++--- packages/core/test/tool-read.test.ts | 38 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/core/src/image/photon.ts b/packages/core/src/image/photon.ts index 610362bf7c3..9723edba926 100644 --- a/packages/core/src/image/photon.ts +++ b/packages/core/src/image/photon.ts @@ -78,9 +78,15 @@ export const make = Effect.gen(function* () { ...JPEG_QUALITIES.map((quality) => ["image/jpeg", () => resized.get_bytes_jpeg(quality)] as const), ] for (const [mime, encode] of encoders) { - const candidate = Buffer.from(encode()).toString("base64") - if (Buffer.byteLength(candidate, "utf-8") <= limits.maxBase64Bytes) - return { ...content, content: candidate, encoding: "base64" as const, mime } + const candidate = encode() + // Base64 uses four bytes per three input bytes, including padding. + if (Math.ceil(candidate.length / 3) * 4 <= limits.maxBase64Bytes) + return { + ...content, + content: Buffer.from(candidate).toString("base64"), + encoding: "base64" as const, + mime, + } } } finally { resized.free() diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index 391129672fa..85a19212c6e 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -437,6 +437,44 @@ describe("ReadTool", () => { }), ) + it.effect("accepts PNG candidates at the exact base64 limit and skips them one byte below", () => + Effect.gen(function* () { + const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node")) + const source = new photon.PhotonImage( + Uint8Array.from({ length: 16 * 4 }, () => 255), + 16, + 1, + ) + const content = { + uri: "file:///wide.png", + content: Buffer.from(source.get_bytes()).toString("base64"), + encoding: "base64" as const, + mime: "image/png", + } + source.free() + const image = yield* Image.Service + + for (const [maxWidth, padding] of [ + [4, "=="], + [5, "="], + [6, ""], + ] as const) { + yield* image.transform((draft) => draft.configure({ maxWidth, maxBase64Bytes: 1_024 })) + const candidate = yield* image.normalize("wide.png", content) + expect(candidate.mime).toBe("image/png") + expect(candidate.content.match(/=*$/)?.[0]).toBe(padding) + + yield* image.transform((draft) => draft.configure({ maxBase64Bytes: candidate.content.length })) + expect(yield* image.normalize("wide.png", content)).toEqual(candidate) + + yield* image.transform((draft) => draft.configure({ maxBase64Bytes: candidate.content.length - 1 })) + const smaller = yield* image.normalize("wide.png", content) + expect(smaller.mime).toBe("image/png") + expect(smaller.content.length).toBeLessThan(candidate.content.length) + } + }), + ) + it.effect("drops images that cannot fit max base64 bytes after resize attempts", () => Effect.gen(function* () { const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="