| #!/usr/bin/perl |
| ## ----------------------------------------------------------------------- |
| ## |
| ## Copyright 2004-2008 H. Peter Anvin - All Rights Reserved |
| ## |
| ## This program is free software; you can redistribute it and/or modify |
| ## it under the terms of the GNU General Public License as published by |
| ## the Free Software Foundation, Inc., 53 Temple Place Ste 330, |
| ## Boston MA 02111-1307, USA; either version 2 of the License, or |
| ## (at your option) any later version; incorporated herein by reference. |
| ## |
| ## ----------------------------------------------------------------------- |
| |
| ## |
| ## ppmtolss16 |
| ## |
| ## Convert a PNM file with max 16 colors to a simple RLE-based format: |
| ## |
| ## uint32 0x1413f33d ; magic (littleendian) |
| ## uint16 xsize ; littleendian |
| ## uint16 ysize ; littleendian |
| ## 16 x uint8 r,g,b ; color map, in 6-bit format (each byte is 0..63) |
| ## |
| ## Then, a sequence of nybbles: |
| ## |
| ## N ... if N is != previous pixel, one pixel of color N |
| ## ... otherwise run sequence follows ... |
| ## M ... if M > 0 then run length is M |
| ## ... otherwise run sequence is encoded in two nybbles, |
| ## littleendian, +16 |
| ## |
| ## The nybble sequences are on a per-row basis; runs may not extend |
| ## across rows and odd-nybble rows are zero-padded. |
| ## |
| ## At the start of row, the "previous pixel" is assumed to be zero. |
| ## |
| ## Usage: |
| ## |
| ## ppmtolss16 [#rrggbb=i ...] < input.ppm > output.rle |
| ## |
| ## Command line options of the form #rrggbb=i indicate that |
| ## the color #rrggbb (hex) should be assigned index i (decimal) |
| ## |
| |
| eval { use bytes; }; |
| eval { binmode STDIN; }; |
| eval { binmode STDOUT; }; |
| |
| $magic = 0x1413f33d; |
| |
| # Get a token from the PPM header. Ignore comments and leading |
| # and trailing whitespace, as is required by the spec. |
| # This routine eats exactly one character of trailing whitespace, |
| # unless it is a comment (in which case it eats the comment up |
| # to and including the end of line.) |
| sub get_token() { |
| my($token, $ch); |
| my($ch); |
| |
| do { |
| $ch = getc(STDIN); |
| return undef if ( !defined($ch) ); # EOF |
| if ( $ch eq '#' ) { |
| do { |
| $ch = getc(STDIN); |
| return undef if ( !defined($ch) ); |
| } while ( $ch ne "\n" ); |
| } |
| } while ( $ch =~ /^[ \t\n\v\f\r]$/ ); |
| |
| $token = $ch; |
| while ( 1 ) { |
| $ch = getc(STDIN); |
| last if ( $ch =~ /^[ \t\n\v\f\r\#]$/ ); |
| $token .= $ch; |
| } |
| if ( $ch eq '#' ) { |
| do { |
| $ch = getc(STDIN); |
| } while ( defined($ch) && $ch ne "\n" ); |
| } |
| return $token; |
| } |
| |
| # Get a token, and make sure it is numeric (and exists) |
| sub get_numeric_token() { |
| my($token) = get_token(); |
| |
| if ( $token !~ /^[0-9]+$/ ) { |
| print STDERR "Format error on input\n"; |
| exit 1; |
| } |
| |
| return $token + 0; |
| } |
| |
| # Must be called before each pixel row is read |
| sub start_new_row() { |
| $getrgb_leftover_bit_cnt = 0; |
| $getrgb_leftover_bit_val = 0; |
| } |
| |
| # Get a single RGB token depending on the PNM type |
| sub getrgb($) { |
| my($form) = @_; |
| my($rgb,$r,$g,$b); |
| |
| if ( $form == 6 ) { |
| # Raw PPM, most common |
| return undef unless ( read(STDIN,$rgb,3) == 3 ); |
| return unpack("CCC", $rgb); |
| } elsif ( $form == 3 ) { |
| # Plain PPM |
| $r = get_numeric_token(); |
| $g = get_numeric_token(); |
| $b = get_numeric_token(); |
| return ($r,$g,$b); |
| } elsif ( $form == 5 ) { |
| # Raw PGM |
| return undef unless ( read(STDIN,$rgb,1) == 1 ); |
| $r = unpack("C", $rgb); |
| return ($r,$r,$r); |
| } elsif ( $form == 2 ) { |
| # Plain PGM |
| $r = get_numeric_token(); |
| return ($r,$r,$r); |
| } elsif ( $form == 4 ) { |
| # Raw PBM |
| if ( !$getrgb_leftover_bit_cnt ) { |
| return undef unless ( read(STDIN,$rgb,1) == 1 ); |
| $getrgb_leftover_bit_val = unpack("C", $rgb); |
| $getrgb_leftover_bit_cnt = 8; |
| } |
| $r = ( $getrgb_leftover_bit_val & 0x80 ) ? 0x00 : 0xff; |
| $getrgb_leftover_bit_val <<= 1; |
| $getrgb_leftover_bit_cnt--; |
| |
| return ($r,$r,$r); |
| } elsif ( $form == 1 ) { |
| # Plain PBM |
| my($ch); |
| |
| do { |
| $ch = getc(STDIN); |
| return undef if ( !defined($ch) ); |
| return (255,255,255) if ( $ch eq '0' ); # White |
| return (0,0,0) if ( $ch eq '1'); # Black |
| if ( $ch eq '#' ) { |
| do { |
| $ch = getc(STDIN); |
| return undef if ( !defined($ch) ); |
| } while ( $ch ne "\n" ); |
| } |
| } while ( $ch =~ /^[ \t\n\v\f\r]$/ ); |
| return undef; |
| } else { |
| die "Internal error: unknown format: $form\n"; |
| } |
| } |
| |
| sub rgbconvert($$$$) { |
| my($r,$g,$b,$maxmult) = @_; |
| my($rgb); |
| |
| $r = int($r*$maxmult); |
| $g = int($g*$maxmult); |
| $b = int($b*$maxmult); |
| $rgb = pack("CCC", $r, $g, $b); |
| return $rgb; |
| } |
| |
| foreach $arg ( @ARGV ) { |
| if ( $arg =~ /^\#([0-9a-f])([0-9a-f])([0-9a-f])=([0-9]+)$/i ) { |
| $r = hex($1) << 4; |
| $g = hex($2) << 4; |
| $b = hex($3) << 4; |
| $i = $4 + 0; |
| } elsif ( $arg =~ /^\#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})=([0-9]+)$/i ) { |
| $r = hex($1); |
| $g = hex($2); |
| $b = hex($3); |
| $i = $4 + 0; |
| } elsif ( $arg =~ /^\#([0-9a-f]{3})([0-9a-f]{3})([0-9a-f]{3})=([0-9]+)$/i ) { |
| $r = hex($1) >> 4; |
| $g = hex($2) >> 4; |
| $b = hex($3) >> 4; |
| $i = $4 + 0; |
| } elsif ( $arg =~ /^\#([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})=([0-9]+)$/i ) { |
| $r = hex($1) >> 8; |
| $g = hex($2) >> 8; |
| $b = hex($3) >> 8; |
| $i = $4 + 0; |
| } else { |
| print STDERR "$0: Unknown argument: $arg\n"; |
| next; |
| } |
| |
| if ( $i > 15 ) { |
| print STDERR "$0: Color index out of range: $arg\n"; |
| next; |
| } |
| |
| $rgb = rgbconvert($r, $g, $b, 64/256); |
| |
| if ( defined($index_forced{$i}) ) { |
| print STDERR "$0: More than one color index $i\n"; |
| exit(1); |
| } |
| $index_forced{$i} = $rgb; |
| $force_index{$rgb} = $i; |
| } |
| |
| $form = get_token(); |
| die "$0: stdin is not a PNM file" if ( $form !~ /^P([1-6])$/ ); |
| $form = $1+0; |
| |
| $xsize = get_numeric_token(); |
| $ysize = get_numeric_token(); |
| if ( $form == 1 || $form == 4 ) { |
| $maxcol = 255; # Internal convention |
| } else { |
| $maxcol = get_numeric_token(); |
| } |
| $maxmult = 64/($maxcol+1); # Equal buckets conversion |
| |
| @data = (); |
| |
| for ( $y = 0 ; $y < $ysize ; $y++ ) { |
| start_new_row(); |
| for ( $x = 0 ; $x < $xsize ; $x++ ) { |
| die "$0: Premature EOF at ($x,$y) of ($xsize,$ysize)\n" |
| if ( !scalar(@pnmrgb = getrgb($form)) ); |
| # Convert to 6-bit representation |
| $rgb = rgbconvert($pnmrgb[0], $pnmrgb[1], $pnmrgb[2], $maxmult); |
| $color_count{$rgb}++; |
| push(@data, $rgb); |
| } |
| } |
| |
| # Sort list of colors according to freqency |
| @colors = sort { $color_count{$b} <=> $color_count{$a} } keys(%color_count); |
| |
| # Now we have our pick of colors. Sort according to intensity; |
| # this is more or less an ugly hack to cover for the fact that |
| # using PPM as input doesn't let the user set the color map, |
| # which the user really needs to be able to do. |
| |
| sub by_intensity() { |
| my($ra,$ga,$ba) = unpack("CCC", $a); |
| my($rb,$gb,$bb) = unpack("CCC", $b); |
| |
| my($ia) = $ra*0.299 + $ga*0.587 + $ba*0.114; |
| my($ib) = $rb*0.299 + $gb*0.587 + $bb*0.114; |
| |
| return ( $ia <=> $ib ) if ( $ia != $ib ); |
| |
| # If same, sort based on RGB components, |
| # with highest priority given to G, then R, then B. |
| |
| return ( $ga <=> $gb ) if ( $ga != $gb ); |
| return ( $ra <=> $rb ) if ( $ra != $rb ); |
| return ( $ba <=> $bb ); |
| } |
| |
| @icolors = sort by_intensity @colors; |
| |
| # Insert forced colors into "final" array |
| @colors = (undef) x 16; |
| foreach $rgb ( keys(%force_index) ) { |
| $i = $force_index{$rgb}; |
| $colors[$i] = $rgb; |
| $color_index{$rgb} = $i; |
| } |
| |
| undef %force_index; |
| |
| # Insert remaining colors in the remaining slots, |
| # in luminosity-sorted order |
| $nix = 0; |
| while ( scalar(@icolors) ) { |
| # Advance to the next free slot |
| $nix++ while ( defined($colors[$nix]) && $nix < 16 ); |
| last if ( $nix >= 16 ); |
| $rgb = shift @icolors; |
| if ( !defined($color_index{$rgb}) ) { |
| $colors[$nix] = $rgb; |
| $color_index{$rgb} = $nix; |
| } |
| } |
| |
| while ( scalar(@icolors) ) { |
| $rgb = shift @icolors; |
| $lost++ if ( !defined($color_index{$rgb}) ); |
| } |
| |
| if ( $lost ) { |
| printf STDERR |
| "$0: Warning: color palette truncated (%d colors ignored)\n", $lost; |
| } |
| |
| undef @icolors; |
| |
| # Output header |
| print pack("Vvv", $magic, $xsize, $ysize); |
| |
| # Output color map |
| for ( $i = 0 ; $i < 16 ; $i++ ) { |
| if ( defined($colors[$i]) ) { |
| print $colors[$i]; |
| } else { |
| # Padding for unused color entries |
| print pack("CCC", 63*$i/15, 63*$i/15, 63*$i/15); |
| } |
| } |
| |
| sub output_nybble($) { |
| my($ny) = @_; |
| |
| if ( !defined($ny) ) { |
| if ( defined($nybble_tmp) ) { |
| $ny = 0; # Force the last byte out |
| } else { |
| return; |
| } |
| } |
| |
| $ny = $ny & 0x0F; |
| |
| if ( defined($nybble_tmp) ) { |
| $ny = ($ny << 4) | $nybble_tmp; |
| print chr($ny); |
| $bytes++; |
| undef $nybble_tmp; |
| } else { |
| $nybble_tmp = $ny; |
| } |
| } |
| |
| sub output_run($$$) { |
| my($last,$this,$run) = @_; |
| |
| if ( $this != $last ) { |
| output_nybble($this); |
| $run--; |
| } |
| while ( $run ) { |
| if ( $run >= 16 ) { |
| output_nybble($this); |
| output_nybble(0); |
| if ( $run > 271 ) { |
| $erun = 255; |
| $run -= 271; |
| } else { |
| $erun = $run-16; |
| $run = 0; |
| } |
| output_nybble($erun); |
| output_nybble($erun >> 4); |
| } else { |
| output_nybble($this); |
| output_nybble($run); |
| $run = 0; |
| } |
| } |
| } |
| |
| $bytes = 0; |
| undef $nybble_tmp; |
| |
| for ( $y = 0 ; $y < $ysize ; $y++ ) { |
| $last = $prev = 0; |
| $run = 0; |
| for ( $x = 0 ; $x < $xsize ; $x++ ) { |
| $rgb = shift(@data); |
| $i = $color_index{$rgb} + 0; |
| if ( $i == $last ) { |
| $run++; |
| } else { |
| output_run($prev, $last, $run); |
| $prev = $last; |
| $last = $i; |
| $run = 1; |
| } |
| } |
| # Output final datum for row; we're always at least one pixel behind |
| output_run($prev, $last, $run); |
| output_nybble(undef); # Flush row |
| } |
| |
| $pixels = $xsize * $ysize; |
| $size = ($pixels+1)/2; |
| printf STDERR "%d pixels, %d bytes, (%2.2f%% compression)\n", |
| $pixels, $bytes, 100*($size-$bytes)/$size; |