File Coverage

blib/lib/Image/ExifTool/WriteQuickTime.pl
Criterion Covered Total %
statement 860 1117 76.9
branch 482 846 56.9
condition 273 557 49.0
subroutine 10 12 83.3
pod 0 11 0.0
total 1625 2543 63.9


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: WriteQuickTime.pl
3             #
4             # Description: Write XMP to QuickTime (MOV and MP4) files
5             #
6             # Revisions: 2013-10-29 - P. Harvey Created
7             #------------------------------------------------------------------------------
8             package Image::ExifTool::QuickTime;
9              
10 22     22   227 use strict;
  22         74  
  22         347397  
11              
12             # maps for adding metadata to various QuickTime-based file types
13             my %movMap = (
14             # MOV (no 'ftyp', or 'ftyp'='qt ') -> XMP in 'moov'-'udta'-'XMP_'
15             QuickTime => 'ItemList', # (default location for QuickTime tags)
16             ItemList => 'Meta', # MOV-Movie-UserData-Meta-ItemList
17             Keys => 'Movie', # MOV-Movie-Meta-Keys !! (hack due to different Meta location)
18             Meta => 'UserData',
19             XMP => 'UserData', # MOV-Movie-UserData-XMP
20             Microsoft => 'UserData', # MOV-Movie-UserData-Microsoft
21             UserData => 'Movie', # MOV-Movie-UserData
22             Movie => 'MOV',
23             GSpherical => 'SphericalVideoXML', # MOV-Movie-Track-SphericalVideoXML
24             SphericalVideoXML => 'Track', # (video track specifically, don't create if it doesn't exist)
25             Track => 'Movie',
26             );
27             my %mp4Map = (
28             # MP4 ('ftyp' compatible brand 'mp41', 'mp42' or 'f4v ') -> XMP at top level
29             QuickTime => 'ItemList', # (default location for QuickTime tags)
30             ItemList => 'Meta', # MOV-Movie-UserData-Meta-ItemList
31             Keys => 'Movie', # MOV-Movie-Meta-Keys !! (hack due to different Meta location)
32             Meta => 'UserData',
33             UserData => 'Movie', # MOV-Movie-UserData
34             Microsoft => 'UserData', # MOV-Movie-UserData-Microsoft
35             Movie => 'MOV',
36             XMP => 'MOV', # MOV-XMP
37             GSpherical => 'SphericalVideoXML', # MOV-Movie-Track-SphericalVideoXML
38             SphericalVideoXML => 'Track', # (video track specifically, don't create if it doesn't exist)
39             Track => 'Movie',
40             );
41             my %heicMap = (
42             # HEIC/HEIF/AVIF ('ftyp' compatible brand 'heic','mif1','avif') -> XMP/EXIF in top level 'meta'
43             Meta => 'MOV',
44             ItemInformation => 'Meta',
45             ItemPropertyContainer => 'Meta',
46             XMP => 'ItemInformation',
47             EXIF => 'ItemInformation',
48             ICC_Profile => 'ItemPropertyContainer',
49             IFD0 => 'EXIF',
50             IFD1 => 'IFD0',
51             ExifIFD => 'IFD0',
52             GPS => 'IFD0',
53             SubIFD => 'IFD0',
54             GlobParamIFD => 'IFD0',
55             PrintIM => 'IFD0',
56             InteropIFD => 'ExifIFD',
57             MakerNotes => 'ExifIFD',
58             );
59             my %cr3Map = (
60             # CR3 ('ftyp' compatible brand 'crx ') -> XMP at top level
61             Movie => 'MOV',
62             XMP => 'MOV',
63             'UUID-Canon'=>'Movie',
64             ExifIFD => 'UUID-Canon',
65             IFD0 => 'UUID-Canon',
66             GPS => 'UUID-Canon',
67             #MakerNoteCanon => 'UUID-Canon', # (doesn't yet work -- goes into ExifIFD instead)
68             'UUID-Canon2' => 'MOV',
69             CanonVRD => 'UUID-Canon2',
70             );
71             my %dirMap = (
72             MOV => \%movMap,
73             MP4 => \%mp4Map,
74             CR3 => \%cr3Map,
75             HEIC => \%heicMap,
76             );
77              
78             # convert ExifTool Format to QuickTime type
79             my %qtFormat = (
80             'undef' => 0x00, string => 0x01,
81             int8s => 0x15, int16s => 0x15, int32s => 0x15, int64s => 0x15,
82             int8u => 0x16, int16u => 0x16, int32u => 0x16, int64u => 0x16,
83             float => 0x17, double => 0x18,
84             );
85             my $undLang = 0x55c4; # numeric code for default ('und') language
86              
87             my $maxReadLen = 100000000; # maximum size of atom to read into memory (100 MB)
88              
89             # boxes that may exist in an "empty" Meta box:
90             my %emptyMeta = (
91             hdlr => 'Handler', 'keys' => 'Keys', lang => 'Language', ctry => 'Country', free => 'Free',
92             );
93              
94             # lookup for CTBO ID number based on uuid for Canon CR3 files
95             my %ctboID = (
96             "\xbe\x7a\xcf\xcb\x97\xa9\x42\xe8\x9c\x71\x99\x94\x91\xe3\xaf\xac" => 1, # XMP
97             "\xea\xf4\x2b\x5e\x1c\x98\x4b\x88\xb9\xfb\xb7\xdc\x40\x6e\x4d\x16" => 2, # PreviewImage
98             # ID 3 is used for 'mdat' atom (not a uuid)
99             );
100              
101             # mark UserData tags that don't have ItemList counterparts as Preferred
102             # - and set Preferred to 0 for any Avoid-ed tag
103             # - also, for now, set Writable to 0 for any tag with a RawConv and no RawConvInv
104             {
105             my $itemList = \%Image::ExifTool::QuickTime::ItemList;
106             my $userData = \%Image::ExifTool::QuickTime::UserData;
107             my (%pref, $tag);
108             foreach $tag (TagTableKeys($itemList)) {
109             my $tagInfo = $$itemList{$tag};
110             if (ref $tagInfo ne 'HASH') {
111             next if ref $tagInfo;
112             $tagInfo = $$itemList{$tag} = { Name => $tagInfo };
113             } else {
114             $$tagInfo{Writable} = 0 if $$tagInfo{RawConv} and not $$tagInfo{RawConvInv};
115             $$tagInfo{Avoid} and $$tagInfo{Preferred} = 0, next;
116             next if defined $$tagInfo{Preferred} and not $$tagInfo{Preferred};
117             }
118             $pref{$$tagInfo{Name}} = 1;
119             }
120             foreach $tag (TagTableKeys($userData)) {
121             my $tagInfo = $$userData{$tag};
122             if (ref $tagInfo ne 'HASH') {
123             next if ref $tagInfo;
124             $tagInfo = $$userData{$tag} = { Name => $tagInfo };
125             } else {
126             $$tagInfo{Writable} = 0 if $$tagInfo{RawConv} and not $$tagInfo{RawConvInv};
127             $$tagInfo{Avoid} and $$tagInfo{Preferred} = 0, next;
128             next if defined $$tagInfo{Preferred} or $pref{$$tagInfo{Name}};
129             }
130             $$tagInfo{Preferred} = 1;
131             }
132             }
133              
134             #------------------------------------------------------------------------------
135             # Format GPSCoordinates for writing
136             # Inputs: 0) PrintConv value
137             # Returns: ValueConv value
138             sub PrintInvGPSCoordinates($)
139             {
140 0     0 0 0 my ($val, $et) = @_;
141 0         0 my @v = split /, */, $val;
142 0 0 0     0 if (@v == 2 or @v == 3) {
143 0   0     0 my $below = ($v[2] and $v[2] =~ /below/i);
144 0         0 $v[0] = Image::ExifTool::GPS::ToDegrees($v[0], 1);
145 0         0 $v[1] = Image::ExifTool::GPS::ToDegrees($v[1], 1);
146 0 0       0 $v[2] = Image::ExifTool::ToFloat($v[2]) * ($below ? -1 : 1) if @v == 3;
    0          
147 0         0 return "@v";
148             }
149 0 0       0 return $val if $val =~ /^([-+]\d+(\.\d*)?){2,3}(CRS.*)?\/?$/; # already in ISO6709 format?
150 0         0 return undef;
151             }
152              
153             #------------------------------------------------------------------------------
154             # Convert GPS coordinates back to ISO6709 format
155             # Inputs: 0) ValueConv value
156             # Returns: ISO6709 coordinates
157             sub ConvInvISO6709($)
158             {
159 0     0 0 0 local $_;
160 0         0 my $val = shift;
161 0         0 my @a = split ' ', $val;
162 0 0 0     0 if (@a == 2 or @a == 3) {
163             # latitude must have 2 digits before the decimal, and longitude 3,
164             # and all values must start with a "+" or "-", and Google Photos
165             # requires at least 3 digits after the decimal point
166             # (and as of Apr 2021, Google Photos doesn't accept coordinats
167             # with more than 5 digits after the decimal place:
168             # https://exiftool.org/forum/index.php?topic=11055.msg67171#msg67171 )
169 0         0 my @fmt = ('%s%02d.%s%s','%s%03d.%s%s','%s%d.%s%s');
170 0         0 foreach (@a) {
171 0 0       0 return undef unless Image::ExifTool::IsFloat($_);
172 0 0 0     0 $_ =~ s/^([-+]?)(\d+)\.?(\d*)/sprintf(shift(@fmt),$1||'+',$2,$3,length($3)<3 ? '0'x(3-length($3)) : '')/e;
  0         0  
173             }
174 0         0 return join '', @a, '/';
175             }
176 0 0       0 return $val if $val =~ /^([-+]\d+(\.\d*)?){2,3}(CRS.*)?\/?$/; # already in ISO6709 format?
177 0         0 return undef;
178             }
179              
180             #------------------------------------------------------------------------------
181             # Handle offsets in iloc (ItemLocation) atom when writing (ref ISO 14496-12:2015 pg.79)
182             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) data ref, 3) output buffer ref
183             # Returns: true on success
184             # Notes: see also ParseItemLocation() in QuickTime.pm
185             # (variable names with underlines correspond to names in ISO 14496-12)
186             sub Handle_iloc($$$$)
187             {
188 3     3 0 15 my ($et, $dirInfo, $dataPt, $outfile) = @_;
189 3         10 my ($i, $j, $num, $pos, $id);
190              
191 3         9 my $off = $$dirInfo{ChunkOffset};
192 3         9 my $len = length $$dataPt;
193 3 50       16 return 0 if $len < 8;
194 3         15 my $ver = Get8u($dataPt, 0);
195 3         15 my $siz = Get16u($dataPt, 4);
196 3         12 my $noff = ($siz >> 12);
197 3         12 my $nlen = ($siz >> 8) & 0x0f;
198 3         8 my $nbas = ($siz >> 4) & 0x0f;
199 3         10 my $nind = $siz & 0x0f;
200 3         17 my %ok = ( 0 => 1, 4 => 1, 8 => 8 );
201 3 50 33     49 return 0 unless $ok{$noff} and $ok{$nlen} and $ok{$nbas} and $ok{$nind};
      33        
      33        
202             # piggy-back on existing code to fix up stco/co64 4/8-byte offsets
203 3 50       17 my $tag = $noff == 4 ? 'stco_iloc' : 'co64_iloc';
204 3 50       13 if ($ver < 2) {
205 3         11 $num = Get16u($dataPt, 6);
206 3         14 $pos = 8;
207             } else {
208 0 0       0 return 0 if $len < 10;
209 0         0 $num = Get32u($dataPt, 6);
210 0         0 $pos = 10;
211             }
212 3         16 for ($i=0; $i<$num; ++$i) {
213 9 50       27 if ($ver < 2) {
214 9 50       24 return 0 if $pos + 2 > $len;
215 9         26 $id = Get16u($dataPt, $pos);
216 9         15 $pos += 2;
217             } else {
218 0 0       0 return 0 if $pos + 4 > $len;
219 0         0 $id = Get32u($dataPt, $pos);
220 0         0 $pos += 4;
221             }
222 9         23 my ($constOff, @offBase, @offItem, $minOffset);
223 9 50 33     42 if ($ver == 1 or $ver == 2) {
224 0 0       0 return 0 if $pos + 2 > $len;
225             # offsets are absolute only if ConstructionMethod is 0, otherwise
226             # the relative offsets are constant as far as we are concerned
227 0         0 $constOff = Get16u($dataPt, $pos) & 0x0f;
228 0         0 $pos += 2;
229             }
230 9 50       28 return 0 if $pos + 2 > $len;
231 9         23 my $drefIdx = Get16u($dataPt, $pos);
232 9 50       22 if ($drefIdx) {
233 0 0 0     0 if ($$et{QtDataRef} and $$et{QtDataRef}[$drefIdx - 1]) {
234 0         0 my $dref = $$et{QtDataRef}[$drefIdx - 1];
235             # these offsets are constant unless the data is in this file
236 0 0 0     0 $constOff = 1 unless $$dref[1] == 1 and $$dref[0] ne 'rsrc';
237             } else {
238 0         0 $et->Error("No data reference for iloc entry $i");
239 0         0 return 0;
240             }
241             }
242 9         15 $pos += 2;
243             # get base offset and save its location if in this file
244 9         31 my $base_offset = GetVarInt($dataPt, $pos, $nbas);
245 9 100 66     41 if ($base_offset and not $constOff) {
246 6 50       26 my $tg = ($nbas == 4 ? 'stco' : 'co64') . '_iloc';
247 6         26 push @offBase, [ $tg, length($$outfile) + 8 + $pos - $nbas, $nbas, 0, $id ];
248             }
249 9 50       27 return 0 if $pos + 2 > $len;
250 9         24 my $ext_num = Get16u($dataPt, $pos);
251 9         15 $pos += 2;
252 9         19 my $listStartPos = $pos;
253             # run through the item list to get offset locations and the minimum offset in this file
254 9         25 for ($j=0; $j<$ext_num; ++$j) {
255 9 50 33     41 $pos += $nind if $ver == 1 or $ver == 2;
256 9         26 my $extent_offset = GetVarInt($dataPt, $pos, $noff);
257 9 50       27 return 0 unless defined $extent_offset;
258 9 50       21 unless ($constOff) {
259 9 50       60 push @offItem, [ $tag, length($$outfile) + 8 + $pos - $noff, $noff, 0, $id ] if $noff;
260 9 50 33     35 $minOffset = $extent_offset if not defined $minOffset or $minOffset > $extent_offset;
261             }
262 9 50       27 return 0 if $pos + $nlen > length $$dataPt;
263 9         25 $pos += $nlen;
264             }
265             # decide whether to fix up the base offset or individual item offsets
266             # (adjust the one that is larger)
267 9 100 66     38 if (defined $minOffset and $minOffset > $base_offset) {
268 3         15 $$_[3] = $base_offset foreach @offItem;
269 3         21 push @$off, @offItem;
270             } else {
271 6         20 $$_[3] = $minOffset foreach @offBase;
272 6         25 push @$off, @offBase;
273             }
274             }
275 3         18 return 1;
276             }
277              
278             #------------------------------------------------------------------------------
279             # Get localized version of tagInfo hash
280             # Inputs: 0) tagInfo hash ref, 1) language code (eg. "fra-FR")
281             # Returns: new tagInfo hash ref, or undef if invalid or no language code
282             sub GetLangInfo($$)
283             {
284 96     96 0 208 my ($tagInfo, $langCode) = @_;
285 96 50       203 return undef unless $langCode;
286             # only allow alternate language tags in lang-alt lists
287 96         172 my $writable = $$tagInfo{Writable};
288 96 50       269 $writable = $$tagInfo{Table}{WRITABLE} unless defined $writable;
289 96 50       209 return undef unless $writable;
290 96         154 $langCode =~ tr/_/-/; # RFC 3066 specifies '-' as a separator
291 96         248 my $langInfo = Image::ExifTool::GetLangInfo($tagInfo, $langCode);
292 96         218 return $langInfo;
293             }
294              
295             #------------------------------------------------------------------------------
296             # validate raw values for writing
297             # Inputs: 0) ExifTool ref, 1) tagInfo hash ref, 2) raw value ref
298             # Returns: error string or undef (and possibly changes value) on success
299             sub CheckQTValue($$$)
300             {
301 577     577 0 1743 my ($et, $tagInfo, $valPtr) = @_;
302 577   100     3827 my $format = $$tagInfo{Format} || $$tagInfo{Writable} || $$tagInfo{Table}{FORMAT};
303 577 100       1712 return undef unless $format;
304 430         1989 return Image::ExifTool::CheckValue($valPtr, $format, $$tagInfo{Count});
305             }
306              
307             #------------------------------------------------------------------------------
308             # Format QuickTime value for writing
309             # Inputs: 0) ExifTool ref, 1) value ref, 2) tagInfo ref, 3) Format (or undef)
310             # Returns: Flags for QT data type, and reformats value as required (sets to undef on error)
311             sub FormatQTValue($$;$$)
312             {
313 22     22 0 62 my ($et, $valPt, $tagInfo, $format) = @_;
314 22         51 my $writable = $$tagInfo{Writable};
315 22         46 my $count = $$tagInfo{Count};
316 22         38 my $flags;
317 22 50       63 $format or $format = $$tagInfo{Format};
318 22 100 33     205 if ($format and $format ne 'string' or not $format and $writable and $writable ne 'string') {
    50 66        
    50 66        
    50 66        
319 2   33     16 $$valPt = WriteValue($$valPt, $format || $writable, $count);
320 2 50 33     13 if ($writable and $qtFormat{$writable}) {
321 0         0 $flags = $qtFormat{$writable};
322             } else {
323 2   50     17 $flags = $qtFormat{$format || 0} || 0;
324             }
325             } elsif ($$valPt =~ /^\xff\xd8\xff/) {
326 0         0 $flags = 0x0d; # JPG
327             } elsif ($$valPt =~ /^(\x89P|\x8aM|\x8bJ)NG\r\n\x1a\n/) {
328 0         0 $flags = 0x0e; # PNG
329             } elsif ($$valPt =~ /^BM.{15}\0/s) {
330 0         0 $flags = 0x1b; # BMP
331             } else {
332 20         32 $flags = 0x01; # UTF8
333 20         86 $$valPt = $et->Encode($$valPt, 'UTF8');
334             }
335 22 50       58 defined $$valPt or $et->WarnOnce("Error converting value for $$tagInfo{Name}");
336 22         52 return $flags;
337             }
338              
339             #------------------------------------------------------------------------------
340             # Set variable-length integer (used by WriteItemInfo)
341             # Inputs: 0) value, 1) integer size in bytes (0, 4 or 8),
342             # Returns: packed integer
343             sub SetVarInt($$)
344             {
345 6     6 0 15 my ($val, $n) = @_;
346 6 50       21 if ($n == 4) {
    0          
347 6         16 return Set32u($val);
348             } elsif ($n == 8) {
349 0         0 return Set64u($val);
350             }
351 0         0 return '';
352             }
353              
354             #------------------------------------------------------------------------------
355             # Write Meta Keys to add/delete entries as necessary ('mdta' handler) (ref PH)
356             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
357             # Returns: updated keys box data
358             sub WriteKeys($$$)
359             {
360 183     183 0 619 my ($et, $dirInfo, $tagTablePtr) = @_;
361 183 100       1323 $et or return 1; # allow dummy access to autoload this package
362 9         34 my $dataPt = $$dirInfo{DataPt};
363 9         27 my $dirLen = length $$dataPt;
364 9         21 my $outfile = $$dirInfo{OutFile};
365 9         27 my ($tag, %done, %remap, %info, %add, $i);
366              
367 9 50       36 $dirLen < 8 and $et->Warn('Short Keys box'), $dirLen = 8, $$dataPt = "\0" x 8;
368 9 100       39 if ($$et{DEL_GROUP}{Keys}) {
369 3         8 $dirLen = 8; # delete all existing keys
370             # deleted keys are identified by a zero entry in the Remap lookup
371 3         14 my $n = Get32u($dataPt, 4);
372 3         17 for ($i=1; $i<=$n; ++$i) { $remap{$i} = 0; }
  6         20  
373 3 50       30 $et->VPrint(0, " [deleting $n Keys entr".($n==1 ? 'y' : 'ies')."]\n");
374 3         8 ++$$et{CHANGED};
375             }
376 9         22 my $pos = 8;
377 9         39 my $newTags = $et->GetNewTagInfoHash($tagTablePtr);
378 9         35 my $newData = substr($$dataPt, 0, $pos);
379              
380 9         32 my $newIndex = 1;
381 9         25 my $index = 1;
382 9         40 while ($pos < $dirLen - 4) {
383 15         53 my $len = unpack("x${pos}N", $$dataPt);
384 15 50 33     79 last if $len < 8 or $pos + $len > $dirLen;
385 15         37 my $ns = substr($$dataPt, $pos + 4, 4);
386 15         40 $tag = substr($$dataPt, $pos + 8, $len - 8);
387 15         41 $tag =~ s/\0.*//s; # truncate at null
388 15 50       73 $tag =~ s/^com\.apple\.quicktime\.// if $ns eq 'mdta'; # remove apple quicktime domain
389 15 50       45 $tag = "Tag_$ns" unless $tag;
390 15         40 $done{$tag} = 1; # set flag to avoid creating this tag
391 15         47 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
392 15 50       44 if ($tagInfo) {
393 15         35 $info{$index} = $tagInfo;
394 15 100       47 if ($$newTags{$tag}) {
395 2         8 my $nvHash = $et->GetNewValueHash($tagInfo);
396             # drop this tag if it is being deleted
397 2 100 33     15 if ($nvHash and $et->IsOverwriting($nvHash) > 0 and not defined $et->GetNewValue($nvHash)) {
      66        
398             # don't delete this key if we could be writing any alternate-language version of this tag
399 1         3 my ($t, $dontDelete);
400 1         6 foreach $t (keys %$newTags) {
401 6 50 66     24 next unless $$newTags{$t}{SrcTagInfo} and $$newTags{$t}{SrcTagInfo} eq $tagInfo;
402 0         0 my $nv = $et->GetNewValueHash($$newTags{$t});
403 0 0 0     0 next unless $et->IsOverwriting($nv) and defined $et->GetNewValue($nv);
404 0         0 $dontDelete = 1;
405 0         0 last;
406             }
407 1 50       5 unless ($dontDelete) {
408             # delete this key
409 1         12 $et->VPrint(1, "$$et{INDENT}\[deleting Keys entry $index '${tag}']\n");
410 1         3 $pos += $len;
411 1         6 $remap{$index++} = 0;
412 1         3 ++$$et{CHANGED};
413 1         3 next;
414             }
415             }
416             }
417             }
418             # add to the Keys box data
419 14         49 $newData .= substr($$dataPt, $pos, $len);
420 14         39 $remap{$index++} = $newIndex++;
421 14         43 $pos += $len;
422             }
423             # add keys for any tags we need to create
424 9         47 foreach $tag (sort keys %$newTags) {
425 12         25 my $tagInfo = $$newTags{$tag};
426 12         20 my $id;
427 12 100 66     49 if ($$tagInfo{LangCode} and $$tagInfo{SrcTagInfo}) {
428 1         5 $id = $$tagInfo{SrcTagInfo}{TagID};
429             } else {
430 11         23 $id = $tag;
431             }
432 12 100       30 next if $done{$id};
433 9         29 my $nvHash = $et->GetNewValueHash($tagInfo);
434 9 50 66     50 next unless $$nvHash{IsCreating} and $et->IsOverwriting($nvHash) and
      66        
435             defined $et->GetNewValue($nvHash);
436             # add new entry to 'keys' data
437 5 50       33 my $val = $id =~ /^com\./ ? $id : "com.apple.quicktime.$id";
438 5         37 $newData .= Set32u(8 + length($val)) . 'mdta' . $val;
439 5         40 $et->VPrint(1, "$$et{INDENT}\[adding Keys entry $newIndex '${id}']\n");
440 5         18 $add{$newIndex++} = $tagInfo;
441 5         14 ++$$et{CHANGED};
442             }
443 9         27 my $num = $newIndex - 1;
444 9 100       26 if ($num) {
445 8         97 Set32u($num, \$newData, 4); # update count in header
446             } else {
447 1         4 $newData = ''; # delete empty Keys box
448             }
449             # save temporary variables for use when writing ItemList:
450             # Remap - lookup for remapping Keys ID numbers (0 if item is deleted)
451             # Info - Keys tag information, based on old index value
452             # Add - Keys items deleted, based on old index value
453             # Num - Number of items in edited Keys box
454 9         89 $$et{Keys} = { Remap => \%remap, Info => \%info, Add => \%add, Num => $num };
455              
456 9         50 return $newData; # return updated Keys box
457             }
458              
459             #------------------------------------------------------------------------------
460             # Write ItemInformation in HEIC files
461             # Inputs: 0) ExifTool ref, 1) dirInfo ref (with BoxPos entry), 2) output buffer ref
462             # Returns: mdat edit list ref (empty if nothing changed)
463             sub WriteItemInfo($$$)
464             {
465 3     3 0 10 my ($et, $dirInfo, $outfile) = @_;
466 3         9 my $boxPos = $$dirInfo{BoxPos}; # hash of [length,position] for each box
467 3         9 my $raf = $$et{RAF};
468 3         8 my $items = $$et{ItemInfo};
469 3         7 my (%did, @mdatEdit, $name);
470              
471 3 50 33     22 return () unless $items and $raf;
472              
473             # extract information from EXIF/XMP metadata items
474 3         11 my $primary = $$et{PrimaryItem};
475 3         13 my $curPos = $raf->Tell();
476 3         7 my $id;
477 3         27 foreach $id (sort { $a <=> $b } keys %$items) {
  9         29  
478 9 50       33 $primary = $id unless defined $primary; # assume primary is lowest-number item if pitm missing
479 9         19 my $item = $$items{$id};
480             # only edit primary EXIF/XMP metadata
481 9 100 66     47 next unless $$item{RefersTo} and $$item{RefersTo}{$primary};
482 3   50     17 my $type = $$item{ContentType} || $$item{Type} || next;
483             # get ExifTool name for this item
484 3         15 $name = { Exif => 'EXIF', 'application/rdf+xml' => 'XMP' }->{$type};
485 3 50       12 next unless $name; # only care about EXIF and XMP
486 3 50       10 next unless $$et{EDIT_DIRS}{$name};
487 3         8 $did{$name} = 1; # set flag to prevent creating this metadata
488 3         5 my ($warn, $extent, $buff, @edit);
489 3 50       8 $warn = 'Missing iloc box' unless $$boxPos{iloc};
490 3 50 33     12 $warn = "No Extents for $type item" unless $$item{Extents} and @{$$item{Extents}};
  3         12  
491 3 50       11 $warn = "Can't currently decode encoded $type metadata" if $$item{ContentEncoding};
492 3 50       10 $warn = "Can't currently decode protected $type metadata" if $$item{ProtectionIndex};
493 3 50       10 $warn = "Can't currently extract $type with construction method $$item{ConstructionMethod}" if $$item{ConstructionMethod};
494 3 50       9 $warn = "$type metadata is not this file" if $$item{DataReferenceIndex};
495 3 50       9 $warn and $et->Warn($warn), next;
496 3   50     16 my $base = $$item{BaseOffset} || 0;
497 3         5 my $val = '';
498 3         6 foreach $extent (@{$$item{Extents}}) {
  3         9  
499 3 50       9 $val .= $buff if defined $buff;
500 3         9 my $pos = $$extent[1] + $base;
501 3 100       9 if ($$extent[2]) {
502 2 50       8 $raf->Seek($pos, 0) or last;
503 2 50       10 $raf->Read($buff, $$extent[2]) or last;
504             } else {
505 1         3 $buff = '';
506             }
507 3         14 push @edit, [ $pos, $pos + $$extent[2] ]; # replace or delete this if changed
508             }
509 3 50       10 next unless defined $buff;
510 3 50       11 $buff = $val . $buff if length $val;
511 3         7 my ($hdr, $subTable, $proc);
512 3 100       11 if ($name eq 'EXIF') {
513 1 50 33     17 if (not length $buff) {
    50          
    50          
514             # create EXIF from scratch
515 0         0 $hdr = "\0\0\0\x06Exif\0\0";
516             } elsif ($buff =~ /^(MM\0\x2a|II\x2a\0)/) {
517 0         0 $et->Warn('Missing Exif header');
518 0         0 $hdr = '';
519             } elsif (length($buff) >= 4 and length($buff) >= 4 + unpack('N',$buff)) {
520 1         5 $hdr = substr($buff, 0, 4 + unpack('N',$buff));
521             } else {
522 0         0 $et->Warn('Invalid Exif header');
523 0         0 next;
524             }
525 1         5 $subTable = GetTagTable('Image::ExifTool::Exif::Main');
526 1         3 $proc = \&Image::ExifTool::WriteTIFF;
527             } else {
528 2         6 $hdr = '';
529 2         18 $subTable = GetTagTable('Image::ExifTool::XMP::Main');
530             }
531 3         26 my %dirInfo = (
532             DataPt => \$buff,
533             DataLen => length $buff,
534             DirStart => length $hdr,
535             DirLen => length($buff) - length $hdr,
536             );
537 3         11 my $changed = $$et{CHANGED};
538 3         17 my $newVal = $et->WriteDirectory(\%dirInfo, $subTable, $proc);
539 3 50 33     39 if (defined $newVal and $changed ne $$et{CHANGED} and
      66        
      33        
540             # nothing changed if deleting an empty directory
541             ($dirInfo{DirLen} or length $newVal))
542             {
543 3 100 66     51 $newVal = $hdr . $newVal if length $hdr and length $newVal;
544 3         16 $edit[0][2] = \$newVal; # replace the old chunk with the new data
545 3         9 $edit[0][3] = $id; # mark this chunk with the item ID
546 3         11 push @mdatEdit, @edit;
547             # update item extent_length
548 3         6 my $n = length $newVal;
549 3         9 foreach $extent (@{$$item{Extents}}) {
  3         13  
550 3         15 my ($nlen, $lenPt) = @$extent[3,4];
551 3 50       16 if ($nlen == 8) {
    50          
552 0         0 Set64u($n, $outfile, $$boxPos{iloc}[0] + 8 + $lenPt);
553             } elsif ($n <= 0xffffffff) {
554 3         20 Set32u($n, $outfile, $$boxPos{iloc}[0] + 8 + $lenPt);
555             } else {
556 0         0 $et->Error("Can't yet promote iloc length to 64 bits");
557 0         0 return ();
558             }
559 3         13 $n = 0;
560             }
561 3 50       7 if (@{$$item{Extents}} != 1) {
  3         14  
562 0         0 $et->Error("Can't yet handle $name in multiple parts. Please submit sample for testing");
563             }
564             }
565 3         18 $$et{CHANGED} = $changed; # (will set this later if successful in editing mdat)
566             }
567 3         29 $raf->Seek($curPos, 0); # seek back to original position
568              
569             # add necessary metadata types if they didn't already exist
570 3         11 my ($countNew, %add, %usedID);
571 3         14 foreach $name ('EXIF','XMP') {
572 6 100 100     52 next if $did{$name} or not $$et{ADD_DIRS}{$name};
573 2         4 my @missing;
574 2   33     19 $$boxPos{$_} or push @missing, $_ foreach qw(iinf iloc);
575 2 50       8 if (@missing) {
576 0 0       0 my $str = @missing > 1 ? join(' and ', @missing) . ' boxes' : "@missing box";
577 0         0 $et->Warn("Can't create $name. Missing expected $str");
578 0         0 last;
579             }
580 2 50       8 unless (defined $$et{PrimaryItem}) {
581 0 0       0 unless (defined $primary) {
582 0         0 $et->Warn("Can't create $name. No items to reference");
583 0         0 last;
584             }
585             # add new primary item reference box after hdrl box
586 0 0       0 if ($primary < 0x10000) {
587 0         0 $add{hdlr} = pack('Na4Nn', 14, 'pitm', 0, $primary);
588             } else {
589 0         0 $add{hdlr} = pack('Na4CCCCN', 16, 'pitm', 1, 0, 0, 0, $primary);
590             }
591 0         0 $et->Warn("Added missing PrimaryItemReference (for item $primary)", 1);
592             }
593 2         6 my $buff = '';
594 2         5 my ($hdr, $subTable, $proc);
595 2 100       9 if ($name eq 'EXIF') {
596 1         3 $hdr = "\0\0\0\x06Exif\0\0";
597 1         13 $subTable = GetTagTable('Image::ExifTool::Exif::Main');
598 1         4 $proc = \&Image::ExifTool::WriteTIFF;
599             } else {
600 1         3 $hdr = '';
601 1         3 $subTable = GetTagTable('Image::ExifTool::XMP::Main');
602             }
603 2         11 my %dirInfo = (
604             DataPt => \$buff,
605             DataLen => 0,
606             DirStart => 0,
607             DirLen => 0,
608             );
609 2         16 my $changed = $$et{CHANGED};
610 2         12 my $newVal = $et->WriteDirectory(\%dirInfo, $subTable, $proc);
611 2 50 33     20 if (defined $newVal and $changed ne $$et{CHANGED}) {
612 2         5 my $irefVer;
613 2 50       8 if ($$boxPos{iref}) {
614 2         13 $irefVer = Get8u($outfile, $$boxPos{iref}[0] + 8);
615             } else {
616             # create iref box after end of iinf box (and save version in boxPos list)
617 0 0       0 $irefVer = ($primary < 0x10000 ? 0 : 1);
618 0         0 $$boxPos{iref} = [ $$boxPos{iinf}[0] + $$boxPos{iinf}[1], 0, $irefVer ];
619             }
620 2 100       10 $newVal = $hdr . $newVal if length $hdr;
621             # add new infe to iinf
622 2 50       13 $add{iinf} = $add{iref} = $add{iloc} = '' unless defined $add{iinf};
623 2         6 my ($type, $mime);
624 2 100       7 if ($name eq 'XMP') {
625 1         3 $type = "mime\0";
626 1         3 $mime = "application/rdf+xml\0";
627             } else {
628 1         3 $type = "Exif\0";
629 1         3 $mime = '';
630             }
631 2         5 my $id = 1;
632 2   66     18 ++$id while $$items{$id} or $usedID{$id}; # find next unused item ID
633 2         7 my $n = length($type) + length($mime) + 16;
634 2 50       9 if ($id < 0x10000) {
635 2         18 $add{iinf} .= pack('Na4CCCCnn', $n, 'infe', 2, 0, 0, 1, $id, 0) . $type . $mime;
636             } else {
637 0         0 $n += 2;
638 0         0 $add{iinf} .= pack('Na4CCCCNn', $n, 'infe', 3, 0, 0, 1, $id, 0) . $type . $mime;
639             }
640             # add new cdsc to iref
641 2 50       7 if ($irefVer) {
642 0         0 $add{iref} .= pack('Na4NnN', 18, 'cdsc', $id, 1, $primary);
643             } else {
644 2         12 $add{iref} .= pack('Na4nnn', 14, 'cdsc', $id, 1, $primary);
645             }
646             # add new entry to iloc table (see ISO14496-12:2015 pg.79)
647 2         11 my $ilocVer = Get8u($outfile, $$boxPos{iloc}[0] + 8);
648 2         9 my $siz = Get16u($outfile, $$boxPos{iloc}[0] + 12); # get size information
649 2         8 my $noff = ($siz >> 12);
650 2         7 my $nlen = ($siz >> 8) & 0x0f;
651 2         7 my $nbas = ($siz >> 4) & 0x0f;
652 2         6 my $nind = $siz & 0x0f;
653 2         5 my ($pbas, $poff);
654 2 50       30 if ($ilocVer == 0) {
    0          
    0          
655             # set offset to 0 as flag that this is a new idat chunk being added
656 2         7 $pbas = length($add{iloc}) + 4;
657 2         5 $poff = $pbas + $nbas + 2;
658 2         15 $add{iloc} .= pack('nn',$id,0) . SetVarInt(0,$nbas) . Set16u(1) .
659             SetVarInt(0,$noff) . SetVarInt(length($newVal),$nlen);
660             } elsif ($ilocVer == 1) {
661 0         0 $pbas = length($add{iloc}) + 6;
662 0         0 $poff = $pbas + $nbas + 2 + $nind;
663 0         0 $add{iloc} .= pack('nnn',$id,0,0) . SetVarInt(0,$nbas) . Set16u(1) . SetVarInt(0,$nind) .
664             SetVarInt(0,$noff) . SetVarInt(length($newVal),$nlen);
665             } elsif ($ilocVer == 2) {
666 0         0 $pbas = length($add{iloc}) + 8;
667 0         0 $poff = $pbas + $nbas + 2 + $nind;
668 0         0 $add{iloc} .= pack('Nnn',$id,0,0) . SetVarInt(0,$nbas) . Set16u(1) . SetVarInt(0,$nind) .
669             SetVarInt(0,$noff) . SetVarInt(length($newVal),$nlen);
670             } else {
671 0         0 $et->Warn("Can't create $name. Unsupported iloc version $ilocVer");
672 0         0 last;
673             }
674             # add new ChunkOffset entry to update this new offset
675 2 50       14 my $off = $$dirInfo{ChunkOffset} or $et->Warn('Internal error. Missing ChunkOffset'), last;
676 2         5 my $newOff;
677 2 50       8 if ($noff == 4) {
    0          
    0          
678 2         1245 $newOff = [ 'stco_iloc', $$boxPos{iloc}[0] + $$boxPos{iloc}[1] + $poff, $noff, 0, $id ];
679             } elsif ($noff == 8) {
680 0         0 $newOff = [ 'co64_iloc', $$boxPos{iloc}[0] + $$boxPos{iloc}[1] + $poff, $noff, 0, $id ];
681             } elsif ($noff == 0) {
682             # offset_size is zero, so store the offset in base_offset instead
683 0 0       0 if ($nbas == 4) {
    0          
684 0         0 $newOff = [ 'stco_iloc', $$boxPos{iloc}[0] + $$boxPos{iloc}[1] + $pbas, $nbas, 0, $id ];
685             } elsif ($nbas == 8) {
686 0         0 $newOff = [ 'co64_iloc', $$boxPos{iloc}[0] + $$boxPos{iloc}[1] + $pbas, $nbas, 0, $id ];
687             } else {
688 0         0 $et->Warn("Can't create $name. Invalid iloc offset+base size");
689 0         0 last;
690             }
691             } else {
692 0         0 $et->Warn("Can't create $name. Invalid iloc offset size");
693 0         0 last;
694             }
695             # add directory as a new mdat chunk
696 2         9 push @$off, $newOff;
697 2         10 push @mdatEdit, [ 0, 0, \$newVal, $id ];
698 2         7 $usedID{$id} = 1;
699 2   50     11 $countNew = ($countNew || 0) + 1;
700 2         15 $$et{CHANGED} = $changed; # set this later if successful in editing mdat
701             }
702             }
703 3 100       15 if ($countNew) {
704             # insert new entries into iinf, iref and iloc boxes,
705             # and add new pitm box after hdlr if necessary
706 2         4 my $added = 0;
707 2         4 my $tag;
708 2         18 foreach $tag (sort { $$boxPos{$a}[0] <=> $$boxPos{$b}[0] } keys %$boxPos) {
  19         37  
709 12 100       30 next unless $add{$tag};
710 6         13 my $pos = $$boxPos{$tag}[0] + $added;
711 6 50 33     20 unless ($$boxPos{$tag}[1]) {
712 0 0       0 $tag eq 'iref' or $et->Error('Internal error adding iref box'), last;
713             # create new iref box
714             $add{$tag} = Set32u(12 + length $add{$tag}) . $tag .
715 0         0 Set8u($$boxPos{$tag}[2]) . "\0\0\0" . $add{$tag};
716             } elsif ($tag ne 'hdlr') {
717             my $n = Get32u($outfile, $pos);
718             Set32u($n + length($add{$tag}), $outfile, $pos); # increase box size
719             }
720 6 100       25 if ($tag eq 'iinf') {
    100          
    50          
    0          
721 2         7 my $iinfVer = Get8u($outfile, $pos + 8);
722 2 50       9 if ($iinfVer == 0) {
723 0         0 my $n = Get16u($outfile, $pos + 12);
724 0         0 Set16u($n + $countNew, $outfile, $pos + 12); # incr count
725             } else {
726 2         8 my $n = Get32u($outfile, $pos + 12);
727 2         8 Set32u($n + $countNew, $outfile, $pos + 12); # incr count
728             }
729             } elsif ($tag eq 'iref') {
730             # nothing more to do
731             } elsif ($tag eq 'iloc') {
732 2         7 my $ilocVer = Get8u($outfile, $pos + 8);
733 2 50       727 if ($ilocVer < 2) {
734 2         13 my $n = Get16u($outfile, $pos + 14);
735 2         12 Set16u($n + $countNew, $outfile, $pos + 14); # incr count
736             } else {
737 0         0 my $n = Get32u($outfile, $pos + 14);
738 0         0 Set32u($n + $countNew, $outfile, $pos + 14); # incr count
739             }
740             # must also update pointer locations in this box
741 2 50       9 if ($added) {
742 0         0 $$_[1] += $added foreach @{$$dirInfo{ChunkOffset}};
  0         0  
743             }
744             } elsif ($tag ne 'hdlr') {
745 0         0 next;
746             }
747             # add new entries to this box (or add pitm after hdlr)
748 6         17 substr($$outfile, $pos + $$boxPos{$tag}[1], 0) = $add{$tag};
749 6         13 $added += length $add{$tag}; # positions are shifted by length of new entries
750             }
751             }
752 3         11 delete $$et{ItemInfo};
753 3 50       59 return @mdatEdit ? \@mdatEdit : undef;
754             }
755              
756             #------------------------------------------------------------------------------
757             # Write a series of QuickTime atoms from file or in memory
758             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
759             # Returns: A) if dirInfo contains DataPt: new directory data
760             # B) otherwise: true on success, 0 if a write error occurred
761             # (true but sets an Error on a file format error)
762             # Notes: Yes, this is a real mess. Just like the QuickTime metadata situation.
763             sub WriteQuickTime($$$)
764             {
765 809     809 0 1531 local $_;
766 809         1787 my ($et, $dirInfo, $tagTablePtr) = @_;
767 809 100       3636 $et or return 1; # allow dummy access to autoload this package
768 305         965 my ($mdat, @mdat, @mdatEdit, $edit, $track, $outBuff, $co, $term, $delCount);
769 305         0 my (%langTags, $canCreate, $delGrp, %boxPos, %didDir, $writeLast, $err, $atomCount);
770 305   50     872 my $outfile = $$dirInfo{OutFile} || return 0;
771 305         716 my $raf = $$dirInfo{RAF}; # (will be null for lower-level atoms)
772 305         550 my $dataPt = $$dirInfo{DataPt}; # (will be null for top-level atoms)
773 305         517 my $dirName = $$dirInfo{DirName};
774 305   100     1013 my $dirStart = $$dirInfo{DirStart} || 0;
775 305         532 my $parent = $$dirInfo{Parent};
776 305         517 my $addDirs = $$et{ADD_DIRS};
777 305         542 my $didTag = $$et{DidTag};
778 305         588 my $newTags = { };
779 305         492 my $createKeys = 0;
780 305 100       772 my ($rtnVal, $rtnErr) = $dataPt ? (undef, undef) : (1, 0);
781              
782 305 100       611 if ($dataPt) {
783 287         996 $raf = new File::RandomAccess($dataPt);
784             } else {
785 18 50       59 return 0 unless $raf;
786             }
787             # use buffered output for everything but 'mdat' atoms
788 305         554 $outBuff = '';
789 305         547 $outfile = \$outBuff;
790              
791 305 100       814 $raf->Seek($dirStart, 1) if $dirStart; # skip header if it exists
792              
793 305         511 my $curPath = join '-', @{$$et{PATH}};
  305         1089  
794 305         724 my ($dir, $writePath) = ($dirName, $dirName);
795 305         1361 $writePath = "$dir-$writePath" while defined($dir = $$et{DirMap}{$dir});
796             # hack to create Keys directories if necessary (its containing Meta is in a different location)
797 305 100 100     1415 if ($$addDirs{Keys} and $curPath =~ /^MOV-Movie(-Meta)?$/) {
    100          
798 8         21 $createKeys = 1; # create new Keys directories
799             } elsif ($curPath eq 'MOV-Movie-Meta-ItemList') {
800 9         23 $createKeys = 2; # create new Keys tags
801 9         22 my $keys = $$et{Keys};
802 9 50       34 if ($keys) {
803             # add new tag entries for existing Keys tags, now that we know their ID's
804             # - first make lookup to convert Keys tagInfo ref to index number
805 9         19 my ($index, %keysInfo);
806 9         21 foreach $index (keys %{$$keys{Info}}) {
  9         40  
807 15 100       63 $keysInfo{$$keys{Info}{$index}} = $index if $$keys{Remap}{$index};
808             }
809 9         43 my $keysTable = GetTagTable('Image::ExifTool::QuickTime::Keys');
810 9         50 my $newKeysTags = $et->GetNewTagInfoHash($keysTable);
811 9         45 foreach (keys %$newKeysTags) {
812 12         23 my $tagInfo = $$newKeysTags{$_};
813 12   100     57 $index = $keysInfo{$tagInfo} || ($$tagInfo{SrcTagInfo} and $keysInfo{$$tagInfo{SrcTagInfo}});
814 12 100       51 next unless $index;
815 2         27 my $id = Set32u($index);
816 2 100       17 if ($$tagInfo{LangCode}) {
817             # add to lookup of language tags we are writing with this ID
818 1 50       7 $langTags{$id} = { } unless $langTags{$id};
819 1         5 $langTags{$id}{$_} = $tagInfo;
820 1         5 $id .= '-' . $$tagInfo{LangCode};
821             }
822 2         8 $$newTags{$id} = $tagInfo;
823             }
824             }
825             } else {
826             # get hash of new tags to edit/create in this directory
827 288         1069 $newTags = $et->GetNewTagInfoHash($tagTablePtr);
828             # make lookup of language tags for each ID
829 288         872 foreach (keys %$newTags) {
830 32 100 66     183 next unless $$newTags{$_}{LangCode} and $$newTags{$_}{SrcTagInfo};
831 2         8 my $id = $$newTags{$_}{SrcTagInfo}{TagID};
832 2 50       9 $langTags{$id} = { } unless $langTags{$id};
833 2         7 $langTags{$id}{$_} = $$newTags{$_};
834             }
835             }
836 305 100 100     1154 if ($curPath eq $writePath or $createKeys) {
837 114         182 $canCreate = 1;
838 114         271 $delGrp = $$et{DEL_GROUP}{$dirName};
839             }
840 305 100       842 $atomCount = $$tagTablePtr{VARS}{ATOM_COUNT} if $$tagTablePtr{VARS};
841              
842 305         470 for (;;) { # loop through all atoms at this level
843 1347 0 33     3333 if (defined $atomCount and --$atomCount < 0 and $dataPt) {
      33        
844             # stop processing now and just copy the rest of the atom
845 0 0       0 Write($outfile, substr($$dataPt, $raf->Tell())) or $rtnVal=$rtnErr, $err=1;
846 0         0 last;
847             }
848 1347         2396 my ($hdr, $buff, $keysIndex);
849 1347         4040 my $n = $raf->Read($hdr, 8);
850 1347 100       3272 unless ($n == 8) {
851 305 50 33     1068 if ($n == 4 and $hdr eq "\0\0\0\0") {
    50          
852             # "for historical reasons" the udta is optionally terminated by 4 zeros (ref 1)
853             # --> hold this terminator to the end
854 0         0 $term = $hdr;
855             } elsif ($n != 0) {
856 0         0 $et->Error("Unknown $n bytes at end of file", 1);
857             }
858 305         522 last;
859             }
860 1042         2927 my $size = Get32u(\$hdr, 0) - 8; # (atom size without 8-byte header)
861 1042         2329 my $tag = substr($hdr, 4, 4);
862 1042 100       3236 if ($size == -7) {
    50          
    50          
863             # read the extended size
864 3 50       41 $raf->Read($buff, 8) == 8 or $et->Error('Truncated extended atom'), last;
865 3         23 $hdr .= $buff;
866 3         21 my ($hi, $lo) = unpack('NN', $buff);
867 3 50 33     25 if ($hi or $lo > 0x7fffffff) {
868 0 0       0 if ($hi > 0x7fffffff) {
    0          
869 0         0 $et->Error('Invalid atom size');
870 0         0 last;
871             } elsif (not $et->Options('LargeFileSupport')) {
872 0         0 $et->Error('End of processing at large atom (LargeFileSupport not enabled)');
873 0         0 last;
874             }
875             }
876 3         65 $size = $hi * 4294967296 + $lo - 16;
877 3 50       18 $size < 0 and $et->Error('Invalid extended atom size'), last;
878             } elsif ($size == -8) {
879 0 0       0 if ($dataPt) {
880 0 0       0 last if $$dirInfo{DirName} eq 'CanonCNTH'; # (this is normal for Canon CNTH atom)
881 0         0 my $pos = $raf->Tell() - 4;
882 0         0 $raf->Seek(0,2);
883 0         0 my $str = $$dirInfo{DirName} . ' with ' . ($raf->Tell() - $pos) . ' bytes';
884 0         0 $et->Error("Terminator found in $str remaining", 1);
885             } else {
886             # size of zero is only valid for top-level atom, and
887             # indicates the atom extends to the end of file
888             # (save in mdat list to write later; with zero end position to copy rest of file)
889 0         0 push @mdat, [ $raf->Tell(), 0, $hdr ];
890             }
891 0         0 last;
892             } elsif ($size < 0) {
893 0         0 $et->Error('Invalid atom size');
894 0         0 last;
895             }
896              
897             # keep track of 'mdat' atom locations for writing later
898 1042 100       3067 if ($tag eq 'mdat') {
    50          
    50          
899 21 50       75 if ($dataPt) {
900 0         0 $et->Error("'mdat' not at top level");
901 0         0 last;
902             }
903 21         97 push @mdat, [ $raf->Tell(), $raf->Tell() + $size, $hdr ];
904 21 50       99 $raf->Seek($size, 1) or $et->Error("Seek error in mdat atom"), return $rtnVal;
905 21         75 next;
906             } elsif ($tag eq 'cmov') {
907 0         0 $et->Error("Can't yet write compressed movie metadata");
908 0         0 return $rtnVal;
909             } elsif ($tag eq 'wide') {
910 0         0 next; # drop 'wide' tag
911             }
912              
913             # read the atom data
914 1021         1541 my $got;
915 1021 100       1873 if (not $size) {
916 2         5 $buff = '';
917 2         7 $got = 0;
918             } else {
919             # read the atom data (but only first 64kB if data is huge)
920 1019 50       2970 $got = $raf->Read($buff, $size > $maxReadLen ? 0x10000 : $size);
921             }
922 1021 50       2251 if ($got != $size) {
923             # ignore up to 256 bytes of garbage at end of file
924 0 0 0     0 if ($got <= 256 and $size >= 1024 and $tag ne 'mdat') {
      0        
925 0         0 my $bytes = $got + length $hdr;
926 0 0       0 if ($$et{OPTIONS}{IgnoreMinorErrors}) {
927 0         0 $et->Warn("Deleted garbage at end of file ($bytes bytes)");
928 0         0 $buff = $hdr = '';
929             } else {
930 0         0 $et->Error("Possible garbage at end of file ($bytes bytes)", 1);
931 0         0 return $rtnVal;
932             }
933             } else {
934 0         0 $tag = PrintableTagID($tag,3);
935 0 0 0     0 if ($size > $maxReadLen and $got == 0x10000) {
936 0         0 my $mb = int($size / 0x100000 + 0.5);
937 0         0 $et->Error("'${tag}' atom is too large for rewriting ($mb MB)");
938             } else {
939 0         0 $et->Error("Truncated '${tag}' atom");
940             }
941 0         0 return $rtnVal;
942             }
943             }
944             # save the handler type for this track
945 1021 100 66     2719 if ($tag eq 'hdlr' and length $buff >= 12) {
946 75         177 my $hdlr = substr($buff,8,4);
947 75 100       435 $$et{HandlerType} = $hdlr if $hdlr =~ /^(vide|soun)$/;
948             }
949              
950             # if this atom stores offsets, save its location so we can fix up offsets later
951             # (are there any other atoms that may store absolute file offsets?)
952 1021 100       3837 if ($tag =~ /^(stco|co64|iloc|mfra|moof|sidx|saio|gps |CTBO|uuid)$/) {
953             # (note that we only need to do this if the media data is stored in this file)
954 49         155 my $flg = $$et{QtDataFlg};
955 49 50 33     688 if ($tag eq 'mfra' or $tag eq 'moof') {
    50 33        
    100 100        
    50          
    100          
    50          
    50          
    50          
956 0         0 $et->Error("Can't yet handle movie fragments when writing");
957 0         0 return $rtnVal;
958             } elsif ($tag eq 'sidx' or $tag eq 'saio') {
959 0         0 $et->Error("Can't yet handle $tag box when writing");
960 0         0 return $rtnVal;
961             } elsif ($tag eq 'iloc') {
962 3 50       20 Handle_iloc($et, $dirInfo, \$buff, $outfile) or $et->Error('Error parsing iloc atom');
963             } elsif ($tag eq 'gps ') {
964             # (only care about the 'gps ' box in 'moov')
965 0 0 0     0 if ($$dirInfo{DirID} and $$dirInfo{DirID} eq 'moov' and length $buff > 8) {
      0        
966 0         0 my $off = $$dirInfo{ChunkOffset};
967 0         0 my $num = Get32u(\$buff, 4);
968 0 0       0 $num = int((length($buff) - 8) / 8) if $num * 8 + 8 > length($buff);
969 0         0 my $i;
970 0         0 for ($i=0; $i<$num; ++$i) {
971 0         0 push @$off, [ 'stco_gps ', length($$outfile) + length($hdr) + 8 + $i * 8, 4 ];
972             }
973             }
974             } elsif ($tag eq 'CTBO' or $tag eq 'uuid') { # hack for updating CR3 CTBO offsets
975 13         27 push @{$$dirInfo{ChunkOffset}}, [ $tag, length($$outfile), length($hdr) + $size ];
  13         96  
976             } elsif (not $flg) {
977 0   0     0 my $grp = $$et{CUR_WRITE_GROUP} || $parent;
978 0         0 $et->Error("Can't locate data reference to update offsets for $grp");
979 0         0 return $rtnVal;
980             } elsif ($flg == 3) {
981 0         0 $et->Error("Can't write files with mixed internal/external media data");
982 0         0 return $rtnVal;
983             } elsif ($flg == 1) {
984             # must update offsets since the data is in this file
985 33         60 push @{$$dirInfo{ChunkOffset}}, [ $tag, length($$outfile) + length($hdr), $size ];
  33         153  
986             }
987             }
988              
989             # rewrite this atom
990 1021         3193 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag, \$buff);
991              
992             # call write hook if it exists
993 1021 100 100     4015 &{$$tagInfo{WriteHook}}($buff,$et) if $tagInfo and $$tagInfo{WriteHook};
  21         106  
994              
995             # allow numerical tag ID's (ItemList entries defined by Keys)
996 1021 50 100     2872 if (not $tagInfo and $dirName eq 'ItemList' and $$et{Keys}) {
      66        
997 21         55 $keysIndex = unpack('N', $tag);
998 21         56 my $newIndex = $$et{Keys}{Remap}{$keysIndex};
999 21 50       58 if (defined $newIndex) {
1000 21         53 $tagInfo = $$et{Keys}{Info}{$keysIndex};
1001 21 100       56 unless ($newIndex) {
1002 7 100       19 if ($tagInfo) {
1003 1         8 $et->VPrint(1," - Keys:$$tagInfo{Name}");
1004             } else {
1005 6   100     23 $delCount = ($delCount || 0) + 1;
1006             }
1007 7         16 ++$$et{CHANGED};
1008 7         16 next;
1009             }
1010             # use the new Keys index of this item if it changed
1011 14 100       40 unless ($keysIndex == $newIndex) {
1012 2         6 $tag = Set32u($newIndex);
1013 2         8 substr($hdr, 4, 4) = $tag;
1014             }
1015             } else {
1016 0         0 undef $keysIndex;
1017             }
1018             }
1019             # delete all ItemList tags when deleting group, but take care not to delete UserData Meta
1020 1014 100       2023 if ($delGrp) {
1021 77 100 66     293 if ($dirName eq 'ItemList') {
    100 100        
1022 44   100     94 $delCount = ($delCount || 0) + 1;
1023 44         73 ++$$et{CHANGED};
1024 44         76 next;
1025             } elsif ($dirName eq 'UserData' and (not $tagInfo or not $$tagInfo{SubDirectory})) {
1026 14   100     34 $delCount = ($delCount || 0) + 1;
1027 14         23 ++$$et{CHANGED};
1028 14         28 next;
1029             }
1030             }
1031 956 50 66     3033 undef $tagInfo if $tagInfo and $$tagInfo{Unknown};
1032              
1033 956 50 66     3465 if ($tagInfo and (not defined $$tagInfo{Writable} or $$tagInfo{Writable})) {
      100        
1034 785         1728 my $subdir = $$tagInfo{SubDirectory};
1035 785         1327 my ($newData, @chunkOffset);
1036              
1037 785 100       1540 if ($subdir) { # process atoms in this container from a buffer in memory
1038              
1039 565 100       1256 undef $$et{HandlerType} if $tag eq 'trak'; # init handler type for this track
1040              
1041 565   66     2109 my $subName = $$subdir{DirName} || $$tagInfo{Name};
1042 565   100     1801 my $start = $$subdir{Start} || 0;
1043 565   100     2158 my $base = ($$dirInfo{Base} || 0) + $raf->Tell() - $size;
1044 565         929 my $dPos = 0;
1045 565         846 my $hdrLen = $start;
1046 565 50       1410 if ($$subdir{Base}) {
1047 0         0 my $localBase = eval $$subdir{Base};
1048 0         0 $dPos -= $localBase;
1049 0         0 $base -= $dPos;
1050             # get length of header before base offset
1051 0 0       0 $hdrLen -= $localBase if $localBase <= $hdrLen;
1052             }
1053             my %subdirInfo = (
1054             Parent => $dirName,
1055             DirName => $subName,
1056             Name => $$tagInfo{Name},
1057             DirID => $tag,
1058             DataPt => \$buff,
1059             DataLen => $size,
1060             DataPos => $dPos,
1061             DirStart => $start,
1062             DirLen => $size - $start,
1063             Base => $base,
1064             HasData => $$subdir{HasData},
1065             Multi => $$subdir{Multi}, # necessary?
1066             OutFile => $outfile,
1067             NoRefTest=> 1, # don't check directory references
1068             WriteGroup => $$tagInfo{WriteGroup},
1069             # initialize array to hold details about chunk offset table
1070             # (each entry has 3-5 items: 0=atom type, 1=table offset, 2=table size,
1071             # 3=optional base offset, 4=optional item ID)
1072 565         5630 ChunkOffset => \@chunkOffset,
1073             );
1074             # set InPlace flag so XMP will be padded properly when
1075             # QuickTimePad is used if this is an XMP directory
1076 565 50       2362 $subdirInfo{InPlace} = 2 if $et->Options('QuickTimePad');
1077             # pass the header pointer if necessary (for EXIF IFD's
1078             # where the Base offset is at the end of the header)
1079 565 100 66     1780 if ($hdrLen and $hdrLen < $size) {
1080 62         199 my $header = substr($buff,0,$hdrLen);
1081 62         207 $subdirInfo{HeaderPtr} = \$header;
1082             }
1083 565 100 66     1610 SetByteOrder('II') if $$subdir{ByteOrder} and $$subdir{ByteOrder} =~ /^Little/;
1084 565         1060 my $oldWriteGroup = $$et{CUR_WRITE_GROUP};
1085 565 100       1247 if ($subName eq 'Track') {
1086 33 100       136 $track or $track = 0;
1087 33         172 $$et{CUR_WRITE_GROUP} = 'Track' . (++$track);
1088             }
1089 565         1626 my $subTable = GetTagTable($$subdir{TagTable});
1090             # demote non-QuickTime errors to warnings
1091 565 100       2722 $$et{DemoteErrors} = 1 unless $$subTable{GROUPS}{0} eq 'QuickTime';
1092 565         1022 my $oldChanged = $$et{CHANGED};
1093 565         2977 $newData = $et->WriteDirectory(\%subdirInfo, $subTable, $$subdir{WriteProc});
1094 565 100       2039 if ($$et{DemoteErrors}) {
1095             # just copy existing subdirectory if a non-quicktime error occurred
1096 33 50       168 $$et{CHANGED} = $oldChanged if $$et{DemoteErrors} > 1;
1097 33         77 delete $$et{DemoteErrors};
1098             }
1099 565 50 100     2016 if (defined $newData and not length $newData and ($$tagInfo{Permanent} or
      33        
      66        
1100             ($$tagTablePtr{PERMANENT} and not defined $$tagInfo{Permanent})))
1101             {
1102             # do nothing if trying to delete tag from a PERMANENT table
1103 0         0 $$et{CHANGED} = $oldChanged;
1104 0         0 undef $newData;
1105             }
1106 565         1087 $$et{CUR_WRITE_GROUP} = $oldWriteGroup;
1107 565         5542 SetByteOrder('MM');
1108             # add back header if necessary
1109 565 100 100     1784 if ($start and defined $newData and (length $newData or
      66        
      66        
1110             (defined $$tagInfo{Permanent} and not $$tagInfo{Permanent})))
1111             {
1112 56         239 $newData = substr($buff,0,$start) . $newData;
1113 56         208 $$_[1] += $start foreach @chunkOffset;
1114             }
1115             # the directory exists, so we don't need to add it
1116 565 100 100     2102 if ($curPath eq $writePath and $$addDirs{$subName} and $$addDirs{$subName} eq $dirName) {
      100        
1117 34         98 delete $$addDirs{$subName};
1118             }
1119 565         3012 $didDir{$tag} = 1; # (note: keyed by tag ID)
1120              
1121             } else { # modify existing QuickTime tags in various formats
1122              
1123 220         850 my $nvHash = $et->GetNewValueHash($tagInfo);
1124 220 100 100     1155 if ($nvHash or $langTags{$tag} or $delGrp) {
      66        
1125 9         13 my $nvHashNoLang = $nvHash;
1126 9         31 my ($val, $len, $lang, $type, $flags, $ctry, $charsetQuickTime);
1127 9         22 my $format = $$tagInfo{Format};
1128 9   66     51 my $hasData = ($$dirInfo{HasData} and $buff =~ /\0...data\0/s);
1129 9         17 my $langInfo = $tagInfo;
1130 9 100 33     54 if ($hasData) {
    50 50        
    50 33        
1131 5         9 my $pos = 0;
1132 5         11 for (;;$pos+=$len) {
1133 10 100       27 if ($pos + 16 > $size) {
1134             # add any new alternate language tags now
1135 5 100       15 if ($langTags{$tag}) {
1136 1         3 my $tg;
1137 1         3 foreach $tg ('', sort keys %{$langTags{$tag}}) {
  1         5  
1138 2 100       8 my $ti = $tg ? $langTags{$tag}{$tg} : $nvHashNoLang;
1139 2         6 $nvHash = $et->GetNewValueHash($ti);
1140 2 100 66     10 next unless $nvHash and not $$didTag{$nvHash};
1141 1         4 $$didTag{$nvHash} = 1;
1142 1 50 33     7 next unless $$nvHash{IsCreating} and $et->IsOverwriting($nvHash);
1143 1         5 my $newVal = $et->GetNewValue($nvHash);
1144 1 50       5 next unless defined $newVal;
1145 1         3 my $prVal = $newVal;
1146 1         6 my $flags = FormatQTValue($et, \$newVal, $tagInfo, $format);
1147 1 50       4 next unless defined $newVal;
1148 1         3 my ($ctry, $lang) = (0, 0);
1149 1 50       4 if ($$ti{LangCode}) {
1150 1 50       9 unless ($$ti{LangCode} =~ /^([A-Z]{3})?[-_]?([A-Z]{2})?$/i) {
1151 0         0 $et->Warn("Invalid language code for $$ti{Name}");
1152 0         0 next;
1153             }
1154             # pack language and country codes
1155 1 50 33     11 if ($1 and $1 ne 'und') {
1156 1         9 $lang = ($lang << 5) | ($_ - 0x60) foreach unpack 'C*', lc($1);
1157             }
1158 1 50 33     11 $ctry = unpack('n', pack('a2',uc($2))) if $2 and $2 ne 'ZZ';
1159             }
1160 1 50       5 $newData = substr($buff, 0, $pos) unless defined $newData;
1161 1         7 $newData .= pack('Na4Nnn',16+length($newVal),'data',$flags,$ctry,$lang).$newVal;
1162 1         5 my $grp = $et->GetGroup($ti, 1);
1163 1         12 $et->VerboseValue("+ $grp:$$ti{Name}", $prVal);
1164 1         5 ++$$et{CHANGED};
1165             }
1166             }
1167 5         11 last;
1168             }
1169 5         36 ($len, $type, $flags, $ctry, $lang) = unpack("x${pos}Na4Nnn", $buff);
1170 5 50       19 $lang or $lang = $undLang; # treat both 0 and 'und' as 'und'
1171 5         8 $langInfo = $tagInfo;
1172 5         16 my $delTag = $delGrp;
1173 5         8 my $newVal;
1174 5         24 my $langCode = GetLangCode($lang, $ctry, 1);
1175 5         11 for (;;) {
1176 5         15 $langInfo = GetLangInfo($tagInfo, $langCode);
1177 5         22 $nvHash = $et->GetNewValueHash($langInfo);
1178 5 0 33     38 last if $nvHash or not $ctry or $lang ne $undLang or length($langCode)==2;
      33        
      33        
1179             # check to see if tag was written with a 2-char country code only
1180 0         0 $langCode = lc unpack('a2',pack('n',$ctry));
1181             }
1182             # set flag to delete language tag when writing default
1183             # (except for a default-language Keys entry)
1184 5 100 66     22 if (not $nvHash and $nvHashNoLang) {
1185 4 50 33     57 if ($lang eq $undLang and not $ctry and not $$didTag{$nvHashNoLang}) {
      33        
1186 4         12 $nvHash = $nvHashNoLang; # write existing default
1187             } else {
1188 0         0 $delTag = 1; # delete tag
1189             }
1190             }
1191 5 50       14 last if $pos + $len > $size;
1192 5 50 33     23 if ($type eq 'data' and $len >= 16) {
    0          
1193 5         9 $pos += 16;
1194 5         9 $len -= 16;
1195 5         13 $val = substr($buff, $pos, $len);
1196             # decode value (see QuickTime.pm for an explanation)
1197 5 50       20 if ($stringEncoding{$flags}) {
1198 5         26 $val = $et->Decode($val, $stringEncoding{$flags});
1199 5 50       23 $val =~ s/\0$// unless $$tagInfo{Binary};
1200 5         11 $flags = 0x01; # write all strings as UTF-8
1201             } else {
1202 0 0       0 if ($format) {
1203             # update flags for the format we are writing
1204 0 0 0     0 if ($$tagInfo{Writable} and $qtFormat{$$tagInfo{Writable}}) {
    0          
1205 0         0 $flags = $qtFormat{$$tagInfo{Writable}};
1206             } elsif ($qtFormat{$format}) {
1207 0         0 $flags = $qtFormat{$format};
1208             }
1209             } else {
1210 0         0 $format = QuickTimeFormat($flags, $len);
1211             }
1212 0 0       0 $val = ReadValue(\$val, 0, $format, $$tagInfo{Count}, $len) if $format;
1213             }
1214 5 100 100     37 if (($nvHash and $et->IsOverwriting($nvHash, $val)) or $delTag) {
    50 66        
1215 3 50       14 $newVal = $et->GetNewValue($nvHash) if defined $nvHash;
1216 3 50 33     36 if ($delTag or not defined $newVal or $$didTag{$nvHash}) {
      33        
1217             # delete the tag
1218 0         0 my $grp = $et->GetGroup($langInfo, 1);
1219 0         0 $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
1220             # copy data up to start of this tag to delete this value
1221 0 0       0 $newData = substr($buff, 0, $pos-16) unless defined $newData;
1222 0         0 ++$$et{CHANGED};
1223 0         0 next;
1224             }
1225 3         21 my $prVal = $newVal;
1226             # format new value for writing (and get new flags)
1227 3         13 $flags = FormatQTValue($et, \$newVal, $tagInfo, $format);
1228 3 50       8 next unless defined $newVal;
1229 3         13 my $grp = $et->GetGroup($langInfo, 1);
1230 3         20 $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
1231 3         15 $et->VerboseValue("+ $grp:$$langInfo{Name}", $prVal);
1232 3 50       13 $newData = substr($buff, 0, $pos-16) unless defined $newData;
1233 3 50       9 my $wLang = $lang eq $undLang ? 0 : $lang;
1234 3         15 $newData .= pack('Na4Nnn', length($newVal)+16, $type, $flags, $ctry, $wLang);
1235 3         8 $newData .= $newVal;
1236 3         8 ++$$et{CHANGED};
1237             } elsif (defined $newData) {
1238 0         0 $newData .= substr($buff, $pos-16, $len+16);
1239             }
1240             } elsif (defined $newData) {
1241 0         0 $newData .= substr($buff, $pos, $len);
1242             }
1243 5 100       25 $$didTag{$nvHash} = 1 if $nvHash;
1244             }
1245 5 50 66     38 $newData .= substr($buff, $pos) if defined $newData and $pos < $size;
1246 5         13 undef $val; # (already constructed $newData)
1247             } elsif ($format) {
1248 0         0 $val = ReadValue(\$buff, 0, $format, undef, $size);
1249             } elsif (($tag =~ /^\xa9/ or $$tagInfo{IText}) and $size >= ($$tagInfo{IText} || 4)) {
1250 4         8 my $hdr;
1251 4 50 33     14 if ($$tagInfo{IText} and $$tagInfo{IText} >= 6) {
1252 0         0 my $iText = $$tagInfo{IText};
1253 0         0 my $pos = $iText - 2;
1254 0         0 $lang = unpack("x${pos}n", $buff);
1255 0         0 $hdr = substr($buff,4,$iText-6);
1256 0         0 $len = $size - $iText;
1257 0         0 $val = substr($buff, $iText, $len);
1258             } else {
1259 4         12 ($len, $lang) = unpack('nn', $buff);
1260 4 50       15 $len -= 4 if 4 + $len > $size; # (see QuickTime.pm for explanation)
1261 4 50 33     16 $len = $size - 4 if $len > $size - 4 or $len < 0;
1262 4         11 $val = substr($buff, 4, $len);
1263             }
1264 4 50       10 $lang or $lang = $undLang; # treat both 0 and 'und' as 'und'
1265 4         7 my $enc;
1266 4 50 33     12 if ($lang < 0x400 and $val !~ /^\xfe\xff/) {
1267 0         0 $charsetQuickTime = $et->Options('CharsetQuickTime');
1268 0         0 $enc = $charsetQuickTime;
1269             } else {
1270 4 50       14 $enc = $val=~s/^\xfe\xff// ? 'UTF16' : 'UTF8';
1271             }
1272 4 50       10 unless ($$tagInfo{NoDecode}) {
1273 4         15 $val = $et->Decode($val, $enc);
1274 4         11 $val =~ s/\0+$//; # remove trailing nulls if they exist
1275             }
1276 4 50       11 $val = $hdr . $val if defined $hdr;
1277 4         16 my $langCode = UnpackLang($lang, 1);
1278 4         14 $langInfo = GetLangInfo($tagInfo, $langCode);
1279 4         15 $nvHash = $et->GetNewValueHash($langInfo);
1280 4 100 66     19 if (not $nvHash and $nvHashNoLang) {
1281 3 50 33     16 if ($lang eq $undLang and not $$didTag{$nvHashNoLang}) {
    0          
1282 3         7 $nvHash = $nvHashNoLang;
1283             } elsif ($canCreate) {
1284             # delete other languages when writing default
1285 0         0 my $grp = $et->GetGroup($langInfo, 1);
1286 0         0 $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
1287 0         0 ++$$et{CHANGED};
1288 0         0 next;
1289             }
1290             }
1291             } else {
1292 0         0 $val = $buff;
1293 0 0 0     0 if ($tag =~ /^\xa9/ or $$tagInfo{IText}) {
1294 0         0 $et->Warn("Corrupted $$tagInfo{Name} value");
1295             }
1296             }
1297 9 100 100     46 if ($nvHash and defined $val) {
1298 3 50       11 if ($et->IsOverwriting($nvHash, $val)) {
1299 3         13 $newData = $et->GetNewValue($nvHash);
1300 3 50 33     13 $newData = '' unless defined $newData or $canCreate;
1301 3         6 ++$$et{CHANGED};
1302 3         25 my $grp = $et->GetGroup($langInfo, 1);
1303 3         19 $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
1304 3 50 33     16 next unless defined $newData and not $$didTag{$nvHash};
1305 3         13 $et->VerboseValue("+ $grp:$$langInfo{Name}", $newData);
1306             # add back necessary header and encode as necessary
1307 3 50 0     8 if (defined $lang) {
    0 0        
    0 0        
      0        
1308 3   50     13 my $iText = $$tagInfo{IText} || 0;
1309 3         5 my $hdr;
1310 3 50       8 if ($iText > 6) {
1311 0 0       0 $newData .= ' 'x($iText-6) if length($newData) < $iText-6;
1312 0         0 $hdr = substr($newData, 0, $iText-6);
1313 0         0 $newData = substr($newData, $iText-6);
1314             }
1315 3 50       9 unless ($$tagInfo{NoDecode}) {
1316 3 50       14 $newData = $et->Encode($newData, $lang < 0x400 ? $charsetQuickTime : 'UTF8');
1317             }
1318 3 50       8 my $wLang = $lang eq $undLang ? 0 : $lang;
1319 3 50       7 if ($iText < 6) {
    0          
1320 3         13 $newData = pack('nn', length($newData), $wLang) . $newData;
1321             } elsif ($iText == 6) {
1322 0         0 $newData = pack('Nn', 0, $wLang) . $newData . "\0";
1323             } else {
1324 0         0 $newData = "\0\0\0\0" . $hdr . pack('n', $wLang) . $newData . "\0";
1325             }
1326             } elsif (not $format or $format =~ /^string/ and
1327             not $$tagInfo{Binary} and not $$tagInfo{ValueConv})
1328             {
1329             # write all strings as UTF-8
1330 0         0 $newData = $et->Encode($newData, 'UTF8');
1331             } elsif ($format and not $$tagInfo{Binary}) {
1332             # format new value for writing
1333 0         0 $newData = WriteValue($newData, $format);
1334             }
1335             }
1336 3         13 $$didTag{$nvHash} = 1; # set flag so we don't add this tag again
1337             }
1338             }
1339             }
1340             # write the new atom if it was modified
1341 785 100       1868 if (defined $newData) {
1342 394         771 my $sizeDiff = length($buff) - length($newData);
1343             # pad to original size if specified, otherwise give verbose message about the changed size
1344 394 50 100     1395 if ($sizeDiff > 0 and $$tagInfo{PreservePadding} and $et->Options('QuickTimePad')) {
    100 66        
1345 0         0 $newData .= "\0" x $sizeDiff;
1346 0         0 $et->VPrint(1, " ($$tagInfo{Name} padded to original size)");
1347             } elsif ($sizeDiff) {
1348 67         355 $et->VPrint(1, " ($$tagInfo{Name} changed size)");
1349             }
1350 394         781 my $len = length($newData) + 8;
1351 394 50       829 $len > 0x7fffffff and $et->Error("$$tagInfo{Name} to large to write"), last;
1352             # update size in ChunkOffset list for modified 'uuid' atom
1353 394 100       838 $$dirInfo{ChunkOffset}[-1][2] = $len if $tag eq 'uuid';
1354 394 100       794 next unless $len > 8; # don't write empty atom header
1355             # maintain pointer to chunk offsets if necessary
1356 385 100       868 if (@chunkOffset) {
1357 153         550 $$_[1] += 8 + length $$outfile foreach @chunkOffset;
1358 153         257 push @{$$dirInfo{ChunkOffset}}, @chunkOffset;
  153         394  
1359             }
1360 385 100       917 if ($$tagInfo{WriteLast}) {
1361 1   50     26 $writeLast = ($writeLast || '') . Set32u($len) . $tag . $newData;
1362             } else {
1363 384         1113 $boxPos{$tag} = [ length($$outfile), length($newData) + 8 ];
1364             # write the updated directory with its atom header
1365 384 50       1424 Write($outfile, Set32u($len), $tag, $newData) or $rtnVal=$rtnErr, $err=1, last;
1366             }
1367 385         1112 next;
1368             }
1369             }
1370             # keep track of data references in this track
1371 562 50 66     2798 if ($tag eq 'dinf') {
    100 66        
    100          
1372 0         0 $$et{QtDataRef} = [ ]; # initialize list of data references
1373             } elsif ($parent eq 'DataInfo' and length($buff) >= 4) {
1374             # save data reference type and version/flags
1375 33         90 push @{$$et{QtDataRef}}, [ $tag, Get32u(\$buff,0) ];
  33         146  
1376             } elsif ($tag eq 'stsd' and length($buff) >= 8) {
1377 33         151 my $n = Get32u(\$buff, 4); # get number of sample descriptions in table
1378 33         140 my ($pos, $flg) = (8, 0);
1379 33         62 my ($i, $msg);
1380 33         123 for ($i=0; $i<$n; ++$i) { # loop through sample descriptions
1381 33 50       125 $pos + 16 <= length($buff) or $msg = 'Truncated sample table', last;
1382 33         99 my $siz = Get32u(\$buff, $pos);
1383 33 50       131 $pos + $siz <= length($buff) or $msg = 'Truncated sample table', last;
1384 33         118 my $drefIdx = Get16u(\$buff, $pos + 14);
1385 33         113 my $drefTbl = $$et{QtDataRef};
1386 33 50 33     230 if (not $drefIdx) {
    50          
1387 0         0 $flg |= 0x01; # in this file if data reference index is 0 (if like iloc)
1388             } elsif ($drefTbl and $$drefTbl[$drefIdx-1]) {
1389 33         83 my $dref = $$drefTbl[$drefIdx-1];
1390             # $flg = 0x01-in this file, 0x02-in some other file
1391 33 50 33     178 $flg |= ($$dref[1] == 1 and $$dref[0] ne 'rsrc') ? 0x01 : 0x02;
1392             } else {
1393 0         0 $msg = "No data reference for sample description $i";
1394 0         0 last;
1395             }
1396 33         105 $pos += $siz;
1397             }
1398 33 50       100 if ($msg) {
1399             # (allow empty sample description for non-audio/video handler types, eg. 'url ', 'meta')
1400 0 0       0 if ($$et{HandlerType}) {
1401 0   0     0 my $grp = $$et{CUR_WRITE_GROUP} || $parent;
1402 0         0 $et->Error("$msg for $grp");
1403 0         0 return $rtnErr;
1404             }
1405 0         0 $flg = 1; # (this seems to be the case)
1406             }
1407 33         102 $$et{QtDataFlg} = $flg;
1408             }
1409 562 50 66     1860 if ($tagInfo and $$tagInfo{WriteLast}) {
1410 0   0     0 $writeLast = ($writeLast || '') . $hdr . $buff;
1411             } else {
1412             # save position of this box in the output buffer
1413 562         1916 $boxPos{$tag} = [ length($$outfile), length($hdr) + length($buff) ];
1414             # copy the existing atom
1415 562 50       2088 Write($outfile, $hdr, $buff) or $rtnVal=$rtnErr, $err=1, last;
1416             }
1417             }
1418 305 50       737 $et->VPrint(0, " [deleting $delCount $dirName tag".($delCount==1 ? '' : 's')."]\n") if $delCount;
    100          
1419              
1420 305 100       849 $createKeys &= ~0x01 unless $$addDirs{Keys}; # (Keys may have been written)
1421              
1422             # add new directories/tags at this level if necessary
1423 305 100 100     1074 if ($canCreate and (exists $$et{EDIT_DIRS}{$dirName} or $createKeys)) {
      100        
1424             # get a hash of tagInfo references to add to this directory
1425 79         341 my $dirs = $et->GetAddDirHash($tagTablePtr, $dirName);
1426             # make sorted list of new tags to be added
1427 79         388 my @addTags = sort(keys(%$dirs), keys %$newTags);
1428 79         202 my ($tag, $index);
1429             # add Keys tags if necessary
1430 79 100       277 if ($createKeys) {
1431 15 100 33     119 if ($curPath eq 'MOV-Movie') {
    100          
    50          
1432             # add Meta for Keys if necessary
1433 2 50       11 unless ($didDir{meta}) {
1434 2         8 $$dirs{meta} = $Image::ExifTool::QuickTime::Movie{meta};
1435 2         7 push @addTags, 'meta';
1436             }
1437             } elsif ($curPath eq 'MOV-Movie-Meta') {
1438             # special case for Keys Meta -- reset directories and start again
1439 4         15 undef @addTags;
1440 4         12 $dirs = { };
1441 4         11 foreach ('keys','ilst') {
1442 8 50       31 next if $didDir{$_}; # don't add again
1443 0         0 $$dirs{$_} = $Image::ExifTool::QuickTime::Meta{$_};
1444 0         0 push @addTags, $_;
1445             }
1446             } elsif ($curPath eq 'MOV-Movie-Meta-ItemList' and $$et{Keys}) {
1447 9         25 foreach $index (sort { $a <=> $b } keys %{$$et{Keys}{Add}}) {
  1         8  
  9         48  
1448 5         18 my $id = Set32u($index);
1449 5         19 $$newTags{$id} = $$et{Keys}{Add}{$index};
1450 5         17 push @addTags, $id;
1451             }
1452             } else {
1453 0         0 $dirs = $et->GetAddDirHash($tagTablePtr, $dirName);
1454 0         0 push @addTags, sort keys %$dirs;
1455             }
1456             }
1457             # (note that $tag may be a binary Keys index here)
1458 79         221 foreach $tag (@addTags) {
1459 53   66     221 my $tagInfo = $$dirs{$tag} || $$newTags{$tag};
1460 53 50 33     184 next if defined $$tagInfo{CanCreate} and not $$tagInfo{CanCreate};
1461             next if defined $$tagInfo{HandlerType} and
1462 53 0 0     134 (not $$et{HandlerType} or $$et{HandlerType} ne $$tagInfo{HandlerType});
      33        
1463 53         101 my $subdir = $$tagInfo{SubDirectory};
1464 53 100       127 unless ($subdir) {
1465 39         121 my $nvHash = $et->GetNewValueHash($tagInfo);
1466 39 100 66     205 next unless $nvHash and not $$didTag{$nvHash};
1467 31 100 66     152 next unless $$nvHash{IsCreating} and $et->IsOverwriting($nvHash);
1468 18         69 my $newVal = $et->GetNewValue($nvHash);
1469 18 50       49 next unless defined $newVal;
1470 18         39 my $prVal = $newVal;
1471 18         84 my $flags = FormatQTValue($et, \$newVal, $tagInfo);
1472 18 50       47 next unless defined $newVal;
1473 18         45 my ($ctry, $lang) = (0, 0);
1474             # handle alternate languages
1475 18 50       49 if ($$tagInfo{LangCode}) {
1476 0         0 $tag = substr($tag, 0, 4); # strip language code from tag ID
1477 0 0       0 unless ($$tagInfo{LangCode} =~ /^([A-Z]{3})?[-_]?([A-Z]{2})?$/i) {
1478 0         0 $et->Warn("Invalid language code for $$tagInfo{Name}");
1479 0         0 next;
1480             }
1481             # pack language and country codes
1482 0 0 0     0 if ($1 and $1 ne 'und') {
1483 0         0 $lang = ($lang << 5) | ($_ - 0x60) foreach unpack 'C*', lc($1);
1484             }
1485 0 0 0     0 $ctry = unpack('n', pack('a2',uc($2))) if $2 and $2 ne 'ZZ';
1486             }
1487 18 100 66     98 if ($$dirInfo{HasData}) {
    50 0        
    0          
1488             # add 'data' header
1489 11         86 $newVal = pack('Na4Nnn',16+length($newVal),'data',$flags,$ctry,$lang).$newVal;
1490             } elsif ($tag =~ /^\xa9/ or $$tagInfo{IText}) {
1491 7 50 66     50 if ($ctry) {
    100          
1492 0         0 my $grp = $et->GetGroup($tagInfo,1);
1493 0         0 $et->Warn("Can't use country code for $grp:$$tagInfo{Name}");
1494 0         0 next;
1495             } elsif ($$tagInfo{IText} and $$tagInfo{IText} >= 6) {
1496             # add 6-byte langText header and trailing null
1497             # (with extra junk before language code if IText > 6)
1498 2         6 my $n = $$tagInfo{IText} - 6;
1499 2 50       8 $newVal .= ' ' x $n if length($newVal) < $n;
1500 2         12 $newVal = "\0\0\0\0" . substr($newVal,0,$n) . pack('n',0,$lang) . substr($newVal,$n) . "\0";
1501             } else {
1502             # add IText header
1503 5         27 $newVal = pack('nn',length($newVal),$lang) . $newVal;
1504             }
1505             } elsif ($ctry or $lang) {
1506 0         0 my $grp = $et->GetGroup($tagInfo,1);
1507 0         0 $et->Warn("Can't use language code for $grp:$$tagInfo{Name}");
1508 0         0 next;
1509             }
1510 18 50       62 if ($$tagInfo{WriteLast}) {
1511 0   0     0 $writeLast = ($writeLast || '') . Set32u(8+length($newVal)) . $tag . $newVal;
1512             } else {
1513 18         62 $boxPos{$tag} = [ length($$outfile), 8 + length($newVal) ];
1514 18 50       81 Write($outfile, Set32u(8+length($newVal)), $tag, $newVal) or $rtnVal=$rtnErr, $err=1;
1515             }
1516 18         90 my $grp = $et->GetGroup($tagInfo, 1);
1517 18         139 $et->VerboseValue("+ $grp:$$tagInfo{Name}", $prVal);
1518 18         63 $$didTag{$nvHash} = 1;
1519 18         40 ++$$et{CHANGED};
1520 18         73 next;
1521             }
1522 14   33     54 my $subName = $$subdir{DirName} || $$tagInfo{Name};
1523             # QuickTime hierarchy is complex, so check full directory path before adding
1524 14         31 my $buff;
1525 14 100 66     144 if ($createKeys and $curPath eq 'MOV-Movie' and $subName eq 'Meta') {
    50 100        
    100 66        
      66        
1526 2         11 $et->VPrint(0, " Creating Meta with mdta Handler and Keys\n");
1527             # init Meta box for Keys tags with mdta Handler and empty Keys+ItemList
1528 2         6 $buff = "\0\0\0\x20hdlr\0\0\0\0\0\0\0\0mdta\0\0\0\0\0\0\0\0\0\0\0\0" .
1529             "\0\0\0\x10keys\0\0\0\0\0\0\0\0" .
1530             "\0\0\0\x08ilst";
1531             } elsif ($createKeys and $curPath eq 'MOV-Movie-Meta') {
1532 0 0       0 $buff = ($subName eq 'Keys' ? "\0\0\0\0\0\0\0\0" : '');
1533             } elsif ($subName eq 'Meta' and $$et{OPTIONS}{QuickTimeHandler}) {
1534 2         15 $et->VPrint(0, " Creating Meta with mdir Handler\n");
1535             # init Meta box for ItemList tags with mdir Handler
1536 2         7 $buff = "\0\0\0\x20hdlr\0\0\0\0\0\0\0\0mdir\0\0\0\0\0\0\0\0\0\0\0\0";
1537             } else {
1538 10 50 33     109 next unless $curPath eq $writePath and $$addDirs{$subName} and $$addDirs{$subName} eq $dirName;
      33        
1539 10         31 $buff = ''; # write from scratch
1540             }
1541             my %subdirInfo = (
1542             Parent => $dirName,
1543             DirName => $subName,
1544             DataPt => \$buff,
1545             DirStart => 0,
1546             HasData => $$subdir{HasData},
1547             OutFile => $outfile,
1548             ChunkOffset => [ ], # (just to be safe)
1549             WriteGroup => $$tagInfo{WriteGroup},
1550 14         132 );
1551 14         61 my $subTable = GetTagTable($$subdir{TagTable});
1552 14         84 my $newData = $et->WriteDirectory(\%subdirInfo, $subTable, $$subdir{WriteProc});
1553 14 50 33     110 if ($newData and length($newData) <= 0x7ffffff7) {
1554 14         35 my $prefix = '';
1555             # add atom version or ID if necessary
1556 14 100       45 if ($$subdir{Start}) {
1557 5 100       40 if ($$subdir{Start} == 4) {
1558 2         5 $prefix = "\0\0\0\0"; # a simple version number
1559             } else {
1560             # get UUID from Condition expression
1561 3         9 my $cond = $$tagInfo{Condition};
1562 3 50 33     294 $prefix = eval qq("$1") if $cond and $cond =~ m{=~\s*\/\^(.*)/};
1563 3 50       28 length($prefix) == $$subdir{Start} or $et->Error('Internal UUID error');
1564             }
1565             }
1566 14         64 my $newHdr = Set32u(8+length($newData)+length($prefix)) . $tag . $prefix;
1567 14 100       61 if ($$tagInfo{WriteLast}) {
1568 1   50     18 $writeLast = ($writeLast || '') . $newHdr . $newData;
1569             } else {
1570 13 100       52 if ($tag eq 'uuid') {
1571             # add offset for new uuid (needed for CR3 CTBO offsets)
1572 2         5 my $off = $$dirInfo{ChunkOffset};
1573 2         14 push @$off, [ $tag, length($$outfile), length($newHdr) + length($newData) ];
1574             }
1575 13         52 $boxPos{$tag} = [ length($$outfile), length($newHdr) + length($newData) ];
1576 13 50       55 Write($outfile, $newHdr, $newData) or $rtnVal=$rtnErr, $err=1;
1577             }
1578             }
1579             # add only once (must delete _after_ call to WriteDirectory())
1580             # (Keys is a special case, and will be removed after Meta is processed)
1581 14 50       116 delete $$addDirs{$subName} unless $subName eq 'Keys';
1582             }
1583             }
1584             # write HEIC metadata after top-level 'meta' box has been processed if editing this information
1585 305 50 66     732 if ($curPath eq 'MOV-Meta' and $$et{EDIT_DIRS}{ItemInformation}) {
1586 3         11 $$dirInfo{BoxPos} = \%boxPos;
1587 3         18 my $mdatEdit = WriteItemInfo($et, $dirInfo, $outfile);
1588 3 50       14 if ($mdatEdit) {
1589 3 50       15 $et->Error('Multiple top-level Meta containers') if $$et{mdatEdit};
1590 3         11 $$et{mdatEdit} = $mdatEdit;
1591             }
1592             }
1593             # write out any necessary terminator
1594 305 50 0     726 Write($outfile, $term) or $rtnVal=$rtnErr, $err=1 if $term and length $$outfile;
      33        
1595              
1596             # delete temporary Keys variables after Meta is processed
1597 305 100       644 if ($dirName eq 'Meta') {
1598             # delete any Meta box with no useful information (ie. only 'hdlr','keys','lang','ctry')
1599 24         71 my $isEmpty = 1;
1600 24   66     169 $emptyMeta{$_} or $isEmpty = 0, last foreach keys %boxPos;
1601 24 100       85 if ($isEmpty) {
1602 2 50       12 $et->VPrint(0,' Deleting ' . join('+', sort map { $emptyMeta{$_} } keys %boxPos)) if %boxPos;
  2         19  
1603 2         5 $$outfile = '';
1604 2         5 ++$$et{CHANGED};
1605             }
1606 24 100       87 if ($curPath eq 'MOV-Movie-Meta') {
1607 9         25 delete $$addDirs{Keys}; # prevent creation of another Meta for Keys tags
1608 9         49 delete $$et{Keys};
1609             }
1610             }
1611              
1612             # return now if writing subdirectory
1613 305 100       653 if ($dataPt) {
1614 287 50       629 $et->Error("Internal error: WriteLast not on top-level atom!\n") if $writeLast;
1615 287 50       2907 return $err ? undef : $$outfile;
1616             }
1617              
1618             # issue minor error if we didn't find an 'mdat' atom
1619 18         88 my $off = $$dirInfo{ChunkOffset};
1620 18 50       79 if (not @mdat) {
1621 0         0 foreach $co (@$off) {
1622 0 0       0 next if $$co[0] eq 'uuid';
1623 0         0 $et->Error('Media data referenced but not found');
1624 0         0 return $rtnVal;
1625             }
1626 0         0 $et->Warn('No media data', 1);
1627             }
1628              
1629             # edit mdat blocks as required
1630             # (0=old pos [0 if creating], 1=old end [0 if creating], 2=new data ref or undef to delete,
1631             # 3=new data item id)
1632 18 100       83 if ($$et{mdatEdit}) {
1633 3         10 @mdatEdit = @{$$et{mdatEdit}};
  3         15  
1634 3         10 delete $$et{mdatEdit};
1635             }
1636 18         56 foreach $edit (@mdatEdit) {
1637 5         13 my (@thisMdat, @newMdat, $changed);
1638 5         12 foreach $mdat (@mdat) {
1639             # keep track of all chunks for the mdat with this header
1640 12 100       34 if (length $$mdat[2]) {
1641 10         20 push @newMdat, @thisMdat;
1642 10         21 undef @thisMdat;
1643             }
1644 12         22 push @thisMdat, $mdat;
1645             # is this edit inside this mdat chunk?
1646             # - $$edit[0] and $$edit[1] will both be zero if we are creating a new chunk
1647             # - $$mdat[1] is zero if mdat runs to end of file
1648             # - $$edit[0] == $$edit[1] == $$mdat[0] if reviving a deleted chunk
1649             # - $$mdat[5] is defined if this was a newly added/edited chunk
1650 12 100 100     50 next if defined $$mdat[5] or $changed; # don't replace a newly added chunk
1651 5 50 33     49 if (not $$edit[0] or # (newly created chunk)
      66        
      33        
      33        
      66        
      66        
1652             # (edit is inside chunk)
1653             ((($$edit[0] < $$mdat[1] or not $$mdat[1]) and $$edit[1] > $$mdat[0]) or
1654             # (edit inserted at start or end of chunk)
1655             ($$edit[0] == $$edit[1] and ($$edit[0] == $$mdat[0] or $$edit[0] == $$mdat[1]))))
1656             {
1657 5 100 33     37 if (not $$edit[0]) {
    50 33        
1658 2         6 $$edit[0] = $$edit[1] = $$mdat[0]; # insert at start of this mdat
1659             } elsif ($$edit[0] < $$mdat[0] or ($$edit[1] > $$mdat[1] and $$mdat[1])) {
1660 0         0 $et->Error('ItemInfo runs across mdat boundary');
1661 0         0 return $rtnVal;
1662             }
1663 5         9 my $hdrChunk = $thisMdat[0];
1664 5 50       19 $hdrChunk or $et->Error('Internal error finding mdat header'), return $rtnVal;
1665             # calculate difference in mdat size
1666 5 50       19 my $diff = ($$edit[2] ? length(${$$edit[2]}) : 0) - ($$edit[1] - $$edit[0]);
  5         16  
1667             # edit size of mdat in header if necessary
1668 5 50       17 if ($diff) {
1669 5 50       16 if (length($$hdrChunk[2]) == 8) {
    0          
1670 5         21 my $size = Get32u(\$$hdrChunk[2], 0);
1671 5 50       19 if ($size) { # (0 size = extends to end of file)
1672 5         12 $size += $diff;
1673 5 50       15 $size > 0xffffffff and $et->Error("Can't yet grow mdat across 4GB boundary"), return $rtnVal;
1674 5         17 Set32u($size, \$$hdrChunk[2], 0);
1675             }
1676             } elsif (length($$hdrChunk[2]) == 16) {
1677 0         0 my $size = Get64u(\$$hdrChunk[2], 8);
1678 0 0       0 if ($size) {
1679 0         0 $size += $diff;
1680 0         0 Set64u($size, \$$hdrChunk[2], 8);
1681             }
1682             } else {
1683 0         0 $et->Error('Internal error. Invalid mdat header');
1684 0         0 return $rtnVal;
1685             }
1686             }
1687 5         10 $changed = 1;
1688             # remove the edited section of this chunk (if any) and replace with new data (if any)
1689 5 50       16 if ($$edit[0] > $$mdat[0]) {
1690 0 0       0 push @thisMdat, [ $$edit[0], $$edit[1], '', 0, $$edit[2], $$edit[3] ] if $$edit[2];
1691             # add remaining data after edit (or empty stub in case it is referenced by an offset)
1692 0         0 push @thisMdat, [ $$edit[1], $$mdat[1], '' ];
1693 0         0 $$mdat[1] = $$edit[0]; # now ends at start of edit
1694             } else {
1695 5 50       17 if ($$edit[2]) {
1696             # insert the new chunk before this chunk, moving the header to the new chunk
1697 5         27 splice @thisMdat, -1, 0, [ $$edit[0],$$edit[1],$$mdat[2],0,$$edit[2],$$edit[3] ];
1698 5         13 $$mdat[2] = ''; # (header was moved to new chunk)
1699             # initialize ChunkOffset pointer if necessary
1700 5 50       17 if ($$edit[3]) {
1701 5         10 my $n = 0;
1702 5         16 foreach $co (@$off) {
1703 19 100 66     72 next unless defined $$co[4] and $$co[4] == $$edit[3];
1704 5         10 ++$n;
1705 5 50       16 if ($$co[0] eq 'stco_iloc') {
1706 5         17 Set32u($$mdat[0], $outfile, $$co[1]);
1707             } else {
1708 0         0 Set64u($$mdat[0], $outfile, $$co[1]);
1709             }
1710             }
1711 5 50       20 $n == 1 or $et->Error('Internal error updating chunk offsets');
1712             }
1713             }
1714 5         13 $$mdat[0] = $$edit[1]; # remove old data
1715             }
1716             }
1717             }
1718 5 50       21 if ($changed) {
1719 5         13 @mdat = ( @newMdat, @thisMdat );
1720 5         14 ++$$et{CHANGED};
1721             } else {
1722 0         0 $et->Error('Internal error modifying mdat');
1723             }
1724             }
1725              
1726             # determine our new mdat positions
1727             # (0=old pos, 1=old end, 2=mdat header, 3=new pos, 4=new data ref if changed, 5=new item ID)
1728 18         65 my $pos = length $$outfile;
1729 18         54 foreach $mdat (@mdat) {
1730 26         76 $pos += length $$mdat[2];
1731 26         74 $$mdat[3] = $pos;
1732 26 100       110 $pos += $$mdat[4] ? length(${$$mdat[4]}) : $$mdat[1] - $$mdat[0];
  5         12  
1733             }
1734              
1735             # fix up offsets for new mdat position(s) (and uuid positions in CR3 images)
1736 18         66 foreach $co (@$off) {
1737 59         180 my ($type, $ptr, $len, $base, $id) = @$co;
1738 59 50       155 $base = 0 unless $base;
1739 59 100       382 unless ($type =~ /^(stco|co64)_?(.*)$/) {
1740 15 100       60 next if $type eq 'uuid';
1741 3 50       23 $type eq 'CTBO' or $et->Error('Internal error fixing offsets'), last;
1742             # update 'CTBO' item offsets/sizes in Canon CR3 images
1743 3 50       13 $$co[2] > 12 or $et->Error('Invalid CTBO atom'), last;
1744 3 50       11 @mdat or $et->Error('Missing CR3 image data'), last;
1745 3         18 my $n = Get32u($outfile, $$co[1] + 8);
1746 3 50       26 $$co[2] < $n * 20 + 12 and $et->Error('Truncated CTBO atom'), last;
1747 3         15 my (%ctboOff, $i);
1748             # determine uuid types, and build an offset lookup based on CTBO ID number
1749 3         15 foreach (@$off) {
1750 25 100 66     81 next unless $$_[0] eq 'uuid' and $$_[2] >= 24; # (ignore undersized and deleted uuid boxes)
1751 10         16 my $pos = $$_[1];
1752 10 100       37 next if $pos + 24 > length $$outfile; # (will happen for WriteLast uuid tags)
1753 9         34 my $siz = Get32u($outfile, $pos); # get size of uuid atom
1754 9 50       36 if ($siz == 1) { # check for extended (8-byte) size
1755 0 0       0 next unless $$_[2] >= 32;
1756 0         0 $pos += 8;
1757             }
1758             # get CTBO entry ID based on 16-byte UUID identifier
1759 9         57 my $id = $ctboID{substr($$outfile, $pos+8, 16)};
1760 9 100       57 $ctboOff{$id} = $_ if defined $id;
1761             }
1762             # calculate new offset for the first mdat (size of -1 indicates it didn't change)
1763 3         60 $ctboOff{3} = [ 'mdat', $mdat[0][3] - length $mdat[0][2], -1 ];
1764 3         26 for ($i=0; $i<$n; ++$i) {
1765 12         29 my $pos = $$co[1] + 12 + $i * 20;
1766 12         30 my $id = Get32u($outfile, $pos);
1767             # ignore if size is zero unless we can add this entry
1768             # (note: can't yet add/delete PreviewImage, but leave this possibility open)
1769 12 50 66     49 next unless Get64u($outfile, $pos + 12) or $id == 1 or $id == 2;
      66        
1770 9 50       33 if (not defined $ctboOff{$id}) {
1771 0 0 0     0 $id==1 or $id==2 or $et->Error("Can't handle CR3 CTBO ID number $id"), last;
1772             # XMP or PreviewImage was deleted -- set offset and size to zero
1773 0         0 $ctboOff{$id} = [ 'uuid', 0, 0 ];
1774             }
1775             # update the new offset and size of this entry
1776 9         42 Set64u($ctboOff{$id}[1], $outfile, $pos + 4);
1777 9 100       45 Set64u($ctboOff{$id}[2], $outfile, $pos + 12) unless $ctboOff{$id}[2] < 0;
1778             }
1779 3         21 next;
1780             }
1781 44 100       243 my $siz = $1 eq 'co64' ? 8 : 4;
1782 44         86 my ($n, $tag);
1783 44 100       121 if ($2) { # is this an offset in an iloc or 'gps ' atom?
1784 11         16 $n = 1;
1785 11         23 $type = $1;
1786 11         19 $tag = $2;
1787             } else { # this is an stco or co84 atom
1788 33 50       91 next if $len < 8;
1789 33         108 $n = Get32u($outfile, $ptr + 4); # get number of entries in table
1790 33         82 $ptr += 8;
1791 33         69 $len -= 8;
1792 33         89 $tag = $1;
1793             }
1794 44         88 my $end = $ptr + $n * $siz;
1795 44 50       119 $end > $ptr + $len and $et->Error("Invalid $tag table"), return $rtnVal;
1796 44         139 for (; $ptr<$end; $ptr+=$siz) {
1797 23         46 my ($ok, $i);
1798 23 100       84 my $val = $type eq 'co64' ? Get64u($outfile, $ptr) : Get32u($outfile, $ptr);
1799 23         116 for ($i=0; $i<@mdat; ++$i) {
1800 38         74 $mdat = $mdat[$i];
1801 38         62 my $pos = $val + $base;
1802 38 100       90 if (defined $$mdat[5]) { # is this chunk associated with an item we edited?
1803             # set offset only for the corresponding new chunk
1804 17 100 66     64 unless (defined $id and $id == $$mdat[5]) {
1805             # could have pointed to empty chunk before inserted chunk
1806 12 50 66     52 next unless $pos == $$mdat[0] and $$mdat[0] != $$mdat[1];
1807             }
1808             } else {
1809             # (have seen $pos == $$mdat[1], which is a real PITA)
1810 21 100 66     112 next unless $pos >= $$mdat[0] and ($pos <= $$mdat[1] or not $$mdat[1]);
      33        
1811             # step to next chunk if contiguous and at the end of this one
1812 18 0 33     58 next if $pos == $$mdat[1] and $i+1 < @mdat and $pos == $mdat[$i+1][0];
      33        
1813             }
1814 23         45 $val += $$mdat[3] - $$mdat[0];
1815 23 50       55 if ($val < 0) {
1816 0         0 $et->Error("Error fixing up $tag offset");
1817 0         0 return $rtnVal;
1818             }
1819 23 100       64 if ($type eq 'co64') {
    50          
1820 12         39 Set64u($val, $outfile, $ptr);
1821             } elsif ($val <= 0xffffffff) {
1822 11         28 Set32u($val, $outfile, $ptr);
1823             } else {
1824 0         0 $et->Error("Can't yet promote $tag offset to 64 bits");
1825 0         0 return $rtnVal;
1826             }
1827 23         60 $ok = 1;
1828 23         41 last;
1829             }
1830 23 50       97 unless ($ok) {
1831 0         0 $et->Error("Chunk offset in $tag atom is outside media data");
1832 0         0 return $rtnVal;
1833             }
1834             }
1835             }
1836              
1837             # switch back to actual output file
1838 18         74 $outfile = $$dirInfo{OutFile};
1839              
1840             # write the metadata
1841 18 50       80 Write($outfile, $outBuff) or $rtnVal = 0;
1842              
1843             # write the media data
1844 18         94 foreach $mdat (@mdat) {
1845 26 50       105 Write($outfile, $$mdat[2]) or $rtnVal = 0; # write mdat header
1846 26 100       112 if ($$mdat[4]) {
1847 5 50       11 Write($outfile, ${$$mdat[4]}) or $rtnVal = 0;
  5         17  
1848             } else {
1849 21 50       109 $raf->Seek($$mdat[0], 0) or $et->Error('Seek error'), last;
1850 21 50       125 if ($$mdat[1]) {
1851 21         146 my $result = Image::ExifTool::CopyBlock($raf, $outfile, $$mdat[1] - $$mdat[0]);
1852 21 50       91 defined $result or $rtnVal = 0, last;
1853 21 50       90 $result or $et->Error("Truncated mdat atom"), last;
1854             } else {
1855             # mdat continues to end of file
1856 0         0 my $buff;
1857 0         0 while ($raf->Read($buff, 65536)) {
1858 0 0       0 Write($outfile, $buff) or $rtnVal = 0, last;
1859             }
1860             }
1861             }
1862             }
1863              
1864             # write the stuff that must come last
1865 18 100 50     72 Write($outfile, $writeLast) or $rtnVal = 0 if $writeLast;
1866              
1867 18         312 return $rtnVal;
1868             }
1869              
1870             #------------------------------------------------------------------------------
1871             # Write QuickTime-format MOV/MP4 file
1872             # Inputs: 0) ExifTool ref, 1) dirInfo ref
1873             # Returns: 1 on success, 0 if this wasn't a valid QuickTime file,
1874             # or -1 if a write error occurred
1875             sub WriteMOV($$)
1876             {
1877 18     18 0 75 my ($et, $dirInfo) = @_;
1878 18 50       85 $et or return 1; # allow dummy access to autoload this package
1879 18 50       96 my $raf = $$dirInfo{RAF} or return 0;
1880 18         64 my ($buff, $ftype);
1881              
1882             # read the first atom header
1883 18 50       87 return 0 unless $raf->Read($buff, 8) == 8;
1884 18         187 my ($size, $tag) = unpack('Na4', $buff);
1885 18 50 33     107 return 0 if $size < 8 and $size != 1;
1886              
1887             # validate the file format
1888 18         85 my $tagTablePtr = GetTagTable('Image::ExifTool::QuickTime::Main');
1889 18 50       129 return 0 unless $$tagTablePtr{$tag};
1890              
1891             # determine the file type (by default, assume MP4 if 'ftyp' exists
1892             # without 'qt ' as a compatible brand, but HEIC is an exception)
1893 18 100 66     211 if ($tag eq 'ftyp' and $size >= 12 and $size < 100000 and
      66        
      33        
      33        
1894             $raf->Read($buff, $size-8) == $size-8 and
1895             $buff !~ /^(....)+(qt )/s)
1896             {
1897 9 100       87 if ($buff =~ /^crx /) {
    100          
1898 3         10 $ftype = 'CR3',
1899             } elsif ($buff =~ /^(heic|mif1|msf1|heix|hevc|hevx|avif)/) {
1900 3         13 $ftype = 'HEIC';
1901             } else {
1902 3         10 $ftype = 'MP4';
1903             }
1904             } else {
1905 9         29 $ftype = 'MOV';
1906             }
1907 18         126 $et->SetFileType($ftype); # need to set "FileType" tag for a Condition
1908 18 100       112 if ($ftype eq 'HEIC') {
1909             # EXIF is preferred in HEIC files
1910 3         21 $et->InitWriteDirs($dirMap{$ftype}, 'EXIF', 'QuickTime');
1911             } else {
1912 15         107 $et->InitWriteDirs($dirMap{$ftype}, 'XMP', 'QuickTime');
1913             }
1914 18         70 $$et{DirMap} = $dirMap{$ftype}; # need access to directory map when writing
1915             # track tags globally to avoid creating multiple tags in the case of duplicate directories
1916 18         67 $$et{DidTag} = { };
1917 18         119 SetByteOrder('MM');
1918 18         122 $raf->Seek(0,0);
1919              
1920             # write the file
1921 18         90 $$dirInfo{Parent} = '';
1922 18         74 $$dirInfo{DirName} = 'MOV';
1923 18         70 $$dirInfo{ChunkOffset} = [ ]; # (just to be safe)
1924 18 50       104 return WriteQuickTime($et, $dirInfo, $tagTablePtr) ? 1 : -1;
1925             }
1926              
1927             1; # end
1928              
1929             __END__