File Coverage

blib/lib/Image/ExifTool/WriteQuickTime.pl
Criterion Covered Total %
statement 969 1293 74.9
branch 552 1004 54.9
condition 322 670 48.0
subroutine 12 13 92.3
pod 0 12 0.0
total 1855 2992 62.0


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