image-size
image-size tells you the pixel width and height of an image without decoding it. You hand it a Buffer or Uint8Array and it reads only the header bytes, matches them against a table of twenty formats (PNG, JPEG, WebP, GIF, SVG, HEIC and AVIF, TIFF, PSD, ICO, and more), and returns {width, height, type}. For JPEGs it also returns the EXIF orientation number, and for multi-image containers like ICO, CUR, and HEIF it returns an images array with every size in the set. A separate entry point, image-size/fromFile, reads a file from disk asynchronously and does the same thing. There are no dependencies and no native build step, and it ships both ESM and CommonJS builds with TypeScript types.
Still the fastest and widest way to get width and height out of a byte range, and the code is small enough that an archived repository is a survivable risk for a build script. For anything user-facing that ingests untrusted files, plan a migration path now, because the maintainer has publicly stepped back from GitHub.
Use it if
- You need width and height from an upload before you store it, and you do not want to pull in an image processing library with a native binary just to read four bytes
- You already have the bytes in memory from a stream, a fetch response, or a multipart parser: imageSize(buffer) is synchronous and returns in microseconds because it never touches the pixel data
- You need to handle a wide format spread, including the ones most libraries skip: HEIC and AVIF from phones, ICO and CUR favicon sets, KTX textures, JPEG-XL, and SVG viewBox dimensions
- You want the full set of sizes inside a multi-resolution file, which the images array gives you for ICO, CUR, and HEIF containers
- You are generating width and height attributes to stop layout shift and want a build-time or request-time helper with a near-zero footprint
- You care about who is maintaining your dependencies. The GitHub repository was archived in June 2026 with a README that reads 'Tired maintainer doesn't want your slop', explaining that the maintainer stopped fielding repeated machine-generated security advisories about an infinite loop. Development, if it resumes, moves to Codeberg. There is no active GitHub issue tracker, and the last npm release was 2.0.2 in April 2025
- You need anything beyond dimensions: colour space, ICC profile, bit depth, animation frame counts, or actual resizing. Header sniffing gives you geometry and nothing else, and sharp already reads all of it in the same call as the transform you were going to do anyway
- You are validating untrusted uploads. This parses attacker-controlled bytes in your process with no sandbox and no size limit on what it will scan, and the format table is exactly the surface that keeps generating denial-of-service reports. Bound the input, run it off the request path, and do not treat the returned type as proof the file is safe
- You are running in a browser. The main entry works on a Uint8Array, but image-size/fromFile imports node:fs, and in a browser createImageBitmap or an Image element already gives you naturalWidth and naturalHeight for free
- You are on version 1.x and expecting a drop-in upgrade. Version 2 removed the synchronous file API entirely, renamed the default export to a named imageSize, and moved file reading to a separate subpath, so the migration touches every call site
- Your buffers are truncated. The library reads headers, but TIFF in particular needs its full header present, and a partial network chunk either throws 'unsupported file type' or returns nothing useful
Setup reality
npm install image-size is genuinely all of it: zero dependencies, no postinstall, no native compilation, bundled .d.ts, and an exports map with proper ESM and CommonJS conditions. Node 16 or newer is required by the engines field. The friction is entirely the version 2 rewrite. In 1.x you wrote sizeOf('photo.jpg') against a default export and got a synchronous answer from disk; in 2.x that call does not exist. You now import {imageSize} from 'image-size' for buffers and {imageSizeFromFile} from 'image-size/fromFile' for paths, and the file version returns a promise. The README recommends reading the file yourself with fs.readFileSync and passing the buffer if you truly need synchronous behaviour. File reads are also throttled to 100 concurrent operations by default, adjustable with setConcurrency from the same subpath, which you will notice if you fan out over thousands of files. Both entry points throw rather than returning null for an unrecognised header, so every call needs a try/catch. Version 1.2.1 is still published under the legacy dist-tag if you cannot migrate yet.
Patterns
Get dimensions from bytes you already havemeasure-a-buffer
import {imageSize} from 'image-size';
// or: const {imageSize} = require('image-size');
const {width, height, type} = imageSize(buffer);
console.log(width, height, type); // 1920 1080 'jpg'Synchronous and cheap because only the header is parsed. The argument must be a Buffer or Uint8Array; passing a string path here does not read a file, it throws.
Read dimensions from diskmeasure-a-file
import {imageSizeFromFile} from 'image-size/fromFile';
const dimensions = await imageSizeFromFile('photos/image.jpg');
console.log(dimensions.width, dimensions.height);This lives in the image-size/fromFile subpath, not the main entry, and it returns a promise. Version 1.x's synchronous sizeOf('path') was removed in 2.0.0 and is not coming back.
Wrap every call, because it throwshandle-errors
import {imageSize} from 'image-size';
function safeSize(bytes) {
try {
return imageSize(bytes);
} catch (err) {
// 'unsupported file type: undefined' for unrecognised headers
// 'disabled file type: png' if you called disableTypes
return null;
}
}There is no null-returning variant. Unrecognised bytes, truncated buffers, and disabled formats all raise, so an unguarded call in a request handler is a 500 waiting for its first bad upload.
Keep synchronous behaviour by reading the file yourselfsynchronous-file-read
import {readFileSync} from 'node:fs';
import {imageSize} from 'image-size';
const buffer = readFileSync('photos/image.jpg');
const dimensions = imageSize(buffer);The README calls this out as not recommended: readFileSync blocks the event loop and pulls the whole file into memory when only the first few kilobytes matter. Fine in a build script, wrong in a server.
List every size inside an ICO, CUR, or HEIF filemulti-size-containers
import {imageSizeFromFile} from 'image-size/fromFile';
const result = await imageSizeFromFile('images/favicon.ico');
console.log(result.width, result.height); // the LARGEST image in the set
for (const img of result.images ?? []) {
console.log(img.width, img.height); // 16x16, 32x32, 48x48, ...
}The top-level width and height report the largest entry, not the first. The images array is only present for multi-image formats, so guard the access.
Swap width and height for rotated photosjpeg-orientation
const {width, height, orientation} = imageSize(jpegBuffer);
// EXIF orientations 5-8 mean the image is stored rotated 90 degrees
const rotated = orientation !== undefined && orientation >= 5;
const display = rotated
? {width: height, height: width}
: {width, height};orientation is the raw EXIF number 1 to 8 and is undefined when the JPEG has no EXIF block, which is common for images that have been through a stripping pipeline. The library does not apply the rotation for you.
Turn off formats you never expectlimit-formats
import {imageSize, disableTypes, types} from 'image-size';
console.log(types); // ['bmp','cur','dds','gif','heif',...]
// Only accept the web formats; everything else now throws.
disableTypes(types.filter((t) => !['jpg', 'png', 'webp', 'gif'].includes(t)));disableTypes mutates module-level state for the whole process, so it is a startup-time call, not a per-request one. It also shrinks the parser surface exposed to untrusted bytes, which is the main reason to bother.
Size an image fetched over the networkmeasure-from-a-response
const res = await fetch('https://example.com/photo.jpg');
const bytes = new Uint8Array(await res.arrayBuffer());
const {width, height} = imageSize(bytes);This downloads the entire file to read its header. If bandwidth matters, probe-image-size aborts the request once it has enough bytes; image-size has no streaming mode.
Raise or lower the file-read concurrency captune-concurrency
import {setConcurrency, imageSizeFromFile} from 'image-size/fromFile';
setConcurrency(500);
const sizes = await Promise.all(paths.map(imageSizeFromFile));The default limit is 100 simultaneous file reads. Raising it helps on fast SSDs and hurts on network filesystems; lowering it is the fix if you are hitting EMFILE from open file descriptors elsewhere in the process.
Know what you get back from an SVGsvg-dimensions
const svg = Buffer.from('<svg width="100" height="50" viewBox="0 0 200 100"/>');
const {width, height, type} = imageSize(svg);
// 100 50 'svg'
// Percentage units are not supported:
// <svg width="100%" height="100%"> falls back to the viewBox, or throws.Only pixel width and height attributes and the viewBox are read. An SVG sized purely in percentages or with CSS has no intrinsic dimensions to report, which is a documented limitation rather than a bug.
Check dimensions from the terminalcli-usage
npx image-size image1.jpg image2.png
# image1.jpg: 1920x1080
# image2.png: 512x512The bin is installed with the package, so a local devDependency gives you it without npx. Handy in CI checks that assert asset sizes; it exits non-zero on an unreadable file.
Stay on 1.x if the v2 migration is not worth itpin-legacy-version
npm install image-size@legacy # resolves to 1.2.1
// the old API still works there:
const sizeOf = require('image-size');
const dimensions = sizeOf('photos/image.jpg'); // sync, reads from disk1.2.1 was published in April 2025, on the same day as 2.0.2, and is tagged legacy rather than deprecated. It receives nothing now that the repository is archived, so treat this as a delay, not a plan.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| probe-image-size | npm | You want to read dimensions from a URL or a readable stream and stop downloading as soon as the header arrives. |
| sharp | npm | You need real metadata, format conversion, or resizing, and can accept a native binary in your install. |
| image-dimensions | npm | You want a modern ESM-only package that runs unchanged in browsers, workers, and Node against a stream or blob. |
| file-type | npm | You only need to know what format the bytes are, not how big the image is. |