Image Tools

Image Resizer

Resize image dimensions, format, and output quality locally in the browser.

Image resizer
Processed locally with Canvas; the image is never uploaded.

Image Resizing

Resize pixel dimensions with browser Canvas and export WebP, JPEG, or PNG. Files stay in the current browser tab and are not uploaded.

Output Rules

  • Lock the original aspect ratio or set both dimensions independently
  • JPEG cannot preserve transparency, so transparent pixels are filled white
  • Quality applies to JPEG and WebP; PNG is encoded losslessly by the browser

Resamples a picture onto a canvas of exactly the width and height you type, then encodes the result as WebP, JPEG or PNG at a quality you choose. Neither axis may exceed 12,000 px and the area stops at 40 million pixels. It suits forcing a photo or screenshot into a fixed layout slot or upload limit.

What one drawImage call does to the pixels

The file is decoded into an <img>, a canvas is sized to the two numbers in the fields, and drawImage runs once with the whole source as the source rectangle. With imageSmoothingEnabled true and imageSmoothingQuality set to high, the engine picks a wide filter kernel: shrinking sums every source pixel that falls under a target pixel, which is why 4000 px to 1000 px also averages away sensor noise. Enlarging does the reverse — it evaluates the kernel between existing samples, so 300 px to 900 px only spreads information that was already there. A single reduction beyond roughly four times can drop hairlines and one-pixel borders between sample positions, and stepping down by halves twice often keeps them.

Where 12,000 px and 40 million pixels come from

Those two limits guard the canvas backing store, not the file format. A canvas allocates four 8-bit channels per pixel, so 40,000,000 pixels reserve roughly 153 MiB before the encoder allocates anything, and some engines refuse large bitmaps outright: older iOS Safari caps canvas area at 16,777,216 pixels (4096 squared) and hands back a blank canvas instead of an error. Because 12000 by 12000 would be 144 million pixels, the area rule normally fires first and the per-side rule mainly bounds thin panoramas. Both target fields must be integers from 1 to 12,000 and their product at most 40,000,000 — 8000 by 5000 passes, 8000 by 5001 is rejected — and this validation runs before any canvas exists.

White fill, quality slider, and the filename rule

Encoding goes through canvas.toBlob(type, quality). PNG ignores the quality argument, so the slider is disabled whenever PNG is selected; JPEG and WebP feed the 0.1 to 1.0 value straight into their quantisers, with 0.86 as the default. If JPEG is chosen the code paints #ffffff over the canvas before drawing, because JPEG has no alpha channel and an unpainted canvas would composite the transparent regions onto black instead. The download name is the original stem, a hyphen, the new dimensions and the encoder extension, with image/jpeg rewritten to jpg: resizing photo.png to 1000 by 750 as WebP yields photo-1000x750.webp.

Worked example

Squeezing a 12-megapixel phone photo into a header slot

Input
photo.jpg, 4000 by 3000, lock aspect ratio on, target width 1000, format WebP, quality 0.86
Output
1000 by 750 WebP, 750,000 pixels, saved as photo-1000x750.webp

The height is derived as Math.round(1000 * 3000 / 4000) = 750, so the 4:3 ratio survives and the result holds 750,000 pixels, exactly one sixteenth of the original 12,000,000. Nothing else is derived: the byte count depends on how much detail survives the resample and on where the WebP quantiser lands at 0.86.

Limits and pitfalls

  • Both dimensions must be integers from 1 to 12,000, and their product may not exceed 40,000,000 pixels, so 8000 by 5000 is accepted while 8000 by 5001 is refused before encoding starts.
  • The quality slider is dead for PNG because the browser encodes it losslessly; exporting a photo as PNG here usually returns a file several times larger than the JPEG that went in.
  • Nothing prevents upscaling: a 300 px thumbnail set to 1200 px is interpolated, not sharpened, and lands soft no matter which format you pick.
  • With the ratio locked the second side is rounded and floored at 1 px, so a 4000 by 30 strip resized to width 40 comes out 40 by 1 rather than collapsing to zero height.
  • Animated GIF, APNG and animated WebP are drawn as their first frame only, and the output carries no EXIF block, so keep the camera original as the master copy.

Related tools

When it goes wrong

The download is called photo-1600x1200.webp, but opening it shows an 800 × 600 picture.

CauseThe filename is built inside download() from the two live number fields, not from the blob: ${base}-${targetWidth}x${targetHeight}.${extension}. Both fields stay editable after encoding, so any change made between "Create resized image" and "Download image" is written into the name while the blob keeps the size it was actually encoded at. The caption beside the Download button reads the same two fields, so it agrees with the wrong name.

FixDecide both numbers first, click Create, then download without touching the fields. If you already changed them, click Create again — the blob is re-encoded at the new size and the name becomes true. To check an existing file, open it rather than trusting the filename.

Aspect ratio is locked, I type 640 into Target height, and Target width snaps back to the flat 3:2 number the photo came in at.

CauseupdateWidth() and updateHeight() recompute the opposite side from sourceWidth and sourceHeight — the size read off the decoded image — never from the pair you just typed. Locked therefore means "one number, original proportions", not "keep the pair I set". They also listen to change, not input, so nothing moves until the field loses focus.

FixUntick Lock aspect ratio when the slot is a different ratio from the source and type both numbers. Keep it ticked only when you want the source proportion, and blur the field before judging the result — mid-typing the value shown is still yours.

Target width was cleared and refilled with 1200, yet the panel keeps saying width and height must be integers from 1 to 12,000.

Causev-model.number hands an emptied field over as an empty string, and Number.isInteger('') is false, so the guard fires on a box that looks fine to the eye. type="number" also accepts a decimal such as 1200.5: the browser sees nothing wrong, and the integer test here does.

FixRetype a plain integer in each field and press Create. If the message persists, one of the two fields still holds a decimal or a blank: click into both, confirm the digits, and retry.

I typed 20000 into Target width and got an error — the field advertises max 12000, and nothing stopped the keystroke.

Causemin="1" and max="12000" only drive the spinner and the browser hint; nothing in the component clamps what you type. Both checks run inside resizeImage() before a canvas exists: one rejects a side outside 1–12,000, a second rejects an area above 40,000,000 pixels, so a wide pair whose sides each look legal can still be turned away at the second gate.

FixKeep both sides inside 1–12,000 and the product under 40,000,000 pixels. With the ratio locked, one oversized width also drags the derived height out of range, so fixing the width alone rarely helps.

I dropped a PDF by mistake, the banner said select an image file, and the old photo is still on screen — pressing Create exports that old photo again.

CauseThe image/ prefix test is the first statement of selectFile() and returns before the block that revokes the old URLs and overwrites sourceFile. Nothing about the previous file is cleared, so the error banner and the still-loaded earlier image sit side by side.

FixPick the intended image again; the rejected drop changed nothing, so the previous file and its dimensions are exactly what will be encoded. Treat the banner as a note about the drop, not as evidence that the loaded file was replaced.

I uploaded a PNG, changed nothing, pressed Create, and the download is logo-800x600.webp — same pixels as the file I put in.

Causeformat starts as image/webp, and both target fields are pre-filled with naturalWidth / naturalHeight once the image decodes, so the first click is a plain WebP re-encode at 0.86 rather than a resize. There is no "keep original format" choice in the dropdown.

FixPick PNG in Output format if the suffix and losslessness matter (the quality slider greys out for PNG), and type real dimensions if you wanted fewer pixels. If the only goal was a smaller file, image-compress has the cap-based path this tool lacks.

One concrete scenario

A 16:9 hero slot fed by 3:2 photos

Three docs pages want a 640 × 360 hero, and every source photo is 3000 × 2000, which is 6,000,000 pixels. With the ratio locked, typing 640 into Target width derives Math.round(640 × 2000 / 3000) = 427, so the export is 640 × 427: 273,280 pixels, 4.6% of the source, and 67 px taller than the slot. Unlocking and typing 360 does reach 640 × 360, or 230,400 pixels, but drawImage stretches the whole frame into the new box and never crops, so every subject loses 15.7% of its height. Export 640 × 427 as WebP at 0.86, or get a genuine 16:9 crop; forcing 640 × 360 here is a squeeze, not a fit.

Frequently Asked Questions

Resize first or compress first?
Resize first: a 4000px original scaled to 800px at quality 85 ends up small and sharp; compress-then-resize magnifies artifacts. Export web images at twice the display size for Retina.
Lock the aspect ratio or stretch freely?
Keep the lock to avoid distortion; free stretching is for the rare "intentionally distort to fit" case. To change the aspect ratio, crop — do it in a crop tool first, then resize here; that beats hard stretching every time.
Can upscaling make a small image sharp?
No algorithm invents detail: plain interpolation blurs, that is the information ceiling. Mild upscales (<150%) with modest sharpening pass; large factors need dedicated AI super-resolution. This tool shows the honest interpolated result.

Content reviewed: