diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ff1217e..bb4f1692 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- **Feature**: RAW files are now hidden (stacked) when another file with the same basename exists ([#537](https://github.com/pulsejet/memories/issues/537), [#152](https://github.com/pulsejet/memories/issues/152), [#419](https://github.com/pulsejet/memories/issues/419))
+- **Feature**: Bulk rotating of images. You can now rotate images losslessly by editing the rotation EXIF metadata. ([#856](https://github.com/pulsejet/memories/issues/856))
- **Feature**: Icon animation when playing live photos ([#898](https://github.com/pulsejet/memories/issues/898))
- **Feature**: Swipe to refresh on timeline ([#547](https://github.com/pulsejet/memories/issues/547))
- **Bugfix**: Allow switching video to direct on Safari ([#650](https://github.com/pulsejet/memories/issues/650))
diff --git a/lib/Controller/ImageController.php b/lib/Controller/ImageController.php
index 77a28d73..5e2c7a05 100644
--- a/lib/Controller/ImageController.php
+++ b/lib/Controller/ImageController.php
@@ -259,7 +259,12 @@ class ImageController extends GenericApiController
// Set the exif data
Exif::setFileExif($file, $raw);
- return new JSONResponse([], Http::STATUS_OK);
+ // If rotation changed then update the previews
+ if ($raw['Orientation'] ?? false) {
+ $this->deletePreviews($file);
+ }
+
+ return $this->info($id, true);
});
}
@@ -452,4 +457,22 @@ class ImageController extends GenericApiController
// Get the tag names
return array_map(static fn ($t) => $t->getName(), $visible);
}
+
+ /**
+ * Invalidate previews for a file.
+ */
+ private function deletePreviews(\OCP\Files\File $file): void
+ {
+ try {
+ $previewRoot = new \OC\Preview\Storage\Root(
+ \OC::$server->get(IRootFolder::class),
+ \OC::$server->get(\OC\SystemConfig::class),
+ );
+
+ $fileId = (string) $file->getId();
+ $previewRoot->getFolder($fileId)->delete();
+ } catch (\Exception $e) {
+ return;
+ }
+ }
}
diff --git a/lib/Exif.php b/lib/Exif.php
index 1d085e66..30fc9396 100644
--- a/lib/Exif.php
+++ b/lib/Exif.php
@@ -334,7 +334,7 @@ class Exif
$data['SourceFile'] = $path;
$raw = json_encode([$data], JSON_UNESCAPED_UNICODE);
$cmd = array_merge(self::getExiftool(), [
- '-overwrite_original',
+ '-overwrite_original', '-n',
'-api', 'LargeFileSupport=1',
'-json=-', $path,
]);
diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue
index 3b543334..9067adde 100644
--- a/src/components/SelectionManager.vue
+++ b/src/components/SelectionManager.vue
@@ -62,6 +62,7 @@ import MoveIcon from 'vue-material-design-icons/ImageMove.vue';
import AlbumsIcon from 'vue-material-design-icons/ImageAlbum.vue';
import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue';
import FolderMoveIcon from 'vue-material-design-icons/FolderMove.vue';
+import RotateLeftIcon from 'vue-material-design-icons/RotateLeft.vue';
import type { IDay, IHeadRow, IPhoto, IRow } from '@typings';
@@ -231,6 +232,11 @@ export default defineComponent({
icon: EditFileIcon,
callback: this.editMetadataSelection.bind(this),
},
+ {
+ name: t('memories', 'Rotate / Flip'),
+ icon: RotateLeftIcon,
+ callback: () => this.editMetadataSelection(this.selection, [5]),
+ },
{
name: t('memories', 'View in folder'),
icon: OpenInNewIcon,
diff --git a/src/components/modal/EditMetadataModal.vue b/src/components/modal/EditMetadataModal.vue
index 9cd5459b..3f7e07bf 100644
--- a/src/components/modal/EditMetadataModal.vue
+++ b/src/components/modal/EditMetadataModal.vue
@@ -39,6 +39,13 @@