diff --git a/cookbook/helper/image_processing.py b/cookbook/helper/image_processing.py index bf84070fa..610f58682 100644 --- a/cookbook/helper/image_processing.py +++ b/cookbook/helper/image_processing.py @@ -1,8 +1,11 @@ +import os +import sys + from PIL import Image from io import BytesIO -def rescale_image(image_object, base_width=720): +def rescale_image_jpeg(image_object, base_width=720): img = Image.open(image_object) icc_profile = img.info.get('icc_profile') # remember color profile to not mess up colors width_percent = (base_width / float(img.size[0])) @@ -13,3 +16,30 @@ def rescale_image(image_object, base_width=720): img.save(img_bytes, 'JPEG', quality=75, optimize=True, icc_profile=icc_profile) return img_bytes + + +def rescale_image_png(image_object, base_width=720): + basewidth = 720 + wpercent = (basewidth / float(image_object.size[0])) + hsize = int((float(image_object.size[1]) * float(wpercent))) + img = image_object.resize((basewidth, hsize), Image.ANTIALIAS) + + im_io = BytesIO() + img.save(im_io, 'PNG', quality=70) + return img + + +def get_filetype(name): + try: + return os.path.splitext(name)[1] + except: + return '.jpeg' + + +def handle_image(request, image_object, filetype='.jpeg'): + if sys.getsizeof(image_object) / 8 > 500: + if filetype == '.jpeg': + return rescale_image_jpeg(image_object), filetype + if filetype == '.png': + return rescale_image_png(image_object), filetype + return image_object, filetype diff --git a/cookbook/integration/chowdown.py b/cookbook/integration/chowdown.py index 05e283556..b23cafdb4 100644 --- a/cookbook/integration/chowdown.py +++ b/cookbook/integration/chowdown.py @@ -3,6 +3,7 @@ import re from io import BytesIO from zipfile import ZipFile +from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.integration.integration import Integration from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword @@ -71,7 +72,7 @@ class Chowdown(Integration): import_zip = ZipFile(f['file']) for z in import_zip.filelist: if re.match(f'^images/{image}$', z.filename): - self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename))) + self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename)) return recipe diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py index 7140290e8..1fb16e7f6 100644 --- a/cookbook/integration/default.py +++ b/cookbook/integration/default.py @@ -1,9 +1,11 @@ import json from io import BytesIO +from re import match from zipfile import ZipFile from rest_framework.renderers import JSONRenderer +from cookbook.helper.image_processing import get_filetype from cookbook.integration.integration import Integration from cookbook.serializer import RecipeExportSerializer @@ -15,8 +17,9 @@ class Default(Integration): recipe_string = recipe_zip.read('recipe.json').decode("utf-8") recipe = self.decode_recipe(recipe_string) - if 'image.png' in recipe_zip.namelist(): - self.import_recipe_image(recipe, BytesIO(recipe_zip.read('image.png'))) + images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist())) + if images: + self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0])) return recipe def decode_recipe(self, string): diff --git a/cookbook/integration/domestica.py b/cookbook/integration/domestica.py index ec9f5ce24..50d92d590 100644 --- a/cookbook/integration/domestica.py +++ b/cookbook/integration/domestica.py @@ -45,7 +45,7 @@ class Domestica(Integration): recipe.steps.add(step) if file['image'] != '': - self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', '')))) + self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))), filetype='.jpeg') return recipe diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 3dbb0f290..b25a1e831 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -1,5 +1,6 @@ import datetime import json +import os import re import uuid from io import BytesIO, StringIO @@ -12,6 +13,7 @@ from django.utils.translation import gettext as _ from django_scopes import scope from cookbook.forms import ImportExportBase +from cookbook.helper.image_processing import get_filetype from cookbook.models import Keyword, Recipe @@ -59,7 +61,7 @@ class Integration: recipe_zip_obj.writestr(filename, recipe_stream.getvalue()) recipe_stream.close() try: - recipe_zip_obj.writestr('image.png', r.image.file.read()) + recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read()) except ValueError: pass @@ -185,13 +187,14 @@ class Integration: self.ignored_recipes.append(recipe.name) @staticmethod - def import_recipe_image(recipe, image_file): + def import_recipe_image(recipe, image_file, filetype='.jpeg'): """ Adds an image to a recipe naming it correctly :param recipe: Recipe object :param image_file: ByteIO stream containing the image + :param filetype: type of file to write bytes to, default to .jpeg if unknown """ - recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}.png') + recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}') recipe.save() def get_recipe_from_file(self, file): diff --git a/cookbook/integration/mealie.py b/cookbook/integration/mealie.py index a67640e00..722a26be3 100644 --- a/cookbook/integration/mealie.py +++ b/cookbook/integration/mealie.py @@ -3,6 +3,7 @@ import re from io import BytesIO from zipfile import ZipFile +from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.integration.integration import Integration from cookbook.models import Recipe, Step, Food, Unit, Ingredient @@ -49,7 +50,7 @@ class Mealie(Integration): import_zip = ZipFile(f['file']) for z in import_zip.filelist: if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename): - self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename))) + self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename)) return recipe diff --git a/cookbook/integration/nextcloud_cookbook.py b/cookbook/integration/nextcloud_cookbook.py index e52e383fa..538fe1a33 100644 --- a/cookbook/integration/nextcloud_cookbook.py +++ b/cookbook/integration/nextcloud_cookbook.py @@ -3,6 +3,7 @@ import re from io import BytesIO from zipfile import ZipFile +from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.integration.integration import Integration from cookbook.models import Recipe, Step, Food, Unit, Ingredient @@ -51,7 +52,7 @@ class NextcloudCookbook(Integration): import_zip = ZipFile(f['file']) for z in import_zip.filelist: if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename): - self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename))) + self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename)) return recipe diff --git a/cookbook/integration/paprika.py b/cookbook/integration/paprika.py index 40dcc0e80..6feca172e 100644 --- a/cookbook/integration/paprika.py +++ b/cookbook/integration/paprika.py @@ -81,6 +81,6 @@ class Paprika(Integration): recipe.steps.add(step) if recipe_json.get("photo_data", None): - self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data']))) + self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg') return recipe diff --git a/cookbook/integration/recettetek.py b/cookbook/integration/recettetek.py index 2f51f47c7..04946d62f 100644 --- a/cookbook/integration/recettetek.py +++ b/cookbook/integration/recettetek.py @@ -7,6 +7,7 @@ from zipfile import ZipFile import imghdr from django.utils.translation import gettext as _ +from cookbook.helper.image_processing import get_filetype from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.integration.integration import Integration from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword @@ -25,7 +26,7 @@ class RecetteTek(Integration): recipe_list = [r for r in recipe_json] return recipe_list - + def get_recipe_from_file(self, file): # Create initial recipe with just a title and a decription @@ -53,7 +54,7 @@ class RecetteTek(Integration): step.save() except Exception as e: print(recipe.name, ': failed to import source url ', str(e)) - + try: # Process the ingredients. Assumes 1 ingredient per line. for ingredient in file['ingredients'].split('\n'): @@ -96,7 +97,7 @@ class RecetteTek(Integration): recipe.waiting_time = int(file['cookingTime']) except Exception as e: print(recipe.name, ': failed to parse cooking time ', str(e)) - + recipe.save() # Import the recipe keywords @@ -110,20 +111,20 @@ class RecetteTek(Integration): pass # TODO: Parse Nutritional Information - + # Import the original image from the zip file, if we cannot do that, attempt to download it again. try: - if file['pictures'][0] !='': + if file['pictures'][0] != '': image_file_name = file['pictures'][0].split('/')[-1] for f in self.files: if '.rtk' in f['name']: import_zip = ZipFile(f['file']) - self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name))) + self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)), filetype=get_filetype(image_file_name)) else: if file['originalPicture'] != '': - response=requests.get(file['originalPicture']) + response = requests.get(file['originalPicture']) if imghdr.what(BytesIO(response.content)) != None: - self.import_recipe_image(recipe, BytesIO(response.content)) + self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture'])) else: raise Exception("Original image failed to download.") except Exception as e: diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py index bd5e241fa..ae198795d 100644 --- a/cookbook/integration/recipekeeper.py +++ b/cookbook/integration/recipekeeper.py @@ -70,7 +70,7 @@ class RecipeKeeper(Integration): for f in self.files: if '.zip' in f['name']: import_zip = ZipFile(f['file']) - self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src")))) + self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))), filetype='.jpeg') except Exception as e: pass diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 96a38b1f2..9a7f42812 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -26,6 +26,7 @@ from rest_framework.schemas.openapi import AutoSchema from rest_framework.schemas.utils import is_list_view from rest_framework.viewsets import ViewSetMixin +from cookbook.helper.image_processing import handle_image from cookbook.helper.ingredient_parser import parse from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner, CustomIsShare, @@ -394,16 +395,9 @@ class RecipeViewSet(viewsets.ModelViewSet): if serializer.is_valid(): serializer.save() - img = Image.open(obj.image) - basewidth = 720 - wpercent = (basewidth / float(img.size[0])) - hsize = int((float(img.size[1]) * float(wpercent))) - img = img.resize((basewidth, hsize), Image.ANTIALIAS) - - im_io = io.BytesIO() - img.save(im_io, 'PNG', quality=70) - obj.image = File(im_io, name=f'{uuid.uuid4()}_{obj.pk}.png') + img, filetype = handle_image(request, obj.image) + obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}') obj.save() return Response(serializer.data) @@ -495,7 +489,6 @@ class BookmarkletImportViewSet(viewsets.ModelViewSet): return self.queryset.filter(space=self.request.space).all() - class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin): queryset = UserFile.objects serializer_class = UserFileSerializer diff --git a/cookbook/views/data.py b/cookbook/views/data.py index caa4f55d0..04f65f6f9 100644 --- a/cookbook/views/data.py +++ b/cookbook/views/data.py @@ -17,6 +17,7 @@ from PIL import Image, UnidentifiedImageError from requests.exceptions import MissingSchema from cookbook.forms import BatchEditForm, SyncForm +from cookbook.helper.image_processing import handle_image from cookbook.helper.permission_helper import group_required, has_group_permission from cookbook.helper.recipe_url_import import parse_cooktime from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe, @@ -186,18 +187,10 @@ def import_url(request): if 'image' in data and data['image'] != '' and data['image'] is not None: try: response = requests.get(data['image']) - img = Image.open(BytesIO(response.content)) - # todo move image processing to dedicated function - basewidth = 720 - wpercent = (basewidth / float(img.size[0])) - hsize = int((float(img.size[1]) * float(wpercent))) - img = img.resize((basewidth, hsize), Image.ANTIALIAS) - - im_io = BytesIO() - img.save(im_io, 'PNG', quality=70) + img, filetype = handle_image(request, response.content) recipe.image = File( - im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png' + img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}' ) recipe.save() except UnidentifiedImageError: