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

photonify

v5.0.0

Published

Resize image buffers into multiple sizes with sharp and store them on the local filesystem or AWS S3.

Downloads

337

Readme

Photonify

npm version npm downloads CI TypeScript node license

Photonify processes image buffers into multiple resized variants in a single call. Given one or more input buffers and a set of named sizes, it resizes each image to every size, encodes the result to jpg, png, tiff, webp, or avif, and writes the output to the local filesystem or uploads it directly to AWS S3. Each output is named <uuid>-<sizeAlias>.<format>, so filenames are unique across runs and safe to write without overwrite checks.

Resizing is powered by sharp and runs through a concurrency-limited worker pool over the flattened (image × size) task list. S3 uploads send each fully-resized buffer directly to the bucket with no temp files, and a failed run best-effort cleans up any files it already wrote. The full API is two functions — processFiles and removeFiles — and ships with TypeScript declarations.

Features

  • 🖼️ Resize one or many images into any number of named sizes in a single call
  • 💾 Write to the local filesystem or upload directly to AWS S3
  • ☁️ Uploads resized buffers straight to S3 (no temp files) with the correct ContentType
  • 🏷️ Unique fingerprinted filenames (<uuid>-<sizeAlias>.<format>)
  • ⚙️ Configurable output format, fit strategy, and parallelism
  • 🧹 removeFiles for batch-deleting S3 objects (auto-chunked past S3's 1000-key limit)
  • 🧩 First-class TypeScript types
  • 🔇 No console noise — errors propagate to you

Requirements

  • Node.js >=20.9.0 (required by sharp 0.35)
  • sharp may need platform-specific setup in some environments — see the sharp install docs

Installation

npm install photonify
# or
yarn add photonify

Quick start

import { processFiles } from 'photonify';
import path from 'path';

// e.g. an image buffer from a multipart upload (Multer, etc.)
const imageBuffer = req.file.buffer;

const { createdFiles } = await processFiles([imageBuffer], {
  outputDest: path.join(__dirname, 'resized_images'),
});

console.log(createdFiles);
// [
//   'a1b2...c3-xl.jpg',
//   'a1b2...c3-lg.jpg',
//   'a1b2...c3-md.jpg',
//   'a1b2...c3-sm.jpg',
// ]

API

processFiles(files, settings)

Resizes each input image into every configured size and stores the results.

  • files: Buffer | Buffer[] — one or more image buffers (required)
  • settings: Settings — see below
  • Returns: Promise<{ createdFiles: string[] }> — the generated filenames, one per (image × size). Filenames are <uuid>-<sizeAlias>.<format>.

Settings

| Option | Type | Default | Notes | | -------------------- | ------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------ | | storage | 'local' \| 's3' | 'local' | Where output is written. Any other value is rejected up front. | | outputDest | string | — | Required for local storage. Directory to write to; created if it doesn't exist. | | outputFormat | 'jpg' \| 'png' \| 'tiff' \| 'webp' \| 'avif' | 'jpg' | Output encoding and file extension. | | sizes | Record<string, { width?: number; height?: number }> | 4 sizes (see below) | Map of alias → dimensions. See Sizes for alias and dimension rules. | | fit | 'contain' \| 'cover' \| 'fill' \| 'inside' \| 'outside' | 'cover' | How images fit the target box. See sharp resize. | | withoutEnlargement | boolean | false | When true, images smaller than a target size are left as-is instead of being upscaled to fill the box. | | formatOptions | FormatOptions | — | Encoder options passed to sharp for the chosen outputFormat, e.g. { quality: 90 }. | | concurrency | number | 4 | Max (image × size) tasks in parallel. A positive integer, or Infinity for no limit. | | s3Config | S3ClientConfig | — | Required for S3 storage. Passed straight to the AWS SDK S3Client. | | s3Bucket | string | — | Required for S3 storage. Destination bucket. |

Sizes

Each entry in sizes maps an alias to a target box. processFiles validates the map up front and rejects before doing any work if:

  • the map is empty;
  • an alias contains anything other than letters, digits, _, or - (the alias becomes part of the filename / S3 key, so / and .. are not allowed);
  • a size has neither width nor height;
  • a width or height is not a positive integer.

Give one dimension to preserve the source aspect ratio, or both to fit the image into the box using fit.

EXIF orientation is applied before resizing, so photos from phones and cameras come out upright. The orientation tag itself is not carried into the output.

When sizes is omitted, these four are produced. Each sets only a width, so the height is derived from the source aspect ratio (no cropping or stretching):

| Alias | Width | Height | | ----- | ----- | ----------------- | | xl | 1280 | from source ratio | | lg | 1024 | from source ratio | | md | 640 | from source ratio | | sm | 320 | from source ratio |

removeFiles(fileNames, settings)

Deletes objects from S3. Requests are automatically chunked into batches of 1000 keys (the S3 DeleteObjects limit), and the call throws if S3 reports any per-key deletion errors.

  • fileNames: string[] — S3 object keys to delete (required)
  • settings: RemoveSettings — { storage: 's3', s3Config, s3Bucket } (all required)
  • Returns: Promise<void>

There is intentionally no local-filesystem delete support. Use Node's fs.unlink directly for local files.

Usage

Local filesystem

import { processFiles } from 'photonify';
import path from 'path';

const { createdFiles } = await processFiles([imageBuffer], {
  outputDest: path.join(__dirname, 'resized_images'),
  outputFormat: 'png',
  sizes: {
    lg: { width: 500, height: 250 },
    md: { width: 250, height: 125 },
  },
});

AWS S3

Each resized image is uploaded straight to S3 — no local staging — with the correct ContentType for the output format. (Each variant is fully buffered in memory, then uploaded; nothing is streamed incrementally.)

import { processFiles } from 'photonify';

const { createdFiles } = await processFiles([imageBuffer], {
  storage: 's3',
  s3Config: {
    region: 'us-west-1',
    // any S3ClientConfig option is supported, e.g. credentials, endpoint, forcePathStyle
  },
  s3Bucket: 'photonify',
});
// createdFiles are the S3 object keys that were uploaded

Custom sizes, formats & aspect ratio

Aliases are arbitrary, and you can constrain a single dimension to preserve the source aspect ratio:

await processFiles([imageBuffer], {
  outputDest: './out',
  outputFormat: 'tiff',
  fit: 'contain',
  sizes: {
    hero: { width: 1600, height: 600 }, // exact box
    thumb: { width: 200 }, // height derived from aspect ratio
    banner: { height: 400 }, // width derived from aspect ratio
  },
});

Controlling parallelism

processFiles runs a concurrency-limited worker pool over every (image × size) pair. Tune it for large batches:

await processFiles(manyBuffers, {
  outputDest: './out',
  concurrency: 8,
});

Removing S3 files

import { removeFiles } from 'photonify';

await removeFiles(['file1.jpg', 'file2.jpg'], {
  storage: 's3',
  s3Config: { region: 'us-west-1' },
  s3Bucket: 'photonify',
});

Error handling

processFiles and removeFiles reject rather than logging. Every rejection is a PhotonifyError (exported from the package), so you can branch on it with instanceof instead of matching the message string. When the failure originates elsewhere — a sharp decode error, an S3 transport error — the original is attached as cause.

On a processing failure, processFiles stops scheduling new work, waits for every in-flight task to finish, then best-effort removes everything the call produced (local files are unlinked; S3 objects are deleted with DeleteObjects). Cleanup failures are ignored, and the S3 rollback is time-bounded (10s per DeleteObjects batch) so an S3 outage cannot add the AWS SDK's full retry latency before the caller sees the original failure. It then rejects with a Photonify: Error processing images error whose cause is the underlying error:

import { processFiles, PhotonifyError } from 'photonify';

try {
  await processFiles([imageBuffer], { outputDest: './out' });
} catch (err) {
  if (err instanceof PhotonifyError) {
    console.error(err.message); // 'Photonify: Error processing images'
    console.error(err.cause); // the original sharp/S3 error
  }
}

TypeScript

Photonify ships its own type declarations. The public functions, the PhotonifyError class, and all the types are exported from the package root:

import { processFiles, removeFiles, PhotonifyError } from 'photonify';
import type {
  Settings,
  RemoveSettings,
  Sizes,
  Size,
  Fit,
  SupportedFileTypes,
  FormatOptions,
  Files,
  ProcessResult,
} from 'photonify';

Photonify uses sharp

Image processing is powered by sharp. See the cross-platform install notes if you deploy to a different OS/architecture than you develop on.

Example app

A working Express example lives at photonify/photonify-express-example, using Multer to access multipart file data.

Changelog

See CHANGELOG.md for release history.

License

MIT — see LICENSE.md.