Skip to content

Commit abd5b8e

Browse files
committed
Added extraction of many additional fields and video support
* Added extraction for many additional fields * Refactored in parts internal calculation of GPS location data * Added support for video (FFMpeg for native)
1 parent 41f23db commit abd5b8e

File tree

4 files changed

+723
-83
lines changed

4 files changed

+723
-83
lines changed

lib/PHPExif/Adapter/Native.php

Lines changed: 192 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace PHPExif\Adapter;
1313

1414
use PHPExif\Exif;
15+
use FFMpeg;
1516

1617
/**
1718
* PHP Exif Native Reader Adapter
@@ -173,23 +174,40 @@ public function getSectionsAsArrays()
173174
*/
174175
public function getExifFromFile($file)
175176
{
176-
$sections = $this->getRequiredSections();
177-
$sections = implode(',', $sections);
178-
$sections = (empty($sections)) ? null : $sections;
179-
180-
$data = @exif_read_data(
181-
$file,
182-
$sections,
183-
$this->getSectionsAsArrays(),
184-
$this->getIncludeThumbnail()
185-
);
186-
187-
if (false === $data) {
188-
return false;
189-
}
177+
$mimeType = mime_content_type($file);
178+
179+
if (strpos($mimeType, 'video') !== 0) {
180+
181+
// Photo
182+
$sections = $this->getRequiredSections();
183+
$sections = implode(',', $sections);
184+
$sections = (empty($sections)) ? null : $sections;
185+
186+
$data = @exif_read_data(
187+
$file,
188+
$sections,
189+
$this->getSectionsAsArrays(),
190+
$this->getIncludeThumbnail()
191+
);
192+
193+
if (false === $data) {
194+
return false;
195+
}
196+
197+
$xmpData = $this->getIptcData($file);
198+
$data = array_merge($data, array(self::SECTION_IPTC => $xmpData));
190199

191-
$xmpData = $this->getIptcData($file);
192-
$data = array_merge($data, array(self::SECTION_IPTC => $xmpData));
200+
} else {
201+
// Video
202+
try {
203+
204+
$data = $this->getVideoData($file);
205+
$data['MimeType'] = $mimeType;
206+
207+
} catch (Exception $exception) {
208+
Logs::error(__METHOD__, __LINE__, $exception->getMessage());
209+
}
210+
}
193211

194212
// map the data:
195213
$mapper = $this->getMapper();
@@ -204,6 +222,164 @@ public function getExifFromFile($file)
204222
return $exif;
205223
}
206224

225+
/**
226+
* Returns an array of video data
227+
*
228+
* @param string $file The file to read the video data from
229+
* @return array
230+
*/
231+
public function getVideoData($filename)
232+
{
233+
234+
$metadata['FileSize'] = filesize($filename);
235+
236+
$path_ffmpeg = exec('which ffmpeg');
237+
$path_ffprobe = exec('which ffprobe');
238+
$ffprobe = FFMpeg\FFProbe::create(array(
239+
'ffmpeg.binaries' => $path_ffmpeg,
240+
'ffprobe.binaries' => $path_ffprobe,
241+
));
242+
243+
$stream = $ffprobe->streams($filename)->videos()->first()->all();
244+
$format = $ffprobe->format($filename)->all();
245+
if (isset($stream['width'])) {
246+
$metadata['Width'] = $stream['width'];
247+
}
248+
if (isset($stream['height'])) {
249+
$metadata['Height'] = $stream['height'];
250+
}
251+
if (isset($stream['tags']) && isset($stream['tags']['rotate']) && ($stream['tags']['rotate'] === '90' || $stream['tags']['rotate'] === '270')) {
252+
$tmp = $metadata['Width'];
253+
$metadata['Width'] = $metadata['Height'];
254+
$metadata['Height'] = $tmp;
255+
}
256+
if (isset($stream['avg_frame_rate'])) {
257+
$framerate = explode('/', $stream['avg_frame_rate']);
258+
if (count($framerate) == 1) {
259+
$framerate = $framerate[0];
260+
} elseif (count($framerate) == 2 && $framerate[1] != 0) {
261+
$framerate = number_format($framerate[0] / $framerate[1], 3);
262+
} else {
263+
$framerate = '';
264+
}
265+
if ($framerate !== '') {
266+
$metadata['framerate'] = $framerate;
267+
}
268+
}
269+
if (isset($format['duration'])) {
270+
$metadata['duration'] = number_format($format['duration'], 3);
271+
}
272+
if (isset($format['tags'])) {
273+
if (isset($format['tags']['creation_time']) && strtotime($format['tags']['creation_time']) !== 0) {
274+
$metadata['DateTimeOriginal'] = date('Y-m-d H:i:s', strtotime($format['tags']['creation_time']));
275+
}
276+
if (isset($format['tags']['location'])) {
277+
$matches = [];
278+
preg_match('/^([+-][0-9\.]+)([+-][0-9\.]+)\/$/', $format['tags']['location'], $matches);
279+
if (count($matches) == 3 &&
280+
!preg_match('/^\+0+\.0+$/', $matches[1]) &&
281+
!preg_match('/^\+0+\.0+$/', $matches[2])) {
282+
$metadata['GPSLatitude'] = $matches[1];
283+
$metadata['GPSLongitude'] = $matches[2];
284+
}
285+
}
286+
// QuickTime File Format defines several additional metadata
287+
// Source: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
288+
// Special case: iPhones write into tags->creation_time the creation time of the file
289+
// -> When converting the video from HEVC (iOS Video format) to MOV, the creation_time
290+
// is the time when the mov file was created, not when the video was shot (fixed in iOS12)
291+
// (see e.g. https://michaelkummer.com/tech/apple/photos-videos-wrong-date/ (for the symptom)
292+
// Solution: Use com.apple.quicktime.creationdate which is the true creation date of the video
293+
if (isset($format['tags']['com.apple.quicktime.creationdate'])) {
294+
$metadata['DateTimeOriginal'] = date('Y-m-d H:i:s', strtotime($format['tags']['com.apple.quicktime.creationdate']));
295+
}
296+
if (isset($format['tags']['com.apple.quicktime.description'])) {
297+
$metadata['description'] = $format['tags']['com.apple.quicktime.description'];
298+
}
299+
if (isset($format['tags']['com.apple.quicktime.title'])) {
300+
$metadata['title'] = $format['tags']['com.apple.quicktime.title'];
301+
}
302+
if (isset($format['tags']['com.apple.quicktime.keywords'])) {
303+
$metadata['keywords'] = $format['tags']['com.apple.quicktime.keywords'];
304+
}
305+
if (isset($format['tags']['com.apple.quicktime.location.ISO6709'])) {
306+
$location_data = $this->readISO6709($format['tags']['com.apple.quicktime.location.ISO6709']);
307+
$metadata['GPSLatitude'] = $location_data['latitude'];
308+
$metadata['GPSLongitude'] = $location_data['longitude'];
309+
$metadata['GPSAltitude'] = $location_data['altitude'];
310+
}
311+
// Not documented, but available on iPhone videos
312+
if (isset($format['tags']['com.apple.quicktime.make'])) {
313+
$metadata['Make'] = $format['tags']['com.apple.quicktime.make'];
314+
}
315+
// Not documented, but available on iPhone videos
316+
if (isset($format['tags']['com.apple.quicktime.model'])) {
317+
$metadata['Model'] = $format['tags']['com.apple.quicktime.model'];
318+
}
319+
}
320+
321+
return $metadata;
322+
}
323+
324+
/**
325+
* Converts results of ISO6709 parsing
326+
* to decimal format for latitude and longitude
327+
* See https://github.com/seanson/python-iso6709.git.
328+
*
329+
* @param string sign
330+
* @param string degrees
331+
* @param string minutes
332+
* @param string seconds
333+
* @param string fraction
334+
*
335+
* @return float
336+
*/
337+
private function convertDMStoDecimal(string $sign, string $degrees, string $minutes, string $seconds, string $fraction): float
338+
{
339+
if ($fraction !== '') {
340+
if ($seconds !== '') {
341+
$seconds = $seconds . $fraction;
342+
} elseif ($minutes !== '') {
343+
$minutes = $minutes . $fraction;
344+
} else {
345+
$degrees = $degrees . $fraction;
346+
}
347+
}
348+
$decimal = floatval($degrees) + floatval($minutes) / 60.0 + floatval($seconds) / 3600.0;
349+
if ($sign == '-') {
350+
$decimal = -1.0 * $decimal;
351+
}
352+
return $decimal;
353+
}
354+
355+
/**
356+
* Returns the latitude, longitude and altitude
357+
* of a GPS coordiante formattet with ISO6709
358+
* See https://github.com/seanson/python-iso6709.git.
359+
*
360+
* @param string val_ISO6709
361+
*
362+
* @return array
363+
*/
364+
private function readISO6709(string $val_ISO6709): array
365+
{
366+
$return = [
367+
'latitude' => null,
368+
'longitude' => null,
369+
'altitude' => null,
370+
];
371+
$matches = [];
372+
// Adjustment compared to https://github.com/seanson/python-iso6709.git
373+
// Altitude have format +XX.XXXX -> Adjustment for decimal
374+
preg_match('/^(?<lat_sign>\+|-)(?<lat_degrees>[0,1]?\d{2})(?<lat_minutes>\d{2}?)?(?<lat_seconds>\d{2}?)?(?<lat_fraction>\.\d+)?(?<lng_sign>\+|-)(?<lng_degrees>[0,1]?\d{2})(?<lng_minutes>\d{2}?)?(?<lng_seconds>\d{2}?)?(?<lng_fraction>\.\d+)?(?<alt>[\+\-][0-9]\d*(\.\d+)?)?\/$/', $val_ISO6709, $matches);
375+
$return['latitude'] = $this->convertDMStoDecimal($matches['lat_sign'], $matches['lat_degrees'], $matches['lat_minutes'], $matches['lat_seconds'], $matches['lat_fraction']);
376+
$return['longitude'] = $this->convertDMStoDecimal($matches['lng_sign'], $matches['lng_degrees'], $matches['lng_minutes'], $matches['lng_seconds'], $matches['lng_fraction']);
377+
if (isset($matches['alt'])) {
378+
$return['altitude'] = doubleval($matches['alt']);
379+
}
380+
return $return;
381+
}
382+
207383
/**
208384
* Returns an array of IPTC data
209385
*

0 commit comments

Comments
 (0)