Image Tools

Image Compressor

Compress image file size with quality and size limits.

Compression settings
80%
Choose image

What is Image Compressor?

Image compressor reduces image file size while maintaining visual quality, optimizing images for web use.

Features

  • Adjustable quality level
  • Multiple format support (JPEG, PNG, WebP)
  • Batch processing
  • Preview before download

Use Cases

  • Website optimization
  • Email attachment size reduction
  • Storage space saving

Re-encodes a bitmap in the browser through Canvas: pixels are redrawn into a box capped at a maximum width and height, then written back out as JPEG, PNG or WebP at a chosen quality. Suited to shrinking photos and screenshots before upload, where a smaller dimension and a lossy re-encode, not a better codec, do the size reduction.

How the maximum-size box is applied

The component never upscales. It reads the decoded dimensions, tests whether either axis exceeds its cap, and only then multiplies both axes by Math.min(maxWidth / width, maxHeight / height), rounding each with Math.round. One ratio serves both axes, so the aspect ratio survives to within a pixel, and the axis whose cap is proportionally tighter lands exactly on its limit while the other stays short. Defaults are 1920 by 1080, so a 4032 by 3024 phone photo is bound by height and comes out 1440 by 1080 rather than 1920 by 1440. Both fields accept 100 to 5000 in steps of 100, and every slider or field change re-runs the whole pipeline after a 300 ms debounce.

Why the quality slider does nothing for PNG

canvas.toBlob() accepts a quality argument that the specification lets browsers ignore, and every mainstream engine ignores it for image/png because the format is lossless. The component therefore passes undefined whenever the output is PNG, either by explicit choice or because Original mode inherits a PNG source. A PNG export shrinks only through dimension reduction; if the caps never trigger, the re-encode can return a file larger than the original, typically when the source was a palette PNG or had already been run through an optimizer. When the blob does exceed the source, two silent retries follow: PNG gives way to JPEG at 0.7, then quality drops 0.2 per attempt down to a floor of 0.3.

What the re-encode removes from the file

The path is FileReader to Image to canvas to toBlob, so the downloaded bytes are freshly encoded pixels, not the original container with adjustments. Three things are lost without any warning in the preview. The EXIF block disappears, taking camera model, capture time and GPS coordinates with it. The Orientation flag disappears as well, but invisibly, because the browser already rotated the pixels while decoding. ICC profiles are dropped and colours are converted into the canvas working space, which visibly shifts untagged wide-gamut exports. Animated GIF, APNG and animated WebP collapse to their first frame, and a JPEG source absorbs a second lossy generation on top of its first.

Worked example

A 12-megapixel phone photo under the default box

Input
4032 x 3024 JPEG, max width 1920, max height 1080, quality 80%, format Original
Output
1440 x 1080 JPEG with the source aspect ratio kept

Both axes scale by one ratio: Math.min(1920 / 4032, 1080 / 3024) is Math.min(0.4762, 0.3571) = 0.3571, so the width becomes 1440 and the height exactly 1080. Height is the binding constraint, which is why the result is not 1920 wide. The resulting byte count cannot be derived from these numbers, since it depends on how much high-frequency detail the photo holds.

Limits and pitfalls

  • Quality is a 0.1 to 1.0 slider in steps of 0.1 (default 0.8) and only affects lossy output; selecting PNG makes it inert, leaving maximum width and height as the only levers.
  • Width and height caps are integers from 100 to 5000 px in steps of 100, applied as a contain fit: 4032 x 3024 under 1920 x 1080 gives 1440 x 1080, and no setting can enlarge a smaller source.
  • If the encoded blob is larger than the source, the tool retries twice on its own (PNG to JPEG at 0.7, then quality minus 0.2 down to 0.3), so the downloaded file may not match the quality the slider shows.
  • Photos under 100 KB whose dimensions were not reduced get an extra nudge: JPEG above 0.7 or WebP above 0.75 loses 0.1, floored at 0.5, which surprises anyone comparing the export against a nominal 0.85 setting.
  • Every export is a re-encode, so EXIF including GPS, the ICC profile and all animation frames are stripped; keep the camera original as the master before compressing.

Related tools

When it goes wrong

The download is named compressed-….png, but the upload form answers "not a valid PNG" and an inspector reports image/jpeg.

CauseOnly the too-large rescue path changes the format. When the blob comes back bigger than the source, a PNG export is retried as image/jpeg at quality 0.7, while downloadCompressed() still derives the extension from the format select (png) or from the source filename — so the .png suffix outlives the JPEG bytes.

FixIf a PNG export came back larger, set the format dropdown to JPEG and download again so the suffix follows the real MIME. For a file already saved, read the first bytes: FF D8 FF is JPEG, 89 50 4E 47 is PNG, and rename to match.

A transparent logo comes back sitting on a white rectangle, or on a black one after switching the output to WebP.

CauseThe 2D context gets alpha: true only for PNG output — explicit PNG, or Original over a PNG source. JPEG output also paints #FFFFFF across the full canvas before drawImage, so alpha composites onto white; WebP output builds the context with alpha: false and paints no background, and an opaque canvas starts out black.

FixKeep the export on PNG, which is the one branch with an alpha channel. Anything whose transparency must survive should not go through this pipeline at all — apart from the width and height caps there is no switch for it.

An iPhone HEIC photo is accepted, then the toast reads 图片加载失败 and no preview ever appears — even in the English UI, where that string stays Chinese.

CauseThe only type gate asks whether the MIME string begins with image/, which image/heic passes. Decoding is then handed to new Image(), and Chrome and Firefox ship no HEIC decoder, so onerror fires before a canvas exists. TIFF and camera RAW fail identically.

FixConvert to JPEG first: on iOS set Settings → Camera → Formats → Most Compatible, or export JPEG from Preview or Photos, then drop that file in.

Maximum width was edited to 1200, yet the export is still 4032 px wide — and emptying the field changes nothing either.

CauseNothing in the component clamps these numbers; min=100 and max=5000 only bind the spinner and form validation. An emptied field arrives as an empty string that coerces to 0 in the width > maxWidth test, so the resize branch is skipped, while a literal 0 gives a ratio of 0, a 0 × 0 canvas, a null blob from toBlob, and a throw raised inside the asynchronous callback that the enclosing try/catch never sees.

FixType an integer between 100 and 5000 and verify the pixel size on the compressed panel before downloading. If the spinner is already stuck, re-select the file or reload the page to clear the busy flag.

Ten screenshots dropped in one gesture produce exactly one compressed image, with no notice about the other nine.

CausehandleDrop reads files[0], rebuilds a DataTransfer holding that single file and hands it to handleFileSelect. There is no queue, and every new selection revokes the previous blob URL.

FixLoad and download one file at a time, or batch the job outside the browser with a zip plus a local encoder; this tool has no queue mode to enable.

Without touching any control, the second image comes out noticeably smaller or blurrier than the first.

Causereset() clears the file, both byte counts, both dimension pairs and the input value, but leaves quality, maximum width, maximum height and output format exactly as they were — the previous recipe is still armed, including a PNG selection that keeps the quality slider inert.

FixBefore the next file, return quality to 80%, the caps to 1920 × 1080 and the format to Original, or reload the page to restore the shipped defaults.

One concrete scenario

One phone photo for the listing, one logo that should not be here

A shop listing needs the same camera photo twice. The original is 4032 × 3024, which is 12,192,768 pixels. Filling both caps with 1200 multiplies both axes by Math.min(1200 / 4032, 1200 / 3024) = 0.2976, giving 1200 × 900 — 1,080,000 pixels, 8.9% of the source, so 91.1% of the pixel data is gone before the quality slider is touched. Height ends 300 px short of its own cap because a single ratio serves both axes. Exporting that at quality 0.8 is the listing image. The 300 × 200 PNG logo in the same folder never crosses the box, so no dimension shrinks, PNG ignores quality, and the re-encode can come back larger while keeping its alpha — that one belongs to pngquant, not here.

Frequently Asked Questions

Will compression visibly hurt quality?
It depends on the quality setting: JPEG/WebP at 80–85 is usually indistinguishable at half the size; below 70 block artifacts appear. Screenshots and text-heavy images are the most artifact-sensitive — use high quality or lossless there.
WebP or JPEG?
WebP is 25–35% smaller at equal quality and supported by all modern browsers — the default for web images; JPEG is the compatibility fallback (old systems, some in-app browsers). The tool encodes locally and outputs either.
Are my images uploaded anywhere?
No. Compression uses the browser Canvas locally — files never leave your device. That is the core advantage over online compressors when the material is internal screenshots or contract scans.

Content reviewed: