exiftool_vendored 11.44.0 → 11.47.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.

@@ -52,6 +52,7 @@ with tag keys. Below is an explanation of the meaning of each special key:
52
52
  FixOffsets - Evaluated for each value pointer to patch maker note offsets
53
53
  LastIFD - Used by WriteExif() to return offset of last IFD written
54
54
  ImageData - Used by WriteExif() to avoid buffering large image data blocks
55
+ NoRefTest - Flag to bypass "referenced by previous directory" test
55
56
 
56
57
  WRITE_PROC : Function reference or name for writing this directory. The
57
58
  function returns the new directory data or undefined on error. It takes the
@@ -6772,6 +6772,7 @@ my %tagExists = (
6772
6772
  'clonetype' => 1,
6773
6773
  'closedcaptioning' => 1,
6774
6774
  'cmmflags' => 1,
6775
+ 'cmp1' => 1,
6775
6776
  'cmykequivalent' => 1,
6776
6777
  'coarsedata' => 1,
6777
6778
  'coarsemapimage' => 1,
@@ -7928,6 +7929,7 @@ my %tagExists = (
7928
7929
  'hyperlinkbase' => 1,
7929
7930
  'hyperlinks' => 1,
7930
7931
  'hyperlinkschanged' => 1,
7932
+ 'iad1' => 1,
7931
7933
  'icc_untagged' => 1,
7932
7934
  'iccbased' => 1,
7933
7935
  'iconenvdata' => 1,
@@ -10383,6 +10385,7 @@ my %tagExists = (
10383
10385
  'usptooriginalcontenttype' => 1,
10384
10386
  'utm' => 1,
10385
10387
  'uuid-canon' => 1,
10388
+ 'uuid-canon2' => 1,
10386
10389
  'uuid-exif' => 1,
10387
10390
  'uuid-exif2' => 1,
10388
10391
  'uuid-exif_bad' => 1,
@@ -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 22584 tags, with 15004 unique tag names.
15
+ They contain a total of 22586 tags, with 15003 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
@@ -7042,8 +7042,21 @@ information.
7042
7042
 
7043
7043
  Tag ID Tag Name Writable
7044
7044
  ------ -------- --------
7045
+ 'IAD1' IAD1 Canon IAD1
7046
+
7047
+ =head3 Canon IAD1 Tags
7048
+
7049
+ Index2 Tag Name Writable
7050
+ ------ -------- --------
7045
7051
  [no tags known]
7046
7052
 
7053
+ =head3 Canon CMP1 Tags
7054
+
7055
+ Index2 Tag Name Writable
7056
+ ------ -------- --------
7057
+ 8 ImageWidth no
7058
+ 10 ImageHeight no
7059
+
7047
7060
  =head3 Canon CNOP Tags
7048
7061
 
7049
7062
  Tag ID Tag Name Writable
@@ -7090,6 +7103,12 @@ Information found in the "skip" atom of Canon MOV videos.
7090
7103
  ------ -------- --------
7091
7104
  'CNDB' Unknown_CNDB? no
7092
7105
 
7106
+ =head3 Canon uuid2 Tags
7107
+
7108
+ Tag ID Tag Name Writable
7109
+ ------ -------- --------
7110
+ 'CNOP' CanonVRD CanonVRD
7111
+
7093
7112
  =head2 CanonCustom Tags
7094
7113
 
7095
7114
  =head3 CanonCustom Functions1D Tags
@@ -23954,22 +23973,29 @@ Frame rate information stored by some Canon video cameras.
23954
23973
 
23955
23974
  The QuickTime format is used for many different types of audio, video and
23956
23975
  image files (most notably, MOV/MP4 videos and HEIC/CR3 images). Exiftool
23957
- extracts standard meta information a variety of audio, video and image
23976
+ extracts standard meta information and a variety of audio, video and image
23958
23977
  parameters, as well as proprietary information written by many camera
23959
23978
  models. Tags with a question mark after their name are not extracted unless
23960
23979
  the Unknown option is set.
23961
23980
 
23962
23981
  When writing, ExifTool creates both QuickTime and XMP tags by default, but
23963
- the group may be specified to write one or the other separately. Newly
23964
- created QuickTime tags are added in the ItemList location if possible,
23965
- otherwise in UserData, but this may be changed by specifying the location.
23966
- Alternate language tags may be accessed for ItemList tags by adding a
23967
- language-country code to the tag name (eg. "ItemList:Artist-fra-FR"), or for
23968
- UserData tags by adding a language code (eg. "UserData:Artist-fra"). If no
23969
- language code is specified when writing, alternate languages are deleted.
23970
- Use the "und" language code to write the default language without deleting
23971
- alternate languages. Note that "eng" is treated as a default language when
23972
- reading, but not when writing.
23982
+ the group may be specified to write one or the other separately. If no
23983
+ location is specified, newly created QuickTime tags are added in the
23984
+ ItemList location if possible, otherwise in UserData, and finally in Keys,
23985
+ but this order may be changed by setting the PREFERRED level of the
23986
+ appropriate table in the config file (see example.config in the full
23987
+ distribution for an example). ExifTool currently writes only top-level
23988
+ metadata in QuickTime-based files; it extracts other track-specific and
23989
+ timed metadata, but can not yet edit tags in these locations.
23990
+
23991
+ Alternate language tags may be accessed for ItemList and Keys tags by adding
23992
+ a 3-character ISO 639-2 language code and an optional ISO 3166-1 alpha 2
23993
+ country code to the tag name (eg. "ItemList:Artist-deu" or
23994
+ "ItemList::Artist-deu-DE"). UserData tags support only a language code
23995
+ (without a country code). If no language code is specified when writing,
23996
+ alternate languages for the tag are deleted. Use the "und" language code to
23997
+ write the default language without deleting alternate languages. Note that
23998
+ "eng" is treated as a default language when reading, but not when writing.
23973
23999
 
23974
24000
  According to the specification, many QuickTime date/time tags should be
23975
24001
  stored as UTC. Unfortunately, digital cameras often store local time values
@@ -24016,6 +24042,7 @@ for the official specification.
24016
24042
  'uuid' XMP XMP
24017
24043
  UUID-PROF QuickTime Profile
24018
24044
  UUID-Flip QuickTime Flip
24045
+ UUID-Canon2 Canon uuid2
24019
24046
  PreviewImage no
24020
24047
  UUID-Unknown? no
24021
24048
  'wide' Wide? no
@@ -24367,6 +24394,10 @@ MP4 data reference box.
24367
24394
 
24368
24395
  As well as these tags, the "mdta" handler uses numerical tag ID's which are
24369
24396
  added dynamically to this table after processing the Meta Keys information.
24397
+ Tags in this table support alternate languages which are accessed by adding
24398
+ a 3-character ISO 639-2 language code and an optional ISO 3166-1 alpha 2
24399
+ country code to the tag name (eg. "ItemList:Title-fra" or
24400
+ "ItemList::Title-fra-FR").
24370
24401
 
24371
24402
  Tag ID Tag Name Writable
24372
24403
  ------ -------- --------
@@ -24821,6 +24852,7 @@ Child atoms found in "sinf" and/or "pinf" atoms.
24821
24852
  25 CompressorName no
24822
24853
  41 BitDepth no
24823
24854
  'CDI1' CDI1 Canon CDI1
24855
+ 'CMP1' CMP1 Canon CMP1
24824
24856
  'JPEG' JPEGInfo? no
24825
24857
  'avcC' AVCConfiguration? no
24826
24858
  'btrt' BitrateInfo QuickTime Bitrate
@@ -24966,9 +24998,9 @@ MP4 video media header.
24966
24998
  =head3 QuickTime UserData Tags
24967
24999
 
24968
25000
  Tag ID's beginning with the copyright symbol (hex 0xa9) are multi-language
24969
- text. Alternate language tags are accessed by adding a dash followed by the
24970
- language/country code to the tag name. ExifTool will extract any
24971
- multi-language user data tags found, even if they don't exist in this table.
25001
+ text. Alternate language tags are accessed by adding a dash followed by a
25002
+ 3-character ISO 639-2 language code to the tag name. ExifTool will extract
25003
+ any multi-language user data tags found, even if they aren't in this table.
24972
25004
 
24973
25005
  Tag ID Tag Name Writable
24974
25006
  ------ -------- --------
@@ -215,7 +215,7 @@ sub FormatIPTC($$$$$;$)
215
215
  sub IptcDate($)
216
216
  {
217
217
  my $val = shift;
218
- unless ($val =~ s/^.*(\d{4}):?(\d{2}):?(\d{2}).*/$1$2$3/s) {
218
+ unless ($val =~ s{^.*(\d{4})[-:/.]?(\d{2})[-:/.]?(\d{2}).*}{$1$2$3}s) {
219
219
  warn "Invalid date format (use YYYY:mm:dd)\n";
220
220
  undef $val;
221
221
  }
@@ -12,21 +12,23 @@ use strict;
12
12
  # maps for adding metadata to various QuickTime-based file types
13
13
  my %movMap = (
14
14
  # MOV (no 'ftyp', or 'ftyp'='qt ') -> XMP in 'moov'-'udta'-'XMP_'
15
- QuickTime => 'ItemList',
16
- ItemList => 'Meta',
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)
17
18
  Meta => 'UserData',
18
- XMP => 'UserData',
19
- UserData => 'Movie',
19
+ XMP => 'UserData', # MOV-Movie-UserData-XMP
20
+ UserData => 'Movie', # MOV-Movie-UserData
20
21
  Movie => 'MOV',
21
22
  );
22
23
  my %mp4Map = (
23
24
  # MP4 ('ftyp' compatible brand 'mp41', 'mp42' or 'f4v ') -> XMP at top level
24
- QuickTime => 'ItemList',
25
- ItemList => 'Meta',
25
+ QuickTime => 'ItemList', # (default location for QuickTime tags)
26
+ ItemList => 'Meta', # MOV-Movie-UserData-Meta-ItemList
27
+ Keys => 'Movie', # MOV-Movie-Meta-Keys !! (hack due to different Meta location)
26
28
  Meta => 'UserData',
27
- UserData => 'Movie',
29
+ UserData => 'Movie', # MOV-Movie-UserData
28
30
  Movie => 'MOV',
29
- XMP => 'MOV',
31
+ XMP => 'MOV', # MOV-XMP
30
32
  );
31
33
  my %heicMap = (
32
34
  # HEIC ('ftyp' compatible brand 'heic' or 'mif1') -> XMP/EXIF in top level 'meta'
@@ -55,6 +57,8 @@ my %cr3Map = (
55
57
  IFD0 => 'UUID-Canon',
56
58
  GPS => 'UUID-Canon',
57
59
  #MakerNoteCanon => 'UUID-Canon', # (doesn't yet work -- goes into ExifIFD instead)
60
+ 'UUID-Canon2' => 'MOV',
61
+ CanonVRD => 'UUID-Canon2',
58
62
  );
59
63
  my %dirMap = (
60
64
  MOV => \%movMap,
@@ -72,6 +76,18 @@ my %qtFormat = (
72
76
  );
73
77
  my $undLang = 0x55c4; # numeric code for default ('und') language
74
78
 
79
+ # boxes that may exist in an "empty" Meta box:
80
+ my %emptyMeta = (
81
+ hdlr => 'Handler', 'keys' => 'Keys', lang => 'Language', ctry => 'Country', free => 'Free',
82
+ );
83
+
84
+ # lookup for CTBO ID number based on uuid for Canon CR3 files
85
+ my %ctboID = (
86
+ "\xbe\x7a\xcf\xcb\x97\xa9\x42\xe8\x9c\x71\x99\x94\x91\xe3\xaf\xac" => 1, # XMP
87
+ "\xea\xf4\x2b\x5e\x1c\x98\x4b\x88\xb9\xfb\xb7\xdc\x40\x6e\x4d\x16" => 2, # PreviewImage
88
+ # ID 3 is used for 'mdat' atom (not a uuid)
89
+ );
90
+
75
91
  # mark UserData tags that don't have ItemList counterparts as Preferred
76
92
  # (and for now, set Writable to 0 for any tag with a RawConv)
77
93
  {
@@ -139,18 +155,6 @@ sub ConvInvISO6709($)
139
155
  return undef;
140
156
  }
141
157
 
142
- #------------------------------------------------------------------------------
143
- # Check to see if path is current
144
- # Inputs: 0) ExifTool ref, 1) directory name
145
- # Returns: true if current path is the root of the specified directory
146
- sub IsCurPath($$)
147
- {
148
- local $_;
149
- my ($et, $dir) = @_;
150
- $dir = $$et{DirMap}{$dir} and $dir eq $_ or last foreach reverse @{$$et{PATH}};
151
- return($dir and $dir eq 'MOV');
152
- }
153
-
154
158
  #------------------------------------------------------------------------------
155
159
  # Handle offsets in iloc (ItemLocation) atom when writing (ref ISO 14496-12:2015 pg.79)
156
160
  # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) data ref, 3) output buffer ref
@@ -239,7 +243,6 @@ sub Handle_iloc($$$$)
239
243
  # decide whether to fix up the base offset or individual item offsets
240
244
  # (adjust the one that is larger)
241
245
  if (defined $minOffset and $minOffset > $base_offset) {
242
- $off or $off = $$dirInfo{ChunkOffset} = [ ];
243
246
  $$_[3] = $base_offset foreach @offItem;
244
247
  push @$off, @offItem;
245
248
  } else {
@@ -318,6 +321,100 @@ sub SetVarInt($$)
318
321
  return '';
319
322
  }
320
323
 
324
+ #------------------------------------------------------------------------------
325
+ # Write Meta Keys to add/delete entries as necessary ('mdta' handler) (ref PH)
326
+ # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
327
+ # Returns: updated keys box data
328
+ sub WriteKeys($$$)
329
+ {
330
+ my ($et, $dirInfo, $tagTablePtr) = @_;
331
+ $et or return 1; # allow dummy access to autoload this package
332
+ my $dataPt = $$dirInfo{DataPt};
333
+ my $dirLen = length $$dataPt;
334
+ my $outfile = $$dirInfo{OutFile};
335
+ my ($tag, %done, %remap, %info, %add, $i);
336
+
337
+ $dirLen < 8 and $et->Warn('Short Keys box'), $dirLen = 8, $$dataPt = "\0" x 8;
338
+ if ($$et{DEL_GROUP}{Keys}) {
339
+ $dirLen = 8; # delete all existing keys
340
+ # deleted keys are identified by a zero entry in the Remap lookup
341
+ my $n = Get32u($dataPt, 4);
342
+ for ($i=1; $i<=$n; ++$i) { $remap{$i} = 0; }
343
+ $et->VPrint(0, " [deleting $n Keys entr".($n==1 ? 'y' : 'ies')."]\n");
344
+ ++$$et{CHANGED};
345
+ }
346
+ my $pos = 8;
347
+ my $newTags = $et->GetNewTagInfoHash($tagTablePtr);
348
+ my $newData = substr($$dataPt, 0, $pos);
349
+
350
+ my $newIndex = 1;
351
+ my $index = 1;
352
+ while ($pos < $dirLen - 4) {
353
+ my $len = unpack("x${pos}N", $$dataPt);
354
+ last if $len < 8 or $pos + $len > $dirLen;
355
+ my $ns = substr($$dataPt, $pos + 4, 4);
356
+ $tag = substr($$dataPt, $pos + 8, $len - 8);
357
+ $tag =~ s/\0.*//s; # truncate at null
358
+ $tag =~ s/^com\.apple\.quicktime\.// if $ns eq 'mdta'; # remove apple quicktime domain
359
+ $tag = "Tag_$ns" unless $tag;
360
+ $done{$tag} = 1; # set flag to avoid creating this tag
361
+ my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
362
+ if ($tagInfo) {
363
+ $info{$index} = $tagInfo;
364
+ if ($$newTags{$tag}) {
365
+ my $nvHash = $et->GetNewValueHash($tagInfo);
366
+ # drop this tag if it is being deleted
367
+ if ($nvHash and $et->IsOverwriting($nvHash) > 0 and not defined $et->GetNewValue($nvHash)) {
368
+ # delete this key
369
+ $et->VPrint(1, "$$et{INDENT}\[deleting Keys entry $index '${tag}']\n");
370
+ $pos += $len;
371
+ $remap{$index++} = 0;
372
+ ++$$et{CHANGED};
373
+ next;
374
+ }
375
+ }
376
+ }
377
+ # add to the Keys box data
378
+ $newData .= substr($$dataPt, $pos, $len);
379
+ $remap{$index++} = $newIndex++;
380
+ $pos += $len;
381
+ }
382
+ # add keys for any tags we need to create
383
+ foreach $tag (sort keys %$newTags) {
384
+ my $tagInfo = $$newTags{$tag};
385
+ my $id;
386
+ if ($$tagInfo{LangCode} and $$tagInfo{SrcTagInfo}) {
387
+ $id = $$tagInfo{SrcTagInfo}{TagID};
388
+ } else {
389
+ $id = $tag;
390
+ }
391
+ next if $done{$id};
392
+ my $nvHash = $et->GetNewValueHash($tagInfo);
393
+ next unless $$nvHash{IsCreating} and $et->IsOverwriting($nvHash) and
394
+ defined $et->GetNewValue($nvHash);
395
+ # add new entry to 'keys' data
396
+ my $val = "com.apple.quicktime.$id";
397
+ $newData .= Set32u(8 + length($val)) . 'mdta' . $val;
398
+ $et->VPrint(1, "$$et{INDENT}\[adding Keys entry $newIndex '${id}']\n");
399
+ $add{$newIndex++} = $tagInfo;
400
+ ++$$et{CHANGED};
401
+ }
402
+ my $num = $newIndex - 1;
403
+ if ($num) {
404
+ Set32u($num, \$newData, 4); # update count in header
405
+ } else {
406
+ $newData = ''; # delete empty Keys box
407
+ }
408
+ # save temporary variables for use when writing ItemList:
409
+ # Remap - lookup for remapping Keys ID numbers (0 if item is deleted)
410
+ # Info - Keys tag information, based on old index value
411
+ # Add - Keys items deleted, based on old index value
412
+ # Num - Number of items in edited Keys box
413
+ $$et{Keys} = { Remap => \%remap, Info => \%info, Add => \%add, Num => $num };
414
+
415
+ return $newData; # return updated Keys box
416
+ }
417
+
321
418
  #------------------------------------------------------------------------------
322
419
  # Write ItemInformation in HEIC files
323
420
  # Inputs: 0) ExifTool ref, 1) dirInfo ref (with BoxPos entry), 2) output buffer ref
@@ -571,13 +668,14 @@ sub WriteItemInfo($$$)
571
668
  # Returns: A) if dirInfo contains DataPt: new directory data
572
669
  # B) otherwise: true on success, 0 if a write error occurred
573
670
  # (true but sets an Error on a file format error)
671
+ # Notes: Yes, this is a real mess. Just like the QuickTime metadata situation.
574
672
  sub WriteQuickTime($$$)
575
673
  {
576
674
  local $_;
577
675
  my ($et, $dirInfo, $tagTablePtr) = @_;
578
676
  $et or return 1; # allow dummy access to autoload this package
579
- my ($mdat, @mdat, @mdatEdit, $edit, $track, $outBuff, $co, $term, $err);
580
- my (%langTags, $keysTags, $canCreate, %didTag, $delGrp, %boxPos);
677
+ my ($mdat, @mdat, @mdatEdit, $edit, $track, $outBuff, $co, $term, $err, $delCount);
678
+ my (%langTags, $canCreate, $delGrp, %boxPos, $createKeys, $newTags, %didDir, $writeLast);
581
679
  my $outfile = $$dirInfo{OutFile} || return 0;
582
680
  my $raf = $$dirInfo{RAF}; # (will be null for lower-level atoms)
583
681
  my $dataPt = $$dirInfo{DataPt}; # (will be null for top-level atoms)
@@ -585,6 +683,7 @@ sub WriteQuickTime($$$)
585
683
  my $dirStart = $$dirInfo{DirStart} || 0;
586
684
  my $parent = $$dirInfo{Parent};
587
685
  my $addDirs = $$et{ADD_DIRS};
686
+ my $didTag = $$et{DidTag};
588
687
  my ($rtnVal, $rtnErr) = $dataPt ? (undef, undef) : (1, 0);
589
688
 
590
689
  if ($dataPt) {
@@ -592,27 +691,47 @@ sub WriteQuickTime($$$)
592
691
  } else {
593
692
  return 0 unless $raf;
594
693
  }
595
- # initialize ItemList key directory count
596
- $$et{KeyCount} = 0 unless defined $$et{KeyCount};
597
-
598
694
  # use buffered output for everything but 'mdat' atoms
599
695
  $outBuff = '';
600
696
  $outfile = \$outBuff;
601
697
 
602
698
  $raf->Seek($dirStart, 1) if $dirStart; # skip header if it exists
603
699
 
604
- # get hash of new tags to add to this directory if this is the proper place for them
605
700
  my $curPath = join '-', @{$$et{PATH}};
606
701
  my ($dir, $writePath) = ($dirName, $dirName);
607
702
  $writePath = "$dir-$writePath" while defined($dir = $$et{DirMap}{$dir});
608
- my $delQt = $$et{DEL_GROUP}{QuickTime};
609
- if ($curPath eq $writePath) {
703
+ # hack for Keys write path (its containing Meta is in a different location)
704
+ $createKeys = 1 if $$addDirs{Keys} and $curPath =~ /^MOV-Movie(-Meta(-ItemList)?)?$/;
705
+ if ($curPath eq $writePath or $createKeys) {
610
706
  $canCreate = 1;
611
- $delGrp = $delQt || $$et{DEL_GROUP}{$dirName};
612
- $et->VPrint(0, " Deleting $dirName tags\n") if $delGrp;
707
+ $delGrp = $$et{DEL_GROUP}{$dirName};
613
708
  }
614
- my $newTags = $et->GetNewTagInfoHash($tagTablePtr);
615
- # make lookup of language tags for this ID
709
+ # edit existing Keys tags in ItemList if we are at the correct path
710
+ if ($curPath eq 'MOV-Movie-Meta-ItemList') {
711
+ $newTags = { };
712
+ my $keys = $$et{Keys};
713
+ if ($keys) {
714
+ # add new tag entries for existing Keys tags, now that we know their ID's
715
+ my ($index, %keysInfo);
716
+ foreach $index (keys %{$$keys{Info}}) {
717
+ $keysInfo{$$keys{Info}{$index}} = $index if $$keys{Remap}{$index};
718
+ }
719
+ my $keysTable = GetTagTable('Image::ExifTool::QuickTime::Keys');
720
+ my $newKeysTags = $et->GetNewTagInfoHash($keysTable);
721
+ foreach (keys %$newKeysTags) {
722
+ my $tagInfo = $$newKeysTags{$_};
723
+ $index = $keysInfo{$tagInfo} || ($$tagInfo{SrcTagInfo} and $keysInfo{$$tagInfo{SrcTagInfo}});
724
+ next unless $index;
725
+ my $id = Set32u($index);
726
+ $id .= '-' . $$tagInfo{LangCode} if $$tagInfo{LangCode};
727
+ $$newTags{$id} = $tagInfo;
728
+ }
729
+ }
730
+ } else {
731
+ # get hash of new tags to edit/create in this directory
732
+ $newTags = $et->GetNewTagInfoHash($tagTablePtr);
733
+ }
734
+ # make lookup of language tags for each ID
616
735
  foreach (keys %$newTags) {
617
736
  next unless $$newTags{$_}{LangCode} and $$newTags{$_}{SrcTagInfo};
618
737
  my $id = $$newTags{$_}{SrcTagInfo}{TagID};
@@ -621,7 +740,7 @@ sub WriteQuickTime($$$)
621
740
  }
622
741
 
623
742
  for (;;) { # loop through all atoms at this level
624
- my ($hdr, $buff);
743
+ my ($hdr, $buff, $keysIndex);
625
744
  my $n = $raf->Read($hdr, 8);
626
745
  unless ($n == 8) {
627
746
  if ($n == 4 and $hdr eq "\0\0\0\0") {
@@ -701,7 +820,7 @@ sub WriteQuickTime($$$)
701
820
 
702
821
  # if this atom stores offsets, save its location so we can fix up offsets later
703
822
  # (are there any other atoms that may store absolute file offsets?)
704
- if ($tag =~ /^(stco|co64|iloc|mfra|gps )$/) {
823
+ if ($tag =~ /^(stco|co64|iloc|mfra|gps |CTBO|uuid)$/) {
705
824
  # (note that we only need to do this if the movie data is stored in this file)
706
825
  my $flg = $$et{QtDataFlg};
707
826
  if ($tag eq 'mfra') {
@@ -713,7 +832,6 @@ sub WriteQuickTime($$$)
713
832
  # (only care about the 'gps ' box in 'moov')
714
833
  if ($$dirInfo{DirID} and $$dirInfo{DirID} eq 'moov' and length $buff > 8) {
715
834
  my $off = $$dirInfo{ChunkOffset};
716
- $off or $off = $$dirInfo{ChunkOffset} = [ ];
717
835
  my $num = Get32u(\$buff, 4);
718
836
  $num = int((length($buff) - 8) / 8) if $num * 8 + 8 > length($buff);
719
837
  my $i;
@@ -721,6 +839,8 @@ sub WriteQuickTime($$$)
721
839
  push @$off, [ 'stco_gps ', length($$outfile) + length($hdr) + 8 + $i * 8, 4 ];
722
840
  }
723
841
  }
842
+ } elsif ($tag eq 'CTBO' or $tag eq 'uuid') { # hack for updating CR3 CTBO offsets
843
+ push @{$$dirInfo{ChunkOffset}}, [ $tag, length($$outfile), length($hdr) + $size ];
724
844
  } elsif (not $flg) {
725
845
  my $grp = $$et{CUR_WRITE_GROUP} || $parent;
726
846
  $et->Error("Can't locate data reference to update offsets for $grp");
@@ -730,9 +850,7 @@ sub WriteQuickTime($$$)
730
850
  return $rtnVal;
731
851
  } elsif ($flg == 1) {
732
852
  # must update offsets since the data is in this file
733
- my $off = $$dirInfo{ChunkOffset};
734
- $off or $off = $$dirInfo{ChunkOffset} = [ ];
735
- push @$off, [ $tag, length($$outfile) + length($hdr), $size ];
853
+ push @{$$dirInfo{ChunkOffset}}, [ $tag, length($$outfile) + length($hdr), $size ];
736
854
  }
737
855
  }
738
856
 
@@ -743,14 +861,40 @@ sub WriteQuickTime($$$)
743
861
  &{$$tagInfo{WriteHook}}($buff,$et) if $tagInfo and $$tagInfo{WriteHook};
744
862
 
745
863
  # allow numerical tag ID's (ItemList entries defined by Keys)
746
- unless ($tagInfo) {
747
- my $id = $$et{KeyCount} . '.' . unpack('N', $tag);
748
- $tagInfo = $et->GetTagInfo($tagTablePtr, $id);
864
+ if (not $tagInfo and $dirName eq 'ItemList' and $$et{Keys}) {
865
+ $keysIndex = unpack('N', $tag);
866
+ my $newIndex = $$et{Keys}{Remap}{$keysIndex};
867
+ if (defined $newIndex) {
868
+ $tagInfo = $$et{Keys}{Info}{$keysIndex};
869
+ unless ($newIndex) {
870
+ if ($tagInfo) {
871
+ $et->VPrint(1," - Keys:$$tagInfo{Name}");
872
+ } else {
873
+ $delCount = ($delCount || 0) + 1;
874
+ }
875
+ ++$$et{CHANGED};
876
+ next;
877
+ }
878
+ # use the new Keys index of this item if it changed
879
+ unless ($keysIndex == $newIndex) {
880
+ $tag = Set32u($newIndex);
881
+ substr($hdr, 4, 4) = $tag;
882
+ }
883
+ } else {
884
+ undef $keysIndex;
885
+ }
749
886
  }
750
- # delete all ItemList/UserData tags if deleting group
751
- if ($delGrp and $dirName =~ /^(ItemList|UserData)$/) {
752
- ++$$et{CHANGED};
753
- next;
887
+ # delete all ItemList tags when deleting group, but take care not to delete UserData Meta
888
+ if ($delGrp) {
889
+ if ($dirName eq 'ItemList') {
890
+ $delCount = ($delCount || 0) + 1;
891
+ ++$$et{CHANGED};
892
+ next;
893
+ } elsif ($dirName eq 'UserData' and (not $tagInfo or not $$tagInfo{SubDirectory})) {
894
+ $delCount = ($delCount || 0) + 1;
895
+ ++$$et{CHANGED};
896
+ next;
897
+ }
754
898
  }
755
899
  undef $tagInfo if $tagInfo and $$tagInfo{Unknown};
756
900
 
@@ -786,6 +930,7 @@ sub WriteQuickTime($$$)
786
930
  HasData => $$subdir{HasData},
787
931
  Multi => $$subdir{Multi}, # necessary?
788
932
  OutFile => $outfile,
933
+ NoRefTest=> 1, # don't check directory references
789
934
  # initialize array to hold details about chunk offset table
790
935
  # (each entry has 3-5 items: 0=atom type, 1=table offset, 2=table size,
791
936
  # 3=optional base offset, 4=optional item ID)
@@ -826,26 +971,15 @@ sub WriteQuickTime($$$)
826
971
  $$_[1] += $start foreach @chunkOffset;
827
972
  }
828
973
  # the directory exists, so we don't need to add it
829
- delete $$addDirs{$subName} if IsCurPath($et, $subName);
974
+ if ($curPath eq $writePath and $$addDirs{$subName} and $$addDirs{$subName} eq $dirName) {
975
+ delete $$addDirs{$subName};
976
+ }
977
+ $didDir{$tag} = 1; # (note: keyed by tag ID)
830
978
 
831
979
  } else { # modify existing QuickTime tags in various formats
832
980
 
833
981
  my $nvHash = $et->GetNewValueHash($tagInfo);
834
- # get new value from Keys source tag if necessary
835
- if (not $nvHash and $$tagInfo{KeysInfo}) {
836
- $nvHash = $et->GetNewValueHash($$tagInfo{KeysInfo});
837
- # may be writing this as a language tag, so fill in $langTags for this ID
838
- unless ($keysTags) {
839
- $keysTags = $et->GetNewTagInfoHash(GetTagTable('Image::ExifTool::QuickTime::Keys'));
840
- }
841
- foreach (keys %$keysTags) {
842
- next unless $$keysTags{$_}{SrcTagInfo};
843
- next unless $$keysTags{$_}{SrcTagInfo} eq $$tagInfo{KeysInfo};
844
- $langTags{$tag} = { } unless $langTags{$tag};
845
- $langTags{$tag}{$_} = $$keysTags{$_};
846
- }
847
- }
848
- if ($nvHash or $langTags{$tag} or $delQt) {
982
+ if ($nvHash or $langTags{$tag} or $delGrp) {
849
983
  my $nvHashNoLang = $nvHash;
850
984
  my ($val, $len, $lang, $type, $flags, $ctry, $charsetQuickTime);
851
985
  my $format = $$tagInfo{Format};
@@ -854,19 +988,50 @@ sub WriteQuickTime($$$)
854
988
  if ($hasData) {
855
989
  my $pos = 0;
856
990
  for (;;$pos+=$len) {
857
- last if $pos + 16 > $size;
991
+ if ($pos + 16 > $size) {
992
+ # add any new alternate language tags now
993
+ if ($langTags{$tag}) {
994
+ my $tg;
995
+ foreach $tg ('', sort keys %{$langTags{$tag}}) {
996
+ my $ti = $tg ? $langTags{$tag}{$tg} : $nvHashNoLang;
997
+ $nvHash = $et->GetNewValueHash($ti);
998
+ next unless $nvHash and not $$didTag{$nvHash};
999
+ $$didTag{$nvHash} = 1;
1000
+ next unless $$nvHash{IsCreating} and $et->IsOverwriting($nvHash);
1001
+ my $newVal = $et->GetNewValue($nvHash);
1002
+ next unless defined $newVal;
1003
+ my $prVal = $newVal;
1004
+ my $flags = FormatQTValue($et, \$newVal, $$tagInfo{Format});
1005
+ next unless defined $newVal;
1006
+ my ($ctry, $lang) = (0,0);
1007
+ if ($$ti{LangCode}) {
1008
+ unless ($$ti{LangCode} =~ /^([A-Z]{3})?[-_]?([A-Z]{2})?$/i) {
1009
+ $et->Warn("Invalid language code for $$ti{Name}");
1010
+ next;
1011
+ }
1012
+ # pack language and country codes
1013
+ if ($1 and $1 ne 'und') {
1014
+ $lang = ($lang << 5) | ($_ - 0x60) foreach unpack 'C*', lc($1);
1015
+ }
1016
+ $ctry = unpack('n', pack('a2',uc($2))) if $2 and $2 ne 'ZZ';
1017
+ }
1018
+ $newData = substr($buff, 0, $pos) unless defined $newData;
1019
+ $newData .= pack('Na4Nnn',16+length($newVal),'data',$flags,$ctry,$lang).$newVal;
1020
+ my $grp = $et->GetGroup($ti, 1);
1021
+ $et->VerboseValue("+ $grp:$$ti{Name}", $prVal);
1022
+ ++$$et{CHANGED};
1023
+ }
1024
+ }
1025
+ last;
1026
+ }
858
1027
  ($len, $type, $flags, $ctry, $lang) = unpack("x${pos}Na4Nnn", $buff);
859
1028
  $lang or $lang = $undLang; # treat both 0 and 'und' as 'und'
860
1029
  $langInfo = $tagInfo;
861
- my $delTag = $delQt;
1030
+ my $delTag = $delGrp;
862
1031
  my $newVal;
863
1032
  my $langCode = GetLangCode($lang, $ctry, 1);
864
1033
  for (;;) {
865
- if ($$tagInfo{KeysInfo}) {
866
- $langInfo = GetLangInfo($$tagInfo{KeysInfo}, $langCode);
867
- } else {
868
- $langInfo = GetLangInfo($tagInfo, $langCode);
869
- }
1034
+ $langInfo = GetLangInfo($tagInfo, $langCode);
870
1035
  $nvHash = $et->GetNewValueHash($langInfo);
871
1036
  last if $nvHash or not $ctry or $lang ne $undLang or length($langCode)==2;
872
1037
  # check to see if tag was written with a 2-char country code only
@@ -875,7 +1040,7 @@ sub WriteQuickTime($$$)
875
1040
  # set flag to delete language tag when writing default
876
1041
  # (except for a default-language Keys entry)
877
1042
  if (not $nvHash and $nvHashNoLang) {
878
- if ($lang eq $undLang and not $ctry and not $didTag{$nvHashNoLang}) {
1043
+ if ($lang eq $undLang and not $ctry and not $$didTag{$nvHashNoLang}) {
879
1044
  $nvHash = $nvHashNoLang; # write existing default
880
1045
  } else {
881
1046
  $delTag = 1; # delete tag
@@ -903,34 +1068,28 @@ sub WriteQuickTime($$$)
903
1068
  }
904
1069
  if (($nvHash and $et->IsOverwriting($nvHash, $val)) or $delTag) {
905
1070
  $newVal = $et->GetNewValue($nvHash) if defined $nvHash;
906
- if ($delTag or (not defined $newVal or $didTag{$nvHash})) {
907
- if ($canCreate) {
908
- my $grp = $et->GetGroup($langInfo, 1);
909
- $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
910
- $newData = substr($buff, 0, $pos-16) unless defined $newData;
911
- ++$$et{CHANGED};
912
- $pos += $len;
913
- next;
914
- }
915
- $newVal = '';
916
- }
917
- my $prVal = $newVal;
918
- # format new value for writing (and get new flags)
919
- $flags = FormatQTValue($et, \$newVal, $$tagInfo{Format});
920
- if (defined $newVal and not ($nvHash and $didTag{$nvHash})) {
921
- next if $newVal eq '' and $val eq '';
922
- ++$$et{CHANGED};
1071
+ if ($delTag or not defined $newVal or $$didTag{$nvHash}) {
1072
+ # delete the tag
923
1073
  my $grp = $et->GetGroup($langInfo, 1);
924
1074
  $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
925
- $et->VerboseValue("+ $grp:$$langInfo{Name}", $prVal);
926
- $didTag{$nvHash} = 1 if $nvHash;
927
- $newData = substr($buff, 0, $pos-16) unless defined $newData;
928
- $newData .= pack('Na4Nnn', length($newVal)+16, $type, $flags, $ctry, $lang);
929
- $newData .= $newVal;
930
- } elsif (defined $newData) {
931
1075
  # copy data up to start of this tag to delete this value
932
- $newData .= substr($buff, $pos-16, $len+16);
1076
+ $newData = substr($buff, 0, $pos-16) unless defined $newData;
1077
+ ++$$et{CHANGED};
1078
+ next;
933
1079
  }
1080
+ my $prVal = $newVal;
1081
+ # format new value for writing (and get new flags)
1082
+ $flags = FormatQTValue($et, \$newVal, $$tagInfo{Format});
1083
+ my $grp = $et->GetGroup($langInfo, 1);
1084
+ $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
1085
+ $et->VerboseValue("+ $grp:$$langInfo{Name}", $prVal);
1086
+ $$didTag{$nvHash} = 1 if $nvHash;
1087
+ $newData = substr($buff, 0, $pos-16) unless defined $newData;
1088
+ $newData .= pack('Na4Nnn', length($newVal)+16, $type, $flags, $ctry, $lang);
1089
+ $newData .= $newVal;
1090
+ ++$$et{CHANGED};
1091
+ } elsif (defined $newData) {
1092
+ $newData .= substr($buff, $pos-16, $len+16);
934
1093
  }
935
1094
  } elsif (defined $newData) {
936
1095
  $newData .= substr($buff, $pos, $len);
@@ -962,10 +1121,9 @@ sub WriteQuickTime($$$)
962
1121
  $val =~ s/\0+$//; # remove trailing nulls if they exist
963
1122
  my $langCode = UnpackLang($lang, 1);
964
1123
  $langInfo = GetLangInfo($tagInfo, $langCode);
965
- # (no need to check $$tagInfo{KeysInfo} because Keys won't get here)
966
1124
  $nvHash = $et->GetNewValueHash($langInfo);
967
1125
  if (not $nvHash and $nvHashNoLang) {
968
- if ($lang eq $undLang and not $didTag{$nvHashNoLang}) {
1126
+ if ($lang eq $undLang and not $$didTag{$nvHashNoLang}) {
969
1127
  $nvHash = $nvHashNoLang;
970
1128
  } elsif ($canCreate) {
971
1129
  # delete other languages when writing default
@@ -982,9 +1140,9 @@ sub WriteQuickTime($$$)
982
1140
  ++$$et{CHANGED};
983
1141
  my $grp = $et->GetGroup($langInfo, 1);
984
1142
  $et->VerboseValue("- $grp:$$langInfo{Name}", $val);
985
- next unless defined $newData and not $didTag{$nvHash};
1143
+ next unless defined $newData and not $$didTag{$nvHash};
986
1144
  $et->VerboseValue("+ $grp:$$langInfo{Name}", $newData);
987
- $didTag{$nvHash} = 1; # set flag so we don't add this tag again
1145
+ $$didTag{$nvHash} = 1; # set flag so we don't add this tag again
988
1146
  # add back necessary header and encode as necessary
989
1147
  if (defined $lang) {
990
1148
  $newData = $et->Encode($newData, $lang < 0x400 ? $charsetQuickTime : 'UTF8');
@@ -999,18 +1157,23 @@ sub WriteQuickTime($$$)
999
1157
  }
1000
1158
  # write the new atom if it was modified
1001
1159
  if (defined $newData) {
1002
- my $len = length $newData;
1003
- $len > 0x7ffffff7 and $et->Error("$$tagInfo{Name} to large to write"), last;
1004
- next unless $len;
1160
+ my $len = length($newData) + 8;
1161
+ $len > 0x7fffffff and $et->Error("$$tagInfo{Name} to large to write"), last;
1162
+ # update size in ChunkOffset list for modified 'uuid' atom
1163
+ $$dirInfo{ChunkOffset}[-1][2] = $len if $tag eq 'uuid';
1164
+ next unless $len > 8; # don't write empty atom header
1005
1165
  # maintain pointer to chunk offsets if necessary
1006
1166
  if (@chunkOffset) {
1007
- $$dirInfo{ChunkOffset} or $$dirInfo{ChunkOffset} = [ ];
1008
1167
  $$_[1] += 8 + length $$outfile foreach @chunkOffset;
1009
1168
  push @{$$dirInfo{ChunkOffset}}, @chunkOffset;
1010
1169
  }
1011
- $boxPos{$tag} = [ length($$outfile), length($newData) + 8 ];
1012
- # write the updated directory now (unless length is zero, or it is needed as padding)
1013
- Write($outfile, Set32u($len+8), $tag, $newData) or $rtnVal=$rtnErr, $err=1, last;
1170
+ if ($$tagInfo{WriteLast}) {
1171
+ $writeLast = ($writeLast || '') . Set32u($len) . $tag . $newData;
1172
+ } else {
1173
+ $boxPos{$tag} = [ length($$outfile), length($newData) + 8 ];
1174
+ # write the updated directory with its atom header
1175
+ Write($outfile, Set32u($len), $tag, $newData) or $rtnVal=$rtnErr, $err=1, last;
1176
+ }
1014
1177
  next;
1015
1178
  }
1016
1179
  }
@@ -1045,25 +1208,61 @@ sub WriteQuickTime($$$)
1045
1208
  }
1046
1209
  $$et{QtDataFlg} = $flg;
1047
1210
  }
1048
- # save position of this box in the output buffer
1049
- $boxPos{$tag} = [ length($$outfile), length($hdr) + length($buff) ];
1050
- # copy the existing atom
1051
- Write($outfile, $hdr, $buff) or $rtnVal=$rtnErr, $err=1, last;
1211
+ if ($tagInfo and $$tagInfo{WriteLast}) {
1212
+ $writeLast = ($writeLast || '') . $hdr . $buff;
1213
+ } else {
1214
+ # save position of this box in the output buffer
1215
+ $boxPos{$tag} = [ length($$outfile), length($hdr) + length($buff) ];
1216
+ # copy the existing atom
1217
+ Write($outfile, $hdr, $buff) or $rtnVal=$rtnErr, $err=1, last;
1218
+ }
1052
1219
  }
1220
+ $et->VPrint(0, " [deleting $delCount $dirName tag".($delCount==1 ? '' : 's')."]\n") if $delCount;
1221
+
1222
+ undef $createKeys unless $$addDirs{Keys}; # (Keys may have been written)
1223
+
1053
1224
  # add new directories/tags at this level if necessary
1054
- if (exists $$et{EDIT_DIRS}{$dirName} and $canCreate) {
1225
+ if ($canCreate and (exists $$et{EDIT_DIRS}{$dirName} or $createKeys)) {
1055
1226
  # get a hash of tagInfo references to add to this directory
1056
1227
  my $dirs = $et->GetAddDirHash($tagTablePtr, $dirName);
1057
1228
  # make sorted list of new tags to be added
1058
1229
  my @addTags = sort(keys(%$dirs), keys %$newTags);
1059
- my $tag;
1230
+ my ($tag, $index);
1231
+ # add Keys tags if necessary
1232
+ if ($createKeys) {
1233
+ if ($curPath eq 'MOV-Movie') {
1234
+ # add Meta for Keys if necessary
1235
+ unless ($didDir{meta}) {
1236
+ $$dirs{meta} = $Image::ExifTool::QuickTime::Movie{meta};
1237
+ push @addTags, 'meta';
1238
+ }
1239
+ } elsif ($curPath eq 'MOV-Movie-Meta') {
1240
+ # special case for Keys Meta -- reset directories and start again
1241
+ undef @addTags;
1242
+ $dirs = { };
1243
+ foreach ('keys','ilst') {
1244
+ next if $didDir{$_}; # don't add again
1245
+ $$dirs{$_} = $Image::ExifTool::QuickTime::Meta{$_};
1246
+ push @addTags, $_;
1247
+ }
1248
+ } elsif ($curPath eq 'MOV-Movie-Meta-ItemList' and $$et{Keys}) {
1249
+ foreach $index (sort { $a <=> $b } keys %{$$et{Keys}{Add}}) {
1250
+ my $id = Set32u($index);
1251
+ $$newTags{$id} = $$et{Keys}{Add}{$index};
1252
+ push @addTags, $id;
1253
+ }
1254
+ } else {
1255
+ $dirs = $et->GetAddDirHash($tagTablePtr, $dirName);
1256
+ push @addTags, sort keys %$dirs;
1257
+ }
1258
+ }
1259
+ # (note that $tag may be a binary Keys index here)
1060
1260
  foreach $tag (@addTags) {
1061
1261
  my $tagInfo = $$dirs{$tag} || $$newTags{$tag};
1062
- next if $$tagInfo{KeysInfo}; # don't try to add keys tags (yet)
1063
1262
  my $subdir = $$tagInfo{SubDirectory};
1064
1263
  unless ($subdir) {
1065
1264
  my $nvHash = $et->GetNewValueHash($tagInfo);
1066
- next unless $nvHash and not $didTag{$nvHash};
1265
+ next unless $nvHash and not $$didTag{$nvHash};
1067
1266
  next unless $$nvHash{IsCreating} and $et->IsOverwriting($nvHash);
1068
1267
  my $newVal = $et->GetNewValue($nvHash);
1069
1268
  next unless defined $newVal;
@@ -1071,16 +1270,18 @@ sub WriteQuickTime($$$)
1071
1270
  my $flags = FormatQTValue($et, \$newVal, $$tagInfo{Format});
1072
1271
  next unless defined $newVal;
1073
1272
  my ($ctry, $lang) = (0,0);
1074
- if (length $tag > 4) { # (is there a language code appended to the tag ID?)
1075
- unless ($tag =~ s/(.{4})-([A-Z]{3})?[-_]?([A-Z]{2})?/$1/si) {
1273
+ # handle alternate languages
1274
+ if ($$tagInfo{LangCode}) {
1275
+ $tag = substr($tag, 0, 4); # strip language code from tag ID
1276
+ unless ($$tagInfo{LangCode} =~ /^([A-Z]{3})?[-_]?([A-Z]{2})?$/i) {
1076
1277
  $et->Warn("Invalid language code for $$tagInfo{Name}");
1077
1278
  next;
1078
1279
  }
1079
1280
  # pack language and country codes
1080
- if ($2 and $2 ne 'und') {
1081
- $lang = ($lang << 5) | ($_ - 0x60) foreach unpack 'C*', lc($2);
1281
+ if ($1 and $1 ne 'und') {
1282
+ $lang = ($lang << 5) | ($_ - 0x60) foreach unpack 'C*', lc($1);
1082
1283
  }
1083
- $ctry = unpack('n', pack('a2',uc($3))) if $3 and $3 ne 'ZZ';
1284
+ $ctry = unpack('n', pack('a2',uc($2))) if $2 and $2 ne 'ZZ';
1084
1285
  }
1085
1286
  if ($$dirInfo{HasData}) {
1086
1287
  # add 'data' header
@@ -1102,16 +1303,37 @@ sub WriteQuickTime($$$)
1102
1303
  $et->Warn("Can't use language code for $grp:$$tagInfo{Name}");
1103
1304
  next;
1104
1305
  }
1105
- Write($outfile, Set32u(8+length($newVal)), $tag, $newVal) or $rtnVal=$rtnErr, $err=1;
1106
- $et->VerboseValue("+ $dirName:$$tagInfo{Name}", $prVal);
1107
- $didTag{$nvHash} = 1;
1306
+ if ($$tagInfo{WriteLast}) {
1307
+ $writeLast = ($writeLast || '') . Set32u(8+length($newVal)) . $tag . $newVal;
1308
+ } else {
1309
+ $boxPos{$tag} = [ length($$outfile), 8 + length($newVal) ];
1310
+ Write($outfile, Set32u(8+length($newVal)), $tag, $newVal) or $rtnVal=$rtnErr, $err=1;
1311
+ }
1312
+ my $grp = $et->GetGroup($tagInfo, 1);
1313
+ $et->VerboseValue("+ $grp:$$tagInfo{Name}", $prVal);
1314
+ $$didTag{$nvHash} = 1;
1108
1315
  ++$$et{CHANGED};
1109
1316
  next;
1110
1317
  }
1111
1318
  my $subName = $$subdir{DirName} || $$tagInfo{Name};
1112
1319
  # QuickTime hierarchy is complex, so check full directory path before adding
1113
- next unless IsCurPath($et, $subName);
1114
- my $buff = ''; # write from scratch
1320
+ my $buff;
1321
+ if ($createKeys and $curPath eq 'MOV-Movie' and $subName eq 'Meta') {
1322
+ $et->VPrint(0, " Creating Meta with mdta Handler and Keys\n");
1323
+ # init Meta box for Keys tags with mdta Handler and empty Keys+ItemList
1324
+ $buff = "\0\0\0\x20hdlr\0\0\0\0\0\0\0\0mdta\0\0\0\0\0\0\0\0\0\0\0\0" .
1325
+ "\0\0\0\x10keys\0\0\0\0\0\0\0\0" .
1326
+ "\0\0\0\x08ilst";
1327
+ } elsif ($createKeys and $curPath eq 'MOV-Movie-Meta') {
1328
+ $buff = ($subName eq 'Keys' ? "\0\0\0\0\0\0\0\0" : '');
1329
+ } elsif ($subName eq 'Meta' and $$et{OPTIONS}{QuickTimeHandler}) {
1330
+ $et->VPrint(0, " Creating Meta with mdir Handler\n");
1331
+ # init Meta box for ItemList tags with mdir Handler
1332
+ $buff = "\0\0\0\x20hdlr\0\0\0\0\0\0\0\0mdir\0\0\0\0\0\0\0\0\0\0\0\0";
1333
+ } else {
1334
+ next unless $curPath eq $writePath and $$addDirs{$subName} and $$addDirs{$subName} eq $dirName;
1335
+ $buff = ''; # write from scratch
1336
+ }
1115
1337
  my %subdirInfo = (
1116
1338
  Parent => $dirName,
1117
1339
  DirName => $subName,
@@ -1119,6 +1341,7 @@ sub WriteQuickTime($$$)
1119
1341
  DirStart => 0,
1120
1342
  HasData => $$subdir{HasData},
1121
1343
  OutFile => $outfile,
1344
+ ChunkOffset => [ ], # (just to be safe)
1122
1345
  );
1123
1346
  my $subTable = GetTagTable($$subdir{TagTable});
1124
1347
  my $newData = $et->WriteDirectory(\%subdirInfo, $subTable, $$subdir{WriteProc});
@@ -1136,13 +1359,25 @@ sub WriteQuickTime($$$)
1136
1359
  }
1137
1360
  }
1138
1361
  my $newHdr = Set32u(8+length($newData)+length($prefix)) . $tag . $prefix;
1139
- Write($outfile, $newHdr, $newData) or $rtnVal=$rtnErr, $err=1;
1362
+ if ($$tagInfo{WriteLast}) {
1363
+ $writeLast = ($writeLast || '') . $newHdr . $newData;
1364
+ } else {
1365
+ if ($tag eq 'uuid') {
1366
+ # add offset for new uuid (needed for CR3 CTBO offsets)
1367
+ my $off = $$dirInfo{ChunkOffset};
1368
+ push @$off, [ $tag, length($$outfile), length($newHdr) + length($newData) ];
1369
+ }
1370
+ $boxPos{$tag} = [ length($$outfile), length($newHdr) + length($newData) ];
1371
+ Write($outfile, $newHdr, $newData) or $rtnVal=$rtnErr, $err=1;
1372
+ }
1140
1373
  }
1141
- delete $$addDirs{$subName}; # add only once (must delete _after_ call to WriteDirectory())
1374
+ # add only once (must delete _after_ call to WriteDirectory())
1375
+ # (Keys is a special case, and will be removed after Meta is processed)
1376
+ delete $$addDirs{$subName} unless $subName eq 'Keys';
1142
1377
  }
1143
1378
  }
1144
1379
  # write HEIC metadata after top-level 'meta' box has been processed if editing this information
1145
- if ($dirName eq 'Meta' and $$et{EDIT_DIRS}{ItemInformation} and $curPath eq $writePath) {
1380
+ if ($curPath eq 'MOV-Meta' and $$et{EDIT_DIRS}{ItemInformation}) {
1146
1381
  $$dirInfo{BoxPos} = \%boxPos;
1147
1382
  my $mdatEdit = WriteItemInfo($et, $dirInfo, $outfile);
1148
1383
  if ($mdatEdit) {
@@ -1151,15 +1386,35 @@ sub WriteQuickTime($$$)
1151
1386
  }
1152
1387
  }
1153
1388
  # write out any necessary terminator
1154
- Write($outfile, $term) or $rtnVal=$rtnErr, $err=1 if $term;
1389
+ Write($outfile, $term) or $rtnVal=$rtnErr, $err=1 if $term and length $$outfile;
1390
+
1391
+ # delete temporary Keys variables after Meta is processed
1392
+ if ($dirName eq 'Meta') {
1393
+ # delete any Meta box with no useful information (ie. only 'hdlr','keys','lang','ctry')
1394
+ my $isEmpty = 1;
1395
+ $emptyMeta{$_} or $isEmpty = 0, last foreach keys %boxPos;
1396
+ if ($isEmpty) {
1397
+ $et->VPrint(0,' Deleting ' . join('+', sort map { $emptyMeta{$_} } keys %boxPos)) if %boxPos;
1398
+ $$outfile = '';
1399
+ ++$$et{CHANGED};
1400
+ }
1401
+ if ($curPath eq 'MOV-Movie-Meta') {
1402
+ delete $$addDirs{Keys}; # prevent creation of another Meta for Keys tags
1403
+ delete $$et{Keys};
1404
+ }
1405
+ }
1155
1406
 
1156
1407
  # return now if writing subdirectory
1157
- return $err ? undef : $$outfile if $dataPt;
1408
+ if ($dataPt) {
1409
+ $et->Error("Internal error: WriteLast not on top-level atom!\n") if $writeLast;
1410
+ return $err ? undef : $$outfile;
1411
+ }
1158
1412
 
1159
1413
  # issue minor error if we didn't find an 'mdat' atom
1160
- my $off = $$dirInfo{ChunkOffset} || [ ];
1414
+ my $off = $$dirInfo{ChunkOffset};
1161
1415
  if (not @mdat) {
1162
- if (@$off) {
1416
+ foreach $co (@$off) {
1417
+ next if $$co[0] eq 'uuid';
1163
1418
  $et->Error('Movie data referenced but not found');
1164
1419
  return $rtnVal;
1165
1420
  }
@@ -1266,11 +1521,52 @@ sub WriteQuickTime($$$)
1266
1521
  $pos += $$mdat[4] ? length(${$$mdat[4]}) : $$mdat[1] - $$mdat[0];
1267
1522
  }
1268
1523
 
1269
- # fix up offsets for new mdat position(s)
1524
+ # fix up offsets for new mdat position(s) (and uuid positions in CR3 images)
1270
1525
  foreach $co (@$off) {
1271
1526
  my ($type, $ptr, $len, $base, $id) = @$co;
1272
1527
  $base = 0 unless $base;
1273
- $type =~ /^(stco|co64)_?(.*)$/ or $et->Error('Internal error fixing offsets'), last;
1528
+ unless ($type =~ /^(stco|co64)_?(.*)$/) {
1529
+ next if $type eq 'uuid';
1530
+ $type eq 'CTBO' or $et->Error('Internal error fixing offsets'), last;
1531
+ # update 'CTBO' item offsets/sizes in Canon CR3 images
1532
+ $$co[2] > 12 or $et->Error('Invalid CTBO atom'), last;
1533
+ @mdat or $et->Error('Missing CR3 image data'), last;
1534
+ my $n = Get32u($outfile, $$co[1] + 8);
1535
+ $$co[2] < $n * 20 + 12 and $et->Error('Truncated CTBO atom'), last;
1536
+ my (%ctboOff, $i);
1537
+ # determine uuid types, and build an offset lookup based on CTBO ID number
1538
+ foreach (@$off) {
1539
+ next unless $$_[0] eq 'uuid' and $$_[2] >= 24; # (ignore undersized and deleted uuid boxes)
1540
+ my $pos = $$_[1];
1541
+ next if $pos + 24 > length $$outfile; # (will happen for WriteLast uuid tags)
1542
+ my $siz = Get32u($outfile, $pos); # get size of uuid atom
1543
+ if ($siz == 1) { # check for extended (8-byte) size
1544
+ next unless $$_[2] >= 32;
1545
+ $pos += 8;
1546
+ }
1547
+ # get CTBO entry ID based on 16-byte UUID identifier
1548
+ my $id = $ctboID{substr($$outfile, $pos+8, 16)};
1549
+ $ctboOff{$id} = $_ if defined $id;
1550
+ }
1551
+ # calculate new offset for the first mdat (size of -1 indicates it didn't change)
1552
+ $ctboOff{3} = [ 'mdat', $mdat[0][3] - length $mdat[0][2], -1 ];
1553
+ for ($i=0; $i<$n; ++$i) {
1554
+ my $pos = $$co[1] + 12 + $i * 20;
1555
+ my $id = Get32u($outfile, $pos);
1556
+ # ignore if size is zero unless we can add this entry
1557
+ # (note: can't yet add/delete PreviewImage, but leave this possibility open)
1558
+ next unless Get64u($outfile, $pos + 12) or $id == 1 or $id == 2;
1559
+ if (not defined $ctboOff{$id}) {
1560
+ $id==1 or $id==2 or $et->Error("Can't handle CR3 CTBO ID number $id"), last;
1561
+ # XMP or PreviewImage was deleted -- set offset and size to zero
1562
+ $ctboOff{$id} = [ 'uuid', 0, 0 ];
1563
+ }
1564
+ # update the new offset and size of this entry
1565
+ Set64u($ctboOff{$id}[1], $outfile, $pos + 4);
1566
+ Set64u($ctboOff{$id}[2], $outfile, $pos + 12) unless $ctboOff{$id}[2] < 0;
1567
+ }
1568
+ next;
1569
+ }
1274
1570
  my $siz = $1 eq 'co64' ? 8 : 4;
1275
1571
  my ($n, $tag);
1276
1572
  if ($2) { # is this an offset in an iloc or 'gps ' atom?
@@ -1354,6 +1650,9 @@ sub WriteQuickTime($$$)
1354
1650
  }
1355
1651
  }
1356
1652
 
1653
+ # write the stuff that must come last
1654
+ Write($outfile, $writeLast) or $rtnVal = 0 if $writeLast;
1655
+
1357
1656
  return $rtnVal;
1358
1657
  }
1359
1658
 
@@ -1397,12 +1696,15 @@ sub WriteMOV($$)
1397
1696
  $et->SetFileType($ftype); # need to set "FileType" tag for a Condition
1398
1697
  $et->InitWriteDirs($dirMap{$ftype}, 'XMP');
1399
1698
  $$et{DirMap} = $dirMap{$ftype}; # need access to directory map when writing
1699
+ # track tags globally to avoid creating multiple tags in the case of duplicate directories
1700
+ $$et{DidTag} = { };
1400
1701
  SetByteOrder('MM');
1401
1702
  $raf->Seek(0,0);
1402
1703
 
1403
1704
  # write the file
1404
1705
  $$dirInfo{Parent} = '';
1405
1706
  $$dirInfo{DirName} = 'MOV';
1707
+ $$dirInfo{ChunkOffset} = [ ]; # (just to be safe)
1406
1708
  return WriteQuickTime($et, $dirInfo, $tagTablePtr) ? 1 : -1;
1407
1709
  }
1408
1710