I'm trying to tack on some information about the physical size for printing my PNGs just before they are generated.
Reading the libpng doc and the pHYs chunk specifications has been helpful but I just can't seem to crack it.
I have tried adding this chunk in the most manual and simplest way possible, however, the .png file ends up corrupted. Am I missing an encoding trick?
For the CRC computation I have used the 32-bit result of this site, having plugged in the ASCII values for the chunk that the code below gives me.
$encoded = $_POST['imgdata'];
$encoded = str_replace(' ', '+', $encoded);
$decoded= base64_decode($encoded);
$test = explode('IDAT',$decoded);
$ppu='00000000000000000010111000100011'; //32-bit integer for the pixels per unit
$dppu=bindec($ppu);
$test[0].=sprintf("%c",bindec('00000000000000000000000000001001')) //length, also 32-bit
.'pHYs' //type
. sprintf("%c",$dppu) //Pixels per unit, x axis
. sprintf("%c",$dppu) //Pixels per unit, y axis
.'1' //Units in metres (1 byte)
. sprintf("%c", bindec(base_convert('0x0BFAAA7E', 16, 2))) //CRC (32-bit)
.'IDAT';
$fintest=implode($test);
echo $fintest;
Please let me know whether hacking it in like this is likely to work. I am also unsure about my 32-bit integers: is zero-padding them as I am doing the correct way to make them 32-bit?
After having spent the day on this, I found that the way to do this was to translate everything byte by byte. Following the docs, the pHYs
chunk takes:
I wrote all this as a binary string, padding the bytes with 0 as necessary.
It then turns out that the chr()
function correctly encodes this to be used in the PNG file, unlike the sprintf("%c",$string)
method I use in the question. Here is the code
$encoded = $_POST['imgdata'];
$encoded = str_replace(' ', '+', $encoded);
$decoded= base64_decode($encoded);
$binstring='00000000000000000000000000001001' //4-byte length
. '01110000010010000101100101110011' //4-byte type or 'pHYs'
. '000000000000000000101110001000110000000000000000001011100010001100000001' //9-byte data
. base_convert('0x0BFAAA7E', 16, 2); //4-byte CRC
foreach (str_split($binstring,8) as $b) {
$on.=chr(bindec($b));
}
The $on variable is then appended to the position just before the 4 bytes of the IDAT
chunk length.
Extending Alex's answer and correcting minor errors in his code, I'll try to provide a working example on how to append a PNG pHYs chunk, or set PNG image resolution in pure PHP without resampling image. The following code reads data from file.png and sends modified image to the standard output.
<?php
// Read file data
$data = file_get_contents('file.png');
// Pixels per inch
$ppi = 300;
// Unit conversion PPI to PPM
$ppm = round($ppi * 100 / 2.54);
$ppm_bin = str_pad(base_convert($ppm, 10, 2), 32, '0', STR_PAD_LEFT);
// Split PNG data at first IDAT chunk
$data_splitted = explode('IDAT', $data, 2);
// Generate "pHYs" chunk data
// 4-byte data length
$length_bin = '00000000000000000000000000001001';
// 4-byte type or 'pHYs'
$chunk_name_bin = '01110000010010000101100101110011';
// 9-byte data
$ppu_data_bin =
$ppm_bin // Pixels per unit, x axis
.$ppm_bin // Pixels per unit, y axis
.'00000001'; // units - 1 for meters
// Calculate 4-byte CRC
$hash_buffer = '';
foreach(str_split($chunk_name_bin.$ppu_data_bin, 8) as $b)
$hash_buffer .= chr(bindec($b));
$crc_bin = str_pad(base_convert(crc32($hash_buffer), 10, 2), 32, '0', STR_PAD_LEFT);
// Create chunk binary string
$binstring = $length_bin
. $chunk_name_bin
. $ppu_data_bin
. $crc_bin;
// Convert binary string to raw
$phys_chunk_raw = '';
foreach(str_split($binstring, 8) as $b)
$phys_chunk_raw .= chr(bindec($b));
// Insert "pHYs" chunk before first IDAT tag
$new_image_data = substr($data_splitted[0], 0, strlen($data_splitted[0]) - 4)
. $phys_chunk_raw
. substr($data_splitted[0], strlen($data_splitted[0]) - 4, 4)
. 'IDAT'
. $data_splitted[1];
// Output modified image
header("Content-Type: image/png");
echo $new_image_data;
?>
Or, more elegant snippet that does the same thing (found at habrahabr.ru):
<?php
// Read file data
$data = file_get_contents('file.png');
// Pixels per inch
$ppi = 300;
$incPos = strpos($data, 'IDAT') - 4; // Get chunk position
$chunk = 'pHYs'.pack('NNc', round($ppi/0.0254), round($ppi/0.0254), 1); // Pack chunk type + chunk data
$incData = pack('N', 9).$chunk.pack('N', crc32($chunk)); // Append chunk's size at beginning, it's crc at the end
$new_image_data = substr_replace($data, $incData, $incPos, 0);
// Output modified image
header("Content-Type: image/png");
echo $new_image_data;
?>
The only thing that you must remember: this approach works only with PNG files that have no pHYs chunk. For example, generated by PHP GD, or JavaScript HTMLCanvasElement.toDataURL(). If your PNG already has a pHYs chunk, you must find and modify existing chunk.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With