Skip to content

Instantly share code, notes, and snippets.

@CruelDrool
Last active February 13, 2022 03:15
Show Gist options
  • Save CruelDrool/1d1598046b618d459e2c278f86e0fa77 to your computer and use it in GitHub Desktop.
Save CruelDrool/1d1598046b618d459e2c278f86e0fa77 to your computer and use it in GitHub Desktop.
Change, add or remove a PNG image's pixel density/resolution/pHYs chunk without any resampling. (PHP CLI)
#!/usr/bin/php
<?php
/*
* Version: 1.0.1
* Author: CruelDrool (https://github.com/CruelDrool)
* Description: Change, add or remove a PNG image's pixel density/resolution/pHYs chunk without any resampling. The core of the code is based upon an answer on Stack Overflow by soulmare: https://stackoverflow.com/a/46541839
*
* For use on Windows:
* 1. Create a folder where you will put this file.
* a. Copy this file into it.
* b. Create a BATCH (.bat) file named the same as this file: png-res.bat.
* i. The content of the BATCH file should be a single line: @php "%~dp0png-res.php" %*
* 2. Put the the new folder path into your PATH, together with the PHP installation folder path.
* a. Advanced System Settings -> Click on the button named "Environment Variables" -> Look in the box called "System Variables" and find "Path".
* i. Edit and add New.
*
* Changelog:
* [1.0.1]
* - Improved the piping functionality.
* - No need to specify the input file option ("-i -") when receiving from the pipe.
* - Can send through the pipe now too (can even save a copy by specifying an output file).
*/
if (!isset($argv)) {
exit;
}
$filename = pathinfo(__FILE__)['filename'];
$usage = sprintf('Usage: %1$s -i [INPUT FILE] -o [OUTPUT FILE] -a [ACTION] -u [UNIT] -d [DENSITY]', $filename);
if ( $argc >= 2 && in_array($argv[1], ['--help', '-help', '/help', '-h', '-?', '/?', '/h']) ) {
printf('%1$s
-h, --help Display this help and exit.
-i Input file\'s path. Required only when not receiving anything from the pipe, ignored otherwise.
-o Output file\'s path. Required only when not *sending* anything through the pipe. However, setting it while piping out will still output a file.
Can be the same as the input, but this will overwrite the file.
-a What action to take (case-insensitive): change, add, remove.
Default: change
-u Name of which unit to use (case-insensitive): PixelsPerMeter (ppm), PixelsPerCentimeter (ppc/ppcm), PixelsPerInch (ppi), DotsPerInch (dpi).
Default: PixelsPerMeter
-d Set pixel density. (Ignored when "remove" is set as the action.)
Default: 2835 (ppm) / 28.35 (ppcm) / 72 (ppi).
--no-rounding Don\'t do any rounding after converting to PixelsPerMeter.
However, any density value set is going to be packed as an unsigned integer (https://www.w3.org/TR/2003/REC-PNG-20031110/#11pHYs).
It just means that something like 5905.5 becomes 5905, instead of 5906. (NB! This has no effect when using the default value, since it\'s at exactly 2835 ppm)
Examples:
%2$s -i input.png -o output.png
(Set density value to default.)
%2$s -i input.png -o output.png -u PixelsPerInch -d 300
%2$s -i input.png -o output.png -u ppi -d 300
%2$s -i input.png -o output.png -u DotsPerInch -d 300
%2$s -i input.png -o output.png -u dpi -d 300
(Set density value to 300 ppi/dpi.)
%2$s -i input.png -o output.png -u ppc -d 118.11
(Same as the examples above, just in PixelsPerCentimeter.)
%2$s -i input.png -o output.png -a add
(Add a missing pHYs chunk with default density value. If it already exists, the chunk will be changed instead.)
%2$s -i input.png -o output.png -a remove
(Remove the pHYs chunk.)
Example of piping:
inkscape --export-type=png --export-filename=- input.svg | %2$s -o output.png -u ppi -d 300
', $usage, $filename);
exit;
}
$options = getopt('i:o:a:u:d:',['no-rounding']);
$needHelp = sprintf('%1$s
Use -h, --help for more help.
', $usage);
if (stream_isatty(STDIN) && stream_isatty(STDOUT) && empty($options)) {
exit($needHelp);
}
$warnings = [];
$errors = [];
if (isset($options['a']) && !in_array(strtolower($options['a']), ['add','remove','change']) ) {
$warnings[] = 'Warning: Invalid action. Defaulting to "change"';
}
if (isset($options['u']) && !in_array(strtolower($options['u']), ['pixelspermeter', 'ppi','pixelspercentimeter', 'ppc', 'ppcm', 'pixelsperinch', 'ppi', 'dotsperinch', 'dpi']) ) {
$warnings[] = 'Warning: Unsupported unit. Defaulting to "PixelsPerMeter"';
}
if (isset($options['d'])) {
$value = floatval($options['d']);
$text = '';
if ( !is_numeric($options['d']) ) {
$text = 'not a number';
} elseif ($value == 0) {
$text = '0';
} elseif ($value < 0) {
$text = 'a negative number';
}
if (!empty($text)) {
$warnings[] = sprintf('Warning: Density provided was %s. Defaulting to 2835 ppm / 28.35 ppcm / 72 ppi.', $text);
}
}
if ( (!isset($options['i'] ) || empty($options['i']) ) && stream_isatty(STDIN) ) {
$errors[] = "Error: No input file provided!";
}
if ( (!isset($options['o'] ) || empty($options['o']) ) && stream_isatty(STDOUT) ) {
$errors[] = "Error: No output file set!";
}
if (!empty($warnings) && stream_isatty(STDOUT)) {
$warnings = implode("\n", $warnings);
echo $warnings;
}
if (!empty($errors)) {
$errors[] = "\n" . $needHelp;
$errors = implode("\n", $errors);
exit($errors);
}
$action = strtolower($options['a'] ?? 'change');
$unit = strtolower($options['u'] ?? 'PixelsPerMeter');
$value = floatval($options['d'] ?? 0);
if (!stream_isatty(STDIN)) {
$data = stream_get_contents(STDIN);
} else {
if (isset($options['i']) && file_exists($options['i'])) {
$data = file_get_contents($options['i']);
} else {
exit("Error: Input file or stream not found!");
}
}
switch ($unit) {
case 'dpi':
case 'dotsperinch':
case 'ppi':
case 'pixelsperinch':
$value = $value > 0 ? $value : 72;
$ppm = $value/0.0254;
break;
case 'ppc':
case 'ppcm':
case 'pixelspercentimeter':
$value = $value > 0 ? $value : 28.35;
$ppm = $value * 100;
break;
case 'ppm':
case 'pixelspermeter':
default:
$ppm = $value > 0 ? $value : 2835;
break;
}
if (!isset($options['no-rounding']))
$ppm = round($ppm);
switch ($action) {
case 'remove':
$func = 'remove';
$args = [$data];
break;
case 'add':
$func = 'add';
$args = [$data, $ppm];
break;
case 'change':
default:
$func = 'change';
$args = [$data, $ppm];
break;
}
$newData = call_user_func_array($func, $args);
if ($newData !== false) {
if (!stream_isatty(STDOUT)) {
fputs(STDOUT, $newData);
}
if (isset($options['o'])) {
file_put_contents($options['o'], $newData);
}
}
function change($data, float $ppm) {
$position = strpos($data, 'pHYs') ;
if ($position == 0) { return add($data, $ppm); } // No pHYs chunk found, add one instead.
$position = $position + 4;
$chunk = pack('NNc', $ppm, $ppm, 1); // Pack chunk data
$chunk = $chunk.pack('N', crc32('pHYs'.$chunk)); // Add CRC of the chunk
$newData = substr_replace($data, $chunk, $position, 13); // Insert new chunk.
return $newData;
}
function remove($data) {
$position = strpos($data, 'pHYs') ;
if ($position == 0) { return $data; } // pHYs chunk already gone, don't remove anything, just dump the input file out (else we will just remove image data - no no no).
$position = $position - 4;
$newData = substr_replace($data, "", $position, 21); // Remove chunk
return $newData;
}
function add($data, float $ppm) {
if (strpos($data, 'pHYs') != 0) { return change($data, $ppm); } // pHYs chunk already present, change it instead.
$position = strpos($data, 'IDAT');
if ($position == 0 ) { return false; } // Is this even a PNG file?
$position = $position - 4;
$chunk = 'pHYs'.pack('NNc', $ppm, $ppm, 1); // Pack chunk data
$chunk = pack('N', 9).$chunk.pack('N', crc32($chunk)); // Prepend chunk's size. Add CRC of the chunk
$newData = substr_replace($data, $chunk, $position, 0); // Insert chunk.
return $newData;
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment