inei
v2.0.0
Published
EXIF and XMP metadata parser for JPEG, PNG, and HEIC/HEIF/AVIF. Zero dependencies. TypeScript.
Maintainers
Readme
inei
EXIF and XMP metadata parser for JPEG, PNG, and HEIC/HEIF/AVIF. Written in TypeScript. Zero dependencies. Runs in browsers and Node.js.
Built as a modern replacement for exifr, which has been unmaintained since 2022.
Install
pnpm add ineinpm install ineiQuick start
import { parseExif } from "inei";
const buffer = await file.arrayBuffer();
const result = parseExif(buffer);
if (result.ok) {
const { image, tags, xmp } = result.data;
}parseExif returns a discriminated union. On success, result.ok is true and result.data contains the parsed metadata. On failure, result.ok is false and result.error is an ExifError with a code and message.
type Result =
| { ok: true; data: ParsedExif }
| { ok: false; error: ExifError };Format support
| Format | EXIF | Image size | XMP | Thumbnail | | -------------- | ---- | ---------- | ------------ | --------- | | JPEG | Yes | Yes (SOF) | Yes (APP1) | Yes | | PNG | Yes (eXIf chunk) | Yes (IHDR) | Yes (iTXt) | Yes | | HEIC/HEIF/AVIF | Yes (ISOBMFF) | Yes (ispe) | Yes (mime item) | Yes |
Format is auto-detected from magic bytes. You do not need to specify it.
API
parseExif(input, options?)
Synchronous. Accepts ArrayBuffer, Uint8Array, or Buffer.
import { parseExif } from "inei";
const result = parseExif(buffer);parseExifFromBlob(input, options?)
Async. Accepts everything parseExif does, plus Blob and File. Use this in browsers when working with file inputs directly.
import { parseExifFromBlob } from "inei";
const input = document.querySelector("input[type=file]");
const file = input.files[0];
const result = await parseExifFromBlob(file);gps(input)
Returns { latitude: number; longitude: number } or undefined.
import { gps } from "inei";
const coords = gps(buffer);
// { latitude: 37.7749, longitude: -122.4194 }orientation(input)
Returns the EXIF orientation value (1-8) or undefined.
import { orientation } from "inei";
const o = orientation(buffer); // 1 = normal, 6 = rotated 90 CW, etc.thumbnail(input)
Returns the embedded JPEG thumbnail as a Uint8Array, or undefined.
import { thumbnail } from "inei";
const bytes = thumbnail(buffer);thumbnailUrl(input)
Browser only. Returns a blob: URL for the embedded thumbnail, or undefined.
import { thumbnailUrl } from "inei";
img.src = thumbnailUrl(buffer);extractThumbnail(input, parsedExif)
Lower-level thumbnail extraction. Use this when you already have a ParsedExif result and want to avoid parsing twice.
import { parseExif, extractThumbnail } from "inei";
const result = parseExif(buffer);
if (result.ok && result.data.thumbnail) {
const bytes = extractThumbnail(buffer, result.data);
}Options
All options are optional. Defaults are shown.
parseExif(buffer, {
readBinaryTags: false, // Include UNDEFINED (binary blob) tags
resolveTagNames: true, // Map numeric tag IDs to human-readable names
simplifyValues: true, // Convert rationals to floats, dates to epoch seconds, GPS to decimal degrees
imageSize: true, // Extract width/height from container (SOF, IHDR, ispe)
hidePointers: true, // Omit internal IFD pointer tags (ExifOffset, GPSInfo, etc.)
returnTags: true, // Include the tag map in the result
includeFormatted: false, // Include pre-formatted strings for common tags
});Result shape
type ParsedExif = {
image?: { width: number; height: number };
thumbnail?: ThumbnailInfo;
tags: Partial<ExifTagMap>; // Simplified values (numbers, strings, decimal degrees)
tagsRaw: Record<string, unknown>; // Same keys, before simplification
formattedTags?: Record<string, string>; // Only present when includeFormatted: true
xmp?: XmpData; // Only present when XMP metadata exists
};Tags
With simplifyValues: true (the default), values are post-processed:
- Rationals (
[num, den]) become floats (num / den) - Dates (
"2023:10:22 18:46:07") become epoch seconds (1697999167) - GPS DMS arrays become decimal degrees (
37.7749) - Timezone offsets are applied to date values
The tags object is typed as Partial<ExifTagMap>. Known tags have typed keys. Unknown tags appear as tag_0x{hex}.
EXIF tags extracted
Camera: Make, Model, Software, SerialNumber
Lens: LensMake, LensModel, LensInfo, LensSpecification, LensSerialNumber
Exposure: ExposureTime, FNumber, ExposureProgram, ISO, ExposureCompensation, ShutterSpeedValue, ApertureValue, MaxApertureValue, MeteringMode, Flash, ExposureMode
Focus: FocalLength, FocalLengthIn35mmFormat, FocalPlaneXResolution, FocalPlaneYResolution, FocalPlaneResolutionUnit
Image: ImageWidth, ImageHeight, Orientation, XResolution, YResolution, ResolutionUnit, ColorSpace, WhiteBalance, CustomRendered, SceneCaptureType
Date: DateTimeOriginal, CreateDate, ModifyDate, OffsetTime, OffsetTimeOriginal, OffsetTimeDigitized, SubSecTimeOriginal, SubSecTimeDigitized
GPS: GPSLatitude, GPSLongitude, GPSAltitude, GPSLatitudeRef, GPSLongitudeRef, GPSAltitudeRef, GPSTimeStamp, GPSDateStamp, GPSVersionID
XMP fields extracted
When present, result.data.xmp contains:
type XmpData = {
title?: string; // dc:title
description?: string; // dc:description
creator?: string; // dc:creator
subject?: string[]; // dc:subject (keywords/tags)
rating?: number; // xmp:Rating
label?: string; // xmp:Label
createDate?: string; // xmp:CreateDate
};XMP is extracted from JPEG (APP1) and PNG (iTXt) automatically.
Formatted output
Pass includeFormatted: true to get human-readable strings for common tags:
const result = parseExif(buffer, { includeFormatted: true });
result.data.formattedTags;
// {
// Shutter: "1/800",
// Aperture: "f/10",
// ISO: "ISO 400",
// FocalLength: "50mm",
// FocalLengthIn35mmFormat: "75mm",
// ExposureCompensation: "0 EV",
// ExposureProgram: "Normal program",
// MeteringMode: "Multi-segment",
// Flash: "Off, did not fire",
// Orientation: "Normal",
// GPS: "37.7749° N, 122.4194° W",
// DateTimeOriginal: "Oct 22, 2023, 6:46 PM",
// Make: "Canon",
// Model: "EOS R6",
// LensMake: "Canon",
// LensModel: "RF50mm F1.2 L USM",
// }Individual formatters
Available from inei/format:
import {
formatShutter,
formatAperture,
formatFocalLength,
formatDate,
formatOrientation,
formatExposureCompensation,
formatExposureProgram,
formatMeteringMode,
formatFlash,
formatGPSCoordinate,
} from "inei/format";
formatShutter(0.00125); // "1/800"
formatAperture(2.8); // "f/2.8"
formatFocalLength(50); // "50mm"
formatDate(1697999167); // "Oct 22, 2023, 6:46 PM"
formatOrientation(6); // "Rotated 90° CW"
formatExposureCompensation(-0.7); // "-0.7 EV"
formatExposureProgram(3); // "Aperture priority"
formatMeteringMode(5); // "Multi-segment"
formatFlash(0x10); // "Off, did not fire"
formatGPSCoordinate(37.77, -122.42); // "37.77° N, 122.42° W"Batch formatter
Available from inei/format-tags:
import { computeFormattedTags } from "inei/format-tags";
const formatted = computeFormattedTags(result.data.tags);Standalone XMP parser
Available from inei/xmp:
import { parseXmp } from "inei/xmp";
const data = parseXmp(xmlString);Export paths
| Path | Contents |
| ----------------- | ----------------------------------------------------- |
| inei | parseExif, parseExifFromBlob, gps, orientation, thumbnail, thumbnailUrl, extractThumbnail, ExifError, types |
| inei/format | Individual formatting functions |
| inei/format-tags| computeFormattedTags |
| inei/xmp | parseXmp, extractXmpFromApp1, extractXmpFromPngITxt |
Error handling
parseExif never throws. It returns { ok: false, error } on failure.
const result = parseExif(buffer);
if (!result.ok) {
console.error(result.error.code, result.error.message);
}Error codes: NOT_JPEG, NO_EXIF, INVALID_TIFF, TRUNCATED, UNKNOWN.
The convenience functions (gps, orientation, thumbnail, thumbnailUrl) return undefined on failure.
Constraints
- ESM only. No CommonJS support.
- Reads the entire buffer into memory. No chunked or streaming reads.
- IPTC and ICC profile parsing are not supported.
- MakerNote data (vendor-specific extensions) is not decoded.
License
ISC
