Last active
February 13, 2022 03:15
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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