+2
-2
@@ -1,6 +0,6 @@ | ||
| import { type ImageMetadata, type ImageFormat, UnsupportedFormatError } from "./types.ts"; | ||
| import { type ImageMetadata, type ImageFormat, UnsupportedFormatError, DIGITAL_SOURCE_TYPE } from "./types.ts"; | ||
| import { serializeXmp } from "./xmp/serialize.ts"; | ||
| import { parseXmp } from "./xmp/parse.ts"; | ||
| export type { ImageMetadata, ImageFormat }; | ||
| export { UnsupportedFormatError }; | ||
| export { UnsupportedFormatError, DIGITAL_SOURCE_TYPE }; | ||
| export { serializeXmp, parseXmp }; | ||
@@ -7,0 +7,0 @@ /** Detect the container format from magic bytes. */ |
+2
-2
@@ -1,2 +0,2 @@ | ||
| import { UnsupportedFormatError, } from "./types.js"; | ||
| import { UnsupportedFormatError, DIGITAL_SOURCE_TYPE, } from "./types.js"; | ||
| import { isWebp, readWebpMetadata, writeWebpMetadata, removeWebpMetadata, } from "./formats/webp.js"; | ||
@@ -8,3 +8,3 @@ import { readAvifMetadata, writeAvifMetadata, removeAvifMetadata, } from "./formats/avif.js"; | ||
| import { parseXmp } from "./xmp/parse.js"; | ||
| export { UnsupportedFormatError }; | ||
| export { UnsupportedFormatError, DIGITAL_SOURCE_TYPE }; | ||
| export { serializeXmp, parseXmp }; | ||
@@ -11,0 +11,0 @@ function dataU32(buf, o) { |
+47
-1
@@ -7,3 +7,3 @@ /** | ||
| * library maps them onto the correct XMP namespaces (dc:, photoshop:, | ||
| * Iptc4xmpCore:, xmpRights:) under the hood. | ||
| * Iptc4xmpCore:, Iptc4xmpExt:, xmpRights:) under the hood. | ||
| */ | ||
@@ -41,3 +41,49 @@ export interface ImageMetadata { | ||
| copyrightNotice?: string; | ||
| /** | ||
| * How the image came to be — an IRI from the IPTC Digital Source Type | ||
| * vocabulary (https://cv.iptc.org/newscodes/digitalsourcetype/). Maps to | ||
| * Iptc4xmpExt:DigitalSourceType. This is the field ecosystems key off to | ||
| * label an image as AI-generated: set it to | ||
| * `DIGITAL_SOURCE_TYPE.trainedAlgorithmicMedia` alongside the `ai` fields. | ||
| */ | ||
| digitalSourceType?: string; | ||
| /** | ||
| * AI-generation provenance (IPTC Photo Metadata Standard 2025.1, Extension | ||
| * schema). Per IPTC guidance, also set `digitalSourceType` and leave | ||
| * `creator` empty for fully AI-generated images — the prompt writer is | ||
| * explicitly not the image creator. | ||
| */ | ||
| ai?: { | ||
| /** | ||
| * Prompt(s) given to the generative AI service, including negative | ||
| * prompts and model parameters if you wish. Maps to | ||
| * Iptc4xmpExt:AIPromptInformation. | ||
| */ | ||
| prompt?: string; | ||
| /** Name of the person who wrote the prompt. Maps to Iptc4xmpExt:AIPromptWriterName. */ | ||
| promptWriter?: string; | ||
| /** | ||
| * The AI engine and/or model used, e.g. "DALL-E via Bing Image Creator". | ||
| * Maps to Iptc4xmpExt:AISystemUsed. | ||
| */ | ||
| system?: string; | ||
| /** Version of the AI system, if known. Maps to Iptc4xmpExt:AISystemVersionUsed. */ | ||
| systemVersion?: string; | ||
| }; | ||
| } | ||
| /** | ||
| * Common IRIs from the IPTC "Digital Source Type" NewsCodes vocabulary, for | ||
| * use as `ImageMetadata.digitalSourceType`. Not exhaustive — any IRI from | ||
| * https://cv.iptc.org/newscodes/digitalsourcetype/ is valid. | ||
| */ | ||
| export declare const DIGITAL_SOURCE_TYPE: { | ||
| /** Generated purely by an AI model from prompts — the standard "AI-generated" disclosure. */ | ||
| readonly trainedAlgorithmicMedia: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"; | ||
| /** Composite that includes AI-generated elements. */ | ||
| readonly compositeWithTrainedAlgorithmicMedia: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"; | ||
| /** Composite of captured and synthetic elements. */ | ||
| readonly compositeSynthetic: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic"; | ||
| /** Original photograph from a digital camera. */ | ||
| readonly digitalCapture: "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"; | ||
| }; | ||
| export type ImageFormat = "webp" | "jpeg" | "png" | "avif" | "heic" | "unknown"; | ||
@@ -44,0 +90,0 @@ export declare class UnsupportedFormatError extends Error { |
+15
-0
@@ -0,1 +1,16 @@ | ||
| /** | ||
| * Common IRIs from the IPTC "Digital Source Type" NewsCodes vocabulary, for | ||
| * use as `ImageMetadata.digitalSourceType`. Not exhaustive — any IRI from | ||
| * https://cv.iptc.org/newscodes/digitalsourcetype/ is valid. | ||
| */ | ||
| export const DIGITAL_SOURCE_TYPE = { | ||
| /** Generated purely by an AI model from prompts — the standard "AI-generated" disclosure. */ | ||
| trainedAlgorithmicMedia: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", | ||
| /** Composite that includes AI-generated elements. */ | ||
| compositeWithTrainedAlgorithmicMedia: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia", | ||
| /** Composite of captured and synthetic elements. */ | ||
| compositeSynthetic: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic", | ||
| /** Original photograph from a digital camera. */ | ||
| digitalCapture: "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", | ||
| }; | ||
| export class UnsupportedFormatError extends Error { | ||
@@ -2,0 +17,0 @@ format; |
+19
-0
@@ -84,3 +84,22 @@ /** | ||
| } | ||
| const digitalSourceType = simple(xmp, "Iptc4xmpExt:DigitalSourceType"); | ||
| if (digitalSourceType) | ||
| meta.digitalSourceType = digitalSourceType; | ||
| // IPTC 2025.1 AI-generation provenance — only attach `ai` if any field is present. | ||
| const aiPrompt = simple(xmp, "Iptc4xmpExt:AIPromptInformation"); | ||
| const aiPromptWriter = simple(xmp, "Iptc4xmpExt:AIPromptWriterName"); | ||
| const aiSystem = simple(xmp, "Iptc4xmpExt:AISystemUsed"); | ||
| const aiSystemVersion = simple(xmp, "Iptc4xmpExt:AISystemVersionUsed"); | ||
| if (aiPrompt || aiPromptWriter || aiSystem || aiSystemVersion) { | ||
| meta.ai = {}; | ||
| if (aiPrompt) | ||
| meta.ai.prompt = aiPrompt; | ||
| if (aiPromptWriter) | ||
| meta.ai.promptWriter = aiPromptWriter; | ||
| if (aiSystem) | ||
| meta.ai.system = aiSystem; | ||
| if (aiSystemVersion) | ||
| meta.ai.systemVersion = aiSystemVersion; | ||
| } | ||
| return meta; | ||
| } |
@@ -15,2 +15,3 @@ /** | ||
| Iptc4xmpCore: "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", | ||
| Iptc4xmpExt: "http://iptc.org/std/Iptc4xmpExt/2008-02-29/", | ||
| plus: "http://ns.useplus.org/ldf/xmp/1.0/", | ||
@@ -33,2 +34,6 @@ }; | ||
| } | ||
| /** A simple text/URI property (photoshop:Credit, the Iptc4xmpExt AI fields). */ | ||
| function simpleEl(tag, value) { | ||
| return ` <${tag}>${esc(value)}</${tag}>\n`; | ||
| } | ||
| /** An unordered array property (dc:subject / keywords). */ | ||
@@ -81,9 +86,21 @@ function bag(tag, values) { | ||
| if (meta.credit) | ||
| props.push(` <photoshop:Credit>${esc(meta.credit)}</photoshop:Credit>\n`); | ||
| props.push(simpleEl("photoshop:Credit", meta.credit)); | ||
| if (meta.copyrightNotice) | ||
| props.push(` <photoshop:Copyright>${esc(meta.copyrightNotice)}</photoshop:Copyright>\n`); | ||
| props.push(simpleEl("photoshop:Copyright", meta.copyrightNotice)); | ||
| if (meta.licenseUrl) | ||
| props.push(` <xmpRights:WebStatement>${esc(meta.licenseUrl)}</xmpRights:WebStatement>\n`); | ||
| props.push(simpleEl("xmpRights:WebStatement", meta.licenseUrl)); | ||
| if (meta.licensor?.url) | ||
| props.push(licensorSeq(meta.licensor.url, meta.licensor.name)); | ||
| // IPTC Extension: digital source type + 2025.1 AI-generation provenance. | ||
| // All five are plain single-valued text/URI properties per the spec. | ||
| if (meta.digitalSourceType) | ||
| props.push(simpleEl("Iptc4xmpExt:DigitalSourceType", meta.digitalSourceType)); | ||
| if (meta.ai?.prompt) | ||
| props.push(simpleEl("Iptc4xmpExt:AIPromptInformation", meta.ai.prompt)); | ||
| if (meta.ai?.promptWriter) | ||
| props.push(simpleEl("Iptc4xmpExt:AIPromptWriterName", meta.ai.promptWriter)); | ||
| if (meta.ai?.system) | ||
| props.push(simpleEl("Iptc4xmpExt:AISystemUsed", meta.ai.system)); | ||
| if (meta.ai?.systemVersion) | ||
| props.push(simpleEl("Iptc4xmpExt:AISystemVersionUsed", meta.ai.systemVersion)); | ||
| const xmlns = Object.entries(NS) | ||
@@ -90,0 +107,0 @@ .map(([prefix, uri]) => ` xmlns:${prefix}="${uri}"`) |
+1
-1
| { | ||
| "name": "aeo-image", | ||
| "version": "1.1.1", | ||
| "version": "1.2.0", | ||
| "description": "Write descriptive + rights metadata (captions, keywords, alt text, creator, license) into WebP, AVIF, HEIC, JPEG & PNG — self-describing images that Google Images reads and recommends embedding, built for the AI-search era. The only pure-JS, zero-dependency library that writes XMP to AVIF/HEIC. Byte-preserving; runs on Node, Bun, Deno & edge.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+29
-3
@@ -50,3 +50,3 @@ # aeo-image | ||
| - 🖼️ **Byte-preserving** — splices metadata only; compressed image data is copied verbatim, never re-encoded. | ||
| - 🧠 **Semantic, AEO-oriented API** — you write `description`/`keywords`/`altText`, not raw tag IDs. We map them onto the correct XMP namespaces (`dc:`, `photoshop:`, `Iptc4xmpCore:`, `xmpRights:`). | ||
| - 🧠 **Semantic, AEO-oriented API** — you write `description`/`keywords`/`altText`, not raw tag IDs. We map them onto the correct XMP namespaces (`dc:`, `photoshop:`, `Iptc4xmpCore:`, `Iptc4xmpExt:`, `xmpRights:`). | ||
| - ☁️ **Runs anywhere** — Node, Deno, Bun, Cloudflare Workers, Vercel/Netlify/Lambda edge functions. No `fs` required; operates on buffers. | ||
@@ -82,3 +82,3 @@ - 🔒 **Privacy-friendly** — `removeMetadata()` strips XMP/EXIF in one call (keeps ICC colour profile). | ||
| Fields conform to the **[IPTC Photo Metadata Standard 2025.1](https://iptc.org/standards/photo-metadata/iptc-standard/)** (the current revision), specifically the descriptive, accessibility, and rights/licensing subset, across these namespaces: | ||
| Fields conform to the **[IPTC Photo Metadata Standard 2025.1](https://iptc.org/standards/photo-metadata/iptc-standard/)** (the current revision), specifically the descriptive, accessibility, rights/licensing, and AI-provenance subset, across these namespaces: | ||
@@ -89,2 +89,3 @@ | Namespace | Prefix | Used for | | ||
| | IPTC Core | `Iptc4xmpCore:` | `AltTextAccessibility` (IPTC **2021.1**+) | | ||
| | IPTC Extension | `Iptc4xmpExt:` | digital source type + AI-generation provenance (IPTC **2025.1**) | | ||
| | Adobe Photoshop | `photoshop:` | credit, copyright | | ||
@@ -94,3 +95,3 @@ | XMP Rights | `xmpRights:` | web statement (license URL) | | ||
| **Not yet implemented:** IPTC 2025.1's AI-generation provenance properties (AI Prompt Information, AI System Used, …) — tracked in the roadmap. | ||
| This includes IPTC 2025.1's four **AI-generation provenance** properties (`AIPromptInformation`, `AIPromptWriterName`, `AISystemUsed`, `AISystemVersionUsed`) plus `DigitalSourceType` — see [Label AI-generated images](#label-ai-generated-images). Readable by exiftool (named tags from **13.40**). C2PA / Content Credentials (cryptographically signed manifests) are a separate standard and out of scope. | ||
@@ -136,2 +137,25 @@ ## Install | ||
| ### Label AI-generated images | ||
| IPTC Photo Metadata 2025.1 added four AI-provenance fields, which pair with | ||
| `DigitalSourceType` — the field ecosystems read to label an image as | ||
| AI-generated: | ||
| ```ts | ||
| import { writeMetadata, DIGITAL_SOURCE_TYPE } from "aeo-image"; | ||
| const output = writeMetadata(input, { | ||
| description: "A neon-lit street market at night in the rain", | ||
| digitalSourceType: DIGITAL_SOURCE_TYPE.trainedAlgorithmicMedia, | ||
| ai: { | ||
| prompt: "neon street market, rain reflections, cinematic 35mm", | ||
| promptWriter: "Jane Doe", | ||
| system: "DALL-E via Bing Image Creator", | ||
| systemVersion: "3", | ||
| }, | ||
| // Per IPTC guidance, leave `creator` empty for fully AI-generated images — | ||
| // the prompt writer is explicitly not the image creator. | ||
| }); | ||
| ``` | ||
| ### Strip (privacy) | ||
@@ -176,2 +200,4 @@ | ||
| | `licensor` | `{ url, name? }` | IPTC PLUS `plus:Licensor` — *Google "Get this image" link* | | ||
| | `digitalSourceType` | `string` (IRI) | `Iptc4xmpExt:DigitalSourceType` — *AI-generated disclosure*; use `DIGITAL_SOURCE_TYPE.*` | | ||
| | `ai` | `{ prompt?, promptWriter?, system?, systemVersion? }` | IPTC 2025.1 `Iptc4xmpExt:AIPromptInformation` / `AIPromptWriterName` / `AISystemUsed` / `AISystemVersionUsed` | | ||
@@ -178,0 +204,0 @@ The last three implement the fields Google Images reads for the **Licensable** badge and license link. All functions return a **new** buffer and never mutate the input. See [`docs/xmp-fields.md`](docs/xmp-fields.md) for the complete field/namespace reference and AEO rationale. |
+2
-1
@@ -5,2 +5,3 @@ import { | ||
| UnsupportedFormatError, | ||
| DIGITAL_SOURCE_TYPE, | ||
| } from "./types.ts"; | ||
@@ -32,3 +33,3 @@ import { | ||
| export type { ImageMetadata, ImageFormat }; | ||
| export { UnsupportedFormatError }; | ||
| export { UnsupportedFormatError, DIGITAL_SOURCE_TYPE }; | ||
| export { serializeXmp, parseXmp }; | ||
@@ -35,0 +36,0 @@ |
+52
-1
@@ -7,3 +7,3 @@ /** | ||
| * library maps them onto the correct XMP namespaces (dc:, photoshop:, | ||
| * Iptc4xmpCore:, xmpRights:) under the hood. | ||
| * Iptc4xmpCore:, Iptc4xmpExt:, xmpRights:) under the hood. | ||
| */ | ||
@@ -38,4 +38,55 @@ export interface ImageMetadata { | ||
| copyrightNotice?: string; | ||
| /** | ||
| * How the image came to be — an IRI from the IPTC Digital Source Type | ||
| * vocabulary (https://cv.iptc.org/newscodes/digitalsourcetype/). Maps to | ||
| * Iptc4xmpExt:DigitalSourceType. This is the field ecosystems key off to | ||
| * label an image as AI-generated: set it to | ||
| * `DIGITAL_SOURCE_TYPE.trainedAlgorithmicMedia` alongside the `ai` fields. | ||
| */ | ||
| digitalSourceType?: string; | ||
| /** | ||
| * AI-generation provenance (IPTC Photo Metadata Standard 2025.1, Extension | ||
| * schema). Per IPTC guidance, also set `digitalSourceType` and leave | ||
| * `creator` empty for fully AI-generated images — the prompt writer is | ||
| * explicitly not the image creator. | ||
| */ | ||
| ai?: { | ||
| /** | ||
| * Prompt(s) given to the generative AI service, including negative | ||
| * prompts and model parameters if you wish. Maps to | ||
| * Iptc4xmpExt:AIPromptInformation. | ||
| */ | ||
| prompt?: string; | ||
| /** Name of the person who wrote the prompt. Maps to Iptc4xmpExt:AIPromptWriterName. */ | ||
| promptWriter?: string; | ||
| /** | ||
| * The AI engine and/or model used, e.g. "DALL-E via Bing Image Creator". | ||
| * Maps to Iptc4xmpExt:AISystemUsed. | ||
| */ | ||
| system?: string; | ||
| /** Version of the AI system, if known. Maps to Iptc4xmpExt:AISystemVersionUsed. */ | ||
| systemVersion?: string; | ||
| }; | ||
| } | ||
| /** | ||
| * Common IRIs from the IPTC "Digital Source Type" NewsCodes vocabulary, for | ||
| * use as `ImageMetadata.digitalSourceType`. Not exhaustive — any IRI from | ||
| * https://cv.iptc.org/newscodes/digitalsourcetype/ is valid. | ||
| */ | ||
| export const DIGITAL_SOURCE_TYPE = { | ||
| /** Generated purely by an AI model from prompts — the standard "AI-generated" disclosure. */ | ||
| trainedAlgorithmicMedia: | ||
| "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", | ||
| /** Composite that includes AI-generated elements. */ | ||
| compositeWithTrainedAlgorithmicMedia: | ||
| "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia", | ||
| /** Composite of captured and synthetic elements. */ | ||
| compositeSynthetic: | ||
| "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic", | ||
| /** Original photograph from a digital camera. */ | ||
| digitalCapture: | ||
| "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture", | ||
| } as const; | ||
| export type ImageFormat = | ||
@@ -42,0 +93,0 @@ | "webp" |
+16
-0
@@ -93,3 +93,19 @@ import type { ImageMetadata } from "../types.ts"; | ||
| const digitalSourceType = simple(xmp, "Iptc4xmpExt:DigitalSourceType"); | ||
| if (digitalSourceType) meta.digitalSourceType = digitalSourceType; | ||
| // IPTC 2025.1 AI-generation provenance — only attach `ai` if any field is present. | ||
| const aiPrompt = simple(xmp, "Iptc4xmpExt:AIPromptInformation"); | ||
| const aiPromptWriter = simple(xmp, "Iptc4xmpExt:AIPromptWriterName"); | ||
| const aiSystem = simple(xmp, "Iptc4xmpExt:AISystemUsed"); | ||
| const aiSystemVersion = simple(xmp, "Iptc4xmpExt:AISystemVersionUsed"); | ||
| if (aiPrompt || aiPromptWriter || aiSystem || aiSystemVersion) { | ||
| meta.ai = {}; | ||
| if (aiPrompt) meta.ai.prompt = aiPrompt; | ||
| if (aiPromptWriter) meta.ai.promptWriter = aiPromptWriter; | ||
| if (aiSystem) meta.ai.system = aiSystem; | ||
| if (aiSystemVersion) meta.ai.systemVersion = aiSystemVersion; | ||
| } | ||
| return meta; | ||
| } |
+21
-8
@@ -19,2 +19,3 @@ import type { ImageMetadata } from "../types.ts"; | ||
| Iptc4xmpCore: "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", | ||
| Iptc4xmpExt: "http://iptc.org/std/Iptc4xmpExt/2008-02-29/", | ||
| plus: "http://ns.useplus.org/ldf/xmp/1.0/", | ||
@@ -42,2 +43,7 @@ } as const; | ||
| /** A simple text/URI property (photoshop:Credit, the Iptc4xmpExt AI fields). */ | ||
| function simpleEl(tag: string, value: string): string { | ||
| return ` <${tag}>${esc(value)}</${tag}>\n`; | ||
| } | ||
| /** An unordered array property (dc:subject / keywords). */ | ||
@@ -90,14 +96,21 @@ function bag(tag: string, values: string[]): string { | ||
| props.push(altLang("Iptc4xmpCore:AltTextAccessibility", meta.altText)); | ||
| if (meta.credit) | ||
| props.push(` <photoshop:Credit>${esc(meta.credit)}</photoshop:Credit>\n`); | ||
| if (meta.credit) props.push(simpleEl("photoshop:Credit", meta.credit)); | ||
| if (meta.copyrightNotice) | ||
| props.push( | ||
| ` <photoshop:Copyright>${esc(meta.copyrightNotice)}</photoshop:Copyright>\n`, | ||
| ); | ||
| props.push(simpleEl("photoshop:Copyright", meta.copyrightNotice)); | ||
| if (meta.licenseUrl) | ||
| props.push( | ||
| ` <xmpRights:WebStatement>${esc(meta.licenseUrl)}</xmpRights:WebStatement>\n`, | ||
| ); | ||
| props.push(simpleEl("xmpRights:WebStatement", meta.licenseUrl)); | ||
| if (meta.licensor?.url) | ||
| props.push(licensorSeq(meta.licensor.url, meta.licensor.name)); | ||
| // IPTC Extension: digital source type + 2025.1 AI-generation provenance. | ||
| // All five are plain single-valued text/URI properties per the spec. | ||
| if (meta.digitalSourceType) | ||
| props.push(simpleEl("Iptc4xmpExt:DigitalSourceType", meta.digitalSourceType)); | ||
| if (meta.ai?.prompt) | ||
| props.push(simpleEl("Iptc4xmpExt:AIPromptInformation", meta.ai.prompt)); | ||
| if (meta.ai?.promptWriter) | ||
| props.push(simpleEl("Iptc4xmpExt:AIPromptWriterName", meta.ai.promptWriter)); | ||
| if (meta.ai?.system) | ||
| props.push(simpleEl("Iptc4xmpExt:AISystemUsed", meta.ai.system)); | ||
| if (meta.ai?.systemVersion) | ||
| props.push(simpleEl("Iptc4xmpExt:AISystemVersionUsed", meta.ai.systemVersion)); | ||
@@ -104,0 +117,0 @@ const xmlns = Object.entries(NS) |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
126459
9.12%2851
6.5%247
11.76%