From 253b09fb22ceea23ec3b0a0ef57daba45a9b0a72 Mon Sep 17 00:00:00 2001 From: tognee Date: Sun, 18 Jun 2023 13:35:17 +0200 Subject: [PATCH 1/4] Implemented rotate image feature --- api/exif_tags.py | 1 + ...hoto_localorientation_photo_orientation.py | 23 +++++++ api/models/photo.py | 60 +++++++++++++++++++ api/thumbnails.py | 12 +++- api/util.py | 55 ++++++++++++++++- api/views/photos.py | 20 +++++++ ownphotos/urls.py | 1 + 7 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 api/migrations/0050_photo_localorientation_photo_orientation.py diff --git a/api/exif_tags.py b/api/exif_tags.py index c439b1f7ea..a78617ef1d 100644 --- a/api/exif_tags.py +++ b/api/exif_tags.py @@ -2,6 +2,7 @@ class Tags: RATING = "Rating" IMAGE_HEIGHT = "ImageHeight" IMAGE_WIDTH = "ImageWidth" + ORIENTATION = "Orientation" DATE_TIME_ORIGINAL = "EXIF:DateTimeOriginal" DATE_TIME = "EXIF:DateTime" QUICKTIME_CREATE_DATE = "QuickTime:CreateDate" diff --git a/api/migrations/0050_photo_localorientation_photo_orientation.py b/api/migrations/0050_photo_localorientation_photo_orientation.py new file mode 100644 index 0000000000..309f13807c --- /dev/null +++ b/api/migrations/0050_photo_localorientation_photo_orientation.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-18 08:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0049_fix_metadata_files_as_main_files'), + ] + + operations = [ + migrations.AddField( + model_name='photo', + name='localOrientation', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='photo', + name='orientation', + field=models.IntegerField(default=1), + ), + ] diff --git a/api/models/photo.py b/api/models/photo.py index 1a06c1a7f5..96d69ab19a 100644 --- a/api/models/photo.py +++ b/api/models/photo.py @@ -90,6 +90,9 @@ class Photo(models.Model): focalLength35Equivalent = models.IntegerField(blank=True, null=True) subjectDistance = models.FloatField(blank=True, null=True) digitalZoomRatio = models.FloatField(blank=True, null=True) + orientation = models.IntegerField(default=1) + + localOrientation = models.IntegerField(default=1) owner = models.ForeignKey( User, on_delete=models.SET(get_deleted_user), default=None @@ -151,8 +154,12 @@ def _save_metadata(self, modified_fields=None, use_sidecar=True): if "timestamp" in modified_fields: # To-Do: Only works for files and not for the sidecar file tags_to_write[Tags.DATE_TIME] = self.timestamp + if "orientation" in modified_fields: + tags_to_write[Tags.ORIENTATION] = self.orientation if tags_to_write: util.write_metadata(self.main_file, tags_to_write, use_sidecar=use_sidecar) + if "orientation" in modified_fields: + self._regenerate_thumbnail(commit=False) def _generate_captions_im2txt(self, commit=True): image_path = self.thumbnail_big.path @@ -264,6 +271,7 @@ def _generate_thumbnail(self, commit=True): outputPath="thumbnails_big", hash=self.image_hash, fileType=".webp", + orientation=self.localOrientation, ) else: createThumbnailForVideo( @@ -282,6 +290,7 @@ def _generate_thumbnail(self, commit=True): outputPath="square_thumbnails", hash=self.image_hash, fileType=".webp", + orientation=self.localOrientation, ) if self.video and not doesVideoThumbnailExists( "square_thumbnails", self.image_hash @@ -303,6 +312,7 @@ def _generate_thumbnail(self, commit=True): outputPath="square_thumbnails_small", hash=self.image_hash, fileType=".webp", + orientation=self.localOrientation, ) if self.video and not doesVideoThumbnailExists( "square_thumbnails_small", self.image_hash @@ -334,6 +344,22 @@ def _generate_thumbnail(self, commit=True): ) raise e + def _regenerate_thumbnail(self, commit=True): + if doesStaticThumbnailExists("thumbnails_big", self.image_hash): + os.remove(self.thumbnail_big.path) + if doesStaticThumbnailExists("square_thumbnails", self.image_hash): + os.remove(self.square_thumbnail.path) + if doesStaticThumbnailExists("square_thumbnails_small", self.image_hash): + os.remove(self.square_thumbnail_small.path) + if doesVideoThumbnailExists("square_thumbnails", self.image_hash): + os.remove(self.square_thumbnail.path) + if doesVideoThumbnailExists("square_thumbnails_small", self.image_hash): + os.remove(self.square_thumbnail_small.path) + self._generate_thumbnail(commit=commit) + self._calculate_aspect_ratio(commit=commit) + self._generate_clip_embeddings(commit=commit) + self._extract_faces() + def _find_album_place(self): return api.models.album_place.AlbumPlace.objects.filter( Q(photos__in=[self]) @@ -538,6 +564,7 @@ def _extract_exif_data(self, commit=True): focalLength35Equivalent, subjectDistance, digitalZoomRatio, + orientation, ) = get_metadata( # noqa: E501 self.main_file.path, tags=[ @@ -553,6 +580,7 @@ def _extract_exif_data(self, commit=True): Tags.FOCAL_LENGTH_35MM, Tags.SUBJECT_DISTANCE, Tags.DIGITAL_ZOOM_RATIO, + Tags.ORIENTATION, ], try_sidecar=True, ) @@ -582,6 +610,8 @@ def _extract_exif_data(self, commit=True): self.subjectDistance = subjectDistance if digitalZoomRatio and isinstance(digitalZoomRatio, numbers.Number): self.digitalZoomRatio = digitalZoomRatio + if orientation and isinstance(orientation, numbers.Number): + self.orientation = orientation if commit: self.save() @@ -812,6 +842,36 @@ def _get_dominant_color(self, palette_size=16): self.save() except Exception: logger.info("Cannot calculate dominant color {} object".format(self)) + + def _rotate_image(self, delta_angle, flip_image=False): + """ + This function updates the field localOrientation + or orientation directly if the user save_metadata_to_disk field is MEDIA_FILE + """ + if delta_angle % 360 == 0 and not flip_image: + return True + + user = User.objects.get(username=self.owner) + save_on_file = (user.save_metadata_to_disk == User.SaveMetadata.MEDIA_FILE and \ + not self.main_file.is_raw()) + + old_orientation = self.orientation if save_on_file else self.localOrientation + angle, is_flipped = util.convert_exif_orientation_to_degrees(old_orientation) + + angle += delta_angle + if flip_image: + is_flipped = not is_flipped + + new_orientation = util.convert_degrees_to_exif_orientation(angle, is_flipped) + + if save_on_file: + self.orientation = new_orientation + self.localOrientation = 1 # reset local orientation when changing file orientation + self.save() + else: + self.localOrientation = new_orientation + self._regenerate_thumbnail() + return True def __str__(self): return "%s" % self.image_hash diff --git a/api/thumbnails.py b/api/thumbnails.py index cd2d6d788f..3b5ddb91e8 100644 --- a/api/thumbnails.py +++ b/api/thumbnails.py @@ -9,8 +9,10 @@ from api.models.file import is_raw -def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType): +def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orientation): try: + angle, is_flipped = util.convert_exif_orientation_to_degrees(orientation) + flip_direction = pyvips.Direction.HORIZONTAL if orientation in [5, 7] else pyvips.Direction.VERTICAL if is_raw(inputPath): if "thumbnails_big" in outputPath: completePath = os.path.join( @@ -33,6 +35,10 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType): height=outputHeight, size=pyvips.enums.Size.DOWN, ) + if angle != 0: + x = x.rotate(angle) + if is_flipped: + x = x.flip(flip_direction) completePath = os.path.join( ownphotos.settings.MEDIA_ROOT, outputPath, hash + fileType ).strip() @@ -42,6 +48,10 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType): x = pyvips.Image.thumbnail( inputPath, 10000, height=outputHeight, size=pyvips.enums.Size.DOWN ) + if angle != 0: + x = x.rotate(angle) + if is_flipped: + x = x.flip(flip_direction) completePath = os.path.join( ownphotos.settings.MEDIA_ROOT, outputPath, hash + fileType ).strip() diff --git a/api/util.py b/api/util.py index 7fa38fc430..9b8c459b40 100644 --- a/api/util.py +++ b/api/util.py @@ -155,7 +155,7 @@ def write_metadata(media_file, tags, use_sidecar=True): if use_sidecar: file_path = get_sidecar_files_in_priority_order(media_file)[0] else: - file_path = media_file + file_path = media_file.path try: logger.info(f"Writing {tags} to {file_path}") @@ -166,3 +166,56 @@ def write_metadata(media_file, tags, use_sidecar=True): finally: if terminate_et: et.terminate() + +def convert_exif_orientation_to_degrees(orientation): + """ + Function to convert EXIF Orientation values to a rotation in degrees + and a boolean indicating if the image is flipped. + Orientation value is an integer, 1 through 8. + The math works better if we make the range from 0 to 7. + Rotation is assumed to be clockwise. + """ + if orientation not in range(1, 9): + return 0, False + this_orientation = orientation - 1 + is_flipped = this_orientation in [1, 3, 4, 6] + # Re-flip flipped orientation + if is_flipped: + flip_delta = 1 if this_orientation % 2 == 0 else -1 + this_orientation = this_orientation + flip_delta + angle = 0 + if this_orientation == 0: + angle = 0 + elif this_orientation == 7: + angle = 90 + elif this_orientation == 2: + angle = 180 + elif this_orientation == 5: + angle = 270 + + return angle, is_flipped + +def convert_degrees_to_exif_orientation(angle, is_flipped=False): + """ + Reverse of the function above. + angle needs to be a multiple of 90, and it's clockwise. + Negative values are treated as counter-clockwise rotation. + """ + COUNTER_CLOCKWISE = 1 + CLOCKWISE = -1 + + angle = int(round(angle / 90.0) * 90) + turns = int(angle / 90) + direction = CLOCKWISE if turns >= 0 else COUNTER_CLOCKWISE + turns = abs(turns) + orientation = 0 + for i in range(turns): + step = 5 + if (i == 7 and direction == COUNTER_CLOCKWISE or \ + i == 0 and direction == CLOCKWISE): + step = 1 + orientation = (orientation + step * direction) % 8 + if is_flipped: + flip_delta = 1 if orientation % 2 == 0 else -1 + orientation = orientation + flip_delta + return orientation + 1 diff --git a/api/views/photos.py b/api/views/photos.py index dff4697f2a..d219dd03ff 100755 --- a/api/views/photos.py +++ b/api/views/photos.py @@ -577,3 +577,23 @@ def delete(self, request): return Response(status=status.HTTP_200_OK) else: return Response(status=status.HTTP_400_BAD_REQUEST) + +class RotatePhoto(APIView): + permission_classes = (IsOwnerOrReadOnly,) + + def post(self, request, format=None): + data = dict(request.data) + image_hash = data["image_hash"] + + angle = data["angle"] + flip = data.get("flip", False) + + photo = Photo.objects.get(image_hash=image_hash) + if photo.owner != request.user: + return Response( + {"status": False, "message": "you are not the owner of this photo"}, + status=400, + ) + + res = photo._rotate_image(angle, flip) + return Response({"status": res}) \ No newline at end of file diff --git a/ownphotos/urls.py b/ownphotos/urls.py index d0bccd324b..a0cf02e872 100644 --- a/ownphotos/urls.py +++ b/ownphotos/urls.py @@ -205,6 +205,7 @@ def post(self, request, *args, **kwargs): re_path(r"^api/photosedit/share", photos.SetPhotosShared.as_view()), re_path(r"^api/photosedit/generateim2txt", photos.GeneratePhotoCaption.as_view()), re_path(r"^api/photosedit/savecaption", photos.SavePhotoCaption.as_view()), + re_path(r"^api/photosedit/rotate", photos.RotatePhoto.as_view()), re_path(r"^api/useralbum/share", views.SetUserAlbumShared.as_view()), re_path(r"^api/trainfaces", faces.TrainFaceView.as_view()), re_path(r"^api/clusterfaces", dataviz.ClusterFaceView.as_view()), From 93ae282c76507503e7e5794a44b731bfc31195ed Mon Sep 17 00:00:00 2001 From: tognee Date: Sun, 18 Jun 2023 13:45:04 +0200 Subject: [PATCH 2/4] Fixed issue when rotating image more than 90deg --- api/models/photo.py | 2 +- api/util.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/models/photo.py b/api/models/photo.py index 96d69ab19a..ee76ab4a2f 100644 --- a/api/models/photo.py +++ b/api/models/photo.py @@ -850,7 +850,7 @@ def _rotate_image(self, delta_angle, flip_image=False): """ if delta_angle % 360 == 0 and not flip_image: return True - + user = User.objects.get(username=self.owner) save_on_file = (user.save_metadata_to_disk == User.SaveMetadata.MEDIA_FILE and \ not self.main_file.is_raw()) diff --git a/api/util.py b/api/util.py index 9b8c459b40..984ff6c29b 100644 --- a/api/util.py +++ b/api/util.py @@ -209,10 +209,10 @@ def convert_degrees_to_exif_orientation(angle, is_flipped=False): direction = CLOCKWISE if turns >= 0 else COUNTER_CLOCKWISE turns = abs(turns) orientation = 0 - for i in range(turns): + for _i in range(turns): step = 5 - if (i == 7 and direction == COUNTER_CLOCKWISE or \ - i == 0 and direction == CLOCKWISE): + if (orientation == 7 and direction == COUNTER_CLOCKWISE or \ + orientation == 0 and direction == CLOCKWISE): step = 1 orientation = (orientation + step * direction) % 8 if is_flipped: From c6092f79db491f864b31a024e1219e157d34fad8 Mon Sep 17 00:00:00 2001 From: tognee Date: Sun, 18 Jun 2023 14:10:46 +0200 Subject: [PATCH 3/4] Sonar optimization for photo rotation feature --- api/models/photo.py | 3 +-- api/thumbnails.py | 42 ++++++++++++++++++++---------------------- api/views/photos.py | 12 +++++++----- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/api/models/photo.py b/api/models/photo.py index ee76ab4a2f..9ae069e593 100644 --- a/api/models/photo.py +++ b/api/models/photo.py @@ -849,7 +849,7 @@ def _rotate_image(self, delta_angle, flip_image=False): or orientation directly if the user save_metadata_to_disk field is MEDIA_FILE """ if delta_angle % 360 == 0 and not flip_image: - return True + return user = User.objects.get(username=self.owner) save_on_file = (user.save_metadata_to_disk == User.SaveMetadata.MEDIA_FILE and \ @@ -871,7 +871,6 @@ def _rotate_image(self, delta_angle, flip_image=False): else: self.localOrientation = new_orientation self._regenerate_thumbnail() - return True def __str__(self): return "%s" % self.image_hash diff --git a/api/thumbnails.py b/api/thumbnails.py index 3b5ddb91e8..f96fca9102 100644 --- a/api/thumbnails.py +++ b/api/thumbnails.py @@ -25,28 +25,14 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta } response = requests.post("http://localhost:8003/", json=json).json() return response["thumbnail"] - else: - bigThumbnailPath = os.path.join( - ownphotos.settings.MEDIA_ROOT, "thumbnails_big", hash + fileType - ) - x = pyvips.Image.thumbnail( - bigThumbnailPath, - 10000, - height=outputHeight, - size=pyvips.enums.Size.DOWN, - ) - if angle != 0: - x = x.rotate(angle) - if is_flipped: - x = x.flip(flip_direction) - completePath = os.path.join( - ownphotos.settings.MEDIA_ROOT, outputPath, hash + fileType - ).strip() - x.write_to_file(completePath, Q=95) - return completePath - else: + bigThumbnailPath = os.path.join( + ownphotos.settings.MEDIA_ROOT, "thumbnails_big", hash + fileType + ) x = pyvips.Image.thumbnail( - inputPath, 10000, height=outputHeight, size=pyvips.enums.Size.DOWN + bigThumbnailPath, + 10000, + height=outputHeight, + size=pyvips.enums.Size.DOWN, ) if angle != 0: x = x.rotate(angle) @@ -55,8 +41,20 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta completePath = os.path.join( ownphotos.settings.MEDIA_ROOT, outputPath, hash + fileType ).strip() - x.write_to_file(completePath) + x.write_to_file(completePath, Q=95) return completePath + x = pyvips.Image.thumbnail( + inputPath, 10000, height=outputHeight, size=pyvips.enums.Size.DOWN + ) + if angle != 0: + x = x.rotate(angle) + if is_flipped: + x = x.flip(flip_direction) + completePath = os.path.join( + ownphotos.settings.MEDIA_ROOT, outputPath, hash + fileType + ).strip() + x.write_to_file(completePath) + return completePath except Exception as e: util.logger.error("Could not create thumbnail for file {}".format(inputPath)) raise e diff --git a/api/views/photos.py b/api/views/photos.py index d219dd03ff..60f47f6ae2 100755 --- a/api/views/photos.py +++ b/api/views/photos.py @@ -22,6 +22,8 @@ StandardResultsSetPagination, ) +PHOTO_OWNER_ERROR_MESSAGE = "you are not the owner of this photo" + class RecentlyAddedPhotoListViewSet(ListViewSet): serializer_class = PigPhotoSerilizer @@ -504,7 +506,7 @@ def post(self, request, format=None): photo = Photo.objects.get(image_hash=image_hash) if photo.owner != request.user: return Response( - {"status": False, "message": "you are not the owner of this photo"}, + {"status": False, "message": PHOTO_OWNER_ERROR_MESSAGE}, status=400, ) @@ -523,7 +525,7 @@ def post(self, request, format=None): photo = Photo.objects.get(image_hash=image_hash) if photo.owner != request.user: return Response( - {"status": False, "message": "you are not the owner of this photo"}, + {"status": False, "message": PHOTO_OWNER_ERROR_MESSAGE}, status=400, ) @@ -591,9 +593,9 @@ def post(self, request, format=None): photo = Photo.objects.get(image_hash=image_hash) if photo.owner != request.user: return Response( - {"status": False, "message": "you are not the owner of this photo"}, + {"status": False, "message": PHOTO_OWNER_ERROR_MESSAGE}, status=400, ) - res = photo._rotate_image(angle, flip) - return Response({"status": res}) \ No newline at end of file + photo._rotate_image(angle, flip) + return Response({"status": True}) \ No newline at end of file From bfadca50910de8344c0dbdc9873d879b154767af Mon Sep 17 00:00:00 2001 From: tognee Date: Tue, 20 Jun 2023 00:34:25 +0200 Subject: [PATCH 4/4] Removed localOrientation and fixed sync issues with image rotation --- ...ientation.py => 0050_photo_orientation.py} | 7 +---- api/models/photo.py | 26 +++++++------------ api/thumbnails.py | 11 +++++++- api/util.py | 12 ++++----- 4 files changed, 26 insertions(+), 30 deletions(-) rename api/migrations/{0050_photo_localorientation_photo_orientation.py => 0050_photo_orientation.py} (62%) diff --git a/api/migrations/0050_photo_localorientation_photo_orientation.py b/api/migrations/0050_photo_orientation.py similarity index 62% rename from api/migrations/0050_photo_localorientation_photo_orientation.py rename to api/migrations/0050_photo_orientation.py index 309f13807c..b157363289 100644 --- a/api/migrations/0050_photo_localorientation_photo_orientation.py +++ b/api/migrations/0050_photo_orientation.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-18 08:29 +# Generated by Django 4.2.1 on 2023-06-19 20:17 from django.db import migrations, models @@ -10,11 +10,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name='photo', - name='localOrientation', - field=models.IntegerField(default=1), - ), migrations.AddField( model_name='photo', name='orientation', diff --git a/api/models/photo.py b/api/models/photo.py index 9ae069e593..a4ddd72d45 100644 --- a/api/models/photo.py +++ b/api/models/photo.py @@ -17,7 +17,7 @@ import api.util as util from api.exif_tags import Tags from api.im2txt.sample import im2txt -from api.models.file import File +from api.models.file import File, is_raw from api.models.user import User, get_deleted_user from api.places365.places365 import place365_instance from api.semantic_search.semantic_search import semantic_search_instance @@ -92,8 +92,6 @@ class Photo(models.Model): digitalZoomRatio = models.FloatField(blank=True, null=True) orientation = models.IntegerField(default=1) - localOrientation = models.IntegerField(default=1) - owner = models.ForeignKey( User, on_delete=models.SET(get_deleted_user), default=None ) @@ -271,7 +269,7 @@ def _generate_thumbnail(self, commit=True): outputPath="thumbnails_big", hash=self.image_hash, fileType=".webp", - orientation=self.localOrientation, + orientation=self.orientation, ) else: createThumbnailForVideo( @@ -290,7 +288,7 @@ def _generate_thumbnail(self, commit=True): outputPath="square_thumbnails", hash=self.image_hash, fileType=".webp", - orientation=self.localOrientation, + orientation=self.orientation, ) if self.video and not doesVideoThumbnailExists( "square_thumbnails", self.image_hash @@ -312,7 +310,7 @@ def _generate_thumbnail(self, commit=True): outputPath="square_thumbnails_small", hash=self.image_hash, fileType=".webp", - orientation=self.localOrientation, + orientation=self.orientation, ) if self.video and not doesVideoThumbnailExists( "square_thumbnails_small", self.image_hash @@ -844,18 +842,14 @@ def _get_dominant_color(self, palette_size=16): logger.info("Cannot calculate dominant color {} object".format(self)) def _rotate_image(self, delta_angle, flip_image=False): - """ - This function updates the field localOrientation - or orientation directly if the user save_metadata_to_disk field is MEDIA_FILE - """ if delta_angle % 360 == 0 and not flip_image: return user = User.objects.get(username=self.owner) save_on_file = (user.save_metadata_to_disk == User.SaveMetadata.MEDIA_FILE and \ - not self.main_file.is_raw()) + not is_raw(self.main_file.path)) - old_orientation = self.orientation if save_on_file else self.localOrientation + old_orientation = self.orientation angle, is_flipped = util.convert_exif_orientation_to_degrees(old_orientation) angle += delta_angle @@ -863,14 +857,12 @@ def _rotate_image(self, delta_angle, flip_image=False): is_flipped = not is_flipped new_orientation = util.convert_degrees_to_exif_orientation(angle, is_flipped) + self.orientation = new_orientation if save_on_file: - self.orientation = new_orientation - self.localOrientation = 1 # reset local orientation when changing file orientation self.save() - else: - self.localOrientation = new_orientation - self._regenerate_thumbnail() + return + self._regenerate_thumbnail() def __str__(self): return "%s" % self.image_hash diff --git a/api/thumbnails.py b/api/thumbnails.py index f96fca9102..3472d4b72c 100644 --- a/api/thumbnails.py +++ b/api/thumbnails.py @@ -13,6 +13,7 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta try: angle, is_flipped = util.convert_exif_orientation_to_degrees(orientation) flip_direction = pyvips.Direction.HORIZONTAL if orientation in [5, 7] else pyvips.Direction.VERTICAL + if is_raw(inputPath): if "thumbnails_big" in outputPath: completePath = os.path.join( @@ -25,6 +26,7 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta } response = requests.post("http://localhost:8003/", json=json).json() return response["thumbnail"] + bigThumbnailPath = os.path.join( ownphotos.settings.MEDIA_ROOT, "thumbnails_big", hash + fileType ) @@ -32,6 +34,7 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta bigThumbnailPath, 10000, height=outputHeight, + no_rotate=True, size=pyvips.enums.Size.DOWN, ) if angle != 0: @@ -43,8 +46,13 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta ).strip() x.write_to_file(completePath, Q=95) return completePath + x = pyvips.Image.thumbnail( - inputPath, 10000, height=outputHeight, size=pyvips.enums.Size.DOWN + inputPath, + 10000, + height=outputHeight, + no_rotate=True, + size=pyvips.enums.Size.DOWN, ) if angle != 0: x = x.rotate(angle) @@ -55,6 +63,7 @@ def createThumbnail(inputPath, outputHeight, outputPath, hash, fileType, orienta ).strip() x.write_to_file(completePath) return completePath + except Exception as e: util.logger.error("Could not create thumbnail for file {}".format(inputPath)) raise e diff --git a/api/util.py b/api/util.py index 984ff6c29b..4be8e583e0 100644 --- a/api/util.py +++ b/api/util.py @@ -186,11 +186,11 @@ def convert_exif_orientation_to_degrees(orientation): angle = 0 if this_orientation == 0: angle = 0 - elif this_orientation == 7: + elif this_orientation == 5: angle = 90 elif this_orientation == 2: angle = 180 - elif this_orientation == 5: + elif this_orientation == 7: angle = 270 return angle, is_flipped @@ -201,8 +201,8 @@ def convert_degrees_to_exif_orientation(angle, is_flipped=False): angle needs to be a multiple of 90, and it's clockwise. Negative values are treated as counter-clockwise rotation. """ - COUNTER_CLOCKWISE = 1 - CLOCKWISE = -1 + CLOCKWISE = 1 + COUNTER_CLOCKWISE = -1 angle = int(round(angle / 90.0) * 90) turns = int(angle / 90) @@ -211,8 +211,8 @@ def convert_degrees_to_exif_orientation(angle, is_flipped=False): orientation = 0 for _i in range(turns): step = 5 - if (orientation == 7 and direction == COUNTER_CLOCKWISE or \ - orientation == 0 and direction == CLOCKWISE): + if (orientation == 7 and direction == CLOCKWISE or + orientation == 0 and direction == COUNTER_CLOCKWISE): step = 1 orientation = (orientation + step * direction) % 8 if is_flipped: