From 7b776f96c18003fccd64a65ca1d96b1826ee6a10 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Tue, 10 Nov 2026 11:50:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=2016=20=E4=BD=8D=20PNG=20?= =?UTF-8?q?=E5=8E=9F=E5=9B=BE=E5=92=8C=E8=89=B2=E5=BD=A9=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/canvas/photoshop-safe-download.ts | 4 +++- lib/storage/qiniu-image-archive.ts | 2 +- tests/routes/canvas-download-recovery.test.ts | 1 + tests/routes/canvas-download-speed.test.ts | 2 ++ tests/routes/image-archive-flow.test.ts | 7 +++++-- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/canvas/photoshop-safe-download.ts b/lib/canvas/photoshop-safe-download.ts index 8bd240dcd..72cc32e75 100644 --- a/lib/canvas/photoshop-safe-download.ts +++ b/lib/canvas/photoshop-safe-download.ts @@ -217,7 +217,9 @@ export async function convertImageToPhotoshopPng(bytes: Buffer): Promise throw new Error("无法解析图片尺寸"); } // RGB/RGBA, 8-bit, single-frame PNG is already compatible; preserve its exact bytes. - if (metadata.format === "png" && metadata.space === "srgb" && metadata.depth === "uchar" + const rgb = (metadata.space === "srgb" && metadata.depth === "uchar") + || (metadata.space === "rgb16" && metadata.depth === "ushort"); + if (metadata.format === "png" && rgb && !metadata.isPalette && (metadata.pages || 1) === 1 && (!metadata.orientation || metadata.orientation === 1)) return bytes; return image.rotate().toColorspace("srgb").png({ compressionLevel: 3, adaptiveFiltering: true }).toBuffer(); } diff --git a/lib/storage/qiniu-image-archive.ts b/lib/storage/qiniu-image-archive.ts index f9d17707a..2062693fd 100644 --- a/lib/storage/qiniu-image-archive.ts +++ b/lib/storage/qiniu-image-archive.ts @@ -29,7 +29,7 @@ function isDownloadReadyPng(prefix: Buffer) { if (prefix.length < 41 || !prefix.subarray(0, 8).equals(PNG_SIGNATURE) || prefix.readUInt32BE(8) !== 13 || prefix.toString("ascii", 12, 16) !== "IHDR" || !prefix.readUInt32BE(16) || !prefix.readUInt32BE(20) - || prefix[24] !== 8 || (prefix[25] !== 2 && prefix[25] !== 6)) return false; + || (prefix[24] !== 8 && prefix[24] !== 16) || (prefix[25] !== 2 && prefix[25] !== 6)) return false; // Animation and EXIF orientation need the existing full-image preparation. // An embedded color profile is fine: the browser gets the exact stored bytes. diff --git a/tests/routes/canvas-download-recovery.test.ts b/tests/routes/canvas-download-recovery.test.ts index ff086d343..831471ae1 100644 --- a/tests/routes/canvas-download-recovery.test.ts +++ b/tests/routes/canvas-download-recovery.test.ts @@ -124,6 +124,7 @@ test("requested downloads are prioritized with two workers before scanning backg }, "./image-byte-cache": { fetchImageBytes: async () => { active++; maximum = Math.max(maximum, active); await new Promise(resolve => releases.push(resolve)); active--; return { buffer: bytes, contentType: "image/png" }; }, cacheImageBytes: async () => {} }, "@/lib/canvas/photoshop-safe-download": { normalizeCanvasDownloadAsset: async () => ({ ok: true, bytes, contentType: "image/png" }) }, + "./qiniu-image-archive": { getReadyQiniuImageDownload: async () => undefined }, "./service": { uploadFile() { throw new Error("Compatible PNG must retain its object"); } }, })("lib/storage/image-download-preparation.ts"); worker.scheduleImageDownloadPreparation("one"); worker.scheduleImageDownloadPreparation("two"); worker.scheduleImageDownloadPreparation("three"); diff --git a/tests/routes/canvas-download-speed.test.ts b/tests/routes/canvas-download-speed.test.ts index a86023176..9aa01762a 100644 --- a/tests/routes/canvas-download-speed.test.ts +++ b/tests/routes/canvas-download-speed.test.ts @@ -106,6 +106,8 @@ test("compatible PNG remains byte-identical; JPEG becomes RGB PNG without resizi const module = serverLoader({})("lib/canvas/photoshop-safe-download.ts"); const bytes = await sharp({ create: { width: 32, height: 48, channels: 4, background: "#12345680" } }).png().toBuffer(); expect((await module.convertImageToPhotoshopPng(bytes)).equals(bytes)).toBe(true); + const rgb16 = await sharp(bytes).toColourspace("rgb16").png().toBuffer(); + expect((await module.convertImageToPhotoshopPng(rgb16)).equals(rgb16)).toBe(true); const jpeg = await sharp(bytes).jpeg().toBuffer(); const converted = await module.convertImageToPhotoshopPng(jpeg); const metadata = await sharp(converted).metadata(); diff --git a/tests/routes/image-archive-flow.test.ts b/tests/routes/image-archive-flow.test.ts index fce0db247..bf2012807 100644 --- a/tests/routes/image-archive-flow.test.ts +++ b/tests/routes/image-archive-flow.test.ts @@ -86,6 +86,7 @@ const loggerMock = { createLogger: () => ({ info() {}, warn() {}, error() {} }) test("Qiniu archives 98K originals without relaying full image bytes through the app", async () => { const image = sharp({ create: { width: 4, height: 4, channels: 3, background: "blue" } }); const rgb = await image.clone().withIccProfile("srgb").png().toBuffer(); + const rgb16 = await image.clone().toColourspace("rgb16").png().toBuffer(); const indexed = await image.clone().png({ palette: true }).toBuffer(); const config = { provider: "qiniu-kodo", region: "cn-east-1", bucket: "archive", accessKeyId: "test-key", secretAccessKey: "test-secret", publicUrl: "https://bucket.example" }; @@ -122,11 +123,13 @@ test("Qiniu archives 98K originals without relaying full image bytes through the })("lib/storage/qiniu-image-archive.ts"); const archived = await load.archiveImageThroughQiniu(source, id); expect(archived.downloadAsset).toMatchObject({ status: "ready", url: archiveUrl, bytes: rgb.length }); + stored = rgb16; + expect((await load.archiveImageThroughQiniu(source, id)).downloadAsset).toMatchObject({ status: "ready", url: archiveUrl, bytes: rgb16.length }); stored = indexed; expect((await load.archiveImageThroughQiniu(source, id)).downloadAsset).toBeUndefined(); expect(await load.archiveImageThroughQiniu("https://another-provider.example/result.png", id)).toBeNull(); - expect(cloudCalls).toHaveLength(2); - expect(reads).toHaveLength(2); + expect(cloudCalls).toHaveLength(3); + expect(reads).toHaveLength(3); }); test("160 requests from the same user and model start without admission waiting", async () => {