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

@digibuffer/image-core

v1.0.2

Published

Node.js image processing — metadata extraction, variant generation, optimization, watermarking. Uses sharp (native binaries, not Edge-compatible).

Readme

@digibuffer/image-core

Node.js image processing — metadata extraction, EXIF parsing, variant generation, optimization, color analysis, and watermarking. Powered by sharp and exifr.

Node.js only. sharp uses native binaries — not compatible with Edge runtimes or browser environments. Use in API routes, job workers, or server-side scripts.

Installation

npm install @digibuffer/image-core

Functions

extractMetadata(input)

Basic image dimensions and format from sharp.

const meta = await extractMetadata(buffer);
interface ImageMetadata {
  width: number;
  height: number;
  format: 'jpeg' | 'png' | 'webp' | 'avif' | 'gif' | 'tiff';
  size?: number;           // file size in bytes
  orientation?: number;    // EXIF orientation 1-8
  hasAlpha?: boolean;
  space?: string;          // 'srgb', 'rgb', 'cmyk', etc.
  depth?: string;          // bits per channel e.g. 'uchar'
  density?: number;        // DPI
  isProgressive?: boolean; // JPEG progressive encoding
}

extractExif(input)

Full EXIF/GPS data parsed from the image. Returns null if no EXIF is present (screenshots, web images, most PNGs).

const exif = await extractExif(buffer);
// → ExifData | null
interface ExifData {
  // Camera
  make?: string;               // e.g. 'Apple', 'Canon', 'Sony'
  model?: string;              // e.g. 'iPhone 15 Pro', 'EOS R5'
  lens?: string;               // e.g. 'iPhone 15 Pro back camera 6.86mm f/1.78'
  software?: string;           // e.g. 'Lightroom 7.0'

  // Exposure
  iso?: number;                // e.g. 100, 800, 3200
  aperture?: number;           // f-number e.g. 1.78, 2.8, 5.6
  shutterSpeed?: string;       // human-readable e.g. '1/1000', '2s'
  focalLength?: number;        // mm e.g. 24, 85
  focalLengthIn35mm?: number;  // 35mm equivalent
  exposureProgram?: string;    // 'Aperture Priority', 'Shutter Priority', 'Manual', etc.
  exposureMode?: string;       // 'Auto', 'Manual', 'Auto Bracket'
  meteringMode?: string;       // 'Spot', 'Center Weighted', 'Pattern', etc.
  flash?: string;              // 'Flash Fired', 'No Flash', 'Flash Did Not Fire'
  whiteBalance?: string;       // 'Auto', 'Manual'

  // Date
  dateTimeOriginal?: Date;     // when the photo was taken
  dateTimeDigitized?: Date;    // when digitized (usually same)

  // GPS
  gps?: {
    latitude: number;          // decimal degrees e.g. 51.5074
    longitude: number;         // decimal degrees e.g. -0.1278
    altitude?: number;         // metres above sea level
  };

  // Image
  orientation?: number;        // EXIF orientation 1-8
  colorSpace?: string;         // 'sRGB', 'Uncalibrated'
  xResolution?: number;        // DPI
  yResolution?: number;        // DPI

  raw?: Record<string, unknown>; // all raw parsed EXIF fields
}

Images that contain EXIF: unedited JPEGs from cameras and smartphones, raw files converted to JPEG. Images that don't: screenshots, web-downloaded PNGs, images processed through most editors (EXIF stripped).


extractColors(input)

Per-channel color statistics using sharp's pixel analysis.

const colors = await extractColors(buffer);
interface ImageColors {
  dominant: { r: number; g: number; b: number }; // channel means → approximate dominant color
  channels: {
    mean: number;    // average pixel value 0-255
    stdev: number;   // standard deviation
    min: number;
    max: number;
  }[];               // [R, G, B] or [R, G, B, A]
  hasAlpha: boolean;
  brightness: number; // perceived brightness 0-100 (ITU-R BT.601 luma)
}

isImage(input)

Returns true if the buffer is a recognised image format.

const ok = await isImage(buffer); // → boolean

generateVariants(input, configs?)

Generate one or more resized/reformatted variants from a single input.

const variants = await generateVariants(buffer, [
  { name: 'thumb',  width: 400,  height: 400,  fit: 'inside', format: 'webp', quality: 90 },
  { name: 'medium', width: 1200, height: 1200, fit: 'inside', format: 'webp', quality: 85 },
]);
interface VariantConfig {
  name: string;
  width: number;
  height: number;
  fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; // default: 'inside'
  format?: ImageFormat;   // default: 'webp'
  quality?: number;       // 1-100, default: 85
  autoRotate?: boolean;   // auto-rotate via EXIF orientation, default: true
}

interface VariantResult {
  name: string;
  buffer: Buffer;
  width: number;
  height: number;
  format: ImageFormat;
  size: number; // bytes
}

generateVariant(input, config)

Single variant — same as generateVariants but for one config.

generateStandardVariants(input)

Shortcut for the two platform-standard variants:

const { thumbnail, medium } = await generateStandardVariants(buffer);
// thumbnail → 800×800 max, WebP, q90, auto-rotate
// medium    → 1600×1600 max, WebP, q85, auto-rotate

Both use fit: 'inside' (never upscales, preserves aspect ratio).


transformImage(input, options)

Resize, reformat, and/or strip metadata in one call.

const result = await transformImage(buffer, {
  width: 1280,
  height: 720,
  fit: 'cover',
  format: 'webp',
  quality: 85,
  autoRotate: true,
  stripMetadata: true,
});
interface TransformOptions {
  width?: number;
  height?: number;
  fit?: FitMode;
  format?: ImageFormat;
  quality?: number;
  autoRotate?: boolean;
  stripMetadata?: boolean;
}

interface TransformResult {
  buffer: Buffer;
  width: number;
  height: number;
  format: ImageFormat;
  size: number;
}

optimizeImage(input, options?)

Reduce file size while preserving quality. Auto-detects format if none specified.

const result = await optimizeImage(buffer, {
  quality: 80,
  stripMetadata: true,
});

// Force JPEG with mozjpeg + progressive encoding
const result = await optimizeImage(buffer, {
  format: 'jpeg',
  quality: 85,
  mozjpeg: true,
  progressive: true,
});

// PNG with max compression
const result = await optimizeImage(buffer, {
  format: 'png',
  quality: 80,
  pngCompressionLevel: 9,
});
interface OptimizeOptions {
  format?: ImageFormat;
  quality?: number;          // 1-100
  mozjpeg?: boolean;         // JPEG: use mozjpeg encoder (better compression)
  pngCompressionLevel?: number; // PNG: 1-9
  progressive?: boolean;     // JPEG: progressive encoding
  stripMetadata?: boolean;   // strip EXIF, ICC, etc.
}

convertFormat(input, format, quality?)

Convert to a different format.

const webp = await convertFormat(buffer, 'webp', 85);
const avif = await convertFormat(buffer, 'avif', 80);

applyWatermark(input, options)

Composite a text watermark (+ optional logo) over an image using SVG.

const result = await applyWatermark(buffer, {
  text: '© MyBrand',
  position: 'bottom-right',
  opacity: 0.7,
  color: '#FFFFFF',
});

// With logo
const result = await applyWatermark(buffer, {
  text: 'digibuffer.com',
  position: 'bottom-left',
  opacity: 0.8,
  logo: logoBuffer,
  logoSizeRatio: 0.06,  // 6% of image width
  fontSizeRatio: 0.02,  // 2% of image width
  color: '#FFFFFF',
});
interface WatermarkOptions {
  text: string;
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'center';
  opacity?: number;        // 0-1, default 0.7
  color?: string;          // hex, default '#FFFFFF'
  logo?: Buffer;           // optional logo image
  logoSizeRatio?: number;  // logo width as fraction of image width, default 0.08
  fontSizeRatio?: number;  // font size as fraction of image width, default 0.025
}

interface WatermarkResult {
  buffer: Buffer;
  width: number;
  height: number;
}

Typical job handler (Node.js / Hono)

import {
  extractMetadata,
  extractExif,
  extractColors,
  generateStandardVariants,
  optimizeImage,
} from '@digibuffer/image-core';

async function processImage(key: string, buffer: Buffer) {
  const [meta, exif, colors, variants] = await Promise.all([
    extractMetadata(buffer),
    extractExif(buffer),
    extractColors(buffer),
    generateStandardVariants(buffer),
  ]);

  // Upload variants to R2/S3
  await storage.put(`${key}/thumbnail.webp`, variants.thumbnail.buffer);
  await storage.put(`${key}/medium.webp`,    variants.medium.buffer);

  // Insert to DB
  await db.insert(mediaFiles).values({
    key,
    width:            meta.width,
    height:           meta.height,
    format:           meta.format,
    size:             meta.size,
    // EXIF
    cameraMake:       exif?.make,
    cameraModel:      exif?.model,
    lens:             exif?.lens,
    iso:              exif?.iso,
    aperture:         exif?.aperture,
    shutterSpeed:     exif?.shutterSpeed,
    focalLength:      exif?.focalLength,
    takenAt:          exif?.dateTimeOriginal,
    gpsLatitude:      exif?.gps?.latitude,
    gpsLongitude:     exif?.gps?.longitude,
    gpsAltitude:      exif?.gps?.altitude,
    // Colors
    dominantR:        colors.dominant.r,
    dominantG:        colors.dominant.g,
    dominantB:        colors.dominant.b,
    brightness:       colors.brightness,
    hasAlpha:         colors.hasAlpha,
    // Variants
    thumbnailKey:     `${key}/thumbnail.webp`,
    thumbnailWidth:   variants.thumbnail.width,
    thumbnailHeight:  variants.thumbnail.height,
    mediumKey:        `${key}/medium.webp`,
    mediumWidth:      variants.medium.width,
    mediumHeight:     variants.medium.height,
  });
}

STANDARD_VARIANTS

The two built-in variant configs:

import { STANDARD_VARIANTS } from '@digibuffer/image-core';
// [
//   { name: 'thumbnail', width: 800,  height: 800,  fit: 'inside', format: 'webp', quality: 90, autoRotate: true },
//   { name: 'medium',    width: 1600, height: 1600, fit: 'inside', format: 'webp', quality: 85, autoRotate: true },
// ]

Pass your own array to generateVariants() to override.


Supported formats

| Input | Output | |---|---| | JPEG, PNG, WebP, AVIF, GIF, TIFF | JPEG, PNG, WebP, AVIF, GIF, TIFF |

EXIF parsing works on JPEG and TIFF. PNG, WebP, AVIF files rarely carry EXIF.


License

Proprietary — All rights reserved.