Skip to content

Commit

Permalink
Use new metadata API
Browse files Browse the repository at this point in the history
Signed-off-by: Louis Chemineau <louis@chmn.me>
  • Loading branch information
artonge committed Oct 26, 2023
1 parent e7243b2 commit bfc41f0
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 3 deletions.
12 changes: 9 additions & 3 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@
use OCA\Photos\Listener\PlaceManagerEventListener;
use OCA\Photos\Listener\SabrePluginAuthInitListener;
use OCA\Photos\Listener\TagListener;
use OCA\Photos\MetadataProvider\ExifMetadataProvider;
use OCA\Photos\MetadataProvider\OriginalDateTimeMetadataProvider;
use OCA\Photos\MetadataProvider\SizeMetadataProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\FilesMetadata\Event\MetadataLiveEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\Share\Events\ShareDeletedEvent;
Expand Down Expand Up @@ -75,8 +78,11 @@ public function register(IRegistrationContext $context): void {
/** Register $principalBackend for the DAV collection */
$context->registerServiceAlias('principalBackend', Principal::class);

// Priority of -1 to be triggered after event listeners populating metadata.
$context->registerEventListener(NodeWrittenEvent::class, PlaceManagerEventListener::class, -1);
// Metadata
$context->registerEventListener(MetadataLiveEvent::class, ExifMetadataProvider::class, 1);
$context->registerEventListener(MetadataLiveEvent::class, SizeMetadataProvider::class);
$context->registerEventListener(MetadataLiveEvent::class, OriginalDateTimeMetadataProvider::class);
$context->registerEventListener(MetadataLiveEvent::class, PlaceManagerEventListener::class, -1);

$context->registerEventListener(NodeDeletedEvent::class, AlbumsManagementEventListener::class);
$context->registerEventListener(UserRemovedEvent::class, AlbumsManagementEventListener::class);
Expand Down
136 changes: 136 additions & 0 deletions lib/MetadataProvider/ExifMetadataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
* @license AGPL-3.0-or-later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OCA\Photos\MetadataProvider;

use OCA\Photos\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\File;
use OCP\FilesMetadata\Event\MetadataLiveEvent;
use Psr\Log\LoggerInterface;

/**
* Extract EXIF, IFD0, and GPS data from a picture file.
* EXIF data reference: https://web.archive.org/web/20220428165430/exif.org/Exif2-2.PDF
*
* @template-implements IEventListener<OCP\FilesMetadata\Event\MetadataLiveEvent>
*/
class ExifMetadataProvider implements IEventListener {
public function __construct(
private LoggerInterface $logger
) {
}

public function handle(Event $event): void {
if (!($event instanceof MetadataLiveEvent)) {
return;
}

$node = $event->getNode();

if (!$node instanceof File) {
return;
}

$path = $node->getPath();
if (str_starts_with($path, 'appdata_') || str_starts_with($path, 'files_versions/') || str_starts_with($path, 'files_trashbin/')) {
return;
}

if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
return;
}

if (!extension_loaded('exif')) {
return;
}

$fileDescriptor = $node->fopen('rb');
if ($fileDescriptor === false) {
return;
}

$rawExifData = null;

try {
// HACK: The stream_set_chunk_size call is 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 yet why 1 as a special meaning.
$oldBufferSize = stream_set_chunk_size($fileDescriptor, 1);
$rawExifData = @exif_read_data($fileDescriptor, 'EXIF, GPS', true);
// We then revert the change after having read the exif data.
stream_set_chunk_size($fileDescriptor, $oldBufferSize);
} catch (\Exception $ex) {
$this->logger->info("Failed to extract metadata for " . $node->getId(), ['exception' => $ex]);
}

if ($rawExifData && array_key_exists('EXIF', $rawExifData)) {
$event->getMetadata()->setArray('photos-exif', $rawExifData['EXIF']);
}

if ($rawExifData && array_key_exists('IFD0', $rawExifData)) {
$event->getMetadata()->setArray('photos-ifd0', $rawExifData['IFD0']);
}

if (
$rawExifData &&
array_key_exists('GPS', $rawExifData) &&
array_key_exists('GPSLatitude', $rawExifData['GPS']) && array_key_exists('GPSLatitudeRef', $rawExifData['GPS']) &&
array_key_exists('GPSLongitude', $rawExifData['GPS']) && array_key_exists('GPSLongitudeRef', $rawExifData['GPS'])
) {
$event->getMetadata()->setArray('photos-gps', [
'latitude' => $this->gpsDegreesToDecimal($rawExifData['GPS']['GPSLatitude'], $rawExifData['GPS']['GPSLatitudeRef']),
'longitude' => $this->gpsDegreesToDecimal($rawExifData['GPS']['GPSLongitude'], $rawExifData['GPS']['GPSLongitudeRef']),
'altitude' => ($rawExifData['GPS']['GPSAltitudeRef'] === "\u{0000}" ? 1 : -1) * $this->parseGPSData($rawExifData['GPS']['GPSAltitude']),
]);
}
}

/**
* @param array|string $coordinates
*/
private 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(fn ($rawDegree) => $this->parseGPSData($rawDegree), $coordinates);

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

private function parseGPSData(string $rawData): float {
$parts = explode('/', $rawData);

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

return floatval($parts[0]) / floatval($parts[1] ?? 1);
}
}
94 changes: 94 additions & 0 deletions lib/MetadataProvider/OriginalDateTimeMetadataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
* @license AGPL-3.0-or-later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OCA\Photos\MetadataProvider;

use DateTime;
use OCA\Photos\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\File;
use OCP\FilesMetadata\Event\MetadataLiveEvent;

/**
* @template-implements IEventListener<OCP\FilesMetadata\Event\MetadataLiveEvent>
*/
class OriginalDateTimeMetadataProvider implements IEventListener {
public function __construct() {
}

public function handle(Event $event): void {
if (!($event instanceof MetadataLiveEvent)) {
return;
}

$node = $event->getNode();

if (!$node instanceof File) {
return;
}

$path = $node->getPath();
if (str_starts_with($path, 'appdata_') || str_starts_with($path, 'files_versions/') || str_starts_with($path, 'files_trashbin/')) {
return;
}

if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
return;
}

$metadata = $event->getMetadata();

if ($metadata->hasKey('photos-exif') && array_key_exists('DateTimeOriginal', $metadata->getArray('photos-exif'))) {
$rawDateTimeOriginal = $metadata->getArray('photos-exif')['DateTimeOriginal'];
$dateTimeOriginal = DateTime::createFromFormat("Y:m:d G:i:s", $rawDateTimeOriginal);
$metadata->setInt('photos-original_date_time', $dateTimeOriginal->getTimestamp(), true);
return;
}

$name = $node->getName();
$matches = [];

$matchesCount = preg_match('/^IMG_([0-9]{8}_[0-9]{6})/', $name, $matches);
if ($matchesCount > 0) {
$dateTimeOriginal = DateTime::createFromFormat("Ymd_Gis", $matches[1]);
$metadata->setInt('photos-original_date_time', $dateTimeOriginal->getTimestamp(), true);
return;
}

$matchesCount = preg_match('/^PANO_([0-9]{8}_[0-9]{6})/', $name, $matches);
if ($matchesCount > 0) {
$dateTimeOriginal = DateTime::createFromFormat("Ymd_Gis", $matches[1]);
$metadata->setInt('photos-original_date_time', $dateTimeOriginal->getTimestamp(), true);
return;
}

$matchesCount = preg_match('/^([0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{4})/', $name, $matches);
if ($matchesCount > 0) {
$dateTimeOriginal = DateTime::createFromFormat("Y-m-d-G-i-s", $matches[1]);
$metadata->setInt('photos-original_date_time', $dateTimeOriginal->getTimestamp(), true);
return;
}

$metadata->setInt('photos-original_date_time', $node->getMTime(), true);
}
}
62 changes: 62 additions & 0 deletions lib/MetadataProvider/PlaceMetadataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright 2023 Louis Chmn <louis@chmn.me>
* @license AGPL-3.0-or-later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OC\FilesMetadata\Provider;

use DateTime;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\File;
use OCP\FilesMetadata\Event\MetadataLiveEvent;

class OriginalDateTimeMetadataProvider implements IEventListener {
public function __construct() {
}

public function handle(Event $event): void {
if ($event instanceof MetadataLiveEvent) {
$node = $event->getNode();

if (!$node instanceof File) {
return;
}

if (!preg_match('/image\/(png|jpeg|heif|webp|tiff)/', $node->getMimetype())) {
return;
}

$event->requestBackgroundJob();
return;
}

if (!($event instanceof MetadataBackgroundEvent)) {
return;
}

return;

$metadata = $event->getMetadata();

$this->mediaPlaceManager->setPlaceForFile($fileId);

$metadata->set('photos-place', '', true)
}
}
72 changes: 72 additions & 0 deletions lib/MetadataProvider/SizeMetadataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
* @license AGPL-3.0-or-later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OCA\Photos\MetadataProvider;

use OCA\Photos\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\File;
use OCP\FilesMetadata\Event\MetadataLiveEvent;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<OCP\FilesMetadata\Event\MetadataLiveEvent>
*/
class SizeMetadataProvider implements IEventListener {
public function __construct(
private LoggerInterface $logger
) {
}

public function handle(Event $event): void {
if (!($event instanceof MetadataLiveEvent)) {
return;
}

$node = $event->getNode();

if (!$node instanceof File) {
return;
}

$path = $node->getPath();
if (str_starts_with($path, 'appdata_') || str_starts_with($path, 'files_versions/') || str_starts_with($path, 'files_trashbin/')) {
return;
}

if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
return;
}

$size = getimagesizefromstring($node->getContent());

if ($size === false) {
return;
}

$event->getMetadata()->setArray('photos-size', [
'width' => $size[0],
'height' => $size[1],
]);
}
}

0 comments on commit bfc41f0

Please sign in to comment.