#------------------------------------------------------------------------------ # File: QuickTime.pm # # Description: Read QuickTime, MP4 and M4A meta information # # Revisions: 10/04/2005 - P. Harvey Created # 12/19/2005 - P. Harvey Added MP4 support # 09/22/2006 - P. Harvey Added M4A support # # References: 1) http://developer.apple.com/documentation/QuickTime/ # 2) http://search.cpan.org/dist/MP4-Info-1.04/ # 3) http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt # 4) http://wiki.multimedia.cx/index.php?title=Apple_QuickTime #------------------------------------------------------------------------------ package Image::ExifTool::QuickTime; use strict; use vars qw($VERSION); use Image::ExifTool qw(:DataAccess :Utils); use Image::ExifTool::Exif; $VERSION = '1.12'; sub FixWrongFormat($); sub ProcessMOV($$;$); # information for time/date-based tags (time zero is Jan 1, 1904) my %timeInfo = ( Groups => { 2 => 'Time' }, # Note: This value will be in UTC if generated by a system that is aware of the time zone ValueConv => 'ConvertUnixTime($val - ((66 * 365 + 17) * 24 * 3600))', PrintConv => '$self->ConvertDateTime($val)', ); # information for duration tags my %durationInfo = ( ValueConv => '$self->{TimeScale} ? $val / $self->{TimeScale} : $val', PrintConv => '$self->{TimeScale} ? sprintf("%.2fs", $val) : $val', ); # QuickTime atoms %Image::ExifTool::QuickTime::Main = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => q{ These tags are used in QuickTime MOV and MP4 videos, and QTIF images. Tags with a question mark after their name are not extracted unless the Unknown option is set. }, free => { Unknown => 1, Binary => 1 }, skip => { Unknown => 1, Binary => 1 }, wide => { Unknown => 1, Binary => 1 }, ftyp => { #MP4 Name => 'FrameType', Unknown => 1, Notes => 'MP4 only', Binary => 1, }, pnot => { Name => 'Preview', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Preview' }, }, PICT => { Name => 'PreviewPICT', Binary => 1, }, moov => { Name => 'Movie', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Movie' }, }, mdat => { Unknown => 1, Binary => 1 }, ); # atoms used in QTIF files %Image::ExifTool::QuickTime::ImageFile = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Image' }, NOTES => 'Tags used in QTIF QuickTime Image Files.', idsc => { Name => 'ImageDescription', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::ImageDesc' }, }, idat => { Name => 'ImageData', Binary => 1, }, iicc => { Name => 'ICC_Profile', SubDirectory => { TagTable => 'Image::ExifTool::ICC_Profile::Main' }, }, ); # image description data block %Image::ExifTool::QuickTime::ImageDesc = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Image' }, 4 => { Name => 'CompressorID', Format => 'string[4]' }, 20 => { Name => 'VendorID', Format => 'string[4]' }, 28 => { Name => 'Quality', Format => 'int32u' }, 32 => { Name => 'ImageWidth', Format => 'int16u' }, 34 => { Name => 'ImageHeight', Format => 'int16u' }, 36 => { Name => 'XResolution', Format => 'int32u' }, 40 => { Name => 'YResolution', Format => 'int32u' }, 48 => { Name => 'FrameCount', Format => 'int16u' }, 50 => { Name => 'NameLength', Format => 'int8u' }, 51 => { Name => 'Compressor', Format => 'string[$val{46}]' }, 82 => { Name => 'BitDepth', Format => 'int16u' }, ); # preview data block %Image::ExifTool::QuickTime::Preview = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Image' }, FORMAT => 'int16u', 0 => { Name => 'PreviewDate', Format => 'int32u', %timeInfo, }, 2 => 'PreviewVersion', 3 => { Name => 'PreviewAtomType', Format => 'string[4]', }, 5 => 'PreviewAtomIndex', ); # movie atoms %Image::ExifTool::QuickTime::Movie = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, mvhd => { Name => 'MovieHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::MovieHdr' }, }, trak => { Name => 'Track', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Track' }, }, udta => { Name => 'UserData', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::UserData' }, }, ); # movie header data block %Image::ExifTool::QuickTime::MovieHdr = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, FORMAT => 'int32u', 0 => { Name => 'Version', Format => 'int8u' }, 1 => { Name => 'CreateDate', %timeInfo, }, 2 => { Name => 'ModifyDate', %timeInfo, }, 3 => { Name => 'TimeScale', RawConv => '$self->{TimeScale} = $val', }, 4 => { Name => 'Duration', %durationInfo }, 5 => { Name => 'PreferredRate', ValueConv => '$val / 0x10000', }, 6 => { Name => 'PreferredVolume', Format => 'int16u', ValueConv => '$val / 256', PrintConv => 'sprintf("%.2f%%", $val * 100)', }, 18 => { Name => 'PreviewTime', %durationInfo }, 19 => { Name => 'PreviewDuration', %durationInfo }, 20 => { Name => 'PosterTime', %durationInfo }, 21 => { Name => 'SelectionTime', %durationInfo }, 22 => { Name => 'SelectionDuration',%durationInfo }, 23 => { Name => 'CurrentTime', %durationInfo }, 24 => 'NextTrackID', ); # track atoms %Image::ExifTool::QuickTime::Track = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, tkhd => { Name => 'TrackHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::TrackHdr' }, }, udta => { Name => 'UserData', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::UserData' }, }, mdia => { #MP4 Name => 'Media', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Media' }, }, ); # track header data block %Image::ExifTool::QuickTime::TrackHdr = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 1 => 'Track#', 2 => 'Video' }, FORMAT => 'int32u', 0 => { Name => 'TrackVersion', Format => 'int8u', Priority => 0, }, 1 => { Name => 'TrackCreateDate', Priority => 0, %timeInfo, }, 2 => { Name => 'TrackModifyDate', Priority => 0, %timeInfo, }, 3 => { Name => 'TrackID', Priority => 0, }, 5 => { Name => 'TrackDuration', Priority => 0, %durationInfo, }, 8 => { Name => 'TrackLayer', Format => 'int16u', Priority => 0, }, 9 => { Name => 'TrackVolume', Format => 'int16u', Priority => 0, ValueConv => '$val / 256', PrintConv => 'sprintf("%.2f%%", $val * 100)', }, 19 => { Name => 'ImageWidth', Priority => 0, RawConv => \&FixWrongFormat, }, 20 => { Name => 'ImageHeight', Priority => 0, RawConv => \&FixWrongFormat, }, ); # user data atoms %Image::ExifTool::QuickTime::UserData = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => q{ Tag ID's beginning with the copyright symbol (hex 0xa9) are multi-language text, but ExifTool only extracts the text from the first language in the record. ExifTool will extract any multi-language user data tags found, even if they don't exist in this table. }, "\xa9cpy" => 'Copyright', "\xa9day" => 'CreateDate', "\xa9dir" => 'Director', "\xa9ed1" => 'Edit1', "\xa9ed2" => 'Edit2', "\xa9ed3" => 'Edit3', "\xa9ed4" => 'Edit4', "\xa9ed5" => 'Edit5', "\xa9ed6" => 'Edit6', "\xa9ed7" => 'Edit7', "\xa9ed8" => 'Edit8', "\xa9ed9" => 'Edit9', "\xa9fmt" => 'Format', "\xa9inf" => 'Information', "\xa9prd" => 'Producer', "\xa9prf" => 'Performers', "\xa9req" => 'Requirements', "\xa9src" => 'Source', "\xa9wrt" => 'Writer', name => 'Name', WLOC => { Name => 'WindowLocation', Format => 'int16u', }, LOOP => { Name => 'LoopStyle', Format => 'int32u', PrintConv => { 1 => 'Normal', 2 => 'Palindromic', }, }, SelO => { Name => 'PlaySelection', Format => 'int8u', }, AllF => { Name => 'PlayAllFrames', Format => 'int8u', }, meta => { Name => 'Meta', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Meta', HasVersion => 1, # must skip 4-byte version number header }, }, 'ptv '=> { Name => 'PrintToVideo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Video' }, }, # hnti => 'HintInfo', # hinf => 'HintTrackInfo', TAGS => [ { # these tags were initially discovered in a Pentax movie, but # seem very similar to those used by Nikon Name => 'PentaxTags', Condition => '$$valPt =~ /^PENTAX DIGITAL CAMERA\0/', SubDirectory => { TagTable => 'Image::ExifTool::Pentax::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'NikonTags', Condition => '$$valPt =~ /^NIKON DIGITAL CAMERA\0/', SubDirectory => { TagTable => 'Image::ExifTool::Nikon::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'SanyoMOV', Condition => q{ $$valPt =~ /^SANYO DIGITAL CAMERA\0/ and $self->{VALUE}->{FileType} eq "MOV" }, SubDirectory => { TagTable => 'Image::ExifTool::Sanyo::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'SanyoMP4', Condition => q{ $$valPt =~ /^SANYO DIGITAL CAMERA\0/ and $self->{VALUE}->{FileType} eq "MP4" }, SubDirectory => { TagTable => 'Image::ExifTool::Sanyo::MP4', ByteOrder => 'LittleEndian', }, }, { Name => 'UnknownTags', Unknown => 1, Binary => 1 }, ], ); # meta atoms %Image::ExifTool::QuickTime::Meta = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, ilst => { Name => 'InfoList', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::InfoList', HasData => 1, # process atoms as containers with 'data' elements }, }, ); # info list atoms # -> these atoms are unique, and contain one or more 'data' atoms %Image::ExifTool::QuickTime::InfoList = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Audio' }, "\xa9ART" => 'Artist', "\xa9alb" => 'Album', "\xa9cmt" => 'Comment', "\xa9com" => 'Composer', "\xa9day" => 'Year', "\xa9des" => 'Description', #4 "\xa9gen" => 'Genre', "\xa9grp" => 'Grouping', "\xa9lyr" => 'Lyrics', "\xa9nam" => 'Title', "\xa9too" => 'Encoder', "\xa9trk" => 'Track', "\xa9wrt" => 'Composer', '----' => { Name => 'iTunesInfo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::iTunesInfo' }, }, aART => 'AlbumArtist', apid => 'AppleStoreID', auth => 'Author', covr => 'CoverArt', cpil => { Name => 'Compilation', PrintConv => { 0 => 'No', 1 => 'Yes' }, }, cprt => 'Copyright', disk => { Name => 'DiskNumber', ValueConv => 'length($val) >= 6 ? join(" of ",unpack("x2nn",$val)) : \$val', }, dscp => 'Description', gnre => 'Genre', perf => 'Performer', pgap => { Name => 'PlayGap', PrintConv => { 0 => 'Insert Gap', 1 => 'No Gap', }, }, rtng => 'Rating', # int titl => 'Title', tmpo => 'BeatsPerMinute', # int trkn => { Name => 'TrackNumber', ValueConv => 'length($val) >= 6 ? join(" of ",unpack("x2nn",$val)) : \$val', }, ); # info list atoms %Image::ExifTool::QuickTime::iTunesInfo = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Audio' }, ); # print to video data block %Image::ExifTool::QuickTime::Video = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, 0 => { Name => 'DisplaySize', PrintConv => { 0 => 'Normal', 1 => 'Double Size', 2 => 'Half Size', 3 => 'Full Screen', 4 => 'Current Size', }, }, 6 => { Name => 'SlideShow', PrintConv => { 0 => 'No', 1 => 'Yes', }, }, ); # MP4 media %Image::ExifTool::QuickTime::Media = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 only (most tags unknown because ISO charges for the specification).', minf => { Name => 'Minf', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Minf' }, }, ); %Image::ExifTool::QuickTime::Minf = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 only (most tags unknown because ISO charges for the specification).', dinf => { Name => 'Dinf', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Dinf' }, }, stbl => { Name => 'Stbl', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Stbl' }, }, ); %Image::ExifTool::QuickTime::Stbl = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 only (most tags unknown because ISO charges for the specification).', ); %Image::ExifTool::QuickTime::Dinf = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 only (most tags unknown because ISO charges for the specification).', ); #------------------------------------------------------------------------------ # Fix incorrect format for ImageWidth/Height as written by Pentax sub FixWrongFormat($) { my $val = shift; return undef unless $val; if ($val & 0xffff0000) { $val = unpack('n',pack('N',$val)); } return $val; } #------------------------------------------------------------------------------ # Process a QuickTime atom # Inputs: 0) ExifTool object reference, 1) directory information reference # 2) optional tag table reference # Returns: 1 on success sub ProcessMOV($$;$) { my ($exifTool, $dirInfo, $tagTablePtr) = @_; my $raf = $$dirInfo{RAF}; my $dataPt = $$dirInfo{DataPt}; my $verbose = $exifTool->Options('Verbose'); my $dataPos = $$dirInfo{Base} || 0; my ($buff, $tag, $size, $track); # more convenient to package data as a RandomAccess file $raf or $raf = new File::RandomAccess($dataPt); # skip leading 4-byte version number if necessary ($raf->Read($buff,4) == 4 and $dataPos += 4) or return 0 if $$dirInfo{HasVersion}; # read size/tag name atom header $raf->Read($buff,8) == 8 or return 0; $dataPos += 8; $tagTablePtr or $tagTablePtr = GetTagTable('Image::ExifTool::QuickTime::Main'); ($size, $tag) = unpack('Na4', $buff); if ($dataPt) { $verbose and $exifTool->VerboseDir($$dirInfo{DirName}); } else { # check on file type if called with a RAF $$tagTablePtr{$tag} or return 0; if ($tag eq 'ftyp') { # read ahead 4 bytes to see if this is an M4A file my $ftyp = 'MP4'; if ($raf->Read($buff, 4) == 4) { $raf->Seek(-4, 1); $ftyp = 'M4A' if $buff eq 'M4A '; } $exifTool->SetFileType($ftyp); # MP4 or M4A } else { $exifTool->SetFileType(); # MOV } SetByteOrder('MM'); } for (;;) { if ($size < 8) { last if $size == 0; $size == 1 or $exifTool->Warn('Invalid atom size'), last; $raf->Read($buff, 8) == 8 or last; $dataPos += 8; my ($hi, $lo) = unpack('NN', $buff); if ($hi or $lo > 0x7fffffff) { $exifTool->Warn('End of processing at large atom'); last; } $size = $lo; } $size -= 8; my $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $tag); # generate tagInfo if Unknown option set if (not defined $tagInfo and ($exifTool->{OPTIONS}->{Unknown} or $tag =~ /^\xa9/)) { my $name = $tag; $name =~ s/([\x00-\x1f\x7f-\xff])/'x'.unpack('H*',$1)/eg; if ($name =~ /^xa9(.*)/) { $tagInfo = { Name => "UserData_$1", Description => "User Data $1", }; } else { $tagInfo = { Name => "Unknown_$name", Description => "Unknown $name", Unknown => 1, Binary => 1, }; } Image::ExifTool::AddTagToTable($tagTablePtr, $tag, $tagInfo); } # load values only if associated with a tag (or verbose) and < 16MB long if ((defined $tagInfo or $verbose) and $size < 0x1000000) { my $val; unless ($raf->Read($val, $size) == $size) { $exifTool->Warn("Truncated '$tag' data"); last; } # use value to get tag info if necessary $tagInfo or $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $tag, \$val); my $hasData = ($$dirInfo{HasData} and $val =~ /^\0...data\0/s); if ($verbose and not $hasData) { $exifTool->VerboseInfo($tag, $tagInfo, Value => $val, DataPt => \$val, DataPos => $dataPos, ); } if ($tagInfo) { my $subdir = $$tagInfo{SubDirectory}; if ($subdir) { my %dirInfo = ( DataPt => \$val, DirStart => 0, DirLen => $size, DirName => $$tagInfo{Name}, HasData => $$subdir{HasData}, HasVersion => $$subdir{HasVersion}, # Base needed for IsOffset tags in binary data Base => $dataPos, ); if ($$subdir{ByteOrder} and $$subdir{ByteOrder} =~ /^Little/) { SetByteOrder('II'); } if ($$tagInfo{Name} eq 'Track') { $track or $track = 0; $exifTool->{SET_GROUP1} = 'Track' . (++$track); } my $subTable = GetTagTable($$subdir{TagTable}); $exifTool->ProcessDirectory(\%dirInfo, $subTable); delete $exifTool->{SET_GROUP1}; SetByteOrder('MM'); } elsif ($hasData) { # handle atoms containing 'data' tags my $pos = 0; for (;;) { last if $pos + 16 > $size; my ($len, $type, $flags) = unpack("x${pos}Na4N", $val); last if $pos + $len > $size; my $value; if ($type eq 'data' and $len >= 16) { $pos += 16; $len -= 16; $value = substr($val, $pos, $len); # format flags: 0x0=binary, 0x1=text, 0xd=image, 0x15=boolean if ($flags == 0x0015) { $value = $len ? ReadValue(\$value, $len-1, 'int8u', 1, 1) : ''; } elsif ($flags != 0x01 and not $$tagInfo{ValueConv}) { # make binary data a scalar reference unless a ValueConv exists my $buf = $value; $value = \$buf; } } $exifTool->VerboseInfo($tag, $tagInfo, Value => ref $value ? $$value : $value, DataPt => \$val, DataPos => $dataPos, Start => $pos, Size => $len, Extra => sprintf(", Type='$type', Flags=0x%x",$flags) ) if $verbose; $exifTool->FoundTag($tagInfo, $value) if defined $value; $pos += $len; } } else { if ($tag =~ /^\xa9/) { # parse international text to extract first string my $len = unpack('n', $val); # $len should include 4 bytes for length and type words, # but Pentax forgets to add these in, so allow for this $len += 4 if $len == $size - 4; $val = substr($val, 4, $len - 4) if $len <= $size; } elsif ($$tagInfo{Format}) { $val = ReadValue(\$val, 0, $$tagInfo{Format}, $$tagInfo{Count}, length($val)); } $exifTool->FoundTag($tagInfo, $val); } } } else { $raf->Seek($size, 1) or $exifTool->Warn("Truncated '$tag' data"), last; } $raf->Read($buff, 8) == 8 or last; $dataPos += $size + 8; ($size, $tag) = unpack('Na4', $buff); } return 1; } #------------------------------------------------------------------------------ # Process a QuickTime Image File # Inputs: 0) ExifTool object reference, 1) directory information reference # Returns: 1 on success sub ProcessQTIF($$) { my $table = GetTagTable('Image::ExifTool::QuickTime::ImageFile'); return ProcessMOV($_[0], $_[1], $table); } 1; # end __END__ =head1 NAME Image::ExifTool::QuickTime - Read QuickTime and MP4 meta information =head1 SYNOPSIS This module is used by Image::ExifTool =head1 DESCRIPTION This module contains routines required by Image::ExifTool to extract information from QuickTime and MP4 video, and M4A audio files. =head1 BUGS The MP4 support is rather pathetic since the specification documentation is not freely available (yes, ISO sucks). =head1 AUTHOR Copyright 2003-2007, Phil Harvey (phil at owl.phy.queensu.ca) This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 REFERENCES =over 4 =item L =back =head1 SEE ALSO L, L =cut