@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +3 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
- package/src/lib/app-utils/media-embedded-fields.js +619 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +12 -0
- package/src/lib/app-utils/name-search.js +11 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/server.d.ts +1 -0
- package/src/server.js +7 -0
- package/src/server.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-embedded-fields.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The metadata a file carries INSIDE its own bytes (AGL-3331): EXIF, IPTC\n * and XMP on a photo, the document info of a PDF, the core and custom\n * properties of an Office file, the tags of a video container.\n *\n * ## Why this module is client-safe and the parsers are not\n *\n * The parsers and writers live in `media-embedded-metadata/` and are exposed\n * only through `@aglyn/aglyn/server` — they need `node:zlib` and\n * `node:crypto`, and nothing in a browser ever holds the bytes they read.\n * What the console needs is the SHAPE of what they produced (it is stored on\n * the media document) and the rules for presenting and editing it, which is\n * everything here. Keeping the two apart keeps a PDF parser out of the\n * console bundle.\n *\n * ## Canonical keys\n *\n * A photo's caption can be written in three places at once — XMP\n * `dc:description`, IPTC 2:120 and EXIF `ImageDescription` — and a PDF's in\n * two. The reader folds every spelling of one idea into one canonical key\n * ({@link MEDIA_EMBEDDED_CATALOG}), so the drawer shows ONE \"Description\"\n * rather than three rows that may or may not agree, and the writer fans an\n * edit back out to every place that already holds it (the Metadata Working\n * Group's rule: never leave a stale copy for the next reader to prefer).\n *\n * Anything a file carries that is not in the catalog — an XMP property from\n * a DAM we have never heard of, a PDF custom property, a PNG text chunk —\n * still surfaces, under a namespaced \"other\" key ({@link otherEmbeddedKey})\n * that round-trips to the writer unchanged.\n */\n\n/** Where a value was read from. Several sources can back one field. */\nexport type MediaEmbeddedSource =\n | 'xmp'\n | 'iptc'\n | 'exif'\n | 'icc'\n | 'png'\n | 'comment'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n | 'svg'\n\n/** How the drawer groups fields, in display order. */\nexport type MediaEmbeddedGroup =\n | 'description'\n | 'rights'\n | 'location'\n | 'capture'\n | 'document'\n | 'technical'\n | 'other'\n\nexport const MEDIA_EMBEDDED_GROUP_ORDER: readonly MediaEmbeddedGroup[] = [\n 'description',\n 'rights',\n 'location',\n 'capture',\n 'document',\n 'technical',\n 'other',\n]\n\nexport const MEDIA_EMBEDDED_GROUP_LABELS: Record<MediaEmbeddedGroup, string> = {\n description: 'Description',\n rights: 'Rights',\n location: 'Location',\n capture: 'Camera',\n document: 'Document',\n technical: 'Technical',\n other: 'Other embedded fields',\n}\n\n/**\n * The container a file was read as. `null`-free: a file whose format has no\n * reader is simply not given an `embeddedMetadata` record at all.\n */\nexport type MediaEmbeddedFormat =\n | 'jpeg'\n | 'png'\n | 'webp'\n | 'gif'\n | 'tiff'\n | 'heif'\n | 'svg'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n\n/**\n * The formats whose bytes can take an edit without being re-encoded.\n *\n * Every writer here rewrites METADATA containers only — JPEG APP segments,\n * PNG ancillary chunks, WebP RIFF chunks, a TIFF's IFD, a PDF incremental\n * update, an Office package's property parts — and copies the image data,\n * page content and every other part through byte for byte. A format that\n * cannot promise that (a video's `moov` would move its sample offsets; an\n * SVG is sanitized on every ingress and is its own markup) is read-only.\n */\nexport const MEDIA_EMBEDDED_WRITABLE_FORMATS: ReadonlySet<MediaEmbeddedFormat> =\n new Set(['jpeg', 'png', 'webp', 'tiff', 'pdf', 'ooxml'])\n\n/** How a value is typed, which decides the editor and the validation. */\nexport type MediaEmbeddedValueKind =\n | 'text'\n | 'longText'\n | 'list'\n | 'date'\n | 'rating'\n | 'gps'\n\nexport interface MediaEmbeddedCatalogEntry {\n label: string\n group: MediaEmbeddedGroup\n kind: MediaEmbeddedValueKind\n /**\n * The formats an edit of this key can be written into. Absent means the\n * key is read-only everywhere — a measured fact such as an exposure time\n * or a page count, which describes the bytes rather than annotating them.\n */\n writable?: readonly MediaEmbeddedFormat[]\n /**\n * `gps` only: the one edit offered is removal. Moving a photo's recorded\n * location is not a thing anybody needs from a DAM; taking it OFF a photo\n * before it goes on a public page is.\n */\n removeOnly?: boolean\n /** Format-specific label, where a format names the idea differently. */\n labels?: Partial<Record<MediaEmbeddedFormat, string>>\n /** Format-specific group — a PDF's creation date is not a camera fact. */\n groups?: Partial<Record<MediaEmbeddedFormat, MediaEmbeddedGroup>>\n}\n\nconst IMAGE_WRITABLE: readonly MediaEmbeddedFormat[] = [\n 'jpeg',\n 'png',\n 'webp',\n 'tiff',\n]\nconst IMAGE_AND_PDF: readonly MediaEmbeddedFormat[] = [...IMAGE_WRITABLE, 'pdf']\nconst IMAGE_PDF_OOXML: readonly MediaEmbeddedFormat[] = [\n ...IMAGE_AND_PDF,\n 'ooxml',\n]\n\n/**\n * Every canonical key, in display order within its group.\n *\n * ## Value formats every reader and writer agrees on\n *\n * - `date`: ISO 8601 — `2021-05-03T10:11:12`, with `Z` or `±HH:MM` only when\n * the source recorded a zone (EXIF 2.3's `OffsetTimeOriginal`, a PDF\n * date's `+05'00'`). Never invented: a local camera time stays local.\n * - `list`: an array of strings (creators, keywords).\n * - `gps`: `\"<lat>,<lon>\"` in signed decimal degrees, six places.\n * - `rating`: `\"0\"`–`\"5\"`, XMP's scale; `\"-1\"` is XMP's \"rejected\".\n * - technical values are display strings already (`1/250`, `f/2.8`,\n * `35 mm`) because nothing edits them.\n */\nexport const MEDIA_EMBEDDED_CATALOG = {\n title: {\n label: 'Title',\n group: 'description',\n kind: 'text',\n writable: IMAGE_PDF_OOXML,\n },\n headline: {\n label: 'Headline',\n group: 'description',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n description: {\n label: 'Description',\n group: 'description',\n kind: 'longText',\n writable: IMAGE_PDF_OOXML,\n labels: { pdf: 'Subject', ooxml: 'Comments' },\n },\n subject: {\n label: 'Subject',\n group: 'description',\n kind: 'text',\n writable: ['ooxml'],\n },\n keywords: {\n label: 'Keywords',\n group: 'description',\n kind: 'list',\n writable: IMAGE_PDF_OOXML,\n },\n category: {\n label: 'Category',\n group: 'description',\n kind: 'text',\n writable: ['ooxml'],\n },\n comment: {\n label: 'Comment',\n group: 'description',\n kind: 'longText',\n writable: ['png'],\n },\n instructions: {\n label: 'Instructions',\n group: 'description',\n kind: 'longText',\n writable: IMAGE_WRITABLE,\n },\n label: {\n label: 'Label',\n group: 'description',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n rating: {\n label: 'Rating',\n group: 'description',\n kind: 'rating',\n writable: IMAGE_WRITABLE,\n },\n creator: {\n label: 'Creator',\n group: 'rights',\n kind: 'list',\n writable: IMAGE_PDF_OOXML,\n labels: { pdf: 'Author', ooxml: 'Author' },\n },\n copyright: {\n label: 'Copyright',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n credit: {\n label: 'Credit line',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n source: {\n label: 'Source',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n usageTerms: {\n label: 'Usage terms',\n group: 'rights',\n kind: 'longText',\n writable: IMAGE_WRITABLE,\n },\n webStatement: {\n label: 'Copyright info URL',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n city: {\n label: 'City',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n state: {\n label: 'State / province',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n country: {\n label: 'Country',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n gps: {\n label: 'GPS position',\n group: 'location',\n kind: 'gps',\n writable: IMAGE_WRITABLE,\n removeOnly: true,\n },\n gpsAltitude: { label: 'Altitude', group: 'location', kind: 'text' },\n createdAt: {\n label: 'Date taken',\n group: 'capture',\n kind: 'date',\n writable: IMAGE_AND_PDF,\n labels: {\n pdf: 'Created',\n ooxml: 'Created',\n svg: 'Created',\n mp4: 'Recorded',\n webm: 'Recorded',\n },\n groups: { pdf: 'document', ooxml: 'document', svg: 'document' },\n },\n make: {\n label: 'Camera make',\n group: 'capture',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n model: {\n label: 'Camera model',\n group: 'capture',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n lens: { label: 'Lens', group: 'capture', kind: 'text', writable: IMAGE_WRITABLE },\n exposure: { label: 'Exposure', group: 'capture', kind: 'text' },\n aperture: { label: 'Aperture', group: 'capture', kind: 'text' },\n iso: { label: 'ISO', group: 'capture', kind: 'text' },\n focalLength: { label: 'Focal length', group: 'capture', kind: 'text' },\n flash: { label: 'Flash', group: 'capture', kind: 'text' },\n modifiedAt: {\n label: 'Modified',\n group: 'document',\n kind: 'date',\n },\n software: {\n label: 'Software',\n group: 'document',\n kind: 'text',\n writable: IMAGE_AND_PDF,\n labels: { pdf: 'Created with', ooxml: 'Application' },\n },\n producer: {\n label: 'PDF producer',\n group: 'document',\n kind: 'text',\n writable: ['pdf'],\n },\n lastModifiedBy: { label: 'Last modified by', group: 'document', kind: 'text' },\n company: { label: 'Company', group: 'document', kind: 'text' },\n pageCount: {\n label: 'Pages',\n group: 'document',\n kind: 'text',\n labels: { ooxml: 'Pages / slides' },\n },\n pdfVersion: { label: 'PDF version', group: 'document', kind: 'text' },\n orientation: { label: 'Orientation', group: 'technical', kind: 'text' },\n colorProfile: { label: 'Color profile', group: 'technical', kind: 'text' },\n encoder: { label: 'Encoder', group: 'technical', kind: 'text' },\n} as const satisfies Record<string, MediaEmbeddedCatalogEntry>\n\nexport type MediaEmbeddedCanonicalKey = keyof typeof MEDIA_EMBEDDED_CATALOG\n\n/**\n * The namespaces an \"other\" key can come from. The key round-trips from\n * reader to writer verbatim, so its only job is to be unambiguous:\n *\n * - `xmp|<namespace URI>|<local name>` — a simple XMP property outside the\n * catalog. Keyed by URI, not prefix, because two files may bind the same\n * namespace to different prefixes and must still produce one key.\n * - `png|<keyword>` — a PNG `tEXt`/`zTXt`/`iTXt` chunk.\n * - `pdf|<Name>` — a PDF document-info entry (Acrobat's \"Custom\" tab).\n * - `ooxml|<name>` — an Office custom document property.\n * - `mp4|<atom or freeform name>`, `webm|<TAG>` — video tags, read-only.\n */\nexport type MediaEmbeddedOtherNamespace =\n | 'xmp'\n | 'png'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n\nexport function otherEmbeddedKey(\n namespace: MediaEmbeddedOtherNamespace,\n ...parts: string[]\n): string {\n return [namespace, ...parts].join('|')\n}\n\nexport function parseOtherEmbeddedKey(\n key: string,\n): { namespace: MediaEmbeddedOtherNamespace; parts: string[] } | null {\n const [namespace = '', ...parts] = key.split('|')\n if (!parts.length) return null\n if (!['xmp', 'png', 'pdf', 'ooxml', 'mp4', 'webm'].includes(namespace))\n return null\n return { namespace: namespace as MediaEmbeddedOtherNamespace, parts }\n}\n\n/** The \"other\" namespaces a writer accepts edits for, by format. */\nconst OTHER_WRITABLE: Partial<\n Record<MediaEmbeddedOtherNamespace, readonly MediaEmbeddedFormat[]>\n> = {\n xmp: IMAGE_WRITABLE,\n png: ['png'],\n pdf: ['pdf'],\n ooxml: ['ooxml'],\n}\n\n/** One field as stored on the media document and shown in the drawer. */\nexport interface MediaEmbeddedField {\n /** A {@link MediaEmbeddedCanonicalKey} or an {@link otherEmbeddedKey}. */\n key: string\n label: string\n group: MediaEmbeddedGroup\n /** Single-valued fields. Absent when `values` is set. */\n value?: string\n /** List-valued fields (creators, keywords). */\n values?: string[]\n /** Every block the value was found in, strongest first. */\n sources: MediaEmbeddedSource[]\n /** Whether the drawer may offer an edit for it on this file. */\n editable: boolean\n}\n\n/**\n * `embeddedMetadata` on a media document (AGL-3331). Server-written only —\n * the Firestore rules lock it like every other fact read from the bytes.\n */\nexport interface MediaEmbeddedMetadata {\n /** Bumped when the reader's output changes shape, so a sweep can re-read. */\n version: number\n format: MediaEmbeddedFormat\n fields: MediaEmbeddedField[]\n /** Whether an edit can be written back into this file at all. */\n writable: boolean\n /** Why not, in a sentence for the drawer, when `writable` is false. */\n readOnlyReason?: string\n /** True when fields were dropped to stay inside the stored caps. */\n truncated?: boolean\n /**\n * The `contentSha256` of the bytes this was read from. A record whose\n * digest no longer matches the document's is stale — the file was\n * replaced by a path that did not re-read it — and is re-read on open.\n */\n contentSha256?: string\n}\n\n/** The reader's output version. See {@link MediaEmbeddedMetadata.version}. */\nexport const MEDIA_EMBEDDED_METADATA_VERSION = 1\n\n/** Stored caps: one media document must never approach Firestore's 1 MiB. */\nexport const MEDIA_EMBEDDED_MAX_FIELDS = 150\nexport const MEDIA_EMBEDDED_MAX_VALUE_LENGTH = 2000\nexport const MEDIA_EMBEDDED_MAX_LIST_ITEMS = 100\n/** A single keyword or creator; IPTC's own limit for keywords is 64 bytes. */\nexport const MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH = 256\n\n/**\n * An edit to a file's embedded metadata: key → new value, or `null` to\n * remove it from the file. A list-valued key takes an array.\n */\nexport type MediaEmbeddedPatch = Record<string, string | string[] | null>\n\nexport function isCanonicalEmbeddedKey(\n key: string,\n): key is MediaEmbeddedCanonicalKey {\n return Object.prototype.hasOwnProperty.call(MEDIA_EMBEDDED_CATALOG, key)\n}\n\n/** The label a key carries on a given format. */\nexport function embeddedFieldLabel(\n key: string,\n format: MediaEmbeddedFormat,\n fallback?: string,\n): string {\n if (isCanonicalEmbeddedKey(key)) {\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return entry.labels?.[format] ?? entry.label\n }\n return fallback ?? parseOtherEmbeddedKey(key)?.parts.at(-1) ?? key\n}\n\n/** The group a key is shown under on a given format. */\nexport function embeddedFieldGroup(\n key: string,\n format: MediaEmbeddedFormat,\n): MediaEmbeddedGroup {\n if (!isCanonicalEmbeddedKey(key)) return 'other'\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return entry.groups?.[format] ?? entry.group\n}\n\n/** How a key's value is edited. Other keys are free text. */\nexport function embeddedFieldKind(key: string): MediaEmbeddedValueKind {\n return isCanonicalEmbeddedKey(key)\n ? (MEDIA_EMBEDDED_CATALOG[key] as MediaEmbeddedCatalogEntry).kind\n : 'text'\n}\n\n/**\n * Whether an edit of `key` can be written into a file of `format`.\n *\n * The one place the answer lives: the reader stamps `editable` with it, the\n * drawer offers the \"add a field\" choices from it, and the write route\n * refuses anything it says no to — so the three cannot disagree about which\n * fields a JPEG can take.\n */\nexport function embeddedKeyWritable(\n key: string,\n format: MediaEmbeddedFormat,\n): boolean {\n if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) return false\n if (isCanonicalEmbeddedKey(key)) {\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return Boolean(entry.writable?.includes(format))\n }\n const other = parseOtherEmbeddedKey(key)\n return Boolean(other && OTHER_WRITABLE[other.namespace]?.includes(format))\n}\n\n/**\n * The canonical keys a file of this format can take, whether or not it\n * holds them yet — what the drawer offers under \"Add a field\".\n */\nexport function embeddedWritableKeys(\n format: MediaEmbeddedFormat,\n): MediaEmbeddedCanonicalKey[] {\n return (Object.keys(MEDIA_EMBEDDED_CATALOG) as MediaEmbeddedCanonicalKey[])\n .filter((key) => embeddedKeyWritable(key, format))\n}\n\n/** A strict ISO 8601 date or date-time, the only date a writer accepts. */\nconst ISO_DATE =\n /^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,3})?)?(?:Z|[+-]\\d{2}:\\d{2})?)?$/\n\n/**\n * Validate an edit before it reaches a writer, or say why it cannot.\n *\n * Runs in the drawer (to explain) and in the write route (to refuse), so\n * the messages are written for the person who typed the value.\n */\nexport function sanitizeEmbeddedPatch(\n format: MediaEmbeddedFormat,\n patch: unknown,\n): { patch: MediaEmbeddedPatch } | { error: string } {\n if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) {\n return { error: 'This kind of file cannot take metadata edits.' }\n }\n if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {\n return { error: 'Nothing to change.' }\n }\n const out: MediaEmbeddedPatch = {}\n for (const [rawKey, raw] of Object.entries(patch as Record<string, unknown>)) {\n const key = rawKey.trim()\n if (!embeddedKeyWritable(key, format)) {\n return {\n error: `${embeddedFieldLabel(key, format)} cannot be written into this file.`,\n }\n }\n const label = embeddedFieldLabel(key, format)\n if (raw === null) {\n out[key] = null\n continue\n }\n const entry: MediaEmbeddedCatalogEntry | undefined = isCanonicalEmbeddedKey(\n key,\n )\n ? MEDIA_EMBEDDED_CATALOG[key]\n : undefined\n if (entry?.removeOnly) {\n return { error: `${label} can only be removed.` }\n }\n const kind = embeddedFieldKind(key)\n if (kind === 'list') {\n const items = (Array.isArray(raw) ? raw : String(raw).split(/[,;\\n]/))\n .map((item) => String(item).trim())\n .filter(Boolean)\n if (items.length > MEDIA_EMBEDDED_MAX_LIST_ITEMS) {\n return { error: `${label} holds at most ${MEDIA_EMBEDDED_MAX_LIST_ITEMS} entries.` }\n }\n if (items.some((item) => item.length > MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH)) {\n return { error: `An entry in ${label} is too long.` }\n }\n out[key] = items.length ? [...new Set(items)] : null\n continue\n }\n if (typeof raw !== 'string') {\n return { error: `${label} must be text.` }\n }\n const value = raw.trim()\n if (!value) {\n out[key] = null\n continue\n }\n if (value.length > MEDIA_EMBEDDED_MAX_VALUE_LENGTH) {\n return { error: `${label} is too long.` }\n }\n if (kind === 'date' && !ISO_DATE.test(value)) {\n return { error: `${label} must be a date.` }\n }\n if (kind === 'rating' && !/^(?:-1|[0-5])$/.test(value)) {\n return { error: `${label} must be between 0 and 5.` }\n }\n out[key] = value\n }\n if (!Object.keys(out).length) return { error: 'Nothing to change.' }\n return { patch: out }\n}\n\n/** The value of a field as one line of text. */\nexport function embeddedFieldText(field: MediaEmbeddedField): string {\n return field.values ? field.values.join(', ') : (field.value ?? '')\n}\n\n/**\n * The stored content types a reader exists for — the drawer asks the\n * server to read a file only when this says there is something to find, so\n * a CSV never costs a round trip to learn it has no caption.\n */\nconst READABLE_TYPES = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n 'image/tiff',\n 'image/heic',\n 'image/heif',\n 'image/avif',\n 'image/svg+xml',\n 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'video/mp4',\n 'video/quicktime',\n 'video/webm',\n])\n\nexport function embeddedMetadataReadable(contentType: unknown): boolean {\n return READABLE_TYPES.has(\n String(contentType ?? '').toLowerCase().split(';')[0]?.trim() ?? '',\n )\n}\n\n/**\n * A field's value as the drawer shows it — the stored value is the\n * machine form every writer agrees on, and this is the only place it is\n * dressed for a person.\n */\nexport function embeddedFieldDisplay(\n field: Pick<MediaEmbeddedField, 'key' | 'value' | 'values'>,\n locale?: string,\n): string {\n if (field.values) return field.values.join(', ')\n const value = field.value ?? ''\n switch (embeddedFieldKind(field.key)) {\n case 'date': {\n const dateOnly = /^\\d{4}-\\d{2}-\\d{2}$/.test(value)\n // A date-only value parsed by `Date` is UTC midnight, which renders as\n // the previous day west of Greenwich; noon keeps it on its own date.\n const parsed = new Date(dateOnly ? `${value}T12:00:00` : value)\n if (Number.isNaN(parsed.getTime())) return value\n return parsed.toLocaleString(locale, {\n dateStyle: 'medium',\n ...(dateOnly ? {} : { timeStyle: 'short' }),\n })\n }\n case 'gps': {\n const [lat, lon] = value.split(',').map(Number)\n if (!Number.isFinite(lat) || !Number.isFinite(lon)) return value\n const part = (n: number, positive: string, negative: string) =>\n `${Math.abs(n).toFixed(5)}° ${n < 0 ? negative : positive}`\n return `${part(lat as number, 'N', 'S')}, ${part(lon as number, 'E', 'W')}`\n }\n case 'rating': {\n const stars = Number(value)\n if (stars === -1) return 'Rejected'\n if (!Number.isInteger(stars) || stars < 0 || stars > 5) return value\n return '★'.repeat(stars) + '☆'.repeat(5 - stars)\n }\n default:\n return value\n }\n}\n"],"names":["MEDIA_EMBEDDED_GROUP_ORDER","MEDIA_EMBEDDED_GROUP_LABELS","description","rights","location","capture","document","technical","other","MEDIA_EMBEDDED_WRITABLE_FORMATS","Set","IMAGE_WRITABLE","IMAGE_AND_PDF","IMAGE_PDF_OOXML","MEDIA_EMBEDDED_CATALOG","title","label","group","kind","writable","headline","labels","pdf","ooxml","subject","keywords","category","comment","instructions","rating","creator","copyright","credit","source","usageTerms","webStatement","city","state","country","gps","removeOnly","gpsAltitude","createdAt","svg","mp4","webm","groups","make","model","lens","exposure","aperture","iso","focalLength","flash","modifiedAt","software","producer","lastModifiedBy","company","pageCount","pdfVersion","orientation","colorProfile","encoder","otherEmbeddedKey","namespace","parts","join","parseOtherEmbeddedKey","key","split","length","includes","OTHER_WRITABLE","xmp","png","MEDIA_EMBEDDED_METADATA_VERSION","MEDIA_EMBEDDED_MAX_FIELDS","MEDIA_EMBEDDED_MAX_VALUE_LENGTH","MEDIA_EMBEDDED_MAX_LIST_ITEMS","MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH","isCanonicalEmbeddedKey","Object","prototype","hasOwnProperty","call","embeddedFieldLabel","format","fallback","entry","at","embeddedFieldGroup","embeddedFieldKind","embeddedKeyWritable","has","Boolean","embeddedWritableKeys","keys","filter","ISO_DATE","sanitizeEmbeddedPatch","patch","error","Array","isArray","out","rawKey","raw","entries","trim","undefined","items","String","map","item","some","value","test","embeddedFieldText","field","values","READABLE_TYPES","embeddedMetadataReadable","contentType","toLowerCase","embeddedFieldDisplay","locale","dateOnly","parsed","Date","Number","isNaN","getTime","toLocaleString","dateStyle","timeStyle","lat","lon","isFinite","part","n","positive","negative","Math","abs","toFixed","stars","isInteger","repeat"],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* EXIF, and the TIFF structure that carries it (AGL-3331).
|
|
19
|
+
*
|
|
20
|
+
* A JPEG's APP1 `Exif\0\0` segment, a WebP `EXIF` chunk, a PNG `eXIf` chunk
|
|
21
|
+
* and a TIFF file all hold the same thing: a TIFF header followed by image
|
|
22
|
+
* file directories (IFDs), TIFF 6.0 §2, which EXIF 2.32 §4.5 adopts as is.
|
|
23
|
+
* Every function here takes the bytes from the byte-order mark (`II*\0` or
|
|
24
|
+
* `MM\0*`) on; containers strip their own framing first.
|
|
25
|
+
*
|
|
26
|
+
* ## Reading
|
|
27
|
+
*
|
|
28
|
+
* {@link readExif} maps IFD0, the Exif IFD (pointer 0x8769) and the GPS IFD
|
|
29
|
+
* (pointer 0x8825) onto the canonical keys of `media-embedded-fields.ts`.
|
|
30
|
+
* Strings are ASCII by the spec and UTF-8 in real files, so they decode as
|
|
31
|
+
* UTF-8 and fall back to Latin-1. Every offset and count is checked against
|
|
32
|
+
* the block, the walk refuses cycles, and a damaged block yields whatever it
|
|
33
|
+
* could read rather than an exception. BigTIFF (version 43) reads too.
|
|
34
|
+
*
|
|
35
|
+
* ## Writing without moving anything
|
|
36
|
+
*
|
|
37
|
+
* A TIFF block is full of absolute offsets this module cannot see: a
|
|
38
|
+
* MakerNote's private IFD points into itself by offset from the TIFF header,
|
|
39
|
+
* a thumbnail is found through IFD1, a TIFF file's pixels through
|
|
40
|
+
* StripOffsets. So the writer NEVER MOVES A BYTE IT DID NOT CREATE:
|
|
41
|
+
*
|
|
42
|
+
* - A value that fits its old slot (four bytes inside the entry, or the old
|
|
43
|
+
* out-of-line length) is overwritten in place and NUL-filled.
|
|
44
|
+
* - A larger value is appended at the end of the block and its entry
|
|
45
|
+
* repointed. An entry is a fixed 12 bytes, so nothing else shifts.
|
|
46
|
+
* - Adding or removing an entry relocates THAT IFD: a fresh copy, entries
|
|
47
|
+
* sorted by tag (TIFF 6.0 §2 "the entries must be sorted in ascending
|
|
48
|
+
* order by the Tag"), next-IFD pointer preserved, goes at the end, and
|
|
49
|
+
* whatever pointed at the old one (the header for IFD0, 0x8769, 0x8825)
|
|
50
|
+
* is repointed.
|
|
51
|
+
* - Bytes nothing references any more (a replaced value's old slot, a
|
|
52
|
+
* relocated IFD's old table, a scrubbed GPS IFD) are zeroed, so a removed
|
|
53
|
+
* value is gone from the file rather than merely unreferenced.
|
|
54
|
+
*
|
|
55
|
+
* Before overwriting or zeroing an old slot the writer checks that no other
|
|
56
|
+
* structure it knows of (another entry's value, an IFD table, strips, tiles,
|
|
57
|
+
* the thumbnail) claims any of those bytes; a shared slot is left alone and
|
|
58
|
+
* the new value appended instead.
|
|
59
|
+
*/
|
|
60
|
+
import { type EmbeddedCandidate, type EmbeddedPatch } from './types';
|
|
61
|
+
/**
|
|
62
|
+
* Field types: TIFF 6.0 §2 "Types", TIFF Technical Note 1 (IFD) and the
|
|
63
|
+
* BigTIFF extension (LONG8, SLONG8, IFD8).
|
|
64
|
+
*/
|
|
65
|
+
export declare const TIFF_TYPE: {
|
|
66
|
+
readonly BYTE: 1;
|
|
67
|
+
readonly ASCII: 2;
|
|
68
|
+
readonly SHORT: 3;
|
|
69
|
+
readonly LONG: 4;
|
|
70
|
+
readonly RATIONAL: 5;
|
|
71
|
+
readonly SBYTE: 6;
|
|
72
|
+
readonly UNDEFINED: 7;
|
|
73
|
+
readonly SSHORT: 8;
|
|
74
|
+
readonly SLONG: 9;
|
|
75
|
+
readonly SRATIONAL: 10;
|
|
76
|
+
readonly FLOAT: 11;
|
|
77
|
+
readonly DOUBLE: 12;
|
|
78
|
+
readonly IFD: 13;
|
|
79
|
+
readonly LONG8: 16;
|
|
80
|
+
readonly SLONG8: 17;
|
|
81
|
+
readonly IFD8: 18;
|
|
82
|
+
};
|
|
83
|
+
/** The IFDs a caller can address. */
|
|
84
|
+
export type TiffDirectory = 'ifd0' | 'exif' | 'gps';
|
|
85
|
+
/**
|
|
86
|
+
* One tag to set or remove. `value` is the raw value bytes IN THE BLOCK'S
|
|
87
|
+
* BYTE ORDER (the same bytes {@link readTiffTagBytes} returns), and `count`
|
|
88
|
+
* defaults to `value.length` divided by the type's size. `null` removes the
|
|
89
|
+
* entry.
|
|
90
|
+
*/
|
|
91
|
+
export interface TiffTagWrite {
|
|
92
|
+
ifd: TiffDirectory;
|
|
93
|
+
tag: number;
|
|
94
|
+
type: number;
|
|
95
|
+
count?: number;
|
|
96
|
+
value: Uint8Array | null;
|
|
97
|
+
}
|
|
98
|
+
/** Everything one pass of {@link editTiff} can do. */
|
|
99
|
+
export interface TiffEdit {
|
|
100
|
+
/** Canonical-key edits, applied to tags that already exist. */
|
|
101
|
+
patch?: EmbeddedPatch;
|
|
102
|
+
/** Raw tag writes, applied after the patch (a later write wins). */
|
|
103
|
+
writes?: readonly TiffTagWrite[];
|
|
104
|
+
/** Whether `writes` may add tags that are not there yet. */
|
|
105
|
+
allowAdd?: boolean;
|
|
106
|
+
/** The largest block the container can hold. */
|
|
107
|
+
maxLength?: number;
|
|
108
|
+
}
|
|
109
|
+
/** Whether the bytes are well-formed UTF-8. */
|
|
110
|
+
export declare function isUtf8(bytes: Uint8Array): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Text in a field whose declared charset nobody honors: UTF-8 when the
|
|
113
|
+
* bytes are valid UTF-8 (pure ASCII is), Latin-1 otherwise. Shared with the
|
|
114
|
+
* IPTC reader, which has the same problem.
|
|
115
|
+
*/
|
|
116
|
+
export declare function decodeLegacyText(bytes: Uint8Array): string;
|
|
117
|
+
/** Whether the bytes start with a TIFF header this module can read. */
|
|
118
|
+
export declare function isTiff(bytes: Uint8Array): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Read the EXIF of a TIFF-structured block. Never throws: a damaged block
|
|
121
|
+
* yields what could be read, a block that is not TIFF yields nothing.
|
|
122
|
+
*/
|
|
123
|
+
export declare function readExif(tiff: Uint8Array): EmbeddedCandidate[];
|
|
124
|
+
/**
|
|
125
|
+
* The raw value of one tag, as stored: `bytes` is a view into `tiff`, in
|
|
126
|
+
* the block's byte order. Null when the block, the IFD or the tag is
|
|
127
|
+
* missing, or when the value runs past the end of the block.
|
|
128
|
+
*/
|
|
129
|
+
export declare function readTiffTagBytes(tiff: Uint8Array, ifd: TiffDirectory, tag: number): {
|
|
130
|
+
type: number;
|
|
131
|
+
count: number;
|
|
132
|
+
bytes: Uint8Array;
|
|
133
|
+
} | null;
|
|
134
|
+
/**
|
|
135
|
+
* The one writer behind {@link writeExif}, {@link writeTiffTags} and the
|
|
136
|
+
* TIFF file writer: a canonical patch and raw tag writes, in a single pass
|
|
137
|
+
* so an IFD is relocated at most once. Returns `tiff` itself when nothing
|
|
138
|
+
* needed to change. Throws {@link EmbeddedWriteError} when the block cannot
|
|
139
|
+
* take the edit safely; it never returns a corrupt block.
|
|
140
|
+
*/
|
|
141
|
+
export declare function editTiff(tiff: Uint8Array, edit: TiffEdit): Uint8Array;
|
|
142
|
+
/**
|
|
143
|
+
* Write a canonical patch into the EXIF tags that ALREADY EXIST — see the
|
|
144
|
+
* module doc for which keys map where — and ignore every key this block
|
|
145
|
+
* does not hold. `null` removes the tag; `gps: null` scrubs the GPS IFD.
|
|
146
|
+
* `maxLength` is the block's ceiling: 65527 inside a JPEG APP1.
|
|
147
|
+
*/
|
|
148
|
+
export declare function writeExif(tiff: Uint8Array, patch: EmbeddedPatch, options?: {
|
|
149
|
+
maxLength?: number;
|
|
150
|
+
}): Uint8Array;
|
|
151
|
+
/**
|
|
152
|
+
* Set or remove raw tags. A tag that is absent is added only with
|
|
153
|
+
* `allowAdd`, and is otherwise skipped; a removal of an absent tag is a
|
|
154
|
+
* no-op. Tags that locate other data (strips, tiles, the thumbnail, the
|
|
155
|
+
* Exif/GPS/Interop pointers, SubIFDs, the MakerNote) are refused.
|
|
156
|
+
*/
|
|
157
|
+
export declare function writeTiffTags(tiff: Uint8Array, writes: TiffTagWrite[], options?: {
|
|
158
|
+
allowAdd?: boolean;
|
|
159
|
+
maxLength?: number;
|
|
160
|
+
}): Uint8Array;
|