feat: implement editMetadata operation in image-engine

This commit is contained in:
Siddharth Kumar Sah
2026-04-06 19:21:15 +08:00
parent 7fba05e1dc
commit 939bb04941
4 changed files with 182 additions and 0 deletions
+3
View File
@@ -5,6 +5,7 @@ import { compress } from "./operations/compress.js";
import { contrast } from "./operations/contrast.js";
import { convert } from "./operations/convert.js";
import { crop } from "./operations/crop.js";
import { editMetadata } from "./operations/edit-metadata.js";
import { flip } from "./operations/flip.js";
import { grayscale } from "./operations/grayscale.js";
import { invert } from "./operations/invert.js";
@@ -20,6 +21,7 @@ import type {
ContrastOptions,
ConvertOptions,
CropOptions,
EditMetadataOptions,
FlipOptions,
OperationResult,
OutputFormat,
@@ -54,6 +56,7 @@ const OPERATION_MAP: Record<
grayscale: (img) => grayscale(img),
sepia: (img) => sepia(img),
invert: (img) => invert(img),
"edit-metadata": (img, opts) => editMetadata(img, opts as unknown as EditMetadataOptions),
};
const FORMAT_MAP: Record<string, string> = {
+1
View File
@@ -6,6 +6,7 @@ export { compress } from "./operations/compress.js";
export { contrast } from "./operations/contrast.js";
export { convert } from "./operations/convert.js";
export { crop } from "./operations/crop.js";
export { editMetadata } from "./operations/edit-metadata.js";
export { flip } from "./operations/flip.js";
export { grayscale } from "./operations/grayscale.js";
export { invert } from "./operations/invert.js";
@@ -0,0 +1,91 @@
import exifReader from "exif-reader";
import type { EditMetadataOptions, Sharp } from "../types.js";
import { sanitizeValue } from "../utils/metadata.js";
const COMMON_FIELD_MAP: Array<{
option: keyof EditMetadataOptions;
ifd: "IFD0" | "IFD2";
tag: string;
}> = [
{ option: "artist", ifd: "IFD0", tag: "Artist" },
{ option: "copyright", ifd: "IFD0", tag: "Copyright" },
{ option: "imageDescription", ifd: "IFD0", tag: "ImageDescription" },
{ option: "software", ifd: "IFD0", tag: "Software" },
{ option: "dateTime", ifd: "IFD0", tag: "DateTime" },
{ option: "dateTimeOriginal", ifd: "IFD2", tag: "DateTimeOriginal" },
];
export async function editMetadata(
image: Sharp,
options: EditMetadataOptions = {},
): Promise<Sharp> {
const edits: { IFD0: Record<string, string>; IFD2: Record<string, string> } = {
IFD0: {},
IFD2: {},
};
for (const { option, ifd, tag } of COMMON_FIELD_MAP) {
const value = options[option];
if (typeof value === "string" && value.length > 0) {
edits[ifd][tag] = value;
}
}
const writtenTags = new Set([...Object.keys(edits.IFD0), ...Object.keys(edits.IFD2)]);
const fieldsToRemove = (options.fieldsToRemove ?? []).filter((f) => !writtenTags.has(f));
const hasEdits = Object.keys(edits.IFD0).length > 0 || Object.keys(edits.IFD2).length > 0;
const hasRemovals = fieldsToRemove.length > 0 || options.clearGps === true;
if (!hasEdits && !hasRemovals) {
return image.keepMetadata();
}
if (hasRemovals) {
const metadata = await image.metadata();
const existingIFD0: Record<string, string> = {};
const existingIFD2: Record<string, string> = {};
if (metadata.exif) {
try {
const parsed = exifReader(metadata.exif);
if (parsed.Image) {
for (const [k, v] of Object.entries(parsed.Image)) {
if (fieldsToRemove.includes(k)) continue;
const sv = sanitizeValue(v);
if (typeof sv === "string" || typeof sv === "number") {
existingIFD0[k] = String(sv);
}
}
}
if (parsed.Photo) {
for (const [k, v] of Object.entries(parsed.Photo)) {
if (fieldsToRemove.includes(k)) continue;
const sv = sanitizeValue(v);
if (typeof sv === "string" || typeof sv === "number") {
existingIFD2[k] = String(sv);
}
}
}
} catch {
// If parsing fails, proceed with just the edits
}
}
const finalIFD0 = { ...existingIFD0, ...edits.IFD0 };
const finalIFD2 = { ...existingIFD2, ...edits.IFD2 };
const exif: Record<string, Record<string, string>> = {};
if (Object.keys(finalIFD0).length > 0) exif.IFD0 = finalIFD0;
if (Object.keys(finalIFD2).length > 0) exif.IFD2 = finalIFD2;
return image.withExif(exif);
}
const exif: Record<string, Record<string, string>> = {};
if (Object.keys(edits.IFD0).length > 0) exif.IFD0 = edits.IFD0;
if (Object.keys(edits.IFD2).length > 0) exif.IFD2 = edits.IFD2;
return image.withExifMerge(exif);
}