File Coverage

lib/File/Information/Deep.pm
Criterion Covered Total %
statement 20 420 4.7
branch 0 240 0.0
condition 0 119 0.0
subroutine 7 27 25.9
pod 2 2 100.0
total 29 808 3.5


line stmt bran cond sub pod time code
1             # Copyright (c) 2025 Philipp Schafft <lion@cpan.org>
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: generic module for extracting information from filesystems
6              
7              
8             package File::Information::Deep;
9              
10 3     3   63 use v5.20;
  3         16  
11 3     3   22 use strict;
  3         7  
  3         102  
12 3     3   16 use warnings;
  3         6  
  3         237  
13              
14 3     3   50 use parent 'File::Information::Base';
  3         7  
  3         35  
15              
16 3     3   305 use Carp;
  3         7  
  3         340  
17 3     3   40 use Scalar::Util qw(weaken);
  3         6  
  3         197  
18 3     3   21 use Fcntl qw(SEEK_SET);
  3         6  
  3         34013  
19              
20             our $VERSION = v0.16;
21              
22             my %_PNG_colour_types = ( # namespace: 4c11d438-f6f3-417f-85e3-e56e46851dae
23             0 => {ise => 'a3934b85-5bec-5cd7-a571-727e4cecfcb1', displayname => 'Greyscale'},
24             2 => {ise => '56262598-1d35-566d-b9a3-0e752d58b8ce', displayname => 'Truecolor'},
25             3 => {ise => '67f61b65-4978-510b-b318-247da7934837', displayname => 'Indexed-color'},
26             4 => {ise => 'cbdafa4e-1cb8-59a9-b6ec-b7a1bef3fcd4', displayname => 'Greyscale with alpha'},
27             6 => {ise => 'c6ef9ba0-3b7f-5248-a4f4-18e39c14d7b3', displayname => 'Truecolor with alpha'},
28             );
29              
30             my %_PNG_filter_method = ( # namespace: 06f15860-8191-41f5-881c-a465be563089
31             0 => {ise => 'b7a197cb-2eee-517f-ae57-8e299d1a92e9', displayname => 'None'},
32             1 => {ise => 'c194fcce-c957-5436-861f-09af8526fed8', displayname => 'Sub'},
33             2 => {ise => 'fe14ef2d-4098-5a7e-81a0-88beae0e1e65', displayname => 'Up'},
34             3 => {ise => 'b0df25b4-b1fb-52cc-a6be-162440bd9628', displayname => 'Average'},
35             4 => {ise => '974cf00a-c2e2-5d08-b1da-08169e09b173', displayname => 'Paeth'},
36             );
37              
38             my %_PNG_compression_method = ( # namespace: b2b8b4bf-3b0f-4037-9bbc-96e6b53ae73d
39             0 => {ise => 'f47c8ff3-5218-555d-bf89-ba30706c29e1', displayname => 'deflate'},
40             );
41              
42             my %_vmv0_section_types = (
43             1 => {ise => 'bc0dc85a-8c72-5ab6-a60b-377fdf76f678', displayname => 'init'},
44             2 => {ise => '18b7bfe0-5e3a-5fe4-ad69-a317e6b2445c', displayname => 'header'},
45             3 => {ise => '5460c878-23d6-56b9-8600-9375d76fefc5', displayname => 'rodata'},
46             4 => {ise => '0520d8d6-3a85-56d2-ae2b-77c517cff2ce', displayname => 'text'},
47             5 => {ise => '95f7f330-a72d-5e0b-ab0f-d46f37edbc9a', displayname => 'trailer'},
48             6 => {ise => '9bbc79eb-5a31-5797-8a05-56e58c530289', displayname => 'resources'},
49             );
50              
51             # Extra tags that do not belong into one of the other lists.
52             my %_wk = (
53             '.section' => {ise => 'dad2de0d-9711-5b57-9a31-562122d756ba', displayname => '.section'},
54             '.chunk' => {ise => 'bff479fa-a818-58dc-b5df-539852fa8b80', displayname => '.chunk'},
55             );
56              
57             my %_properties = (
58             pdf_version => {loader => \&_load_pdf},
59             pdf_pages => {loader => \&_load_pdf},
60             odf_keywords => {loader => \&_load_odf},
61             data_uriid_barcodes => {loader => \&_load_barcodes, rawtype => 'Data::URIID::Barcode'},
62             vmv0_filesize => {loader => \&_load_vmv0},
63             vmv0_section_pointer => {loader => \&_load_vmv0},
64             vmv0_section => {loader => \&_load_vmv0, rawtype => 'File::Information::Chunk'},
65             vmv0_minimum_handles => {loader => \&_load_vmv0},
66             vmv0_minimum_ram => {loader => \&_load_vmv0},
67             vmv0_boundary_text => {loader => \&_load_vmv0},
68             vmv0_boundary_load => {loader => \&_load_vmv0},
69             png_ihdr_width => {loader => \&_load_png},
70             png_ihdr_height => {loader => \&_load_png},
71             png_ihdr_bit_depth => {loader => \&_load_png},
72             png_ihdr_color_type => {loader => \&_load_png},
73             png_ihdr_compression_method => {loader => \&_load_png},
74             png_ihdr_filter_method => {loader => \&_load_png},
75             png_ihdr_interlace_method => {loader => \&_load_png},
76             gif_screen_width => {loader => \&_load_gif},
77             gif_screen_height => {loader => \&_load_gif},
78             gpl_palette_name => {loader => \&_load_gpl},
79             gpl_palette_columns => {loader => \&_load_gpl},
80             gpl_palette_colours => {loader => \&_load_gpl},
81             rgbtxt_palette_colours => {loader => \&_load_rgbtxt},
82             libpng_ihdr_width => {loader => \&_load_libpng},
83             libpng_ihdr_height => {loader => \&_load_libpng},
84             libpng_ihdr_color_type => {loader => \&_load_libpng},
85             libpng_plte_colours => {loader => \&_load_libpng},
86             );
87              
88             my %_vmv0_code_P1_info = (
89             0 => 'vmv0_filesize',
90             2 => 'vmv0_minimum_handles',
91             3 => 'vmv0_minimum_ram',
92             4 => 'vmv0_boundary_text',
93             5 => 'vmv0_boundary_load',
94             );
95              
96             my @_odf_medadata_keys = qw(title description subject creator language initial_creator editing_cycles editing_duration generator creation_date date);
97             my @_image_info_keys = qw(height width file_media_type file_ext color_type resolution SamplesPerPixel BitsPerSample Comment Interlace Compression Gamma LastModificationTime);
98             my @_image_extra_keys = qw(Thumb::URI Thumb::Image::Width Thumb::Image::Height Thumb::MTime Software);
99             my @_dynamic_loaders = (\&_load_odf, \&_load_audio_scan);
100              
101             my %_audio_scan_tags = (
102             vorbiscomments => {
103             title => 'title',
104             },
105             riffwave => {
106             title => 'inam',
107             },
108             id3 => {
109             title => 'tit2',
110             },
111             );
112              
113             foreach my $keyword (qw(Author CreationDate ModDate Creator Producer Title Subject Keywords)) {
114             $_properties{'pdf_info_'.lc($keyword)} = {loader => \&_load_pdf};
115             }
116             foreach my $keyword (qw(CreationDate ModDate)) {
117             $_properties{'pdf_info_'.lc($keyword)}{parsing} = 'pdf_date';
118             }
119              
120             foreach my $key (@_odf_medadata_keys) {
121             $_properties{'odf_info_'.$key} = {loader => \&_load_odf};
122             }
123             foreach my $key (qw(creation_date date)) {
124             $_properties{'odf_info_'.$key}{parsing} = 'iso8601';
125             }
126              
127             foreach my $key (@_image_info_keys) {
128             $_properties{'image_info_'.lc($key)} = {loader => \&_load_image_info};
129             }
130             foreach my $key (@_image_extra_keys) {
131             $_properties{'image_info_extra_'.lc($key =~ s/::/_/r)} = {loader => \&_load_image_info};
132             }
133             $_properties{image_info_extra_thumb_mtime}{rawtype} = 'unixts';
134             $_properties{image_info_extra_thumb_uri}{rawtype} = 'uri';
135              
136              
137             # Register well known:
138             foreach my $value (
139             values(%_PNG_colour_types),
140             values(%_PNG_filter_method),
141             values(%_PNG_compression_method),
142             values(%_vmv0_section_types),
143             values(%_wk),
144             ) {
145             Data::Identifier->new(ise => $value->{ise}, displayname => $value->{displayname})->register;
146             }
147              
148              
149             #@returns File::Information::Base
150             sub parent {
151 0     0 1   my ($self) = @_;
152 0           return $self->{parent};
153             }
154              
155             # ----------------
156             sub property_info {
157 0     0 1   my ($self, @args) = @_;
158              
159 0 0         unless (defined $self->{_dynamic}) {
160 0           $self->{_dynamic} = 1;
161 0           foreach my $cb (@_dynamic_loaders) {
162 0           $self->$cb('__dummy__');
163             }
164             }
165              
166 0           return $self->SUPER::property_info(@args);
167             }
168              
169             sub _new {
170 0     0     my ($pkg, %opts) = @_;
171 0           my $self = $pkg->SUPER::_new(%opts, properties => \%_properties);
172 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
173 0           my $parent = $self->{parent};
174              
175 0           weaken($self->{parent});
176              
177             # copy a few critical values:
178 0           $pv->{contentise} = {raw => $parent->get('contentise', lifecycle => 'current', as => 'uuid')};
179 0           eval { $pv->{mediatype} = {raw => $parent->get('mediatype', lifecycle => 'current', as => 'mediatype')} };
  0            
180              
181 0           return $self;
182             }
183              
184             sub _dynamic_property {
185 0     0     my ($self, $prefix, $property) = @_;
186 0           my $key;
187              
188 0           $property = lc($property);
189 0           $property =~ s/::/_/g;
190 0           $property =~ s/[^a-z0-9]/_/g;
191 0   0       $_properties{$key = $prefix.'_'.$property} //= {};
192              
193 0           return $key;
194             }
195              
196             sub _check_mediatype {
197 0     0     my ($self, @mediasubtypes) = @_;
198 0           my $v;
199              
200 0 0         return undef unless defined $self->{properties_values}{current}{mediatype}{raw};
201              
202 0           $v = $self->{properties_values}{current}{mediatype}{raw};
203              
204 0           foreach my $mediasubtype (@mediasubtypes) {
205 0 0         return 1 if $v eq $mediasubtype;
206             }
207              
208 0           return undef;
209             }
210              
211             sub _pdf_extract_date {
212 0     0     my ($self, $value) = @_;
213 0           require DateTime::Format::Strptime;
214              
215 0           state $pdf_date_core_pattern = '%Y%m%d%H%M%S';
216 0           state $pdf_date_format_0 = DateTime::Format::Strptime->new('pattern' => $pdf_date_core_pattern, 'time_zone' => 'UTC');
217 0           my $dt;
218             my $core;
219 0           my $parser;
220              
221             # General format: D:YYYYMMDDHHmmSSOHH'mm'
222              
223 0 0         if (($core) = $value =~ /^D:([0-9]{14})Z'{0,2}$/) {
    0          
224 0           $parser = $pdf_date_format_0;
225             } elsif (my ($mycore, $tz_dir, $tz_h, $tz_m) = $value =~ /^D:([0-9]{14})(\+|\-)([0-9]{2})'([0-9]{2})'$/) {
226 0           my $tz = sprintf('%s%s%s', $tz_dir, $tz_h, $tz_m);
227 0           $core = $mycore;
228 0           $parser = DateTime::Format::Strptime->new('pattern' => $pdf_date_core_pattern, 'time_zone' => $tz);
229             }
230              
231 0 0 0       return undef unless defined($core) && defined($parser);
232              
233 0           return $parser->parse_datetime($core);
234             }
235              
236             sub _load_pdf {
237 0     0     my ($self, $key, %opts) = @_;
238 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
239              
240 0 0         return if defined $self->{_loaded_pdf};
241 0           $self->{_loaded_pdf} = 1;
242              
243 0 0         return unless defined $self->{path};
244 0 0         return unless $self->_check_mediatype('application/pdf');
245              
246             # Check for module;
247 0 0         if (eval {
248 0           require PDF::API2;
249 0           PDF::API2->VERSION(2.044);
250 0           PDF::API2->import();
251 0           1;
252             }) {
253 0           my $pdf = PDF::API2->open($self->{path});
254 0           my %info = $pdf->info_metadata();
255              
256 0           $pv->{pdf_version} = {raw => $pdf->version};
257 0           $pv->{pdf_pages} = {raw => $pdf->page_count};
258              
259 0           foreach my $key (keys %info) {
260 0 0         if (defined(my $value = $info{$key})) {
261 0           my $pv_key = 'pdf_info_'.lc($key);
262              
263 0 0 0       $value = $self->_pdf_extract_date($value) if ($_properties{$pv_key}{parsing} // '') eq 'pdf_date';
264 0           $pv->{$pv_key} = {raw => $value};
265             }
266             }
267              
268 0           $pdf->close;
269             }
270             }
271              
272             sub _load_odf {
273 0     0     my ($self, $key, %opts) = @_;
274 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
275              
276 0 0         return if defined $self->{_loaded_odf};
277 0           $self->{_loaded_odf} = 1;
278              
279 0 0         return unless defined $self->{path};
280 0 0         return unless $self->_check_mediatype(qw(application/vnd.oasis.opendocument.text));
281              
282             # Check for module;
283 0 0         if (eval {
284 0           require OpenOffice::OODoc;
285 0           require DateTime::Format::ISO8601;
286 0           OpenOffice::OODoc->import();
287 0           DateTime::Format::ISO8601->import();
288 0           1;
289             }) {
290 0           my $document = odfDocument(file => $self->{path});
291 0           my $meta = odfMeta(file => $document);
292              
293 0           foreach my $key (@_odf_medadata_keys) {
294 0           my $func = $meta->can($key);
295 0           my $value = $meta->$func();
296 0           my $pv_key = 'odf_info_'.$key;
297 0 0 0       next unless defined($value) && length($value);
298              
299 0 0 0       $value = DateTime::Format::ISO8601->parse_datetime($value) if ($_properties{$pv_key}{parsing} // '') eq 'iso8601';
300              
301 0           $pv->{$pv_key} = {raw => $value};
302             }
303              
304 0           $pv->{odf_keywords} = [map {{raw => $_}} $meta->keywords];
  0            
305 0 0         delete $pv->{odf_keywords} unless scalar @{$pv->{odf_keywords}};
  0            
306              
307             {
308 0           my %stats = $meta->statistic;
  0            
309 0           foreach my $key (keys %stats) {
310 0           my $pv_key = $self->_dynamic_property(odf_stats => $key);
311 0           my $value = $stats{$key};
312 0 0 0       next unless defined($value) && length($value);
313 0           $pv->{$pv_key} = {raw => $value};
314             }
315             }
316              
317 0           foreach my $el ($meta->getUserPropertyElements) {
318 0           my $pv_key = $self->_dynamic_property(odf_user_properties => $el->att('meta:name'));
319 0           my $value = $el->text;
320 0           $pv->{$pv_key} = {raw => $value};
321             }
322             }
323             }
324              
325             sub _load_vmv0_decode_opcode {
326 0     0     my ($self, $in) = @_;
327 0           my ($op0, $op1) = unpack('CC', $in);
328 0           my $code = ($op0 & 0370) >> 3;
329 0           my $P = ($op0 & 0007) >> 0;
330 0           my $codeX = ($op1 & 0300) >> 6;
331 0           my $S = ($op1 & 0070) >> 3;
332 0           my $T = ($op1 & 0007) >> 0;
333              
334 0           return {code => $code, P => $P, codeX => $codeX, S => $S, T => $T, first => $op0, second => $op1};
335             }
336              
337             sub _load_vmv0__load_chunks {
338 0     0     my ($self, %opts) = @_;
339 0           my $fh = delete $opts{fh};
340 0           my $start = delete $opts{start};
341 0           my @res;
342              
343 0 0         return undef unless $fh;
344              
345 0           while (1) {
346 0 0         $fh->seek($start, SEEK_SET) or die $!;
347 0 0         if (read($fh, my $in, 2) != 2) {
348 0           last;
349             } else {
350 0           my $opcode = $self->_load_vmv0_decode_opcode($in);
351 0           my $opcode_length = 2;
352 0           my $outer_length;
353             my $inner_offset;
354 0           my $length;
355 0           my %new_opts = %opts;
356 0           my $flags;
357             my $type;
358 0           my $identifier;
359              
360 0 0 0       last unless $opcode->{first} == 6 && $opcode->{codeX} == 0 && $opcode->{S} && $opcode->{T} < 4;
      0        
      0        
361              
362             # Read length:
363 0 0         if ($opcode->{T} == 1) {
    0          
364 0 0         last unless read($fh, $in, 2) == 2;
365 0           $length = unpack('n', $in) * 2;
366 0           $opcode_length += 2;
367             } elsif ($opcode->{T} == 2) {
368 0 0         last unless read($fh, $in, 4) == 4;
369 0           $length = unpack('N', $in) * 2;
370 0           $opcode_length += 4;
371             }
372              
373 0 0         next unless defined $length;
374 0           $inner_offset = $opcode_length;
375              
376             # Read flags and type:
377 0 0         last unless read($fh, $in, 4) == 4;
378 0           ($flags, $type) = unpack('nn', $in);
379 0           $inner_offset += 4;
380              
381             # Read identifier (if any):
382 0 0         if ($flags & 0x0002) {
383 0 0         last unless read($fh, $in, 2) == 2;
384 0           $identifier = unpack('n', $in);
385 0           $inner_offset += 2;
386             }
387              
388 0 0         if (($flags & 0xC000) == 0x0000) { # SNI
    0          
389 0           $type = Data::Identifier->new('039e0bb7-5dd3-40ee-a98c-596ff6cce405' => $type);
390             } elsif (($flags & 0xC000) == 0x4000) { # SID
391 0           $type = Data::Identifier->new(sid => $type);
392             } else {
393 0           $type = undef;
394             }
395              
396 0           $outer_length = $length + $opcode_length;
397             push(@res, File::Information::Chunk->_new(%opts,
398             start => $start,
399             size => $outer_length,
400             outer_type => {ise => $_wk{'.chunk'}->{ise}},
401 0 0         inner_type => {raw => $type, ise => $type->ise},
402             inner_start => $start + $inner_offset,
403             inner_size => $outer_length - $inner_offset - ($flags & 0x1 ? 1 : 0),
404             ));
405              
406 0           $start += $outer_length;
407             }
408             }
409              
410 0 0         return undef unless scalar @res;
411 0           return \@res;
412             }
413              
414             sub _load_vmv0__chunk {
415 0     0     my ($self, %opts) = @_;
416              
417 0 0         if (defined(my $fh = delete $opts{fh})) {
418 0 0         $fh->seek($opts{start}, SEEK_SET) or die $!;
419 0 0         if (read($fh, my $in, 2) == 2) {
420 0           my $opcode = $self->_load_vmv0_decode_opcode($in);
421 0 0 0       if ($opcode->{first} == 0 && $opcode->{codeX} == 0 && ($opcode->{T} & 0x4)) {
      0        
422 0           my $n = $opcode->{T} - 4;
423              
424 0 0         if ($n > 0) {
425 0           $n *= 2;
426 0 0         (read($fh, my $magic, $n) == $n) or croak 'IO error: Cannot read '.$n.' bytes';
427 0 0         if (length($magic) == $n) {
428 0           $opts{outer_magic} = {raw => $magic};
429             }
430             }
431              
432 0           my $section_type = $opcode->{S};
433 0 0 0       if ($section_type == 5 || $section_type == 6) {
434 0           $opts{subchunks} = $self->_load_vmv0__load_chunks(%opts, start => $fh->tell, fh => $fh);
435             }
436 0           $opts{outer_type} = {ise => $_wk{'.section'}->{ise}};
437 0           $opts{inner_type} = {ise => $_vmv0_section_types{$section_type}->{ise}};
438             }
439             }
440             }
441              
442 0           return File::Information::Chunk->_new(%opts);
443             }
444             sub _load_vmv0 {
445 0     0     my ($self, $key, %opts) = @_;
446 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
447              
448 0 0         return if defined $self->{_loaded_vmv0};
449 0           $self->{_loaded_vmv0} = 1;
450              
451 0 0         return unless $self->_check_mediatype(qw(application/vnd.sirtx.vmv0));
452              
453             {
454 0           my $inode = $self->parent->inode;
  0            
455 0           my $data = $inode->peek(wanted => 1024);
456 0           my %section_pointer;
457              
458 0           while (length($data)) {
459 0           my $opcode = $self->_load_vmv0_decode_opcode(substr($data, 0, 2, ''));
460 0           my ($op0, $op1, $code, $P, $codeX, $S, $T) = $opcode->@{qw(first second code P codeX S T)};
461 0           my $extra_as_num;
462 0           my $extra_len = 0;
463 0           my $extra;
464              
465 0 0         last if $code != 0;
466 0 0         last if $P > 2;
467 0 0         last if $codeX != 0;
468              
469 0 0 0       last if $op0 == 0 && $codeX == 0 && $S > 2; # last on non-opcode sections
      0        
470              
471 0           $extra_len = ($T & 0x3) * 2;
472              
473 0           $extra = substr($data, 0, $extra_len, '');
474              
475 0 0         if ($extra_len == 0) {
    0          
    0          
476 0           $extra_as_num = 0;
477             } elsif ($extra_len == 2) {
478 0           $extra_as_num = unpack('n', $extra);
479             } elsif ($extra_len == 4) {
480 0           $extra_as_num = unpack('N', $extra);
481             }
482              
483 0 0         if ($code == 0) {
484 0 0         if ($P == 1) {
485 0 0         if ($codeX == 0) {
486 0 0         if (defined $extra_as_num) {
487 0 0         if (defined(my $f = $_vmv0_code_P1_info{$S})) {
    0          
488 0           $pv->{$f} = {raw => $extra_as_num*2};
489             } elsif ($S == 1) {
490 0   0       $section_pointer{$extra_as_num*2} //= undef;
491             }
492             }
493             }
494             }
495             }
496              
497             #warn sprintf('[code=%u, P=%u; codeX=%u, S=%u, T=%u; extra_len=%u, extra_as_num=%s]', $code, $P, $codeX, $S, $T, $extra_len, $extra_as_num // '<undef>');
498             }
499              
500 0 0         if (scalar keys %section_pointer) {
501 0           my @pointers = sort {$a <=> $b} keys %section_pointer;
  0            
502 0           my $fh = $inode->_get_fh;
503 0           my @sections;
504             my $last;
505              
506 0           $pv->{vmv0_section_pointer} = [map {{raw => $_}} @pointers];
  0            
507              
508 0           require File::Information::Chunk;
509              
510 0           foreach my $c (@pointers) {
511 0 0         if (defined $last) {
512 0           push(@sections, $self->_load_vmv0__chunk(instance => $self->instance, path => $self->{path}, parent => $self, inode => $inode, start => $last, end => $c, fh => $fh));
513             }
514 0           $last = $c;
515             }
516 0 0         if (defined $last) {
517 0           push(@sections, $self->_load_vmv0__chunk(instance => $self->instance, path => $self->{path}, parent => $self, inode => $inode, start => $last, end => scalar($inode->get('size')), fh => $fh));
518             }
519 0           $pv->{vmv0_section} = [map {{raw => $_}} @sections];
  0            
520             }
521             }
522             }
523              
524             sub _load_png {
525 0     0     my ($self, $key, %opts) = @_;
526 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
527              
528 0 0         return if defined $self->{_loaded_png};
529 0           $self->{_loaded_png} = 1;
530              
531 0 0         return unless $self->_check_mediatype(qw(image/png));
532              
533             {
534 0           my $inode = $self->parent->inode;
  0            
535 0           my $data = $inode->peek(wanted => 1024);
536              
537 0 0         if (substr($data, 0, 8) eq "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a") {
538 0 0         if (substr($data, 8, 8) eq "\0\0\0\x0dIHDR") {
539 0   0       my $crc = eval {require Digest::CRC; Digest::CRC->new(type => 'crc32');} // eval { require Digest; Digest->new('CRC-32'); } // croak 'No CRC-32 support';
  0   0        
  0            
  0            
  0            
540 0           $crc->add(substr($data, 8 + 4, 4 + 13));
541 0 0         if (substr($data, 16 + 13, 4) eq pack('H8', $crc->hexdigest)) {
542 0           my ($width, $height, $bit_depth, $color_type, $compression_method, $filter_method, $interlace_method) = unpack('NNCCCCC', substr($data, 16, 13));
543 0           $pv->{png_ihdr_width} = {raw => $width};
544 0           $pv->{png_ihdr_height} = {raw => $height};
545 0           $pv->{png_ihdr_bit_depth} = {raw => $bit_depth};
546 0           $pv->{png_ihdr_interlace_method} = {raw => $interlace_method};
547 0 0         if (defined(my $ct = $_PNG_colour_types{$color_type})) {
548 0           $pv->{png_ihdr_color_type} = {raw => $color_type, ise => $ct->{ise}};
549             }
550 0 0         if (defined(my $fm = $_PNG_filter_method{$filter_method})) {
551 0           $pv->{png_ihdr_filter_method} = {raw => $filter_method, ise => $fm->{ise}};
552             }
553 0 0         if (defined(my $cm = $_PNG_compression_method{$compression_method})) {
554 0           $pv->{png_ihdr_compression_method} = {raw => $compression_method, ise => $cm->{ise}};
555             }
556             }
557             }
558             }
559             }
560             }
561              
562             sub _load_gif {
563 0     0     my ($self, $key, %opts) = @_;
564 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
565              
566 0 0         return if defined $self->{_loaded_gif};
567 0           $self->{_loaded_gif} = 1;
568              
569 0 0         return unless $self->_check_mediatype(qw(image/gif));
570              
571             {
572 0           my $inode = $self->parent->inode;
  0            
573 0           my $data = $inode->peek(wanted => 1024);
574              
575 0 0         if (substr($data, 0, 6) eq 'GIF89a') { # TODO: check if the following code also holds true for GIF87a
576 0           my ($width, $height) = unpack('vv', substr($data, 6, 4));
577 0 0         $pv->{gif_screen_width} = {raw => $width} if $width > 0;
578 0 0         $pv->{gif_screen_height} = {raw => $height} if $height > 0;
579             }
580             }
581             }
582              
583             sub _load_gpl {
584 0     0     my ($self, $key, %opts) = @_;
585 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
586              
587 0 0         return if defined $self->{_loaded_gpl};
588 0           $self->{_loaded_gpl} = 1;
589              
590             {
591 0           my $inode = $self->parent->inode;
  0            
592 0           my $data = $inode->peek(wanted => 64);
593              
594 0 0         if ($data =~ /^GIMP Palette\r?\n/) {
595 0           my $fh = $inode->_get_fh;
596 0           my @colours;
597              
598 0 0         return unless eval { require Data::URIID::Colour; 1; };
  0            
  0            
599              
600 0           while (defined(my $line = <$fh>)) {
601 0           $line =~ s/\r?\n$//;
602 0           $line =~ s/^\s*#.*$//;
603 0 0         next unless length($line);
604 0 0         if ($line eq 'GIMP Palette') {
    0          
    0          
    0          
    0          
605             # magic, good, no-op
606             } elsif ($line =~ /^Name:\s+(\S.+)$/) {
607 0           $pv->{gpl_palette_name} = {raw => $1};
608             } elsif ($line =~ /^Columns:\s+([1-9][0-9]*)$/) {
609 0           $pv->{gpl_palette_columns} = {raw => int($1)};
610             } elsif ($line =~ /^(0|[1-9][0-9]*)\s+(0|[1-9][0-9]*)\s+(0|[1-9][0-9]*)\s+(\S(?:.*\S)?)$/) {
611 0           push(@colours, {raw => Data::URIID::Colour->new(
612             rgb => sprintf('#%02x%02x%02x', $1, $2, $3),
613             displayname => $4,
614             )});
615             } elsif ($line =~ /^(0|[1-9][0-9]*)\s+(0|[1-9][0-9]*)\s+(0|[1-9][0-9]*)$/) {
616 0           push(@colours, {raw => Data::URIID::Colour->new(
617             rgb => sprintf('#%02x%02x%02x', $1, $2, $3),
618             )});
619             } else {
620             # BAD line!?
621             }
622             }
623              
624 0 0         $pv->{gpl_palette_colours} = \@colours if scalar @colours;
625             }
626             }
627             }
628              
629             sub _load_rgbtxt {
630 0     0     my ($self, $key, %opts) = @_;
631 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
632              
633 0 0         return if defined $self->{_loaded_rgbtxt};
634 0           $self->{_loaded_rgbtxt} = 1;
635              
636             {
637 0           my $inode = $self->parent->inode;
  0            
638 0           my $data = $inode->peek(wanted => 64);
639              
640 0 0         if ($data =~ /^\! \$Xorg: rgb\.txt,v .+ Exp \$\r?\n/) {
641 0           my $fh = $inode->_get_fh;
642 0           my @colours;
643              
644 0 0         return unless eval { require Data::URIID::Colour; 1; };
  0            
  0            
645              
646 0           while (defined(my $line = <$fh>)) {
647 0           $line =~ s/\r?\n$//;
648 0 0         next unless length($line);
649 0 0         if ($line =~ /^(0|[1-9][0-9]*)\s+(0|[1-9][0-9]*)\s+(0|[1-9][0-9]*)\s+(\S(?:.*\S)?)$/) {
650 0           push(@colours, {raw => Data::URIID::Colour->new(
651             rgb => sprintf('#%02x%02x%02x', $1, $2, $3),
652             displayname => $4,
653             )});
654             } else {
655             # BAD line!?
656             }
657             }
658              
659 0 0         $pv->{rgbtxt_palette_colours} = \@colours if scalar @colours;
660             }
661             }
662             }
663              
664             sub _load_image_info {
665 0     0     my ($self, $key, %opts) = @_;
666 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
667              
668 0 0         return if defined $self->{_loaded_image_info};
669 0           $self->{_loaded_image_info} = 1;
670              
671 0 0         return unless defined $self->{path};
672              
673 0           foreach my $data (eval {
674 0           require Image::Info;
675 0           Image::Info->import();
676 0           Image::Info::image_info($self->{path});
677             }) {
678 0 0 0       next if defined($data->{error}) && length($data->{error});
679              
680 0           foreach my $key (@_image_info_keys) {
681 0           my $pv_key = 'image_info_'.lc($key);
682 0           my $value = delete $data->{$key};
683              
684 0 0 0       next unless defined($value) && length($value);
685              
686 0           $pv->{$pv_key} = {raw => $value};
687             }
688 0           foreach my $key (@_image_extra_keys) {
689 0           my $pv_key = 'image_info_extra_'.lc($key =~ s/::/_/r);
690 0           my $value = delete $data->{$key};
691              
692 0 0 0       next unless defined($value) && length($value);
693              
694 0           $pv->{$pv_key} = {raw => $value};
695             }
696             }
697             }
698              
699             sub _load_audio_scan {
700 0     0     my ($self, $key, %opts) = @_;
701 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
702              
703 0 0         return if defined $self->{_loaded_audio_scan};
704 0           $self->{_loaded_audio_scan} = 1;
705              
706 0 0         return unless defined $self->{path};
707              
708             # Check for module;
709 0 0         if (defined(my $data = eval {
710 0           local $ENV{AUDIO_SCAN_NO_ARTWORK} = 1;
711 0           require Audio::Scan;
712 0           Audio::Scan->import();
713 0           Audio::Scan->scan($self->{path});
714             })) {
715 0           my $info = $data->{info};
716 0           my $tags = $data->{tags};
717              
718 0           foreach my $key (keys %{$info}) {
  0            
719 0           my $value = $info->{$key};
720 0           my $pv_key;
721              
722 0 0 0       next unless defined($value) && length($value);
723 0 0         next if ref $value;
724              
725 0           $pv_key = $self->_dynamic_property(audio_scan_info => $key);
726 0           $pv->{$pv_key} = {raw => $value};
727             }
728              
729 0           foreach my $key (keys %{$tags}) {
  0            
730 0           my $value = $tags->{$key};
731 0           my $pv_key;
732              
733 0 0 0       next unless defined($value) && length($value);
734 0 0         next if ref $value;
735              
736 0           $pv_key = $self->_dynamic_property(audio_scan_tags => $key);
737 0           $pv->{$pv_key} = {raw => $value};
738             }
739              
740             {
741 0           my $style;
  0            
742              
743 0 0         if ($self->_check_mediatype(qw(application/ogg audio/ogg video/ogg audio/flac))) {
    0          
744 0           $style = 'vorbiscomments';
745             } elsif ($self->_check_mediatype(qw(audio/x-wav))) {
746 0           $style = 'riffwave';
747             } else {
748 0           $style = 'id3'; # bad guess
749             }
750              
751 0 0 0       if (defined($style) && defined(my $map = $_audio_scan_tags{$style})) {
752 0           foreach my $key (keys %{$map}) {
  0            
753 0           my $src_pv_key = $self->_dynamic_property(audio_scan_tags => $map->{$key});
754 0           my $pv_key = $self->_dynamic_property(audio_scan => $key);
755 0           my $value = $pv->{$src_pv_key};
756              
757 0 0 0       if (defined($value) && ref($value) eq 'HASH' && defined($value->{raw})) {
      0        
758 0           $pv->{$pv_key} = {raw => $value->{raw}};
759             }
760             }
761             }
762             }
763             }
764             }
765              
766             sub _load_barcodes {
767 0     0     my ($self, $key, %opts) = @_;
768 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
769 0           my @barcodes;
770              
771 0 0         return if defined $self->{_loaded_barcodes};
772 0           $self->{_loaded_barcodes} = 1;
773              
774 0 0         return unless defined $self->{path};
775 0 0         return unless eval { require Data::URIID::Barcode; 1; };
  0            
  0            
776              
777 0           @barcodes = eval { Data::URIID::Barcode->sheet(filename => $self->{path}) };
  0            
778              
779 0 0         if (scalar @barcodes) {
780 0           $pv->{data_uriid_barcodes} = [map {{raw => $_}} @barcodes];
  0            
781             }
782             }
783              
784             sub _load_libpng {
785 0     0     my ($self, $key, %opts) = @_;
786 0   0       my $pv = ($self->{properties_values} //= {})->{current} //= {};
      0        
787              
788 0 0         return if defined $self->{_loaded_libpng};
789 0           $self->{_loaded_libpng} = 1;
790              
791 0 0         return unless $self->_check_mediatype(qw(image/png));
792 0 0         return unless defined $self->{path};
793 0 0         return unless eval { require Image::PNG::Libpng; 1; };
  0            
  0            
794              
795 0 0         if (defined(my $png = eval {Image::PNG::Libpng::read_png_file($self->{path})})) {
  0            
796 0           my $IHDR = $png->get_IHDR;
797              
798 0           $pv->{libpng_ihdr_width} = {raw => $IHDR->{width}};
799 0           $pv->{libpng_ihdr_height} = {raw => $IHDR->{height}};
800              
801 0 0         if (defined(my $ct = $_PNG_colour_types{$IHDR->{color_type}})) {
802 0           $pv->{libpng_ihdr_color_type} = {raw => $IHDR->{color_type}, ise => $ct->{ise}};
803             }
804              
805 0 0         if (defined(my $PLTE = eval {$png->get_PLTE})) {
  0            
806 0 0         if (eval { require Data::URIID::Colour; 1; }) {
  0            
  0            
807             my @colours = map {{
808 0           raw => Data::URIID::Colour->new(rgb => sprintf('#%02x%02x%02x', $_->{red}, $_->{green}, $_->{blue})),
809 0           }} @{$PLTE};
  0            
810 0 0         $pv->{libpng_plte_colours} = \@colours if scalar @colours;
811             }
812             }
813             }
814             }
815              
816             1;
817              
818             __END__
819              
820             =pod
821              
822             =encoding UTF-8
823              
824             =head1 NAME
825              
826             File::Information::Deep - generic module for extracting information from filesystems
827              
828             =head1 VERSION
829              
830             version v0.16
831              
832             =head1 SYNOPSIS
833              
834             use File::Information;
835              
836             my File::Information::Deep $deep = $obj->deep;
837              
838             # ...
839              
840             B<Note:> This package inherits from L<File::Information::Base>.
841              
842             This package allows for deep inspection of files.
843             This permits to read data directly from files, not just external metadata
844             (such as filesystem attributes).
845             This however comes at the price of performance.
846              
847             B<Note:>
848             If you want to use data from deep inspection, you need to load this object (by calling C<$obj-E<gt>deep>)
849             before calling any L<File::Information::Base/get> or similar methods.
850              
851             =head1 METHODS
852              
853             =head2 parent
854              
855             my File::Information::Base $parent = $deep->parent;
856              
857             Returns the parent that was used to create this object.
858              
859             =head1 AUTHOR
860              
861             Philipp Schafft <lion@cpan.org>
862              
863             =head1 COPYRIGHT AND LICENSE
864              
865             This software is Copyright (c) 2024-2025 by Philipp Schafft <lion@cpan.org>.
866              
867             This is free software, licensed under:
868              
869             The Artistic License 2.0 (GPL Compatible)
870              
871             =cut