exiftool_vendored 11.42.5 → 11.43.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of exiftool_vendored might be problematic. Click here for more details.

@@ -12,7 +12,7 @@ meta information extracted from or written to a file.
12
12
  =head1 TAG TABLES
13
13
 
14
14
  The tables listed below give the names of all tags recognized by ExifTool.
15
- They contain a total of 22582 tags, with 15004 unique tag names.
15
+ They contain a total of 22584 tags, with 15004 unique tag names.
16
16
 
17
17
  B<Tag ID>, B<Index#> or B<Sequence> is given in the first column of each
18
18
  table. A B<Tag ID> is the computer-readable equivalent of a tag name, and
@@ -24227,6 +24227,7 @@ ItemList tags written by the "mdta" handler. The prefix of
24227
24227
  'direction.facing' CameraDirection string
24228
24228
  'direction.motion' CameraMotion string
24229
24229
  'director' Director string
24230
+ 'displayname' DisplayName string
24230
24231
  'genre' Genre string
24231
24232
  'information' Information string
24232
24233
  'keywords' Keywords string
@@ -24436,6 +24437,7 @@ added dynamically to this table after processing the Meta Keys information.
24436
24437
  'yrrc' Year string
24437
24438
  "\xa9ART" Artist string
24438
24439
  "\xa9alb" Album string
24440
+ "\xa9aut" Author string
24439
24441
  "\xa9cmt" Comment string
24440
24442
  "\xa9com" Composer string
24441
24443
  "\xa9cpy" Copyright string
@@ -24515,9 +24517,9 @@ defined in this table.
24515
24517
  'clap' CleanAperture no
24516
24518
  'colr' ICC_Profile ICC_Profile
24517
24519
  'hvcC' HEVCConfiguration? no
24518
- 'irot' Rotation no
24520
+ 'irot' Rotation int8u
24519
24521
  'ispe' ImageSpatialExtent no
24520
- 'pasp' PixelAspectRatio no
24522
+ 'pasp' PixelAspectRatio int32u
24521
24523
  'pixi' ImagePixelDepth no
24522
24524
  'rloc' RelativeLocation no
24523
24525
 
@@ -412,7 +412,7 @@ sub ExifErr($$$)
412
412
  {
413
413
  my ($et, $errStr, $tagTablePtr) = @_;
414
414
  # MakerNote errors are minor by default
415
- my $minor = ($$tagTablePtr{GROUPS}{0} eq 'MakerNotes');
415
+ my $minor = ($$tagTablePtr{GROUPS}{0} eq 'MakerNotes' or $$et{FILE_TYPE} eq 'MOV');
416
416
  if ($$tagTablePtr{VARS} and $$tagTablePtr{VARS}{MINOR_ERRORS}) {
417
417
  $et->Warn("$errStr. IFD dropped.") and return '' if $minor;
418
418
  $minor = 1;
@@ -574,7 +574,7 @@ sub WriteExif($$$)
574
574
  # only account for nextIFD pointer if we are going to use it
575
575
  $len += 4 if $dataLen==$len+6 and ($$dirInfo{Multi} or $buff =~ /\0{4}$/);
576
576
  UpdateTiffEnd($et, $offset+$base+2+$len);
577
- } elsif ($dirLen) {
577
+ } elsif ($dirLen and $dirStart + 4 >= $dataLen) {
578
578
  # error if we can't load IFD (unless we are creating
579
579
  # from scratch, in which case dirLen will be zero)
580
580
  my $str = $et->Options('IgnoreMinorErrors') ? 'Deleted bad' : 'Bad';
@@ -586,7 +586,10 @@ sub WriteExif($$$)
586
586
  $numEntries = Get16u($dataPt, $dirStart);
587
587
  $dirEnd = $dirStart + 2 + 12 * $numEntries;
588
588
  if ($dirEnd > $dataLen) {
589
- return ExifErr($et, "Truncated $name directory", $tagTablePtr);
589
+ my $n = int(($dataLen - $dirStart - 2) / 12);
590
+ my $rtn = ExifErr($et, "Truncated $name directory", $tagTablePtr);
591
+ return undef unless $n and defined $rtn;
592
+ $numEntries = $n; # continue processing the entries we have
590
593
  }
591
594
  # sort entries if necessary (but not in maker notes IFDs)
592
595
  unless ($inMakerNotes) {
@@ -1345,11 +1348,15 @@ NoOverwrite: next if $isNew > 0;
1345
1348
  if ($$et{DEL_GROUP}{MakerNotes} and
1346
1349
  ($$et{DEL_GROUP}{MakerNotes} != 2 or $isNew <= 0))
1347
1350
  {
1348
- if ($isNew <= 0) {
1349
- ++$$et{CHANGED};
1350
- $verbose and print $out " Deleting MakerNotes\n";
1351
+ if ($et->IsRawType()) {
1352
+ $et->WarnOnce("Can't delete MakerNotes from $$et{FileType} file",1);
1353
+ } else {
1354
+ if ($isNew <= 0) {
1355
+ ++$$et{CHANGED};
1356
+ $verbose and print $out " Deleting MakerNotes\n";
1357
+ }
1358
+ next;
1351
1359
  }
1352
- next;
1353
1360
  }
1354
1361
  my $saveOrder = GetByteOrder();
1355
1362
  if ($isNew >= 0 and defined $set{$newID}) {
@@ -9,9 +9,9 @@ package Image::ExifTool::QuickTime;
9
9
 
10
10
  use strict;
11
11
 
12
- # map for adding directories to QuickTime-format files
12
+ # maps for adding metadata to various QuickTime-based file types
13
13
  my %movMap = (
14
- # MOV (no 'ftyp', or 'ftyp'='qt ') -> 'moov'-'udta'-'XMP_'
14
+ # MOV (no 'ftyp', or 'ftyp'='qt ') -> XMP in 'moov'-'udta'-'XMP_'
15
15
  QuickTime => 'ItemList',
16
16
  ItemList => 'Meta',
17
17
  Meta => 'UserData',
@@ -20,7 +20,7 @@ my %movMap = (
20
20
  Movie => 'MOV',
21
21
  );
22
22
  my %mp4Map = (
23
- # MP4 ('ftyp' compatible brand 'mp41', 'mp42' or 'f4v ') -> top level 'uuid'
23
+ # MP4 ('ftyp' compatible brand 'mp41', 'mp42' or 'f4v ') -> XMP at top level
24
24
  QuickTime => 'ItemList',
25
25
  ItemList => 'Meta',
26
26
  Meta => 'UserData',
@@ -28,10 +28,39 @@ my %mp4Map = (
28
28
  Movie => 'MOV',
29
29
  XMP => 'MOV',
30
30
  );
31
+ my %heicMap = (
32
+ # HEIC ('ftyp' compatible brand 'heic' or 'mif1') -> XMP/EXIF in top level 'meta'
33
+ Meta => 'MOV',
34
+ ItemInformation => 'Meta',
35
+ ItemPropertyContainer => 'Meta',
36
+ XMP => 'ItemInformation',
37
+ EXIF => 'ItemInformation',
38
+ ICC_Profile => 'ItemPropertyContainer',
39
+ IFD0 => 'EXIF',
40
+ IFD1 => 'IFD0',
41
+ ExifIFD => 'IFD0',
42
+ GPS => 'IFD0',
43
+ SubIFD => 'IFD0',
44
+ GlobParamIFD => 'IFD0',
45
+ PrintIM => 'IFD0',
46
+ InteropIFD => 'ExifIFD',
47
+ MakerNotes => 'ExifIFD',
48
+ );
49
+ my %cr3Map = (
50
+ # CR3 ('ftyp' compatible brand 'crx ') -> XMP at top level
51
+ Movie => 'MOV',
52
+ XMP => 'MOV',
53
+ 'UUID-Canon'=>'Movie',
54
+ ExifIFD => 'UUID-Canon',
55
+ IFD0 => 'UUID-Canon',
56
+ GPS => 'UUID-Canon',
57
+ #MakerNoteCanon => 'UUID-Canon', # (doesn't yet work -- goes into ExifIFD instead)
58
+ );
31
59
  my %dirMap = (
32
- MOV => \%movMap,
33
- MP4 => \%mp4Map,
34
- HEIC => \%mp4Map,
60
+ MOV => \%movMap,
61
+ MP4 => \%mp4Map,
62
+ CR3 => \%cr3Map,
63
+ HEIC => \%heicMap,
35
64
  );
36
65
 
37
66
  # convert ExifTool Format to QuickTime type
@@ -131,7 +160,7 @@ sub IsCurPath($$)
131
160
  sub Handle_iloc($$$$)
132
161
  {
133
162
  my ($et, $dirInfo, $dataPt, $outfile) = @_;
134
- my ($i, $j, $num, $pos);
163
+ my ($i, $j, $num, $pos, $id);
135
164
 
136
165
  my $off = $$dirInfo{ChunkOffset};
137
166
  my $len = length $$dataPt;
@@ -157,11 +186,11 @@ sub Handle_iloc($$$$)
157
186
  for ($i=0; $i<$num; ++$i) {
158
187
  if ($ver < 2) {
159
188
  return 0 if $pos + 2 > $len;
160
- # $id = Get16u($dataPt, $pos);
189
+ $id = Get16u($dataPt, $pos);
161
190
  $pos += 2;
162
191
  } else {
163
192
  return 0 if $pos + 4 > $len;
164
- # $id = Get32u($dataPt, $pos);
193
+ $id = Get32u($dataPt, $pos);
165
194
  $pos += 4;
166
195
  }
167
196
  my ($constOff, @offBase, @offItem, $minOffset);
@@ -189,7 +218,7 @@ sub Handle_iloc($$$$)
189
218
  my $base_offset = GetVarInt($dataPt, $pos, $nbas);
190
219
  if ($base_offset and not $constOff) {
191
220
  my $tg = ($nbas == 4 ? 'stco' : 'co64') . '_iloc';
192
- push @offBase, [ $tg, length($$outfile) + 8 + $pos - $nbas, $nbas ];
221
+ push @offBase, [ $tg, length($$outfile) + 8 + $pos - $nbas, $nbas, 0, $id ];
193
222
  }
194
223
  return 0 if $pos + 2 > $len;
195
224
  my $ext_num = Get16u($dataPt, $pos);
@@ -201,7 +230,7 @@ sub Handle_iloc($$$$)
201
230
  my $extent_offset = GetVarInt($dataPt, $pos, $noff);
202
231
  return 0 unless defined $extent_offset;
203
232
  unless ($constOff) {
204
- push @offItem, [ $tag, length($$outfile) + 8 + $pos - $noff, $noff ] if $noff;
233
+ push @offItem, [ $tag, length($$outfile) + 8 + $pos - $noff, $noff, 0, $id ] if $noff;
205
234
  $minOffset = $extent_offset if not defined $minOffset or $minOffset > $extent_offset;
206
235
  }
207
236
  return 0 if $pos + $nlen > length $$dataPt;
@@ -274,6 +303,268 @@ sub FormatQTValue($$;$)
274
303
  return $flags;
275
304
  }
276
305
 
306
+ #------------------------------------------------------------------------------
307
+ # Set variable-length integer (used by WriteItemInfo)
308
+ # Inputs: 0) value, 1) integer size in bytes (0, 4 or 8),
309
+ # Returns: packed integer
310
+ sub SetVarInt($$)
311
+ {
312
+ my ($val, $n) = @_;
313
+ if ($n == 4) {
314
+ return Set32u($val);
315
+ } elsif ($n == 8) {
316
+ return Set64u($val);
317
+ }
318
+ return '';
319
+ }
320
+
321
+ #------------------------------------------------------------------------------
322
+ # Write ItemInformation in HEIC files
323
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref (with BoxPos entry), 2) output buffer ref
324
+ # Returns: mdat edit list ref (empty if nothing changed)
325
+ sub WriteItemInfo($$$)
326
+ {
327
+ my ($et, $dirInfo, $outfile) = @_;
328
+ my $boxPos = $$dirInfo{BoxPos};
329
+ my $raf = $$et{RAF};
330
+ my $items = $$et{ItemInfo};
331
+ my (%did, @mdatEdit, $name);
332
+
333
+ return () unless $items and $raf;
334
+
335
+ # extract information from EXIF/XMP metadata items
336
+ if ($items and $raf) {
337
+ my $curPos = $raf->Tell();
338
+ my $primary = $$et{PrimaryItem} || 0;
339
+ my $id;
340
+ foreach $id (sort { $a <=> $b } keys %$items) {
341
+ my $item = $$items{$id};
342
+ # only edit primary EXIF/XMP metadata
343
+ next unless $$item{RefersTo} and $$item{RefersTo}{$primary};
344
+ my $type = $$item{ContentType} || $$item{Type} || next;
345
+ # get ExifTool name for this item
346
+ $name = { Exif => 'EXIF', 'application/rdf+xml' => 'XMP' }->{$type};
347
+ next unless $name; # only care about EXIF and XMP
348
+ next unless $$et{EDIT_DIRS}{$name};
349
+ $did{$name} = 1; # set flag to prevent creating this metadata
350
+ my ($warn, $extent, $buff, @edit);
351
+ $warn = 'Missing iloc box' unless $$boxPos{iloc};
352
+ $warn = "No Extents for $type item" unless $$item{Extents} and @{$$item{Extents}};
353
+ $warn = "Can't currently decode encoded $type metadata" if $$item{ContentEncoding};
354
+ $warn = "Can't currently decode protected $type metadata" if $$item{ProtectionIndex};
355
+ $warn = "Can't currently extract $type with construction method $$item{ConstructionMethod}" if $$item{ConstructionMethod};
356
+ $warn = "$type metadata is not this file" if $$item{DataReferenceIndex};
357
+ $warn and $et->Warn($warn), next;
358
+ my $base = $$item{BaseOffset} || 0;
359
+ my $val = '';
360
+ foreach $extent (@{$$item{Extents}}) {
361
+ $val .= $buff if defined $buff;
362
+ my $pos = $$extent[1] + $base;
363
+ if ($$extent[2]) {
364
+ $raf->Seek($pos, 0) or last;
365
+ $raf->Read($buff, $$extent[2]) or last;
366
+ } else {
367
+ $buff = '';
368
+ }
369
+ push @edit, [ $pos, $pos + $$extent[2] ]; # replace or delete this if changed
370
+ }
371
+ next unless defined $buff;
372
+ $buff = $val . $buff if length $val;
373
+ my ($hdr, $subTable, $proc);
374
+ if ($name eq 'EXIF') {
375
+ $hdr = "\0\0\0\x06Exif\0\0";
376
+ $subTable = GetTagTable('Image::ExifTool::Exif::Main');
377
+ $proc = \&Image::ExifTool::WriteTIFF;
378
+ } else {
379
+ $hdr = '';
380
+ $subTable = GetTagTable('Image::ExifTool::XMP::Main');
381
+ }
382
+ my %dirInfo = (
383
+ DataPt => \$buff,
384
+ DataLen => length $buff,
385
+ DirStart => length $hdr,
386
+ DirLen => length($buff) - length $hdr,
387
+ );
388
+ my $changed = $$et{CHANGED};
389
+ my $newVal = $et->WriteDirectory(\%dirInfo, $subTable, $proc);
390
+ if (defined $newVal and $changed ne $$et{CHANGED} and
391
+ # nothing changed if deleting an empty directory
392
+ ($dirInfo{DirLen} or length $newVal))
393
+ {
394
+ $newVal = $hdr . $newVal if length $hdr and length $newVal;
395
+ $edit[0][2] = \$newVal; # replace the old chunk with the new data
396
+ $edit[0][3] = $id; # mark this chunk with the item ID
397
+ push @mdatEdit, @edit;
398
+ # update item extent_length
399
+ my $n = length $newVal;
400
+ foreach $extent (@{$$item{Extents}}) {
401
+ my ($nlen, $lenPt) = @$extent[3,4];
402
+ if ($nlen == 8) {
403
+ Set64u($n, $outfile, $$boxPos{iloc}[0] + 8 + $lenPt);
404
+ } elsif ($n <= 0xffffffff) {
405
+ Set32u($n, $outfile, $$boxPos{iloc}[0] + 8 + $lenPt);
406
+ } else {
407
+ $et->Error("Can't yet promote iloc offset to 64 bits");
408
+ return ();
409
+ }
410
+ $n = 0;
411
+ }
412
+ if (@{$$item{Extents}} != 1) {
413
+ $et->Error("Can't yet handle $name in multiple parts. Please submit sample for testing");
414
+ }
415
+ }
416
+ $$et{CHANGED} = $changed; # (will set this later if successful in editing mdat)
417
+ }
418
+ $raf->Seek($curPos, 0); # seek back to original position
419
+ }
420
+ # add necessary metadata types if they didn't already exist
421
+ my ($countNew, %add, %usedID);
422
+ foreach $name ('EXIF','XMP') {
423
+ next if $did{$name} or not $$et{ADD_DIRS}{$name};
424
+ unless ($$boxPos{iinf} and $$boxPos{iref} and $$boxPos{iloc}) {
425
+ $et->Warn("Can't create $name. Missing expected box");
426
+ last;
427
+ }
428
+ my $primary = $$et{PrimaryItem};
429
+ unless (defined $primary) {
430
+ $et->Warn("Can't create $name. No primary item reference");
431
+ last;
432
+ }
433
+ my $buff = '';
434
+ my ($hdr, $subTable, $proc);
435
+ if ($name eq 'EXIF') {
436
+ $hdr = "\0\0\0\x06Exif\0\0";
437
+ $subTable = GetTagTable('Image::ExifTool::Exif::Main');
438
+ $proc = \&Image::ExifTool::WriteTIFF;
439
+ } else {
440
+ $hdr = '';
441
+ $subTable = GetTagTable('Image::ExifTool::XMP::Main');
442
+ }
443
+ my %dirInfo = (
444
+ DataPt => \$buff,
445
+ DataLen => 0,
446
+ DirStart => 0,
447
+ DirLen => 0,
448
+ );
449
+ my $changed = $$et{CHANGED};
450
+ my $newVal = $et->WriteDirectory(\%dirInfo, $subTable, $proc);
451
+ if (defined $newVal and $changed ne $$et{CHANGED}) {
452
+ $newVal = $hdr . $newVal if length $hdr;
453
+ # add new infe to iinf
454
+ $add{iinf} = $add{iref} = $add{iloc} = '' unless defined $add{iinf};
455
+ my ($type, $mime);
456
+ if ($name eq 'XMP') {
457
+ $type = "mime\0";
458
+ $mime = "application/rdf+xml\0";
459
+ } else {
460
+ $type = "Exif\0";
461
+ $mime = '';
462
+ }
463
+ my $id = 1;
464
+ ++$id while $$items{$id} or $usedID{$id}; # find next unused item ID
465
+ my $n = length($type) + length($mime) + 16;
466
+ if ($id < 0x10000) {
467
+ $add{iinf} .= pack('Na4CCCCnn', $n, 'infe', 2, 0, 0, 1, $id, 0) . $type . $mime;
468
+ } else {
469
+ $n += 2;
470
+ $add{iinf} .= pack('Na4CCCCNn', $n, 'infe', 3, 0, 0, 1, $id, 0) . $type . $mime;
471
+ }
472
+ # add new cdsc to iref
473
+ my $irefVer = Get8u($outfile, $$boxPos{iref}[0] + 8);
474
+ if ($irefVer) {
475
+ $add{iref} .= pack('Na4NnN', 18, 'cdsc', $id, 1, $primary);
476
+ } else {
477
+ $add{iref} .= pack('Na4nnn', 14, 'cdsc', $id, 1, $primary);
478
+ }
479
+ # add new entry to iloc table (see ISO14496-12:2015 pg.79)
480
+ my $ilocVer = Get8u($outfile, $$boxPos{iloc}[0] + 8);
481
+ my $siz = Get16u($outfile, $$boxPos{iloc}[0] + 12); # get size information
482
+ my $noff = ($siz >> 12);
483
+ my $nlen = ($siz >> 8) & 0x0f;
484
+ my $nbas = ($siz >> 4) & 0x0f;
485
+ my $nind = $siz & 0x0f;
486
+ my $p;
487
+ if ($ilocVer == 0) {
488
+ # set offset to 0 as flag that this is a new idat chunk being added
489
+ $p = length($add{iloc}) + 4 + $nbas + 2;
490
+ $add{iloc} .= pack('nn',$id,0) . SetVarInt(0,$nbas) . Set16u(1) .
491
+ SetVarInt(0,$noff) . SetVarInt(length($newVal),$nlen);
492
+ } elsif ($ilocVer == 1) {
493
+ $p = length($add{iloc}) + 6 + $nbas + 2 + $nind;
494
+ $add{iloc} .= pack('nnn',$id,0,0) . SetVarInt(0,$nbas) . Set16u(1) . SetVarInt(0,$nind) .
495
+ SetVarInt(0,$noff) . SetVarInt(length($newVal),$nlen);
496
+ } elsif ($ilocVer == 2) {
497
+ $p = length($add{iloc}) + 8 + $nbas + 2 + $nind;
498
+ $add{iloc} .= pack('Nnn',$id,0,0) . SetVarInt(0,$nbas) . Set16u(1) . SetVarInt(0,$nind) .
499
+ SetVarInt(0,$noff) . SetVarInt(length($newVal),$nlen);
500
+ } else {
501
+ $et->Warn("Can't create $name. Unsupported iloc version $ilocVer");
502
+ last;
503
+ }
504
+ # add new ChunkOffset entry to update this new offset
505
+ my $off = $$dirInfo{ChunkOffset} or $et->Warn('Internal error. Missing ChunkOffset'), last;
506
+ my $newOff;
507
+ if ($noff == 4) {
508
+ $newOff = [ 'stco_iloc', $$boxPos{iloc}[0] + $$boxPos{iloc}[1] + $p, $noff, 0, $id ];
509
+ } elsif ($noff == 8) {
510
+ $newOff = [ 'co64_iloc', $$boxPos{iloc}[0] + $$boxPos{iloc}[1] + $p, $noff, 0, $id ];
511
+ } else {
512
+ $et->Warn("Can't create $name. Invalid iloc offset size");
513
+ last;
514
+ }
515
+ # add directory as a new mdat chunk
516
+ push @$off, $newOff;
517
+ push @mdatEdit, [ 0, 0, \$newVal, $id ];
518
+ $usedID{$id} = 1;
519
+ $countNew = ($countNew || 0) + 1;
520
+ $$et{CHANGED} = $changed; # set this later if successful in editing mdat
521
+ }
522
+ }
523
+ if ($countNew) {
524
+ # insert new entries into iinf, iref and iloc boxes
525
+ my $added = 0;
526
+ my $tag;
527
+ foreach $tag (sort { $$boxPos{$a}[0] <=> $$boxPos{$b}[0] } keys %$boxPos) {
528
+ next unless $add{$tag};
529
+ my $pos = $$boxPos{$tag}[0] + $added;
530
+ my $n = Get32u($outfile, $pos);
531
+ Set32u($n + length($add{$tag}), $outfile, $pos); # increase box size
532
+ if ($tag eq 'iinf') {
533
+ my $iinfVer = Get8u($outfile, $pos + 8);
534
+ if ($iinfVer == 0) {
535
+ $n = Get16u($outfile, $pos + 12);
536
+ Set16u($n + $countNew, $outfile, $pos + 12); # incr count
537
+ } else {
538
+ $n = Get32u($outfile, $pos + 12);
539
+ Set32u($n + $countNew, $outfile, $pos + 12); # incr count
540
+ }
541
+ } elsif ($tag eq 'iref') {
542
+ # nothing more to do
543
+ } elsif ($tag eq 'iloc') {
544
+ my $ilocVer = Get8u($outfile, $pos + 8);
545
+ if ($ilocVer < 2) {
546
+ $n = Get16u($outfile, $pos + 14);
547
+ Set16u($n + $countNew, $outfile, $pos + 14); # incr count
548
+ } else {
549
+ $n = Get32u($outfile, $pos + 14);
550
+ Set32u($n + $countNew, $outfile, $pos + 14); # incr count
551
+ }
552
+ # must also update pointer locations in this box
553
+ if ($added) {
554
+ $$_[1] += $added foreach @{$$dirInfo{ChunkOffset}};
555
+ }
556
+ } else {
557
+ next;
558
+ }
559
+ # add new entries to this box
560
+ substr($$outfile, $pos + $$boxPos{$tag}[1], 0) = $add{$tag};
561
+ $added += length $add{$tag}; # positions are shifted by length of new entries
562
+ }
563
+ }
564
+ delete $$et{ItemInfo};
565
+ return @mdatEdit ? \@mdatEdit : undef;
566
+ }
567
+
277
568
  #------------------------------------------------------------------------------
278
569
  # Write a series of QuickTime atoms from file or in memory
279
570
  # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
@@ -285,8 +576,8 @@ sub WriteQuickTime($$$)
285
576
  local $_;
286
577
  my ($et, $dirInfo, $tagTablePtr) = @_;
287
578
  $et or return 1; # allow dummy access to autoload this package
288
- my ($mdat, @mdat, $track, $outBuff, $co, $term, $err);
289
- my (%langTags, $keysTags, $canCreate, %didTag, $delGrp);
579
+ my ($mdat, @mdat, @mdatEdit, $edit, $track, $outBuff, $co, $term, $err);
580
+ my (%langTags, $keysTags, $canCreate, %didTag, $delGrp, %boxPos);
290
581
  my $outfile = $$dirInfo{OutFile} || return 0;
291
582
  my $raf = $$dirInfo{RAF}; # (will be null for lower-level atoms)
292
583
  my $dataPt = $$dirInfo{DataPt}; # (will be null for top-level atoms)
@@ -495,8 +786,8 @@ sub WriteQuickTime($$$)
495
786
  Multi => $$subdir{Multi}, # necessary?
496
787
  OutFile => $outfile,
497
788
  # initialize array to hold details about chunk offset table
498
- # (each entry has 3 or 4 items: 0=atom type, 1=table offset, 2=table size,
499
- # 4=optional base offset)
789
+ # (each entry has 3-5 items: 0=atom type, 1=table offset, 2=table size,
790
+ # 3=optional base offset, 4=optional item ID)
500
791
  ChunkOffset => \@chunkOffset,
501
792
  );
502
793
  # pass the header pointer if necessary (for EXIF IFD's
@@ -515,12 +806,17 @@ sub WriteQuickTime($$$)
515
806
  # demote non-QuickTime errors to warnings
516
807
  $$et{DemoteErrors} = 1 unless $$subTable{GROUPS}{0} eq 'QuickTime';
517
808
  my $oldChanged = $$et{CHANGED};
518
- $newData = $et->WriteDirectory(\%subdirInfo, $subTable);
809
+ $newData = $et->WriteDirectory(\%subdirInfo, $subTable, $$subdir{WriteProc});
519
810
  if ($$et{DemoteErrors}) {
520
- # just copy existing subdirectory a non-quicktime error occurred
811
+ # just copy existing subdirectory if a non-quicktime error occurred
521
812
  $$et{CHANGED} = $oldChanged if $$et{DemoteErrors} > 1;
522
813
  delete $$et{DemoteErrors};
523
814
  }
815
+ if (defined $newData and not length $newData and $$tagTablePtr{PERMANENT}) {
816
+ # do nothing if trying to delete tag from a PERMANENT table
817
+ $$et{CHANGED} = $oldChanged;
818
+ undef $newData;
819
+ }
524
820
  $$et{CUR_WRITE_GROUP} = $oldWriteGroup;
525
821
  SetByteOrder('MM');
526
822
  # add back header if necessary
@@ -711,6 +1007,7 @@ sub WriteQuickTime($$$)
711
1007
  $$_[1] += 8 + length $$outfile foreach @chunkOffset;
712
1008
  push @{$$dirInfo{ChunkOffset}}, @chunkOffset;
713
1009
  }
1010
+ $boxPos{$tag} = [ length($$outfile), length($newData) + 8 ];
714
1011
  # write the updated directory now (unless length is zero, or it is needed as padding)
715
1012
  Write($outfile, Set32u($len+8), $tag, $newData) or $rtnVal=$rtnErr, $err=1, last;
716
1013
  next;
@@ -747,6 +1044,8 @@ sub WriteQuickTime($$$)
747
1044
  }
748
1045
  $$et{QtDataFlg} = $flg;
749
1046
  }
1047
+ # save position of this box in the output buffer
1048
+ $boxPos{$tag} = [ length($$outfile), length($hdr) + length($buff) ];
750
1049
  # copy the existing atom
751
1050
  Write($outfile, $hdr, $buff) or $rtnVal=$rtnErr, $err=1, last;
752
1051
  }
@@ -821,7 +1120,7 @@ sub WriteQuickTime($$$)
821
1120
  OutFile => $outfile,
822
1121
  );
823
1122
  my $subTable = GetTagTable($$subdir{TagTable});
824
- my $newData = $et->WriteDirectory(\%subdirInfo, $subTable);
1123
+ my $newData = $et->WriteDirectory(\%subdirInfo, $subTable, $$subdir{WriteProc});
825
1124
  if ($newData and length($newData) <= 0x7ffffff7) {
826
1125
  my $prefix = '';
827
1126
  # add atom version or ID if necessary
@@ -841,6 +1140,15 @@ sub WriteQuickTime($$$)
841
1140
  delete $$addDirs{$subName}; # add only once (must delete _after_ call to WriteDirectory())
842
1141
  }
843
1142
  }
1143
+ # write HEIC metadata after top-level 'meta' box has been processed if editing this information
1144
+ if ($dirName eq 'Meta' and $$et{EDIT_DIRS}{ItemInformation} and $curPath eq $writePath) {
1145
+ $$dirInfo{BoxPos} = \%boxPos;
1146
+ my $mdatEdit = WriteItemInfo($et, $dirInfo, $outfile);
1147
+ if ($mdatEdit) {
1148
+ $et->Error('Multiple top-level Meta containers') if $$et{mdatEdit};
1149
+ $$et{mdatEdit} = $mdatEdit;
1150
+ }
1151
+ }
844
1152
  # write out any necessary terminator
845
1153
  Write($outfile, $term) or $rtnVal=$rtnErr, $err=1 if $term;
846
1154
 
@@ -857,18 +1165,109 @@ sub WriteQuickTime($$$)
857
1165
  $et->Warn('No movie data', 1);
858
1166
  }
859
1167
 
1168
+ # edit mdat blocks as required
1169
+ # (0=old pos [0 if creating], 1=old end [0 if creating], 2=new data ref or undef to delete,
1170
+ # 3=new data item id)
1171
+ if ($$et{mdatEdit}) {
1172
+ @mdatEdit = @{$$et{mdatEdit}};
1173
+ delete $$et{mdatEdit};
1174
+ }
1175
+ foreach $edit (@mdatEdit) {
1176
+ my (@thisMdat, @newMdat, $changed);
1177
+ foreach $mdat (@mdat) {
1178
+ # keep track of all chunks for the mdat with this header
1179
+ if (length $$mdat[2]) {
1180
+ push @newMdat, @thisMdat;
1181
+ undef @thisMdat;
1182
+ }
1183
+ push @thisMdat, $mdat;
1184
+ # is this edit inside this mdat chunk?
1185
+ # - $$edit[0] and $$edit[1] will both be zero if we are creating a new chunk
1186
+ # - $$mdat[1] is zero if mdat runs to end of file
1187
+ # - $$edit[0] == $$edit[1] == $$mdat[0] if reviving a deleted chunk
1188
+ # - $$mdat[5] is defined if this was a newly added/edited chunk
1189
+ next if defined $$mdat[5] or $changed; # don't replace a newly added chunk
1190
+ if (not $$edit[0] or # (newly created chunk)
1191
+ # (edit is inside chunk)
1192
+ ((($$edit[0] < $$mdat[1] or not $$mdat[1]) and $$edit[1] > $$mdat[0]) or
1193
+ # (edit inserted at start or end of chunk)
1194
+ ($$edit[0] == $$edit[1] and ($$edit[0] == $$mdat[0] or $$edit[0] == $$mdat[1]))))
1195
+ {
1196
+ if (not $$edit[0]) {
1197
+ $$edit[0] = $$edit[1] = $$mdat[0]; # insert at start of this mdat
1198
+ } elsif ($$edit[0] < $$mdat[0] or ($$edit[1] > $$mdat[1] and $$mdat[1])) {
1199
+ $et->Error('ItemInfo runs across mdat boundary');
1200
+ return $rtnVal;
1201
+ }
1202
+ my $hdrChunk = $thisMdat[0];
1203
+ $hdrChunk or $et->Error('Internal error finding mdat header'), return $rtnVal;
1204
+ # calculate difference in mdat size
1205
+ my $diff = ($$edit[2] ? length(${$$edit[2]}) : 0) - ($$edit[1] - $$edit[0]);
1206
+ # edit size of mdat in header if necessary
1207
+ if ($diff) {
1208
+ if (length($$hdrChunk[2]) == 8) {
1209
+ my $size = Get32u(\$$hdrChunk[2], 0) + $diff;
1210
+ $size > 0xffffffff and $et->Error("Can't yet grow mdat across 4GB boundary"), return $rtnVal;
1211
+ Set32u($size, \$$hdrChunk[2], 0);
1212
+ } elsif (length($$hdrChunk[2]) == 16) {
1213
+ my $size = Get64u(\$$hdrChunk[2], 8) + $diff;
1214
+ Set64u($size, \$$hdrChunk[2], 8);
1215
+ } else {
1216
+ $et->Error('Internal error. Invalid mdat header');
1217
+ return $rtnVal;
1218
+ }
1219
+ }
1220
+ $changed = 1;
1221
+ # remove the edited section of this chunk (if any) and replace with new data (if any)
1222
+ if ($$edit[0] > $$mdat[0]) {
1223
+ push @thisMdat, [ $$edit[0], $$edit[1], '', 0, $$edit[2], $$edit[3] ] if $$edit[2];
1224
+ # add remaining data after edit (or empty stub in case it is referenced by an offset)
1225
+ push @thisMdat, [ $$edit[1], $$mdat[1], '' ];
1226
+ $$mdat[1] = $$edit[0]; # now ends at start of edit
1227
+ } else {
1228
+ if ($$edit[2]) {
1229
+ # insert the new chunk before this chunk, moving the header to the new chunk
1230
+ splice @thisMdat, -1, 0, [ $$edit[0],$$edit[1],$$mdat[2],0,$$edit[2],$$edit[3] ];
1231
+ $$mdat[2] = ''; # (header was moved to new chunk)
1232
+ # initialize ChunkOffset pointer if necessary
1233
+ if ($$edit[3]) {
1234
+ my $n = 0;
1235
+ foreach $co (@$off) {
1236
+ next unless defined $$co[4] and $$co[4] == $$edit[3];
1237
+ ++$n;
1238
+ if ($$co[0] eq 'stco_iloc') {
1239
+ Set32u($$mdat[0], $outfile, $$co[1]);
1240
+ } else {
1241
+ Set64u($$mdat[0], $outfile, $$co[1]);
1242
+ }
1243
+ }
1244
+ $n == 1 or $et->Error('Internal error updating chunk offsets');
1245
+ }
1246
+ }
1247
+ $$mdat[0] = $$edit[1]; # remove old data
1248
+ }
1249
+ }
1250
+ }
1251
+ if ($changed) {
1252
+ @mdat = ( @newMdat, @thisMdat );
1253
+ ++$$et{CHANGED};
1254
+ } else {
1255
+ $et->Error('Internal error modifying mdat');
1256
+ }
1257
+ }
1258
+
860
1259
  # determine our new mdat positions
861
- # (0=old pos, 1=old end, 2=mdat header, 3=new pos)
1260
+ # (0=old pos, 1=old end, 2=mdat header, 3=new pos, 4=new data ref if changed, 5=new item ID)
862
1261
  my $pos = length $$outfile;
863
1262
  foreach $mdat (@mdat) {
864
1263
  $pos += length $$mdat[2];
865
1264
  $$mdat[3] = $pos;
866
- $pos += $$mdat[1] - $$mdat[0];
1265
+ $pos += $$mdat[4] ? length(${$$mdat[4]}) : $$mdat[1] - $$mdat[0];
867
1266
  }
868
1267
 
869
1268
  # fix up offsets for new mdat position(s)
870
1269
  foreach $co (@$off) {
871
- my ($type, $ptr, $len, $base) = @$co;
1270
+ my ($type, $ptr, $len, $base, $id) = @$co;
872
1271
  $base = 0 unless $base;
873
1272
  $type =~ /^(stco|co64)_?(.*)$/ or $et->Error('Internal error fixing offsets'), last;
874
1273
  my $siz = $1 eq 'co64' ? 8 : 4;
@@ -887,10 +1286,23 @@ sub WriteQuickTime($$$)
887
1286
  my $end = $ptr + $n * $siz;
888
1287
  $end > $ptr + $len and $et->Error("Invalid $tag table"), return $rtnVal;
889
1288
  for (; $ptr<$end; $ptr+=$siz) {
890
- my $ok;
1289
+ my ($ok, $i);
891
1290
  my $val = $type eq 'co64' ? Get64u($outfile, $ptr) : Get32u($outfile, $ptr);
892
- foreach $mdat (@mdat) {
893
- next unless $val+$base >= $$mdat[0] and $val+$base <= $$mdat[1]; # (have seen == $$mdat[1])
1291
+ for ($i=0; $i<@mdat; ++$i) {
1292
+ $mdat = $mdat[$i];
1293
+ my $pos = $val + $base;
1294
+ if (defined $$mdat[5]) { # is this chunk associated with an item we edited?
1295
+ # set offset only for the corresponding new chunk
1296
+ unless (defined $id and $id == $$mdat[5]) {
1297
+ # could have pointed to empty chunk before inserted chunk
1298
+ next unless $pos == $$mdat[0] and $$mdat[0] != $$mdat[1];
1299
+ }
1300
+ } else {
1301
+ # (have seen $pos == $$mdat[1], which is a real PITA)
1302
+ next unless $pos >= $$mdat[0] and ($pos <= $$mdat[1] or not $$mdat[1]);
1303
+ # step to next chunk if contiguous and at the end of this one
1304
+ next if $pos == $$mdat[1] and $i+1 < @mdat and $pos == $mdat[$i+1][0];
1305
+ }
894
1306
  $val += $$mdat[3] - $$mdat[0];
895
1307
  if ($val < 0) {
896
1308
  $et->Error("Error fixing up $tag offset");
@@ -922,17 +1334,21 @@ sub WriteQuickTime($$$)
922
1334
 
923
1335
  # write the movie data
924
1336
  foreach $mdat (@mdat) {
925
- $raf->Seek($$mdat[0], 0) or $et->Error('Seek error'), last;
926
1337
  Write($outfile, $$mdat[2]) or $rtnVal = 0; # write mdat header
927
- if ($$mdat[1]) {
928
- my $result = Image::ExifTool::CopyBlock($raf, $outfile, $$mdat[1] - $$mdat[0]);
929
- defined $result or $rtnVal = 0, last;
930
- $result or $et->Error("Truncated mdat atom"), last;
1338
+ if ($$mdat[4]) {
1339
+ Write($outfile, ${$$mdat[4]}) or $rtnVal = 0;
931
1340
  } else {
932
- # mdat continues to end of file
933
- my $buff;
934
- while ($raf->Read($buff, 65536)) {
935
- Write($outfile, $buff) or $rtnVal = 0, last;
1341
+ $raf->Seek($$mdat[0], 0) or $et->Error('Seek error'), last;
1342
+ if ($$mdat[1]) {
1343
+ my $result = Image::ExifTool::CopyBlock($raf, $outfile, $$mdat[1] - $$mdat[0]);
1344
+ defined $result or $rtnVal = 0, last;
1345
+ $result or $et->Error("Truncated mdat atom"), last;
1346
+ } else {
1347
+ # mdat continues to end of file
1348
+ my $buff;
1349
+ while ($raf->Read($buff, 65536)) {
1350
+ Write($outfile, $buff) or $rtnVal = 0, last;
1351
+ }
936
1352
  }
937
1353
  }
938
1354
  }
@@ -967,7 +1383,13 @@ sub WriteMOV($$)
967
1383
  $raf->Read($buff, $size-8) == $size-8 and
968
1384
  $buff !~ /^(....)+(qt )/s)
969
1385
  {
970
- $ftype = $buff =~ /^(heic|mif1|msf1|heix|hevc|hevx)/ ? 'HEIC' : 'MP4';
1386
+ if ($buff =~ /^crx /) {
1387
+ $ftype = 'CR3',
1388
+ } elsif ($buff =~ /^(heic|mif1|msf1|heix|hevc|hevx)/) {
1389
+ $ftype = 'HEIC';
1390
+ } else {
1391
+ $ftype = 'MP4';
1392
+ }
971
1393
  } else {
972
1394
  $ftype = 'MOV';
973
1395
  }