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

ngx-media-optimizer

v2.1.0

Published

Framework-agnostic image optimization library with parallel processing, format conversion, and compression. Works with Angular, React, Vue, and vanilla JavaScript.

Readme

ngx-media-optimizer

Framework-agnostic image optimization library. Works with Angular, React, Vue, and vanilla JS.

NPM Version NPM Downloads Bundle Size License Tests

Compress and convert images entirely in the browser — no server, no dependencies. Built on the browser's native OffscreenCanvas and createImageBitmap APIs, with zero third-party packages.

  • Zero dependencies — no browser-image-compression, no bundled C++ codecs, no surprises
  • Format conversion — WebP, AVIF, JPEG, PNG
  • Parallel batch processing — auto-detected concurrency, configurable
  • Binary-search quality — hits a target file size budget automatically
  • Reactive state — zero-dependency callback subscriptions, no RxJS, no framework required
  • 228 tests passing

Installation

npm install ngx-media-optimizer

Quick start

Angular

import { Component, inject, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { ImageConverterService, type ImageFile } from 'ngx-media-optimizer';

@Component({
  standalone: true,
  template: `
    <input type="file" multiple accept="image/*" (change)="onFiles($event)" />
    @for (img of images; track img.id) {
      @if (img.status === 'completed') {
        <img [src]="img.compressedUrl" />
        <span>{{ svc.formatBytes(img.originalSize) }} → {{ svc.formatBytes(img.compressedSize) }}</span>
      }
    }
  `
})
export class ImageUploaderComponent implements OnDestroy {
  protected readonly svc = new ImageConverterService();   // plain class — no DI needed
  protected images: ReadonlyArray<ImageFile> = [];
  private readonly cdr = inject(ChangeDetectorRef);
  private readonly unsub = this.svc.onImagesChange(imgs => {
    this.images = imgs;
    this.cdr.markForCheck();
  });

  async onFiles(e: Event): Promise<void> {
    const files = (e.target as HTMLInputElement).files!;
    await this.svc.convertFormat(files, { outputFormat: 'webp', quality: 80 });
  }

  ngOnDestroy(): void {
    this.unsub();
    this.svc.destroy();
  }
}

React

import { useEffect, useState } from 'react';
import { ImageConverterService, type ImageFile } from 'ngx-media-optimizer';

// Create once outside the component (or use a context / singleton)
const svc = new ImageConverterService();

export function ImageUploader() {
  const [images, setImages] = useState<ReadonlyArray<ImageFile>>([]);

  useEffect(() => svc.onImagesChange(setImages), []);

  async function onFiles(e: React.ChangeEvent<HTMLInputElement>) {
    if (e.target.files) {
      await svc.convertFormat(e.target.files, { outputFormat: 'webp', quality: 80 });
    }
  }

  return (
    <>
      <input type="file" multiple accept="image/*" onChange={onFiles} />
      {images.map(img =>
        img.status === 'completed' && (
          <div key={img.id}>
            <img src={img.compressedUrl} />
            <span>{img.originalSize} → {img.compressedSize} bytes</span>
          </div>
        )
      )}
    </>
  );
}

Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { ImageConverterService, type ImageFile } from 'ngx-media-optimizer';

const svc = new ImageConverterService();
const images = ref<ReadonlyArray<ImageFile>>([]);
let unsub: (() => void) | undefined;

onMounted(() => { unsub = svc.onImagesChange(imgs => { images.value = imgs; }); });
onUnmounted(() => unsub?.());

async function onFiles(e: Event) {
  const files = (e.target as HTMLInputElement).files!;
  await svc.convertFormat(files, { outputFormat: 'webp', quality: 80 });
}
</script>

<template>
  <input type="file" multiple accept="image/*" @change="onFiles" />
  <template v-for="img in images" :key="img.id">
    <img v-if="img.status === 'completed'" :src="img.compressedUrl" />
  </template>
</template>

How it works

Processing pipeline for every image:

  1. createImageBitmap(file) — decode the source image
  2. Stepwise halving — if the image needs resizing, dimensions are halved iteratively (avoids quality loss from a single large-scale resize)
  3. OffscreenCanvas.convertToBlob({ type, quality }) — encode to the target format using the browser's native codec
  4. Binary-search quality (when maxSizeMB is set) — up to 5 iterations to find the highest quality that still fits the budget, within ±2% accuracy

Everything runs on the calling thread — there is no worker involved. useWebWorker in options is a no-op kept for backward compatibility and will be removed in a future major version.


API — ImageConverterService

The main service. Handles conversion, compression, state, and batch processing.

convertFormat(files, options): Promise<void>

Converts images to a different format.

import { ImageConverterService } from 'ngx-media-optimizer';

const svc = new ImageConverterService();

try {
  await svc.convertFormat(files, {
    outputFormat: 'webp',   // required
    quality: 80,            // 0–100, default 80
    maxSizeMB: 1,           // target file size budget
    maxWidthOrHeight: 1920, // default 1920
    concurrency: 4,         // default: auto-detected from navigator.hardwareConcurrency
    sortOrder: 'asc',       // 'asc' | 'desc' | 'none' — sort by size before processing
  });
} catch (err) {
  console.error(err);
}

compressImages(files, options): Promise<void>

Same as convertFormat but keeps the original format.

await svc.compressImages(files, {
  quality: 85,
  maxSizeMB: 0.5,
});

abortProcessing(): void

Cancels any in-flight processing. Already-completed images are kept; pending ones become 'error'.

svc.abortProcessing();

removeAllImages(): void

Clears the image list and revokes all object URLs.

destroy(): void

Releases all resources held by the service instance. Revokes all blob URLs, clears all event listeners, and cancels pending download timers. Call this when the owning component is destroyed.

// Angular
ngOnDestroy(): void { this.svc.destroy(); }

// React
useEffect(() => () => svc.destroy(), []);

// Vue
onUnmounted(() => svc.destroy());

State subscriptions

All callbacks return an unsubscribe function. Call it in ngOnDestroy / useEffect cleanup / onUnmounted.

const unsub = svc.onImagesChange(images => { /* ... */ });
const unsub2 = svc.onUploadingChange(isUploading => { /* ... */ });
const unsub3 = svc.onProgressChange(progress => { /* ... */ });

// cleanup
unsub();

State getters

svc.images              // ReadonlyArray<ImageFile>
svc.completedImages     // only status === 'completed'
svc.completedCount      // number

svc.totalOriginalSize   // bytes
svc.totalCompressedSize // bytes
svc.savingsPercentage   // 0–100

svc.isUploading         // boolean
svc.uploadProgress      // 0–100

Utility methods

svc.formatBytes(1536000)                        // "1.46 MB"
svc.getImageSize(file)                          // "2.38 MB"
svc.getSavingsPercentage(1000000, 600000)       // 40

API — ImageUtilsService

Standalone utility service. Fully independent — use it without ImageConverterService if you only need analysis or validation.

Validation

import { ImageUtilsService } from 'ngx-media-optimizer';

const utils = new ImageUtilsService();

// Check if a file is a supported image
utils.isValidImage(file)  // boolean — checks MIME type and extension

// Check if a file exceeds a size threshold
utils.needsCompression(file, 1)  // boolean — true if file > 1 MB

Image analysis

// Dimensions (cached after first call)
const { width, height } = await utils.getImageDimensions(file);

// Full info object (cached)
const info = await utils.getImageInfo(file);
// { name, size, formattedSize, format, width, height, aspectRatio, aspectRatioString }

// Transparency (cached) — useful before converting to JPEG
const transparent = await utils.hasTransparency(file);

// Animated GIF/WebP detection (cached)
const animated = await utils.isAnimated(file);

// Dominant color as hex string
const color = await utils.getDominantColor(file);  // e.g. "#3a7bd5"

Format capabilities

// Which formats does this browser actually support?
const formats = await utils.getSupportedFormats();
// e.g. ['webp', 'avif', 'jpeg', 'png']

Uses OffscreenCanvas.convertToBlob to probe each format — results reflect what the browser can actually encode, not just what it can decode.

Size estimation

// Estimate compressed size without encoding (synchronous heuristic)
const bytes = utils.estimateCompressedSize(file, 80, 'webp');

// Find the highest quality level that fits a size budget (binary search, up to 5 iterations)
const quality = utils.getBestQuality(file, 0.5, 'webp');  // quality for ≤ 0.5 MB

estimateCompressedSize is a fast synchronous heuristic based on file size and quality. getBestQuality does the actual binary search — call it before processing if you need an accurate quality recommendation.

Thumbnail

const thumb = await utils.createThumbnail(file, 200);  // max 200px on longest side

Batch validation

const results = await utils.validateBatch(fileList, {
  maxSizeMB: 5,
  minSizeMB: 0,
  maxWidth: 4000,
  maxHeight: 4000,
});
// results: Array<{ file: File; valid: boolean; errors: string[] }>

Cache

All async methods cache results in LRU caches (O(1) hit/eviction). Cache is per-service-instance.

utils.clearCache();
utils.getCacheStats();
// { dimensions: number; info: number; transparency: number; dominantColor: number }

Types

ImageFile (discriminated union)

type ImageFile =
  | { id: string; name: string; originalSize: number; originalUrl: string; quality: number; status: 'pending' | 'processing' | 'error' }
  | { id: string; name: string; originalSize: number; originalUrl: string; quality: number; status: 'completed'; compressedSize: number; compressedUrl: string };

compressedUrl and compressedSize are only accessible after narrowing on status === 'completed'. TypeScript enforces this.

if (img.status === 'completed') {
  console.log(img.compressedUrl);  // safe
  console.log(img.compressedSize); // safe
}

CompletedImageFile is exported as a convenience alias:

import type { CompletedImageFile } from 'ngx-media-optimizer';

function render(img: CompletedImageFile) {
  return `<img src="${img.compressedUrl}" />`;
}

ConvertOptions / CompressOptions

interface BaseProcessOptions {
  quality?: number;           // 0–100, default 80
  maxSizeMB?: number;         // default 10
  maxWidthOrHeight?: number;  // default 1920
  concurrency?: number;       // default: navigator.hardwareConcurrency / 2
  sortOrder?: 'asc' | 'desc' | 'none';  // default 'asc' (smallest first)
}

interface ConvertOptions extends BaseProcessOptions {
  outputFormat: 'webp' | 'jpeg' | 'png' | 'avif';  // required
}

interface CompressOptions extends BaseProcessOptions {}

ImageFormat

type ImageFormat = 'webp' | 'jpeg' | 'png' | 'avif';

ImageInfo

interface ImageInfo {
  name: string;
  size: number;
  formattedSize: string;
  format: string;       // e.g. "image/jpeg"
  width: number;
  height: number;
  aspectRatio: number;
  aspectRatioString: string;  // e.g. "16:9"
}

Error classes

import { ValidationError, AbortError, CompressionError } from 'ngx-media-optimizer';

try {
  await svc.convertFormat(files, opts);
} catch (err) {
  if (err instanceof ValidationError)  { /* invalid file */ }
  if (err instanceof AbortError)       { /* abortProcessing() was called */ }
  if (err instanceof CompressionError) { /* codec failure */ }
}

Migration from v1 to v2

v2 has one breaking change: browser-image-compression has been removed as a dependency. The library now encodes natively via OffscreenCanvas. For most use cases this is transparent — results will be slightly different (native codec quality curves differ from libvips), but the API is unchanged.

Removed in v2.0.1:

  • useWebWorker option — remove it from your options objects

Renamed in v2: | v1 | v2 | |---|---| | validateImage(file) | isValidImage(file) | | shouldCompress(file, n) | needsCompression(file, n) |

New in v2:

  • getSupportedFormats() — probe actual codec support
  • estimateCompressedSize(file, quality, format?) — fast size heuristic
  • getBestQuality(file, targetMB, format?) — binary-search quality
  • hasTransparency(file) — before converting to JPEG
  • isAnimated(file) — detect animated GIF / WebP
  • sortOrder option on all operations

Contributing

git clone https://github.com/barbozaa/media-optimizer-workspace.git
cd media-optimizer-workspace
npm install

# run the 228 tests
npx vitest run

# build the library
npm run build:lib

License

MIT © Barboza