Compress scans before they upload.
Compress JPEGs, PNGs, single- or multi-page TIFFs, and PDFs entirely on the user's device, WebAssembly in the browser, no server round-trip. DPI, page layout, and filters under your control. Output a single PDF, a multi-page TIFF, or one JPEG per page.
Compress an image right here
Drop an image (or a PDF / TIFF) and compress it entirely in this browser tab, nothing is uploaded. It outputs a PDF by default, open Advanced options to change the layout, DPI or output format. This is the SDK itself running on your device.
Advanced options
Unlicensed demo, outputs carry a watermark band. Ask for an evaluation blob to remove it.
Client-side PDF compression in JavaScript
Hand the SDK an array of file buffers. It compresses every page against your settings and hands back the bytes, all without a network call.
- ✓ JPEG, PNG, TIFF (single or multi-page) and PDF accepted as input, plus other common image formats
- ✓ Multi-page inputs fan out and compress per page
- ✓ Output a JPEG per page, a single PDF, or a multi-page TIFF
- ✓ DPI, page layout (A0–A6), and colour filter per job
- ✓ Every method resolves
{STATUS, DESCRIPTION, DATA}, never throws - ✓ Runs offline once the page has loaded
const helper = new ImageWizHelper(); await helper.ready; await helper.initialize(LICENSE_BLOB); // tune the job await helper.SetDPI(200); await helper.SetPageLayout(4); // 0..6 → A0..A6 await helper.SetFilter(0); // 0 Normal · 200 B&W · 201 Grayscale // read the picked files straight into memory const buffers = await Promise.all( [...input.files].map(async f => new Uint8Array(await f.arrayBuffer())) ); const res = await helper.compressToPdf(buffers); if (res.STATUS) { // res.DATA is a Uint8Array, one PDF of every page save('out.pdf', res.DATA, 'application/pdf'); } else { console.error(res.DESCRIPTION); }
The upload shrinks before it starts
A phone camera photographs a form at several megabytes a page. Compressing server-side means you pay to receive all of it first. Compressing on the device means you never do.
Less upload traffic
Only the compressed bytes cross the network. The raw original never leaves the device, so you are not billed for ingress on data you were going to discard anyway.
No compression tier to run
There is no queue, no worker fleet, and no autoscaling group doing image work. The compute runs on hardware your user already owns and you do not pay for.
Documents stay put
Nothing is transmitted to be processed, so there is no third-party processing hop to describe in a DPA and no retention question to answer.
No round-trip latency
The result is in memory the moment the call resolves. You can render a preview before the user has decided whether to submit at all.
Works offline
Once the page has loaded there is no network dependency. Field staff can capture and compress with no connectivity and sync the finished file later.
Nothing to install
It is a script on your page. No native runtime, no browser extension, no per-desktop rollout, which is usually the reason a scanning step never shipped.
Four calls, start to finish
ready, that resolves once the WebAssembly module is live.initialize(). It is non-fatal: an empty or rejected blob still runs, watermarked.compressToPdf, compressToTiff, or compressToJPG with your buffers and read DATA.Defaults, if you set nothing
The SDK starts at 200 DPI, A4 page layout, aspect-fit scaling, and EXIF rotation on, and outputs a PDF. ResetToDefault() returns to exactly that state, it and the EXIF rotation toggles are exposed only by the main-thread build.
Every knob is explicit
No heuristics deciding your output for you. Three setters cover the common cases; a fourth handles anything they don't.
Resolution, SetDPI
Set any DPI you like, 100, 150, 200, 300, 500, 600 are the usual stops. Dropping a 600 DPI phone capture to 200 discards resolution a printed form was never going to use, which is often the single biggest size win.
Page layout, SetPageLayout
Pass 0 to 6 for A0 through A6, 4 is A4. Anything outside that range falls back to A4 rather than failing, so a bad value degrades quietly instead of breaking the job.
Filters, SetFilter
Applied per page during the compress call, so the filter and the compression are one pass:
- 0, Normal. Leave the colour as captured.
- 200, Black & White.
SetFilter(200)thencompressToTiff()gives a B&W TIFF. - 201, Grayscale. Drops colour, keeps tone.
Custom page layout, SetCustomPageLayout(layout, width, height, colorInt)
When A0–A6 isn't the shape you need, a fixed-pixel thumbnail, a bespoke form size, a letter-size target. Pass 11 to stretch the page to your dimensions or 12 to fit it inside them preserving aspect ratio, then width and height in pixels, then a background colour as 0xRRGGBB00 for the letterboxing that aspect-fit leaves behind.
One input array, three shapes out
Each method takes the same array of buffers. What differs is what comes back in DATA.
compressToPdf
Returns one Uint8Array, a single PDF containing every page of every input, in order. The usual choice when the file is going to a human or an archive.
compressToTiff
Returns one Uint8Array, a multi-page TIFF of every input. Pair it with SetFilter(200) when a downstream system expects bitonal scans.
compressToJPG
Returns an array of Uint8Array, one JPEG per page, not one file. This is the method that behaves differently from the other two; index it, don't blob it.
Per-page progress with compressToJPGStream
Same JPEG-per-page output, delivered as each page finishes rather than all at the end, you get index, total, name, fileData, and progress per page. Useful for rendering thumbnails into a grid while a fifty-page batch is still running. The main-thread build exposes it as an event; the worker build takes a callback.
Main thread or worker pool
Two builds, the same method names and the same response contract. Pick one per page.
Instant, main thread
Synchronous WebAssembly on the main thread. The simplest thing that works, and the right default for a one-document-at-a-time tool.
- ✓ Fewest moving parts
- ✓ Exclusive extras: EXIF rotation toggles,
ResetToDefault(), page count straight from aFile - ✓ Input buffers stay usable after the call
- , Blocks the UI on a large job
Background, worker pool
A dispatcher spreads work across a pool of workers. Config setters broadcast to every worker handle, so the pool stays consistent.
- ✓ UI stays responsive under load
- ✓ Fan a batch out in parallel with
Promise.all - ✓ Same methods, same
{STATUS, DESCRIPTION, DATA} - , Input
ArrayBuffers are transferred, and detach on your side
The transfer gotcha, stated plainly
The worker build transfers your input buffers into the worker for speed. A transferred buffer is detached on the caller's side afterwards, if you need those bytes again (to show the original next to the result, say), re-read the file rather than reusing the array.
In the browser, or on your server?
These are different tools for different moments. The honest test is simple: where is the document right now, and who owns the CPU?
Use the Browser JS SDK when…
- ✓ The document is being captured or picked by a user, right now, in a browser.
- ✓ You want the upload to be small, the point is to not send the original.
- ✓ The documents should not leave the device at all.
- ✓ The workflow has to survive patchy or absent connectivity.
- ✓ You'd rather not operate an image-processing tier for a step this small.
Use the Compression SDK when…
- ✓ The files are already on your servers. Sending them to a browser to compress would be absurd.
- ✓ You're compressing a back-file, millions of documents, unattended, overnight.
- ✓ Documents arrive by email, scanner, or SFTP, with no browser in the path.
- ✓ You need throughput on hardware you control and can predict.
- ✓ It's a desktop or backend application, not a web page.
They're frequently both
Compressing at capture keeps the upload small and the user waiting less. Compressing on the server catches everything that arrived through some other door, and re-compresses archives on a schedule. Running both is a normal architecture, not a redundancy.
The rest of the toolkit
One script, one handle. Compression is one of the things it does on-device.
Conversion
Images to PDF, TIFF to PDF, or a multi-page TIFF out as one JPEG per page.
Read more →Page organizer
Explode a PDF or TIFF into pages, reorder or drop them, and repack, client-side.
Read more →OCR cleanup
Bitonal clean-up that produces the 1-bpp TIFF an OCR engine expects.
Read more →Viewer
Render multi-page TIFF and PDF pages to images on-device, with no server renderer.
Read more →Questions worth answering first
compressToPdf and compressToTiff take an array of buffers and return one file containing every page of every input; compressToJPG takes the same array and returns one JPEG per page.ResetToDefault() restores exactly that state; it and the EXIF rotation toggle are exposed only by the main-thread build.initialize() is non-fatal: with a missing, invalid, or demo license the SDK still initializes and every operation still succeeds, the output carries a watermark band. That means you can wire up and verify the whole flow before licensing anything. A false from initialize() means the process handle could not be created, not merely that the license was rejected.{STATUS, DESCRIPTION, DATA}, and failures never throw across the API. Check STATUS; on false, read DESCRIPTION for the message. DATA is present only on success.SetFilter(200) for black & white, SetFilter(201) for grayscale, SetFilter(0) to leave colour alone. The filter is applied per page during the compress call, so SetFilter(200) followed by compressToTiff() yields a black-and-white TIFF in one pass.SetCustomPageLayout(layout, width, height, colorInt), 11 to stretch or 12 to aspect-fit, width and height in pixels, and a background colour as 0xRRGGBB00 to fill what aspect-fit leaves over. If you pass SetPageLayout a value outside 0..6, it falls back to A4 rather than erroring.readImageInformation(buffer) returns file size in KB, total page count, and per page the number, width, height, colour type (BW, GREYSCALE, or COLOR) and DPI. It's the natural way to decide settings per document, or to show a before-and-after in your UI.Licensing questions, volume terms, or an evaluation blob, talk to us.