exif-parser
exif-parser is a synchronous, dependency-free JPEG metadata reader for Node buffers and browser ArrayBuffers. It walks JPEG sections, decodes EXIF TIFF directories, resolves a fixed table of tag names, simplifies rational values, converts GPS coordinates and selected dates, reports JPEG dimensions, and can locate or extract an embedded EXIF thumbnail. It reads metadata only; it does not decode pixels, apply orientation, validate authenticity, remove privacy data, write tags, or support modern image containers.
Keep exif-parser only for narrow, tested JPEG metadata paths that depend on its exact old output. For new upload, browser, or multi-format work, exifr or ExifReader provides a maintained surface and avoids several unreleased fixes and lossy conversion surprises.
Use it if
- You maintain an existing JPEG-only pipeline that already expects version 0.1.12's tag names and timestamp conversions
- You need a small synchronous reader with no runtime dependencies and can catch failures from untrusted binary input
- You only need common EXIF tags, JPEG dimensions, GPS coordinates, or an embedded JPEG thumbnail
- You can pass a Node Buffer or browser ArrayBuffer and do not need streams, TypeScript declarations, writes, XMP, or IPTC
- You accept HEIC, HEIF, AVIF, WebP, PNG, RAW, or standalone TIFF: the parser always enters a JPEG section reader and throws when the data does not start with JPEG markers
- You need current maintenance or browser packaging: npm 0.1.12 was published in July 2017, has no TypeScript declarations or bundled browser build, and later repository fixes were never released to npm
- You need comprehensive or precise metadata: tag names come from a fixed old table, strings differ between Node UTF-8 and browser byte-to-character decoding, signed SHORT and LONG formats are read with unsigned methods, and maker notes, XMP, and IPTC are outside the API
- You need trustworthy capture times: simplified EXIF dates without an offset are interpreted as UTC even though camera EXIF DateTimeOriginal normally describes local wall time without a timezone
- You need to edit, strip, or enforce metadata policy: this package only returns values, and neither GPS removal nor pixel rotation for the EXIF Orientation tag is performed
Setup reality
npm install exif-parser adds a CommonJS package with no runtime dependencies, peer requirements, native compilation, config, credentials, engines declaration, ESM exports, or TypeScript declarations. In Node, pass a Buffer; in a browser, pass a real ArrayBuffer. Typed-array views are not documented inputs, so slice the exact underlying byte range when a Uint8Array has a nonzero offset. create() detects the environment through ArrayBuffer and otherwise assumes Node Buffer methods. parse() is synchronous and can throw on non-JPEG data, truncated sections, invalid offsets, malformed TIFF headers, unsupported formats, and out-of-range DataView or Buffer reads. Treat every upload as hostile, cap input size, and catch errors. The README says fetching the first 65,635 bytes is enough because JPEG APP1 is limited to 65,535 bytes and appears near the start; that optimization is useful for EXIF, but a Range request can be ignored by a server and image dimensions or unusual layouts still deserve testing. Defaults resolve known tag ids to names, simplify singletons and rational pairs, convert GPS DMS to signed decimal degrees, interpret a few date strings as Unix seconds, read image dimensions, omit pointer and binary tags, and return all other tags. These conveniences are lossy. Disable simplification or name resolution when exact section and type ids matter. Browser users do not get a browser field or dist file from npm; the README sends them to a separate prebuilt-bundle repository or an old Makefile using Browserify and Uglify. Embedded thumbnails can be JPEG or TIFF according to hasThumbnail, but getThumbnailSize always invokes the JPEG section parser, so use it only for a JPEG thumbnail. Parsing metadata does not prove it is accurate or safe to display, and returned GPS fields are privacy-sensitive.
Patterns
Parse EXIF from a Node bufferparse-node-buffer
const fs = require('node:fs');
const exif = require('exif-parser');
const buffer = fs.readFileSync('photo.jpg');
let result;
try {
result = exif.create(buffer).parse();
} catch (error) {
throw new Error(`Invalid or unsupported JPEG: ${error.message}`);
}
console.log(result.tags);parse is synchronous and throws for malformed, truncated, or non-JPEG input. Cap upload sizes and reject errors rather than assuming every image-like file is parseable.
Parse a browser File as ArrayBufferparse-browser-file
async function readExif(file) {
const arrayBuffer = await file.arrayBuffer();
return ExifParser.create(arrayBuffer).parse();
}
const result = await readExif(fileInput.files[0]);The browser build is not included in the npm package; the README points to a separate bundle exposing the ExifParser global. A bundler can import the CommonJS package instead.
Pass the exact bytes from a Uint8Array viewparse-typed-array-slice
function exactArrayBuffer(view) {
return view.buffer.slice(
view.byteOffset,
view.byteOffset + view.byteLength
);
}
const result = exif.create(exactArrayBuffer(bytes)).parse();Passing view.buffer directly includes unrelated leading or trailing bytes when byteOffset or byteLength does not cover the whole underlying buffer.
Request only the JPEG EXIF prefixfetch-jpeg-prefix
const response = await fetch(photoUrl, {
headers: { Range: 'bytes=0-65634' },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = exif.create(await response.arrayBuffer()).parse();The 65,635-byte prefix follows the README's APP1 guidance. Servers may ignore Range and return the full file; verify response size and test image-size discovery on your camera corpus.
Read common named tags safelyread-common-tags
const { Make, Model, Orientation, DateTimeOriginal } = result.tags;
console.log({
camera: [Make, Model].filter(Boolean).join(' '),
orientation: Orientation || 1,
capturedAtSeconds: DateTimeOriginal,
});Tags can be absent or forged. DateTimeOriginal becomes Unix seconds under default simplification and timezone-less EXIF dates are interpreted as UTC.
Read simplified GPS coordinatesread-gps
const { GPSLatitude: latitude, GPSLongitude: longitude } = result.tags;
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
console.log({ latitude, longitude });
}Default simplification converts DMS arrays and N/S/E/W references to signed decimal degrees. Location metadata is sensitive personal data; minimize retention and access.
Read JPEG dimensions without decoding pixelsget-image-size
const parser = exif.create(buffer)
.enableReturnTags(false)
.enableImageSize(true);
const size = parser.parse().getImageSize();
if (size) console.log(size.width, size.height);Dimensions come from a JPEG SOF section and may be undefined when none was found in the supplied bytes. This does not validate or decode the pixel payload.
Keep rational arrays and EXIF date stringspreserve-raw-values
const result = exif.create(buffer)
.enableSimpleValues(false)
.parse();
console.log(result.tags.ExposureTime);
console.log(result.tags.DateTimeOriginal);Disabling simplification preserves arrays and strings and avoids automatic GPS and date casting, but raw rational components are still read into JavaScript numbers.
Return section and numeric tag idsreturn-numeric-tags
const result = exif.create(buffer)
.enableTagNames(false)
.parse();
for (const tag of result.tags) {
console.log(tag.section, tag.type.toString(16), tag.value);
}This avoids dependence on the package's fixed tag-name table. Section constants are internal numbers, and the returned entries do not include the original EXIF format id.
Return undefined-format binary fieldsinclude-binary-fields
const result = exif.create(buffer)
.enableBinaryFields(true)
.parse();Binary format 7 values are Buffer objects in Node and ArrayBuffers in browsers. They can be large and opaque, so enable them only when a known tag requires the bytes.
Extract an embedded JPEG thumbnailextract-jpeg-thumbnail
const result = exif.create(buffer)
.enableReturnTags(false)
.parse();
if (result.hasThumbnail('image/jpeg')) {
const thumbnail = result.getThumbnailBuffer();
fs.writeFileSync('thumbnail.jpg', thumbnail);
}The input buffer must contain the complete embedded thumbnail. Validate the extracted bytes before serving them; EXIF metadata and thumbnails are untrusted file content.
Read dimensions of a JPEG thumbnailread-thumbnail-size
if (result.hasThumbnail('image/jpeg')) {
const size = result.getThumbnailSize();
console.log(size && `${size.width}x${size.height}`);
}Call getThumbnailSize only for image/jpeg. Although hasThumbnail can recognize image/tiff, the size method always parses the thumbnail as JPEG.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| exifr | npm | Use a maintained JavaScript reader with broader image formats, modern browser and Node APIs, chunked parsing, and richer metadata support |
| exifreader | npm | Use a current cross-platform parser when EXIF, IPTC, XMP, ICC, PNG, WebP, HEIC, and other metadata families matter |
| sharp | npm | Use a Node image pipeline when metadata inspection must be paired with rotation, resizing, conversion, or stripping, and native binaries are acceptable |
| piexifjs | npm | Use a JPEG EXIF reader and writer when changing or removing tags is the actual requirement |