File Coverage

blib/lib/Image/ExifTool/Trailer.pm
Criterion Covered Total %
statement 55 125 44.0
branch 21 88 23.8
condition 9 75 12.0
subroutine 5 6 83.3
pod 0 3 0.0
total 90 297 30.3


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: Trailer.pm
3             #
4             # Description: Read JPEG trailer written by various makes of phone
5             #
6             # Revisions: 2025-01-27 - P. Harvey Created
7             #------------------------------------------------------------------------------
8              
9             package Image::ExifTool::Trailer;
10              
11 5     5   32 use strict;
  5         10  
  5         225  
12 5     5   26 use vars qw($VERSION);
  5         8  
  5         263  
13 5     5   22 use Image::ExifTool qw(:DataAccess :Utils);
  5         25  
  5         10073  
14              
15             $VERSION = '1.01';
16              
17             %Image::ExifTool::Trailer::Vivo = (
18             GROUPS => { 0 => 'Trailer', 1 => 'Vivo', 2 => 'Image' },
19             VARS => { ID_FMT => 'none' },
20             NOTES => 'Information written in JPEG trailer by some Vivo phones.',
21             # (don't know for sure what type of image this is, but it is in JPEG format)
22             HDRImage => {
23             Notes => 'highlights of HDR image',
24             Groups => { 2 => 'Preview' },
25             Binary => 1,
26             },
27             JSONInfo => { },
28             HiddenData => {
29             Notes => 'hidden in EXIF, not in trailer. This data is lost if the file is edited',
30             Groups => { 0 => 'EXIF' },
31             },
32             );
33              
34             %Image::ExifTool::Trailer::OnePlus = (
35             GROUPS => { 0 => 'Trailer', 1 => 'OnePlus', 2 => 'Image' },
36             NOTES => 'Information written in JPEG trailer by some OnePlus phones.',
37             JSONInfo => { },
38             'private.emptyspace' => { # length of the entire OnePlus trailer
39             Name => 'OnePlusTrailerLen',
40             ValueConv => 'length $val == 4 ? unpack("N", $val) : $val',
41             Unknown => 1,
42             },
43             'watermark.device' => {
44             Name => 'Device',
45             ValueConv => '"0x" . join(" ", unpack("H10Z*", $val))',
46             Format => 'string',
47             },
48             );
49              
50             # Google and/or Android information in JPEG trailer
51             %Image::ExifTool::Trailer::Google = (
52             GROUPS => { 0 => 'Trailer', 1 => 'Google', 2 => 'Image' },
53             NOTES => q{
54             Google-defined information written in the trailer of JPEG images by some
55             phones. This information is referenced by DirectoryItem entries in the XMP.
56             Note that some of this information may also be referenced from other
57             metadata formats, and hence may be extracted twice. For example,
58             MotionPhotoVideo may also exist within a Samsung trailer as
59             EmbbededVideoFile, or GainMapImage may also exist in an MPF trailer as
60             MPImage2.
61             },
62             MotionPhoto => { Name => 'MotionPhotoVideo', Groups => { 2 => 'Video' } },
63             GainMap => { Name => 'GainMapImage', Groups => { 2 => 'Preview' } },
64             Depth => { Name => 'DepthMapImage', Groups => { 2 => 'Preview' } },
65             Confidence => { Name => 'ConfidenceMapImage',Groups => { 2 => 'Preview' } },
66             'android/depthmap' => { Name => 'DepthMapImage', Groups => { 2 => 'Preview' } },
67             'android/confidencemap' => { Name => 'ConfidenceMapImage', Groups => { 2 => 'Preview' } },
68             );
69              
70             #------------------------------------------------------------------------------
71             # Process Vivo trailer
72             # Inputs: 0) ExifTool object reference, 1) dirInfo reference
73             # Returns: 1 on success, 0 on failure, -1 if we must scan for the start
74             # of the trailer to set the ExifTool TrailerStart member
75             # - takes Offset as positive offset from end of trailer to end of file,
76             # and returns DataPos and DirLen, and updates OutFile when writing
77             sub ProcessVivo($$)
78             {
79 45     45 0 123 my ($et, $dirInfo) = @_;
80 45         109 my $raf = $$dirInfo{RAF};
81 45         84 my $buff;
82              
83             # return now unless we are at a position to scan for the trailer
84             # (must scan because the trailer footer doesn't indicate the trailer length)
85 45 100       179 return -1 unless $$dirInfo{ScanForTrailer};
86              
87 26 50       103 my $pos = $$et{TrailerStart} or return 0;
88 26         92 my $len = $$et{FileEnd} - $pos - $$dirInfo{Offset};
89 26 50       92 $raf->Seek($pos, 0) or return 0;
90 26 50 33     242 return 0 unless $len > 0 and $len < 1e7 and $raf->Read($buff, $len) == $len and
      33        
      33        
      33        
91             $buff =~ /\xff{4}\x1b\*9HWfu\x84\x93\xa2\xb1$/ and # validate footer
92             $buff =~ /(streamdata|vivo\{")/g; # find start
93 26         136 my $start = pos($buff) - length($1);
94 26 50       92 if ($start) {
95 26         60 $pos += $start;
96 26         50 $len -= $start;
97 26         115 $buff = substr($buff, $start);
98             }
99             # set trailer position and length
100 26         147 @$dirInfo{'DataPos','DirLen'} = ($pos, $len);
101              
102             # let ProcessTrailers copy or delete this trailer
103 26 100       120 return -1 if $$dirInfo{OutFile};
104              
105 19 50 33     176 $et->DumpTrailer($dirInfo) if $$et{OPTIONS}{Verbose} or $$et{HTML_DUMP};
106 19         98 my $tbl = GetTagTable('Image::ExifTool::Trailer::Vivo');
107 19         78 pos($buff) = 0; # rewind search to start of buffer
108 19 50 33     106 if ($buff =~ /^streamdata\xff\xd8\xff/ and $buff =~ /\xff\xd9stream(info|coun)/g) {
109 0         0 $et->HandleTag($tbl, HDRImage => substr($buff, 10, pos($buff)-20));
110             }
111             # continue looking for Vivo JSON data
112 19 50       119 if ($buff =~ /vivo\{"/g) {
113 19         49 my $jsonStart = pos($buff) - 2;
114 19 50       79 if ($buff =~ /\}\0/g) {
115 19         51 my $jsonLen = pos($buff) - 1 - $jsonStart;
116 19         98 $et->HandleTag($tbl, JSONInfo => substr($buff, $jsonStart, $jsonLen));
117             }
118             }
119 19         87 return 1;
120             }
121              
122             #------------------------------------------------------------------------------
123             # Process OnePlus trailer
124             # Inputs: 0) ExifTool object reference, 1) dirInfo reference
125             # Returns: 1 on success, 0 on failure, -1 if we must scan for the start
126             # of the trailer to set the ExifTool TrailerStart member
127             # - takes Offset as positive offset from end of trailer to end of file,
128             # and returns DataPos and DirLen, and updates OutFile when writing
129             sub ProcessOnePlus($$)
130             {
131 0     0 0 0 my ($et, $dirInfo) = @_;
132 0         0 my $raf = $$dirInfo{RAF};
133 0         0 my ($buff, $buf2);
134              
135             # return now unless we are at a position to scan for the trailer
136             # (must scan because the trailer footer doesn't indicate the entire trailer length)
137 0 0       0 return -1 unless $$dirInfo{ScanForTrailer};
138              
139             # return -1 to let ProcessTrailers copy or delete the entire trailer
140 0 0       0 return -1 if $$dirInfo{OutFile};
141              
142 0 0       0 my $start = $$et{TrailerStart} or return 0;
143 0 0 0     0 $raf->Seek(-8-$$dirInfo{Offset}, 2) and $raf->Read($buff, 8) == 8 or return 0;
144 0         0 my $end = $raf->Tell(); # (same as FileEnd - Offset)
145              
146 0   0     0 my $dump = ($$et{OPTIONS}{Verbose} or $$et{HTML_DUMP});
147 0         0 my $tagTable = GetTagTable('Image::ExifTool::Trailer::OnePlus');
148 0         0 my $trailLen = 0;
149 0 0       0 if ($buff =~ /^jxrs...\0$/) {
150 0         0 my $jlen = unpack('x4V', $buff);
151 0         0 my $maxOff = 0;
152 0 0 0     0 if ($jlen < $end-$start and $jlen > 8 and $raf->Seek($end-$jlen) and
      0        
      0        
153             $raf->Read($buff, $jlen-8) == $jlen-8)
154             {
155 0         0 $buff =~ s/\0+$//; # remove trailing null(s)
156 0         0 require Image::ExifTool::Import;
157 0         0 my $list = Image::ExifTool::Import::ReadJSONObject(undef, \$buff);
158 0 0       0 if (ref $list eq 'ARRAY') {
159 0   0     0 $$_{offset} and $$_{offset} > $maxOff and $maxOff = $$_{offset} foreach @$list;
      0        
160 0         0 $trailLen = $maxOff + $jlen;
161 0 0 0     0 if ($dump and $trailLen) {
162 0         0 $et->DumpTrailer({
163             RAF => $raf,
164             DirName => 'OnePlus',
165             DataPos => $end-$trailLen,
166             DirLen => $trailLen,
167             });
168             }
169 0         0 $et->HandleTag($tagTable, JSONInfo => $buff);
170 0         0 foreach (@$list) {
171 0         0 my ($off, $name, $len) = @$_{qw(offset name length)};
172 0 0 0     0 next unless $off and $name and $len;
      0        
173 0 0 0     0 if ($raf->Seek($end-$jlen-$off) and $raf->Read($buf2, $len) == $len) {
174 0         0 $et->HandleTag($tagTable, $name, $buf2, DataPos => $end-$jlen-$off, DataPt => \$buf2);
175             }
176             }
177             } else {
178 0         0 $et->HandleTag($tagTable, JSONInfo => $buff);
179 0         0 $et->Warn('Error parsing OnePlus JSON information');
180             }
181             }
182             }
183 0         0 @$dirInfo{'DataPos','DirLen'} = ($end - $trailLen, $trailLen);
184              
185 0         0 return 1;
186             }
187              
188             #------------------------------------------------------------------------------
189             # Process Google trailer
190             # Inputs: 0) ExifTool object reference, 1) dirInfo reference
191             # Returns: 1 on success, 0 on failure, -1 if we must scan for the start
192             # of the trailer to set the ExifTool TrailerStart member
193             # - this trailer won't be identified when writing because XMP isn't extracted then
194             sub ProcessGoogle($$)
195             {
196 2     2 0 5 my ($et, $dirInfo) = @_;
197 2         4 my $raf = $$dirInfo{RAF};
198 2         6 my $info = $$et{VALUE};
199              
200 2         12 my ($tag, $mime, $len, $pad) = @$info{qw(DirectoryItemSemantic DirectoryItemMime
201             DirectoryItemLength DirectoryItemPadding)};
202              
203 2 50 33     20 unless (ref $tag eq 'ARRAY' and ref $mime eq 'ARRAY') {
204 0         0 undef $pad;
205 0         0 ($tag, $mime, $len) = @$info{qw(ContainerDirectoryItemDataURI
206             ContainerDirectoryItemMime ContainerDirectoryItemLength)};
207 0 0 0     0 unless (ref $mime eq 'ARRAY' and ref $tag eq 'ARRAY') {
208 0         0 delete $$et{ProcessGoogleTrailer};
209 0         0 return 0;
210             }
211             }
212             # we need to know TrailerStart to be able to read/write this trailer
213 2 100       10 return -1 unless $$dirInfo{ScanForTrailer};
214              
215 1         3 delete $$et{ProcessGoogleTrailer}; # reset flag to process the Google trailer
216              
217 1 50       5 return -1 if $$dirInfo{OutFile}; # let caller handle the writing
218              
219             # sometimes DirectoryItemLength is missing the Primary entry
220 1 50       5 $len = [ $len ] unless ref $len eq 'ARRAY';
221 1         7 unshift @$len, 0 while @$len < @$mime;
222              
223 1 50       4 my $start = $$et{TrailerStart} or return 0;
224 1         3 my $end = $$et{FileEnd}; # (ignore Offset for now because some entries may run into other trailers)
225              
226 1   33     6 my $dump = ($$et{OPTIONS}{Verbose} or $$et{HTML_DUMP});
227 1         6 my $tagTable = GetTagTable('Image::ExifTool::Trailer::Google');
228              
229             # (ignore first entry: "Primary" or "primary_image")
230 1         3 my ($i, $pos, $buff, $regex, $grp, $type);
231 1         4 for ($i=1, $pos=0; defined $$mime[$i]; ++$i) {
232 1         4 my $more = $end - $start - $pos;
233 1 50       4 last if $more < 16;
234 0 0 0     0 next unless $$len[$i] and defined $$tag[$i];
235 0 0       0 last if $$len[$i] > $more;
236 0 0 0     0 $raf->Seek($start+$pos) and $raf->Read($buff, 16) == 16 and $raf->Seek($start+$pos) or last;
      0        
237 0 0       0 if ($$mime[$i] eq 'image/jpeg') {
    0          
238 0         0 $regex = '\xff\xd8\xff[\xdb\xe0\xe1]';
239             } elsif ($$mime[$i] eq 'video/mp4') {
240 0         0 $regex = '\0\0\0.ftyp(mp42|isom)';
241             } else {
242 0         0 $et->Warn("Google trailer $$tag[$i] $$mime[$i] not handled");
243 0         0 next;
244             }
245 0 0       0 if ($buff =~ /^$regex/s) {
246 0 0       0 last unless $raf->Read($buff, $$len[$i]) == $$len[$i];
247             } else {
248 0 0       0 last if $pos; # don't skip unknown information again
249 0 0       0 last unless $raf->Read($buff, $more) == $more;
250 0 0       0 last unless $buff =~ /($regex)/sg;
251 0         0 $pos += pos($buff) - length($1);
252 0         0 $more = $end - $start - $pos;
253 0 0       0 last if $$len[$i] > $end - $start - $pos;
254 0         0 $buff = substr($buff, $pos, $$len[$i]);
255             }
256 0 0       0 unless ($$tagTable{$$tag[$i]}) {
257 0         0 my $name = $$tag[$i];
258 0         0 $name =~ s/([^A-Za-z])([a-z])/$1\u$2/g; # capitalize words
259 0         0 $name = Image::ExifTool::MakeTagName($$tag[$i]);
260 0 0       0 if ($$mime[$i] eq 'image/jpeg') {
261 0         0 ($type, $grp) = ('Image', 'Preview');
262             } else {
263 0         0 ($type, $grp) = ('Video', 'Video');
264             }
265 0         0 $et->VPrint(0, $$et{INDENT}, "[adding Google:$name]\n");
266 0         0 AddTagToTable($tagTable, $$tag[$i], { Name => "$name$type", Groups => { 2 => $grp } });
267             }
268 0 0       0 $dump and $et->DumpTrailer({
269             RAF => $raf,
270             DirName => $$tag[$i],
271             DataPos => $start + $pos,
272             DirLen => $$len[$i],
273             });
274 0         0 $et->HandleTag($tagTable, $$tag[$i], \$buff, DataPos => $start + $pos, DataPt => \$buff);
275             # (haven't seen non-zero padding, but I assume this is how it works
276 0 0 0     0 $pos += $$len[$i] + (($pad and $$pad[$i]) ? $$pad[$i] : 0);
277             }
278 1 50 33     6 if (defined $$tag[$i] and defined $$mime[$i]) {
279 1         11 $et->Warn("Error reading $$tag[$i] $$mime[$i] from trailer", 1);
280             }
281 1 50       8 return 0 unless $pos;
282              
283 0           @$dirInfo{'DataPos','DirLen'} = ($start, $pos);
284              
285 0           return 1;
286             }
287              
288             1; # end
289              
290             __END__