From d176fad4da06626e07ac9a9654034a95709c59b3 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 27 Sep 2026 17:13:52 +0200 Subject: [PATCH] fix(nitro): repair stale meta.image in bundles that are already lossless A bundle whose texture is already VP8L was returned untouched, so a stale spritesheet.meta.image survived the normalisation and the client could not find the texture member. Rebuild the archive in that case and reuse the existing VP8L bytes instead of decoding them again. --- src/lib/services/swf/nitro-builder.ts | 19 +++++++++++++++++-- src/lib/services/swf/nitro-webp.test.ts | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/lib/services/swf/nitro-builder.ts b/src/lib/services/swf/nitro-builder.ts index 84ffd4db..f94f7658 100644 --- a/src/lib/services/swf/nitro-builder.ts +++ b/src/lib/services/swf/nitro-builder.ts @@ -520,14 +520,29 @@ function inflateJsonOrGzip( * renamed. `meta.image` is corrected by `createNitroBundle`. * * A bundle that is already lossless WebP is returned untouched, which keeps - * the operation idempotent and avoids a pointless re-encode. + * the operation idempotent and avoids a pointless re-encode — but only when + * its metadata is already consistent. A bundle whose `meta.image` names a + * member that is not in the file is still rebuilt, because a client that + * cannot find the texture renders nothing and a re-encode is not what fixes + * that. * * Throws if the texture claims to be PNG or WebP but cannot actually be * decoded; callers that would rather keep the original should catch. */ export async function toWebpLosslessBundle(bundle: Buffer): Promise { const parsed = parseNitroBundle(bundle); - if (isLosslessWebp(parsed.texture)) return bundle; + const meta = (parsed.json as { spritesheet?: { meta?: { image?: string } } }) + .spritesheet?.meta; + if (isLosslessWebp(parsed.texture)) { + if (meta?.image === parsed.textureFileName) return bundle; + // Stale metadata only: rebuild the archive with the pointer corrected + // and reuse the existing VP8L bytes rather than re-encoding them. + return createNitroBundle( + parsed.json, + parsed.texture, + parsed.textureFileName.replace(/\.(?:png|webp)$/i, ""), + ); + } const image = sharp(parsed.texture); const { width = 0, height = 0 } = await image.metadata(); diff --git a/src/lib/services/swf/nitro-webp.test.ts b/src/lib/services/swf/nitro-webp.test.ts index 36cd40ca..e4c6c83a 100644 --- a/src/lib/services/swf/nitro-webp.test.ts +++ b/src/lib/services/swf/nitro-webp.test.ts @@ -223,6 +223,22 @@ describe("normalising a bundle to WebP Lossless", () => { ); await expect(toWebpLosslessBundle(broken)).rejects.toThrow(); }); + + it("repairs stale meta.image in a bundle that is already lossless", async () => { + // Already VP8L, so there is nothing to re-encode — but the pointer + // names a member the archive does not contain, which a re-encode + // would not be what fixes. + const stale = await bundleWith("webp", "chair.png"); + expect(isLosslessWebp(parseNitroBundle(stale).texture)).toBe(true); + + const after = parseNitroBundle(await toWebpLosslessBundle(stale)); + expect(after.textureFileName).toBe("chair.webp"); + expect( + (after.json.spritesheet as { meta: { image: string } }).meta.image, + ).toBe("chair.webp"); + // The existing VP8L bytes are reused rather than decoded again. + expect(after.texture.equals(parseNitroBundle(stale).texture)).toBe(true); + }); }); describe("spritesheet encoding", () => {