npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

extract-raw-preview

v1.1.0

Published

Zero-dependency camera RAW thumbnail extractor for JavaScript. Extract embedded JPEG/PNG previews in Node.js and browsers from CR2, CR3, NEF, ARW, RAF, DNG, TIFF, PSD, and more—no native addons.

Readme

extract-raw-preview — camera RAW thumbnail extractor

npm version npm downloads CI License: MIT Node.js

A zero-dependency JavaScript and TypeScript library for extracting embedded JPEG and PNG thumbnails from camera RAW images. It supports Canon CR2/CR3, Nikon NEF, Sony ARW, Fujifilm RAF, Adobe DNG, Olympus ORF, Panasonic RW2, Pentax PEF, TIFF, JPEG EXIF thumbnails, and Photoshop PSD/PSB files in Node.js and the browser.

npm install extract-raw-preview

Why use it?

  • Fast previews without RAW decoding — copies the camera's embedded preview instead of demosaicing sensor data.
  • Browser and Node.js support — process a local upload client-side, or read a path, file: URL, or bytes on a server.
  • No native addons, WASM, binaries, or runtime dependencies — pure ESM JavaScript with TypeScript declarations.
  • Library and CLI — use the async API or run npx extract-raw-preview photo.cr3.
  • Multiple candidates — select the largest or smallest decodable preview, enforce a byte limit, or inspect every embedded thumbnail.

Use it to create photo-upload previews, RAW contact sheets, gallery and digital asset management thumbnails, file-manager previews, or fast ingestion pipelines. The image is returned exactly as stored: there is no pixel decode, re-encode, or resize, and the API does not extract or expose general EXIF metadata.

Unlike ExifTool- or LibRaw-based tools, extract-raw-preview walks each container itself (TIFF IFDs, CR3 BMFF, RAF headers, and PSD image resources). This keeps it portable enough for client-side browser apps and serverless JavaScript environments.

Supported containers:

  • TIFF-family RAW — Adobe DNG, Canon CR2, Nikon NEF, Sony ARW, Olympus ORF, Panasonic RW2, Pentax PEF, and generic TIFF
  • Other containers — Fujifilm RAF, Canon CR3, JPEG (EXIF thumbnail), Photoshop PSD and PSB

Need decoded pixels, white balance, resizing, or support for every camera format? Use a full RAW decoder such as LibRaw instead. This package is intentionally focused on quickly extracting an existing JPEG or PNG preview.

Quick start

Node.js

import { writeFile } from "node:fs/promises";
import { extractThumbnail, listThumbnails } from "extract-raw-preview";

const result = await extractThumbnail("IMG_1234.CR2");

if (result.found) {
  await writeFile("thumb.jpg", result.data); // 1936×1288 JPEG, as stored
} else {
  console.log(result.reason); // e.g. "only lossless-jpeg preview present"
}

const candidates = await listThumbnails("photo.dng");
// 5760×3840 image/jpeg 1235564B decodable=true
// 3888×2592 image/jpeg 9578955B decodable=false

Browser

Pass the file bytes from an <input type="file"> or drag-and-drop event. Bundlers pick the browser build automatically, with no node:fs or native addons:

import { extractThumbnail } from "extract-raw-preview";

const bytes = new Uint8Array(await file.arrayBuffer());
const result = await extractThumbnail(bytes);

if (result.found) {
  const image = document.querySelector("img");

  if (image) {
    const blob = new Blob([Uint8Array.from(result.data)], { type: result.mimeType });
    const previewUrl = URL.createObjectURL(blob);
    image.addEventListener("load", () => URL.revokeObjectURL(previewUrl), { once: true });
    image.src = previewUrl;
  }
}

Command line

npx extract-raw-preview photo.cr2                 # writes photo.thumb.jpg
npx extract-raw-preview photo.dng -o thumb.jpg --json
npx extract-raw-preview photo.tiff --list         # inspect, write nothing

Input can be a path, a file: URL, or a Uint8Array. Format is sniffed from magic bytes. Paths and file: URLs are Node-only.

RAW preview extraction vs. RAW conversion

Most camera RAW files contain one or more ready-made JPEG previews for camera playback and cataloging. This package extracts those embedded bytes, which is much faster and lighter than decoding the sensor data. It does not demosaic, color-correct, resize, or convert a RAW image to JPEG. For full RAW-to-JPEG conversion, use a native or WebAssembly RAW decoder.

API

All functions are async.

| Function | Returns | | --- | --- | | extractThumbnail(input, options?) | The best decodable preview, or { found: false, reason } | | listThumbnails(input) | Every candidate, largest first — including non-decodable ones. No maxBytes filter. | | detectFormat(input) | A FormatId from the table below, or undefined. Reads at most 64 KiB. |

extractThumbnail options:

| Option | Default | Notes | | --- | --- | --- | | format | sniffed | Skip sniffing. | | prefer | "largest" | Or "smallest". Ranked by pixel count, then byte length. | | maxBytes | 8 MiB | Drops oversized candidates; never throws. |

type Thumbnail = {
  data: Uint8Array;       // stored JPEG/PNG copy
  mimeType: "image/jpeg" | "image/png";
  width: number;
  height: number;
  byteLength: number;
  origin: "embedded-jpeg" | "embedded-png";
  decodable: boolean;     // false for lossless JPEG (SOF3)
};

type ExtractResult =
  | ({ found: true; format: FormatId } & Thumbnail)
  | { found: false; format: FormatId; reason: string };

Missing preview vs. error

A file with no usable preview resolves:

| reason | Meaning | | --- | --- | | no embedded preview | Nothing embedded. | | only lossless-jpeg preview present | Previews exist but are SOF3. | | no decodable preview present | Previews exist but none open. | | all candidates exceed maxBytes | Everything was over the cap. |

Broken input rejects with ExtractError:

| code | When | | --- | --- | | ERR_UNRECOGNIZED_FORMAT | Unknown signature and no options.format. | | ERR_UNSUPPORTED_FORMAT | options.format has no extractor. | | ERR_TRUNCATED | File ends before a required structure. | | ERR_IO | Path/URL could not be read. |

import { ExtractError } from "extract-raw-preview";

try {
  await extractThumbnail("photo.cr2");
} catch (error) {
  if (error instanceof ExtractError) console.error(error.code, error.message);
}

Formats

Format is sniffed from magic bytes (first 64 KiB), not the filename. Pass format / --format with the id below to skip sniffing.

| Id | Format | Extensions | Where the preview lives | | --- | --- | --- | --- | | tiff | TIFF | .tif, .tiff | JPEG-compressed IFDs and SubIFDs | | dng | Adobe DNG | .dng | Reduced-resolution JPEG SubIFDs | | cr2 | Canon RAW 2 | .cr2 | IFD JPEG strips plus a JPEGInterchangeFormat thumbnail | | nef | Nikon NEF | .nef | SubIFDs or JPEGInterchangeFormat | | arw | Sony ARW | .arw | JPEGInterchangeFormat on IFD0 / IFD1 | | orf | Olympus ORF | .orf | IFD1 JPEGInterchangeFormat | | rw2 | Panasonic RW2 | .rw2 | Panasonic JpgFromRaw, plus its nested EXIF thumbnail | | pef | Pentax PEF | .pef | JPEGInterchangeFormat on later IFDs | | jpeg | JPEG | .jpg, .jpeg | EXIF IFD1 thumbnail (the primary image is not a preview) | | raf | Fujifilm RAF | .raf | Header JPEG, plus any nested EXIF thumbnail | | cr3 | Canon RAW 3 | .cr3 | BMFF THMB, Canon uuid, and mdat JPEGs | | psd | Photoshop | .psd | Image resource 1036 (JPEG thumbnail) | | psb | Photoshop Large | .psb | Same resource layout as PSD |

TIFF-container RAWs (DNG, CR2, NEF, ARW, PEF, ORF, RW2) share one IFD walker. JPEG reads EXIF IFD1; RAF, CR3, and PSD/PSB have their own parsers. Lossless JPEG (SOF3) previews are listed with decodable: false and are never returned by extractThumbnail.

CLI

extract-raw-preview <file> [options]

  -o, --output <path>   Write here (default: <name>.thumb.<ext> next to the source)
  --list                Print all candidates; write nothing
  --json                JSON on stdout (`data` omitted)
  --format <id>         Force a format id from the table above
  --prefer <strategy>   largest (default) | smallest
  --max-bytes <n>       Per-candidate cap (default 8388608)
  -h, --help

Human mode prints path WxH mimeType byteLength. Diagnostics go to stderr, so --json stdout stays parseable.

| Exit | Meaning | | --- | --- | | 0 | Success, including { found: false } and --list with no candidates | | 1 | Operational error (unreadable, unrecognized, truncated, write failure) | | 2 | Bad flags or missing arguments |

Development

npm install
npm run fixtures     # download real samples into test/fixtures/ (gitignored)
npm run fixtures:thumbs  # extract previews into test/fixtures/thumbs/
npm run lint         # biome format + lint
npm run typecheck
npm test             # fixtures:check + build + tests

Fixtures are pinned by URL, size, and SHA-256 in scripts/fixtures.yaml. npm run fixtures:check (run as pretest) fails unless every supported format has a verified on-disk sample with a decodable preview.

To add a format: one file in src/formats/, one line in src/registry.ts, and a redistributable fixture (CC0 / public domain / permissive test-suite license) under a new key in the YAML catalog. The preview must be stored JPEG or PNG bytes — this library copies them and does not decode pixels. To add a sample to an existing format, append an entry under that format's list.

Releasing

Versioning uses Changesets. See RELEASING.md for the PR flow, the first 1.0.0 publish, and npm trusted publishing.

License

MIT © Dan Radenkovic