{"_id":"@alosha/pixsqueeze","_rev":"3-d7689ec58d5fc9a72186960949302bbc","name":"@alosha/pixsqueeze","dist-tags":{"latest":"1.4.1"},"versions":{"1.3.5":{"name":"@alosha/pixsqueeze","version":"1.3.5","keywords":["image","compress","compressor","image-compressor","image-compression","heic","tiff","camera-raw","raw","webp","image-converter","lossy-compression","file-upload","upload","javascript","browser","client-side","front-end","web"],"author":{"url":"https://github.com/avlisodraude","name":"Eduardo Silva Navarrete"},"license":"MIT","_id":"@alosha/pixsqueeze@1.3.5","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"homepage":"https://pixsqueeze.alosha.dev/demo","bugs":{"url":"https://github.com/avlisodraude/pixsqueeze/issues"},"dist":{"shasum":"27de64cb746e820d61728f3f9329c6559178390b","tarball":"https://registry.npmjs.org/@alosha/pixsqueeze/-/pixsqueeze-1.3.5.tgz","fileCount":15,"integrity":"sha512-H5jjRVB9W6mW5ADdcN5Z8tTDB92jJjQ9l8GrvGvrcop+vTcKew0mWAbPnpwT6l5NnwIix1AAOCXPLZLy0j2ZLg==","signatures":[{"sig":"MEUCICdNh736vCqpdhOhBGc0j9+Hlp2HHPa9p5NQp5sQOiRnAiEA24NdX16BT6uxiQ6JyJ+CGFobJU17i40b+0q3v1/m6aY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":148988},"main":"dist/pixsqueeze.common.js","types":"types/index.d.ts","module":"dist/pixsqueeze.esm.js","browser":"dist/pixsqueeze.js","gitHead":"de33fe6d5f04392ff741bee3d3b4ae690ac9cea9","scripts":{"dev":"rollup -c -m -w","lint":"npm run lint:js && npm run lint:css","test":"NODE_ENV=test karma start test/karma.conf.js","build":"rollup -c","clean":"node --eval \"require('fs').rmSync('dist', { recursive: true, force: true })\"","start":"npm run dev","server":"node server/index.js","lint:js":"eslint --fix","prepare":"husky","release":"npm run clean && npm run lint && npm run build && npm test","lint:css":"stylelint docs/**/*.css --fix"},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"overrides":{"mocha":{"diff":">=8.0.3","serialize-javascript":">=7.0.5"},"@tootallnate/once":">=2.0.1"},"repository":{"url":"git+https://github.com/avlisodraude/pixsqueeze.git","type":"git"},"_npmVersion":"11.16.0","description":"Compress images in the browser before upload — HEIC, TIFF & camera RAW supported via server-side conversion.","directories":{},"_nodeVersion":"22.22.3","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.5.0","husky":"^9.1.7","karma":"^6.4.4","mocha":"^11.7.6","sharp":"^0.34.5","eslint":"^10.4.1","multer":"^2.1.1","rollup":"^4.61.1","express":"^5.2.1","globals":"^16.5.0","puppeteer":"^25.1.0","stylelint":"^17.12.0","@eslint/js":"^10.0.1","karma-chai":"^0.1.0","compression":"^1.8.1","karma-mocha":"^2.0.1","lint-staged":"^17.0.7","heic-convert":"^2.1.0","@commitlint/cli":"^21.0.2","stylelint-order":"^8.1.1","karma-mocha-reporter":"^2.2.5","@rollup/plugin-terser":"^1.0.0","karma-chrome-launcher":"^3.2.0","eslint-plugin-import-x":"^4.16.2","@rollup/plugin-commonjs":"^29.0.3","stylelint-config-standard":"^40.0.0","@rollup/plugin-node-resolve":"^16.0.3","@commitlint/config-conventional":"^21.0.2"},"_npmOperationalInternal":{"tmp":"tmp/pixsqueeze_1.3.5_1782674811866_0.6291047334062203","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@alosha/pixsqueeze","version":"1.4.0","keywords":["image","compress","compressor","image-compressor","image-compression","heic","tiff","camera-raw","raw","webp","image-converter","lossy-compression","file-upload","upload","javascript","browser","client-side","front-end","web"],"author":{"url":"https://github.com/avlisodraude","name":"Eduardo Silva Navarrete"},"license":"MIT","_id":"@alosha/pixsqueeze@1.4.0","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"homepage":"https://pixsqueeze.alosha.dev/demo","bugs":{"url":"https://github.com/avlisodraude/pixsqueeze/issues"},"dist":{"shasum":"22e9e453a352952b7f04e18f5b553b0dca4eda29","tarball":"https://registry.npmjs.org/@alosha/pixsqueeze/-/pixsqueeze-1.4.0.tgz","fileCount":24,"integrity":"sha512-wOSPENwiVmcFzQc8L0BtcknrSLGQE8flRR8JxjgLGLERuhxN+sy4qqI6cbyyADhbGU+HePHKo2hfJqhmQjdnrg==","signatures":[{"sig":"MEUCIDVywxl6Ls/OOlOo0Y4Nq9evl/DBy6i12EYXM21KGryMAiEAo/K40KbjG0ssRuMwVjXZR+4SQIfJtUB+/uoNJd1pp78=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":174423},"main":"dist/pixsqueeze.common.js","types":"types/index.d.ts","module":"dist/pixsqueeze.esm.js","browser":"dist/pixsqueeze.js","exports":{".":{"types":"./types/index.d.ts","import":"./dist/pixsqueeze.esm.js","require":"./dist/pixsqueeze.common.js"},"./raw":{"types":"./types/raw.d.ts","import":"./dist/raw.esm.js","require":"./dist/raw.common.js"},"./heic":{"types":"./types/heic.d.ts","import":"./dist/heic.esm.js","require":"./dist/heic.common.js"},"./tiff":{"types":"./types/tiff.d.ts","import":"./dist/tiff.esm.js","require":"./dist/tiff.common.js"},"./src/*":"./src/*","./package.json":"./package.json"},"gitHead":"bafa14156b939c99483fc6fd688786b12a17d873","scripts":{"dev":"rollup -c -m -w","lint":"npm run lint:js && npm run lint:css","test":"NODE_ENV=test karma start test/karma.conf.js","build":"rollup -c","clean":"node --eval \"require('fs').rmSync('dist', { recursive: true, force: true })\"","start":"npm run dev","server":"node server/index.js","lint:js":"eslint --fix","prepare":"husky","release":"npm run clean && npm run lint && npm run build && npm test","lint:css":"stylelint docs/**/*.css --fix"},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"overrides":{"mocha":{"diff":">=8.0.3","serialize-javascript":">=7.0.5"},"@tootallnate/once":">=2.0.1"},"repository":{"url":"git+https://github.com/avlisodraude/pixsqueeze.git","type":"git"},"_npmVersion":"11.16.0","description":"Compress images in the browser before upload — HEIC, TIFF & camera RAW supported via server-side conversion.","directories":{},"_nodeVersion":"22.22.3","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"chai":"^4.5.0","husky":"^9.1.7","karma":"^6.4.4","mocha":"^11.7.6","sharp":"^0.34.5","eslint":"^10.4.1","multer":"^2.1.1","rollup":"^4.61.1","express":"^5.2.1","globals":"^16.5.0","puppeteer":"^25.1.0","stylelint":"^17.12.0","@eslint/js":"^10.0.1","karma-chai":"^0.1.0","compression":"^1.8.1","karma-mocha":"^2.0.1","lint-staged":"^17.0.7","heic-convert":"^2.1.0","@commitlint/cli":"^21.0.2","stylelint-order":"^8.1.1","karma-mocha-reporter":"^2.2.5","@rollup/plugin-terser":"^1.0.0","karma-chrome-launcher":"^3.2.0","eslint-plugin-import-x":"^4.16.2","@rollup/plugin-commonjs":"^29.0.3","stylelint-config-standard":"^40.0.0","@rollup/plugin-node-resolve":"^16.0.3","@commitlint/config-conventional":"^21.0.2"},"_npmOperationalInternal":{"tmp":"tmp/pixsqueeze_1.4.0_1783608450803_0.2468998023467488","host":"s3://npm-registry-packages-npm-production"}},"1.4.1":{"name":"@alosha/pixsqueeze","version":"1.4.1","publishConfig":{"access":"public"},"description":"Compress images in the browser before upload — HEIC, TIFF & camera RAW supported via server-side conversion.","main":"dist/pixsqueeze.common.js","module":"dist/pixsqueeze.esm.js","browser":"dist/pixsqueeze.js","types":"types/index.d.ts","exports":{".":{"types":"./types/index.d.ts","import":"./dist/pixsqueeze.esm.js","require":"./dist/pixsqueeze.common.js"},"./heic":{"types":"./types/heic.d.ts","import":"./dist/heic.esm.js","require":"./dist/heic.common.js"},"./tiff":{"types":"./types/tiff.d.ts","import":"./dist/tiff.esm.js","require":"./dist/tiff.common.js"},"./raw":{"types":"./types/raw.d.ts","import":"./dist/raw.esm.js","require":"./dist/raw.common.js"},"./package.json":"./package.json","./src/*":"./src/*"},"scripts":{"build":"rollup -c","clean":"node --eval \"require('fs').rmSync('dist', { recursive: true, force: true })\"","dev":"rollup -c -m -w","lint":"npm run lint:js && npm run lint:css","lint:css":"stylelint docs/**/*.css --fix","lint:js":"eslint --fix","prepare":"husky","release":"npm run clean && npm run lint && npm run build && npm test","server":"node server/index.js","start":"npm run dev","test":"NODE_ENV=test karma start test/karma.conf.js"},"repository":{"type":"git","url":"git+https://github.com/avlisodraude/pixsqueeze.git"},"keywords":["image","compress","compressor","image-compressor","image-compression","heic","tiff","camera-raw","raw","webp","image-converter","lossy-compression","file-upload","upload","javascript","browser","client-side","front-end","web"],"author":{"name":"Eduardo Silva Navarrete","url":"https://github.com/avlisodraude"},"license":"MIT","bugs":{"url":"https://github.com/avlisodraude/pixsqueeze/issues"},"homepage":"https://pixsqueeze.alosha.dev/demo","overrides":{"@tootallnate/once":">=2.0.1","mocha":{"diff":">=8.0.3","serialize-javascript":">=7.0.5"}},"devDependencies":{"@commitlint/cli":"^21.0.2","@commitlint/config-conventional":"^21.0.2","@eslint/js":"^10.0.1","@rollup/plugin-commonjs":"^29.0.3","@rollup/plugin-node-resolve":"^16.0.3","@rollup/plugin-terser":"^1.0.0","chai":"^4.5.0","eslint":"^10.4.1","eslint-plugin-import-x":"^4.16.2","globals":"^16.5.0","husky":"^9.1.7","karma":"^6.4.4","karma-chai":"^0.1.0","karma-chrome-launcher":"^3.2.0","karma-mocha":"^2.0.1","karma-mocha-reporter":"^2.2.5","lint-staged":"^17.0.7","mocha":"^11.7.6","puppeteer":"^25.1.0","rollup":"^4.61.1","stylelint":"^17.12.0","stylelint-config-standard":"^40.0.0","stylelint-order":"^8.1.1","compression":"^1.8.1","express":"^5.2.1","heic-convert":"^2.1.0","multer":"^2.1.1","sharp":"^0.34.5"},"gitHead":"d834b17553b4df7f61bec271717786ec8c7b2cf5","_id":"@alosha/pixsqueeze@1.4.1","_nodeVersion":"22.22.3","_npmVersion":"11.16.0","dist":{"integrity":"sha512-QQri8AF27zVaKRJ7nW6oDzKJx+5Fxiuj10UCUbqfq7fmF+pR574pTQOlpHG783xhCU05AoOwTLO7+VCt2KQdEQ==","shasum":"a2b41d94f3ff49d39bceb05b7836eb6b14e72038","tarball":"https://registry.npmjs.org/@alosha/pixsqueeze/-/pixsqueeze-1.4.1.tgz","fileCount":24,"unpackedSize":175341,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBg4GC8gTjKqsTa9ylzQZp4Xd+2n1oyH4Uff3DMkj2JVAiBfOBx627Qt5G6F/483dC5KzykrW6oUIo/yE8ZGWahiSQ=="}]},"_npmUser":{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"},"directories":{},"maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pixsqueeze_1.4.1_1783608788544_0.5957954002790506"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-28T19:26:51.651Z","modified":"2026-07-09T14:53:08.811Z","1.3.5":"2026-06-28T19:26:52.014Z","1.4.0":"2026-07-09T14:47:30.960Z","1.4.1":"2026-07-09T14:53:08.677Z"},"bugs":{"url":"https://github.com/avlisodraude/pixsqueeze/issues"},"author":{"name":"Eduardo Silva Navarrete","url":"https://github.com/avlisodraude"},"license":"MIT","homepage":"https://pixsqueeze.alosha.dev/demo","keywords":["image","compress","compressor","image-compressor","image-compression","heic","tiff","camera-raw","raw","webp","image-converter","lossy-compression","file-upload","upload","javascript","browser","client-side","front-end","web"],"repository":{"type":"git","url":"git+https://github.com/avlisodraude/pixsqueeze.git"},"description":"Compress images in the browser before upload — HEIC, TIFF & camera RAW supported via server-side conversion.","maintainers":[{"name":"avlisodraude","email":"avlis_odraude@hotmail.com"}],"readme":"# PixSqueeze\n\n[![Coverage Status](https://img.shields.io/codecov/c/github/avlisodraude/pixsqueeze.svg)](https://codecov.io/gh/avlisodraude/pixsqueeze) [![Downloads](https://img.shields.io/npm/dm/@alosha/pixsqueeze.svg)](https://www.npmjs.com/package/@alosha/pixsqueeze) [![Version](https://img.shields.io/npm/v/@alosha/pixsqueeze.svg)](https://www.npmjs.com/package/@alosha/pixsqueeze) [![Gzip Size](https://img.shields.io/bundlephobia/minzip/@alosha/pixsqueeze.svg)](https://unpkg.com/@alosha/pixsqueeze/dist/pixsqueeze.common.js) [![Types included](https://img.shields.io/badge/types-included-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)\n\n> JavaScript image compressor with server-side conversion for HEIC, TIFF, and camera RAW formats. The client-side compression uses the browser's native [HTMLCanvasElement.toBlob()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) method — **lossy**, **asynchronous**, and behaviour varies across browsers. Precompress images on the client side before uploading, with automatic server-assisted pre-conversion for formats browsers cannot natively read.\n\nBuilt by [Alosha](https://alosha.dev) — privacy-first developer tools powered by open source.\n\n- [▶ Live demo](https://pixsqueeze.alosha.dev/demo) · [Hosted Batch API](https://pixsqueeze.alosha.dev)\n\n## Table of contents\n\n- [Quick start](#quick-start)\n- [Main Files](#main-files)\n- [Getting started](#getting-started)\n- [Recipes](#recipes)\n  - [Compress an image before upload](#compress-an-image-before-upload)\n  - [Resize while compressing](#resize-while-compressing)\n  - [Convert to WebP or JPEG](#convert-to-webp-or-jpeg)\n  - [Compress an iPhone HEIC photo](#compress-an-iphone-heic-photo)\n  - [Compress a TIFF or camera RAW file](#compress-a-tiff-or-camera-raw-file)\n  - [Add a watermark while compressing](#add-a-watermark-while-compressing)\n  - [Convert to grayscale while compressing](#convert-to-grayscale-while-compressing)\n  - [Cancel an in-progress compression](#cancel-an-in-progress-compression)\n- [Demo server](#demo-server)\n- [Server-side conversion API](#server-side-conversion-api)\n- [Batch API](#batch-api)\n  - [Production recipes](#production-recipes)\n- [Options](#options)\n- [Methods](#methods)\n- [No conflict](#no-conflict)\n- [Browser support](#browser-support)\n- [Contributing](#contributing)\n- [Versioning](#versioning)\n- [License](#license)\n\n## Quick start\n\nInstall it:\n\n```shell\nnpm install @alosha/pixsqueeze\n```\n\nCompress a file the user picked, in three lines:\n\n```js\nimport PixSqueeze from \"@alosha/pixsqueeze\";\n\nnew PixSqueeze(file, {\n  quality: 0.6,\n  success: (result) => uploadToServer(result), // a `File`/`Blob`, ready to send\n  error: (err) => console.error(err.message),\n});\n```\n\nThat's the whole API surface for the common case — pick a quality between `0` (smallest, lowest quality) and `1` (largest, original quality), get a compressed file back in `success`. Everything below shows how to handle more specific scenarios: resizing, converting formats, handling iPhone photos (HEIC), scans (TIFF), camera RAW files, watermarks, and more.\n\n[⬆ back to top](#table-of-contents)\n\n## Main Files\n\n```text\ndist/\n├── pixsqueeze.js        (UMD)\n├── pixsqueeze.min.js    (UMD, compressed)\n├── pixsqueeze.common.js (CommonJS, default)\n└── pixsqueeze.esm.js    (ES Module)\n```\n\n## Getting started\n\n### Install\n\n```shell\nnpm install @alosha/pixsqueeze\n```\n\n### Usage\n\n#### Syntax\n\n```js\nnew PixSqueeze(file[, options])\n```\n\n**file**\n\n- Type: [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) or [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)\n\nThe target image file for compressing.\n\n**options**\n\n- Type: `Object`\n- Optional\n\nThe options for compressing. Check out the available [options](#options).\n\n#### Example\n\n```html\n<input type=\"file\" id=\"file\" accept=\"image/*\" />\n```\n\n```js\nimport PixSqueeze from \"@alosha/pixsqueeze\";\n\ndocument.getElementById(\"file\").addEventListener(\"change\", (e) => {\n  const file = e.target.files[0];\n\n  if (!file) {\n    return;\n  }\n\n  new PixSqueeze(file, {\n    quality: 0.6,\n\n    // The compression process is asynchronous,\n    // which means you have to access the `result` in the `success` hook function.\n    success(result) {\n      const formData = new FormData();\n\n      // The third parameter is required for server\n      formData.append(\"file\", result, result.name);\n\n      // Send the compressed image file to server with XMLHttpRequest.\n      fetch(\"/path/to/upload\", { method: \"POST\", body: formData });\n    },\n    error(err) {\n      console.log(err.message);\n    },\n  });\n});\n```\n\n[⬆ back to top](#table-of-contents)\n\n## Recipes\n\nShort, focused examples for the situations you'll run into most. Each one is meant to be copied and adapted directly.\n\n### Compress an image before upload\n\nThe most common use case — shrink a file the user picked before sending it to your server.\n\n```js\nimport PixSqueeze from \"@alosha/pixsqueeze\";\n\nfunction compressAndUpload(file) {\n  new PixSqueeze(file, {\n    quality: 0.6,\n    success(result) {\n      const formData = new FormData();\n\n      formData.append(\"file\", result, result.name);\n\n      fetch(\"/upload\", { method: \"POST\", body: formData });\n    },\n    error(err) {\n      console.error(\"Compression failed:\", err.message);\n    },\n  });\n}\n```\n\n### Resize while compressing\n\nCap the output dimensions — useful for thumbnails, avatars, or fitting images into a known layout. Use `maxWidth`/`maxHeight` to set a ceiling, or `width`/`height` with `resize` to fit/crop into an exact box.\n\n```js\n// Cap the longest side at 1280px, keep the aspect ratio\nnew PixSqueeze(file, {\n  maxWidth: 1280,\n  maxHeight: 1280,\n  quality: 0.7,\n  success: (result) => console.log(result),\n});\n\n// Force an exact 400x400 square, cropping to fill (like a profile picture)\nnew PixSqueeze(file, {\n  width: 400,\n  height: 400,\n  resize: \"cover\",\n  quality: 0.8,\n  success: (result) => console.log(result),\n});\n```\n\n### Convert to WebP or JPEG\n\nForce the output format regardless of the input — handy for standardizing what your server stores.\n\n```js\nnew PixSqueeze(file, {\n  mimeType: \"image/webp\", // or \"image/jpeg\", \"image/png\", \"auto\"\n  quality: 0.75,\n  success: (result) => console.log(result.type), // \"image/webp\"\n});\n```\n\n> **Note:** Safari cannot encode to WebP via canvas. If you need guaranteed WebP output across all browsers, convert on the server instead.\n\n### Compress an iPhone HEIC photo\n\nBrowsers can't decode HEIC/HEIF — the format iPhones save photos in by default. The conversion happens on a server: detect the format in the browser, POST the file to a conversion endpoint, then hand the returned JPEG to the client-side compressor.\n\n> **You need to run the conversion endpoint yourself.** The npm package ships the browser-side helpers only. The Express server that implements `/api/convert/heic`, `/api/convert/tiff` and `/api/convert/raw` lives in [`server/`](https://github.com/avlisodraude/compressorjs/tree/main/server) in this repository — clone it and deploy it, or point the helpers at your own endpoint via their second argument. There is no hosted endpoint bundled with the package.\n\n```js\nimport PixSqueeze from \"@alosha/pixsqueeze\";\nimport { isHeicFile, convertHeicOnServer } from \"@alosha/pixsqueeze/heic\";\n\nasync function handleFile(file) {\n  const resolvedFile = (await isHeicFile(file)) ? await convertHeicOnServer(file) : file;\n\n  new PixSqueeze(resolvedFile, {\n    quality: 0.6,\n    success: (result) => console.log(\"Ready to upload:\", result),\n    error: (err) => console.error(err.message),\n  });\n}\n```\n\n> Run `npm run server` to start the bundled conversion server locally — see [Server-side conversion API](#server-side-conversion-api) for the full endpoint reference and ready-to-use detection helpers.\n\n### Compress a TIFF or camera RAW file\n\nSame idea as HEIC — these formats aren't readable by `<canvas>`, so they're routed through a `/api/convert/tiff` or `/api/convert/raw` endpoint first. Each format has its own subpath import.\n\n```js\nimport PixSqueeze from \"@alosha/pixsqueeze\";\nimport { isTiffFile, convertTiffOnServer } from \"@alosha/pixsqueeze/tiff\";\nimport { isRawFile, convertRawOnServer } from \"@alosha/pixsqueeze/raw\";\n\n// Scanned document\nif (await isTiffFile(tiffFile)) {\n  const jpeg = await convertTiffOnServer(tiffFile);\n  new PixSqueeze(jpeg, { quality: 0.7, success: (r) => console.log(r) });\n}\n\n// Camera RAW (.cr2, .nef, .arw, .dng, etc. — see the full list in the API docs)\nif (await isRawFile(rawFile)) {\n  const jpeg = await convertRawOnServer(rawFile);\n  new PixSqueeze(jpeg, { quality: 0.7, success: (r) => console.log(r) });\n}\n```\n\nEach `convert*OnServer` helper takes an optional second argument to override the endpoint, so you can point it at wherever you deployed the server:\n\n```js\nconst jpeg = await convertRawOnServer(rawFile, \"https://images.example.com/convert/raw\");\n```\n\n### Add a watermark while compressing\n\nUse the `drew` hook to draw on top of the image after it's been placed on the canvas, before the final compressed output is produced.\n\n```js\nnew PixSqueeze(file, {\n  quality: 0.8,\n  drew(context, canvas) {\n    context.font = \"bold 2rem sans-serif\";\n    context.fillStyle = \"rgba(255, 255, 255, 0.6)\";\n    context.fillText(\"© Your Brand\", 20, canvas.height - 20);\n  },\n  success: (result) => console.log(result),\n});\n```\n\n### Convert to grayscale while compressing\n\nUse the `beforeDraw` hook to apply a canvas filter before the image is drawn.\n\n```js\nnew PixSqueeze(file, {\n  quality: 0.8,\n  beforeDraw(context, canvas) {\n    context.filter = \"grayscale(100%)\";\n  },\n  success: (result) => console.log(result),\n});\n```\n\n### Cancel an in-progress compression\n\nUseful when the user picks a new file before the previous compression finishes, or navigates away.\n\n```js\nconst job = new PixSqueeze(file, {\n  success: (result) => console.log(result),\n  error: (err) => console.log(\"Aborted or failed:\", err.message),\n});\n\n// Later, e.g. on a \"cancel\" button click or when a newer file is selected:\njob.abort();\n```\n\n[⬆ back to top](#table-of-contents)\n\n## Demo server\n\nAn Express development server lives under `server/` **in this repository**. It serves the `docs/` demo page and exposes the server-side conversion API endpoints.\n\n> This server is **not** part of the npm package — `npm install @alosha/pixsqueeze` gives you the browser-side compressor and the conversion helpers, not the endpoints they call. To use HEIC/TIFF/RAW conversion in production you must deploy this server yourself (or reimplement the three endpoints against your own image pipeline). It is a development server: review it before exposing it to untrusted input.\n\n### Start the server\n\n```shell\nnpm run server\n```\n\nOpens at `http://localhost:3000` by default. Set the `PORT` environment variable to override.\n\n### Server dependencies\n\n| Package        | Purpose                                               |\n| -------------- | ----------------------------------------------------- |\n| `express`      | HTTP server                                           |\n| `multer`       | Multipart file upload handling (200 MB limit)         |\n| `sharp`        | TIFF and RAW → JPEG conversion via libvips            |\n| `heic-convert` | HEIC/HEIF → JPEG conversion (runs in a worker thread) |\n| `compression`  | Gzip/Brotli for static assets and JSON responses      |\n\n[⬆ back to top](#table-of-contents)\n\n## Server-side conversion API\n\nBrowsers cannot natively decode HEIC, TIFF, or camera RAW files. The demo app sends these formats to the server, converts them to JPEG, and then feeds the result to the client-side compressor.\n\nAll endpoints accept a `multipart/form-data` POST with a single field named `file` and respond with:\n\n- **`200`** — `image/jpeg` binary body + `X-Original-Name` header with the renamed filename\n- **`400`** — No file uploaded or malformed request\n- **`413`** — File exceeds the 200 MB upload limit\n- **`415`** — File type not accepted by this endpoint\n- **`422`** — Conversion failed (corrupt or unsupported variant)\n\n### `POST /api/convert/heic`\n\nConverts a HEIC/HEIF image to JPEG at quality 95. Detection is done by MIME type **and** ISO Base Media File Format magic bytes, so iOS files with an empty MIME type are handled correctly. Conversion runs in a dedicated worker thread to avoid blocking the Node.js event loop.\n\n```shell\ncurl -X POST http://localhost:3000/api/convert/heic \\\n  -F \"file=@photo.heic\" \\\n  --output photo.jpg\n```\n\n### `POST /api/convert/tiff`\n\nConverts a TIFF image (including multi-page TIFFs) to JPEG at quality 95. Detection uses MIME type, `.tiff`/`.tif` file extension, and little/big-endian magic bytes.\n\n```shell\ncurl -X POST http://localhost:3000/api/convert/tiff \\\n  -F \"file=@scan.tiff\" \\\n  --output scan.jpg\n```\n\n### `POST /api/convert/raw`\n\nConverts camera RAW files to JPEG at quality 95. Automatically applies embedded orientation metadata. Supported formats:\n\n| Format    | Extensions                                     |\n| --------- | ---------------------------------------------- |\n| Adobe DNG | `.dng`                                         |\n| Canon     | `.cr2`, `.cr3`                                 |\n| Nikon     | `.nef`, `.nrw`                                 |\n| Sony      | `.arw`                                         |\n| Fujifilm  | `.raf`                                         |\n| Panasonic | `.rw2`                                         |\n| Pentax    | `.pef`                                         |\n| Olympus   | `.orf`                                         |\n| Samsung   | `.srw`                                         |\n| Other     | `.3fr`, `.dcr`, `.kdc`, `.mrw`, `.rwl`, `.x3f` |\n\n> **Note:** RAF and RW2 conversion requires the `sharp` binary to be built with LibRaw support. If your installed binary does not include it, the endpoint returns a `422` with a clear error message.\n\n```shell\ncurl -X POST http://localhost:3000/api/convert/raw \\\n  -F \"file=@DSC_0001.NEF\" \\\n  --output DSC_0001.jpg\n```\n\n[⬆ back to top](#table-of-contents)\n\n## Batch API\n\nNeed to compress hundreds or thousands of images server-side — without a browser? The **PixSqueeze Batch API** is a hosted REST API that accepts up to 1,000 images per request and returns compressed results as base64-encoded files.\n\n### Pricing\n\n| Plan       | Images / month | Price      |\n| ---------- | -------------- | ---------- |\n| Free       | 100            | $0         |\n| Starter    | 2,000          | $9 / mo    |\n| Pro        | 20,000         | $29 / mo   |\n| Business   | Unlimited      | $99 / mo   |\n\nFull documentation and dashboard: [alosha.dev](https://alosha.dev) · [API docs](https://alosha.dev/docs)\n\n### Get an API key\n\n```shell\ncurl -X POST https://pixsqueeze-api-production.up.railway.app/auth/register \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\":\"you@example.com\"}'\n```\n\nResponse:\n\n```json\n{\n  \"apiKey\": \"psx_...\",\n  \"plan\": \"FREE\",\n  \"monthlyLimit\": 100,\n  \"message\": \"Account created. Keep your API key safe — it will not be shown again.\"\n}\n```\n\n> **Note:** your key is shown only once. Registering an already-used email returns `409` — it never reveals the existing key. If you lose or leak your key, rotate it: `POST /auth/rotate-key` with `Authorization: Bearer <current-key>` returns a fresh key and revokes the old one.\n\n### Compress a batch of images\n\n```shell\ncurl -X POST https://pixsqueeze-api-production.up.railway.app/compress/batch \\\n  -H \"Authorization: Bearer psx_YOUR_API_KEY\" \\\n  -F \"files[]=@photo1.jpg\" \\\n  -F \"files[]=@photo2.heic\" \\\n  -F \"files[]=@scan.tiff\" \\\n  -F \"quality=0.7\" \\\n  -F \"maxWidth=1280\"\n```\n\nResponse:\n\n```json\n{\n  \"processed\": 3,\n  \"results\": [\n    { \"originalName\": \"photo1.jpg\", \"mimeType\": \"image/jpeg\", \"data\": \"base64...\" },\n    { \"originalName\": \"photo2.heic\", \"mimeType\": \"image/jpeg\", \"data\": \"base64...\" },\n    { \"originalName\": \"scan.tiff\",  \"mimeType\": \"image/jpeg\", \"data\": \"base64...\" }\n  ],\n  \"usage\": { \"used\": 3, \"limit\": 100, \"remaining\": 97 }\n}\n```\n\nSupported input formats: JPEG, PNG, WebP, GIF, HEIC/HEIF, TIFF, and camera RAW (CR2, NEF, ARW, DNG, and more).\n\n### Production recipes\n\nReal problems, complete solutions — copy, paste, ship.\n\n#### Compress and store user uploads in your Node backend\n\n**The problem:** you want to shrink every uploaded image before it hits storage, without bundling an image library or a native codec into your API.\n\n```js\nimport { writeFile } from \"node:fs/promises\";\n\n// Forward an upload to PixSqueeze, then persist the compressed result.\nconst form = new FormData();\nform.append(\"files[]\", new Blob([buffer]), \"upload.jpg\");\nform.append(\"quality\", \"0.7\");\nform.append(\"maxWidth\", \"1600\");\n\nconst res = await fetch(\"https://pixsqueeze-api-production.up.railway.app/compress/batch\", {\n  method: \"POST\",\n  headers: { Authorization: \"Bearer \" + process.env.PIXSQUEEZE_KEY },\n  body: form,\n});\n\nconst { results, usage } = await res.json();\nfor (const img of results) {\n  await writeFile(img.originalName, Buffer.from(img.data, \"base64\"));\n}\nconsole.log(usage.remaining + \" compressions left this month\");\n```\n\n**Why it works:** the batch endpoint returns each compressed image as base64 plus a live usage counter in one round-trip, so you compress, persist, and track quota without a second call or any image library in your own stack.\n\n#### Convert iPhone HEIC uploads to WebP on the fly\n\n**The problem:** phones upload HEIC and cameras upload RAW — formats browsers cannot display — and decoding them client-side is a non-starter.\n\n```js\n// Browsers can't decode HEIC — let PixSqueeze convert + compress server-side.\nconst form = new FormData();\nform.append(\"files[]\", heicFile); // a .heic straight from an iPhone\nform.append(\"mimeType\", \"image/webp\"); // force WebP output\nform.append(\"quality\", \"0.8\");\n\nconst res = await fetch(\"https://pixsqueeze-api-production.up.railway.app/compress/batch\", {\n  method: \"POST\",\n  headers: { Authorization: \"Bearer \" + apiKey },\n  body: form,\n});\n\nconst { results } = await res.json();\nconst src = \"data:image/webp;base64,\" + results[0].data; // ready for <img src>\n```\n\n**Why it works:** PixSqueeze decodes HEIC/RAW/TIFF server-side and re-encodes to a web format in the same request, so you accept whatever a device produces and hand the browser a WebP it can actually render — no client-side codec.\n\n### Check monthly usage\n\n```shell\ncurl https://pixsqueeze-api-production.up.railway.app/usage \\\n  -H \"Authorization: Bearer psx_YOUR_API_KEY\"\n```\n\n```json\n{\n  \"plan\": \"FREE\",\n  \"used\": 3,\n  \"limit\": 100,\n  \"remaining\": 97,\n  \"resetDate\": \"2026-07-01\"\n}\n```\n\n[⬆ back to top](#table-of-contents)\n\n## Options\n\nYou may set compressor options with `new PixSqueeze(file, options)`.\nIf you want to change the global default options, You may use `PixSqueeze.setDefaults(options)`.\n\n### strict\n\n- Type: `boolean`\n- Default: `true`\n\nIndicates whether to output the original image instead of the compressed one when the size of the compressed image is greater than the original one's, except the following cases:\n\n- The `retainExif` option is set to `true`.\n- The `mimeType` option is set and its value is different from the mime type of the image.\n- The `width` option is set and its value is greater than the natural width of the image.\n- The `height` option is set and its value is greater than the natural height of the image.\n- The `minWidth` option is set and its value is greater than the natural width of the image.\n- The `minHeight` option is set and its value is greater than the natural height of the image.\n- The `maxWidth` option is set and its value is less than the natural width of the image.\n- The `maxHeight` option is set and its value is less than the natural height of the image.\n\n### checkOrientation\n\n- Type: `boolean`\n- Default: `true`\n\nIndicates whether to read the image's Exif Orientation value (JPEG image only), and then rotate or flip the image automatically with the value.\n\n**Notes:**\n\n- Don't trust this all the time as some JPEG images have incorrect (not standard) Orientation values.\n- If the size of the target image is too large (e.g., greater than 10 MB), you should disable this option to avoid an out-of-memory crash.\n- The image's Exif information will be removed after compressed, so if you need the Exif information, you may need to upload the original image as well.\n\n### retainExif\n\n- Type: `boolean`\n- Default: `false`\n\nIndicates whether to retain the image's Exif information after compressed.\n\n### maxWidth\n\n- Type: `number`\n- Default: `Infinity`\n\nThe max-width of the output image. The value should be greater than `0`.\n\n> Avoid getting a blank output image, you might need to set the `maxWidth` and `maxHeight` options to limited numbers, because of [the size limits of a canvas element](https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element), recommend to use `4096` or lesser.\n\n### maxHeight\n\n- Type: `number`\n- Default: `Infinity`\n\nThe max height of the output image. The value should be greater than `0`.\n\n### minWidth\n\n- Type: `number`\n- Default: `0`\n\nThe min-width of the output image. The value should be greater than `0` and should not be greater than the `maxWidth`.\n\n### minHeight\n\n- Type: `number`\n- Default: `0`\n\nThe min-height of the output image. The value should be greater than `0` and should not be greater than the `maxHeight`.\n\n### width\n\n- Type: `number`\n- Default: `undefined`\n\nThe width of the output image. If not specified, the natural width of the original image will be used, or if the `height` option is set, the width will be computed automatically by the natural aspect ratio.\n\n### height\n\n- Type: `number`\n- Default: `undefined`\n\nThe height of the output image. If not specified, the natural height of the original image will be used, or if the `width` option is set, the height will be computed automatically by the natural aspect ratio.\n\n### resize\n\n- Type: `string`\n- Default: `\"none\"`\n- Options: `\"none\"`, `\"contain\"`, and `\"cover\"`.\n\nSets how the size of the image should be resized to the container specified by the `width` and `height` options.\n\n**Note:** This option only available when both the `width` and `height` options are specified.\n\n### quality\n\n- Type: `number`\n- Default: `0.8`\n\nThe quality of the output image. It must be a number between `0` and `1`. If this argument is anything else, the default values `0.92` and `0.80` are used for `image/jpeg` and `image/webp` respectively. Other arguments are ignored. Be careful to use `1` as it may make the size of the output image become larger.\n\n**Note:** This option only available for `image/jpeg` and `image/webp` images.\n\n> Check out the documentation of the [HTMLCanvasElement.toBlob()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) method for more detail.\n\n**Examples**:\n\n| Quality | Input size | Output size | Compression ratio | Description   |\n| ------- | ---------- | ----------- | ----------------- | ------------- |\n| 0       | 2.12 MB    | 114.61 KB   | 94.72%            | -             |\n| 0.2     | 2.12 MB    | 349.57 KB   | 83.90%            | -             |\n| 0.4     | 2.12 MB    | 517.10 KB   | 76.18%            | -             |\n| 0.6     | 2.12 MB    | 694.99 KB   | 67.99%            | Recommend     |\n| 0.8     | 2.12 MB    | 1.14 MB     | 46.41%            | Recommend     |\n| 1       | 2.12 MB    | 2.12 MB     | 0%                | Not recommend |\n| NaN     | 2.12 MB    | 2.01 MB     | 5.02%             | -             |\n\n### mimeType\n\n- Type: `string`\n- Default: `'auto'`\n- Options: `\"auto\"`, `\"image/png\"`, `\"image/jpeg\"`, and `\"image/webp\"`.\n\nThe mime type of the output image. By default, the original mime type of the source image file will be used.\n\n> **Note:** Safari does not support `mimeType` conversion to `\"image/webp\"`. For more details, see the [browser compatibility of the `HTMLCanvasElement.toBlob()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#browser_compatibility).\n\n### convertTypes\n\n- Type: `Array` or `string` (multiple types should be separated by commas)\n- Default: `[\"image/png\"]`\n- Examples:\n  - `[\"image/png\", \"image/webp\"]`\n  - `\"image/png,image/webp\"`\n\nFiles whose file type is included in this list, and whose file size exceeds the `convertSize` value will be converted to JPEGs.\n\n> For image file type support, see the [Image file type and format guide](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types).\n\n### convertSize\n\n- Type: `number`\n- Default: `5000000` (5 MB)\n\nFiles whose file type is included in the `convertTypes` list, and whose file size exceeds this value will be converted to JPEGs. To disable this, just set the value to `Infinity`.\n\n**Examples**:\n\n| convertSize | Input size (type) | Output size (type) | Compression ratio |\n| ----------- | ----------------- | ------------------ | ----------------- |\n| 5 MB        | 1.87 MB (PNG)     | 1.87 MB (PNG)      | 0%                |\n| 5 MB        | 5.66 MB (PNG)     | 450.24 KB (JPEG)   | 92.23%            |\n| 5 MB        | 9.74 MB (PNG)     | 883.89 KB (JPEG)   | 91.14%            |\n\n### beforeDraw(context, canvas)\n\n- Type: `Function`\n- Default: `null`\n- Parameters:\n  - `context`: The 2d rendering context of the canvas.\n  - `canvas`: The canvas for compression.\n\nThe hook function to execute before drawing the image into the canvas for compression.\n\n```js\nnew PixSqueeze(file, {\n  beforeDraw(context, canvas) {\n    context.fillStyle = \"#fff\";\n    context.fillRect(0, 0, canvas.width, canvas.height);\n    context.filter = \"grayscale(100%)\";\n  },\n});\n```\n\n### drew(context, canvas)\n\n- Type: `Function`\n- Default: `null`\n- Parameters:\n  - `context`: The 2d rendering context of the canvas.\n  - `canvas`: The canvas for compression.\n\nThe hook function to execute after drawing the image into the canvas for compression.\n\n```js\nnew PixSqueeze(file, {\n  drew(context, canvas) {\n    context.fillStyle = \"#fff\";\n    context.font = \"2rem serif\";\n    context.fillText(\"watermark\", 20, canvas.height - 20);\n  },\n});\n```\n\n### success(result)\n\n- Type: `Function`\n- Default: `null`\n- Parameters:\n  - `result`: The compressed image (a `File` (**read only**) or `Blob` object).\n\nThe hook function to execute when successful to compress the image.\n\n### error(err)\n\n- Type: `Function`\n- Default: `null`\n- Parameters:\n  - `err`: The compression error (an `Error` object).\n\nThe hook function executes when fails to compress the image.\n\n[⬆ back to top](#table-of-contents)\n\n## Methods\n\n### abort()\n\nAbort the compression process.\n\n```js\nconst compressor = new PixSqueeze(file);\n\n// Do something...\ncompressor.abort();\n```\n\n## No conflict\n\nIf you have to use another compressor with the same namespace, just call the `PixSqueeze.noConflict` static method to revert to it.\n\n```html\n<script src=\"other-compressor.js\"></script>\n<script src=\"pixsqueeze.js\"></script>\n<script>\n  PixSqueeze.noConflict();\n  // Code that uses other `PixSqueeze` can follow here.\n</script>\n```\n\n## Browser support\n\n- Chrome (latest)\n- Firefox (latest)\n- Safari (latest)\n- Opera (latest)\n- Edge (latest)\n\n## Contributing\n\nPlease read through our [contributing guidelines](.github/CONTRIBUTING.md).\n\n## Versioning\n\nMaintained under the [Semantic Versioning guidelines](https://semver.org/).\n\n## License\n\n[MIT](https://opensource.org/licenses/MIT) © Eduardo Silva Navarrete\n\n---\n\nBuilt by [Alosha](https://alosha.dev). PixSqueeze is part of the Alosha open-source portfolio — see the hosted offering at [pixsqueeze.alosha.dev](https://pixsqueeze.alosha.dev).\n\n[⬆ back to top](#table-of-contents)\n","readmeFilename":"README.md"}