@portable/exif
Functions
Exif.parse
Reads EXIF metadata from a JPEG, TIFF, PNG, WebP, HEIC or AVIF file, or from raw EXIF data that
starts with Exif\0\0 or a TIFF header. The input can be just the start of the file: tags
with values past the end of the input are left out.
function parse(bytes: Uint8Array): Metadata;import * as Exif from '@portable/exif';
const response = await fetch('https://example.com/photo.jpg', {
headers: { range: 'bytes=0-65535' },
});
const exif = Exif.parse(new Uint8Array(await response.arrayBuffer()));
console.log(
exif.ifd0.Make,
exif.exif.DateTimeOriginal,
exif.latitude,
exif.longitude,
);Exif.parseDate
Parses an EXIF date such as '2013:05:10 15:21:35' into a Date. Takes a tag value as parse
returns it, so a missing tag or a value that isn't a string returns undefined. EXIF dates have
no time zone, so pass the matching OffsetTime* tag as the offset option ('+02:00').
Without one, the date is read as UTC, so getUTCHours() and the other getUTC* methods return
the time the camera recorded. Also accepts the ISO 8601 form '2004-09-04T23:39:06-08:00' that some
writers use. Returns undefined for anything else, including the all-zero dates that cameras
write when the clock is unset.
function parseDate(
value: TagValue | undefined,
options: parseDate.Options = {},
): Date | undefined;| Option | Type | Default | Description |
|---|---|---|---|
offset | TagValue | UTC offset such as '+02:00', used when value has none. Pass the matching OffsetTime* tag as it is. A value that isn't a string offset is ignored. |
import * as Exif from '@portable/exif';
const { exif } = Exif.parse(bytes);
const taken = Exif.parseDate(exif.DateTimeOriginal, {
offset: exif.OffsetTimeOriginal,
});Exif.parseStream
Reads EXIF metadata from a stream of a JPEG, TIFF, PNG, WebP, HEIC or AVIF file, or of raw EXIF
data. It reads only until the metadata is found, cancels the stream, and returns what parse
returns for the whole file.
async function parseStream(
stream: ReadableStream<Uint8Array>,
options: parseStream.Options = {},
): Promise<Metadata>;| Option | Type | Default | Description |
|---|---|---|---|
maxBytes | number | 16777216 | Most bytes of the stream to hold in memory at once. Bytes that are skipped without being kept don't count. |
signal | AbortSignal | Aborting rejects with signal.reason and cancels the stream. |
Throws
MaxBytesErrorReading the metadata needs more thanmaxBytesof the stream in memory.signal.reasonwhensignalis aborted.
import * as Exif from '@portable/exif';
const response = await fetch('https://example.com/photo.jpg');
const exif = await Exif.parseStream(response.body!);
console.log(exif.ifd0.Make, exif.exif.DateTimeOriginal);Exif.strip
Removes the EXIF and XMP metadata from a JPEG, PNG or WebP file without re-encoding it. Every byte of the image data is copied unchanged.
function strip(
bytes: Uint8Array,
options: strip.Options = {},
): Uint8Array<ArrayBuffer>;| Option | Type | Default | Description |
|---|---|---|---|
keepOrientation | boolean | true | Write back an EXIF block that holds only the Orientation tag, when the original has one other than 1, so that viewers and resizers still rotate the image. |
keepColorProfile | boolean | true | Keep the ICC color profile: the JPEG ICC_PROFILE APP2 segments, the PNG iCCP chunk or the WebP ICCP chunk. |
Throws
UnsupportedErrorThe input is not a JPEG, PNG or WebP file.InvalidInputErrorA JPEG has something other than a marker between its segments, or a segment length below 2.
import * as Exif from '@portable/exif';
const photo = new Uint8Array(await file.arrayBuffer());
const stripped = Exif.strip(photo);
Exif.parse(stripped).gps; // {}Classes
Exif.StripStream
A TransformStream that removes the EXIF and XMP metadata from a JPEG, PNG or WebP file, as
strip does. A JPEG or PNG is written out as it arrives, and the stream holds at most one
metadata segment or chunk. A WebP is held whole, because its RIFF header and VP8X flags come
before the chunks that change them.
class StripStream extends TransformStream<Uint8Array, Uint8Array> {
constructor(options: StripStream.Options = {});
}| Option | Type | Default | Description |
|---|---|---|---|
keepOrientation | boolean | true | Write back an EXIF block that holds only the Orientation tag, when the original has one other than 1, so that viewers and resizers still rotate the image. |
keepColorProfile | boolean | true | Keep the ICC color profile: the JPEG ICC_PROFILE APP2 segments, the PNG iCCP chunk or the WebP ICCP chunk. |
signal | AbortSignal | Aborting errors the stream with signal.reason. |
import * as Exif from '@portable/exif';
const [stored, inspected] = request.body!.tee();
const [, exif] = await Promise.all([
bucket.put(key, stored.pipeThrough(new Exif.StripStream())),
Exif.parseStream(inspected),
]);Errors
Exif.InvalidInputError
Thrown when EXIF data has a malformed TIFF header, or strip finds a malformed JPEG.
class InvalidInputError extends Errors.InvalidInputError {
readonly kind: string;
}import * as Exif from '@portable/exif';
try {
Exif.parse(photo);
} catch (error) {
if (error instanceof Exif.InvalidInputError) {
console.log('corrupt EXIF data');
}
}Exif.MaxBytesError
Thrown when parseStream would need to hold more than maxBytes of the stream at once.
class MaxBytesError extends Errors.MaxBytesError {
constructor(options: Errors.MaxBytesError.Options);
readonly kind: string;
}import * as Exif from '@portable/exif';
const response = await fetch('https://example.com/photo.jpg');
try {
await Exif.parseStream(response.body!, { maxBytes: 1_000_000 });
} catch (error) {
if (error instanceof Exif.MaxBytesError) {
console.log(`needs more than ${error.maxBytes} bytes`);
}
}Exif.TruncatedError
Thrown when TIFF or raw EXIF input ends before its 8-byte header.
class TruncatedError extends Errors.TruncatedError {
constructor(
message: string = 'Input ended inside the TIFF header.',
options?: ErrorOptions,
);
readonly kind: string;
}import * as Exif from '@portable/exif';
try {
Exif.parse(new Uint8Array([0x49, 0x49, 0x2a, 0x00]));
} catch (error) {
if (error instanceof Exif.TruncatedError) {
console.log(error.message);
}
}Exif.UnsupportedError
Thrown when the input is not JPEG, TIFF, PNG, WebP, HEIF or raw EXIF data, or when strip gets a
format other than JPEG, PNG or WebP.
class UnsupportedError extends Errors.UnsupportedError {
constructor(
message: string = 'Unsupported format: expected JPEG, TIFF, PNG, WebP, HEIF or EXIF data.',
options?: ErrorOptions,
);
readonly kind: string;
}import * as Exif from '@portable/exif';
try {
Exif.parse(new TextEncoder().encode('GIF89a'));
} catch (error) {
if (error instanceof Exif.UnsupportedError) {
console.log(error.message);
}
}Types
Exif.Metadata
Metadata read by parse. Every IFD is an object, empty when the file doesn't have it.
type Metadata = Ifds & {
latitude?: number | undefined;
longitude?: number | undefined;
imageSize?:
| {
width: number;
height: number;
}
| undefined;
thumbnail?:
| {
offset: number;
length: number;
bytes: Uint8Array | undefined;
}
| undefined;
};Exif.Tags
Tags of one IFD by name. A tag missing from the name table is keyed by its hex id, such as
'0xC4A5'.
type Tags = {
readonly [name: string]: TagValue;
};Exif.TagValue
A tag value. ASCII tags are strings cut at the first NUL. Numeric tags, rationals included, are a
number when they hold one value and an array otherwise. UNDEFINED tags are a Uint8Array view
into the input.
type TagValue = string | number | readonly number[] | Uint8Array;