How to set pHYs chunk (DPI) of PNG file

Hello! I'm trying to update the DPI setting of a PNG image by reading an existing file, updating the pHYs chunk, and writing the output to a new file. For example, I'd update the pHYs chunk to x_res=11811, y_res=11811, units=1, with x_res and y_res values depending on what DPI I want.

I've been messing around with the png crate but it seems really complicated and difficult to grasp for what seems like it should be a minor edit. So far I've been trying to figure out how to decode the image, then manually rebuild it row by row and re-encode it. Is there a simpler way to do this?

I looked at the image crate but I couldn't find any way to change the pHYs chunk or DPI settings with that.

Okay, I spent a lot of time reading the png crate documentation and examples and came up with this code that can update the DPI / pHYs chunk.

At first I was manually getting each property from the input image Info and setting each property in the output image, but then I realized I could clone it entirely, then pass it to the Encoder when creating it using Encoder::with_info() instead of Encoder::new().

Here's the code, hopefully this helps someone else stumbling across this thread! :3

Cargo.toml dependencies:

[dependencies]
png = "0.17.16"

Program:

// For reading and opening files
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;

fn main() {
	// sets desired DPI (Dots per inch)
	// (hard-coded to 300 DPI for example)
	let dpi = 300.0;
	// calculates pixels per meter from DPI
	// (there are 39.37 inches in a meter)
	let ppm = dpi * 39.37;
	let ppm = ppm as u32;

	// reads existing image from file into a Reader with Decoder
	let decoder = png::Decoder::new(File::open("input.png").unwrap());
	let mut reader = decoder.read_info().unwrap();

	// Allocates the output buffer.
	let mut buf = vec![0; reader.output_buffer_size()];
	// Reads the next frame. An APNG might contain multiple frames.
	let info = reader.next_frame(&mut buf).unwrap();
	// Grabs the bytes of the image.
	let bytes = &buf[..info.buffer_size()];

	// Creates a new file to write output to
	let path = Path::new("output.png");
	let file = File::create(path).unwrap();
	let w = &mut BufWriter::new(file);

	// Copies Info from input file to reuse for output file
	let mut output_info = reader.info().clone();

	// Sets pixel dimensions (as pixels per meter)
	output_info.pixel_dims = Some(png::PixelDimensions {
		xppu: ppm,
		yppu: ppm,
		unit: png::Unit::Meter,
	});

	// Creates encoder with output_info as the Info
	let encoder = png::Encoder::with_info(w, output_info).unwrap();

	// Saves image header and data to output file
	let mut writer = encoder.write_header().unwrap();
	writer.write_image_data(bytes).unwrap();
}
3 Likes