analyze($path); if (!empty($info['error'])) return null; $candidates = array( array('tags', 'png', 'XML:com.adobe.xmp'), array('tags', 'jpeg', 'XMP'), array('tags', 'jpeg', 'XML:com.adobe.xmp'), array('tags', 'quicktime', 'XMP'), array('tags', 'quicktime', 'XML:com.adobe.xmp'), array('tags', 'matroska', 'XMP'), array('tags', 'matroska', 'XML:com.adobe.xmp'), array('tags', 'id3v2', 'XMP'), array('xmp', 'data'), ); foreach ($candidates as $keys) { $node = $info; foreach ($keys as $k) { if (!is_array($node) || !isset($node[$k])) { $node = null; break; } $node = $node[$k]; } $packet = self::coerceToString($node); if ($packet !== null && self::isWellFormedXmpPacket($packet)) { return $packet; } } return self::deepFindXmp($info, 0); } private static function coerceToString($node) { if (is_string($node) && strlen($node) > 32) return $node; if (is_array($node)) { foreach ($node as $v) { if (is_string($v) && strlen($v) > 32) return $v; } } return null; } private static function deepFindXmp($node, $depth) { if ($depth > 10) return null; if (is_string($node)) { if (self::isWellFormedXmpPacket($node)) { return $node; } return null; } if (!is_array($node)) return null; foreach ($node as $v) { $found = self::deepFindXmp($v, $depth + 1); if ($found !== null) return $found; } return null; } private static function isWellFormedXmpPacket($s) { if (!is_string($s) || strlen($s) < 64) return false; $head = $s; if (substr($head, 0, 3) === "\xEF\xBB\xBF") { $head = substr($head, 3); } $head = ltrim($head, " \t\r\n"); if (strpos($head, '') !== false); } private static function extractFromIsoBmffUuid($path) { $fh = @fopen($path, 'rb'); if (!$fh) return null; $result = null; $uuidBytes = hex2bin(self::ADOBE_XMP_UUID); while (!feof($fh)) { $hdr = fread($fh, 8); if (strlen($hdr) < 8) break; $unpacked = unpack('Nsize/a4type', $hdr); $size = $unpacked['size']; $type = $unpacked['type']; $header = 8; if ($size === 1) { $big = fread($fh, 8); if (strlen($big) < 8) break; $hi = unpack('N', substr($big, 0, 4))[1]; $lo = unpack('N', substr($big, 4, 4))[1]; $size = ($hi << 32) | $lo; $header = 16; } elseif ($size === 0) { $size = PHP_INT_MAX; } if ($size < $header) break; $payloadLength = $size - $header; if ($type === 'uuid' && $payloadLength >= 16) { $uuid = fread($fh, 16); if (strlen($uuid) < 16) break; if ($uuid === $uuidBytes) { $lenBytes = fread($fh, 4); if (strlen($lenBytes) === 4) { $packetLen = unpack('N', $lenBytes)[1]; $packet = fread($fh, $packetLen); if (strlen($packet) === $packetLen) { $result = $packet; } } break; } fseek($fh, $payloadLength - 16, SEEK_CUR); continue; } fseek($fh, $payloadLength, SEEK_CUR); } fclose($fh); return $result; } public static function parse($packet) { if ($packet === null || $packet === '') return null; $start = strpos($packet, '', $start); if ($end !== false) { $packet = substr($packet, $end + 2); } } } $packet = ltrim($packet, "\xEF\xBB\xBF \t\r\n"); $doc = new DOMDocument('1.0', 'UTF-8'); $prev = libxml_use_internal_errors(true); $loaded = $doc->loadXML($packet, LIBXML_NONET | LIBXML_NOCDATA); libxml_clear_errors(); libxml_use_internal_errors($prev); if (!$loaded) return null; $xpath = new DOMXPath($doc); $xpath->registerNamespace('dc', self::NS_DC); $xpath->registerNamespace('exif', self::NS_EXIF); $xpath->registerNamespace('xmpDM', self::NS_XMPDM); $latRaw = self::firstText($xpath, '//exif:GPSLatitude'); $lonRaw = self::firstText($xpath, '//exif:GPSLongitude'); if ($latRaw === null || $lonRaw === null) return null; $lat = self::parseGps($latRaw); $lon = self::parseGps($lonRaw); if ($lat === null || $lon === null) return null; $shotRaw = self::firstText($xpath, '//xmpDM:ShotDate'); if ($shotRaw === null) { $shotRaw = self::firstText($xpath, '//xmpDM:StartDate'); } if ($shotRaw === null) return null; $ts = strtotime($shotRaw); if ($ts === false) return null; $timecode = self::firstText($xpath, '//xmpDM:StartTimecode'); if ($timecode === null) { $timecode = self::firstText($xpath, '//xmpDM:startTimecode'); } if ($timecode === null) $timecode = ''; $creator = ''; $creatorNodes = $xpath->query('//dc:creator//rdf:li'); if ($creatorNodes !== false) { $parts = array(); foreach ($creatorNodes as $n) { $parts[] = trim($n->textContent); } $creator = implode(', ', $parts); } return array( 'lat' => $lat, 'lon' => $lon, 'date' => (int) $ts, 'date_raw' => (string) $shotRaw, 'timecode' => (string) $timecode, 'creator' => $creator, ); } private static function firstText(DOMXPath $xpath, $query) { $nodes = $xpath->query($query); if ($nodes === false || $nodes->length === 0) return null; $text = trim($nodes->item(0)->textContent); return $text === '' ? null : $text; } private static function parseGps($s) { $s = trim($s); if (!preg_match('/^([0-9.]+),([0-9.]+)([NSEWnsew])$/', $s, $m)) { if (is_numeric($s)) return (float) $s; return null; } $v = (float) $m[1] + (float) $m[2] / 60.0; $hemi = strtoupper($m[3]); if ($hemi === 'S' || $hemi === 'W') $v = -$v; return $v; } } // END_OF_GEOPLOT_XMP