exiftool_vendored 13.16.0 → 13.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,318 @@
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
+ use strict;
12
+ use vars qw($VERSION);
13
+ use Image::ExifTool qw(:DataAccess :Utils);
14
+
15
+ $VERSION = '1.00';
16
+
17
+ %Image::ExifTool::Trailer::Vivo = (
18
+ GROUPS => { 0 => 'Trailer', 1 => 'Vivo', 2 => 'Image' },
19
+ VARS => { NO_ID => 1 },
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
+ my ($et, $dirInfo) = @_;
80
+ my $raf = $$dirInfo{RAF};
81
+ 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
+ return -1 unless $$dirInfo{ScanForTrailer};
86
+
87
+ my $pos = $$et{TrailerStart} or return 0;
88
+ my $len = $$et{FileEnd} - $pos - $$dirInfo{Offset};
89
+ $raf->Seek($pos, 0) or return 0;
90
+ return 0 unless $len > 0 and $len < 1e7 and $raf->Read($buff, $len) == $len and
91
+ $buff =~ /\xff{4}\x1b\*9HWfu\x84\x93\xa2\xb1$/ and # validate footer
92
+ $buff =~ /(streamdata|vivo\{")/g; # find start
93
+ my $start = pos($buff) - length($1);
94
+ if ($start) {
95
+ $pos += $start;
96
+ $len -= $start;
97
+ $buff = substr($buff, $start);
98
+ }
99
+ # set trailer position and length
100
+ @$dirInfo{'DataPos','DirLen'} = ($pos, $len);
101
+
102
+ # let ProcessTrailers copy or delete this trailer
103
+ return -1 if $$dirInfo{OutFile};
104
+
105
+ $et->DumpTrailer($dirInfo) if $$et{OPTIONS}{Verbose} or $$et{HTML_DUMP};
106
+ my $tbl = GetTagTable('Image::ExifTool::Trailer::Vivo');
107
+ pos($buff) = 0; # rewind search to start of buffer
108
+ if ($buff =~ /^streamdata\xff\xd8\xff/ and $buff =~ /\xff\xd9stream(info|coun)/g) {
109
+ $et->HandleTag($tbl, HDRImage => substr($buff, 10, pos($buff)-20));
110
+ }
111
+ # continue looking for Vivo JSON data
112
+ if ($buff =~ /vivo\{"/g) {
113
+ my $jsonStart = pos($buff) - 2;
114
+ if ($buff =~ /\}\0/g) {
115
+ my $jsonLen = pos($buff) - 1 - $jsonStart;
116
+ $et->HandleTag($tbl, JSONInfo => substr($buff, $jsonStart, $jsonLen));
117
+ }
118
+ }
119
+ 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
+ my ($et, $dirInfo) = @_;
132
+ my $raf = $$dirInfo{RAF};
133
+ 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
+ return -1 unless $$dirInfo{ScanForTrailer};
138
+
139
+ # return -1 to let ProcessTrailers copy or delete the entire trailer
140
+ return -1 if $$dirInfo{OutFile};
141
+
142
+ my $start = $$et{TrailerStart} or return 0;
143
+ $raf->Seek(-8-$$dirInfo{Offset}, 2) and $raf->Read($buff, 8) == 8 or return 0;
144
+ my $end = $raf->Tell(); # (same as FileEnd - Offset)
145
+
146
+ my $dump = ($$et{OPTIONS}{Verbose} or $$et{HTML_DUMP});
147
+ my $tagTable = GetTagTable('Image::ExifTool::Trailer::OnePlus');
148
+ my $trailLen = 0;
149
+ if ($buff =~ /^jxrs...\0$/) {
150
+ my $jlen = unpack('x4V', $buff);
151
+ my $maxOff = 0;
152
+ if ($jlen < $end-$start and $jlen > 8 and $raf->Seek($end-$jlen) and
153
+ $raf->Read($buff, $jlen-8) == $jlen-8)
154
+ {
155
+ $buff =~ s/\0+$//; # remove trailing null(s)
156
+ require Image::ExifTool::Import;
157
+ my $list = Image::ExifTool::Import::ReadJSONObject(undef, \$buff);
158
+ if (ref $list eq 'ARRAY') {
159
+ $$_{offset} and $$_{offset} > $maxOff and $maxOff = $$_{offset} foreach @$list;
160
+ $trailLen = $maxOff + $jlen;
161
+ if ($dump and $trailLen) {
162
+ $et->DumpTrailer({
163
+ RAF => $raf,
164
+ DirName => 'OnePlus',
165
+ DataPos => $end-$trailLen,
166
+ DirLen => $trailLen,
167
+ });
168
+ }
169
+ $et->HandleTag($tagTable, JSONInfo => $buff);
170
+ foreach (@$list) {
171
+ my ($off, $name, $len) = @$_{qw(offset name length)};
172
+ next unless $off and $name and $len;
173
+ if ($raf->Seek($end-$jlen-$off) and $raf->Read($buf2, $len) == $len) {
174
+ $et->HandleTag($tagTable, $name, $buf2, DataPos => $end-$jlen-$off, DataPt => \$buf2);
175
+ }
176
+ }
177
+ } else {
178
+ $et->HandleTag($tagTable, JSONInfo => $buff);
179
+ $et->Warn('Error parsing OnePlus JSON information');
180
+ }
181
+ }
182
+ }
183
+ @$dirInfo{'DataPos','DirLen'} = ($end - $trailLen, $trailLen);
184
+
185
+ 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
+ my ($et, $dirInfo) = @_;
197
+ my $raf = $$dirInfo{RAF};
198
+ my $info = $$et{VALUE};
199
+
200
+ my ($tag, $mime, $len, $pad) = @$info{qw(DirectoryItemSemantic DirectoryItemMime
201
+ DirectoryItemLength DirectoryItemPadding)};
202
+
203
+ unless (ref $tag eq 'ARRAY' and ref $mime eq 'ARRAY') {
204
+ undef $pad;
205
+ ($tag, $mime, $len) = @$info{qw(ContainerDirectoryItemDataURI
206
+ ContainerDirectoryItemMime ContainerDirectoryItemLength)};
207
+ unless (ref $mime eq 'ARRAY' and ref $tag eq 'ARRAY') {
208
+ delete $$et{ProcessGoogleTrailer};
209
+ return 0;
210
+ }
211
+ }
212
+ # we need to know TrailerStart to be able to read/write this trailer
213
+ return -1 unless $$dirInfo{ScanForTrailer};
214
+
215
+ delete $$et{ProcessGoogleTrailer}; # reset flag to process the Google trailer
216
+
217
+ return -1 if $$dirInfo{OutFile}; # let caller handle the writing
218
+
219
+ # sometimes DirectoryItemLength is missing the Primary entry
220
+ $len = [ $len ] unless ref $len eq 'ARRAY';
221
+ unshift @$len, 0 while @$len < @$mime;
222
+
223
+ my $start = $$et{TrailerStart} or return 0;
224
+ my $end = $$et{FileEnd}; # (ignore Offset for now because some entries may run into other trailers)
225
+
226
+ my $dump = ($$et{OPTIONS}{Verbose} or $$et{HTML_DUMP});
227
+ my $tagTable = GetTagTable('Image::ExifTool::Trailer::Google');
228
+
229
+ # (ignore first entry: "Primary" or "primary_image")
230
+ my ($i, $pos, $buff, $regex, $grp, $type);
231
+ for ($i=1, $pos=0; defined $$mime[$i]; ++$i) {
232
+ my $more = $end - $start - $pos;
233
+ last if $more < 16;
234
+ next unless $$len[$i] and defined $$tag[$i];
235
+ last if $$len[$i] > $more;
236
+ $raf->Seek($start+$pos) and $raf->Read($buff, 16) == 16 and $raf->Seek($start+$pos) or last;
237
+ if ($$mime[$i] eq 'image/jpeg') {
238
+ $regex = '\xff\xd8\xff[\xdb\xe0\xe1]';
239
+ } elsif ($$mime[$i] eq 'video/mp4') {
240
+ $regex = '\0\0\0.ftyp(mp42|isom)';
241
+ } else {
242
+ $et->Warn("Google trailer $$tag[$i] $$mime[$i] not handled");
243
+ next;
244
+ }
245
+ if ($buff =~ /^$regex/s) {
246
+ last unless $raf->Read($buff, $$len[$i]) == $$len[$i];
247
+ } else {
248
+ last if $pos; # don't skip unknown information again
249
+ last unless $raf->Read($buff, $more) == $more;
250
+ last unless $buff =~ /($regex)/sg;
251
+ $pos += pos($buff) - length($1);
252
+ $more = $end - $start - $pos;
253
+ last if $$len[$i] > $end - $start - $pos;
254
+ $buff = substr($buff, $pos, $$len[$i]);
255
+ }
256
+ unless ($$tagTable{$$tag[$i]}) {
257
+ my $name = $$tag[$i];
258
+ $name =~ s/([^A-Za-z])([a-z])/$1\u$2/g; # capitalize words
259
+ $name = Image::ExifTool::MakeTagName($$tag[$i]);
260
+ if ($$mime[$i] eq 'image/jpeg') {
261
+ ($type, $grp) = ('Image', 'Preview');
262
+ } else {
263
+ ($type, $grp) = ('Video', 'Video');
264
+ }
265
+ $et->VPrint(0, $$et{INDENT}, "[adding Google:$name]\n");
266
+ AddTagToTable($tagTable, $$tag[$i], { Name => "$name$type", Groups => { 2 => $grp } });
267
+ }
268
+ $dump and $et->DumpTrailer({
269
+ RAF => $raf,
270
+ DirName => $$tag[$i],
271
+ DataPos => $start + $pos,
272
+ DirLen => $$len[$i],
273
+ });
274
+ $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
+ $pos += $$len[$i] + (($pad and $$pad[$i]) ? $$pad[$i] : 0);
277
+ }
278
+ if (defined $$tag[$i] and defined $$mime[$i]) {
279
+ $et->Warn("Error reading $$tag[$i] $$mime[$i] from trailer", 1);
280
+ }
281
+ return 0 unless $pos;
282
+
283
+ @$dirInfo{'DataPos','DirLen'} = ($start, $pos);
284
+
285
+ return 1;
286
+ }
287
+
288
+ 1; # end
289
+
290
+ __END__
291
+
292
+ =head1 NAME
293
+
294
+ Image::ExifTool::Trailer - Read JPEG trailer written by various phone makes
295
+
296
+ =head1 SYNOPSIS
297
+
298
+ This module is used by Image::ExifTool
299
+
300
+ =head1 DESCRIPTION
301
+
302
+ This module contains definitions required by Image::ExifTool to read
303
+ metadata the trailer written by some Vivo, OnePlus and Google phones.
304
+
305
+ =head1 AUTHOR
306
+
307
+ Copyright 2003-2025, Phil Harvey (philharvey66 at gmail.com)
308
+
309
+ This library is free software; you can redistribute it and/or modify it
310
+ under the same terms as Perl itself.
311
+
312
+ =head1 SEE ALSO
313
+
314
+ L<Image::ExifTool::TagNames/Trailer Tags>,
315
+ L<Image::ExifTool(3pm)|Image::ExifTool>
316
+
317
+ =cut
318
+
@@ -566,7 +566,7 @@ sub WriteCRW($$)
566
566
  my $trailPt;
567
567
  while ($success) {
568
568
  # check to see if trailer(s) exist(s)
569
- my $trailInfo = Image::ExifTool::IdentifyTrailer($raf) or last;
569
+ my $trailInfo = $et->IdentifyTrailer($raf) or last;
570
570
  # rewrite the trailer(s)
571
571
  $buff = '';
572
572
  $$trailInfo{OutFile} = \$buff;
@@ -1061,6 +1061,10 @@ sub WriteQuickTime($$$)
1061
1061
  $et->Error("Can't yet write compressed movie metadata");
1062
1062
  return $rtnVal;
1063
1063
  } elsif ($tag eq 'wide') {
1064
+ if ($size) {
1065
+ $et->Warn("Incorrect size for 'wide' atom ($size bytes)");
1066
+ $raf->Seek($size, 1) or $et->Error('Truncated wide atom');
1067
+ }
1064
1068
  next; # drop 'wide' tag
1065
1069
  }
1066
1070
 
@@ -1221,6 +1225,7 @@ sub WriteQuickTime($$$)
1221
1225
  OutFile => $outfile,
1222
1226
  NoRefTest=> 1, # don't check directory references
1223
1227
  WriteGroup => $$tagInfo{WriteGroup},
1228
+ Permanent => $$tagInfo{Permanent},
1224
1229
  # initialize array to hold details about chunk offset table
1225
1230
  # (each entry has 3-5 items: 0=atom type, 1=table offset, 2=table size,
1226
1231
  # 3=optional base offset, 4=optional item ID)
@@ -1560,7 +1565,9 @@ sub WriteQuickTime($$$)
1560
1565
  }
1561
1566
  if ($msg) {
1562
1567
  # (allow empty sample description for non-audio/video handler types, eg. 'url ', 'meta')
1563
- if ($$et{MediaType}) {
1568
+ # (also, incorrectly written 'mett' SampleEntry by Google phones,
1569
+ # see https://exiftool.org/forum/index.php?msg=91158)
1570
+ if ($avType{$$et{MediaType}}) {
1564
1571
  my $grp = $$et{CUR_WRITE_GROUP} || $parent;
1565
1572
  $et->Error("$msg for $grp");
1566
1573
  return $rtnErr;
@@ -4262,9 +4262,11 @@ sub WriteDirectory($$$;$)
4262
4262
  if ($permanentDir{$grp0} and not ($$dirInfo{TagInfo} and $$dirInfo{TagInfo}{Deletable})) {
4263
4263
  undef $delFlag;
4264
4264
  }
4265
- # (never delete an entire QuickTime group)
4266
4265
  if ($delFlag) {
4267
- if (($grp0 =~ /^(MakerNotes)$/ or $grp1 =~ /^(IFD0|ExifIFD|MakerNotes)$/) and
4266
+ if ($$dirInfo{Permanent}) {
4267
+ $self->Warn("Not deleting permanent $dirName directory");
4268
+ undef $grp1;
4269
+ } elsif (($grp0 =~ /^(MakerNotes)$/ or $grp1 =~ /^(IFD0|ExifIFD|MakerNotes)$/) and
4268
4270
  $self->IsRawType() and
4269
4271
  # allow non-permanent MakerNote directories to be deleted (ie. NikonCapture)
4270
4272
  (not $$dirInfo{TagInfo} or not defined $$dirInfo{TagInfo}{Permanent} or
@@ -6027,7 +6029,7 @@ sub WriteJPEG($$)
6027
6029
  Write($outfile, $hdr, $s, $segData) or $err = 1;
6028
6030
  my ($buff, $endPos, $trailInfo);
6029
6031
  my $delPreview = $$self{DEL_PREVIEW};
6030
- $trailInfo = IdentifyTrailer($raf) unless $$delGroup{Trailer};
6032
+ $trailInfo = $self->IdentifyTrailer($raf) unless $$delGroup{Trailer};
6031
6033
  my $nvTrail = $self->GetNewValueHash($Image::ExifTool::Extra{Trailer});
6032
6034
  unless ($oldOutfile or $delPreview or $trailInfo or $$delGroup{Trailer} or $nvTrail or
6033
6035
  $$self{HiddenData})
@@ -402,6 +402,7 @@ my %sLocationDetails = (
402
402
  STRUCT_NAME => 'LocationDetails',
403
403
  NAMESPACE => 'Iptc4xmpExt',
404
404
  GROUPS => { 2 => 'Location' },
405
+ NOTES => 'Note that the GPS elements of this structure are in the "exif" namespace.',
405
406
  Identifier => { List => 'Bag', Namespace => 'xmp' },
406
407
  City => { },
407
408
  CountryCode => { },
@@ -420,6 +421,7 @@ my %sLocationDetails = (
420
421
  PrintConvInv => '$val=~s/\s*m$//;$val',
421
422
  },
422
423
  GPSAltitudeRef => {
424
+ Namespace => 'exif',
423
425
  Writable => 'integer',
424
426
  PrintConv => {
425
427
  OTHER => sub {
@@ -1995,7 +1997,8 @@ my %sACDSeeRegionStruct = (
1995
1997
  Struct => {
1996
1998
  STRUCT_NAME => 'DeviceItem',
1997
1999
  NAMESPACE => { Item => 'http://ns.google.com/photos/dd/1.0/item/' },
1998
- Mime => { },
2000
+ # use this as a key to process Google trailer
2001
+ Mime => { RawConv => '$$self{ProcessGoogleTrailer} = $val' },
1999
2002
  Length => { Writable => 'integer' },
2000
2003
  Padding => { Writable => 'integer' },
2001
2004
  DataURI => { },
@@ -2149,7 +2152,7 @@ my %sACDSeeRegionStruct = (
2149
2152
  STRUCT_NAME => 'Item',
2150
2153
  # (use 'GItem' to avoid conflict with Google Device Container Item)
2151
2154
  NAMESPACE => { GItem => 'http://ns.google.com/photos/1.0/container/item/'},
2152
- Mime => { },
2155
+ Mime => { RawConv => '$$self{ProcessGoogleTrailer} = $val' },
2153
2156
  Semantic => { },
2154
2157
  Length => { Writable => 'integer' },
2155
2158
  Label => { },