summaryrefslogtreecommitdiffstats
path: root/lib/private/Metadata/Provider/ExifProvider.php
blob: aa0b5464682165739c81701f0fe5e86142b9a05c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?php

namespace OC\Metadata\Provider;

use OC\Metadata\FileMetadata;
use OC\Metadata\IMetadataProvider;
use OCP\Files\File;
use Psr\Log\LoggerInterface;

class ExifProvider implements IMetadataProvider {
	private LoggerInterface $logger;

	public function __construct(
		LoggerInterface $logger
	) {
		$this->logger = $logger;
	}

	public static function groupsProvided(): array {
		return ['size', 'gps'];
	}

	public static function isAvailable(): bool {
		return extension_loaded('exif');
	}

	public function execute(File $file): array {
		$exifData = [];
		$fileDescriptor = $file->fopen('rb');

		$data = null;
		try {
			// Needed to make reading exif data reliable.
			// This is to trigger this condition: https://github.com/php/php-src/blob/d64aa6f646a7b5e58359dc79479860164239580a/main/streams/streams.c#L710
			// But I don't understand why 1 as a special meaning.
			// Revert right after reading the exif data.
			$oldBufferSize = stream_set_chunk_size($fileDescriptor, 1);
			$data = exif_read_data($fileDescriptor, 'ANY_TAG', true);
			stream_set_chunk_size($fileDescriptor, $oldBufferSize);
		} catch (\Exception $ex) {
			$this->logger->warning("Couldn't extract metadata for ".$file->getId(), ['exception' => $ex]);
		}

		$size = new FileMetadata();
		$size->setGroupName('size');
		$size->setId($file->getId());
		$size->setMetadata([]);

		if (!$data) {
			$sizeResult = getimagesizefromstring($file->getContent());
			if ($sizeResult !== false) {
				$size->setMetadata([
					'width' => $sizeResult[0],
					'height' => $sizeResult[1],
				]);

				$exifData['size'] = $size;
			}
		} elseif (array_key_exists('COMPUTED', $data)) {
			if (array_key_exists('Width', $data['COMPUTED']) && array_key_exists('Height', $data['COMPUTED'])) {
				$size->setMetadata([
					'width' => $data['COMPUTED']['Width'],
					'height' => $data['COMPUTED']['Height'],
				]);

				$exifData['size'] = $size;
			}
		}

		if ($data && array_key_exists('GPS', $data)
			&& array_key_exists('GPSLatitude', $data['GPS']) && array_key_exists('GPSLatitudeRef', $data['GPS'])
			&& array_key_exists('GPSLongitude', $data['GPS']) && array_key_exists('GPSLongitudeRef', $data['GPS'])
		) {
			$gps = new FileMetadata();
			$gps->setGroupName('gps');
			$gps->setId($file->getId());
			$gps->setMetadata([
				'latitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLatitude'], $data['GPS']['GPSLatitudeRef']),
				'longitude' => $this->gpsDegreesToDecimal($data['GPS']['GPSLongitude'], $data['GPS']['GPSLongitudeRef']),
			]);

			$exifData['gps'] = $gps;
		}

		return $exifData;
	}

	public static function getMimetypesSupported(): string {
		return '/image\/.*/';
	}

	/**
	 * @param array|string $coordinates
	 */
	private static function gpsDegreesToDecimal($coordinates, ?string $hemisphere): float {
		if (is_string($coordinates)) {
			$coordinates = array_map("trim", explode(",", $coordinates));
		}

		if (count($coordinates) !== 3) {
			throw new \Exception('Invalid coordinate format: ' . json_encode($coordinates));
		}

		[$degrees, $minutes, $seconds] = array_map(function (string $rawDegree) {
			$parts = explode('/', $rawDegree);

			if ($parts[1] === '0') {
				return 0;
			}

			return floatval($parts[0]) / floatval($parts[1] ?? 1);
		}, $coordinates);

		$sign = ($hemisphere === 'W' || $hemisphere === 'S') ? -1 : 1;
		return $sign * ($degrees + $minutes / 60 + $seconds / 3600);
	}
}