From 8cb0970f8fb239784cb86623f4a4dceb5945366c Mon Sep 17 00:00:00 2001 From: StudioMaX Date: Wed, 26 May 2021 14:33:34 +0600 Subject: [PATCH] Improved support of Nikon-specific atoms in QuickTime videos --- getid3/module.audio-video.quicktime.php | 231 +--- getid3/module.tag.nikon-nctg.php | 1448 +++++++++++++++++++++++ 2 files changed, 1486 insertions(+), 193 deletions(-) create mode 100644 getid3/module.tag.nikon-nctg.php diff --git a/getid3/module.audio-video.quicktime.php b/getid3/module.audio-video.quicktime.php index 421ba84a..7069c126 100644 --- a/getid3/module.audio-video.quicktime.php +++ b/getid3/module.audio-video.quicktime.php @@ -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)); @@ -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.'
'; - 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 diff --git a/getid3/module.tag.nikon-nctg.php b/getid3/module.tag.nikon-nctg.php new file mode 100644 index 00000000..46e2f21d --- /dev/null +++ b/getid3/module.tag.nikon-nctg.php @@ -0,0 +1,1448 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.tag.nikon-nctg.php // +// // +///////////////////////////////////////////////////////////////// + +/** + * Module for analyzing Nikon NCTG metadata in MOV files + * + * @author Pavel Starosek + * @author Phil Harvey + * + * @link https://exiftool.org/TagNames/Nikon.html#NCTG + * @link https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/Nikon.pm + * @link https://leo-van-stee.github.io/ + */ +class getid3_tag_nikon_nctg +{ + const EXIF_TYPE_UINT8 = 0x0001; + const EXIF_TYPE_CHAR = 0x0002; + const EXIF_TYPE_UINT16 = 0x0003; + const EXIF_TYPE_UINT32 = 0x0004; + const EXIF_TYPE_URATIONAL = 0x0005; + const EXIF_TYPE_INT8 = 0x0006; + const EXIF_TYPE_RAW = 0x0007; + const EXIF_TYPE_INT16 = 0x0008; + const EXIF_TYPE_INT32 = 0x0009; + const EXIF_TYPE_RATIONAL = 0x000A; + + protected static $exifTypeSizes = array( + self::EXIF_TYPE_UINT8 => 1, + self::EXIF_TYPE_CHAR => 1, + self::EXIF_TYPE_UINT16 => 2, + self::EXIF_TYPE_UINT32 => 4, + self::EXIF_TYPE_URATIONAL => 8, + self::EXIF_TYPE_INT8 => 1, + self::EXIF_TYPE_RAW => 1, + self::EXIF_TYPE_INT16 => 2, + self::EXIF_TYPE_INT32 => 4, + self::EXIF_TYPE_RATIONAL => 8, + ); + + protected static $exposurePrograms = array( + 0 => 'Not Defined', + 1 => 'Manual', + 2 => 'Program AE', + 3 => 'Aperture-priority AE', + 4 => 'Shutter speed priority AE', + 5 => 'Creative (Slow speed)', + 6 => 'Action (High speed)', + 7 => 'Portrait', + 8 => 'Landscape' + ); + + protected static $meteringModes = array( + 0 => 'Unknown', + 1 => 'Average', + 2 => 'Center-weighted average', + 3 => 'Spot', + 4 => 'Multi-spot', + 5 => 'Multi-segment', + 6 => 'Partial', + 255 => 'Other' + ); + + protected static $cropHiSpeeds = array( + 0 => 'Off', + 1 => '1.3x Crop', + 2 => 'DX Crop', + 3 => '5:4 Crop', + 4 => '3:2 Crop', + 6 => '16:9 Crop', + 8 => '2.7x Crop', + 9 => 'DX Movie Crop', + 10 => '1.3x Movie Crop', + 11 => 'FX Uncropped', + 12 => 'DX Uncropped', + 15 => '1.5x Movie Crop', + 17 => '1:1 Crop' + ); + + protected static $colorSpaces = array( + 1 => 'sRGB', + 2 => 'Adobe RGB' + ); + + protected static $vibrationReductions = array( + 1 => 'On', + 2 => 'Off' + ); + + protected static $VRModes = array( + 0 => 'Normal', + 1 => 'On (1)', + 2 => 'Active', + 3 => 'Sport' + ); + + protected static $activeDLightnings = array( + 0 => 'Off', + 1 => 'Low', + 3 => 'Normal', + 5 => 'High', + 7 => 'Extra High', + 8 => 'Extra High 1', + 9 => 'Extra High 2', + 10 => 'Extra High 3', + 11 => 'Extra High 4', + 65535 => 'Auto' + ); + + protected static $pictureControlDataAdjusts = array( + 0 => 'default', + 1 => 'quick', + 2 => 'full' + ); + + protected static $pictureControlDataFilterEffects = array( + 0x80 => 'off', + 0x81 => 'yellow', + 0x82 => 'orange', + 0x83 => 'red', + 0x84 => 'green', + 0xff => 'n/a' + ); + + protected static $pictureControlDataToningEffects = 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' + ); + + protected static $isoInfoExpansions = array( + 0x0000 => 'Off', + 0x0101 => 'Hi 0.3', + 0x0102 => 'Hi 0.5', + 0x0103 => 'Hi 0.7', + 0x0104 => 'Hi 1.0', + 0x0105 => 'Hi 1.3', + 0x0106 => 'Hi 1.5', + 0x0107 => 'Hi 1.7', + 0x0108 => 'Hi 2.0', + 0x0109 => 'Hi 2.3', + 0x010a => 'Hi 2.5', + 0x010b => 'Hi 2.7', + 0x010c => 'Hi 3.0', + 0x010d => 'Hi 3.3', + 0x010e => 'Hi 3.5', + 0x010f => 'Hi 3.7', + 0x0110 => 'Hi 4.0', + 0x0111 => 'Hi 4.3', + 0x0112 => 'Hi 4.5', + 0x0113 => 'Hi 4.7', + 0x0114 => 'Hi 5.0', + 0x0201 => 'Lo 0.3', + 0x0202 => 'Lo 0.5', + 0x0203 => 'Lo 0.7', + 0x0204 => 'Lo 1.0', + ); + + protected static $isoInfoExpansions2 = array( + 0x0000 => 'Off', + 0x0101 => 'Hi 0.3', + 0x0102 => 'Hi 0.5', + 0x0103 => 'Hi 0.7', + 0x0104 => 'Hi 1.0', + 0x0105 => 'Hi 1.3', + 0x0106 => 'Hi 1.5', + 0x0107 => 'Hi 1.7', + 0x0108 => 'Hi 2.0', + 0x0201 => 'Lo 0.3', + 0x0202 => 'Lo 0.5', + 0x0203 => 'Lo 0.7', + 0x0204 => 'Lo 1.0', + ); + + protected static $vignetteControls = array( + 0 => 'Off', + 1 => 'Low', + 3 => 'Normal', + 5 => 'High' + ); + + protected static $flashModes = array( + 0 => 'Did Not Fire', + 1 => 'Fired, Manual', + 3 => 'Not Ready', + 7 => 'Fired, External', + 8 => 'Fired, Commander Mode', + 9 => 'Fired, TTL Mode', + 18 => 'LED Light' + ); + + protected static $flashInfoSources = array( + 0 => 'None', + 1 => 'External', + 2 => 'Internal' + ); + + protected static $flashInfoExternalFlashFirmwares = array( + '0 0' => 'n/a', + '1 1' => '1.01 (SB-800 or Metz 58 AF-1)', + '1 3' => '1.03 (SB-800)', + '2 1' => '2.01 (SB-800)', + '2 4' => '2.04 (SB-600)', + '2 5' => '2.05 (SB-600)', + '3 1' => '3.01 (SU-800 Remote Commander)', + '4 1' => '4.01 (SB-400)', + '4 2' => '4.02 (SB-400)', + '4 4' => '4.04 (SB-400)', + '5 1' => '5.01 (SB-900)', + '5 2' => '5.02 (SB-900)', + '6 1' => '6.01 (SB-700)', + '7 1' => '7.01 (SB-910)', + ); + + protected static $flashInfoExternalFlashFlags = array( + 0 => 'Fired', + 2 => 'Bounce Flash', + 4 => 'Wide Flash Adapter', + 5 => 'Dome Diffuser', + ); + + protected static $flashInfoExternalFlashStatuses = array( + 0 => 'Flash Not Attached', + 1 => 'Flash Attached', + ); + + protected static $flashInfoExternalFlashReadyStates = array( + 0 => 'n/a', + 1 => 'Ready', + 6 => 'Not Ready', + ); + + protected static $flashInfoGNDistances = array( + 0 => 0, 19 => '2.8 m', + 1 => '0.1 m', 20 => '3.2 m', + 2 => '0.2 m', 21 => '3.6 m', + 3 => '0.3 m', 22 => '4.0 m', + 4 => '0.4 m', 23 => '4.5 m', + 5 => '0.5 m', 24 => '5.0 m', + 6 => '0.6 m', 25 => '5.6 m', + 7 => '0.7 m', 26 => '6.3 m', + 8 => '0.8 m', 27 => '7.1 m', + 9 => '0.9 m', 28 => '8.0 m', + 10 => '1.0 m', 29 => '9.0 m', + 11 => '1.1 m', 30 => '10.0 m', + 12 => '1.3 m', 31 => '11.0 m', + 13 => '1.4 m', 32 => '13.0 m', + 14 => '1.6 m', 33 => '14.0 m', + 15 => '1.8 m', 34 => '16.0 m', + 16 => '2.0 m', 35 => '18.0 m', + 17 => '2.2 m', 36 => '20.0 m', + 18 => '2.5 m', 255 => 'n/a' + ); + + protected static $flashInfoControlModes = array( + 0x00 => 'Off', + 0x01 => 'iTTL-BL', + 0x02 => 'iTTL', + 0x03 => 'Auto Aperture', + 0x04 => 'Automatic', + 0x05 => 'GN (distance priority)', + 0x06 => 'Manual', + 0x07 => 'Repeating Flash', + ); + + protected static $flashInfoColorFilters = array( + 0 => 'None', + 1 => 'FL-GL1 or SZ-2FL Fluorescent', + 2 => 'FL-GL2', + 9 => 'TN-A1 or SZ-2TN Incandescent', + 10 => 'TN-A2', + 65 => 'Red', + 66 => 'Blue', + 67 => 'Yellow', + 68 => 'Amber', + ); + + protected static $highISONoiseReductions = array( + 0 => 'Off', + 1 => 'Minimal', + 2 => 'Low', + 3 => 'Medium Low', + 4 => 'Normal', + 5 => 'Medium High', + 6 => 'High' + ); + + protected static $AFInfo2ContrastDetectAFChoices = array( + 0 => 'Off', + 1 => 'On', + 2 => 'On (2)' + ); + + protected static $AFInfo2AFAreaModesWithoutContrastDetectAF = array( + 0 => 'Single Area', + 1 => 'Dynamic Area', + 2 => 'Dynamic Area (closest subject)', + 3 => 'Group Dynamic', + 4 => 'Dynamic Area (9 points)', + 5 => 'Dynamic Area (21 points)', + 6 => 'Dynamic Area (51 points)', + 7 => 'Dynamic Area (51 points, 3D-tracking)', + 8 => 'Auto-area', + 9 => 'Dynamic Area (3D-tracking)', + 10 => 'Single Area (wide)', + 11 => 'Dynamic Area (wide)', + 12 => 'Dynamic Area (wide, 3D-tracking)', + 13 => 'Group Area', + 14 => 'Dynamic Area (25 points)', + 15 => 'Dynamic Area (72 points)', + 16 => 'Group Area (HL)', + 17 => 'Group Area (VL)', + 18 => 'Dynamic Area (49 points)', + 128 => 'Single', + 129 => 'Auto (41 points)', + 130 => 'Subject Tracking (41 points)', + 131 => 'Face Priority (41 points)', + 192 => 'Pinpoint', + 193 => 'Single', + 195 => 'Wide (S)', + 196 => 'Wide (L)', + 197 => 'Auto', + ); + + protected static $AFInfo2AFAreaModesWithContrastDetectAF = array( + 0 => 'Contrast-detect', + 1 => 'Contrast-detect (normal area)', + 2 => 'Contrast-detect (wide area)', + 3 => 'Contrast-detect (face priority)', + 4 => 'Contrast-detect (subject tracking)', + 128 => 'Single', + 129 => 'Auto (41 points)', + 130 => 'Subject Tracking (41 points)', + 131 => 'Face Priority (41 points)', + 192 => 'Pinpoint', + 193 => 'Single', + 194 => 'Dynamic', + 195 => 'Wide (S)', + 196 => 'Wide (L)', + 197 => 'Auto', + 198 => 'Auto (People)', + 199 => 'Auto (Animal)', + 200 => 'Normal-area AF', + 201 => 'Wide-area AF', + 202 => 'Face-priority AF', + 203 => 'Subject-tracking AF', + ); + + protected static $AFInfo2PhaseDetectAFChoices = array( + 0 => 'Off', + 1 => 'On (51-point)', + 2 => 'On (11-point)', + 3 => 'On (39-point)', + 4 => 'On (73-point)', + 5 => 'On (5)', + 6 => 'On (105-point)', + 7 => 'On (153-point)', + 8 => 'On (81-point)', + 9 => 'On (105-point)', + ); + + protected static $NikkorZLensIDS = array( + 1 => 'Nikkor Z 24-70mm f/4 S', + 2 => 'Nikkor Z 14-30mm f/4 S', + 4 => 'Nikkor Z 35mm f/1.8 S', + 8 => 'Nikkor Z 58mm f/0.95 S Noct', + 9 => 'Nikkor Z 50mm f/1.8 S', + 11 => 'Nikkor Z DX 16-50mm f/3.5-6.3 VR', + 12 => 'Nikkor Z DX 50-250mm f/4.5-6.3 VR', + 13 => 'Nikkor Z 24-70mm f/2.8 S', + 14 => 'Nikkor Z 85mm f/1.8 S', + 15 => 'Nikkor Z 24mm f/1.8 S', + 16 => 'Nikkor Z 70-200mm f/2.8 VR S', + 17 => 'Nikkor Z 20mm f/1.8 S', + 18 => 'Nikkor Z 24-200mm f/4-6.3 VR', + 21 => 'Nikkor Z 50mm f/1.2 S', + 22 => 'Nikkor Z 24-50mm f/4-6.3', + 23 => 'Nikkor Z 14-24mm f/2.8 S', + ); + + protected static $nikonTextEncodings = array( + 1 => 'UTF-8', + 2 => 'UTF-16' + ); + + /** + * Ref 4 + * + * @var int[][] + */ + protected static $decodeTables = array( + array( + 0xc1,0xbf,0x6d,0x0d,0x59,0xc5,0x13,0x9d,0x83,0x61,0x6b,0x4f,0xc7,0x7f,0x3d,0x3d, + 0x53,0x59,0xe3,0xc7,0xe9,0x2f,0x95,0xa7,0x95,0x1f,0xdf,0x7f,0x2b,0x29,0xc7,0x0d, + 0xdf,0x07,0xef,0x71,0x89,0x3d,0x13,0x3d,0x3b,0x13,0xfb,0x0d,0x89,0xc1,0x65,0x1f, + 0xb3,0x0d,0x6b,0x29,0xe3,0xfb,0xef,0xa3,0x6b,0x47,0x7f,0x95,0x35,0xa7,0x47,0x4f, + 0xc7,0xf1,0x59,0x95,0x35,0x11,0x29,0x61,0xf1,0x3d,0xb3,0x2b,0x0d,0x43,0x89,0xc1, + 0x9d,0x9d,0x89,0x65,0xf1,0xe9,0xdf,0xbf,0x3d,0x7f,0x53,0x97,0xe5,0xe9,0x95,0x17, + 0x1d,0x3d,0x8b,0xfb,0xc7,0xe3,0x67,0xa7,0x07,0xf1,0x71,0xa7,0x53,0xb5,0x29,0x89, + 0xe5,0x2b,0xa7,0x17,0x29,0xe9,0x4f,0xc5,0x65,0x6d,0x6b,0xef,0x0d,0x89,0x49,0x2f, + 0xb3,0x43,0x53,0x65,0x1d,0x49,0xa3,0x13,0x89,0x59,0xef,0x6b,0xef,0x65,0x1d,0x0b, + 0x59,0x13,0xe3,0x4f,0x9d,0xb3,0x29,0x43,0x2b,0x07,0x1d,0x95,0x59,0x59,0x47,0xfb, + 0xe5,0xe9,0x61,0x47,0x2f,0x35,0x7f,0x17,0x7f,0xef,0x7f,0x95,0x95,0x71,0xd3,0xa3, + 0x0b,0x71,0xa3,0xad,0x0b,0x3b,0xb5,0xfb,0xa3,0xbf,0x4f,0x83,0x1d,0xad,0xe9,0x2f, + 0x71,0x65,0xa3,0xe5,0x07,0x35,0x3d,0x0d,0xb5,0xe9,0xe5,0x47,0x3b,0x9d,0xef,0x35, + 0xa3,0xbf,0xb3,0xdf,0x53,0xd3,0x97,0x53,0x49,0x71,0x07,0x35,0x61,0x71,0x2f,0x43, + 0x2f,0x11,0xdf,0x17,0x97,0xfb,0x95,0x3b,0x7f,0x6b,0xd3,0x25,0xbf,0xad,0xc7,0xc5, + 0xc5,0xb5,0x8b,0xef,0x2f,0xd3,0x07,0x6b,0x25,0x49,0x95,0x25,0x49,0x6d,0x71,0xc7 + ), + array( + 0xa7,0xbc,0xc9,0xad,0x91,0xdf,0x85,0xe5,0xd4,0x78,0xd5,0x17,0x46,0x7c,0x29,0x4c, + 0x4d,0x03,0xe9,0x25,0x68,0x11,0x86,0xb3,0xbd,0xf7,0x6f,0x61,0x22,0xa2,0x26,0x34, + 0x2a,0xbe,0x1e,0x46,0x14,0x68,0x9d,0x44,0x18,0xc2,0x40,0xf4,0x7e,0x5f,0x1b,0xad, + 0x0b,0x94,0xb6,0x67,0xb4,0x0b,0xe1,0xea,0x95,0x9c,0x66,0xdc,0xe7,0x5d,0x6c,0x05, + 0xda,0xd5,0xdf,0x7a,0xef,0xf6,0xdb,0x1f,0x82,0x4c,0xc0,0x68,0x47,0xa1,0xbd,0xee, + 0x39,0x50,0x56,0x4a,0xdd,0xdf,0xa5,0xf8,0xc6,0xda,0xca,0x90,0xca,0x01,0x42,0x9d, + 0x8b,0x0c,0x73,0x43,0x75,0x05,0x94,0xde,0x24,0xb3,0x80,0x34,0xe5,0x2c,0xdc,0x9b, + 0x3f,0xca,0x33,0x45,0xd0,0xdb,0x5f,0xf5,0x52,0xc3,0x21,0xda,0xe2,0x22,0x72,0x6b, + 0x3e,0xd0,0x5b,0xa8,0x87,0x8c,0x06,0x5d,0x0f,0xdd,0x09,0x19,0x93,0xd0,0xb9,0xfc, + 0x8b,0x0f,0x84,0x60,0x33,0x1c,0x9b,0x45,0xf1,0xf0,0xa3,0x94,0x3a,0x12,0x77,0x33, + 0x4d,0x44,0x78,0x28,0x3c,0x9e,0xfd,0x65,0x57,0x16,0x94,0x6b,0xfb,0x59,0xd0,0xc8, + 0x22,0x36,0xdb,0xd2,0x63,0x98,0x43,0xa1,0x04,0x87,0x86,0xf7,0xa6,0x26,0xbb,0xd6, + 0x59,0x4d,0xbf,0x6a,0x2e,0xaa,0x2b,0xef,0xe6,0x78,0xb6,0x4e,0xe0,0x2f,0xdc,0x7c, + 0xbe,0x57,0x19,0x32,0x7e,0x2a,0xd0,0xb8,0xba,0x29,0x00,0x3c,0x52,0x7d,0xa8,0x49, + 0x3b,0x2d,0xeb,0x25,0x49,0xfa,0xa3,0xaa,0x39,0xa7,0xc5,0xa7,0x50,0x11,0x36,0xfb, + 0xc6,0x67,0x4a,0xf5,0xa5,0x12,0x65,0x7e,0xb0,0xdf,0xaf,0x4e,0xb3,0x61,0x7f,0x2f + ) + ); + + /** + * @var getID3 + */ + private $getid3; + + public function __construct(getID3 $getid3) + { + $this->getid3 = $getid3; + } + + /** + * Get a copy of all NCTG tags extracted from the video + * + * @param string $atomData + * + * @return array + */ + public function parse($atomData) { + // 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 / unsigned byte (size field *= 1-byte) + // 0x0002 = char / ascii strings (size field *= 1-byte) + // 0x0003 = DWORD+ / unsigned short (size field *= 2-byte), values are stored CDAB + // 0x0004 = QWORD+ / unsigned long (size field *= 4-byte), values are stored EFGHABCD + // 0x0005 = float / unsigned rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + // 0x0006 = signed byte (size field *= 1-byte) + // 0x0007 = raw bytes (size field *= 1-byte) + // 0x0008 = signed short (size field *= 2-byte), values are stored as CDAB + // 0x0009 = signed long (size field *= 4-byte), values are stored as EFGHABCD + // 0x000A = float / signed rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + // * 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', + 0x00000019 => 'TimeZone', + 0x00000022 => 'FrameWidth', + 0x00000023 => 'FrameHeight', + 0x00000032 => 'AudioChannels', + 0x00000033 => 'AudioBitsPerSample', + 0x00000034 => 'AudioSampleRate', + 0x00001002 => 'NikonDateTime', + 0x00001013 => 'ElectronicVR', + 0x0110829a => 'ExposureTime', + 0x0110829d => 'FNumber', + 0x01108822 => 'ExposureProgram', + 0x01109204 => 'ExposureCompensation', + 0x01109207 => 'MeteringMode', + 0x0110920a => 'FocalLength', // mm + 0x0110a431 => 'SerialNumber', + 0x0110a432 => 'LensInfo', + 0x0110a433 => 'LensMake', + 0x0110a434 => 'LensModel', + 0x0110a435 => 'LensSerialNumber', + 0x01200000 => 'GPSVersionID', + 0x01200001 => 'GPSLatitudeRef', + 0x01200002 => 'GPSLatitude', + 0x01200003 => 'GPSLongitudeRef', + 0x01200004 => 'GPSLongitude', + 0x01200005 => 'GPSAltitudeRef', // 0 = Above Sea Level, 1 = Below Sea Level + 0x01200006 => 'GPSAltitude', + 0x01200007 => 'GPSTimeStamp', + 0x01200008 => 'GPSSatellites', + 0x01200010 => 'GPSImgDirectionRef', // M = Magnetic North, T = True North + 0x01200011 => 'GPSImgDirection', + 0x01200012 => 'GPSMapDatum', + 0x0120001d => 'GPSDateStamp', + 0x02000001 => 'MakerNoteVersion', + 0x02000005 => 'WhiteBalance', + 0x02000007 => 'FocusMode', + 0x0200000b => 'WhiteBalanceFineTune', + 0x0200001b => 'CropHiSpeed', + 0x0200001e => 'ColorSpace', + 0x0200001f => 'VRInfo', + 0x02000022 => 'ActiveDLighting', + 0x02000023 => 'PictureControlData', + 0x02000024 => 'WorldTime', + 0x02000025 => 'ISOInfo', + 0x0200002a => 'VignetteControl', + 0x0200002c => 'UnknownInfo', + 0x02000032 => 'UnknownInfo2', + 0x02000039 => 'LocationInfo', + 0x02000083 => 'LensType', + 0x02000084 => 'Lens', + 0x02000087 => 'FlashMode', + 0x02000098 => 'LensData', + 0x020000a7 => 'ShutterCount', + 0x020000a8 => 'FlashInfo', + 0x020000ab => 'VariProgram', + 0x020000b1 => 'HighISONoiseReduction', + 0x020000b7 => 'AFInfo2', + 0x020000c3 => 'BarometerInfo', + ); + + $firstPassNeededTags = array( + 0x00000002, // Model + 0x0110a431, // SerialNumber + 0x020000a7, // ShutterCount + ); + + $datalength = strlen($atomData); + $parsed = array(); + $model = $serialNumber = $shutterCount = null; + for ($pass = 0; $pass < 2; ++$pass) { + $offset = 0; + $parsed = array(); + $data = null; + while ($offset < $datalength) { + $record_type = getid3_lib::BigEndian2Int(substr($atomData, $offset, 4)); + $offset += 4; + $data_size_type = getid3_lib::BigEndian2Int(substr($atomData, $offset, 2)); + $data_size = static::$exifTypeSizes[$data_size_type]; + $offset += 2; + $data_count = getid3_lib::BigEndian2Int(substr($atomData, $offset, 2)); + $offset += 2; + $data = array(); + + if ($pass === 0 && !in_array($record_type, $firstPassNeededTags, true)) { + $offset += $data_count * $data_size; + continue; + } + + switch ($data_size_type) { + case self::EXIF_TYPE_UINT8: // 0x0001 = flag / unsigned byte (size field *= 1-byte) + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_CHAR: // 0x0002 = char / ascii strings (size field *= 1-byte) + $data = substr($atomData, $offset, $data_count * $data_size); + $offset += ($data_count * $data_size); + $data = rtrim($data, "\x00"); + break; + case self::EXIF_TYPE_UINT16: // 0x0003 = DWORD+ / unsigned short (size field *= 2-byte), values are stored CDAB + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_UINT32: // 0x0004 = QWORD+ / unsigned long (size field *= 4-byte), values are stored EFGHABCD + // нужно проверить FrameCount + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_URATIONAL: // 0x0005 = float / unsigned rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + for ($i = 0; $i < $data_count; ++$i) { + $numerator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 0, 4)); + $denomninator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 4, 4)); + if ($denomninator == 0) { + $data[] = false; + } else { + $data[] = (float)$numerator / $denomninator; + } + } + $offset += ($data_size * $data_count); + break; + case self::EXIF_TYPE_INT8: // 0x0006 = bytes / signed byte (size field *= 1-byte) + // NOT TESTED + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size), false, true); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_RAW: // 0x0007 = raw bytes (size field *= 1-byte) + $data = substr($atomData, $offset, $data_count * $data_size); + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_INT16: // 0x0008 = signed short (size field *= 2-byte), values are stored as CDAB + for ($i = 0; $i < $data_count; ++$i) { + $value = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + if ($value >= 0x8000) { + $value -= 0x10000; + } + $data[] = $value; + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_INT32: // 0x0009 = signed long (size field *= 4-byte), values are stored as EFGHABCD + // NOT TESTED + for ($i = 0; $i < $data_count; ++$i) { + $data = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size), false, true); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_RATIONAL: // 0x000A = float / signed rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + // NOT TESTED + for ($i = 0; $i < $data_count; ++$i) { + $numerator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 0, 4), false, true); + $denomninator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 4, 4), false, true); + if ($denomninator == 0) { + $data[] = false; + } else { + $data[] = (float)$numerator / $denomninator; + } + } + $offset += ($data_size * $data_count); + if (count($data) == 1) { + $data = $data[0]; + } + break; + default: + $this->getid3->warning('QuicktimeParseNikonNCTG()::unknown $data_size_type: ' . $data_size_type); + break 2; + } + + if (is_array($data) && count($data) === 1) { + $data = $data[0]; + } + + switch ($record_type) { + case 0x00000002: + $model = $data; + break; + case 0x00000013: // FrameCount + if (is_array($data) && count($data) === 2 && $data[1] == 0) { + $data = $data[0]; + } + break; + case 0x00000011: // CreateDate + case 0x00000012: // DateTimeOriginal + case 0x00001002: // NikonDateTime + $data = strtotime($data); + break; + case 0x00001013: // ElectronicVR + $data = (bool) $data; + break; + case 0x0110829a: // ExposureTime + // Print exposure time as a fraction + /** @var float $data */ + if ($data < 0.25001 && $data > 0) { + $data = sprintf("1/%d", intval(0.5 + 1 / $data)); + } + break; + case 0x01109204: // ExposureCompensation + $data = $this->printFraction($data); + break; + case 0x01108822: // ExposureProgram + $data = isset(static::$exposurePrograms[$data]) ? static::$exposurePrograms[$data] : $data; + break; + case 0x01109207: // MeteringMode + $data = isset(static::$meteringModes[$data]) ? static::$meteringModes[$data] : $data; + break; + case 0x0110a431: // SerialNumber + $serialNumber = $this->serialKey($data, $model); + break; + case 0x01200000: // GPSVersionID + $parsed['GPS']['computed']['version'] = 'v'.implode('.', $data); + break; + case 0x01200002: // GPSLatitude + if (is_array($data)) { + $direction_multiplier = ((isset($parsed['GPSLatitudeRef']) && ($parsed['GPSLatitudeRef'] === 'S')) ? -1 : 1); + $parsed['GPS']['computed']['latitude'] = $direction_multiplier * ($data[0] + ($data[1] / 60) + ($data[2] / 3600)); + } + break; + case 0x01200004: // GPSLongitude + if (is_array($data)) { + $direction_multiplier = ((isset($parsed['GPSLongitudeRef']) && ($parsed['GPSLongitudeRef'] === 'W')) ? -1 : 1); + $parsed['GPS']['computed']['longitude'] = $direction_multiplier * ($data[0] + ($data[1] / 60) + ($data[2] / 3600)); + } + break; + case 0x01200006: // GPSAltitude + if (isset($parsed['GPSAltitudeRef'])) { + $direction_multiplier = (!empty($parsed['GPSAltitudeRef']) ? -1 : 1); // 0 = above sea level; 1 = below sea level + $parsed['GPS']['computed']['altitude'] = $direction_multiplier * $data; + } + break; + case 0x0120001d: // GPSDateStamp + if (isset($parsed['GPSTimeStamp']) && is_array($parsed['GPSTimeStamp']) && $data !== '') { + $explodedDate = explode(':', $data); + $parsed['GPS']['computed']['timestamp'] = gmmktime($parsed['GPSTimeStamp'][0], $parsed['GPSTimeStamp'][1], $parsed['GPSTimeStamp'][2], $explodedDate[1], $explodedDate[2], $explodedDate[0]); + } + break; + case 0x02000001: // MakerNoteVersion + $data = ltrim(substr($data, 0, 2) . '.' . substr($data, 2, 2), '0'); + break; + case 0x0200001b: // CropHiSpeed + if (is_array($data) && count($data) === 7) { + $name = isset(static::$cropHiSpeeds[$data[0]]) ? static::$cropHiSpeeds[$data[0]] : sprintf('Unknown (%d)', $data[0]); + $data = array( + 'Name' => $name, + 'OriginalWidth' => $data[1], + 'OriginalHeight' => $data[2], + 'CroppedWidth' => $data[3], + 'CroppedHeight' => $data[4], + 'PixelXPosition' => $data[5], + 'PixelYPosition' => $data[6], + ); + } + break; + case 0x0200001e: // ColorSpace + $data = isset(static::$colorSpaces[$data]) ? static::$colorSpaces[$data] : $data; + break; + case 0x0200001f: // VRInfo + $data = array( + 'VRInfoVersion' => substr($data, 0, 4), + 'VibrationReduction' => isset(static::$vibrationReductions[ord(substr($data, 4, 1))]) + ? static::$vibrationReductions[ord(substr($data, 4, 1))] + : null, + 'VRMode' => static::$VRModes[ord(substr($data, 6, 1))], + ); + break; + case 0x02000022: // ActiveDLighting + $data = isset(static::$activeDLightnings[$data]) ? static::$activeDLightnings[$data] : $data; + break; + case 0x02000023: // PictureControlData + switch (substr($data, 0, 2)) { + case '01': + $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' => static::$pictureControlDataAdjusts[ord(substr($data, 48, 1))], + 'PictureControlQuickAdjust' => $this->printPC(ord(substr($data, 49, 1)) - 0x80), + 'Sharpness' => $this->printPC(ord(substr($data, 50, 1)) - 0x80, 'No Sharpening', '%d'), + 'Contrast' => $this->printPC(ord(substr($data, 51, 1)) - 0x80), + 'Brightness' => $this->printPC(ord(substr($data, 52, 1)) - 0x80), + 'Saturation' => $this->printPC(ord(substr($data, 53, 1)) - 0x80), + 'HueAdjustment' => $this->printPC(ord(substr($data, 54, 1)) - 0x80, 'None'), + 'FilterEffect' => static::$pictureControlDataFilterEffects[ord(substr($data, 55, 1))], + 'ToningEffect' => static::$pictureControlDataToningEffects[ord(substr($data, 56, 1))], + 'ToningSaturation' => $this->printPC(ord(substr($data, 57, 1)) - 0x80), + ); + break; + case '02': + $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' => static::$pictureControlDataAdjusts[ord(substr($data, 48, 1))], + 'PictureControlQuickAdjust' => $this->printPC(ord(substr($data, 49, 1)) - 0x80), + 'Sharpness' => $this->printPC(ord(substr($data, 51, 1)) - 0x80, 'None', '%.2f', 4), + 'Clarity' => $this->printPC(ord(substr($data, 53, 1)) - 0x80, 'None', '%.2f', 4), + 'Contrast' => $this->printPC(ord(substr($data, 55, 1)) - 0x80, 'None', '%.2f', 4), + 'Brightness' => $this->printPC(ord(substr($data, 57, 1)) - 0x80, 'Normal', '%.2f', 4), + 'Saturation' => $this->printPC(ord(substr($data, 59, 1)) - 0x80, 'None', '%.2f', 4), + 'Hue' => $this->printPC(ord(substr($data, 61, 1)) - 0x80, 'None', '%.2f', 4), + 'FilterEffect' => static::$pictureControlDataFilterEffects[ord(substr($data, 63, 1))], + 'ToningEffect' => static::$pictureControlDataToningEffects[ord(substr($data, 64, 1))], + 'ToningSaturation' => $this->printPC(ord(substr($data, 65, 1)) - 0x80, 'None', '%.2f', 4), + ); + break; + case '03': + $data = array( + 'PictureControlVersion' => substr($data, 0, 4), + 'PictureControlName' => rtrim(substr($data, 8, 20), "\x00"), + 'PictureControlBase' => rtrim(substr($data, 28, 20), "\x00"), + 'PictureControlAdjust' => static::$pictureControlDataAdjusts[ord(substr($data, 54, 1))], + 'PictureControlQuickAdjust' => $this->printPC(ord(substr($data, 55, 1)) - 0x80), + 'Sharpness' => $this->printPC(ord(substr($data, 57, 1)) - 0x80, 'None', '%.2f', 4), + 'MidRangeSharpness' => $this->printPC(ord(substr($data, 59, 1)) - 0x80, 'None', '%.2f', 4), + 'Clarity' => $this->printPC(ord(substr($data, 61, 1)) - 0x80, 'None', '%.2f', 4), + 'Contrast' => $this->printPC(ord(substr($data, 63, 1)) - 0x80, 'None', '%.2f', 4), + 'Brightness' => $this->printPC(ord(substr($data, 65, 1)) - 0x80, 'Normal', '%.2f', 4), + 'Saturation' => $this->printPC(ord(substr($data, 67, 1)) - 0x80, 'None', '%.2f', 4), + 'Hue' => $this->printPC(ord(substr($data, 69, 1)) - 0x80, 'None', '%.2f', 4), + 'FilterEffect' => static::$pictureControlDataFilterEffects[ord(substr($data, 71, 1))], + 'ToningEffect' => static::$pictureControlDataToningEffects[ord(substr($data, 72, 1))], + 'ToningSaturation' => $this->printPC(ord(substr($data, 73, 1)) - 0x80, 'None', '%.2f', 4), + ); + break; + default: + $data = array( + 'PictureControlVersion' => substr($data, 0, 4), + ); + break; + } + break; + case 0x02000024: // WorldTime + // https://exiftool.org/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); + } + $hours = (int)abs($timezone / 60); + $minutes = abs($timezone) - $hours * 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' => sprintf('%s%02d:%02d', $timezone >= 0 ? '+' : '-', $hours, $minutes), + 'dst' => $dst, + 'display' => $datedisplayformat + ); + break; + case 0x02000025: // ISOInfo + $data = array( + 'ISO' => (int)ceil(100 * pow(2, ord(substr($data, 0, 1)) / 12 - 5)), + 'ISOExpansion' => static::$isoInfoExpansions[getid3_lib::BigEndian2Int(substr($data, 4, 2))], + 'ISO2' => (int)ceil(100 * pow(2, ord(substr($data, 6, 1)) / 12 - 5)), + 'ISOExpansion2' => static::$isoInfoExpansions2[getid3_lib::BigEndian2Int(substr($data, 10, 2))] + ); + break; + case 0x0200002a: // VignetteControl + $data = isset(static::$vignetteControls[$data]) ? static::$vignetteControls[$data] : $data; + break; + case 0x0200002c: // UnknownInfo + $data = array( + 'UnknownInfoVersion' => substr($data, 0, 4), + ); + break; + case 0x02000032: // UnknownInfo2 + $data = array( + 'UnknownInfo2Version' => substr($data, 0, 4), + ); + break; + case 0x02000039: // LocationInfo + $encoding = isset(static::$nikonTextEncodings[ord(substr($data, 4, 1))]) + ? static::$nikonTextEncodings[ord(substr($data, 4, 1))] + : null; + $data = array( + 'LocationInfoVersion' => substr($data, 0, 4), + 'TextEncoding' => $encoding, + 'CountryCode' => trim(substr($data, 5, 3), "\x00"), + 'POILevel' => ord(substr($data, 8, 1)), + 'Location' => getid3_lib::iconv_fallback($encoding, $this->getid3->info['encoding'], substr($data, 9, 70)), + ); + break; + case 0x02000083: // LensType + if ($data) { + $decodedBits = array( + '1' => (bool) (($data >> 4) & 1), + 'MF' => (bool) (($data >> 0) & 1), + 'D' => (bool) (($data >> 1) & 1), + 'E' => (bool) (($data >> 6) & 1), + 'G' => (bool) (($data >> 2) & 1), + 'VR' => (bool) (($data >> 3) & 1), + '[7]' => (bool) (($data >> 7) & 1), // AF-P? + '[8]' => (bool) (($data >> 5) & 1) // FT-1? + ); + if ($decodedBits['D'] === true && $decodedBits['G'] === true) { + $decodedBits['D'] = false; + } + } else { + $decodedBits = array('AF' => true); + } + $data = $decodedBits; + break; + case 0x0110a432: // LensInfo + case 0x02000084: // Lens + if (count($data) !== 4) { + break; + } + + $value = $data[0]; + if ($data[1] && $data[1] !== $data[0]) { + $value .= '-' . $data[1]; + } + $value .= 'mm f/' . $data[2]; + if ($data[3] && $data[3] !== $data[2]) { + $value .= '-' . $data[3]; + } + $data = $value; + break; + case 0x02000087: // FlashMode + $data = isset(static::$flashModes[$data]) ? static::$flashModes[$data] : $data; + break; + case 0x02000098: // LensData + $version = substr($data, 0, 4); + + switch ($version) { + case '0100': + $data = array( + 'LensDataVersion' => $version, + 'LensIDNumber' => ord(substr($data, 6, 1)), + 'LensFStops' => ord(substr($data, 7, 1)) / 12, + 'MinFocalLength' => 5 * pow(2, ord(substr($data, 8, 1)) / 24), // mm + 'MaxFocalLength' => 5 * pow(2, ord(substr($data, 9, 1)) / 24), // mm + 'MaxApertureAtMinFocal' => pow(2, ord(substr($data, 10, 1)) / 24), + 'MaxApertureAtMaxFocal' => pow(2, ord(substr($data, 11, 1)) / 24), + 'MCUVersion' => ord(substr($data, 12, 1)), + ); + break; + case '0101': + case '0201': + case '0202': + case '0203': + $isEncrypted = $version !== '0101'; + if ($isEncrypted) { + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + } + + $data = array( + 'LensDataVersion' => $version, + 'ExitPupilPosition' => ord(substr($data, 4, 1)) > 0 ? 2048 / ord(substr($data, 4, 1)) : 0, // mm + 'AFAperture' => pow(2, ord(substr($data, 5, 1)) / 24), + 'FocusPosition' => '0x' . str_pad(strtoupper(dechex(ord(substr($data, 8, 1)))), 2, '0', STR_PAD_LEFT), + 'FocusDistance' => 0.01 * pow(10, ord(substr($data, 9, 1)) / 40), // m + 'FocalLength' => 5 * pow(2, ord(substr($data, 10, 1)) / 24), // mm + 'LensIDNumber' => ord(substr($data, 11, 1)), + 'LensFStops' => ord(substr($data, 12, 1)) / 12, + 'MinFocalLength' => 5 * pow(2, ord(substr($data, 13, 1)) / 24), // mm + 'MaxFocalLength' => 5 * pow(2, ord(substr($data, 14, 1)) / 24), // mm + 'MaxApertureAtMinFocal' => pow(2, ord(substr($data, 15, 1)) / 24), + 'MaxApertureAtMaxFocal' => pow(2, ord(substr($data, 16, 1)) / 24), + 'MCUVersion' => ord(substr($data, 17, 1)), + 'EffectiveMaxAperture' => pow(2, ord(substr($data, 18, 1)) / 24), + ); + break; + case '0204': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'ExitPupilPosition' => ord(substr($data, 4, 1)) > 0 ? 2048 / ord(substr($data, 4, 1)) : 0, // mm + 'AFAperture' => pow(2, ord(substr($data, 5, 1)) / 24), + 'FocusPosition' => '0x' . str_pad(strtoupper(dechex(ord(substr($data, 8, 1)))), 2, '0', STR_PAD_LEFT), + 'FocusDistance' => 0.01 * pow(10, ord(substr($data, 10, 1)) / 40), // m + 'FocalLength' => 5 * pow(2, ord(substr($data, 11, 1)) / 24), // mm + 'LensIDNumber' => ord(substr($data, 12, 1)), + 'LensFStops' => ord(substr($data, 13, 1)) / 12, + 'MinFocalLength' => 5 * pow(2, ord(substr($data, 14, 1)) / 24), // mm + 'MaxFocalLength' => 5 * pow(2, ord(substr($data, 15, 1)) / 24), // mm + 'MaxApertureAtMinFocal' => pow(2, ord(substr($data, 16, 1)) / 24), + 'MaxApertureAtMaxFocal' => pow(2, ord(substr($data, 17, 1)) / 24), + 'MCUVersion' => ord(substr($data, 18, 1)), + 'EffectiveMaxAperture' => pow(2, ord(substr($data, 19, 1)) / 24), + ); + break; + case '0400': + case '0401': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'LensModel' => substr($data, 394, 64), + ); + break; + case '0402': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'LensModel' => substr($data, 395, 64), + ); + break; + case '0403': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'LensModel' => substr($data, 684, 64), + ); + break; + case '0800': + case '0801': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $newData = array( + 'LensDataVersion' => $version, + ); + + if (!preg_match('#^.\0+#s', substr($data, 3, 17))) { + $newData['ExitPupilPosition'] = ord(substr($data, 4, 1)) > 0 ? 2048 / ord(substr($data, 4, 1)) : 0; // mm + $newData['AFAperture'] = pow(2, ord(substr($data, 5, 1)) / 24); + $newData['FocusPosition'] = '0x' . str_pad(strtoupper(dechex(ord(substr($data, 9, 1)))), 2, '0', STR_PAD_LEFT); + $newData['FocusDistance'] = 0.01 * pow(10, ord(substr($data, 11, 1)) / 40); // m + $newData['FocalLength'] = 5 * pow(2, ord(substr($data, 12, 1)) / 24); // mm + $newData['LensIDNumber'] = ord(substr($data, 13, 1)); + $newData['LensFStops'] = ord(substr($data, 14, 1)) / 12; + $newData['MinFocalLength'] = 5 * pow(2, ord(substr($data, 15, 1)) / 24); // mm + $newData['MaxFocalLength'] = 5 * pow(2, ord(substr($data, 16, 1)) / 24); // mm + $newData['MaxApertureAtMinFocal'] = pow(2, ord(substr($data, 17, 1)) / 24); + $newData['MaxApertureAtMaxFocal'] = pow(2, ord(substr($data, 18, 1)) / 24); + $newData['MCUVersion'] = ord(substr($data, 19, 1)); + $newData['EffectiveMaxAperture'] = pow(2, ord(substr($data, 20, 1)) / 24); + } + + if (!preg_match('#^.\0+#s', substr($data, 47, 17))) { + $newData['LensID'] = static::$NikkorZLensIDS[getid3_lib::LittleEndian2Int(substr($data, 48, 2))]; + $newData['MaxAperture'] = pow(2, (getid3_lib::LittleEndian2Int(substr($data, 54, 2)) / 384 - 1)); + $newData['FNumber'] = pow(2, (getid3_lib::LittleEndian2Int(substr($data, 56, 2)) / 384 - 1)); + $newData['FocalLength'] = getid3_lib::LittleEndian2Int(substr($data, 60, 2)); // mm + $newData['FocusDistance'] = 0.01 * pow(10, ord(substr($data, 79, 1)) / 40); // m + } + + $data = $newData; + break; + default: + // $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + ); + break; + } + break; + case 0x020000a7: // ShutterCount + $shutterCount = $data; + break; + case 0x020000a8: // FlashInfo + $version = substr($data, 0, 4); + + switch ($version) { + case '0100': + case '0101': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 10, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 11, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 12, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 13, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 14, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 15, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 16, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 15, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 17, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 17, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 16, 1)) & 0xF0) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 18, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 18, 1), false, true) / 6), + ); + break; + case '0102': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 10, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 16, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 16, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 18, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 18, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 17, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 19, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 19, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 20, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 20, 1), false, true) / 6), + ); + break; + case '0103': + case '0104': + case '0105': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 10, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashColorFilter' => static::$flashInfoColorFilters[ord(substr($data, 16, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 19, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 19, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 18, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 20, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 20, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 18, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 21, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 21, 1), false, true) / 6), + 'ExternalFlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 27, 1), false, true) / 6), + 'FlashExposureComp3' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 29, 1), false, true) / 6), + 'FlashExposureComp4' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 39, 1), false, true) / 6), + ); + break; + case '0106': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => self::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashColorFilter' => static::$flashInfoColorFilters[ord(substr($data, 16, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0x0F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 39, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 39, 1), false, true) / 6), + 'FlashGroupAOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 40, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 40, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 18, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 41, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 41, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 18, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 42, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 42, 1), false, true) / 6), + ); + break; + case '0107': + case '0108': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashZoomOverride' => (bool)(ord(substr($data, 8, 1)) & 0x80), + 'ExternalFlashStatus' => static::$flashInfoExternalFlashStatuses[ord(substr($data, 8, 1)) & 0x01], + 'ExternalFlashReadyState' => static::$flashInfoExternalFlashReadyStates[ord(substr($data, 9, 1)) & 0x07], + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 40, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 40, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 18, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 41, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 41, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 18, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 42, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 42, 1), false, true) / 6), + ); + break; + case '0300': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 27, 1), false, true) / 6), + ); + break; + default: + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + ); + break; + } + break; + case 0x020000b1: // HighISONoiseReduction + $data = isset(static::$highISONoiseReductions[$data]) ? static::$highISONoiseReductions[$data] : $data; + break; + case 0x020000b7: // AFInfo2 + $avInfo2Version = substr($data, 0, 4); + $contrastDetectAF = ord(substr($data, 4, 1)); + $phaseDetectAF = ord(substr($data, 6, 1)); + $rows = array( + 'AFInfo2Version' => $avInfo2Version, + 'ContrastDetectAF' => static::$AFInfo2ContrastDetectAFChoices[$contrastDetectAF], + 'AFAreaMode' => $contrastDetectAF + ? static::$AFInfo2AFAreaModesWithContrastDetectAF[ord(substr($data, 5, 1))] + : static::$AFInfo2AFAreaModesWithoutContrastDetectAF[ord(substr($data, 5, 1))], + 'PhaseDetectAF' => static::$AFInfo2PhaseDetectAFChoices[$phaseDetectAF], + ); + + if ($avInfo2Version === '0100') { + $rows['AFImageWidth'] = getid3_lib::BigEndian2Int(substr($data, 16, 2)); + $rows['AFImageHeight'] = getid3_lib::BigEndian2Int(substr($data, 18, 2)); + $rows['AFAreaXPosition'] = getid3_lib::BigEndian2Int(substr($data, 20, 2)); + $rows['AFAreaYPosition'] = getid3_lib::BigEndian2Int(substr($data, 22, 2)); + $rows['AFAreaWidth'] = getid3_lib::BigEndian2Int(substr($data, 24, 2)); + $rows['AFAreaHeight'] = getid3_lib::BigEndian2Int(substr($data, 26, 2)); + $rows['ContrastDetectAFInFocus'] = (bool)ord(substr($data, 28, 1)); + } elseif (strpos($avInfo2Version, '03') === 0) { + $rows['AFImageWidth'] = getid3_lib::BigEndian2Int(substr($data, 42, 2)); + $rows['AFImageHeight'] = getid3_lib::BigEndian2Int(substr($data, 44, 2)); + if ($contrastDetectAF === 2 + || ($contrastDetectAF === 1 && $avInfo2Version === '0301') + ) { + $rows['AFAreaXPosition'] = getid3_lib::BigEndian2Int(substr($data, 46, 2)); + $rows['AFAreaYPosition'] = getid3_lib::BigEndian2Int(substr($data, 48, 2)); + } + $rows['AFAreaWidth'] = getid3_lib::BigEndian2Int(substr($data, 50, 2)); + $rows['AFAreaHeight'] = getid3_lib::BigEndian2Int(substr($data, 52, 2)); + } elseif ($contrastDetectAF === 1 && $avInfo2Version === '0101') { + $rows['AFImageWidth'] = getid3_lib::BigEndian2Int(substr($data, 70, 2)); + $rows['AFImageHeight'] = getid3_lib::BigEndian2Int(substr($data, 72, 2)); + $rows['AFAreaXPosition'] = getid3_lib::BigEndian2Int(substr($data, 74, 2)); + $rows['AFAreaYPosition'] = getid3_lib::BigEndian2Int(substr($data, 76, 2)); + $rows['AFAreaWidth'] = getid3_lib::BigEndian2Int(substr($data, 78, 2)); + $rows['AFAreaHeight'] = getid3_lib::BigEndian2Int(substr($data, 80, 2)); + $rows['ContrastDetectAFInFocus'] = (bool) ord(substr($data, 82, 1)); + } + + $data = $rows; + break; + case 0x020000c3: // BarometerInfo + $data = array( + 'BarometerInfoVersion' => substr($data, 0, 4), + 'Altitude' => getid3_lib::BigEndian2Int(substr($data, 6, 4), false, true), // m + ); + 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 int $value 0x80 subtracted + * @param string $normalName 'Normal' (0 value) string + * @param string|null $format format string for numbers (default '%+d'), 3) v2 divisor + * @param int|null $div + * + * @return string + */ + protected function printPC($value, $normalName = 'Normal', $format = '%+d', $div = 1) { + switch ($value) { + case 0: + return $normalName; + case 0x7f: + return 'n/a'; + case -0x80: + return 'Auto'; + case -0x7f: + return 'User'; + } + + return sprintf($format, $value / $div); + } + + /** + * @param int|float $value + * + * @return string + */ + protected function printFraction($value) { + if (!$value) { + return '0'; + } elseif ((int) $value /$value > 0.999) { + return sprintf("%+d", (int) $value); + } elseif ((int) ($value * 2) / ($value * 2) > 0.999) { + return sprintf("%+d/2", (int) ($value * 2)); + } elseif ((int) ($value * 3) / ($value * 3) > 0.999) { + return sprintf("%+d/3", (int) ($value * 3)); + } + + return sprintf("%+.3g", $value); + } + + /** + * @param int $firstByte + * @param int $secondByte + * + * @return string + */ + protected function flashFirmwareLookup($firstByte, $secondByte) + { + $indexKey = $firstByte.' '.$secondByte; + if (isset(static::$flashInfoExternalFlashFirmwares[$indexKey])) { + return static::$flashInfoExternalFlashFirmwares[$indexKey]; + } + + return sprintf('%d.%.2d (Unknown model)', $firstByte, $secondByte); + } + + /** + * @param int $flags + * + * @return string[]|string + */ + protected function externalFlashFlagsLookup($flags) + { + $result = array(); + foreach (static::$flashInfoExternalFlashFlags as $bit => $value) { + if (($flags >> $bit) & 1) { + $result[] = $value; + } + } + + return $result; + } + + /** + * @param string $data + * @param mixed|null $serialNumber + * @param mixed|null $shutterCount + * @param int $decryptStart + * + * @return false|string + */ + protected function decryptLensInfo( + $data, + $serialNumber = null, + $shutterCount = null, + $decryptStart = 0 + ) { + if (null === $serialNumber && null === $shutterCount) { + return false; + } + + if (!is_int($serialNumber) || !is_int($shutterCount)) { + if (null !== $serialNumber && null !== $shutterCount) { + $this->getid3->warning('Invalid '.(!is_int($serialNumber) ? 'SerialNumber' : 'ShutterCount')); + } else { + $this->getid3->warning('Cannot decrypt Nikon tags because '.(null === $serialNumber ? 'SerialNumber' : 'ShutterCount').' key is not defined.'); + } + + return false; + } + + $start = $decryptStart; + $length = strlen($data) - $start; + + return $this->decrypt($data, $serialNumber, $shutterCount, $start, $length); + } + + /** + * Decrypt Nikon data block + * + * @param string $data + * @param int $serialNumber + * @param int $count + * @param int $start + * @param int $length + * + * @return string + */ + protected function decrypt($data, $serialNumber, $count, $start = 0, $length = null) + { + $maxLen = strlen($data) - $start; + if (null === $length || $length > $maxLen) { + $length = $maxLen; + } + + if ($length <= 0) { + return $data; + } + + $key = 0; + for ($i = 0; $i < 4; ++$i) { + $key ^= ($count >> ($i * 8)) & 0xFF; + } + $ci = static::$decodeTables[0][$serialNumber & 0xff]; + $cj = static::$decodeTables[1][$key]; + $ck = 0x60; + $unpackedData = array(); + for ($i = $start; $i < $length + $start; ++$i) { + $cj = ($cj + $ci * $ck) & 0xff; + $ck = ($ck + 1) & 0xff; + $unpackedData[] = ord($data[$i]) ^ $cj; + } + + $end = $start + $length; + $pre = $start ? substr($data, 0, $start) : ''; + $post = $end < strlen($data) ? substr($data, $end) : ''; + + return $pre . implode('', array_map('chr', $unpackedData)) . $post; + } + + /** + * Get serial number for use as a decryption key + * + * @param string $serialNumber + * @param string|null $model + * + * @return int|null + */ + protected function serialKey($serialNumber, $model = null) + { + if (empty($serialNumber) || ctype_digit($serialNumber)) { + return (int) $serialNumber; + } + + if (null !== $model && preg_match('#\bD50$#', $model)) { + return 0x22; + } + + return 0x60; + } +}