Skip to main content

Overview

The @standardagents/sip package (Small Image Processor) provides memory-efficient image processing designed for memory-constrained edge computing environments.

Key Features

  • Process 100MP+ images with less than 1MB peak memory
  • DCT-based scaling for JPEG (decode at reduced resolution)
  • Scanline-by-scanline processing (never holds full image in memory)
  • Native WASM codecs (libjpeg-turbo, libspng)
  • Works in edge runtimes, browsers, and Node.js

Why sip?

Edge computing environments typically have strict memory limits (e.g., 128MB). Traditional image processing libraries decode the entire image into memory:
Image SizeDecoded Memory
12MP (4000×3000)~36MB
25MP (6000×4166)~75MB
50MP (8688×5792)~151MB
100MP (11375×8992)~307MB
A single 25MP image upload can crash your edge function without sip’s streaming approach.
sip’s solution:
  1. DCT Scaling - Decode JPEG at 1/2, 1/4, or 1/8 scale during decompression
  2. Scanline Streaming - Process one row at a time (~50KB peak for any image size)
  3. Native WASM - libjpeg-turbo and libspng compiled to WebAssembly

Installation

npm
npm install @standardagents/sip
pnpm
pnpm add @standardagents/sip
yarn
yarn add @standardagents/sip
JPEG and PNG processing requires the WASM module to be built. See WASM Build below.

Quick Start

import { sip, probe } from "@standardagents/sip"
import { readFile, writeFile } from "fs/promises"

// Read an image file
const imageBuffer = await readFile("my-image.jpg")

// Get image info without decoding
const info = probe(imageBuffer)
console.log(info)
// { format: 'jpeg', width: 4000, height: 3000, hasAlpha: false }

// Process image: resize and convert to JPEG
const result = await sip.process(imageBuffer, {
  maxWidth: 2048,
  maxHeight: 2048,
  quality: 85,
})

console.log(result)
// {
//   data: ArrayBuffer,
//   width: 2048,
//   height: 1536,
//   mimeType: 'image/jpeg',
//   originalFormat: 'jpeg'
// }

// Save the resized image
await writeFile("my-image-resized.jpg", Buffer.from(result.data))

Functions

probe

Detect image format and dimensions by reading only the header bytes. No full decode occurs.
import { probe } from "@standardagents/sip"

const info = probe(imageBuffer)
// { format: 'jpeg', width: 4000, height: 3000, hasAlpha: false }
Parameters:
ParameterTypeDescription
dataArrayBuffer | Uint8ArrayImage data
Returns: ProbeResult
interface ProbeResult {
  format: "jpeg" | "png" | "webp" | "avif" | "unknown"
  width: number   // 0 if unknown
  height: number  // 0 if unknown
  hasAlpha: boolean
}
Format Detection:
FormatMagic Bytes
JPEGFF D8 FF
PNG89 50 4E 47 0D 0A 1A 0A
WebPRIFF....WEBP
AVIF....ftyp with avif/avis brand

sip.process

Process an image: decode, resize, and encode to JPEG.
import { sip } from "@standardagents/sip"

const result = await sip.process(imageBuffer, {
  maxWidth: 2048,
  maxHeight: 2048,
  maxBytes: 1.5 * 1024 * 1024,
  quality: 85,
})
Parameters:
ParameterTypeDescription
dataArrayBufferInput image data
optionsProcessOptionsProcessing configuration
Options:
OptionTypeDefaultDescription
maxWidthnumber4096Maximum output width in pixels
maxHeightnumber4096Maximum output height in pixels
maxBytesnumber1572864 (1.5MB)Target maximum file size
qualitynumber85JPEG quality (1-100)
Returns: Promise<ProcessResult>
interface ProcessResult {
  data: ArrayBuffer        // Output JPEG data
  width: number            // Output width
  height: number           // Output height
  mimeType: "image/jpeg"   // Always JPEG
  originalFormat: ImageFormat  // Input format detected
}
Behavior:
  • Aspect Ratio: Always preserved. Output fits within maxWidth×maxHeight box
  • Quality Reduction: If output exceeds maxBytes, retries with lower quality
  • Size Reduction: If still over maxBytes at minimum quality, resizes smaller

initStreaming

Initialize the WASM module. Optional but recommended for reducing first-call latency.
import { initStreaming } from "@standardagents/sip"

// Call early in your application startup
const available = await initStreaming()
console.log(`WASM available: ${available}`)
Returns: Promise<boolean> - true if WASM loaded successfully.

Format Support

Input Formats

FormatDecoderMethodMemory Usage
JPEGlibjpeg-turbo (WASM)DCT scaling + scanlineMinimal
PNGlibspng (WASM)Progressive scanlineMinimal
WebP@jsquash/webpFull decodeHigher
AVIF@jsquash/avifFull decodeHigher

Output Format

Output is always JPEG. This simplifies the encoder and provides universal browser compatibility.

Memory Model

DCT Scaling (JPEG only)

JPEG uses Discrete Cosine Transform (DCT) for compression. libjpeg-turbo can decode at reduced resolution during decompression:
ScaleOutput SizeMemory Savings
1/1Full sizeNone
1/250% each dimension4× less
1/425% each dimension16× less
1/812.5% each dimension64× less
Example: 6800×4500 image (30.6MP)
ScaleOutputDecode Memory
1/16800×4500~92MB
1/23400×2250~23MB
1/41700×1125~5.7MB
1/8850×563~1.4MB
sip automatically selects the optimal scale based on target dimensions.

Scanline Streaming

Instead of decoding the entire image, sip processes row-by-row:
Input Image (any size)

Decode Row 0 → Resize → Encode  ← Only these rows in memory
Decode Row 1 → Resize → Encode

Output JPEG (streamed)
Memory per row (2000px width): ~24KB total, constant regardless of image height.

WASM Build

Prerequisites

Install the Emscripten SDK:
# Clone emsdk
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

# Install and activate latest version
./emsdk install latest
./emsdk activate latest

# Add to current shell
source ./emsdk_env.sh

Building

cd packages/sip
pnpm build:wasm
This will:
  1. Download libjpeg-turbo, libspng, and miniz to wasm/libs/
  2. Compile with Emscripten
  3. Output dist/sip.js and dist/sip.wasm
The WASM files are not committed to git. Your CI/CD pipeline must run pnpm build:wasm.

Configuration Examples

Recommended Settings by Use Case:
Use CasemaxWidthmaxHeightmaxBytesquality
Thumbnails40040050KB80
Preview10241024200KB82
Standard204820481.5MB85
High Quality409640965MB90

Examples

Thumbnail Generation

import { sip } from "@standardagents/sip"

async function createThumbnail(buffer: ArrayBuffer): Promise<ArrayBuffer> {
  const result = await sip.process(buffer, {
    maxWidth: 200,
    maxHeight: 200,
    maxBytes: 30 * 1024, // 30KB max
    quality: 80,
  })
  return result.data
}

Upload Handler with Validation

import { sip, probe } from "@standardagents/sip"

const ALLOWED_FORMATS = ["jpeg", "png", "webp", "avif"]
const MAX_INPUT_SIZE = 50 * 1024 * 1024 // 50MB

async function handleImageUpload(buffer: ArrayBuffer) {
  // Validate input size
  if (buffer.byteLength > MAX_INPUT_SIZE) {
    throw new Error(`Image too large: ${buffer.byteLength} bytes`)
  }

  // Validate format
  const info = probe(buffer)
  if (!ALLOWED_FORMATS.includes(info.format)) {
    throw new Error(`Unsupported format: ${info.format}`)
  }

  // Process
  return await sip.process(buffer, {
    maxWidth: 4096,
    maxHeight: 4096,
    maxBytes: 2 * 1024 * 1024,
    quality: 85,
  })
}

Edge Function Integration

import { sip, probe, initStreaming } from "@standardagents/sip"

// Warm up WASM on startup
let wasmReady = initStreaming()

export default {
  async fetch(request: Request): Promise<Response> {
    await wasmReady

    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 })
    }

    try {
      const buffer = await request.arrayBuffer()
      const result = await sip.process(buffer, {
        maxWidth: 2048,
        maxHeight: 2048,
        quality: 85,
      })

      return new Response(result.data, {
        headers: {
          "Content-Type": "image/jpeg",
          "Content-Length": String(result.data.byteLength),
        },
      })
    } catch (error) {
      return new Response(`Processing failed: ${error.message}`, { status: 500 })
    }
  },
}

Multiple Size Generation

import { sip } from "@standardagents/sip"

async function generateSizes(buffer: ArrayBuffer) {
  const [thumbnail, preview, full] = await Promise.all([
    sip.process(buffer, { maxWidth: 150, maxHeight: 150, quality: 80 }),
    sip.process(buffer, { maxWidth: 800, maxHeight: 800, quality: 82 }),
    sip.process(buffer, { maxWidth: 2048, maxHeight: 2048, quality: 85 }),
  ])

  return {
    thumbnail: thumbnail.data,
    preview: preview.data,
    full: full.data,
  }
}

TypeScript Support

The package includes full TypeScript definitions:
import type {
  ProbeResult,
  ProcessOptions,
  ProcessResult,
  ImageFormat,
} from "@standardagents/sip"

Limitations

No PNG, WebP, or AVIF output. All processed images become JPEG.
Transparency is discarded. PNG/WebP with alpha become opaque JPEG.
Without WASM, JPEG and PNG processing throws an error. WebP and AVIF still work but use more memory.
The output may slightly exceed maxBytes. The algorithm tries quality reduction first, then dimension reduction.
EXIF orientation is not applied. Handle EXIF separately if needed.

Error Handling

try {
  const result = await sip.process(imageBuffer, options)
} catch (error) {
  if (error.message.includes("WASM module not available")) {
    // WASM not built - run `pnpm build:wasm`
    console.error("Run: pnpm build:wasm")
  } else if (error.message.includes("Unknown image format")) {
    // Unsupported format
    console.error("Unsupported image format")
  } else {
    // Other error (corrupt image, etc.)
    console.error("Processing failed:", error.message)
  }
}

Comparison with Alternatives

Library100MP JPEG MemoryEdge CompatibleOutput Formats
sip~50KBYesJPEG
sharp (Node)~300MBNoMany
jimp~300MBNoMany
@cf-wasm/photon~300MBLimited*Many
*@cf-wasm/photon requires paid bindings for large images in some edge environments.

Next Steps