| 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; |
|---|