PNG-to-JPG in Rust is straightforward with the image crate — it handles both formats natively with no C dependencies. The main subtlety is alpha channels: PNGs may have transparency, but JPEG doesn't support it. Always composite onto a white (or custom) background before saving as JPEG. For performance-critical batch pipelines, turbojpeg's libjpeg-turbo binding is 2-3x faster.
Method 1: image crate (pure Rust, no C deps)
The image crate handles PNG loading and JPEG encoding natively in Rust — no system libraries needed.
# Cargo.toml
[dependencies]
image = "0.25"use image::{DynamicImage, GenericImageView, RgbImage, Rgba};
use std::path::Path;
use std::fs;
/// Convert PNG to JPEG, compositing transparent pixels onto a white background.
fn png_to_jpg(
src: &str,
dst: &str,
quality: u8, // 1-100; 85 is a good default
) -> Result<(), Box> {
let img = image::open(src)?;
// Composite onto white background to handle alpha
let (width, height) = img.dimensions();
let mut rgb = RgbImage::new(width, height);
let rgba = img.to_rgba8();
for (x, y, pixel) in rgba.enumerate_pixels() {
let Rgba([r, g, b, a]) = *pixel;
let alpha = a as f32 / 255.0;
let bg = (1.0 - alpha) * 255.0;
rgb.put_pixel(x, y, image::Rgb([
(r as f32 * alpha + bg) as u8,
(g as f32 * alpha + bg) as u8,
(b as f32 * alpha + bg) as u8,
]));
}
// Save as JPEG with quality
let mut out = std::io::BufWriter::new(fs::File::create(dst)?);
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, quality);
encoder.encode_image(&DynamicImage::ImageRgb8(rgb))?;
Ok(())
}
fn main() -> Result<(), Box> {
png_to_jpg("screenshot.png", "screenshot.jpg", 85)?;
println!("Done");
Ok(())
}
// Simpler version if you don't need alpha handling
// (safe when you know the PNG has no transparency)
fn png_to_jpg_simple(src: &str, dst: &str) -> Result<(), Box> {
let img = image::open(src)?.to_rgb8();
img.save(dst)?; // auto-detects JPEG from .jpg extension
Ok(())
}
The to_rgb8() method drops the alpha channel by discarding transparency. For PNGs with actual transparent areas, this turns them black — use the compositing approach above for correct results.
Method 2: turbojpeg crate (libjpeg-turbo, 2-3x faster encoding)
turbojpeg wraps libjpeg-turbo, the most optimized JPEG encoder available. Better for high-throughput batch pipelines.
# Cargo.toml
[dependencies]
image = "0.25"
turbojpeg = "1"
# Needs libjpeg-turbo: apt install libjpeg-turbo-devuse image::{GenericImageView, RgbImage, Rgba};
use std::fs;
fn png_to_jpg_turbo(
src: &str,
dst: &str,
quality: i32, // 1-100
) -> Result<(), Box> {
let img = image::open(src)?;
let (width, height) = img.dimensions();
// Composite onto white
let mut rgb = RgbImage::new(width, height);
for (x, y, Rgba([r, g, b, a])) in img.to_rgba8().enumerate_pixels() {
let alpha = *a as f32 / 255.0;
let bg = (1.0 - alpha) * 255.0;
rgb.put_pixel(x, y, image::Rgb([
(*r as f32 * alpha + bg) as u8,
(*g as f32 * alpha + bg) as u8,
(*b as f32 * alpha + bg) as u8,
]));
}
let raw = rgb.as_raw();
let jpeg_data = turbojpeg::compress_image(
raw,
width as usize,
height as usize,
turbojpeg::PixelFormat::RGB,
quality,
turbojpeg::Subsamp::Sub2x2, // 4:2:0 chrominance subsampling (standard)
)?;
fs::write(dst, jpeg_data.as_slice())?;
Ok(())
}
fn main() -> Result<(), Box> {
png_to_jpg_turbo("photo.png", "photo.jpg", 85)?;
println!("Done");
Ok(())
}
turbojpeg is 2-3x faster than the image crate's JPEG encoder for large images. For processing 1000+ images, this difference is significant. The downside: it requires the libjpeg-turbo system library.
Method 3: ChangeThisFile API (reqwest, no C deps)
The API handles PNG-to-JPG server-side. Source auto-detected from filename — pass target=jpg. Free tier: 1,000 conversions/month, no card needed.
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["multipart", "blocking"] }use reqwest::blocking::{Client, multipart};
use std::fs;
use std::path::Path;
const API_KEY: &str = "ctf_sk_your_key_here";
fn png_to_jpg_api(src: &str, dst: &str) -> Result<(), Box> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()?;
let file_bytes = fs::read(src)?;
let filename = Path::new(src).file_name().unwrap().to_string_lossy().to_string();
let form = multipart::Form::new()
.part(
"file",
multipart::Part::bytes(file_bytes)
.file_name(filename)
.mime_str("image/png")?,
)
.text("target", "jpg");
let resp = client
.post("https://changethisfile.com/v1/convert")
.header("Authorization", format!("Bearer {}", API_KEY))
.multipart(form)
.send()?;
if !resp.status().is_success() {
return Err(format!("API error: {}", resp.status()).into());
}
fs::write(dst, resp.bytes()?)?;
Ok(())
}
fn main() -> Result<(), Box> {
png_to_jpg_api("image.png", "image.jpg")?;
println!("Done");
Ok(())
}
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| image crate | Pure Rust, WASM targets, cross-compilation | Slower JPEG encoding than turbojpeg |
| turbojpeg | High-throughput batch, performance-critical pipelines | Requires libjpeg-turbo system library |
| ChangeThisFile API | No image libraries, minimal binary, serverless | Network call per image; 25MB free tier limit |
Production tips
- Always handle alpha channels before JPEG encoding. to_rgb8() drops alpha by truncating — transparent pixels become black. Composite onto a white or custom-colored background first.
- Use BufWriter for JPEG output. The JPEG encoder makes many small writes — wrapping the file in BufWriter significantly reduces I/O syscalls.
- Quality 85 is a good JPEG default. Quality 95+ adds file size with minimal visual benefit. Quality 75-80 is often indistinguishable and 25-40% smaller.
- Parallelize with Rayon. PNG-to-JPEG conversion is CPU-bound. par_iter() across a Vec of input paths scales linearly with core count with zero changes to the conversion logic.
- turbojpeg Sub2x2 is standard for photos. 4:2:0 subsampling (Sub2x2) reduces chroma resolution, which human vision is less sensitive to. Use Sub1x1 (4:4:4) for screenshots or text images.
The image crate is the right default for most Rust projects — pure Rust, zero C deps, and handles quality correctly. turbojpeg is worth adding for high-throughput batch pipelines. For scripts where you don't want any image crate dependency, the API handles it via reqwest. Free tier: 1,000 conversions/month.