diff --git a/src/media/web-media.test.ts b/src/media/web-media.test.ts index 03dad5c37990..dbdb2bffb35a 100644 --- a/src/media/web-media.test.ts +++ b/src/media/web-media.test.ts @@ -13,7 +13,7 @@ import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import { withEnvAsync } from "../test-utils/env.js"; -import { resizeToJpeg } from "./media-services.js"; +import { createImageProcessor, resizeToJpeg } from "./media-services.js"; import { encodePngRgba, fillPixel } from "./png-encode.js"; let effectiveImageBytesCap: typeof import("./web-media.js").effectiveImageBytesCap; @@ -488,6 +488,35 @@ describe("loadWebMedia", () => { } }); + it("renames transparent WebP images converted to PNG across direct and local image owners", async () => { + const { optimizeImageBufferForWebMedia } = await import("./web-media.js"); + const sourcePng = createLargeTransparentColorBlockPng(64); + const sourceWebp = (await createImageProcessor().encode(sourcePng, { format: "webp" })).data; + const imageCompression = { models: [{ maxSidePx: 32, preferredSidePx: 32 }] }; + + const direct = await optimizeImageBufferForWebMedia({ + buffer: sourceWebp, + contentType: "image/webp", + fileName: "portrait.WebP", + maxBytes: 1024 * 1024, + imageCompression, + }); + const convertedPath = path.join(fixtureRoot, "portrait.WebP"); + await fs.writeFile(convertedPath, sourceWebp); + const loaded = await loadWebMedia(convertedPath, { + maxBytes: 1024 * 1024, + localRoots: [fixtureRoot], + imageCompression, + }); + + for (const result of [direct, loaded]) { + expect(result.kind).toBe("image"); + expect(result.contentType).toBe("image/png"); + expect(result.fileName).toBe("portrait.png"); + expect(readPngDimensions(result.buffer)).toEqual({ width: 32, height: 32 }); + } + }); + it("applies model image maxBytes to the effective image cap", async () => { await expect( loadWebMediaRaw(tinyPngFile, { diff --git a/src/media/web-media.ts b/src/media/web-media.ts index 7134af32d6eb..69d3e1be9936 100644 --- a/src/media/web-media.ts +++ b/src/media/web-media.ts @@ -604,7 +604,7 @@ function assertHostReadMediaAllowed(params: { ); } -function toJpegFileName(fileName?: string): string | undefined { +function toImageFileName(fileName: string | undefined, mimeType: string): string | undefined { if (!fileName) { return undefined; } @@ -613,10 +613,10 @@ function toJpegFileName(fileName?: string): string | undefined { return fileName; } const parsed = path.parse(trimmed); - if (!parsed.ext || HEIC_EXT_RE.test(parsed.ext)) { - return path.format({ dir: parsed.dir, name: parsed.name || trimmed, ext: ".jpg" }); - } - return path.format({ dir: parsed.dir, name: parsed.name, ext: ".jpg" }); + const ext = extensionForMime(mimeType); + return mimeType !== "image/jpeg" && parsed.ext.toLowerCase() === ext + ? fileName + : path.format({ dir: parsed.dir, name: parsed.name || trimmed, ext }); } type OptimizedImage = { @@ -994,7 +994,7 @@ export async function optimizeImageBufferForWebMedia(params: { buffer: optimized.buffer, contentType: optimized.mimeType, kind: "image", - fileName: optimized.format === "jpeg" ? toJpegFileName(params.fileName) : params.fileName, + fileName: toImageFileName(params.fileName, optimized.mimeType), }; } @@ -1054,7 +1054,7 @@ async function loadWebMediaInternal( throw new Error(formatCapReduce("Media", cap, optimized.buffer.length)); } - const fileName = optimized.format === "jpeg" ? toJpegFileName(meta?.fileName) : meta?.fileName; + const fileName = toImageFileName(meta?.fileName, optimized.mimeType); return { buffer: optimized.buffer,