mirror of https://github.com/VERT-sh/VERT.git
fix: metadata for images
also makes conversion more performant by not recreating the image as a png blob
This commit is contained in:
parent
6001f7e8c3
commit
04e4cbef2e
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
ImageMagick,
|
|
||||||
initializeImageMagick,
|
initializeImageMagick,
|
||||||
MagickFormat,
|
MagickFormat,
|
||||||
MagickImage,
|
MagickImage,
|
||||||
|
|
@ -11,6 +10,7 @@ import { makeZip } from "client-zip";
|
||||||
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||||
import { parseAni } from "$lib/parse/ani";
|
import { parseAni } from "$lib/parse/ani";
|
||||||
import { parseIcns } from "vert-wasm";
|
import { parseIcns } from "vert-wasm";
|
||||||
|
import { log } from "$lib/logger";
|
||||||
|
|
||||||
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
|
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
|
||||||
|
|
||||||
|
|
@ -67,7 +67,11 @@ const handleMessage = async (message: any): Promise<any> => {
|
||||||
const convertedImgs: Uint8Array[] = [];
|
const convertedImgs: Uint8Array[] = [];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
imgs.map(async (img, i) => {
|
imgs.map(async (img, i) => {
|
||||||
const output = await magickConvert(img, message.to, compression);
|
const output = await magickConvert(
|
||||||
|
img,
|
||||||
|
message.to,
|
||||||
|
compression,
|
||||||
|
);
|
||||||
convertedImgs[i] = output;
|
convertedImgs[i] = output;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -94,7 +98,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
||||||
zip: true,
|
zip: true,
|
||||||
};
|
};
|
||||||
} else if (message.input.from === ".ani") {
|
} else if (message.input.from === ".ani") {
|
||||||
console.log("Parsing ANI file");
|
log(["workers", "imagemagick"], "Parsing ANI file")
|
||||||
try {
|
try {
|
||||||
const parsedAni = parseAni(new Uint8Array(buffer));
|
const parsedAni = parseAni(new Uint8Array(buffer));
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
|
|
@ -108,7 +112,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
message.to,
|
message.to,
|
||||||
compression
|
compression,
|
||||||
);
|
);
|
||||||
files.push(
|
files.push(
|
||||||
new File(
|
new File(
|
||||||
|
|
@ -158,7 +162,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
||||||
const converted = await magickConvert(
|
const converted = await magickConvert(
|
||||||
img,
|
img,
|
||||||
message.to,
|
message.to,
|
||||||
compression
|
compression,
|
||||||
);
|
);
|
||||||
outputs.push(converted);
|
outputs.push(converted);
|
||||||
break;
|
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 {
|
return {
|
||||||
type: "finished",
|
type: "finished",
|
||||||
|
|
@ -248,82 +306,31 @@ const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
|
||||||
return new Uint8Array(arrayBuffer);
|
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 (
|
const magickConvert = async (
|
||||||
img: IMagickImage,
|
img: IMagickImage,
|
||||||
to: string,
|
to: string,
|
||||||
compression?: number,
|
compression?: number,
|
||||||
|
originalMetadata?: Map<string, string>,
|
||||||
) => {
|
) => {
|
||||||
const intermediary = await magickToBlob(img);
|
|
||||||
const buf = new Uint8Array(await intermediary.arrayBuffer());
|
|
||||||
let fmt = to.slice(1).toUpperCase();
|
let fmt = to.slice(1).toUpperCase();
|
||||||
if (fmt === "JFIF") fmt = "JPEG";
|
if (fmt === "JFIF") fmt = "JPEG";
|
||||||
|
|
||||||
const result = await new Promise<Uint8Array>((resolve) => {
|
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)
|
||||||
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
|
if (compression) img.quality = compression;
|
||||||
if (compression) image.quality = compression;
|
|
||||||
image.write(fmt as unknown as MagickFormat, (o) => {
|
if (originalMetadata) {
|
||||||
resolve(structuredClone(o));
|
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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue