File Coverage

blib/lib/Image/ExifTool/WriteQuickTime.pl
Criterion Covered Total %
statement 851 1106 76.9
branch 476 834 57.0
condition 273 557 49.0
subroutine 10 12 83.3
pod 0 11 0.0
total 1610 2520 63.8


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