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

hazo_images

v1.7.0

Published

Image processing pipeline for the hazo ecosystem — Sharp wrapper, thumbnail generation, EXIF handling, and hazo_files integration helper

Readme

hazo_images

Image processing pipeline for the hazo ecosystem. Sharp wrapper with EXIF handling, auto-rotate, resize, thumbnail generation, and a full suite of geometry / filter / output controls. Composes with hazo_files via an integration helper. Ships a framework-agnostic route-handler factory and, since @1.4.0, a real React editor dialog in hazo_images/ui.

Image storage is owned by hazo_files. This package handles processing only.

Install

npm install hazo_images

# Required peer dep — provides errors, correlation IDs, logger, config loader
npm install hazo_core

# Required peer deps for image processing
npm install sharp

# Optional — only needed if using uploadProcessedImage
npm install hazo_files

# Optional — only needed if using hazo_images/ui components
npm install hazo_ui react react-dom

Usage

processImage — standalone pipeline primitive

import { processImage } from 'hazo_images/server';

const fs = await import('node:fs/promises');
const buffer = await fs.readFile('./photo.jpg');

const result = await processImage(buffer, {
  stripExif: true,       // default: true — privacy-safe
  autoRotate: true,      // default: true — corrects EXIF orientation
  maxDimension: 4096,    // default: 4096 — resize longest side, keep AR
  webp: false,           // default: false — preserve original format
  thumbnails: [256, 512, 1024],
});

console.log(result.metadata);
// { width: 2400, height: 1600, format: 'jpeg' }

console.log(result.thumbnails.length);
// 3 — one per requested size

// result.buffer — processed main image as Buffer
// result.thumbnails[0] — { size: 256, buffer: Buffer, format: 'jpeg' }

uploadProcessedImage — compose with hazo_files

import { uploadProcessedImage } from 'hazo_images/server';
import { createInitializedFileManager } from 'hazo_files/server';

const fm = await createInitializedFileManager({ config: { provider: 'local', local: { basePath: './uploads' } } });
const buffer = await fs.readFile('./photo.jpg');

const result = await uploadProcessedImage(fm, buffer, '/photos/wedding.jpg', {
  thumbnails: [256, 512, 1024],
  stripExif: true,
  autoRotate: true,
  maxDimension: 4096,
  webp: false,
});

console.log(result.main);    // FileItem — the processed main image
console.log(result.thumbs);  // { 256: FileItem, 512: FileItem, 1024: FileItem }

Thumbnails are named {basename}__thumb_{size}.{ext} and stored in the same directory as the main file:

/photos/wedding.jpg
/photos/wedding__thumb_256.jpg
/photos/wedding__thumb_512.jpg
/photos/wedding__thumb_1024.jpg

WebP transcoding

const result = await processImage(buffer, {
  webp: true,
  thumbnails: [256, 512],
});
// result.metadata.format === 'webp'
// result.thumbnails[0].format === 'webp'
// thumbnail extension in uploadProcessedImage: __thumb_256.webp

Error Types

All errors extend HazoError subclasses from hazo_core (Wave 2). The legacy class names are preserved so existing instanceof checks and errorType discriminators keep working.

import {
  ImageProcessingError,
  ImageUploadError,
  SharpMissingError,
  UnsupportedFormatError,
} from 'hazo_images';
import { HazoError } from 'hazo_core';

try {
  const result = await processImage(badBuffer);
} catch (err) {
  if (err instanceof UnsupportedFormatError) {
    // Buffer was not a recognized image format (e.g. PDF, text)
    console.error('Not an image:', err.message);
  } else if (err instanceof ImageProcessingError) {
    // Sharp failed during processing
    console.error('Processing failed:', err.message);
    console.error('Original error:', err.originalError);
  } else if (HazoError.is(err)) {
    // Any other hazo_images error — read `err.code` to discriminate
    console.error(err.code, err.message);
  }
}

Error reference

| Class | Extends | Code | HTTP | When | |---|---|---|---|---| | UnsupportedFormatError | HazoValidationError | HAZO_IMAGES_UNSUPPORTED_FORMAT | 400 | Buffer is not a recognized image format | | ImageProcessingError | HazoExternalError | HAZO_IMAGES_PROCESSING_FAILED | 502 | Sharp failed during processing or thumbnail generation | | SharpMissingError | HazoConfigError | HAZO_IMAGES_CONFIG_SHARP_MISSING | n/a | Optional sharp peer dep is not installed | | ImageUploadError | HazoExternalError | HAZO_IMAGES_UPLOAD_FAILED | 502 | Underlying FileManager.uploadFile returned success: false |

Subpath exports

| Import | Contents | |---|---| | hazo_images | Shared types + error classes (isomorphic, no Node deps) | | hazo_images/server | processImage, uploadProcessedImage, createImageProcessHandler (Node.js only) | | hazo_images/ui | ImageUploader, ImageViewer, ImageEditorDialog + control-schema helpers (React, client-only) | | hazo_images/runware | createRunwareClient, assemblePrompts, 4 error classes (Node.js only) |

Supported formats

Passes through whatever Sharp supports: JPEG, PNG, WebP, AVIF, TIFF, GIF (static), SVG (rasterized). HEIC requires the optional sharp-heic package installed separately.

Options reference

interface ProcessImageOptions {
  stripExif?: boolean;    // default: true  — strip all EXIF metadata
  autoRotate?: boolean;   // default: true  — apply EXIF orientation, then strip tag
  maxDimension?: number;  // default: 4096  — longest side; 0 = disable resize
  webp?: boolean;         // default: false — transcode to WebP
  thumbnails?: number[];  // default: []    — pixel sizes for longest side

  // Geometry (Phase 1)
  crop?: {                // Extract region — applied BEFORE rotate, clamped to (auto-oriented) input dims
    left: number; top: number; width: number; height: number;
    shape?: 'rect' | 'square' | 'circle'; // 'circle' masks corners transparent & forces PNG output
  };
  rotate?: number;        // Rotate by degrees (Sharp fills gaps with black)
  flip?: boolean;         // Flip vertically (top↔bottom)
  flop?: boolean;         // Flop horizontally (left↔right)

  // Filters (Phase 1)
  grayscale?: boolean;    // Convert to greyscale
  sharpen?: number;       // Unsharp-mask sigma (e.g. 1.5)
  blur?: number;          // Gaussian blur sigma (e.g. 1.0)
  brightness?: number;    // Modulate multiplier — 1 = unchanged (e.g. 1.2 = +20%)
  saturation?: number;    // Modulate multiplier — 1 = unchanged (e.g. 0 = greyscale)
  hue?: number;           // Hue rotation in degrees (e.g. 180 = complementary)
  tint?: string;          // Hex colour tint applied via Sharp's tint() (e.g. "#ff0000")

  // Output (Phase 1)
  quality?: number;       // JPEG / WebP quality 1–100 (default: 80)
}

Pipeline order

The Sharp pipeline applies operations in this order:

  1. autoOrient — apply EXIF orientation
  2. extractcrop region (clamped to the auto-oriented input dims). For shape: 'circle' a circular alpha mask is composited (corners → transparent) and output is forced to PNG. The crop is baked to an intermediate buffer so the following rotate is applied after the crop (Sharp otherwise reorders rotate before a pre-resize extract).
  3. rotate — explicit rotation (degrees)
  4. flip / flop — vertical / horizontal mirror
  5. withMetadata(false) — strip EXIF (when stripExif: true)
  6. resize — longest side to maxDimension (maintaining aspect ratio)
  7. modulatebrightness, saturation, hue
  8. grayscale
  9. sharpen / blur
  10. tint
  11. Format encode — webp / jpeg / png with quality (PNG forced for circular crops)
  12. Thumbnail generation (each thumbnail runs the same pipeline with a different resize target)

Route handler factory (createImageProcessHandler)

createImageProcessHandler returns a framework-agnostic (request: Request) => Promise<Response> suitable for mounting directly as a Next.js App-Router route handler (or any other WHATWG Request/Response environment).

// app/api/images/process/route.ts
import { createImageProcessHandler } from 'hazo_images/server';
import { createInitializedFileManager } from 'hazo_files/server';

export const POST = createImageProcessHandler({
  getFileManager: async () =>
    createInitializedFileManager({ config: { provider: 'local', local: { basePath: './uploads' } } }),
  maxUploadBytes: 15 * 1024 * 1024,  // default: 15 MB
  allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
  previewMaxDimension: 1600,          // default: 1600 — size of preview returned in response
  savePathPrefix: '/hazo-images',     // default: '/hazo-images'
});

Request format (multipart FormData)

| Field | Type | Required | Description | |---|---|---|---| | image | File | yes | The source image file | | options | JSON string | no | ProcessImageOptions to apply | | save | "true" | no | Persist via the file manager | | filename | string | no | Override the stored filename (otherwise uses uploaded filename) |

Response JSON

Success:

{
  "ok": true,
  "metadata": { "width": 1200, "height": 800, "format": "jpeg" },
  "preview": "data:image/jpeg;base64,…",
  "originalPreview": "data:image/jpeg;base64,…",
  "thumbnails": [{ "size": 256, "format": "jpeg", "dataUrl": "data:image/jpeg;base64,…" }],
  "saved": { "path": "/hazo-images/photo.jpg", "url": "https://…" }
}

saved is null when save is not "true". saveError (string) is present if the file manager threw but the preview still succeeded.

Error:

{ "ok": false, "error": "Unsupported image format", "errorType": "UnsupportedFormatError" }

HTTP status: UnsupportedFormatError → 400, ImageProcessingError → 422, other → 500.

Types

import type {
  CreateImageProcessHandlerOptions,
  ImageProcessHandlerFileManager,
} from 'hazo_images/server';

ImageProcessHandlerFileManager is a duck-typed interface (uploadFile method) — any hazo_files FileManager satisfies it.


<ImageEditorDialog> (hazo_images/ui)

A full-featured client-side image editor dialog built on hazo_ui (shadcn/ui primitives). Requires hazo_ui ^4.4.0, react, and react-dom (all optional peers — install them only if you use this subpath).

"use client" required. All exports from hazo_images/ui are client components. Import them inside "use client" files or dynamic-import with ssr: false.

Minimal usage (uncontrolled, default endpoint)

'use client';
import { ImageEditorDialog } from 'hazo_images/ui';

export function MyPage() {
  return (
    <ImageEditorDialog
      imageUrl="/uploads/photo.jpg"
      endpoint="/api/images/process"
      onSave={(result) => console.log('saved', result.saved?.path)}
    />
  );
}

The dialog talks to the endpoint URL with the multipart format expected by createImageProcessHandler. A live before/after preview updates after each debounced control change.

Customised controls

import { ImageEditorDialog, DEFAULT_IMAGE_EDIT_CONTROLS } from 'hazo_images/ui';

<ImageEditorDialog
  imageUrl="/uploads/photo.jpg"
  endpoint="/api/images/process"
  controls={[
    ...DEFAULT_IMAGE_EDIT_CONTROLS,
    { key: 'tint', label: 'Tint', type: 'color', tab: 'filters', default: '#ffffff' },
  ]}
  defaultOptions={{ quality: 90, maxDimension: 2000 }}
  debounceMs={400}
  labels={{ save: 'Export', cancel: 'Discard' }}
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | imageUrl | string | — | Source image URL (displayed as "before") | | endpoint | string | — | URL of createImageProcessHandler route (use this or onProcess) | | onProcess | (opts: ProcessImageOptions) => Promise<...> | — | Custom processing callback instead of endpoint | | onSave | (result) => void | — | Called after a successful save | | controls | ImageEditControl[] | DEFAULT_IMAGE_EDIT_CONTROLS | Declarative control schema (see below) | | defaultOptions | ProcessImageOptions | {} | Options pre-applied on open | | debounceMs | number | 300 | Debounce delay (ms) before sending preview request | | labels | Partial<EditorLabels> | — | Override button / tab label strings | | headerSlot | React.ReactNode | — | Extra content in the dialog header | | footerSlot | React.ReactNode | — | Extra content in the dialog footer | | className | string | — | Extra class on the dialog container |

The dialog is organised into three tabs: Geometry (crop / rotate / flip / flop), Filters (grayscale / sharpen / blur / brightness / saturation / hue / tint), and Output (format / quality / resize).

The stage shows a live preview. Geometry (rotate / flip / flop) and most filters (blur / brightness / saturation / hue / grayscale / tint) render instantly as a CSS transform / filter / blend on the stage image — they update in realtime while you drag, with no server round-trip. These are omitted from the debounced (debounceMs) server preview to avoid double-applying; Save bakes them server-side via Sharp so output is exact. sharpen, crop, and output (format / quality / resize) are reflected via the debounced server preview.

The rotate control shows 0° / 90° / 180° / 270° preset buttons beside the slider for one-click quarter turns. The tint control is a native color picker (swatch + editable hex + Clear).

Crop is a toggle mode on the Geometry tab: click Crop image to swap the stage to the original with a draggable box (8 resize handles) and a shape picker — rectangle, square (1:1), or circle (1:1, masked to a transparent-cornered PNG) — then Done to return to the live preview. A Reset button (outlined) restores every control to its default and clears the crop, keeping the loaded image. Override the Reset, Crop image, and Done strings via labels.reset / labels.crop / labels.cropDone.

Helper exports

import {
  DEFAULT_IMAGE_EDIT_CONTROLS, // ImageEditControl[] — the default control schema
  defaultControlValues,        // Record<string, unknown> — zeroed defaults
  controlValuesToOptions,      // (values) => ProcessImageOptions
  validate_image_edit_controls // (controls) => string[] — validation errors
} from 'hazo_images/ui';

Control schema types

import type { ImageEditControl } from 'hazo_images/ui';
// Each control: { key, label, type, tab, default, min?, max?, step? }
// type: 'slider' | 'switch' | 'select' | 'color'
// tab: 'geometry' | 'filters' | 'output'

Runware (AI image generation)

hazo_images/runware wraps the Runware REST API behind a factory client.

import { createRunwareClient } from 'hazo_images/runware';

const client = createRunwareClient({
  // apiKey?: defaults to HAZO_IMAGES_RUNWARE_API_KEY then RUNWARE_API_KEY
  defaultModel: 'runware:z-image@turbo',
  timeoutMs: 60_000,
});

const { imageURL, cost, taskUUID } = await client.generateImage({
  positivePrompt: 'a chibi explorer with a stopwatch',
  width: 1024,
  height: 1024,
});

Error classes

| Class | Triggered by | Code(s) | |---|---|---| | RunwareApiError | network fail, 5xx, abort, empty result | HAZO_IMAGES_RUNWARE_HTTP_FAILURE, HAZO_IMAGES_RUNWARE_TIMEOUT, HAZO_IMAGES_RUNWARE_EMPTY_RESULT | | RunwareAuthError | missing key, 401, 403 | HAZO_IMAGES_RUNWARE_MISSING_KEY, HAZO_IMAGES_RUNWARE_AUTH_FAILED | | RunwareRateLimitError | 429 — exposes retryAfter: number \| null | HAZO_IMAGES_RUNWARE_RATE_LIMITED | | RunwareValidationError | 400 or errors[] in body | upstream code or HAZO_IMAGES_RUNWARE_BAD_REQUEST |

assemblePrompts

Generic helper for the prefix + character + image + suffix pattern:

import { assemblePrompts } from 'hazo_images/runware';

const { positive, negative } = assemblePrompts({
  imagePrompt: 'astronaut on mars',
  masterStylePrefix: 'cinematic,',
  characterPositive: 'red spacesuit',
  masterNegative: 'low quality, blurry',
});

Roadmap

| Version | Scope | |---|---| | @1.0 | Server pipeline: processImage + uploadProcessedImage | | @1.1 | Wave 2 standardisation: HazoError subclasses, structured logging, INI config | | @1.2 | hazo_images/runware sub-export (AI image generation) | | @1.3 | blur Gaussian-blur option on processImage | | @1.4 | Extended pipeline options (crop / rotate / flip / flop / grayscale / sharpen / brightness / saturation / hue / tint / quality); createImageProcessHandler factory; hazo_images/ui (ImageEditorDialog + helpers) | | @1.5 | ImageEditorDialog: instant CSS live preview for geometry + filters (realtime while dragging); interactive crop (drag + 8 handles) with rect / square / circle shapes; rotate 0/90/180/270 presets; tint color picker; Reset button. Server: crop runs before rotate; crop.shape: 'circle' masks to a transparent PNG |

License

MIT