PNG and JPG serve very different purposes. PNG is a lossless format built for graphics, logos, and anything with transparency. JPG is a lossy format built for photographs and complex images where a small quality trade-off yields huge file-size savings. Converting from PNG to JPG makes sense in many situations — but there's one critical pitfall you need to understand first.
When to convert PNG to JPG
Good candidates for conversion:
| PNG use case | Convert to JPG? |
|---|---|
| Photograph saved as PNG | Yes — JPG compresses photos far better |
| Screenshot of a webpage | Usually yes — mostly opaque content |
| Marketing banner (no transparency) | Yes — big size reduction |
| Logo with transparent background | No — JPG doesn't support transparency |
| Icon with hard edges | No — JPG creates edge artefacts |
| Image for further editing | No — stay lossless until final export |
The single rule: if the image has transparency, do not convert to JPG without deciding what to replace the transparent areas with first. JPG has no alpha channel. Every transparent pixel must become a solid colour.
The transparency problem
PNG supports an alpha channel — each pixel has an opacity value from 0 (fully transparent) to 255 (fully opaque). JPG simply cannot store this information. When a naive converter encounters a transparent PNG, it typically fills transparent areas with black, which looks terrible on most backgrounds.
The correct approach:
- Create a canvas or image buffer filled with your desired background colour (white is the safest default).
- Composite the PNG on top of that background.
- Export the result as JPG.
This is called alpha compositing or flattening the alpha channel.
File size comparison
Here's what you can realistically expect when converting photos and graphics:
| Image type | PNG size | JPG at 85% quality | Reduction |
|---|---|---|---|
| Photo (1920×1080) | 3.2 MB | 420 KB | ~87% |
| Screenshot (1440×900) | 1.8 MB | 290 KB | ~84% |
| Marketing banner (1200×628) | 950 KB | 140 KB | ~85% |
| Logo (512×512, no transparency) | 85 KB | 28 KB | ~67% |
A quality setting of 80–90 is the sweet spot for most web images — nearly indistinguishable from the original at a fraction of the size.
JavaScript (browser + Node.js)
Browser: Canvas API
function pngToJpg(file, quality = 0.85, bgColour = '#ffffff') {
return new Promise((resolve) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
// Fill background first — handles transparency
ctx.fillStyle = bgColour;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw the PNG on top
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
canvas.toBlob(resolve, 'image/jpeg', quality);
};
img.src = url;
});
}
// Usage
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
const blob = await pngToJpg(e.target.files[0], 0.85);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'converted.jpg';
link.click();
});
Node.js: Sharp
const sharp = require('sharp');
async function pngToJpg(inputPath, outputPath, options = {}) {
const { quality = 85, background = { r: 255, g: 255, b: 255 } } = options;
await sharp(inputPath)
.flatten({ background }) // Replace transparent areas with background colour
.jpeg({ quality, mozjpeg: true }) // mozjpeg: better compression
.toFile(outputPath);
}
// Convert a single file
await pngToJpg('input.png', 'output.jpg');
// Convert with custom background (e.g. light grey)
await pngToJpg('input.png', 'output.jpg', {
quality: 90,
background: { r: 240, g: 240, b: 240 },
});
Python: Pillow
from PIL import Image
import os
def png_to_jpg(input_path: str, output_path: str, quality: int = 85,
background: tuple = (255, 255, 255)) -> None:
"""
Convert PNG to JPG with proper alpha handling.
Args:
input_path: Path to the source PNG file.
output_path: Path for the output JPG file.
quality: JPEG quality 1–95 (85 is a good default).
background: RGB tuple for the background colour under transparency.
"""
img = Image.open(input_path)
# Handle RGBA and P (palette with transparency) modes
if img.mode in ('RGBA', 'LA', 'P'):
bg = Image.new('RGB', img.size, background)
if img.mode == 'P':
img = img.convert('RGBA')
if img.mode in ('RGBA', 'LA'):
bg.paste(img, mask=img.split()[-1]) # Use alpha as mask
img = bg
elif img.mode != 'RGB':
img = img.convert('RGB')
img.save(output_path, 'JPEG', quality=quality, optimize=True)
# Single file
png_to_jpg('photo.png', 'photo.jpg')
# Batch convert all PNGs in a folder
import glob
for png_file in glob.glob('images/*.png'):
jpg_file = os.path.splitext(png_file)[0] + '.jpg'
png_to_jpg(png_file, jpg_file, quality=85)
print(f'Converted: {png_file} → {jpg_file}')
Go: image/jpeg + image/png
package main
import (
"image"
"image/color"
"image/draw"
"image/jpeg"
"image/png"
"os"
)
// PNGToJPG converts a PNG file to JPG with alpha compositing.
func PNGToJPG(inputPath, outputPath string, quality int, bg color.RGBA) error {
// Open and decode the PNG
src, err := os.Open(inputPath)
if err != nil {
return err
}
defer src.Close()
srcImg, err := png.Decode(src)
if err != nil {
return err
}
// Create an opaque RGB canvas filled with the background colour
bounds := srcImg.Bounds()
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, &image.Uniform{bg}, image.Point{}, draw.Src)
// Composite the source image over the background (handles alpha)
draw.Draw(dst, bounds, srcImg, bounds.Min, draw.Over)
// Encode as JPEG
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
return jpeg.Encode(out, dst, &jpeg.Options{Quality: quality})
}
func main() {
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
if err := PNGToJPG("input.png", "output.jpg", 85, white); err != nil {
panic(err)
}
}
PHP: GD
<?php
function pngToJpg(
string $inputPath,
string $outputPath,
int $quality = 85,
array $background = [255, 255, 255]
): void {
$src = imagecreatefrompng($inputPath);
if (!$src) {
throw new RuntimeException("Failed to open PNG: $inputPath");
}
$width = imagesx($src);
$height = imagesy($src);
// Create a true-colour canvas for the output
$dst = imagecreatetruecolor($width, $height);
// Fill with background colour (replaces transparency)
$bg = imagecolorallocate($dst, ...$background);
imagefill($dst, 0, 0, $bg);
// Copy source over background (flattens alpha)
imagecopy($dst, $src, 0, 0, 0, 0, $width, $height);
imagejpeg($dst, $outputPath, $quality);
imagedestroy($src);
imagedestroy($dst);
}
// Usage
pngToJpg('input.png', 'output.jpg', 85);
// Batch convert
foreach (glob('images/*.png') as $pngFile) {
$jpgFile = preg_replace('/\.png$/i', '.jpg', $pngFile);
pngToJpg($pngFile, $jpgFile);
echo "Converted: $pngFile → $jpgFile\n";
}
Quick reference
| Task | Tool / function |
|---|---|
| Browser, in-page | canvas.toBlob(cb, 'image/jpeg', quality) |
| Node.js | sharp(input).flatten().jpeg() |
| Python | Pillow — Image.open() → paste on RGB bg → save('JPEG') |
| Go | image/png decode → draw.Over composite → jpeg.Encode |
| PHP | imagecreatefrompng → imagecopy → imagejpeg |
| CLI | ffmpeg -i input.png output.jpg or convert input.png output.jpg |
Six common mistakes
1. Ignoring transparency Converting a logo with a transparent background to JPG without filling the alpha channel produces a black-background image. Always flatten alpha before encoding as JPG.
2. Re-compressing an already lossy image If you already have a JPG, don't convert it to PNG and back to JPG — you'll stack two rounds of lossy compression. Work from the original source whenever possible.
3. Using quality 100 to "avoid quality loss" JPG at quality 100 is not lossless — it still applies the DCT transform. The file will be enormous (often larger than the original PNG) and still not pixel-perfect. Use 85–90 for general use, 95 for archival.
4. Converting screenshots with text JPG creates block artefacts around hard edges and text. Screenshots and diagrams stay sharper as PNG. Only convert screenshots with photographic content.
5. Not setting optimize=True in Pillow
Adding optimize=True to Pillow's save() call enables Huffman table optimisation, typically saving an extra 5–15% without any quality loss.
6. Expecting identical results across tools
JPG quality "85" in Pillow, Sharp, and GD all produce different byte sizes because they use different Huffman tables and sub-sampling settings. Sharp with mozjpeg: true usually produces the smallest files.
FAQ
Can I convert JPG back to PNG without quality loss? No. The lossy compression in the original JPG cannot be undone. Converting JPG → PNG creates a lossless PNG, but the JPG artefacts are already baked in. Always keep the original source file.
Will the JPG look different from the PNG? For photographic images at quality 85+, the difference is typically invisible to the naked eye. For graphics with solid colour fills, flat gradients, or text, you may notice subtle block artefacts around edges.
What quality setting should I use? 85–90 for most web images. 75–80 for thumbnails where bandwidth matters more. 95 if the image will be printed or used as a source file.
Does the conversion change the image dimensions? No. Converting PNG to JPG is a re-encoding operation that changes only the compression algorithm, not the pixel dimensions or the image content (aside from minor lossy artefacts).
How do I convert multiple files at once?
See the batch examples in Python and PHP above. For command-line bulk conversion, use ImageMagick: mogrify -format jpg -quality 85 *.png.
Is there a way to do it without losing any quality? If transparency isn't an issue, keeping the file as PNG is the lossless option. There's no lossless JPG equivalent — the closest is PNG (lossless), AVIF (can be lossless), or WebP (lossless mode). JPG is inherently lossy.