Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved support of Nikon-specific atoms in QuickTime videos #324

Merged
merged 1 commit into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 38 additions & 193 deletions getid3/module.audio-video.quicktime.php
Original file line number Diff line number Diff line change
Expand Up @@ -1623,33 +1623,61 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset
break;

case 'NCDT':
// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html
// https://exiftool.org/TagNames/Nikon.html
// Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100
$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 4, $atomHierarchy, $ParseAllPossibleAtoms);
break;
case 'NCTH': // Nikon Camera THumbnail image
case 'NCVW': // Nikon Camera preVieW image
// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html
case 'NCM1': // Nikon Camera preview iMage 1
case 'NCM2': // Nikon Camera preview iMage 2
// https://exiftool.org/TagNames/Nikon.html
if (preg_match('/^\xFF\xD8\xFF/', $atom_data)) {
$descriptions = array(
'NCTH' => 'Nikon Camera Thumbnail Image',
'NCVW' => 'Nikon Camera Preview Image',
'NCM1' => 'Nikon Camera Preview Image 1',
'NCM2' => 'Nikon Camera Preview Image 2',
);
$atom_structure['data'] = $atom_data;
$atom_structure['image_mime'] = 'image/jpeg';
$atom_structure['description'] = (($atomname == 'NCTH') ? 'Nikon Camera Thumbnail Image' : (($atomname == 'NCVW') ? 'Nikon Camera Preview Image' : 'Nikon preview image'));
$info['quicktime']['comments']['picture'][] = array('image_mime'=>$atom_structure['image_mime'], 'data'=>$atom_data, 'description'=>$atom_structure['description']);
$atom_structure['description'] = isset($descriptions[$atomname]) ? $descriptions[$atomname] : 'Nikon preview image';
$info['quicktime']['comments']['picture'][] = array(
'image_mime' => $atom_structure['image_mime'],
'data' => $atom_data,
'description' => $atom_structure['description']
);
}
break;
case 'NCTG': // Nikon - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#NCTG
$atom_structure['data'] = $this->QuicktimeParseNikonNCTG($atom_data);
case 'NCTG': // Nikon - https://exiftool.org/TagNames/Nikon.html#NCTG
getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.nikon-nctg.php', __FILE__, true);
$nikonNCTG = new getid3_tag_nikon_nctg($this->getid3);

$atom_structure['data'] = $nikonNCTG->parse($atom_data);
break;
case 'NCHD': // Nikon:MakerNoteVersion - https://exiftool.org/TagNames/Nikon.html
$makerNoteVersion = '';
for ($i = 0, $iMax = strlen($atom_data); $i < $iMax; ++$i) {
if (ord($atom_data[$i]) >= 0x00 && ord($atom_data[$i]) <= 0x1F) {
$makerNoteVersion .= ' '.ord($atom_data[$i]);
} else {
$makerNoteVersion .= $atom_data[$i];
}
}
$makerNoteVersion = rtrim($makerNoteVersion, "\x00");
$atom_structure['data'] = array(
'MakerNoteVersion' => $makerNoteVersion
);
break;
case 'NCHD': // Nikon:MakerNoteVersion - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html
case 'NCDB': // Nikon - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html
case 'CNCV': // Canon:CompressorVersion - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html
case 'NCDB': // Nikon - https://exiftool.org/TagNames/Nikon.html
case 'CNCV': // Canon:CompressorVersion - https://exiftool.org/TagNames/Canon.html
$atom_structure['data'] = $atom_data;
break;

case "\x00\x00\x00\x00":
// some kind of metacontainer, may contain a big data dump such as:
// mdta keys \005 mdtacom.apple.quicktime.make (mdtacom.apple.quicktime.creationdate ,mdtacom.apple.quicktime.location.ISO6709 $mdtacom.apple.quicktime.software !mdtacom.apple.quicktime.model ilst \01D \001 \015data \001DE\010Apple 0 \002 (data \001DE\0102011-05-11T17:54:04+0200 2 \003 *data \001DE\010+52.4936+013.3897+040.247/ \01D \004 \015data \001DE\0104.3.1 \005 \018data \001DE\010iPhone 4
// http://www.geocities.com/xhelmboyx/quicktime/formats/qti-layout.txt
// https://xhelmboyx.tripod.com/formats/qti-layout.txt

$atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1));
$atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3));
Expand Down Expand Up @@ -2632,189 +2660,6 @@ public function QuicktimeStoreFrontCodeLookup($sfid) {
return (isset($QuicktimeStoreFrontCodeLookup[$sfid]) ? $QuicktimeStoreFrontCodeLookup[$sfid] : 'invalid');
}

/**
* @param string $atom_data
*
* @return array
*/
public function QuicktimeParseNikonNCTG($atom_data) {
// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#NCTG
// Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100
// Data is stored as records of:
// * 4 bytes record type
// * 2 bytes size of data field type:
// 0x0001 = flag (size field *= 1-byte)
// 0x0002 = char (size field *= 1-byte)
// 0x0003 = DWORD+ (size field *= 2-byte), values are stored CDAB
// 0x0004 = QWORD+ (size field *= 4-byte), values are stored EFGHABCD
// 0x0005 = float (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together
// 0x0007 = bytes (size field *= 1-byte), values are stored as ??????
// 0x0008 = ????? (size field *= 2-byte), values are stored as ??????
// * 2 bytes data size field
// * ? bytes data (string data may be null-padded; datestamp fields are in the format "2011:05:25 20:24:15")
// all integers are stored BigEndian

$NCTGtagName = array(
0x00000001 => 'Make',
0x00000002 => 'Model',
0x00000003 => 'Software',
0x00000011 => 'CreateDate',
0x00000012 => 'DateTimeOriginal',
0x00000013 => 'FrameCount',
0x00000016 => 'FrameRate',
0x00000022 => 'FrameWidth',
0x00000023 => 'FrameHeight',
0x00000032 => 'AudioChannels',
0x00000033 => 'AudioBitsPerSample',
0x00000034 => 'AudioSampleRate',
0x02000001 => 'MakerNoteVersion',
0x02000005 => 'WhiteBalance',
0x0200000b => 'WhiteBalanceFineTune',
0x0200001e => 'ColorSpace',
0x02000023 => 'PictureControlData',
0x02000024 => 'WorldTime',
0x02000032 => 'UnknownInfo',
0x02000083 => 'LensType',
0x02000084 => 'Lens',
);

$offset = 0;
$data = null;
$datalength = strlen($atom_data);
$parsed = array();
while ($offset < $datalength) {
$record_type = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 4)); $offset += 4;
$data_size_type = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); $offset += 2;
$data_size = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); $offset += 2;
switch ($data_size_type) {
case 0x0001: // 0x0001 = flag (size field *= 1-byte)
$data = getid3_lib::BigEndian2Int(substr($atom_data, $offset, $data_size * 1));
$offset += ($data_size * 1);
break;
case 0x0002: // 0x0002 = char (size field *= 1-byte)
$data = substr($atom_data, $offset, $data_size * 1);
$offset += ($data_size * 1);
$data = rtrim($data, "\x00");
break;
case 0x0003: // 0x0003 = DWORD+ (size field *= 2-byte), values are stored CDAB
$data = '';
for ($i = $data_size - 1; $i >= 0; $i--) {
$data .= substr($atom_data, $offset + ($i * 2), 2);
}
$data = getid3_lib::BigEndian2Int($data);
$offset += ($data_size * 2);
break;
case 0x0004: // 0x0004 = QWORD+ (size field *= 4-byte), values are stored EFGHABCD
$data = '';
for ($i = $data_size - 1; $i >= 0; $i--) {
$data .= substr($atom_data, $offset + ($i * 4), 4);
}
$data = getid3_lib::BigEndian2Int($data);
$offset += ($data_size * 4);
break;
case 0x0005: // 0x0005 = float (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together
$data = array();
for ($i = 0; $i < $data_size; $i++) {
$numerator = getid3_lib::BigEndian2Int(substr($atom_data, $offset + ($i * 8) + 0, 4));
$denomninator = getid3_lib::BigEndian2Int(substr($atom_data, $offset + ($i * 8) + 4, 4));
if ($denomninator == 0) {
$data[$i] = false;
} else {
$data[$i] = (double) $numerator / $denomninator;
}
}
$offset += (8 * $data_size);
if (count($data) == 1) {
$data = $data[0];
}
break;
case 0x0007: // 0x0007 = bytes (size field *= 1-byte), values are stored as ??????
$data = substr($atom_data, $offset, $data_size * 1);
$offset += ($data_size * 1);
break;
case 0x0008: // 0x0008 = ????? (size field *= 2-byte), values are stored as ??????
$data = substr($atom_data, $offset, $data_size * 2);
$offset += ($data_size * 2);
break;
default:
echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'<br>';
break 2;
}

switch ($record_type) {
case 0x00000011: // CreateDate
case 0x00000012: // DateTimeOriginal
$data = strtotime($data);
break;
case 0x0200001e: // ColorSpace
switch ($data) {
case 1:
$data = 'sRGB';
break;
case 2:
$data = 'Adobe RGB';
break;
}
break;
case 0x02000023: // PictureControlData
$PictureControlAdjust = array(0=>'default', 1=>'quick', 2=>'full');
$FilterEffect = array(0x80=>'off', 0x81=>'yellow', 0x82=>'orange', 0x83=>'red', 0x84=>'green', 0xff=>'n/a');
$ToningEffect = array(0x80=>'b&w', 0x81=>'sepia', 0x82=>'cyanotype', 0x83=>'red', 0x84=>'yellow', 0x85=>'green', 0x86=>'blue-green', 0x87=>'blue', 0x88=>'purple-blue', 0x89=>'red-purple', 0xff=>'n/a');
$data = array(
'PictureControlVersion' => substr($data, 0, 4),
'PictureControlName' => rtrim(substr($data, 4, 20), "\x00"),
'PictureControlBase' => rtrim(substr($data, 24, 20), "\x00"),
//'?' => substr($data, 44, 4),
'PictureControlAdjust' => $PictureControlAdjust[ord(substr($data, 48, 1))],
'PictureControlQuickAdjust' => ord(substr($data, 49, 1)),
'Sharpness' => ord(substr($data, 50, 1)),
'Contrast' => ord(substr($data, 51, 1)),
'Brightness' => ord(substr($data, 52, 1)),
'Saturation' => ord(substr($data, 53, 1)),
'HueAdjustment' => ord(substr($data, 54, 1)),
'FilterEffect' => $FilterEffect[ord(substr($data, 55, 1))],
'ToningEffect' => $ToningEffect[ord(substr($data, 56, 1))],
'ToningSaturation' => ord(substr($data, 57, 1)),
);
break;
case 0x02000024: // WorldTime
// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#WorldTime
// timezone is stored as offset from GMT in minutes
$timezone = getid3_lib::BigEndian2Int(substr($data, 0, 2));
if ($timezone & 0x8000) {
$timezone = 0 - (0x10000 - $timezone);
}
$timezone /= 60;

$dst = (bool) getid3_lib::BigEndian2Int(substr($data, 2, 1));
switch (getid3_lib::BigEndian2Int(substr($data, 3, 1))) {
case 2:
$datedisplayformat = 'D/M/Y'; break;
case 1:
$datedisplayformat = 'M/D/Y'; break;
case 0:
default:
$datedisplayformat = 'Y/M/D'; break;
}

$data = array('timezone'=>floatval($timezone), 'dst'=>$dst, 'display'=>$datedisplayformat);
break;
case 0x02000083: // LensType
$data = array(
//'_' => $data,
'mf' => (bool) ($data & 0x01),
'd' => (bool) ($data & 0x02),
'g' => (bool) ($data & 0x04),
'vr' => (bool) ($data & 0x08),
);
break;
}
$tag_name = (isset($NCTGtagName[$record_type]) ? $NCTGtagName[$record_type] : '0x'.str_pad(dechex($record_type), 8, '0', STR_PAD_LEFT));
$parsed[$tag_name] = $data;
}
return $parsed;
}

/**
* @param string $keyname
* @param string|array $data
Expand Down
Loading