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