Skip to content
/ exifr Public
forked from MikeKovarik/exifr

πŸ“‘ The fastest and most versatile JavaScript EXIF reading library.

License

Notifications You must be signed in to change notification settings

Sitemark/exifr

Β 
Β 

Repository files navigation

@sitemark/exifr

Fork from: https://github.com/MikeKovarik/exifr/commit/bf9fc270b00a87347fd691f7a7df14c3263a8328

Installation

@sitemark/exifr is published to GitHub Packages instead of npm.

To ensure that npm or pnpm uses the correct repository, add the following line to your .npmrc file:

@sitemark:registry=https://npm.pkg.github.com

This configuration will make sure that all @sitemark packages are installed from GitHub instead of the default npm registry.


Releasing a New Version

We use Changesets to manage versioning and changelogs. The release process is mostly automated. Here’s how it works:

  1. When making changes that require a version bump, run the following command to create a changeset:

    npx @changesets/cli

    Follow the prompts to describe your changes and select the appropriate version bump (major, minor, or patch).

  2. Commit the generated changeset file:

    git add .changeset/
    git commit -m "Add changeset for <description>"
  3. After your PR is merged, the Changesets bot will automatically create a new PR containing the version bump and changelog updates. Review and merge this PR.

  4. Once the version bump PR is merged, the bot will automatically publish the new version to GitHub Packages and create a corresponding release on GitHub.

Features

Works everywhere and accepts pretty much everything you throw at it.

  • Isomorphic.
    Works in both Node and Browsers.
  • Wide range of inputs
    ArrayBuffer, Uint8Array, DataView, <img> elements, string URL and paths, Object URL, Base64 URL
  • Blazing Fast.
    Like really fast. Like 1-2ms fast.
  • Efficient.
    Only reads first few bytes of the file.
  • Fine grained parsing
    only need GPS coords? No need to parse the whole exif
  • Promise based
    Uses Node.js 10.x experimental Promise FS API
  • Comes as UMD module (along with ESM source).
    No need to bundle or browserify. Just import, require() or <script> it in your .mjs, .js or .html file.
  • Simple output
    meaningful descriptive strings instead of enum values, dates converted to Date instances, etc...
  • No dependencies

Supports

  • Basic TIFF/EXIF support
  • XMP Segments - Additional software/photoshop related data. Returned as a string (exifr does not include XML parser).
  • IPTC Segments - Captions & copyrights
  • Embedded thumbnail extraction

Usage

ESM in Node.js

import * as exifr from '@sitemark/exifr'
// exifr handles disk reading. Only reads a few hundred bytes.
exifr.parse('./myimage.jpg')
  .then(exif => console.log('Camera:', exif.Make, exif.Model))
  .catch(console.error)

CJS in Node.js

let exifr = require('exifr')
let fs = require('fs').promises
// Or read the file on your own and feed the buffer into exifr.
fs.readFile('./myimage.jpg')
  .then(exifr.parse)
  .then(exif => console.log('lat lon', exif.latitude, exif.longitude))
  .catch(console.error)

UMD in Browser

<img src="./myimage.jpg">
<script src="./node_modules/exifr/index.js"></script>
<script>
  // UMD module exposed on global window.exifr object
  exifr.parse(document.querySelector('img'))
    .then(exif => console.log('Exposure:', exif.ExposureTime))
</script>

ESM in Browser

<input id="filepicker" type="file" multiple>
<script type="module">
  import {parse} from './node_modules/exifr/index.js'

  document.querySelector('#filepicker').addEventListener('change', async e => {
    let files = Array.from(e.target.files)
    let promises = files.map(parse)
    let exifs = await Promise.all(promises)
    let dates = exifs.map(exif => exif.DateTimeOriginal.toGMTString())
    console.log(`${files.length} photos taken on:`, dates)
  })
</script>

Extracting thumbnail

let img = document.querySelector("#thumb")
document.querySelector('input[type="file"]').addEventListener('change', async e => {
  let file = e.target.files[0]
  img.src = await exifr.thumbnailUrl(file)
})
let thumbBuffer = await exifr.thumbnailBuffer(imageBuffer)

Usage in WebWorker

let worker = new Worker('./worker.js')
worker.postMessage('../test/IMG_20180725_163423.jpg')
worker.onmessage = e => console.log(e.data)
// worker.js
importScripts('./node_modules/exifr/index.js')
let exifr = self.exifr // UMD
self.onmessage = async e => postMessage(await exifr.parse(e.data))

API

exifr exports parse, thumbnailBuffer, thumbnailUrl functions and ExifParser class

parse(input[, options]) => Promise<object>

Accepts any input argument, parses it and returns exif object.

thumbnailBuffer(input) => Promise<Buffer|ArrayBuffer>

Extracts embedded thumbnail from the photo and returns it as a Buffer (Node.JS) or an ArrayBuffer (browser).

Only parses as little EXIF as necessary to find offset of the thumbnail.

thumbnailUrl(input) => Promise<string>

Browser only - exports the thumbnail wrapped in Object URL.

User is expected to revoke the URL when not needed anymore.

ExifParser class

Afore mentioned functions are wrappers that internally instantiate new ExifParse(options) class, then call parser.read(input), and finally call either parser.parse() or parser.extractThumbnail().

To do both parsing EXIF and extracting thumbnail efficiently you can use this class yourself.

let parser = new ExifParser(options)
let exif = await parser.read(input)
let thumb = await parser.extractThumbnail()

Arguments and options

input

can be:

  • string file path
  • string URL
  • string Base64
  • string Object URL / Blob URL
  • Buffer
  • ArrayBuffer
  • Uint8Array
  • DataView
  • File
  • Blob
  • <img> element

options

is optional argument and can be either:

  • object with granular settings
  • boolean shortcut to enable parsing all segments and blocks

Reading file from disk or fetching url

Chunked mode

In browser it's sometimes better to fetch a larger chunk in hope that it contains the whole EXIF (and not just its beginning like in case of options.seekChunkSize) in prevention of additional loading and fetching. options.parseChunkSize sets that number of bytes to download at once. Node.js only relies on the options.seekChunkSize.

Whole file mode

If you're not concerned about performance and time (mostly in Node.js) you can tell exifr to just read the whole file into memory at once.`

  • options.wholeFile bool/undefined default undefined
    Sets whether to read the file as a whole or just by small chunks.
    Used when file path or url to the image is given.

    • true - whole file mode
      forces fetching/reading whole file
    • undefined - chunked mode, default value
      Reads first few bytes of the file to look for EXIF in (seekChunkSize) and allows reading/fetching additional chunks.
      Ends up with multiple small disk reads for each segment (xmp, icc, iptc)
      NOTE: Very efficient in Node.js, especially with SSD. Not ideal for browsers
    • false - chunked mode
      Reads only one much larger chunk (parseChunkSize) in hopes that the EXIF isn't larger then the chunk.
      Disallows further disk reads. i.e. ignores any EXIF found beyond the chunk.
  • options.seekChunkSize number default: 512 Bytes (0.5 KB)
    Byte size of the first chunk that will be read and parsed for EXIF.
    EXIF is usually within the first few bytes of the file. If not than there likely is no EXIF. It's not necessary to read through the whole file.
    Node.js: Used for all input types.
    Browser: Used when input arg is buffer. Otherwise, parseChunkSize is used.

  • options.parseChunkSize number default: 64 * 1024 (64KB)
    Size of the chunk to fetch in the browser in chunked mode.
    Much like seekChunkSize but used in the browser (and only if we're given URL) where subsequent chunk fetching is more expensive than fetching one larger chunk with hope that it contains the EXIF.
    Node.js: Not used.
    Browser: Used when input arg is string URL. Otherwise, seekChunkSize is used.

If parsing file known to have EXIF fails try:

  • Increasing seekChunkSize
  • Increasing parseChunkSize in the browser if file URL is used as input.
  • Disabling chunked mode (read whole file)

Segments & Blocks

  • options.tiff: true - APP1 - TIFF
    The basic EXIF tags (image, exif, gps)
    TIFF contains the following blocks / is requred for reading the following block:
    • options.exif: true - Sub Exif.
    • options.gps: true - GPS latitue and longitude data.
    • options.thumbnail: false - Size and other information about embedded thumbnail.
    • options.interop: false - This is a thing too.
  • options.xmp: false - APP1 - XMP
    XML based extension, often used by editors like Photoshop.
  • options.icc: false - APP2 - ICC
    Not implemented yet
  • options.iptc: false - APP13 - IPTC
    Captions and copyrights

Output format

  • options.postProcess number default: true
    Translate enum values to strings, convert dates to Date instances, etc...

  • options.mergeOutput number default: true
    Changes output format by merging all segments and blocks into a single object.

Note on performance

As you've already read, this lib was built to be fast. Fast enough to handle whole galleries.

We're able to parse image within a couple of milliseconds (tens of millis on phones) thanks to selective disk reads (Node.js) and Blob / ArrayBuffer (Browser) manipulations. Because you don't need to read the whole file and parse through a MBs of data if we an educated guess can be made to only read a couple of small chunks where EXIF usually is. Plus each supported data type is approached differently to ensure the best performance.

Observations from testing with +-4MB pictures (Highest quality, highest resolution Google Pixel photos, tested on a decade old quad core CPU). Note: These are no scientific measurements.

  • Node: Selective disk reads take about 1ms.
  • Node: Processing fully buffered data take about 2.5ms on average.
  • Browser: ArrayBuffer takes about 2ms
  • Browser: Blob can go up to 10ms on average.
  • Browser: <img> with Object URL as a src varies between 5ms to 30ms
  • Drag-n-dropping gallery of 90 images took 160ms to load, parse and create exif objects. Extracting GPS data and logging it to console took another 60ms (220ms all together).
  • Phones are significantly slower. Usually 40-150ms per photo. This is seriously impacted by loading the photo into browser, not so much of a parsing problem. But real-world photo-to-exif time can be as slow as 150ms.

TODOs and Future ideas

The library is already production ready and battle-tested, but there's always room for improvement

  • API for providing custom XML parser
  • modularizing the library
    • by parsers (minimalistic with TIFF only, default with IPTC, ICC & XMP parsing)
    • with & without
    • minified / default (with and without tag dictionary, minified for the web)
  • Parsing ICC
  • Parsing readernotes.
    node-exif module already has a few great implementations and PRs (Canon makernote).
  • WebP image support
  • tidy up file reader / loader code
  • .tif & .tiff image support
  • Thumbnail extraction

Probably as an additional opt-in extension file to keep the core as light as possible.

Licence

MIT, Mike KovaΕ™Γ­k, Mutiny.cz

About

πŸ“‘ The fastest and most versatile JavaScript EXIF reading library.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages

  • JavaScript 94.2%
  • HTML 5.8%