Convert a PNG to JPEG
Converts a PNG to a baseline JPEG in your browser, at a quality you choose and with transparency painted onto a colour you choose. The PNG decoder and the JPEG encoder both run in this tab, so the same settings give the same file whichever browser you use, and nothing is sent anywhere.
Any PNG, including 16-bit, palette and interlaced ones. It is read in this tab and never sent anywhere.
How it works
What this does
Turns a PNG into a JPEG without it leaving your computer, and asks you the one question that every other converter answers for you badly: what colour should the transparent parts become?
That question exists because JPEG stores three colour channels and no transparency. A PNG with a see-through background still has colour values underneath those pixels, and in a file exported from a design tool they are almost always zero. Drop the alpha channel without doing anything else and the zeros become visible — which is why a logo converted carelessly arrives as a white mark on a black rectangle.
The method
There are two codecs here and neither is the browser’s.
The obvious way to build this is createImageBitmap into an OffscreenCanvas
and out through convertToBlob. It is four lines, and it was rejected. The
HTML specification treats the quality argument as a hint and leaves the mapping
to the implementation, so quality 0.8 is a different file in Chrome, in Firefox
and in Safari — a different size, different artefacts, sometimes different
chroma subsampling. A page whose main readout is “your file is now 827 bytes”
cannot have that number depend on which browser is open. Worse, whether
convertToBlob flattens transparency onto black or onto white is also up to
the engine, and it is not overridable from outside. So the PNG decoder and the
JPEG encoder are both written out, against the specifications listed above,
and the only platform API involved is DecompressionStream for the zlib stream
inside the PNG — the one part of the job that has a single right answer.
The decoder is the ordinary one: check the signature, walk the chunks, inflate the IDAT stream, then undo the row filters. That last step is the reason a PNG cannot be decoded out of order, because each row is predicted from the row above it and the pixel to its left, and Paeth chooses between three neighbours based on which way the gradient runs. Interlaced files arrive as seven passes of increasing resolution and are reassembled into one image before anything else happens.
Then the flattening, which is the part that matters. Each pixel becomes
source × alpha + background × (1 − alpha), computed on the values as stored.
Those values are gamma-encoded sRGB, so this is not the physically correct
blend; done properly, in linear light, a half-transparent mid-grey over white
comes out a few per cent lighter than it does here. It is done this way on
purpose, because every browser canvas and every image editor composites in sRGB
too, and being right in isolation would mean the JPEG no longer matched the PNG
you were just looking at.
The encoder is baseline sequential JPEG. Colours convert to Y, Cb and Cr with the full-range BT.601 coefficients JFIF specifies — the studio-range variant produces a file that looks washed out in every viewer and is invisible in code review. Below quality 90 the two colour planes are averaged down two-to-one in each direction with a box filter rather than point-sampled, because point sampling a red-and-white pattern throws away half the colour and keeps the wrong half. Each 8×8 block goes through the discrete cosine transform, is divided by the Annex K quantisation table scaled for the chosen quality, and is rounded; blocks that overhang the right or bottom edge repeat the last real pixel rather than padding with grey, so the encoder does not spend bits describing an edge that is not in the picture.
Before you read on
You encode the same PNG to JPEG at quality 0.8 through the browser's own canvas, in Chrome, in Firefox and in Safari. What do you get?
Three different files — different sizes, different artefacts, and sometimes different chroma subsampling, because the specification treats the quality argument as a hint rather than a contract. A page whose main readout is your file is now 827 bytes cannot have that number depend on which browser is open. Worse, whether the canvas flattens transparency onto black or onto white is also left to the engine and is not overridable from outside. So the PNG decoder and the JPEG encoder here are both written out against their specifications, and the only platform API involved is DecompressionStream for the zlib stream inside the PNG — the one part of the job with a single right answer.
- Opaque pixels3,07275.0%
- Transparent, filled with the background1,02425.0%
A worked example
A 64 × 64 PNG: a horizontal red-to-blue gradient, with the leftmost sixteen
columns fully transparent and storing zeros underneath, which is what an
exporter writes. Converted onto a #ffffff background:
Quality 40 766 B
Quality 82 827 B
Quality 95 1.2 KB
Dimensions 64 × 64
Transparent pixels 25.0% of pixels, filled with #ffffff
Those are the readouts as the panel prints them, rounded the way a file manager rounds. The encoder writes 766, 827 and 1,271 bytes exactly, and it is the byte counts the test file asserts, so if the encoder ever changes without this page changing with it, the build fails.
The arithmetic underneath is checked against the specification rather than
against another encoder: the quantisation tables at quality 50 must equal the
published Annex K tables exactly, the four Huffman tables must be complete
prefix-free codes whose symbol sets are precisely the ones a coefficient can
need, and no literal 0xFF inside the entropy-coded data may go unstuffed. The
decoder is checked the other way round, against pixels — every colour type and
bit depth the format allows, all five row filters, and an interlaced file
asserted to produce byte-for-byte the same image as the non-interlaced version
of itself.
What it does not do
It does not carry colour management across. An ICC profile, a gamma chunk or chromaticity data in the PNG is dropped, and the JPEG is written as plain sRGB, so a wide-gamut photograph will come out looking flatter than the original.
It writes baseline JPEG only, not progressive, so a large file loads top to bottom rather than sharpening in place. It does not verify the CRC on each PNG chunk, so a corrupt file usually surfaces as a decompression failure rather than as a checksum error. It has no way to hit a target file size — you choose a quality and find out what that costs. It does not resize, crop or rotate, and it ignores any EXIF orientation the PNG may carry. Files are capped at 32 MB and 40 megapixels, because both the decoded image and the three colour planes have to be held in memory at once.
And because it does not resize, a large PNG becomes a large JPEG. To resize a photo as well, do that first: fewer pixels compress better than the same pixels compressed harder.
How it is done
- Read the chosen file in this tab and check the eight-byte PNG signature, then walk the chunk chain for IHDR, PLTE, tRNS, IDAT and pHYs.
- Decompress the concatenated IDAT chunks as one zlib stream, using the browser's own DecompressionStream rather than a bundled inflater.
- Undo the per-row filter — None, Sub, Up, Average or Paeth — working down the image, because each row is predicted from the one above it and the pixel to its left.
- Expand the samples to eight-bit RGBA, whatever the file's colour type and bit depth, reassembling the seven passes first if the PNG is interlaced.
- Composite every pixel onto the chosen background colour using its alpha, so a transparent pixel becomes that colour rather than whatever was underneath it.
- Convert to Y, Cb and Cr with the full-range BT.601 coefficients that JFIF specifies, and halve the resolution of the two colour planes with a box filter if 4:2:0 is in use.
- Take the discrete cosine transform of each 8x8 block, divide by the Annex K quantisation table scaled for the chosen quality, and round.
- Huffman-code the coefficients with the Annex K tables, stuffing a zero byte after every literal 0xFF, and write the JFIF marker segments around the result.
What it assumes
- Transparency is flattened onto the chosen colour before anything else happens, and the blend is done in sRGB rather than in linear light. That is not physically correct — a half-transparent pixel comes out a few per cent darker than it should — but it is what a browser canvas and every image editor do, so the JPEG matches the PNG you were looking at.
- Quality is the scale from Annex K of the JPEG specification, not the browser's. The same number gives byte-for-byte the same file here in every browser, but it will not match "quality 82" in Photoshop or GIMP, because each of those starts from different tables.
- Below quality 90 the two colour channels are stored at half resolution in each direction. On a photograph this is invisible; on saturated text or thin coloured lines it shows as fringing, which is why the setting can be forced to 4:4:4.
- A 16-bit PNG is reduced to eight bits by keeping the high byte of each sample. JPEG is an eight-bit format, so the low byte has nowhere to go.
- Colour profiles are not carried across. An iCCP, gAMA or cHRM chunk in the PNG is ignored and the result is written as plain sRGB, so a wide-gamut image will come out looking flatter than the original.
Common questions
Is my PNG uploaded anywhere?
No. The PNG is read in this browser tab and both the decoding and the encoding happen in a worker on your own machine. There is no server to send it to, and the automated test for this page fails if any request leaves this site while a file is loaded.
Why does my transparent background turn black in other converters?
Because JPEG has no alpha channel, so the transparency has to become some real colour, and most editors leave whatever was stored underneath the transparent pixels. That is usually zero, which is black. The colour values are still in the PNG — the alpha channel was the only thing hiding them. Setting the background here paints over them before anything is encoded.
Why is my JPEG bigger than the PNG was?
Because PNG is very good at exactly what JPEG is bad at. Flat colour, sharp edges, screenshots and line art compress beautifully losslessly and badly with a cosine transform. If the "smaller by" figure comes out negative, the honest answer is that this image should stay a PNG. JPEG wins on photographs, and usually by a lot.
What quality should I use?
Around 80 for anything going on a web page, and 60 if you are trying to clear an attachment limit. Above 90 the file grows quickly for a difference almost nobody can see, and above 95 it grows very quickly indeed. Below about 50 the eight-by-eight blocks start to become visible in smooth areas such as skies.
Will this match "Save as JPEG" at the same quality in Photoshop?
No, and no two encoders do. Quality is not a number defined by the JPEG specification; it is a way of scaling the quantisation tables, and every encoder scales differently and starts from different tables. This one uses the scaling published in Annex K of the specification, which is the closest thing to a common reference, and it gives the same result every time on every browser.
Does it handle 16-bit, palette or interlaced PNGs?
Yes, all of them. The decoder covers every colour type and bit depth the format allows — one, two, four, eight and sixteen bits, greyscale, truecolour, palette, and either of those with an alpha channel — plus Adam7 interlacing and all five row filters. A 16-bit file is reduced to eight bits, because JPEG has nowhere to put the rest.
Sources
- Portable Network Graphics (PNG) Specification (Third Edition)
- Recommendation T.81: Information technology – Digital compression and coding of continuous-tone still images – Requirements and guidelines
- Recommendation T.871: Information technology – Digital compression and coding of continuous-tone still images: JPEG File Interchange Format (JFIF)
- RFC 1950: ZLIB Compressed Data Format Specification version 3.3