1212namespace PHPExif \Adapter ;
1313
1414use 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