保留 16 位 PNG 原图和色彩信息

This commit is contained in:
WuYongJie 2026-11-10 11:50:00 +08:00
parent 17d5f6fec9
commit 7b776f96c1
5 changed files with 12 additions and 4 deletions

View File

@ -217,7 +217,9 @@ export async function convertImageToPhotoshopPng(bytes: Buffer): Promise<Buffer>
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();
}

View File

@ -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.

View File

@ -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<void>(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");

View File

@ -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();

View File

@ -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 () => {