Skip to content

Commit 4d890db

Browse files
committed
Added FFprobe adapter, fix Travis and add additional fields
1 . FFprobe as addtional adapter - FFprobe/FFmpeg can be used as an additional adapter to extract metadata from videos. 2. Travis CI build was failing - Removed tests using PHP5, PHP7.0 and PHP7.1 (all not maintained any more) 3. Added several new fields for extraction
1 parent 43c116e commit 4d890db

23 files changed

+1924
-373
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ phpunit.xml
33
tests/log
44
vendor
55
composer.phar
6+
.DS_Store

Diff for: composer.json

+11-8
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@
1010
"role": "Developer"
1111
}
1212
],
13-
"keywords": ["EXIF", "IPTC", "jpeg", "tiff", "exiftool"],
13+
"keywords": ["EXIF", "IPTC", "jpeg", "tiff", "exiftool", "FFmpeg", "FFprobe"],
1414
"require": {
15-
"php": ">=5.4"
15+
"php": ">=7.1",
16+
"php-ffmpeg/php-ffmpeg": "^0.14.0"
1617
},
1718
"require-dev": {
1819
"jakub-onderka/php-parallel-lint": "^1.0",
19-
"phpmd/phpmd": "~2.2",
20-
"phpunit/phpunit": ">=4.0 <6.0",
21-
"satooshi/php-coveralls": "~0.6",
22-
"sebastian/phpcpd": "1.4.*@stable",
23-
"squizlabs/php_codesniffer": "1.4.*@stable"
20+
"php-coveralls/php-coveralls": "^2.2",
21+
"phpmd/phpmd": "^2.7",
22+
"phpunit/phpunit": ">=8.4",
23+
"sebastian/phpcpd": "^4.1",
24+
"friendsofphp/php-cs-fixer": "^2.16",
25+
"squizlabs/php_codesniffer": "^3.5"
2426
},
2527
"suggest": {
2628
"lib-exiftool": "Use perl lib exiftool as adapter",
27-
"ext-exif": "Use exif PHP extension as adapter"
29+
"ext-exif": "Use exif PHP extension as adapter",
30+
"FFmpeg": "Use FFmpeg/FFprobe as adapter"
2831
},
2932
"autoload": {
3033
"psr-0": {

Diff for: lib/PHPExif/Adapter/FFprobe.php

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
/**
3+
* PHP Exif Native Reader Adapter
4+
*
5+
* @link http://github.com/miljar/PHPExif for the canonical source repository
6+
* @copyright Copyright (c) 2013 Tom Van Herreweghe <[email protected]>
7+
* @license http://github.com/miljar/PHPExif/blob/master/LICENSE MIT License
8+
* @category PHPExif
9+
* @package Reader
10+
*/
11+
12+
namespace PHPExif\Adapter;
13+
14+
use PHPExif\Exif;
15+
use FFMpeg;
16+
17+
/**
18+
* PHP Exif Native Reader Adapter
19+
*
20+
* Uses native PHP functionality to read data from a file
21+
*
22+
* @category PHPExif
23+
* @package Reader
24+
*/
25+
class FFprobe extends AdapterAbstract
26+
{
27+
28+
/**
29+
* @var string
30+
*/
31+
protected $mapperClass = '\\PHPExif\\Mapper\\FFprobe';
32+
33+
34+
/**
35+
* Reads & parses the EXIF data from given file
36+
*
37+
* @param string $file
38+
* @return \PHPExif\Exif|boolean Instance of Exif object with data
39+
*/
40+
public function getExifFromFile($file)
41+
{
42+
$mimeType = mime_content_type($file);
43+
44+
// file is not a video -> wrong adapter
45+
if (strpos($mimeType, 'video') !== 0) {
46+
return false;
47+
}
48+
49+
50+
$ffprobe = FFMpeg\FFProbe::create();
51+
52+
$stream = $ffprobe->streams($file)->videos()->first()->all();
53+
$format = $ffprobe->format($file)->all();
54+
55+
$data = array_merge($stream, $format, array('MimeType' => $mimeType, 'filesize' => filesize($file)));
56+
57+
58+
// map the data:
59+
$mapper = $this->getMapper();
60+
$mappedData = $mapper->mapRawData($data);
61+
62+
// hydrate a new Exif object
63+
$exif = new Exif();
64+
$hydrator = $this->getHydrator();
65+
$hydrator->hydrate($exif, $mappedData);
66+
$exif->setRawData($data);
67+
68+
return $exif;
69+
}
70+
}

Diff for: lib/PHPExif/Adapter/Native.php

+27-191
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,18 @@ class Native extends AdapterAbstract
7171
* @var array
7272
*/
7373
protected $iptcMapping = array(
74-
'title' => '2#005',
75-
'keywords' => '2#025',
76-
'copyright' => '2#116',
77-
'caption' => '2#120',
78-
'headline' => '2#105',
79-
'credit' => '2#110',
80-
'source' => '2#115',
81-
'jobtitle' => '2#085'
74+
'title' => '2#005',
75+
'keywords' => '2#025',
76+
'copyright' => '2#116',
77+
'caption' => '2#120',
78+
'headline' => '2#105',
79+
'credit' => '2#110',
80+
'source' => '2#115',
81+
'jobtitle' => '2#085',
82+
'city' => '2#090',
83+
'sublocation' => '2#092',
84+
'state' => '2#095',
85+
'country' => '2#101'
8286
);
8387

8488

@@ -176,38 +180,27 @@ public function getExifFromFile($file)
176180
{
177181
$mimeType = mime_content_type($file);
178182

179-
if (strpos($mimeType, 'video') !== 0) {
180183

181-
// Photo
182-
$sections = $this->getRequiredSections();
183-
$sections = implode(',', $sections);
184-
$sections = (empty($sections)) ? null : $sections;
185184

186-
$data = @exif_read_data(
187-
$file,
188-
$sections,
189-
$this->getSectionsAsArrays(),
190-
$this->getIncludeThumbnail()
191-
);
185+
// Photo
186+
$sections = $this->getRequiredSections();
187+
$sections = implode(',', $sections);
188+
$sections = (empty($sections)) ? null : $sections;
192189

193-
if (false === $data) {
194-
return false;
195-
}
190+
$data = @exif_read_data(
191+
$file,
192+
$sections,
193+
$this->getSectionsAsArrays(),
194+
$this->getIncludeThumbnail()
195+
);
196196

197-
$xmpData = $this->getIptcData($file);
198-
$data = array_merge($data, array(self::SECTION_IPTC => $xmpData));
199-
200-
} else {
201-
// Video
202-
try {
197+
if (false === $data) {
198+
return false;
199+
}
203200

204-
$data = $this->getVideoData($file);
205-
$data['MimeType'] = $mimeType;
201+
$xmpData = $this->getIptcData($file);
202+
$data = array_merge($data, array(self::SECTION_IPTC => $xmpData));
206203

207-
} catch (Exception $exception) {
208-
Logs::error(__METHOD__, __LINE__, $exception->getMessage());
209-
}
210-
}
211204

212205
// map the data:
213206
$mapper = $this->getMapper();
@@ -222,163 +215,6 @@ public function getExifFromFile($file)
222215
return $exif;
223216
}
224217

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-
}
382218

383219
/**
384220
* Returns an array of IPTC data

0 commit comments

Comments
 (0)