Image Tools

Image Format Converter

Convert images between JPEG, PNG, WebP and other formats.

Conversion settings
90%
Choose image

What is Image Format Converter?

Image format converter transforms images between different formats (JPEG, PNG, WebP, GIF).

Supported Formats

  • JPEG: Photos, lossy compression
  • PNG: Transparency support, lossless
  • WebP: Modern format, best compression
  • GIF: Animations, limited colors

Use Cases

  • Format compatibility
  • Transparency handling
  • Web optimization

A browser-side re-encoder that turns one image into JPEG, PNG or WebP by decoding it into a canvas and handing the pixels to toBlob. Pixel dimensions stay identical and no upload happens, which suits quick format swaps, screenshots destined for a WebP-only page, or a PNG you need as a photograph.

The pipeline is decode, repaint, re-encode

Choosing a file fires FileReader.readAsDataURL, so the bytes first become a base64 data URL before an Image element decodes them. Only then does painting start: a canvas sized from img.width and img.height receives the bitmap, and canvas.toBlob passes those pixels to the engine encoder for the MIME type you selected. Nothing is repackaged — the source bitstream is thrown away and rebuilt from decoded samples. That explains three symptoms: EXIF, GPS, XMP and ICC blocks never reach the output; an HEIC or AVIF file cannot be read at all on engines without a decoder; and the same settings yield different bytes in Chrome and Safari, because the JPEG and WebP encoders behind toBlob are separate implementations.

What the quality slider does and does not touch

The slider spans 0.1 to 1.0 in ten steps, defaults to 0.9, and is passed as the second argument of toBlob only when the target is JPEG or WebP. For PNG the argument is omitted entirely, so the number on screen is inert and 10% produces the same file as 100%. PNG out of a canvas is always 8-bit RGBA with a plain zlib stream and no palette quantisation or adaptive filtering, which is why a PNG round trip often grows: the original may use indexed colour or better row filters than the encoder emits. Quality is also encoder-defined rather than portable — Chromium maps the 0 to 1 float onto libjpeg’s integer 0 to 100 scale, so 0.9 is a sane photographic default while 0.5 already shows blocking around text edges.

Where alpha, depth and gamut go

A canvas backing store is 8 bits per channel sRGB, and every pixel starts at rgba(0,0,0,0). Areas the image never covers keep that value; JPEG has no alpha plane, so the encoder discards the alpha channel and those pixels are written as black — a transparent logo lands on black, not white, because no fill is applied before drawing. The same flattening hits precision and colour: a 16-bit-per-channel PNG loses its extra bits, and Display P3 or Adobe RGB pixels are converted to sRGB during drawImage, shifting saturated greens and reds by a visible amount. Animated GIF contributes its first frame only, and SVG is rasterised once at its intrinsic size.

Worked example

Converting an orientation-tagged phone photo to JPEG

Input
PXL_20240611.jpg, stored pixels 4032×3024, EXIF Orientation = 6 (rotate 90° clockwise), target JPEG at 90%
Output
converted.jpg, image/jpeg, 3024×4032 portrait, no EXIF / APP1 segment

The browser applies orientation while decoding, so img.width and img.height already report 3024 and 4032 and the canvas is created upright. The tag itself is not copied forward. Opening converted.jpg in any EXIF viewer shows dimensions and JPEG structure only: camera model, GPS coordinates and the Orientation entry are gone.

Limits and pitfalls

  • When the engine cannot encode the requested type, toBlob silently falls back to image/png and the component never inspects blob.type: a browser without a WebP encoder still saves a file named converted.webp that is really PNG bytes.
  • Output names are fixed at converted.jpg, converted.png or converted.webp. The original filename is discarded and a selection holds one file, so there is no batch path.
  • Each pass is a generation: JPEG to JPEG at 90% re-quantises 8×8 DCT coefficients, so quality decays monotonically. Keep a lossless master and derive every format from it.
  • No byte or pixel cap exists in the component; the ceiling is memory. The canvas needs width × height × 4 bytes and the data URL adds a base64 copy near 1.33× the file, so a 48-megapixel phone original can exhaust a mobile tab.
  • Any file whose type begins with image/ is accepted, including SVG and animated GIF, but only a single rasterised frame comes back — no animation and no vector data survives.

Related tools

When it goes wrong

I choose a file, the original preview shows up on the left, but the converted panel stays blank and there is no download button to click.

CausehandleFileSelect calls convertImage() on the very next line after reader.readAsDataURL, and reader.onload has not fired yet, so originalImageUrl is still an empty string. The guard if (!originalFile.value || !originalImageUrl.value) return; bails out and nothing is encoded. No watcher retries once the data URL does arrive.

FixNudge the quality slider or change the format dropdown once. Either one fires convertImage with the decode already finished, and the converted panel plus the download button appear.

The second panel sits on the spinner forever and the download button never leaves the disabled state.

Causecanvas.toBlob hands back null when the canvas cannot be encoded: its area is past what the engine allows, or the source decoded to zero width or height, as an SVG without intrinsic dimensions does. The null branch throws inside the asynchronous callback, which sits on a different call stack from the surrounding try/catch, so isConverting is never restored to false and the button stays :disabled.

FixReload the page and retry with a smaller raster image; give any SVG explicit width and height attributes, or rasterise it at a real pixel size before loading it here.

After dragging the quality slider down from 100% to 10% the tab feels heavier, and it does not recover once I stop dragging.

Cause@input on the range fires at every 0.1 step, so each step runs a full toBlob and URL.createObjectURL. The only revokeObjectURL call lives in reset(), and no element in the template is bound to it, so a 100% to 10% sweep leaves nine encoded blobs of the full pixel count alive.

FixSettle the format first, then move the slider in one go, and reload the page before the next file — that reload is the only path that releases the outstanding blob URLs.

I want proof the output got smaller, but neither panel shows a byte count, and the two previews look exactly alike.

CauseOnly originalFile.type is rendered, as a badge on the source panel; blob.size is never read. Both previews also run through max-width: 100% and max-height: 400px, so a 3000 px image is scaled down to fit and every quality step looks the same on screen.

FixDownload the file and read its size in the OS, or drop the source and the result into the image-info tool side by side. Judge quality by opening the downloaded file at 100% zoom, not in the preview.

I corrected a setting and re-picked the same file to redo it, and nothing at all happened — the old result stays on screen.

CauseA file input only fires change when its value actually changes, and this component never clears input.value, so choosing the identical file again emits no event and no re-read — the preview and result on screen are still the previous pass.

FixPick a different image and then the original again, or reload the page. Moving the slider after the re-select also re-encodes, because the previous file and its data URL are still held in memory.

One concrete scenario

Hunting the smallest WebP by dragging the slider, ten encodes at a time

A support agent has one 2880 × 1800 screenshot, 5,184,000 pixels, and wants the smallest WebP that still reads clearly. The slider runs 0.1 to 1.0 in 0.1 steps and every step fires an encode: dragging from 100% down to 10% fires nine encodes, 0.9 through 0.1, over all 5,184,000 pixels each time, and since only reset() would revoke the previous URL — and no button calls it — nine finished blobs sit in memory at once, each the size that quality level produced. Dimensions never move, so the encoder setting is the only variable. Drag once, download the winner, reload; when the 5,184,000 pixels themselves are the problem, that is image-resize.

Frequently Asked Questions

Why did the background turn white (or black) after PNG → JPEG?
JPEG has no alpha channel, so transparent pixels must be filled with a concrete color — the tool defaults to white with other options available. To keep transparency, convert to WebP, which supports alpha alongside lossy compression.
Do repeated conversions degrade quality?
Every lossy transcode (JPEG/WebP) accumulates loss. Keep a lossless master (PNG/TIFF) and derive all versions from it — avoid the JPEG-of-a-JPEG generational smear.
Can it convert HEIC (iPhone photos)?
It depends on the browser’s decoder: Safari supports HEIC natively; Chromium-based browsers often do not. If conversion fails, export JPEG from the system first, or set the iPhone camera to "Most Compatible" before shooting.

Content reviewed: