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

image-normalizer

v1.0.2

Published

Client-side image preprocessing library with MIME validation, HEIC conversion and image compression.

Readme

image-normalizer

TypeScript library for preprocessing and normalizing images in browser web applications.

Live demo — interactive playground with full documentation.

Users pick photos from any device — Android, iPhone, or desktop — and you get back a validated, resized, compressed File ready to upload. One function handles MIME detection, HEIC conversion, validation, and compression.

Table of contents


Why use this library?

Uploading user images in web apps is harder than it looks:

| Problem | What image-normalizer does | | --- | --- | | Users rename files or pick wrong extensions | Detects the real MIME type from file magic bytes (file-type) | | iPhone photos are HEIC/HEIF | Converts them to JPEG automatically (heic-to) | | Large photos slow uploads | Resizes and compresses in the browser (browser-image-compression) | | You need consistent output | Returns a new File with your chosen format, size limits, and quality |

The original File is never modified. You always receive a fresh normalized copy.


Requirements

  • Browser only — relies on File, Blob, AbortSignal, and Web Workers.
  • ES modulesimport { normalizeImage } from "image-normalizer".
  • Works with React, Vue, Svelte, Angular, or plain HTML/JS.

This package does not run in Node.js. Use it on the client before uploading to your server.


Installation

pnpm add image-normalizer
# or
npm install image-normalizer
# or
yarn add image-normalizer

Quick start

import { normalizeImage } from "image-normalizer";

const input = document.querySelector<HTMLInputElement>("#photo");

input?.addEventListener("change", async (event) => {
  const file = (event.target as HTMLInputElement).files?.[0];
  if (!file) return;

  const result = await normalizeImage(file);

  console.log(result.file);           // normalized File
  console.log(result.detectedMime);   // e.g. "image/heic"
  console.log(result.originalSize);   // bytes before
  console.log(result.normalizedSize); // bytes after
});

All options have sensible defaults (10 MB max, 2048 px max dimension, JPEG output, 0.8 quality). See API reference to customize.


Examples

File input (vanilla JS)

<input type="file" id="photo" accept="image/*" />
<p id="status"></p>

<script type="module">
  import { normalizeImage } from "image-normalizer";

  const input = document.querySelector("#photo");
  const status = document.querySelector("#status");

  input.addEventListener("change", async () => {
    const file = input.files?.[0];
    if (!file) return;

    status.textContent = "Processing…";

    try {
      const { file: normalized, originalSize, normalizedSize } =
        await normalizeImage(file, {
          maxFileSizeMB: 10,
          maxWidthOrHeight: 1920,
          quality: 0.85,
          outputFormat: "image/jpeg",
        });

      const saved = ((1 - normalizedSize / originalSize) * 100).toFixed(0);
      status.textContent = `Ready: ${normalized.name} (${saved}% smaller)`;
    } catch (error) {
      status.textContent = error instanceof Error ? error.message : "Failed";
    }
  });
</script>

React

import { useState } from "react";
import { normalizeImage } from "image-normalizer";
import {
  FileTooLargeError,
  UnsupportedMimeError,
} from "image-normalizer";

export function PhotoUpload() {
  const [error, setError] = useState<string | null>(null);
  const [preview, setPreview] = useState<string | null>(null);

  async function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0];
    if (!file) return;

    setError(null);

    try {
      const result = await normalizeImage(file, {
        maxWidthOrHeight: 2048,
        outputFormat: "image/webp",
      });

      setPreview(URL.createObjectURL(result.file));
      // upload result.file to your API
    } catch (err) {
      if (err instanceof UnsupportedMimeError) {
        setError(`Unsupported type: ${err.detectedMime}`);
      } else if (err instanceof FileTooLargeError) {
        setError("File is too large. Max 10 MB.");
      } else {
        setError("Could not process image.");
      }
    }
  }

  return (
    <div>
      <input type="file" accept="image/*" onChange={handleChange} />
      {error && <p role="alert">{error}</p>}
      {preview && <img src={preview} alt="Preview" width={200} />}
    </div>
  );
}

Drag and drop

import { normalizeImage } from "image-normalizer";

const dropZone = document.querySelector<HTMLElement>("#drop-zone")!;

dropZone.addEventListener("dragover", (e) => {
  e.preventDefault();
  dropZone.classList.add("drag-over");
});

dropZone.addEventListener("dragleave", () => {
  dropZone.classList.remove("drag-over");
});

dropZone.addEventListener("drop", async (e) => {
  e.preventDefault();
  dropZone.classList.remove("drag-over");

  const file = e.dataTransfer?.files[0];
  if (!file) return;

  const result = await normalizeImage(file);
  await uploadToServer(result.file);
});

Upload with fetch

import { normalizeImage } from "image-normalizer";

async function uploadPhoto(file: File) {
  const { file: normalized } = await normalizeImage(file, {
    maxWidthOrHeight: 1600,
    quality: 0.8,
    outputFormat: "image/jpeg",
  });

  const formData = new FormData();
  formData.append("photo", normalized, normalized.name);

  const response = await fetch("/api/upload", {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    throw new Error("Upload failed");
  }

  return response.json();
}

Cancel with AbortSignal

Useful when the user navigates away or clicks "Cancel" during processing.

import { normalizeImage } from "image-normalizer";

const controller = new AbortController();

// Call controller.abort() to cancel
document.querySelector("#cancel")?.addEventListener("click", () => {
  controller.abort();
});

try {
  const result = await normalizeImage(file, {
    signal: controller.signal,
  });
} catch (error) {
  if (controller.signal.aborted) {
    console.log("Cancelled by user");
  } else {
    throw error;
  }
}

Restrict allowed formats

By default, JPEG, PNG, WebP, HEIC, and HEIF are accepted. Narrow the list if your backend only supports certain types:

import { normalizeImage } from "image-normalizer";

// Only JPEG and PNG — rejects HEIC, WebP, etc.
const result = await normalizeImage(file, {
  acceptedTypes: ["image/jpeg", "image/png"],
});

iPhone HEIC photos

iOS often saves photos as HEIC even when the file extension looks like .jpg. The library detects the real type and converts HEIC/HEIF to JPEG before compression:

import { normalizeImage } from "image-normalizer";

const result = await normalizeImage(iphonePhoto, {
  outputFormat: "image/jpeg",
  quality: 0.85,
});

// result.detectedMime → "image/heic" (what was actually in the file)
// result.file.type     → "image/jpeg" (what you upload)
// result.file.name     → "IMG_1234.jpg" (extension updated)

Error handling

All errors extend ImageNormalizerError and expose a stable code for programmatic handling:

import {
  normalizeImage,
  ImageNormalizerError,
  UnsupportedMimeError,
  UnknownFileTypeError,
  FileTooLargeError,
  HeicConversionError,
  CompressionError,
} from "image-normalizer";

try {
  await normalizeImage(file);
} catch (error) {
  if (error instanceof UnsupportedMimeError) {
    // User picked GIF, PDF, etc.
    console.log(error.code);           // "UNSUPPORTED_MIME"
    console.log(error.detectedMime);   // e.g. "image/gif"
    console.log(error.acceptedTypes);  // what you allowed
  } else if (error instanceof UnknownFileTypeError) {
    // Corrupted or unrecognized file
    console.log(error.fileName);
  } else if (error instanceof FileTooLargeError) {
    console.log(error.fileSize, error.maxFileSize);
  } else if (error instanceof HeicConversionError) {
    console.log(error.cause); // underlying conversion error
  } else if (error instanceof CompressionError) {
    console.log(error.cause);
  } else if (error instanceof ImageNormalizerError) {
    console.log(error.code, error.message);
  }
}

API reference

normalizeImage(file, options?)

Processes a File through the full normalization pipeline.

function normalizeImage(
  file: File,
  options?: NormalizeImageOptions,
): Promise<NormalizeImageResult>;

| Parameter | Type | Description | | --- | --- | --- | | file | File | User-selected image file | | options | NormalizeImageOptions | Optional configuration (see below) |

Returns: Promise<NormalizeImageResult>

Throws: One of the exported errors if validation or processing fails.


NormalizeImageOptions

All fields are optional. Unset fields use the defaults shown.

| Option | Type | Default | Description | | --- | --- | --- | --- | | acceptedTypes | AcceptedImageMime[] | jpeg, png, webp, heic, heif | Allowed input MIME types (detected from file content, not extension) | | maxFileSizeMB | number | 10 | Maximum original file size in megabytes | | maxFileSize | number | — | Maximum original file size in bytes. Overrides maxFileSizeMB when set | | outputFormat | OutputFormat | "image/jpeg" | Desired output MIME type after compression | | quality | number | 0.8 | Compression quality from 0 (lowest) to 1 (highest). Also used for HEIC → JPEG conversion | | maxWidthOrHeight | number | 2048 | Maximum width or height in pixels (aspect ratio preserved) | | useWebWorker | boolean | true | Run compression in a Web Worker (faster, non-blocking UI) | | signal | AbortSignal | — | Pass an AbortSignal to cancel compression mid-process |

acceptedTypes values

type AcceptedImageMime =
  | "image/jpeg"
  | "image/png"
  | "image/webp"
  | "image/heic"
  | "image/heif";

outputFormat values

type OutputFormat = "image/jpeg" | "image/png" | "image/webp";

Size limit notes

  • Size is validated on the original file before any conversion or compression.
  • Use maxFileSize for precise byte limits (e.g. 5 * 1024 * 1024 for 5 MB).
  • Use maxFileSizeMB for readable config (e.g. 10 for 10 MB).

Quality notes

  • quality: 0.8 is a good balance for photos.
  • PNG output ignores quality more aggressively than JPEG/WebP.
  • Lower maxWidthOrHeight often reduces file size more than lowering quality.

NormalizeImageResult

Object returned on success:

| Field | Type | Description | | --- | --- | --- | | file | File | New normalized file (original is untouched) | | detectedMime | string | MIME type detected from magic bytes of the original file | | originalSize | number | Size of the original file in bytes | | normalizedSize | number | Size of the normalized file in bytes |

The returned file:

  • Has its extension updated to match outputFormat (e.g. photo.heicphoto.jpg).
  • Keeps the original lastModified timestamp.
  • Has type set to your outputFormat.

Exported types

import type {
  AcceptedImageMime,
  NormalizeImageOptions,
  NormalizeImageResult,
  OutputFormat,
  ImageNormalizerErrorCode,
} from "image-normalizer";

| Type | Description | | --- | --- | | AcceptedImageMime | Union of allowed input MIME types | | OutputFormat | Union of allowed output MIME types | | NormalizeImageOptions | Options object for normalizeImage | | NormalizeImageResult | Success result object | | ImageNormalizerErrorCode | String literal union of all error codes |


Exported errors

import {
  ImageNormalizerError,
  UnsupportedMimeError,
  UnknownFileTypeError,
  FileTooLargeError,
  HeicConversionError,
  CompressionError,
} from "image-normalizer";

| Class | code | When it throws | Extra properties | | --- | --- | --- | --- | | UnsupportedMimeError | UNSUPPORTED_MIME | Detected type not in acceptedTypes | detectedMime, acceptedTypes | | UnknownFileTypeError | UNKNOWN_FILE_TYPE | Magic bytes don't match any known type | fileName | | FileTooLargeError | FILE_TOO_LARGE | Original file exceeds size limit | fileSize, maxFileSize | | HeicConversionError | HEIC_CONVERSION_FAILED | HEIC/HEIF → JPEG conversion failed | cause | | CompressionError | COMPRESSION_FAILED | Resize/compress step failed | cause |

All classes extend ImageNormalizerError, which extends Error and adds:

class ImageNormalizerError extends Error {
  readonly code: ImageNormalizerErrorCode;
}

How it works

Processing runs in a fixed pipeline. If any step fails, later steps are skipped and an error is thrown.

User File
    │
    ▼
┌─────────────────────┐
│ 1. Detect MIME      │  file-type (magic bytes)
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ 2. Validate type    │  against acceptedTypes
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ 3. Validate size    │  against maxFileSize
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ 4. Convert HEIC     │  heic-to (only if HEIC/HEIF)
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ 5. Compress/resize  │  browser-image-compression
└─────────┬───────────┘
          ▼
┌─────────────────────┐
│ 6. Build result     │  new File + metadata
└─────────────────────┘

Important behaviors:

  • MIME detection uses file content, not the filename extension.
  • HEIC/HEIF is converted to JPEG first; then outputFormat is applied during compression.
  • EXIF metadata is not preserved (preserveExif: false).
  • The input File is never mutated.

Supported formats

Input (detected from file content)

| MIME type | Notes | | --- | --- | | image/jpeg | Standard photos | | image/png | Screenshots, graphics with transparency | | image/webp | Modern web format | | image/heic | Common on iPhones | | image/heif | HEIF variant |

Output (after normalization)

| MIME type | Extension | | --- | --- | | image/jpeg | .jpg | | image/png | .png | | image/webp | .webp |

GIF, SVG, BMP, and other formats are not supported as input.


Content Security Policy

If useWebWorker is true (the default), compression runs in a Web Worker. Your CSP must allow:

script-src 'self' blob: https://cdn.jsdelivr.net;

| Directive | Why | | --- | --- | | blob: | Web Worker scripts are created from blob URLs | | https://cdn.jsdelivr.net | Default CDN used by browser-image-compression for worker scripts |

To avoid the CDN requirement, disable workers:

await normalizeImage(file, { useWebWorker: false });

Trade-off: compression runs on the main thread and may block UI on large images.

HEIC conversion (heic-to) may require additional CSP rules in strict environments. Test with your production CSP before shipping.


FAQ

Does this work in Node.js or Next.js Server Actions?

No. This library is browser-only. In Next.js, call normalizeImage from a Client Component or inside useEffect / event handlers — not in Server Components or API routes.

Why was my .jpg file rejected as HEIC?

iPhones often save photos as HEIC with a .jpg extension. The library reads magic bytes, not the extension, so it correctly detects image/heic. Conversion handles this automatically.

Can I get a Blob instead of a File?

The result is always a File, but you can convert it:

const { file } = await normalizeImage(input);
const blob = file.slice(0, file.size, file.type);

Will the normalized file always be smaller?

Usually, but not guaranteed. Small or already-optimized images might end up similar in size or slightly larger after re-encoding. Check originalSize vs normalizedSize.

Can I process multiple files?

Call normalizeImage once per file. For batches:

const results = await Promise.all(
  Array.from(files).map((file) => normalizeImage(file)),
);

What happens to EXIF data (GPS, orientation)?

EXIF is stripped during compression. This is intentional for privacy and consistency. If you need EXIF, this library is not the right tool.


Development

git clone <repo-url>
cd image-normalizer
pnpm install

pnpm test          # run tests in watch mode
pnpm test:run      # single test run
pnpm test:coverage # with coverage report
pnpm build         # build dist/
pnpm typecheck     # TypeScript check
pnpm lint          # ESLint
pnpm format        # Prettier check

License

MIT — see LICENSE.