fix: metadata for images

also makes conversion more performant by not recreating the image as a png blob
This commit is contained in:
Maya 2025-09-06 22:19:27 +08:00
parent 6001f7e8c3
commit 04e4cbef2e
1 changed files with 79 additions and 72 deletions

View File

@ -1,5 +1,4 @@
import {
ImageMagick,
initializeImageMagick,
MagickFormat,
MagickImage,
@ -11,6 +10,7 @@ import { makeZip } from "client-zip";
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
import { parseAni } from "$lib/parse/ani";
import { parseIcns } from "vert-wasm";
import { log } from "$lib/logger";
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
@ -67,7 +67,11 @@ const handleMessage = async (message: any): Promise<any> => {
const convertedImgs: Uint8Array[] = [];
await Promise.all(
imgs.map(async (img, i) => {
const output = await magickConvert(img, message.to, compression);
const output = await magickConvert(
img,
message.to,
compression,
);
convertedImgs[i] = output;
}),
);
@ -94,7 +98,7 @@ const handleMessage = async (message: any): Promise<any> => {
zip: true,
};
} else if (message.input.from === ".ani") {
console.log("Parsing ANI file");
log(["workers", "imagemagick"], "Parsing ANI file")
try {
const parsedAni = parseAni(new Uint8Array(buffer));
const files: File[] = [];
@ -108,7 +112,7 @@ const handleMessage = async (message: any): Promise<any> => {
}),
),
message.to,
compression
compression,
);
files.push(
new File(
@ -158,7 +162,7 @@ const handleMessage = async (message: any): Promise<any> => {
const converted = await magickConvert(
img,
message.to,
compression
compression,
);
outputs.push(converted);
break;
@ -222,7 +226,61 @@ const handleMessage = async (message: any): Promise<any> => {
}),
);
const converted = await magickConvert(img, message.to, compression);
// extract metadata
let metadata: Map<string, string> | undefined;
try {
metadata = new Map();
const exifProfile = img.getProfile("exif");
if (exifProfile) {
metadata.set("exif:profile", "true");
}
const iccProfile = img.getProfile("icc");
if (iccProfile) {
metadata.set("icc:profile", "true");
}
const attributeNames = img.attributeNames;
if (attributeNames && attributeNames.length > 0) {
for (const attrName of attributeNames) {
try {
if (
attrName.startsWith("exif:") ||
attrName.startsWith("icc:") ||
attrName.startsWith("date:") ||
attrName.startsWith("tiff:") ||
attrName.startsWith("xmp:") ||
attrName.startsWith("iptc:")
) {
const value = img.getAttribute(attrName);
if (value) {
metadata.set(attrName, value);
}
}
} catch {
// do nothing
}
}
}
if (metadata.size === 0) {
metadata = undefined;
}
log(["workers", "imagemagick"], `Parsed ${metadata.size} metadata values`)
} catch (e) {
console.warn("Failed to extract metadata:", e);
metadata = undefined;
}
const converted = await magickConvert(
img,
message.to,
compression,
metadata,
);
return {
type: "finished",
@ -248,82 +306,31 @@ const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
return new Uint8Array(arrayBuffer);
};
const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
const canvas = new OffscreenCanvas(img.width, img.height);
return new Promise<Blob>((resolve, reject) =>
img.getPixels(async (p) => {
// const area = p.getArea(0, 0, img.width, img.height);
// const chunkSize = img.hasAlpha ? 4 : 3;
// const chunks = Math.ceil(area.length / chunkSize);
// const data = new Uint8ClampedArray(chunks * 4);
// for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) {
// data[k] = area[j];
// data[k + 1] = area[j + 1];
// data[k + 2] = area[j + 2];
// data[k + 3] = img.hasAlpha ? area[j + 3] : 255;
// }
// const ctx = canvas.getContext("2d");
// if (!ctx) {
// reject(new Error("Failed to get canvas context"));
// return;
// }
// console.log(img.width, img.height);
// console.log(data.length, img.width * img.height * 4);
// ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0);
// const blob = await canvas.convertToBlob({
// type: "image/png",
// });
const data = p.toByteArray(0, 0, img.width, img.height, "RGBA");
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Failed to get canvas context"));
return;
}
if (!data) {
reject(new Error("Pixel data is null"));
return;
}
const imageData = new ImageData(
new Uint8ClampedArray(data),
img.width,
img.height,
);
ctx.putImageData(imageData, 0, 0);
const blob = await canvas.convertToBlob({
type: "image/png",
});
resolve(blob);
}),
);
};
const magickConvert = async (
img: IMagickImage,
to: string,
compression?: number,
originalMetadata?: Map<string, string>,
) => {
const intermediary = await magickToBlob(img);
const buf = new Uint8Array(await intermediary.arrayBuffer());
let fmt = to.slice(1).toUpperCase();
if (fmt === "JFIF") fmt = "JPEG";
const result = await new Promise<Uint8Array>((resolve) => {
ImageMagick.read(buf, MagickFormat.Png, (image) => {
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
if (compression) image.quality = compression;
image.write(fmt as unknown as MagickFormat, (o) => {
resolve(structuredClone(o));
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
if (compression) img.quality = compression;
if (originalMetadata) {
originalMetadata.forEach((value, key) => {
try {
if (!key.endsWith(":profile")) img.setAttribute(key, value);
} catch (e) {
console.warn(`Failed to set metadata ${key}: ${e}`);
}
});
}
img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {
resolve(structuredClone(o));
});
});