File Coverage

blib/lib/Image/TextMode/SAUCE.pm
Criterion Covered Total %
statement 54 66 81.8
branch 13 20 65.0
condition 2 10 20.0
subroutine 12 14 85.7
pod 12 12 100.0
total 93 122 76.2


line stmt bran cond sub pod time code
1             package Image::TextMode::SAUCE;
2              
3 30     30   1214 use Moo;
  30         11929  
  30         140  
4 30     30   8988 use Types::Standard qw( Int Str ArrayRef Bool );
  30         51690  
  30         178  
5              
6             # some SAUCE constants
7             my $SAUCE_ID = 'SAUCE';
8             my $SAUCE_VERSION = '00';
9             my $SAUCE_FILLER = "\0" x 22;
10             my $COMNT_ID = 'COMNT';
11              
12             =head1 NAME
13              
14             Image::TextMode::SAUCE - Create, manipulate and save SAUCE metadata
15              
16             =head1 DESCRIPTION
17              
18             This module reads and writes SAUCE metadata. SAUCE metadata is a 128-byte
19             record stored after an EOF char at the end of a given file.
20              
21             =head1 ACCESSORS
22              
23             =over 4
24              
25             =item * sauce_id - identified at the start of the record (default: SAUCE)
26              
27             =item * version - sauce version (default: 00)
28              
29             =item * title - title of the work
30              
31             =item * author - author name
32              
33             =item * group - group affiliation
34              
35             =item * date - YYYYMMDD date (default: today's date)
36              
37             =item * filesize - the size of the file, less sauce info
38              
39             =item * datatype_id - numeric identifier for the data type
40              
41             =item * filetype_id - numeric identifier for the file sub-type
42              
43             =item * tinfo1 - first slot of filetype-specific info
44              
45             =item * tinfo2 - second slot of filetype-specific info
46              
47             =item * tinfo3 - third slot of filetype-specific info
48              
49             =item * tinfo4 - fourth slot of filetype-specific info
50              
51             =item * comment_count - number of comments stored before the sauce record
52              
53             =item * flags_id - datatype specific flags
54              
55             =item * filler - 22 spaces to fill in the remaining bytes
56              
57             =item * comment_id - identifier for comments section (default: COMNT)
58              
59             =item * comments - array ref of comment lines
60              
61             =item * has_sauce - undef before read; after read: true if file has sauce record
62              
63             =back
64              
65             =cut
66              
67             has 'sauce_id' => ( is => 'rw', isa => Str, default => sub { $SAUCE_ID } );
68              
69             has 'version' =>
70             ( is => 'rw', isa => Str, default => sub { $SAUCE_VERSION } );
71              
72             has 'title' => ( is => 'rw', isa => Str, default => sub { '' } );
73              
74             has 'author' => ( is => 'rw', isa => Str, default => sub { '' } );
75              
76             has 'group' => ( is => 'rw', isa => Str, default => sub { '' } );
77              
78             has 'date' => (
79             is => 'rw',
80             isa => Str,
81             default => sub {
82             my @t = ( localtime )[ 5, 4, 3 ];
83             return sprintf '%4d%02d%02d', 1900 + $t[ 0 ], $t[ 1 ] + 1, $t[ 2 ];
84             }
85             );
86              
87             has 'filesize' => ( is => 'rw', isa => Int, default => 0 );
88              
89             has 'filetype_id' => ( is => 'rw', isa => Int, default => 0 );
90              
91             has 'datatype_id' => ( is => 'rw', isa => Int, default => 0 );
92              
93             has 'tinfo1' => ( is => 'rw', isa => Int, default => 0 );
94              
95             has 'tinfo2' => ( is => 'rw', isa => Int, default => 0 );
96              
97             has 'tinfo3' => ( is => 'rw', isa => Int, default => 0 );
98              
99             has 'tinfo4' => ( is => 'rw', isa => Int, default => 0 );
100              
101             has 'comment_count' => ( is => 'rw', isa => Int, default => 0 );
102              
103             has 'flags_id' => ( is => 'rw', isa => Int, default => 0 );
104              
105             has 'filler' =>
106             ( is => 'rw', isa => Str, default => sub { $SAUCE_FILLER } );
107              
108             has 'comment_id' =>
109             ( is => 'rw', isa => Str, default => sub { $COMNT_ID } );
110              
111             has 'comments' => ( is => 'rw', isa => ArrayRef, default => sub { [] } );
112              
113             has 'has_sauce' => ( is => 'rw', isa => Bool );
114              
115             # define datatypes and filetypes as per SAUCE specs
116             my @datatypes
117             = qw(None Character Graphics Vector Sound BinaryText XBin Archive Executable);
118             my $filetypes = {
119             None => {
120             filetypes => [ 'Undefined' ],
121             flags => [ 'None' ]
122             },
123             Character => {
124             filetypes =>
125             [ qw( ASCII ANSi ANSiMation RIP PCBoard Avatar HTML Source TundraDraw ) ],
126             flags => [ ( 'ANSiFlags' ) x 3, ( 'None' ) x 6 ],
127             tinfo => [
128             ( { tinfo1 => 'Width', tinfo2 => 'Height' } ) x 3,
129             { tinfo1 => 'Width', tinfo2 => 'Height', tinfo3 => 'Colors' },
130             ( { tinfo1 => 'Width', tinfo2 => 'Height' } ) x 2,
131             ( {} ) x 2,
132             { tinfo1 => 'Width', tinfo2 => 'Height' }
133             ]
134             },
135             Bitmap => {
136             filetypes => [
137             qw( GIF PCX LBM/IFF TGA FLI FLC BMP GL DL WPG PNG JPG MPG AVI )
138             ],
139             flags => [ ( 'None' ) x 14 ],
140             tinfo => [
141             ( { tinfo1 => 'Width',
142             tinfo2 => 'Height',
143             tinfo3 => 'Bits Per Pixel'
144             }
145             ) x 14
146             ]
147             },
148             Vector => {
149             filetypes => [ qw( DXF DWG WPG 3DS ) ],
150             flags => [ ( 'None' ) x 4 ],
151             },
152             Audio => {
153             filetypes => [
154             qw( MOD 669 STM S3M MTM FAR ULT AMF DMF OKT ROL CMF MIDI SADT VOC WAV SMP8 SMP8S SMP16 SMP16S PATCH8 PATCH16 XM HSC IT )
155             ],
156             flags => [ ( 'None' ) x 20 ],
157             tinfo => [ ( {} ) x 16, ( { tinfo1 => 'Sampling Rate' } ) x 4 ]
158             },
159             BinaryText => {
160             filetypes => [ qw( Undefined ) ],
161             flags => [ 'ANSiFlags' ],
162             },
163             XBin => {
164             filetypes => [ qw( Undefined ) ],
165             flags => [ 'None' ],
166             tinfo => [ { tinfo1 => 'Width', tinfo2 => 'Height' }, ]
167             },
168             Archive => {
169             filetypes => [ qw( ZIP ARJ LZH ARC TAR ZOO RAR UC2 PAK SQZ ) ],
170             flags => [ ( 'None' ) x 10 ],
171             },
172             Executable => {
173             filetypes => [ qw( Undefined ) ],
174             flags => [ 'None' ],
175             }
176             };
177              
178             # vars for use with pack() and unpack()
179             my $sauce_template = 'A5 A2 A35 A20 A20 A8 V C C v v v v C C Z22';
180             my @sauce_fields
181             = qw( sauce_id version title author group date filesize datatype_id filetype_id tinfo1 tinfo2 tinfo3 tinfo4 comment_count flags_id filler );
182             my $comnt_template = 'A5 A64';
183             my @comnt_fields = qw( comment_id comments );
184              
185             =head1 METHODS
186              
187             =head2 new( %args )
188              
189             Creates a new SAUCE metadata instance.
190              
191             =head2 read( $fh )
192              
193             Read the sauce record from C<$fh>.
194              
195             =cut
196              
197             sub read { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
198 40     40 1 87221 my ( $self, $fh ) = @_;
199              
200 40         74 my $buffer;
201             my %info;
202              
203 40         186 seek( $fh, 0, 2 );
204 40 100       199 return if tell $fh < 128;
205              
206 25         52 seek( $fh, -128, 2 );
207 25         414 my $size = read( $fh, $buffer, 128 );
208              
209             # Check for "SAUCE00" header
210 25 100       148 if ( substr( $buffer, 0, 7 ) ne "$SAUCE_ID$SAUCE_VERSION" ) {
211 22         255 $self->has_sauce( 0 );
212 22         6284 return;
213             }
214              
215 3         73 @info{ @sauce_fields } = unpack( $sauce_template, $buffer );
216              
217             # Do we have any comments?
218 3         12 my $comment_count = $info{ comment_count };
219              
220 3         55 $self->$_( $info{ $_ } ) for keys %info;
221 3         21737 $self->has_sauce( 1 );
222              
223 3 100       655 if ( $comment_count > 0 ) {
224 2         13 seek( $fh, -128 - 5 - $comment_count * 64, 2 );
225 2         18 read( $fh, $buffer, 5 + $comment_count * 64 );
226              
227 2 50       10 if ( substr( $buffer, 0, 5 ) eq $COMNT_ID ) {
228 2         13 my $template
229             = $comnt_template
230             . ( split( / /s, $comnt_template ) )[ 1 ]
231             x ( $comment_count - 1 );
232 2         13 my ( $id, @comments ) = unpack( $template, $buffer );
233 2         11 $self->comment_id( $id );
234 2         1072 $self->comments( \@comments );
235             }
236             }
237             }
238              
239             =head2 write( $fh )
240              
241             Write the sauce record to C<$fh>.
242              
243             =cut
244              
245             sub write { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
246 2     2 1 3663 my ( $self, $fh ) = @_;
247              
248 2         7 seek( $fh, 0, 2 );
249 2         6 print $fh chr( 26 );
250              
251             # comments...
252 2         3 my $comments = scalar @{ $self->comments };
  2         28  
253 2 100       584 if ( $comments ) {
254 1         19 print $fh pack(
255             $comnt_template
256             . (
257             ( split( / /s, $comnt_template ) )[ 1 ] x ( $comments - 1 )
258             ),
259             $self->comment_id,
260 1         23 @{ $self->comments }
261             );
262             }
263              
264             # SAUCE...
265 2         24 my @template = split( / /s, $sauce_template );
266 2         7 for ( 0 .. $#sauce_fields ) {
267 32         37 my $field = $sauce_fields[ $_ ];
268 32 50       547 my $value = ( $field ne 'comments' ) ? $self->$field : $comments;
269 32         242 print $fh pack( $template[ $_ ], $value );
270             }
271              
272             }
273              
274             =head2 record_size( )
275              
276             Return the size of the SAUCE record in bytes.
277              
278             =cut
279              
280             sub record_size {
281 1     1 1 6 my $self = shift;
282              
283 1 50       16 return 0 unless $self->has_sauce;
284              
285 1         7 my $size = 128;
286              
287 1 50       18 if( $self->comment_count ) {
288 1         22 $size += 5 + ( 64 * $self->comment_count );
289             }
290              
291 1         6 return $size;
292             }
293              
294             =head2 datatype( )
295              
296             The string name of the data represented in datatype_id.
297              
298             =cut
299              
300             sub datatype {
301 14   50 14 1 1273 return $datatypes[ $_[ 0 ]->datatype_id || 0 ];
302             }
303              
304             =head2 filetype( )
305              
306             The string name of the data represented in filetype_id.
307              
308             =cut
309              
310             sub filetype {
311             # Filetype for "BinaryText" (id: 5) is used to encode the image width
312 2 50   2 1 2101 if( $_[ 0 ]->datatype_id == 5 ) {
313 0         0 return 'Undefined';
314             }
315              
316             return $filetypes->{ $_[ 0 ]->datatype }->{ filetypes }
317 2   50     18 ->[ $_[ 0 ]->filetype_id || 0 ];
318             }
319              
320             =head2 flags( )
321              
322             The string name of the data represented in flags_id.
323              
324             =cut
325              
326             sub flags {
327             return $filetypes->{ $_[ 0 ]->datatype }->{ flags }
328 2     2 1 2062 ->[ $_[ 0 ]->filetype_id ];
329             }
330              
331             =head2 tinfo1_name( )
332              
333             The string name of the data represented in tinfo1.
334              
335             =cut
336              
337             sub tinfo1_name {
338             return $filetypes->{ $_[ 0 ]->datatype }->{ tinfo }
339 2     2 1 3223 ->[ $_[ 0 ]->filetype_id ]->{ tinfo1 };
340             }
341              
342             =head2 tinfo2_name( )
343              
344             The string name of the data represented in tinfo2.
345              
346             =cut
347              
348             sub tinfo2_name {
349             return $filetypes->{ $_[ 0 ]->datatype }->{ tinfo }
350 2     2 1 1584 ->[ $_[ 0 ]->filetype_id ]->{ tinfo2 };
351             }
352              
353             =head2 tinfo3_name( )
354              
355             The string name of the data represented in tinfo3.
356              
357             =cut
358              
359             sub tinfo3_name {
360             return $filetypes->{ $_[ 0 ]->datatype }->{ tinfo }
361 2     2 1 781 ->[ $_[ 0 ]->filetype_id ]->{ tinfo3 };
362             }
363              
364             =head2 tinfo4_name( )
365              
366             The string name of the data represented in tinfo4.
367              
368             =cut
369              
370             sub tinfo4_name {
371             return $filetypes->{ $_[ 0 ]->datatype }->{ tinfo }
372 2     2 1 1746 ->[ $_[ 0 ]->filetype_id ]->{ tinfo4 };
373             }
374              
375             =head2 tinfos( )
376              
377             An alias for filler() to match the SAUCE 00.5 specification. The value may be
378             a font name for ASCII, ANSI, ANSiMation, and BinaryText files.
379              
380             =cut
381              
382             sub tinfos {
383 0     0 1   shift->filler( @_ );
384             }
385              
386             =head2 parse_ansiflags( )
387              
388             For filetypes that support it, extract the metadata embeded in the flags.
389             Currently, those fields are:
390              
391             =over 4
392              
393             =item * blink_mode
394              
395             =item * 9th_bit
396              
397             =item * dos_aspect
398              
399             =back
400              
401             =cut
402              
403             sub parse_ansiflags {
404 0     0 1   my $self = shift;
405 0           my $flags = {};
406              
407 0           my $dt = $self->datatype_id;
408 0           my $ft = $self->filetype_id;
409 0 0 0       return $flags unless $dt == 5 || ( $dt == 1 && $ft <= 2 );
      0        
410              
411 0           my $fid = $self->flags_id;
412 0           $flags->{ 'blink_mode' } = ($fid & 1) ^ 1;
413 0           $flags->{ '9th_bit' } = ($fid & 6) == 4;
414 0           $flags->{ 'dos_aspect' } = ($fid & 24) == 8;
415              
416 0           return $flags;
417             }
418              
419             =head1 SEE ALSO
420              
421             =over 4
422              
423             =item * http://www.acid.org/info/sauce/sauce.htm
424              
425             =back
426              
427             =head1 AUTHOR
428              
429             Brian Cassidy Ebricas@cpan.orgE
430              
431             =head1 COPYRIGHT AND LICENSE
432              
433             Copyright 2008-2014 by Brian Cassidy
434              
435             This library is free software; you can redistribute it and/or modify
436             it under the same terms as Perl itself.
437              
438             =cut
439              
440             1;