Skip to content
LogoLogo

@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;
OptionTypeDefaultDescription
offsetTagValueUTC 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>;
OptionTypeDefaultDescription
maxBytesnumber16777216Most bytes of the stream to hold in memory at once. Bytes that are skipped without being kept don't count.
signalAbortSignalAborting rejects with signal.reason and cancels the stream.

Throws

  • MaxBytesError Reading the metadata needs more than maxBytes of the stream in memory.
  • signal.reason when signal is 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>;
OptionTypeDefaultDescription
keepOrientationbooleantrueWrite 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.
keepColorProfilebooleantrueKeep the ICC color profile: the JPEG ICC_PROFILE APP2 segments, the PNG iCCP chunk or the WebP ICCP chunk.

Throws

  • UnsupportedError The input is not a JPEG, PNG or WebP file.
  • InvalidInputError A 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 = {});
}
OptionTypeDefaultDescription
keepOrientationbooleantrueWrite 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.
keepColorProfilebooleantrueKeep the ICC color profile: the JPEG ICC_PROFILE APP2 segments, the PNG iCCP chunk or the WebP ICCP chunk.
signalAbortSignalAborting 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;