=7, reduce stupidity declare (strict_types=1); // target size default define ("DIMENSION", 160); // limits define ("DIMENSION_MIN", 64); define ("DIMENSION_MAX", 512); // not recommended // these are loadable dimensions, i.e. before image is shrunk define ("MAX_WIDTH", 8192); define ("MAX_HEIGHT", 8192); define ("MIN_WIDTH", 64); define ("MIN_HEIGHT", 64); define ("E_OK", 0); define ("E_CL", 1); define ("E_LOAD", 2); define ("E_NOT_UEF", 3); define ("E_TRUNC", 4); define ("E_CHK_TOOBIG", 5); // define ("E_POS_TWICE", 6); define ("E_GD_LOAD", 7); define ("E_GD_GETSZ", 8); define ("E_NO_GD", 9); define ("E_IMG_LARGE", 10); define ("E_IMG_SMALL", 11); define ("E_TO_IDXD", 12); define ("E_TO_GREY", 13); define ("E_GD_GETPX", 14); define ("E_PAST_EOF", 15); define ("E_GZENCODE", 16); define ("E_SAVE", 17); define ("E_NOT_IXED", 18); define ("E_HELP", 19); define ("E_OVERWRITE", 20); define ("E_NO_IMGS", 21); define ("E_GD_NOJPG", 22); define ("E_GD_NOGIF", 23); define ("E_GD_NOPNG", 24); define ("CHUNK_LEN_MAX", 1024 * 1024); define ("UEF_MAGIC", "UEF File!\0"); define ("UEF_INLAY_VERSION", "0.2"); define ("UEF_INLAY_PROGNAME", "uef_inlay.php"); $e = my_main($_SERVER['argv']); print "\n"; return $e; die(); class UefChunk { var $type; var $len; var $data; var $ix; function to_string() : string { return sprintf("ix %d, type &%x, len %d", $this->ix, $this->type, $this->len); } function __construct(int $type) { $this->type = $type; } function encode() : string { return to_le16($this->type).to_le32($this->len).$this->data; } } class MyImage { // configuration: var $fn; var $insert_position; var $grey; // state: var $image; // gdimage var $shrunk; // gdimage var $indexed; // gdimage var $w; var $h; var $sw; // shrunk width var $sh; // shrunk height var $is_indexed; var $dimension; function __construct(string $fn, int $ip, bool $grey, int $size) { $this->fn = $fn; $this->insert_position = $ip; $this->grey = $grey; $this->is_indexed = false; $this->dimension = $size; } function load() : int { $a1 = array(); print "M: $this->fn:\n"; if (FALSE === ($a0 = getimagesize($this->fn, $a1))) { print "E: Failed to check image file: $this->fn\n"; return E_GD_LOAD; } $e = MyImage::check_load_size($a0[0], $a0[1]); if (E_OK != $e) { return $e; } // print_r($a0); // size info //print_r($a1); // extra info // die(); if (!isset($a0["mime"])) { print "E: BUG: getimagesize() failed to return MIME type\n"; return E_GD_LOAD; } else if ($a0["mime"] == "image/gif") { if ( ! function_exists("imagecreatefromgif") ) { print "\nE: PHP's GD library does not seem to include GIF support.\n"; return E_GD_NOGIF; } $this->image = @imagecreatefromgif($this->fn); } else if ($a0["mime"] == "image/jpeg") { if ( ! function_exists("imagecreatefromjpeg") ) { print "\nE: PHP's GD library does not seem to include JPEG support.\n"; return E_GD_NOJPG; } $this->image = @imagecreatefromjpeg($this->fn); } else if ($a0["mime"] == "image/png") { if ( ! function_exists("imagecreatefrompng") ) { print "\nE: PHP's GD library does not seem to include PNG support.\n"; return E_GD_NOPNG; } $this->image = @imagecreatefrompng($this->fn); } print "M: MIME type is ".$a0["mime"].".\n"; if (!isset($this->image) || (FALSE === $this->image)) { //data) { print "E: Failed to load image file: $this->fn\n"; return E_GD_LOAD; } $this->w = imagesx($this->image); $this->h = imagesy($this->image); $e = MyImage::check_load_size($this->w, $this->h); if (E_OK != $e) { return $e; } $wxh = "($this->w x $this->h) $this->fn\n"; $this->sw = $this->w; $this->sh = $this->h; if ( ! imageistruecolor($this->image) ) { print "M: Loaded image already has a palette! Yes!\n"; $this->is_indexed = true; } return E_OK; } static private function check_load_size (int $w, int $h) : int { $wxh = "$w x $h"; if (($w > MAX_WIDTH) || ($h > MAX_HEIGHT)) { print "E: Loaded image is too large ($wxh, max. ".MAX_WIDTH." x ".MAX_HEIGHT.")\n"; return E_IMG_LARGE; } else if (($w < MIN_WIDTH) || ($h < MIN_HEIGHT)) { print "E: Loaded image is too small ($wxh, min. ".MIN_WIDTH." x ".MIN_HEIGHT.")\n"; return E_IMG_SMALL; } return E_OK; } function shrink () : int { if (($this->w <= $this->dimension) && ($this->h <= $this->dimension)) { $this->shrunk = $this->image; print "M: No shrinkola necessary.\n"; return E_OK; // no shrink necessary } if ($this->w > $this->h) { $shrunk = imagescale($this->image, $this->dimension, -1, IMG_BICUBIC); } else { $shrunk = imagescale($this->image, (int) round(($this->w * $this->dimension) / $this->h), -1, IMG_BICUBIC); } $this->shrunk = $shrunk; $w = imagesx($this->shrunk); $h = imagesy($this->shrunk); $this->sw = $w; $this->sh = $h; print "M: Shrank ($this->w x $this->h) to ($w x $h).\n"; return E_OK; } function noconvert () : int { $palette_size = imagecolorstotal($this->shrunk); print "M: Loaded palette has $palette_size colours\n"; if (($palette_size > 256) || ($palette_size === false) || ($palette_size == 0)) { print "B: noconvert called, but loaded image is not indexed ($palette_size colours)\n"; return E_NOT_IXED; } $this->indexed = $this->shrunk; $this->is_indexed = true; // print "M: Loaded palette:\nM: "; // for ($i=0; $i < $palette_size; $i++) { // $a = imagecolorsforindex($this->indexed, $i); // printf("#%02x%02x%02x ", $a['red'], $a['green'], $a['blue']); // if ((7==($i&7))&&($i<($palette_size-1))) { // print "\nM: "; // } // } // print "\n"; return E_OK; } function convert (bool $dither, int $num_colours) : int { $this->indexed = $this->shrunk; $this->is_indexed = true; imagetruecolortopalette($this->indexed, $dither, $num_colours); if (FALSE === $this->indexed) { print "E: Error converting image to $num_colours". "-colour indexed.\n"; return E_TO_IDXD; } $palette_size = imagecolorstotal($this->indexed); if ($num_colours > $palette_size) { print "W: Partial palette: wanted $num_colours, got $palette_size.\n"; } else { print "M: Palette fully used ($num_colours entries).\n"; } // print "M: Assigned palette:\nM: "; // for ($i=0; $i < $palette_size; $i++) { // $a = imagecolorsforindex($this->indexed, $i); // printf("#%02x%02x%02x ", $a['red'], $a['green'], $a['blue']); // if ((7==($i&7))&&($i<($palette_size-1))) { // print "\nM: "; // } // } // print "\n"; return E_OK; } function to_grey() : int { if (false === imagefilter ($this->shrunk, IMG_FILTER_GRAYSCALE)) { print "E: Could not convert to greyscale.\n"; return E_TO_GREY; } return E_OK; } function to_uef_chunk (int $chunk_ix, UefChunk &$c_out) : int { $bpp = 8; $c = new UefChunk(3); $c->ix = $chunk_ix; $c->data = ""; $c->data .= to_le16($this->sw); $c->data .= to_le16($this->sh); $c->data .= chr($bpp | ((isset($this->grey) && $this->grey) ? 0x80 : 0)); $img = isset($this->indexed) ? $this->indexed : $this->shrunk; // do we have a palette? if (isset($this->indexed)) { $palette_size = imagecolorstotal($this->indexed); for ($i=0; $i < 256; $i++) { $r=0; $g=0; $b=0; if ($i < $palette_size) { $a = imagecolorsforindex($this->indexed, $i); $r = 0xff&$a['red']; $g = 0xff&$a['green']; $b = 0xff&$a['blue']; } $entry = chr($b).chr($g).chr($r); // printf("[%02x,%02x,%02x] ", $b, $g, $r); $c->data .= $entry; } } // now write the pixels themselves for ($y=0;$y<$this->sh;$y++) { for ($x=0;$x<$this->sw;$x++) { // imagecolorat() returns the index, or the colour $p = imagecolorat($img, $x, $y); if (false === $p) { print "E: imagecolorat() failed at ($x, $y)\n"; return E_GD_GETPX; } $c->data .= chr(0xff&$p); } } $c->len = strlen($c->data); $c_out = $c; return E_OK; } } class MyConfig { var $ipfile; var $images; var $fn_uef_in; var $fn_uef_out; var $compress; var $overwrite; function __construct() { // print "MyConfig construct\n"; $this->images = array(); $this->compress = true; $this->overwrite = false; } private static function parse_int (string $in, int &$out) : int { if (!ctype_digit($in)) { print "E: Not an integer: $in\n"; return E_CL; } $out = (int) $in; return E_OK; } private static function validate_dimension (int $d) : int { if (($dDIMENSION_MAX)) { print "E: Image size must be between ".DIMENSION_MIN." and ".DIMENSION_MAX."\n"; return E_CL; } return E_OK; } function parse_cli (array $argv) : int { $state = 0; $insert_position = -1; $grey = false; $dimension = 0; $pos_already_specified = false; array_shift($argv); $dupe = 0; foreach ($argv as $_=>$arg) { if (0 == $state) { if ($arg[0] != '+') { // filename $state = 100; $this->fn_uef_in = $arg; } else if ($arg == "+h") { MyConfig::usage(); return E_HELP; } else if ($arg == "+f") { $state = 1; } else if ($arg == "+p") { if ($pos_already_specified) { $dupe=2; break; } $pos_already_specified = true; $state = 2; } else if ($arg == "+g") { if ( $grey ) { $dupe=2; break; } $grey = true; $state = 0; } else if ($arg == "+s") { if ( $dimension > 0 ) { $dupe=2; break; } $state = 3; } else if ($arg == "+!") { if ( $this->overwrite ) { $dupe=1; break; } $this->overwrite = true; print "M: Allowing overwrite of existing output file.\n"; } else if ($arg == "+n") { if ( ! $this->compress ) { $dupe=2; break; } print "M: Do not compress output UEF file.\n"; $this->compress = false; } } else if (100 == $state) { $this->fn_uef_out = $arg; $state = 101; } else if (101 == $state) { print "E: junk args\n"; return E_CL; } else if (1 == $state) { // default to position 1 rather than 0; this will probably place it after the origin if ($insert_position < 0) { $insert_position = 1; } $p = new MyImage($arg, $insert_position, $grey, ($dimension==0)?DIMENSION:$dimension); $this->images[] = $p; $state = 0; $insert_position++; // unless overridden, place the next scan after this one $grey = false; $dimension = 0; $pos_already_specified = false; } else if (2 == $state) { if ( ! ctype_digit($arg) ) { print "E: Bad chunk position: $arg.\n"; return E_CL; } // if ($insert_position != -1) { // print "E: Position specified twice for this chunk.\n"; // return E_POS_TWICE; // } $insert_position = (int) $arg; $state = 0; } else if (3==$state) { $e = MyConfig::parse_int($arg, $dimension); if (E_OK != $e) { return $e; } $e = MyConfig::validate_dimension($dimension); if (E_OK != $e) { return $e; } print "M: Next image size set to $dimension.\n"; $state=0; } } if ($dupe==1) { print "E: Cannot specify $arg twice.\n"; return E_CL; } else if ($dupe==2) { print "E: Cannot specify $arg twice per image file.\n"; return E_CL; } if ( ! isset($this->fn_uef_in) ) { MyConfig::usage(); return E_CL; } if (! isset($this->fn_uef_out)) { print "E: Output UEF filename is mandatory! (Try +h.)\n"; return E_CL; } return E_OK; } function to_string () : string { $r = ""; $r .= "UEF file: $this->fn_uef_in\n"; $r .= "PNGs:\n"; foreach ($this->images as $_=>$v) { $r.= " $v->fn\n"; } return $r; } static function usage () : void { print "\n".UEF_INLAY_PROGNAME.", version ".UEF_INLAY_VERSION."\n\n"; //.". "; print "Help:\n\n"; print " php -f uef_inlay.php [options] \n\n"; print " [options] may be:\n\n". " +f append an image; GIF recommended; JPEG, PNG also work\n". " +p specify chunk insert position for next image\n". " +s specify maximum width or height for next image\n". " +g use greyscale for next image\n". " +! allow overwriting output UEF file\n". " +h show help\n"; print "\nTypical usage to attach two inlay scans to a UEF file might be:\n"; print "\n php -f uef_inlay.php +f scan1.gif +p 1 +f scan2.gif input.uef output.uef\n"; print "\nThe best input format is likely to be a GIF that is already smaller than the\n". "chosen size (+s) for the image (default ".DIMENSION."); PHP is quite bad at resizing and\n". "converting images. Ensuring that input arrives at the right size and with a sane\n". "palette is likely to yield best results.\n"; } } class Uef { var $major; var $minor; var $chunks; function __construct() { $this->chunks = array(); } function parse (string $s) : int { $unz = @gzdecode($s); if (FALSE !== $unz) { print "M: Decompressed UEF: ".strlen($s)." -> ".strlen($unz)."\n"; $s = $unz; } if (substr($s,0,10) != UEF_MAGIC) { print "E: Not a UEF\n"; return E_NOT_UEF; } $this->minor = ord($s[10]); $this->major = ord($s[11]); // 0.2: fixed bug $len = strlen($s); for ($cix=0, $i=12; $i<$len; $cix++) { if ($len < 6) { print "E: Truncated chunk header\n"; return E_TRUNC; } $chunk = new UefChunk(from_le16(substr($s, $i, 2))); $chunk->ix = $cix; $chunk->len = from_le32(substr($s, $i+2, 4)); if ($chunk->len > CHUNK_LEN_MAX) { printf ("E: Chunk %d is too large (%d, max. %d)\n", $cix, $chunk->len, CHUNK_LEN_MAX); return E_CHK_TOOBIG; } if ($len < ($i + 6 + $chunk->len)) { printf("E: Truncated chunk %d data at pos &%x\n", $cix, ($i + 6 + $chunk->len)); return E_TRUNC; } $chunk->data = substr($s, $i+6, $chunk->len); $i += 6 + $chunk->len; $this->chunks[] = $chunk; } return E_OK; } function to_string() : string { $r=""; $r .= "UEF version: $this->major.$this->minor\n"; foreach ($this->chunks as $_=>$c) { $r.=$c->to_string()."\n"; } return $r; } function insert_chunk (UefChunk $uc, int $position) : int { $len = count($this->chunks); if ($position >= $len) { print "E: Insert position ($position) was beyond end of UEF file ($len).\n"; return E_PAST_EOF; } $uca = array ($position => $uc); array_splice($this->chunks, $position, 0, $uca); return E_OK; } function write_file (string $fn, int $major, int $minor, bool $compress) { $s=UEF_MAGIC.chr($minor).chr($major); foreach ($this->chunks as $_=>$c) { $s .= $c->encode(); } if ($compress) { $s = @gzencode($s); if (FALSE === $s) { print "E: compressing failed\n"; return E_GZENCODE; } } if (FALSE === @file_put_contents($fn, $s)) { print "E: Could not write UEF file: $fn\n"; return E_SAVE; } print "M: Wrote ".strlen($s)." bytes: $fn\n"; return E_OK; } } // end class Uef function from_le16($s) : int { return ((ord($s[1]) << 8) & 0xff00) | (ord($s[0]) & 0xff); } function from_le32($s) : int { return ((ord($s[3]) << 24) & 0xff000000) | ((ord($s[2]) << 16) & 0xff0000 ) | ((ord($s[1]) << 8) & 0xff00 ) | (ord($s[0]) & 0xff ); } function to_le16(int $i) : string { return chr(0xff&$i).chr(0xff&($i>>8)); } function to_le32(int $i) : string { return chr(0xff&$i).chr(0xff&($i>>8)).chr(0xff&($i>>16)).chr(0xff&($i>>24)); } function my_main (array $argv) : int { print "\n"; if ( ! function_exists("gd_info")) { print "E: No PHP GD extension available.\n"; return E_NO_GD; } $cfg = new MyConfig(); $e = $cfg->parse_cli($argv); if (E_OK != $e) { return $e; } if (0==count($cfg->images)) { print "E: No images were supplied!\n"; return E_NO_IMGS; } //print $cfg->to_string()."\n"; $uef_s = @file_get_contents($cfg->fn_uef_in); if (FALSE === $uef_s) { print "E: Failed to load UEF file: $cfg->fn_uef_in\n"; return E_LOAD; } $uef = new Uef(); $e = $uef->parse($uef_s); if (E_OK != $e) { return $e; } print "M: UEF contains ".count($uef->chunks)." chunks.\n"; $imgs = $cfg->images; foreach ($imgs as $_=>$img) { if (E_OK != ($e = $img->load())) { return $e; } if (E_OK != ($e = $img->shrink())) { return $e; } // ONLY use a palette if image is colour. Otherwise // just use 0-255 grey values, and use $shrunk rather than $indexed if ($img->grey) { if (E_OK != ($e = $img->to_grey())) { return $e; } } else { if ($img->indexed) { // already has a palette if (E_OK != ($e = $img->noconvert())) { return $e; } } else { // needs converting to indexed if (E_OK != ($e = $img->convert(false, 256))) { return $e; } } } $uc = new UefChunk(3); if (E_OK != ($e = $img->to_uef_chunk($img->insert_position, $uc))) { return $e; } if (E_OK != ($e = $uef->insert_chunk($uc, $img->insert_position))) { return $e; } $len = $uef->chunks[$img->insert_position]->len; $type = $uef->chunks[$img->insert_position]->type; print "M: Inserted chunk, type &".sprintf("%x",$type).", length $len, at position $img->insert_position.\n"; } if (file_exists($cfg->fn_uef_out) && ! $cfg->overwrite) { print "E: Refusing to overwrite existing output file (try +!): $cfg->fn_uef_out\n"; return E_OVERWRITE; } $e = $uef->write_file($cfg->fn_uef_out, $uef->major, $uef->minor, $cfg->compress); return $e; } ?>