first commit
This commit is contained in:
86
docs/media/image/exif/README.md
Normal file
86
docs/media/image/exif/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# @suaveplan/exif - Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images
|
||||
|
||||
Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images. Parses JPEG segment chains to locate and decode the EXIF IFD (TIFF byte-order aware, including GPS and SubIFD), IPTC IIM (caption, keywords, copyright), and XMP Dublin Core (raw XML string extraction). Writes back a revised EXIF IFD without re-encoding the image pixels, and strips all metadata segments for privacy-preserving image export. Includes a `ByteReader`/`ByteWriter` pair with rational arithmetic helpers and CRC-32 for PNG chunks.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [crc32](./src/binary/crc32/crc32.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [crc32](./src/binary/crc32/crc32.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [rational](./src/binary/rational/rational.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [rational](./src/binary/rational/rational.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [reader](./src/binary/reader/byte-reader.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [byte-reader](./src/binary/reader/byte-reader.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [writer](./src/binary/writer/byte-writer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [byte-writer](./src/binary/writer/byte-writer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-metadata](./src/core/read-metadata.md) — The main entry point for reading image metadata. Orchestrates format detection, JPEG segment parsing, and EXIF/XMP/IPTC…
|
||||
- [strip-metadata](./src/core/strip-metadata.md) — Removes metadata segments from a JPEG image buffer and returns a clean image with only the pixel data intact. Privacy-s…
|
||||
- [write-metadata](./src/core/write-metadata.md) — Writes or updates EXIF metadata in a JPEG image buffer. Merges the provided patch over any existing EXIF data and rebui…
|
||||
- [errors](./src/errors/exif-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [exif-error](./src/errors/exif-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [exif](./src/exif/read-exif.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-exif](./src/exif/read-exif.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [format](./src/format/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [detect](./src/format/detect.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [ifd](./src/ifd/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [parse](./src/ifd/parse.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [iptc](./src/iptc/read-iptc.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-iptc](./src/iptc/read-iptc.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [jpeg](./src/jpeg/segments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [segments](./src/jpeg/segments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [xmp](./src/xmp/read-xmp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [read-xmp](./src/xmp/read-xmp.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/exif
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { readMetadata, writeMetadata, stripMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Read all metadata
|
||||
const meta = readMetadata(imageBuffer);
|
||||
console.log(meta.format); // "jpeg"
|
||||
console.log(meta.exif?.make); // "Canon"
|
||||
console.log(meta.exif?.gps?.latitude);
|
||||
|
||||
// Write / update metadata
|
||||
const updated = writeMetadata(imageBuffer, { orientation: 1, software: "My App" });
|
||||
|
||||
// Strip all metadata for privacy
|
||||
const clean = stripMetadata(imageBuffer);
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/core/read-metadata.md`](./src/core/read-metadata.md) — Main entry point for reading image metadata
|
||||
- [`src/core/write-metadata.md`](./src/core/write-metadata.md) — Write or update EXIF metadata in JPEG buffers
|
||||
- [`src/core/strip-metadata.md`](./src/core/strip-metadata.md) — Remove metadata segments for privacy-safe export
|
||||
- [`src/binary/reader/byte-reader.md`](./src/binary/reader/byte-reader.md) — `ByteReader` with endianness support
|
||||
- [`src/binary/writer/byte-writer.md`](./src/binary/writer/byte-writer.md) — `ByteWriter` for TIFF reconstruction
|
||||
- [`src/binary/rational/rational.md`](./src/binary/rational/rational.md) — Rational number arithmetic helpers
|
||||
- [`src/binary/crc32/crc32.md`](./src/binary/crc32/crc32.md) — CRC-32 for PNG chunk validation
|
||||
- [`src/format/detect.md`](./src/format/detect.md) — Magic-byte image format detection
|
||||
- [`src/jpeg/segments.md`](./src/jpeg/segments.md) — JPEG segment chain parsing
|
||||
- [`src/ifd/parse.md`](./src/ifd/parse.md) — TIFF IFD parsing (GPS, SubIFD)
|
||||
- [`src/exif/read-exif.md`](./src/exif/read-exif.md) — Raw EXIF/TIFF buffer parsing
|
||||
- [`src/iptc/read-iptc.md`](./src/iptc/read-iptc.md) — IPTC IIM caption/keywords/copyright parsing
|
||||
- [`src/xmp/read-xmp.md`](./src/xmp/read-xmp.md) — XMP Dublin Core raw XML extraction
|
||||
- [`src/errors/exif-error.md`](./src/errors/exif-error.md) — `ExifError` and `EXIF_CODES` registry
|
||||
|
||||
## Subpath Exports
|
||||
|
||||
| Subpath | Description |
|
||||
| --- | --- |
|
||||
| `@suaveplan/exif` | Main API: readMetadata, writeMetadata, stripMetadata |
|
||||
| `@suaveplan/exif/binary` | ByteReader, ByteWriter, rational utilities, CRC-32 |
|
||||
| `@suaveplan/exif/format` | detectImageFormat — magic byte detection |
|
||||
| `@suaveplan/exif/exif` | readExifFromTiffBuffer — raw TIFF/EXIF parsing |
|
||||
| `@suaveplan/exif/iptc` | readIptc — IPTC IIM parsing |
|
||||
| `@suaveplan/exif/xmp` | readXmp — XMP Dublin Core parsing |
|
||||
| `@suaveplan/exif/errors` | ExifError, EXIF_CODES |
|
||||
55
docs/media/image/exif/src/binary/crc32/crc32.md
Normal file
55
docs/media/image/exif/src/binary/crc32/crc32.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# CRC-32
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the standard IEEE 802.3 CRC-32 algorithm (polynomial `0xEDB88320`, reflected) used for PNG chunk integrity verification. The implementation is a pure TypeScript, lookup-table-based computation that works in any JavaScript runtime without native bindings or Node.js built-ins.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-buffer `crc32()` for straightforward integrity checks
|
||||
- Multi-chunk `crc32Multi()` that avoids buffer concatenation allocation
|
||||
- Pre-computed 256-entry lookup table for O(n) throughput
|
||||
- Returns unsigned 32-bit integer (safe for comparison with `>>>0` normalised values)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { crc32 } from "@suaveplan/exif/binary";
|
||||
|
||||
// Standard check value: crc32("123456789") === 0xCBF43926
|
||||
const encoder = new TextEncoder();
|
||||
const checkValue = crc32(encoder.encode("123456789"));
|
||||
console.log(checkValue.toString(16)); // "cbf43926"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { crc32Multi } from "@suaveplan/exif/binary";
|
||||
|
||||
// PNG chunk CRC: covers chunk type bytes + chunk data bytes
|
||||
function pngChunkCrc(type: Uint8Array, data: Uint8Array): number {
|
||||
return crc32Multi([type, data]);
|
||||
}
|
||||
|
||||
const typeBytes = new TextEncoder().encode("IHDR");
|
||||
const ihdrData = new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0]);
|
||||
const checksum = pngChunkCrc(typeBytes, ihdrData);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `crc32(data: Uint8Array): number`
|
||||
|
||||
Computes the CRC-32 of `data`. Returns an unsigned 32-bit integer. The standard test vector is: `crc32(new TextEncoder().encode("123456789")) === 0xCBF43926`.
|
||||
|
||||
### `crc32Multi(chunks: Uint8Array[]): number`
|
||||
|
||||
Computes the CRC-32 of all `chunks` as if they were concatenated, without allocating a combined buffer. Returns an unsigned 32-bit integer. Equivalent to `crc32(concat(...chunks))`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The lookup table is built once at module load time using the standard reflected polynomial `0xEDB88320` (the bit-reversal of `0x04C11DB7`). Each byte of input XORs into the low 8 bits of the running CRC, selects a table entry, then is combined with the shifted CRC via XOR. The final `>>> 0` ensures the result is a non-negative JavaScript number even when the high bit is set. `crc32Multi` avoids buffer allocation by maintaining the running CRC across chunk boundaries.
|
||||
67
docs/media/image/exif/src/binary/rational/rational.md
Normal file
67
docs/media/image/exif/src/binary/rational/rational.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Rational
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides conversion and formatting utilities for TIFF RATIONAL values — pairs of integers representing numerator and denominator. EXIF metadata stores many camera settings (exposure time, f-number, focal length, GPS coordinates) as RATIONAL pairs, so correct round-trip conversion between floating-point numbers and rational pairs is essential for accurate metadata reading and writing.
|
||||
|
||||
## Features
|
||||
|
||||
- `numberToRational` — floating-point to rational via Stern-Brocot tree approximation with configurable max denominator
|
||||
- `rationalToNumber` — rational to float with zero-denominator guard
|
||||
- `rationalToString` — human-readable "n/d" formatting
|
||||
- `simplifyRational` — GCD reduction to lowest terms with sign normalisation
|
||||
- Handles negative values and zero correctly
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { numberToRational, rationalToNumber, rationalToString } from "@suaveplan/exif/binary";
|
||||
|
||||
const r = numberToRational(0.01); // exposure 1/100 s
|
||||
console.log(rationalToString(r)); // "1/100"
|
||||
console.log(rationalToNumber(r)); // 0.01
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { numberToRational, simplifyRational, rationalToString } from "@suaveplan/exif/binary";
|
||||
|
||||
// Limit denominator for compact storage
|
||||
const focalLength = numberToRational(50.0, 100);
|
||||
console.log(rationalToString(focalLength)); // "50/1"
|
||||
|
||||
// GPS coordinate degree representation
|
||||
const degrees = numberToRational(40.7128, 1_000_000);
|
||||
const simple = simplifyRational(degrees);
|
||||
console.log(rationalToString(simple));
|
||||
|
||||
// Simplify a raw EXIF rational
|
||||
const raw = { numerator: 6, denominator: 4 };
|
||||
console.log(rationalToString(simplifyRational(raw))); // "3/2"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `numberToRational(n: number, maxDenominator?: number): Rational`
|
||||
|
||||
Converts a floating-point number to the closest `Rational` with denominator ≤ `maxDenominator` (default `1_000_000`). Uses the Stern-Brocot tree / mediant algorithm to guarantee the best approximation for the given denominator bound. Throws `ExifError(INVALID_DATA)` for non-finite inputs.
|
||||
|
||||
### `rationalToNumber(r: Rational): number`
|
||||
|
||||
Returns `numerator / denominator`. Returns `0` when `denominator === 0` to avoid propagating `NaN` or `Infinity` into metadata fields.
|
||||
|
||||
### `rationalToString(r: Rational): string`
|
||||
|
||||
Returns the string `"${numerator}/${denominator}"`, e.g. `"1/100"`.
|
||||
|
||||
### `simplifyRational(r: Rational): Rational`
|
||||
|
||||
Reduces `r` to lowest terms using Euclidean GCD. Normalises negative denominators to the numerator. Returns `{ numerator: 0, denominator: 1 }` when numerator is zero.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Stern-Brocot tree search is bounded by a maximum of 1000 iterations in addition to the denominator limit, ensuring termination even for pathological inputs. The mediant criterion (`|medVal - abs| < 1e-10`) provides exact representation for most photographic fractions such as `1/100`, `1/250`, `2.8`, `50.0`. The GCD in `simplifyRational` uses the iterative Euclidean algorithm to avoid stack overflow on large inputs.
|
||||
117
docs/media/image/exif/src/binary/reader/byte-reader.md
Normal file
117
docs/media/image/exif/src/binary/reader/byte-reader.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# ByteReader
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a low-level, bounds-checked binary reader for TIFF/EXIF byte streams. It wraps a `Uint8Array` with a configurable byte order and exposes typed read methods for all TIFF primitive types. The runtime-agnostic implementation avoids Node.js `Buffer` and browser-specific APIs, relying solely on `DataView` and standard typed arrays.
|
||||
|
||||
## Features
|
||||
|
||||
- Big-endian and little-endian support with runtime `setByteOrder()` switching
|
||||
- Full TIFF primitive coverage: uint8/16/32, int16/32, float32/64, rational, signed rational
|
||||
- Bounds-checked reads that throw `ExifError` with `EXIF.PARSE_ERROR` on overflow
|
||||
- String reads with null-termination handling (`readString`, `readNullTermString`)
|
||||
- `slice()` factory that creates a sub-reader sharing the same byte order
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
|
||||
const data = new Uint8Array([0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08]);
|
||||
const reader = new ByteReader(data, "BE");
|
||||
|
||||
const magic = reader.readUint16(2); // 0x002A
|
||||
const ifdOffset = reader.readUint32(4); // 8
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
|
||||
// Switch byte order mid-stream (e.g. after reading TIFF header)
|
||||
const reader = new ByteReader(tiffBuffer, "BE");
|
||||
const bom = reader.readUint16(0);
|
||||
if (bom === 0x4949) reader.setByteOrder("LE"); // Intel = little-endian
|
||||
|
||||
// Read a RATIONAL (8 bytes: numerator + denominator)
|
||||
const exposureTime = reader.readRational(offset);
|
||||
console.log(`${exposureTime.numerator}/${exposureTime.denominator}`);
|
||||
|
||||
// Sub-slice for IFD parsing
|
||||
const ifdReader = reader.slice(ifdOffset, ifdOffset + ifdSize);
|
||||
const entryCount = ifdReader.readUint16(0);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constructor(data: Uint8Array, byteOrder?: ByteOrder): ByteReader`
|
||||
|
||||
Creates a new reader wrapping `data`. Default `byteOrder` is `"BE"` (big-endian).
|
||||
|
||||
### `setByteOrder(order: ByteOrder): void`
|
||||
|
||||
Switches the active byte order for all subsequent reads.
|
||||
|
||||
### `get length: number`
|
||||
|
||||
Returns the total byte length of the underlying buffer.
|
||||
|
||||
### `readUint8(offset: number): number`
|
||||
|
||||
Reads one unsigned byte at `offset`.
|
||||
|
||||
### `readUint16(offset: number): number`
|
||||
|
||||
Reads a 2-byte unsigned integer using the active byte order.
|
||||
|
||||
### `readUint32(offset: number): number`
|
||||
|
||||
Reads a 4-byte unsigned integer using the active byte order.
|
||||
|
||||
### `readInt16(offset: number): number`
|
||||
|
||||
Reads a 2-byte signed integer using the active byte order.
|
||||
|
||||
### `readInt32(offset: number): number`
|
||||
|
||||
Reads a 4-byte signed integer using the active byte order.
|
||||
|
||||
### `readFloat32(offset: number): number`
|
||||
|
||||
Reads a 4-byte IEEE 754 single-precision float using the active byte order.
|
||||
|
||||
### `readFloat64(offset: number): number`
|
||||
|
||||
Reads an 8-byte IEEE 754 double-precision float using the active byte order.
|
||||
|
||||
### `readBytes(offset: number, length: number): Uint8Array`
|
||||
|
||||
Returns a copy of `length` bytes starting at `offset`.
|
||||
|
||||
### `readString(offset: number, length: number): string`
|
||||
|
||||
Reads `length` bytes as a Latin-1 string, stopping at the first null byte.
|
||||
|
||||
### `readNullTermString(offset: number): string`
|
||||
|
||||
Scans forward from `offset` until a null byte, then reads the preceding bytes as a string.
|
||||
|
||||
### `readRational(offset: number): Rational`
|
||||
|
||||
Reads an 8-byte TIFF RATIONAL (two unsigned 32-bit integers: numerator then denominator).
|
||||
|
||||
### `readSRational(offset: number): Rational`
|
||||
|
||||
Reads an 8-byte TIFF SRATIONAL (two signed 32-bit integers).
|
||||
|
||||
### `slice(start: number, end: number): ByteReader`
|
||||
|
||||
Returns a new `ByteReader` over `data.slice(start, end)` with the same byte order.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All read methods call `assertBounds` before touching `DataView` to ensure clear error messages on malformed input. The `view` is constructed from the array's `buffer`, `byteOffset`, and `byteLength` to correctly handle sliced `Uint8Array` inputs that do not start at offset 0 in their underlying `ArrayBuffer`. The `readString` and `readNullTermString` methods use Latin-1 (one byte per character) which matches TIFF/EXIF ASCII strings; UTF-8 decoding is not attempted here as EXIF ASCII tags are defined as 7-bit ASCII.
|
||||
100
docs/media/image/exif/src/binary/writer/byte-writer.md
Normal file
100
docs/media/image/exif/src/binary/writer/byte-writer.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# ByteWriter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a dynamic binary writer for building TIFF/EXIF byte streams from scratch. It maintains an auto-growing `Uint8Array` buffer and exposes typed write methods for all TIFF primitive types. This is the write-side counterpart to `ByteReader`, sharing the same byte-order model and error infrastructure.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-growing buffer (doubles capacity on overflow, no pre-allocation required)
|
||||
- Big-endian and little-endian support with runtime `setByteOrder()` switching
|
||||
- Full TIFF primitive coverage: uint8/16/32, int16, rational, raw bytes, strings
|
||||
- Fluent API — all write methods return `this` for chaining
|
||||
- `toUint8Array()` snapshot of the written bytes at any point
|
||||
- `ExifError` with `EXIF.WRITE_ERROR` on non-Latin-1 string characters
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ByteWriter } from "@suaveplan/exif/binary";
|
||||
|
||||
const writer = new ByteWriter(64, "BE");
|
||||
writer.writeUint16(0x4D4D); // big-endian BOM
|
||||
writer.writeUint16(0x002A); // TIFF magic
|
||||
writer.writeUint32(8); // IFD0 offset
|
||||
|
||||
const bytes = writer.toUint8Array();
|
||||
// Uint8Array [ 0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08 ]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ByteWriter } from "@suaveplan/exif/binary";
|
||||
|
||||
// Fluent chaining
|
||||
const exifHeader = new ByteWriter()
|
||||
.setByteOrder("LE")
|
||||
.writeBytes(new Uint8Array([0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) // "Exif\0\0"
|
||||
.writeUint16(0x4949) // LE BOM
|
||||
.writeUint16(0x002A)
|
||||
.writeUint32(8)
|
||||
.toUint8Array();
|
||||
|
||||
// Writing a RATIONAL (e.g. exposure time 1/100)
|
||||
const writer = new ByteWriter();
|
||||
writer.writeRational({ numerator: 1, denominator: 100 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `constructor(initialSize?: number, byteOrder?: ByteOrder): ByteWriter`
|
||||
|
||||
Creates a new writer. `initialSize` defaults to 256 bytes. `byteOrder` defaults to `"BE"`.
|
||||
|
||||
### `get position: number`
|
||||
|
||||
Returns the current write cursor position (total bytes written so far).
|
||||
|
||||
### `setByteOrder(order: ByteOrder): this`
|
||||
|
||||
Switches the active byte order. Returns `this` for chaining.
|
||||
|
||||
### `writeUint8(value: number): this`
|
||||
|
||||
Writes one unsigned byte.
|
||||
|
||||
### `writeUint16(value: number): this`
|
||||
|
||||
Writes a 2-byte unsigned integer using the active byte order.
|
||||
|
||||
### `writeUint32(value: number): this`
|
||||
|
||||
Writes a 4-byte unsigned integer using the active byte order.
|
||||
|
||||
### `writeInt16(value: number): this`
|
||||
|
||||
Writes a 2-byte signed integer using the active byte order.
|
||||
|
||||
### `writeBytes(bytes: Uint8Array): this`
|
||||
|
||||
Appends a raw byte array.
|
||||
|
||||
### `writeString(str: string): this`
|
||||
|
||||
Writes a Latin-1 string without null termination. Throws `ExifError(WRITE_ERROR)` if any character is outside the Latin-1 range (> U+00FF).
|
||||
|
||||
### `writeRational(r: Rational): this`
|
||||
|
||||
Writes an 8-byte TIFF RATIONAL (two unsigned 32-bit integers: numerator then denominator).
|
||||
|
||||
### `toUint8Array(): Uint8Array`
|
||||
|
||||
Returns a copy of all bytes written so far (from position 0 to the current cursor).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The buffer starts at `initialSize` and doubles whenever the next write would overflow. This amortizes allocation cost to O(log n) reallocations for n total bytes written. The `view` is recreated after each reallocation since `DataView` binds to a specific `ArrayBuffer` instance. `toUint8Array()` always returns a fresh slice to prevent the caller from holding a reference to the internal buffer.
|
||||
65
docs/media/image/exif/src/core/read-metadata.md
Normal file
65
docs/media/image/exif/src/core/read-metadata.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# readMetadata
|
||||
|
||||
## Purpose
|
||||
|
||||
The main entry point for reading image metadata. Orchestrates format detection, JPEG segment parsing, and EXIF/XMP/IPTC decoding into a unified `ImageMetadata` result. Callers do not need to interact with the lower-level parsers directly — this function handles the full pipeline from raw `Uint8Array` to structured metadata.
|
||||
|
||||
## Features
|
||||
|
||||
- Auto-detects image format via magic bytes
|
||||
- For JPEG: parses all APP segments and dispatches to EXIF, XMP, and IPTC readers
|
||||
- For PNG: reads IHDR chunk dimensions
|
||||
- `ReadOptions` to skip individual metadata types for performance
|
||||
- Throws `ExifError(UNSUPPORTED_FORMAT)` for completely unrecognised formats
|
||||
- Gracefully skips malformed EXIF segments without throwing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readMetadata } from "@suaveplan/exif";
|
||||
|
||||
const metadata = readMetadata(imageBuffer);
|
||||
console.log(metadata.format); // "jpeg"
|
||||
console.log(metadata.exif?.make); // "Canon"
|
||||
console.log(metadata.exif?.dateTimeOriginal);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { readMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Skip XMP for faster processing
|
||||
const metadata = readMetadata(imageBuffer, { skipXmp: true });
|
||||
|
||||
if (metadata.exif?.gps?.latitude) {
|
||||
const { degrees, minutes, seconds, ref } = metadata.exif.gps.latitude;
|
||||
const decimal = (degrees + minutes / 60 + seconds / 3600) * (ref === "S" ? -1 : 1);
|
||||
console.log("GPS lat:", decimal.toFixed(6));
|
||||
}
|
||||
|
||||
if (metadata.iptc?.keywords) {
|
||||
console.log("Keywords:", metadata.iptc.keywords.join(", "));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readMetadata(buffer: Uint8Array, options?: ReadOptions): ImageMetadata`
|
||||
|
||||
Reads all available metadata from `buffer`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — raw image bytes
|
||||
- `options.skipExif` — skip EXIF APP1 parsing (default: false)
|
||||
- `options.skipIptc` — skip IPTC APP13 parsing (default: false)
|
||||
- `options.skipXmp` — skip XMP APP1 parsing (default: false)
|
||||
|
||||
**Returns:** `ImageMetadata` with `format` always set, and optional `exif`, `iptc`, `xmp`, `width`, `height`.
|
||||
|
||||
**Throws:** `ExifError(UNSUPPORTED_FORMAT)` if `detectImageFormat` returns `"unknown"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The EXIF APP1 segment begins with a 6-byte `"Exif\0\0"` prefix before the TIFF header; this is stripped before calling `readExifFromTiffBuffer`. The XMP APP1 segment begins with the null-terminated namespace URI `"http://ns.adobe.com/xap/1.0/\0"` before the XML payload. Image dimensions are extracted from EXIF (imageWidth/imageHeight fields) when present and from the PNG IHDR chunk as a fallback. Malformed EXIF data is silently skipped to avoid crashing on images with corrupt metadata.
|
||||
60
docs/media/image/exif/src/core/strip-metadata.md
Normal file
60
docs/media/image/exif/src/core/strip-metadata.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# stripMetadata
|
||||
|
||||
## Purpose
|
||||
|
||||
Removes metadata segments from a JPEG image buffer and returns a clean image with only the pixel data intact. Privacy-sensitive metadata (GPS location, camera serial numbers, timestamps) is removed without re-encoding or degrading the image quality. The JPEG stream is rebuilt by concatenating only the non-metadata segments with the original compressed image data.
|
||||
|
||||
## Features
|
||||
|
||||
- Strips EXIF APP1 segments (camera, GPS, orientation metadata)
|
||||
- Strips XMP APP1 segments (Adobe editorial metadata)
|
||||
- Strips IPTC APP13 segments (Photoshop 3.0 news/editorial metadata)
|
||||
- Each strip operation is independently controllable via `StripOptions`
|
||||
- Non-destructive: compressed image data (SOS + payload + EOI) is preserved byte-for-byte
|
||||
- Throws `ExifError(UNSUPPORTED_FORMAT)` for non-JPEG inputs
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { stripMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Strip all metadata (default behaviour)
|
||||
const clean = stripMetadata(jpegBuffer);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { stripMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Keep IPTC (editorial attribution) but remove GPS and XMP
|
||||
const stripped = stripMetadata(jpegBuffer, {
|
||||
stripExif: true,
|
||||
stripXmp: true,
|
||||
stripIptc: false,
|
||||
});
|
||||
|
||||
// Verify the result is still a valid JPEG
|
||||
console.log(stripped[0] === 0xFF && stripped[1] === 0xD8); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `stripMetadata(buffer: Uint8Array, options?: StripOptions): Uint8Array`
|
||||
|
||||
Strips metadata from a JPEG buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — raw JPEG image bytes
|
||||
- `options.stripExif` — strip EXIF APP1 segments (default: `true`)
|
||||
- `options.stripIptc` — strip IPTC APP13 segments (default: `true`)
|
||||
- `options.stripXmp` — strip XMP APP1 segments (default: `true`)
|
||||
|
||||
**Returns:** New `Uint8Array` with the specified segments removed.
|
||||
|
||||
**Throws:** `ExifError(UNSUPPORTED_FORMAT)` if `buffer` is not a JPEG image.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function calls `parseJpegSegments` to obtain the structural segment list, then rebuilds the JPEG by writing the SOI marker, each non-stripped segment (marker + length + data), and finally the raw SOS-onward bytes from the original buffer. The SOS segment and all subsequent compressed data are appended verbatim — no re-encoding occurs. Default options strip all three metadata types (EXIF, XMP, IPTC) to provide the safest default for privacy-sensitive use cases. Passing `{ stripExif: false, stripIptc: false, stripXmp: false }` is a no-op that returns a structurally identical (though freshly allocated) buffer.
|
||||
64
docs/media/image/exif/src/core/write-metadata.md
Normal file
64
docs/media/image/exif/src/core/write-metadata.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# writeMetadata
|
||||
|
||||
## Purpose
|
||||
|
||||
Writes or updates EXIF metadata in a JPEG image buffer. Merges the provided patch over any existing EXIF data and rebuilds the APP1 segment, inserting one if the image does not already have EXIF. The compressed image data is preserved byte-for-byte — no re-encoding occurs.
|
||||
|
||||
## Features
|
||||
|
||||
- Merges patch fields over existing EXIF data when an APP1 segment is present
|
||||
- Inserts a new APP1 segment when no EXIF exists
|
||||
- Writes IFD0 fields: Make, Model, Software, DateTime, Orientation, ImageWidth, ImageHeight
|
||||
- Uses big-endian (Motorola) TIFF format for maximum compatibility
|
||||
- Rebuilds valid JPEG stream with correct APP1 marker and length
|
||||
- Throws `ExifError(UNSUPPORTED_FORMAT)` for non-JPEG inputs
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { writeMetadata } from "@suaveplan/exif";
|
||||
|
||||
const updated = writeMetadata(jpegBuffer, {
|
||||
software: "My App 1.0",
|
||||
orientation: 1,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { writeMetadata, readMetadata } from "@suaveplan/exif";
|
||||
|
||||
// Read → patch → write round-trip
|
||||
const before = readMetadata(jpegBuffer);
|
||||
const after = writeMetadata(jpegBuffer, {
|
||||
...before.exif,
|
||||
orientation: 6, // rotate 90° CW
|
||||
software: "Suaveplan Processor",
|
||||
});
|
||||
|
||||
const verify = readMetadata(after);
|
||||
console.log(verify.exif?.orientation); // 6
|
||||
console.log(verify.exif?.software); // "Suaveplan Processor"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `writeMetadata(buffer: Uint8Array, patch: Partial<ExifData>): Uint8Array`
|
||||
|
||||
Updates EXIF metadata in a JPEG buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — raw JPEG image bytes
|
||||
- `patch` — `ExifData` fields to set. Fields not included in `patch` retain their existing values from the original EXIF (if present).
|
||||
|
||||
**Returns:** New `Uint8Array` containing the updated JPEG.
|
||||
|
||||
**Throws:** `ExifError(UNSUPPORTED_FORMAT)` if `buffer` is not a JPEG image.
|
||||
|
||||
**Supported fields:** `make`, `model`, `software`, `dateTime`, `orientation`, `imageWidth`, `imageHeight`. GPS and ExifIFD sub-IFD writing are not yet supported.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The TIFF stream is always written in big-endian (Motorola, `MM`) format for maximum compatibility with reading software. IFD entries are sorted by tag number as required by the TIFF specification. String values (ASCII type) are null-terminated; values longer than 3 characters are stored in the data area rather than inline. The rebuilt JPEG inserts the new APP1 immediately after the SOI marker (before any other APP segments), which is the canonical position for EXIF. Any existing EXIF APP1 segment is replaced to prevent duplicate segments.
|
||||
99
docs/media/image/exif/src/errors/exif-error.md
Normal file
99
docs/media/image/exif/src/errors/exif-error.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# ExifError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides the stable error code registry and base error class for the `@suaveplan/exif` package. Every error thrown by the package uses `ExifError` with a code from `EXIF_CODES`, enabling programmatic error handling without string matching on error messages.
|
||||
|
||||
The design mirrors the `@suaveplan/error` pattern but keeps the package dependency-free: no runtime `@suaveplan/*` imports are needed.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable code registry via `EXIF_CODES` constant object
|
||||
- `ExifError` class with `code` and optional `cause` chaining
|
||||
- TypeScript `ExifErrorCode` union type for exhaustive switch statements
|
||||
- Four distinct codes covering all error surfaces: format detection, parsing, writing, and data validation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ExifError, EXIF_CODES } from "@suaveplan/exif/errors";
|
||||
|
||||
throw new ExifError("Buffer is not a JPEG or PNG image", EXIF_CODES.UNSUPPORTED_FORMAT);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ExifError, EXIF_CODES, type ExifErrorCode } from "@suaveplan/exif/errors";
|
||||
|
||||
function handleExifError(err: ExifError): void {
|
||||
switch (err.code as ExifErrorCode) {
|
||||
case "EXIF.UNSUPPORTED_FORMAT":
|
||||
console.warn("Unsupported image format:", err.message);
|
||||
break;
|
||||
case "EXIF.PARSE_ERROR":
|
||||
console.error("Parsing failed:", err.message, err.cause);
|
||||
break;
|
||||
case "EXIF.WRITE_ERROR":
|
||||
console.error("Write failed:", err.message);
|
||||
break;
|
||||
case "EXIF.INVALID_DATA":
|
||||
console.error("Invalid metadata:", err.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// With cause chaining
|
||||
try {
|
||||
const data = parseIfd(reader, 0);
|
||||
} catch (cause) {
|
||||
throw new ExifError("IFD parse failed at offset 0", EXIF_CODES.PARSE_ERROR, { cause });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EXIF_CODES`
|
||||
|
||||
```ts
|
||||
const EXIF_CODES: {
|
||||
UNSUPPORTED_FORMAT: "EXIF.UNSUPPORTED_FORMAT";
|
||||
PARSE_ERROR: "EXIF.PARSE_ERROR";
|
||||
WRITE_ERROR: "EXIF.WRITE_ERROR";
|
||||
INVALID_DATA: "EXIF.INVALID_DATA";
|
||||
}
|
||||
```
|
||||
|
||||
Stable string code registry. Use these constants rather than literal strings to guard against typos.
|
||||
|
||||
### `ExifErrorCode`
|
||||
|
||||
```ts
|
||||
type ExifErrorCode = "EXIF.UNSUPPORTED_FORMAT" | "EXIF.PARSE_ERROR" | "EXIF.WRITE_ERROR" | "EXIF.INVALID_DATA";
|
||||
```
|
||||
|
||||
Union type of all valid codes, suitable for exhaustive switches.
|
||||
|
||||
### `ExifError`
|
||||
|
||||
```ts
|
||||
class ExifError extends Error {
|
||||
readonly code: ExifErrorCode;
|
||||
constructor(message: string, code: ExifErrorCode, options?: { cause?: unknown }): ExifError;
|
||||
}
|
||||
```
|
||||
|
||||
- **message** — human-readable description of the error
|
||||
- **code** — one of the `EXIF_CODES` values for programmatic handling
|
||||
- **options.cause** — upstream error to chain (accessible via `err.cause`)
|
||||
|
||||
The `name` property is always `"ExifError"` for `instanceof`-free type narrowing via `err.name`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This module is intentionally minimal. It carries no runtime dependencies and exports only what is needed for error handling across the package. The `EXIF_CODES` object is declared `as const` so TypeScript narrows each property to its literal type, enabling the `ExifErrorCode` union to be derived without duplication.
|
||||
|
||||
`ExifError` extends `Error` directly rather than `@suaveplan/error`'s base class to keep the package fully dependency-free. This is a deliberate trade-off: the package is a leaf node in the dependency graph and should impose zero peer requirements on consumers.
|
||||
74
docs/media/image/exif/src/exif/read-exif.md
Normal file
74
docs/media/image/exif/src/exif/read-exif.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# readExifFromTiffBuffer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Reads EXIF metadata from a raw TIFF byte buffer. Handles the full TIFF IFD chain: IFD0 for image-level tags, ExifIFD (0x8769) for camera settings, and GPS IFD (0x8825) for geolocation data. Supports both big-endian (Motorola, `MM`) and little-endian (Intel, `II`) TIFF byte orders.
|
||||
|
||||
## Features
|
||||
|
||||
- Reads both Motorola (big-endian) and Intel (little-endian) TIFF streams
|
||||
- Maps IFD0 tags: Make, Model, Software, DateTime, Orientation, ImageWidth, ImageHeight
|
||||
- Follows ExifIFD: ExposureTime, FNumber, ISO, FocalLength, FocalLengthIn35mm, Flash, WhiteBalance, ExposureMode, ExposureProgram, DateTimeOriginal, DateTimeDigitized
|
||||
- Follows GPS IFD: latitude, longitude, altitude, timestamp, datestamp, speed, direction
|
||||
- Returns all parsed tags in `rawTags` for custom access
|
||||
- Throws `ExifError(PARSE_ERROR)` on invalid TIFF header
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readExifFromTiffBuffer } from "@suaveplan/exif/exif";
|
||||
|
||||
// Buffer starts at TIFF header (after "Exif\0\0" prefix is stripped)
|
||||
const exif = readExifFromTiffBuffer(tiffBuffer);
|
||||
console.log(exif.make, exif.model);
|
||||
console.log(exif.exposureTime); // { numerator: 1, denominator: 100 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { readExifFromTiffBuffer } from "@suaveplan/exif/exif";
|
||||
import { rationalToNumber } from "@suaveplan/exif/binary";
|
||||
|
||||
const exif = readExifFromTiffBuffer(tiffBuffer);
|
||||
|
||||
if (exif.gps?.latitude) {
|
||||
const { degrees, minutes, seconds, ref } = exif.gps.latitude;
|
||||
const decimal = degrees + minutes / 60 + seconds / 3600;
|
||||
const signed = ref === "S" ? -decimal : decimal;
|
||||
console.log("Latitude:", signed.toFixed(6));
|
||||
}
|
||||
|
||||
if (exif.exposureTime) {
|
||||
const shutter = rationalToNumber(exif.exposureTime);
|
||||
console.log(`Shutter: ${shutter}s`);
|
||||
}
|
||||
|
||||
// Access raw tag by number
|
||||
const customTag = exif.rawTags?.[0x013B]; // Artist tag
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readExifFromTiffBuffer(buffer: Uint8Array): ExifData`
|
||||
|
||||
Parses EXIF metadata from a raw TIFF buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `buffer` — `Uint8Array` starting at the TIFF byte-order mark (`MM` or `II`), not at the JPEG `Exif\0\0` prefix
|
||||
|
||||
**Returns:** `ExifData` with all recognised tags populated.
|
||||
|
||||
**Throws:** `ExifError(PARSE_ERROR)` if:
|
||||
|
||||
- The buffer is shorter than 8 bytes
|
||||
- The byte-order mark is not `0x4D4D` (BE) or `0x4949` (LE)
|
||||
- The TIFF magic number is not `0x002A`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 6-byte `"Exif\0\0"` prefix must be stripped before calling this function — the buffer must start at the TIFF header. The `readMetadata` orchestrator handles this stripping automatically. GPS coordinate rationals are expected to be an array of three RATIONAL values (degrees, minutes, seconds) as per the EXIF GPS IFD specification. The GPS timestamp is parsed from three RATIONAL values and formatted as `HH:MM:SS`. All collected IFD entries from both IFD0 and ExifIFD are merged into `rawTags` for consumer access.
|
||||
66
docs/media/image/exif/src/format/detect.md
Normal file
66
docs/media/image/exif/src/format/detect.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# detectImageFormat
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Identifies the image format of a raw byte buffer by inspecting its leading magic bytes. This is the entry point for all metadata operations in `@suaveplan/exif`: the correct parser is selected based on the detected format, so detection must be reliable before any other processing occurs.
|
||||
|
||||
The implementation avoids file extensions and MIME type strings — it reads only the binary signature, which is the only trustworthy signal for format identification.
|
||||
|
||||
## Features
|
||||
|
||||
- Detects JPEG (3-byte signature), PNG (8-byte signature), WebP (12-byte RIFF signature), and TIFF (both big-endian and little-endian variants)
|
||||
- Returns `'unknown'` rather than throwing for unrecognised formats
|
||||
- Zero dependencies — pure TypeScript with no external imports
|
||||
- Handles buffers shorter than the minimum signature length gracefully
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { detectImageFormat } from "@suaveplan/exif/format";
|
||||
|
||||
const jpegBytes = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0, ...]);
|
||||
console.log(detectImageFormat(jpegBytes)); // "jpeg"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { detectImageFormat } from "@suaveplan/exif/format";
|
||||
import { readMetadata } from "@suaveplan/exif";
|
||||
|
||||
async function processImage(buffer: Uint8Array) {
|
||||
const format = detectImageFormat(buffer);
|
||||
|
||||
switch (format) {
|
||||
case "jpeg":
|
||||
case "png":
|
||||
return readMetadata(buffer);
|
||||
case "webp":
|
||||
case "tiff":
|
||||
throw new Error(`Format ${format} metadata reading not yet supported`);
|
||||
case "unknown":
|
||||
throw new Error("Unrecognised image format");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `detectImageFormat(buffer: Uint8Array): ImageFormat`
|
||||
|
||||
Inspects the leading bytes of `buffer` and returns one of:
|
||||
|
||||
- `"jpeg"` — buffer starts with `FF D8 FF`
|
||||
- `"png"` — buffer starts with `89 50 4E 47 0D 0A 1A 0A`
|
||||
- `"webp"` — buffer starts with `RIFF` and bytes 8–11 are `WEBP`
|
||||
- `"tiff"` — buffer starts with `49 49 2A 00` (LE) or `4D 4D 00 2A` (BE)
|
||||
- `"unknown"` — none of the above match, or buffer is shorter than 4 bytes
|
||||
|
||||
Does not throw. All inputs are handled.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The magic-byte checks are ordered by specificity: JPEG and PNG checks come first because they are the primary supported formats. WebP requires a 12-byte buffer to read both the RIFF header and the WEBP brand. TIFF detection handles both byte orders since TIFF files start with their own byte-order mark. The function returns `"unknown"` for buffers shorter than 4 bytes rather than attempting a partial match, to avoid false positives on truncated data.
|
||||
86
docs/media/image/exif/src/ifd/parse.md
Normal file
86
docs/media/image/exif/src/ifd/parse.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# IFD Parse
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements TIFF IFD (Image File Directory) parsing, the core data structure used by EXIF to store metadata. Each IFD entry is a 12-byte record containing a tag number, data type, count, and either an inline value or an offset to data elsewhere in the TIFF stream. This module handles all 12 standard TIFF data types and provides typed accessor helpers for downstream EXIF, GPS, and sub-IFD readers.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses all 12 TIFF primitive types: BYTE, ASCII, SHORT, LONG, RATIONAL, SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT, DOUBLE
|
||||
- Inline vs. offset-stored value selection based on total data size
|
||||
- Graceful handling of malformed entries (skips rather than throwing)
|
||||
- Typed accessors: `getTagNumber`, `getTagString`, `getTagRational`, `getTagRationalArray`
|
||||
- Bounds-checked with `ExifError(PARSE_ERROR)` on structural violations
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
import { parseIfd, getTagString } from "@suaveplan/exif";
|
||||
|
||||
const reader = new ByteReader(tiffBuffer, "BE");
|
||||
const ifd0 = parseIfd(reader, 8);
|
||||
|
||||
const make = getTagString(ifd0, 0x010F); // Make tag
|
||||
const model = getTagString(ifd0, 0x0110); // Model tag
|
||||
console.log(`${make} ${model}`);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ByteReader } from "@suaveplan/exif/binary";
|
||||
import { parseIfd, getTagNumber, getTagRational } from "@suaveplan/exif";
|
||||
|
||||
const reader = new ByteReader(tiffBuffer, "LE");
|
||||
|
||||
// Parse IFD0 then follow ExifIFD pointer
|
||||
const ifd0 = parseIfd(reader, 8);
|
||||
const exifIfdOffset = getTagNumber(ifd0, 0x8769); // ExifIFD pointer
|
||||
|
||||
if (exifIfdOffset !== undefined) {
|
||||
const exifIfd = parseIfd(reader, exifIfdOffset);
|
||||
const exposureTime = getTagRational(exifIfd, 0x829A);
|
||||
const fNumber = getTagRational(exifIfd, 0x829D);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `IfdEntry`
|
||||
|
||||
```ts
|
||||
interface IfdEntry {
|
||||
tag: number;
|
||||
type: number;
|
||||
count: number;
|
||||
value: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### `parseIfd(reader: ByteReader, ifdOffset: number): Map<number, IfdEntry>`
|
||||
|
||||
Parses the IFD at `ifdOffset` within `reader`. Returns a `Map<tag, IfdEntry>`. Throws `ExifError(PARSE_ERROR)` if the IFD count field extends beyond the buffer. Malformed individual entries are silently skipped.
|
||||
|
||||
### `getTagNumber(entries: Map<number, IfdEntry>, tag: number): number | undefined`
|
||||
|
||||
Extracts a SHORT or LONG tag as a number.
|
||||
|
||||
### `getTagString(entries: Map<number, IfdEntry>, tag: number): string | undefined`
|
||||
|
||||
Extracts an ASCII tag as a string.
|
||||
|
||||
### `getTagRational(entries: Map<number, IfdEntry>, tag: number): Rational | undefined`
|
||||
|
||||
Extracts a single RATIONAL or SRATIONAL tag.
|
||||
|
||||
### `getTagRationalArray(entries: Map<number, IfdEntry>, tag: number): Rational[] | undefined`
|
||||
|
||||
Extracts an array of RATIONAL values (e.g. GPS coordinate triples where each axis is three RATIONALs for degrees/minutes/seconds).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Inline storage applies when `typeSize * count <= 4`: the value is packed into the 4-byte value/offset field of the IFD entry. For larger values, the field contains a file offset to the actual data. ASCII strings have trailing nulls stripped. The `UNDEFINED` type (7) returns a raw `Uint8Array`. Sign extension for SBYTE uses the left-shift/right-shift trick `<< 24 >> 24`.
|
||||
73
docs/media/image/exif/src/iptc/read-iptc.md
Normal file
73
docs/media/image/exif/src/iptc/read-iptc.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# readIptc
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Parses IPTC IIM (Information Interchange Model) metadata from a Photoshop 3.0 APP13 JPEG segment payload. IPTC is the editorial metadata standard used by news agencies and photo distributors for fields such as headline, caption, copyright, keywords, and geographic information. The parser traverses the Photoshop 8BIM resource block structure to locate the IPTC-NAA data block (resource ID 0x0404) and then iterates its Record 2 datasets.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses the Photoshop 3.0 + 8BIM wrapper before accessing IPTC data
|
||||
- Extracts all standard Record 2 editorial fields: headline, caption, copyright, creator, keywords, city, country code, country name, source
|
||||
- Handles repeatable fields (keywords) correctly as arrays
|
||||
- Returns an empty object gracefully if no IPTC data is present
|
||||
- Zero external dependencies — pure TypeScript byte-level parsing
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readIptc } from "@suaveplan/exif/iptc";
|
||||
|
||||
const iptc = readIptc(app13SegmentData);
|
||||
console.log(iptc.headline);
|
||||
console.log(iptc.keywords); // string[]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseJpegSegments, isIptcSegment } from "@suaveplan/exif";
|
||||
import { readIptc } from "@suaveplan/exif/iptc";
|
||||
|
||||
const segments = parseJpegSegments(jpegBuffer);
|
||||
const iptcSeg = segments.find(isIptcSegment);
|
||||
|
||||
if (iptcSeg) {
|
||||
const iptc = readIptc(iptcSeg.data);
|
||||
console.log("Headline:", iptc.headline);
|
||||
console.log("Copyright:", iptc.copyright);
|
||||
console.log("Creator:", iptc.creator);
|
||||
console.log("Keywords:", iptc.keywords?.join(", "));
|
||||
console.log("Location:", [iptc.city, iptc.country].filter(Boolean).join(", "));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readIptc(data: Uint8Array): IptcData`
|
||||
|
||||
Parses IPTC data from a raw APP13 segment payload (the full `data` field of a `JpegSegment` with marker `0xFFED`).
|
||||
|
||||
**Returns:** `IptcData` object with all recognisable fields populated, or an empty `{}` if the data does not match the Photoshop 3.0 + 8BIM + IPTC structure.
|
||||
|
||||
**Does not throw.** Malformed or truncated data results in partial or empty output.
|
||||
|
||||
### Dataset mapping
|
||||
|
||||
| Dataset code | IptcData field |
|
||||
|---|---|
|
||||
| Record 2, DS 5 (0x05) | `headline` |
|
||||
| Record 2, DS 80 (0x50) | `creator` |
|
||||
| Record 2, DS 120 (0x78) | `caption` |
|
||||
| Record 2, DS 25 (0x19) | `keywords[]` |
|
||||
| Record 2, DS 90 (0x5A) | `city` |
|
||||
| Record 2, DS 100 (0x64) | `countryCode` |
|
||||
| Record 2, DS 101 (0x65) | `country` |
|
||||
| Record 2, DS 116 (0x74) | `copyright` |
|
||||
| Record 2, DS 115 (0x73) | `source` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Photoshop 3.0 wrapper uses Pascal strings for resource names (1-byte length followed by the name, padded to an even byte boundary). The 8BIM block iteration stops on the first non-`8BIM` signature to avoid reading into compressed image data. IPTC record markers are `0x1C` followed by record number and dataset number. Only Record 2 (Application Record) datasets are extracted; Record 1 (Envelope Record) envelope fields are skipped. The string decoder uses Latin-1 (one byte per character); IPTC IIM data is specified as 7-bit ASCII.
|
||||
83
docs/media/image/exif/src/jpeg/segments.md
Normal file
83
docs/media/image/exif/src/jpeg/segments.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# JPEG Segments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Parses the segment structure of a JPEG file and identifies metadata segments (EXIF, XMP, IPTC). JPEG is a container format: each segment is introduced by a 2-byte marker (`0xFF` + type), followed by a 2-byte big-endian length and payload. This module provides the structural parser that upstream readers (EXIF, IPTC, XMP) rely on to locate their data.
|
||||
|
||||
## Features
|
||||
|
||||
- `parseJpegSegments` — iterates all APP/marker segments up to and including SOS
|
||||
- Handles padding bytes (multiple consecutive `0xFF`) between segments
|
||||
- `isExifSegment` — identifies APP1 segments with the `Exif\0\0` header
|
||||
- `isXmpSegment` — identifies APP1 segments with the XMP namespace header
|
||||
- `isIptcSegment` — identifies APP13 segments with the `Photoshop 3.0\0` header
|
||||
- Bounds-checking with `ExifError(PARSE_ERROR)` on malformed input
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseJpegSegments, isExifSegment } from "@suaveplan/exif";
|
||||
|
||||
const segments = parseJpegSegments(jpegBuffer);
|
||||
const exifSeg = segments.find(isExifSegment);
|
||||
if (exifSeg) {
|
||||
console.log("EXIF data found, length:", exifSeg.data.length);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseJpegSegments, isExifSegment, isXmpSegment, isIptcSegment } from "@suaveplan/exif";
|
||||
|
||||
const segments = parseJpegSegments(jpegBuffer);
|
||||
|
||||
for (const seg of segments) {
|
||||
if (isExifSegment(seg)) {
|
||||
console.log(`EXIF APP1 at offset ${seg.offset}, ${seg.data.length} bytes`);
|
||||
} else if (isXmpSegment(seg)) {
|
||||
console.log(`XMP APP1 at offset ${seg.offset}`);
|
||||
} else if (isIptcSegment(seg)) {
|
||||
console.log(`IPTC APP13 at offset ${seg.offset}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `JpegSegment`
|
||||
|
||||
```ts
|
||||
interface JpegSegment {
|
||||
marker: number; // e.g. 0xFFE1 for APP1
|
||||
data: Uint8Array; // payload (length field excluded)
|
||||
offset: number; // byte offset of marker in original buffer
|
||||
}
|
||||
```
|
||||
|
||||
### `parseJpegSegments(buffer: Uint8Array): JpegSegment[]`
|
||||
|
||||
Parses all JPEG segments from `buffer`. Returns segments from SOI through SOS (inclusive). Throws `ExifError(PARSE_ERROR)` if:
|
||||
|
||||
- The buffer does not start with `FF D8` (SOI)
|
||||
- A segment's length field is less than 2
|
||||
- A segment's data extends beyond the buffer
|
||||
|
||||
### `isExifSegment(seg: JpegSegment): boolean`
|
||||
|
||||
Returns `true` if `seg` is an APP1 segment (`0xFFE1`) whose payload begins with `Exif\0\0`.
|
||||
|
||||
### `isXmpSegment(seg: JpegSegment): boolean`
|
||||
|
||||
Returns `true` if `seg` is an APP1 segment whose payload begins with `http://ns.adobe.com/xap/1.0/\0`.
|
||||
|
||||
### `isIptcSegment(seg: JpegSegment): boolean`
|
||||
|
||||
Returns `true` if `seg` is an APP13 segment (`0xFFED`) whose payload begins with `Photoshop 3.0\0`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The parser skips padding `0xFF` bytes between markers as permitted by the JFIF specification. Standalone markers (SOI, EOI, RST0–RST7) have no length field and are handled as zero-data segments. Parsing stops at the SOS marker to avoid processing compressed image data as segments. The `data` field of each segment excludes the 2-byte length field, so `data.length === segmentLength - 2`.
|
||||
93
docs/media/image/exif/src/xmp/read-xmp.md
Normal file
93
docs/media/image/exif/src/xmp/read-xmp.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# readXmp
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Parses XMP (Extensible Metadata Platform) metadata from a raw XMP XML string extracted from a JPEG APP1 segment. XMP is Adobe's XML-based metadata format built on RDF/XML. This module extracts the most commonly used Dublin Core fields (dc:creator, dc:description, dc:rights, dc:subject, dc:title) and XMP Rights fields using pattern-based extraction rather than a full XML parser, trading completeness for zero dependencies.
|
||||
|
||||
## Features
|
||||
|
||||
- Extracts dc:creator as a string array (from rdf:Seq)
|
||||
- Extracts dc:description as a language-keyed Record (from rdf:Alt)
|
||||
- Extracts dc:rights or xmpRights:UsageTerms as a plain string
|
||||
- Extracts dc:subject as a string array (from rdf:Bag)
|
||||
- Extracts dc:title as a language-keyed Record (from rdf:Alt)
|
||||
- Always returns the raw XMP XML string in the `raw` field
|
||||
- Never throws — partial results on malformed or missing fields
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { readXmp } from "@suaveplan/exif/xmp";
|
||||
|
||||
const xml = `<x:xmpmeta><rdf:RDF>
|
||||
<rdf:Description>
|
||||
<dc:creator><rdf:Seq><rdf:li>Jane Doe</rdf:li></rdf:Seq></dc:creator>
|
||||
<dc:rights>© 2024 Jane Doe</dc:rights>
|
||||
</rdf:Description>
|
||||
</rdf:RDF></x:xmpmeta>`;
|
||||
|
||||
const xmp = readXmp(xml);
|
||||
console.log(xmp.creator); // ["Jane Doe"]
|
||||
console.log(xmp.rights); // "© 2024 Jane Doe"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { readXmp } from "@suaveplan/exif/xmp";
|
||||
|
||||
const xml = `<x:xmpmeta>
|
||||
<rdf:RDF>
|
||||
<rdf:Description>
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang="en">Sunset over mountains</rdf:li>
|
||||
<rdf:li xml:lang="fr">Coucher de soleil sur les montagnes</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>landscape</rdf:li>
|
||||
<rdf:li>sunset</rdf:li>
|
||||
<rdf:li>mountains</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>`;
|
||||
|
||||
const xmp = readXmp(xml);
|
||||
console.log(xmp.title?.["en"]); // "Sunset over mountains"
|
||||
console.log(xmp.title?.["fr"]); // "Coucher de soleil sur les montagnes"
|
||||
console.log(xmp.subject); // ["landscape", "sunset", "mountains"]
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `readXmp(xmlString: string): XmpData`
|
||||
|
||||
Parses XMP metadata from `xmlString`.
|
||||
|
||||
**Returns:** `XmpData` with all recognised fields populated. Always includes `raw: xmlString`. Fields absent from the XMP are `undefined` in the result.
|
||||
|
||||
**Does not throw.** Returns `{ raw: xmlString }` on empty or unrecognised input.
|
||||
|
||||
### `XmpData`
|
||||
|
||||
```ts
|
||||
interface XmpData {
|
||||
raw: string;
|
||||
creator?: string[];
|
||||
description?: Record<string, string>;
|
||||
rights?: string;
|
||||
subject?: string[];
|
||||
title?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The extraction uses three internal helpers: `extractTextNode` for simple inline values, `extractRdfList` for `rdf:Bag`/`rdf:Seq` item arrays, and `extractRdfAlt` for `rdf:Alt` language maps. Pattern matching is tolerant of namespace prefixes (any prefix or no prefix). The `rdf:Alt` extraction captures optional `xml:lang` attributes and defaults to `"x-default"` when absent. This approach handles the most common XMP output from Lightroom, Photoshop, and camera firmware without the complexity and size of a full DOM parser.
|
||||
Reference in New Issue
Block a user