[e16e8f2] | 1 | #!/usr/bin/perl |
---|
| 2 | ## ----------------------------------------------------------------------- |
---|
| 3 | ## |
---|
| 4 | ## Copyright 2004-2008 H. Peter Anvin - All Rights Reserved |
---|
| 5 | ## |
---|
| 6 | ## This program is free software; you can redistribute it and/or modify |
---|
| 7 | ## it under the terms of the GNU General Public License as published by |
---|
| 8 | ## the Free Software Foundation, Inc., 53 Temple Place Ste 330, |
---|
| 9 | ## Boston MA 02111-1307, USA; either version 2 of the License, or |
---|
| 10 | ## (at your option) any later version; incorporated herein by reference. |
---|
| 11 | ## |
---|
| 12 | ## ----------------------------------------------------------------------- |
---|
| 13 | |
---|
| 14 | ## |
---|
| 15 | ## ppmtolss16 |
---|
| 16 | ## |
---|
| 17 | ## Convert a PNM file with max 16 colors to a simple RLE-based format: |
---|
| 18 | ## |
---|
| 19 | ## uint32 0x1413f33d ; magic (littleendian) |
---|
| 20 | ## uint16 xsize ; littleendian |
---|
| 21 | ## uint16 ysize ; littleendian |
---|
| 22 | ## 16 x uint8 r,g,b ; color map, in 6-bit format (each byte is 0..63) |
---|
| 23 | ## |
---|
| 24 | ## Then, a sequence of nybbles: |
---|
| 25 | ## |
---|
| 26 | ## N ... if N is != previous pixel, one pixel of color N |
---|
| 27 | ## ... otherwise run sequence follows ... |
---|
| 28 | ## M ... if M > 0 then run length is M |
---|
| 29 | ## ... otherwise run sequence is encoded in two nybbles, |
---|
| 30 | ## littleendian, +16 |
---|
| 31 | ## |
---|
| 32 | ## The nybble sequences are on a per-row basis; runs may not extend |
---|
| 33 | ## across rows and odd-nybble rows are zero-padded. |
---|
| 34 | ## |
---|
| 35 | ## At the start of row, the "previous pixel" is assumed to be zero. |
---|
| 36 | ## |
---|
| 37 | ## Usage: |
---|
| 38 | ## |
---|
| 39 | ## ppmtolss16 [#rrggbb=i ...] < input.ppm > output.rle |
---|
| 40 | ## |
---|
| 41 | ## Command line options of the form #rrggbb=i indicate that |
---|
| 42 | ## the color #rrggbb (hex) should be assigned index i (decimal) |
---|
| 43 | ## |
---|
| 44 | |
---|
| 45 | eval { use bytes; }; |
---|
| 46 | eval { binmode STDIN; }; |
---|
| 47 | eval { binmode STDOUT; }; |
---|
| 48 | |
---|
| 49 | $magic = 0x1413f33d; |
---|
| 50 | |
---|
| 51 | # Get a token from the PPM header. Ignore comments and leading |
---|
| 52 | # and trailing whitespace, as is required by the spec. |
---|
| 53 | # This routine eats exactly one character of trailing whitespace, |
---|
| 54 | # unless it is a comment (in which case it eats the comment up |
---|
| 55 | # to and including the end of line.) |
---|
| 56 | sub get_token() { |
---|
| 57 | my($token, $ch); |
---|
| 58 | my($ch); |
---|
| 59 | |
---|
| 60 | do { |
---|
| 61 | $ch = getc(STDIN); |
---|
| 62 | return undef if ( !defined($ch) ); # EOF |
---|
| 63 | if ( $ch eq '#' ) { |
---|
| 64 | do { |
---|
| 65 | $ch = getc(STDIN); |
---|
| 66 | return undef if ( !defined($ch) ); |
---|
| 67 | } while ( $ch ne "\n" ); |
---|
| 68 | } |
---|
| 69 | } while ( $ch =~ /^[ \t\n\v\f\r]$/ ); |
---|
| 70 | |
---|
| 71 | $token = $ch; |
---|
| 72 | while ( 1 ) { |
---|
| 73 | $ch = getc(STDIN); |
---|
| 74 | last if ( $ch =~ /^[ \t\n\v\f\r\#]$/ ); |
---|
| 75 | $token .= $ch; |
---|
| 76 | } |
---|
| 77 | if ( $ch eq '#' ) { |
---|
| 78 | do { |
---|
| 79 | $ch = getc(STDIN); |
---|
| 80 | } while ( defined($ch) && $ch ne "\n" ); |
---|
| 81 | } |
---|
| 82 | return $token; |
---|
| 83 | } |
---|
| 84 | |
---|
| 85 | # Get a token, and make sure it is numeric (and exists) |
---|
| 86 | sub get_numeric_token() { |
---|
| 87 | my($token) = get_token(); |
---|
| 88 | |
---|
| 89 | if ( $token !~ /^[0-9]+$/ ) { |
---|
| 90 | print STDERR "Format error on input\n"; |
---|
| 91 | exit 1; |
---|
| 92 | } |
---|
| 93 | |
---|
| 94 | return $token + 0; |
---|
| 95 | } |
---|
| 96 | |
---|
| 97 | # Must be called before each pixel row is read |
---|
| 98 | sub start_new_row() { |
---|
| 99 | $getrgb_leftover_bit_cnt = 0; |
---|
| 100 | $getrgb_leftover_bit_val = 0; |
---|
| 101 | } |
---|
| 102 | |
---|
| 103 | # Get a single RGB token depending on the PNM type |
---|
| 104 | sub getrgb($) { |
---|
| 105 | my($form) = @_; |
---|
| 106 | my($rgb,$r,$g,$b); |
---|
| 107 | |
---|
| 108 | if ( $form == 6 ) { |
---|
| 109 | # Raw PPM, most common |
---|
| 110 | return undef unless ( read(STDIN,$rgb,3) == 3 ); |
---|
| 111 | return unpack("CCC", $rgb); |
---|
| 112 | } elsif ( $form == 3 ) { |
---|
| 113 | # Plain PPM |
---|
| 114 | $r = get_numeric_token(); |
---|
| 115 | $g = get_numeric_token(); |
---|
| 116 | $b = get_numeric_token(); |
---|
| 117 | return ($r,$g,$b); |
---|
| 118 | } elsif ( $form == 5 ) { |
---|
| 119 | # Raw PGM |
---|
| 120 | return undef unless ( read(STDIN,$rgb,1) == 1 ); |
---|
| 121 | $r = unpack("C", $rgb); |
---|
| 122 | return ($r,$r,$r); |
---|
| 123 | } elsif ( $form == 2 ) { |
---|
| 124 | # Plain PGM |
---|
| 125 | $r = get_numeric_token(); |
---|
| 126 | return ($r,$r,$r); |
---|
| 127 | } elsif ( $form == 4 ) { |
---|
| 128 | # Raw PBM |
---|
| 129 | if ( !$getrgb_leftover_bit_cnt ) { |
---|
| 130 | return undef unless ( read(STDIN,$rgb,1) == 1 ); |
---|
| 131 | $getrgb_leftover_bit_val = unpack("C", $rgb); |
---|
| 132 | $getrgb_leftover_bit_cnt = 8; |
---|
| 133 | } |
---|
| 134 | $r = ( $getrgb_leftover_bit_val & 0x80 ) ? 0x00 : 0xff; |
---|
| 135 | $getrgb_leftover_bit_val <<= 1; |
---|
| 136 | $getrgb_leftover_bit_cnt--; |
---|
| 137 | |
---|
| 138 | return ($r,$r,$r); |
---|
| 139 | } elsif ( $form == 1 ) { |
---|
| 140 | # Plain PBM |
---|
| 141 | my($ch); |
---|
| 142 | |
---|
| 143 | do { |
---|
| 144 | $ch = getc(STDIN); |
---|
| 145 | return undef if ( !defined($ch) ); |
---|
| 146 | return (255,255,255) if ( $ch eq '0' ); # White |
---|
| 147 | return (0,0,0) if ( $ch eq '1'); # Black |
---|
| 148 | if ( $ch eq '#' ) { |
---|
| 149 | do { |
---|
| 150 | $ch = getc(STDIN); |
---|
| 151 | return undef if ( !defined($ch) ); |
---|
| 152 | } while ( $ch ne "\n" ); |
---|
| 153 | } |
---|
| 154 | } while ( $ch =~ /^[ \t\n\v\f\r]$/ ); |
---|
| 155 | return undef; |
---|
| 156 | } else { |
---|
| 157 | die "Internal error: unknown format: $form\n"; |
---|
| 158 | } |
---|
| 159 | } |
---|
| 160 | |
---|
| 161 | sub rgbconvert($$$$) { |
---|
| 162 | my($r,$g,$b,$maxmult) = @_; |
---|
| 163 | my($rgb); |
---|
| 164 | |
---|
| 165 | $r = int($r*$maxmult); |
---|
| 166 | $g = int($g*$maxmult); |
---|
| 167 | $b = int($b*$maxmult); |
---|
| 168 | $rgb = pack("CCC", $r, $g, $b); |
---|
| 169 | return $rgb; |
---|
| 170 | } |
---|
| 171 | |
---|
| 172 | foreach $arg ( @ARGV ) { |
---|
| 173 | if ( $arg =~ /^\#([0-9a-f])([0-9a-f])([0-9a-f])=([0-9]+)$/i ) { |
---|
| 174 | $r = hex($1) << 4; |
---|
| 175 | $g = hex($2) << 4; |
---|
| 176 | $b = hex($3) << 4; |
---|
| 177 | $i = $4 + 0; |
---|
| 178 | } elsif ( $arg =~ /^\#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})=([0-9]+)$/i ) { |
---|
| 179 | $r = hex($1); |
---|
| 180 | $g = hex($2); |
---|
| 181 | $b = hex($3); |
---|
| 182 | $i = $4 + 0; |
---|
| 183 | } elsif ( $arg =~ /^\#([0-9a-f]{3})([0-9a-f]{3})([0-9a-f]{3})=([0-9]+)$/i ) { |
---|
| 184 | $r = hex($1) >> 4; |
---|
| 185 | $g = hex($2) >> 4; |
---|
| 186 | $b = hex($3) >> 4; |
---|
| 187 | $i = $4 + 0; |
---|
| 188 | } elsif ( $arg =~ /^\#([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})=([0-9]+)$/i ) { |
---|
| 189 | $r = hex($1) >> 8; |
---|
| 190 | $g = hex($2) >> 8; |
---|
| 191 | $b = hex($3) >> 8; |
---|
| 192 | $i = $4 + 0; |
---|
| 193 | } else { |
---|
| 194 | print STDERR "$0: Unknown argument: $arg\n"; |
---|
| 195 | next; |
---|
| 196 | } |
---|
| 197 | |
---|
| 198 | if ( $i > 15 ) { |
---|
| 199 | print STDERR "$0: Color index out of range: $arg\n"; |
---|
| 200 | next; |
---|
| 201 | } |
---|
| 202 | |
---|
| 203 | $rgb = rgbconvert($r, $g, $b, 64/256); |
---|
| 204 | |
---|
| 205 | if ( defined($index_forced{$i}) ) { |
---|
| 206 | print STDERR "$0: More than one color index $i\n"; |
---|
| 207 | exit(1); |
---|
| 208 | } |
---|
| 209 | $index_forced{$i} = $rgb; |
---|
| 210 | $force_index{$rgb} = $i; |
---|
| 211 | } |
---|
| 212 | |
---|
| 213 | $form = get_token(); |
---|
| 214 | die "$0: stdin is not a PNM file" if ( $form !~ /^P([1-6])$/ ); |
---|
| 215 | $form = $1+0; |
---|
| 216 | |
---|
| 217 | $xsize = get_numeric_token(); |
---|
| 218 | $ysize = get_numeric_token(); |
---|
| 219 | if ( $form == 1 || $form == 4 ) { |
---|
| 220 | $maxcol = 255; # Internal convention |
---|
| 221 | } else { |
---|
| 222 | $maxcol = get_numeric_token(); |
---|
| 223 | } |
---|
| 224 | $maxmult = 64/($maxcol+1); # Equal buckets conversion |
---|
| 225 | |
---|
| 226 | @data = (); |
---|
| 227 | |
---|
| 228 | for ( $y = 0 ; $y < $ysize ; $y++ ) { |
---|
| 229 | start_new_row(); |
---|
| 230 | for ( $x = 0 ; $x < $xsize ; $x++ ) { |
---|
| 231 | die "$0: Premature EOF at ($x,$y) of ($xsize,$ysize)\n" |
---|
| 232 | if ( !scalar(@pnmrgb = getrgb($form)) ); |
---|
| 233 | # Convert to 6-bit representation |
---|
| 234 | $rgb = rgbconvert($pnmrgb[0], $pnmrgb[1], $pnmrgb[2], $maxmult); |
---|
| 235 | $color_count{$rgb}++; |
---|
| 236 | push(@data, $rgb); |
---|
| 237 | } |
---|
| 238 | } |
---|
| 239 | |
---|
| 240 | # Sort list of colors according to freqency |
---|
| 241 | @colors = sort { $color_count{$b} <=> $color_count{$a} } keys(%color_count); |
---|
| 242 | |
---|
| 243 | # Now we have our pick of colors. Sort according to intensity; |
---|
| 244 | # this is more or less an ugly hack to cover for the fact that |
---|
| 245 | # using PPM as input doesn't let the user set the color map, |
---|
| 246 | # which the user really needs to be able to do. |
---|
| 247 | |
---|
| 248 | sub by_intensity() { |
---|
| 249 | my($ra,$ga,$ba) = unpack("CCC", $a); |
---|
| 250 | my($rb,$gb,$bb) = unpack("CCC", $b); |
---|
| 251 | |
---|
| 252 | my($ia) = $ra*0.299 + $ga*0.587 + $ba*0.114; |
---|
| 253 | my($ib) = $rb*0.299 + $gb*0.587 + $bb*0.114; |
---|
| 254 | |
---|
| 255 | return ( $ia <=> $ib ) if ( $ia != $ib ); |
---|
| 256 | |
---|
| 257 | # If same, sort based on RGB components, |
---|
| 258 | # with highest priority given to G, then R, then B. |
---|
| 259 | |
---|
| 260 | return ( $ga <=> $gb ) if ( $ga != $gb ); |
---|
| 261 | return ( $ra <=> $rb ) if ( $ra != $rb ); |
---|
| 262 | return ( $ba <=> $bb ); |
---|
| 263 | } |
---|
| 264 | |
---|
| 265 | @icolors = sort by_intensity @colors; |
---|
| 266 | |
---|
| 267 | # Insert forced colors into "final" array |
---|
| 268 | @colors = (undef) x 16; |
---|
| 269 | foreach $rgb ( keys(%force_index) ) { |
---|
| 270 | $i = $force_index{$rgb}; |
---|
| 271 | $colors[$i] = $rgb; |
---|
| 272 | $color_index{$rgb} = $i; |
---|
| 273 | } |
---|
| 274 | |
---|
| 275 | undef %force_index; |
---|
| 276 | |
---|
| 277 | # Insert remaining colors in the remaining slots, |
---|
| 278 | # in luminosity-sorted order |
---|
| 279 | $nix = 0; |
---|
| 280 | while ( scalar(@icolors) ) { |
---|
| 281 | # Advance to the next free slot |
---|
| 282 | $nix++ while ( defined($colors[$nix]) && $nix < 16 ); |
---|
| 283 | last if ( $nix >= 16 ); |
---|
| 284 | $rgb = shift @icolors; |
---|
| 285 | if ( !defined($color_index{$rgb}) ) { |
---|
| 286 | $colors[$nix] = $rgb; |
---|
| 287 | $color_index{$rgb} = $nix; |
---|
| 288 | } |
---|
| 289 | } |
---|
| 290 | |
---|
| 291 | while ( scalar(@icolors) ) { |
---|
| 292 | $rgb = shift @icolors; |
---|
| 293 | $lost++ if ( !defined($color_index{$rgb}) ); |
---|
| 294 | } |
---|
| 295 | |
---|
| 296 | if ( $lost ) { |
---|
| 297 | printf STDERR |
---|
| 298 | "$0: Warning: color palette truncated (%d colors ignored)\n", $lost; |
---|
| 299 | } |
---|
| 300 | |
---|
| 301 | undef @icolors; |
---|
| 302 | |
---|
| 303 | # Output header |
---|
| 304 | print pack("Vvv", $magic, $xsize, $ysize); |
---|
| 305 | |
---|
| 306 | # Output color map |
---|
| 307 | for ( $i = 0 ; $i < 16 ; $i++ ) { |
---|
| 308 | if ( defined($colors[$i]) ) { |
---|
| 309 | print $colors[$i]; |
---|
| 310 | } else { |
---|
| 311 | # Padding for unused color entries |
---|
| 312 | print pack("CCC", 63*$i/15, 63*$i/15, 63*$i/15); |
---|
| 313 | } |
---|
| 314 | } |
---|
| 315 | |
---|
| 316 | sub output_nybble($) { |
---|
| 317 | my($ny) = @_; |
---|
| 318 | |
---|
| 319 | if ( !defined($ny) ) { |
---|
| 320 | if ( defined($nybble_tmp) ) { |
---|
| 321 | $ny = 0; # Force the last byte out |
---|
| 322 | } else { |
---|
| 323 | return; |
---|
| 324 | } |
---|
| 325 | } |
---|
| 326 | |
---|
| 327 | $ny = $ny & 0x0F; |
---|
| 328 | |
---|
| 329 | if ( defined($nybble_tmp) ) { |
---|
| 330 | $ny = ($ny << 4) | $nybble_tmp; |
---|
| 331 | print chr($ny); |
---|
| 332 | $bytes++; |
---|
| 333 | undef $nybble_tmp; |
---|
| 334 | } else { |
---|
| 335 | $nybble_tmp = $ny; |
---|
| 336 | } |
---|
| 337 | } |
---|
| 338 | |
---|
| 339 | sub output_run($$$) { |
---|
| 340 | my($last,$this,$run) = @_; |
---|
| 341 | |
---|
| 342 | if ( $this != $last ) { |
---|
| 343 | output_nybble($this); |
---|
| 344 | $run--; |
---|
| 345 | } |
---|
| 346 | while ( $run ) { |
---|
| 347 | if ( $run >= 16 ) { |
---|
| 348 | output_nybble($this); |
---|
| 349 | output_nybble(0); |
---|
| 350 | if ( $run > 271 ) { |
---|
| 351 | $erun = 255; |
---|
| 352 | $run -= 271; |
---|
| 353 | } else { |
---|
| 354 | $erun = $run-16; |
---|
| 355 | $run = 0; |
---|
| 356 | } |
---|
| 357 | output_nybble($erun); |
---|
| 358 | output_nybble($erun >> 4); |
---|
| 359 | } else { |
---|
| 360 | output_nybble($this); |
---|
| 361 | output_nybble($run); |
---|
| 362 | $run = 0; |
---|
| 363 | } |
---|
| 364 | } |
---|
| 365 | } |
---|
| 366 | |
---|
| 367 | $bytes = 0; |
---|
| 368 | undef $nybble_tmp; |
---|
| 369 | |
---|
| 370 | for ( $y = 0 ; $y < $ysize ; $y++ ) { |
---|
| 371 | $last = $prev = 0; |
---|
| 372 | $run = 0; |
---|
| 373 | for ( $x = 0 ; $x < $xsize ; $x++ ) { |
---|
| 374 | $rgb = shift(@data); |
---|
| 375 | $i = $color_index{$rgb} + 0; |
---|
| 376 | if ( $i == $last ) { |
---|
| 377 | $run++; |
---|
| 378 | } else { |
---|
| 379 | output_run($prev, $last, $run); |
---|
| 380 | $prev = $last; |
---|
| 381 | $last = $i; |
---|
| 382 | $run = 1; |
---|
| 383 | } |
---|
| 384 | } |
---|
| 385 | # Output final datum for row; we're always at least one pixel behind |
---|
| 386 | output_run($prev, $last, $run); |
---|
| 387 | output_nybble(undef); # Flush row |
---|
| 388 | } |
---|
| 389 | |
---|
| 390 | $pixels = $xsize * $ysize; |
---|
| 391 | $size = ($pixels+1)/2; |
---|
| 392 | printf STDERR "%d pixels, %d bytes, (%2.2f%% compression)\n", |
---|
| 393 | $pixels, $bytes, 100*($size-$bytes)/$size; |
---|