Skip to content

Commit b4e591b

Browse files
authored
Merge pull request #8 from agusmakmun/mr-7
first commit for image crop on axis. added function #7
2 parents 30fe24d + 1327229 commit b4e591b

File tree

14 files changed

+272
-165
lines changed

14 files changed

+272
-165
lines changed

image_optimizer/fields.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
from __future__ import unicode_literals
3-
41
from django.db.models import ImageField
52
from .utils import image_optimizer
63

@@ -11,19 +8,26 @@ class OptimizedImageField(ImageField):
118
def save_form_data(self, instance, data):
129
"""Remove the OptimizedNotOptimized object on clearing the image."""
1310
# Are we updating an image?
14-
updating_image = True if data and getattr(instance, self.name) != data else False
11+
updating_image = (
12+
True if data and getattr(instance, self.name) != data else False
13+
)
1514

1615
if updating_image:
1716
data = image_optimizer(
1817
data,
1918
self.optimized_image_output_size,
20-
self.optimized_image_resize_method
19+
self.optimized_image_resize_method,
2120
)
2221

2322
super().save_form_data(instance, data)
2423

25-
def __init__(self, optimized_image_output_size=None,
26-
optimized_image_resize_method=None, *args, **kwargs):
24+
def __init__(
25+
self,
26+
optimized_image_output_size=None,
27+
optimized_image_resize_method=None,
28+
*args,
29+
**kwargs
30+
):
2731
"""
2832
Initialize OptimizedImageField instance.
2933
@@ -51,10 +55,10 @@ def deconstruct(self):
5155
"""
5256
name, path, args, kwargs = super().deconstruct()
5357

54-
if kwargs.get('optimized_image_output_size'):
55-
del kwargs['optimized_image_output_size']
58+
if kwargs.get("optimized_image_output_size"):
59+
del kwargs["optimized_image_output_size"]
5660

57-
if kwargs.get('optimized_image_resize_method'):
58-
del kwargs['optimized_image_resize_method']
61+
if kwargs.get("optimized_image_resize_method"):
62+
del kwargs["optimized_image_resize_method"]
5963

6064
return name, path, args, kwargs

image_optimizer/settings.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
# -*- coding: utf-8 -*-
2-
from __future__ import unicode_literals
3-
41
from django.conf import settings
52

6-
OPTIMIZED_IMAGE_METHOD = getattr(settings, 'OPTIMIZED_IMAGE_METHOD', 'pillow')
7-
TINYPNG_KEY = getattr(settings, 'TINYPNG_KEY', None)
3+
OPTIMIZED_IMAGE_METHOD = getattr(settings, "OPTIMIZED_IMAGE_METHOD", "pillow")
4+
TINYPNG_KEY = getattr(settings, "TINYPNG_KEY", None)

image_optimizer/utils.py

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
# -*- coding: utf-8 -*-
2-
from __future__ import unicode_literals
3-
4-
from io import BytesIO
5-
from PIL import Image
6-
from resizeimage import resizeimage
7-
81
import tinify
92
import logging
103
import requests
4+
from io import BytesIO
5+
from PIL import Image
6+
from resizeimage import resizeimage
7+
from uuid import uuid4
118

12-
from .settings import (OPTIMIZED_IMAGE_METHOD, TINYPNG_KEY)
9+
from .settings import OPTIMIZED_IMAGE_METHOD, TINYPNG_KEY
1310

1411

1512
BACKGROUND_TRANSPARENT = (255, 255, 255, 0)
@@ -23,10 +20,10 @@ def get_file_extension(file_name):
2320
extension = None
2421

2522
# Get image file extension
26-
if file_name.split('.')[-1].lower() != 'jpg':
27-
extension = file_name.split('.')[-1].upper()
23+
if file_name.split(".")[-1].lower() != "jpg":
24+
extension = file_name.split(".")[-1].upper()
2825
else:
29-
extension = 'JPEG'
26+
extension = "JPEG"
3027

3128
return extension
3229

@@ -39,11 +36,11 @@ def image_optimizer(image_data, output_size=None, resize_method=None):
3936
"""
4037
Optimize an image that has not been saved to a file.
4138
:param `image_data` is image data, e.g from request.FILES['image']
42-
:param `output_size` is float pixel scale of image (width, height) or None, e.g: (400, 300)
43-
:param `resize_method` is string resize method, choices are: None, "thumbnail", or "cover".
39+
:param `output_size` is float pixel scale of image (width, height) or None, for example: (400, 300) # noqa: E501
40+
:param `resize_method` is string resize method, choices are: None, "thumbnail", or "cover". # noqa: E501
4441
:return optimized image data.
4542
"""
46-
if OPTIMIZED_IMAGE_METHOD == 'pillow':
43+
if OPTIMIZED_IMAGE_METHOD == "pillow":
4744
image = Image.open(image_data)
4845
bytes_io = BytesIO()
4946

@@ -53,49 +50,108 @@ def image_optimizer(image_data, output_size=None, resize_method=None):
5350
# resize_method. 'thumbnail' is used by default
5451
if output_size is not None:
5552

56-
if resize_method not in ('thumbnail', 'cover', None):
57-
message = 'optimized_image_resize_method misconfigured, it\'s value must be \'thumbnail\', \'cover\' or None'
53+
if resize_method not in ("thumbnail", "cover", None):
54+
message = (
55+
"optimized_image_resize_method misconfigured, "
56+
"it's value must be 'thumbnail', 'cover' or None"
57+
)
5858
raise Exception(message)
5959

60-
elif resize_method is 'thumbnail':
61-
image = resizeimage.resize_thumbnail(image, output_size, resample=Image.LANCZOS)
60+
elif resize_method == "thumbnail":
61+
image = resizeimage.resize_thumbnail(
62+
image, output_size, resample=Image.LANCZOS
63+
)
6264

63-
elif resize_method is 'cover':
65+
elif resize_method == "cover":
6466
image = resizeimage.resize_cover(image, output_size, validate=False)
6567

66-
output_image = Image.new('RGBA', output_size, BACKGROUND_TRANSPARENT)
67-
output_image_center = (int((output_size[0] - image.size[0]) / 2),
68-
int((output_size[1] - image.size[1]) / 2))
68+
output_image = Image.new(
69+
"RGBA",
70+
output_size,
71+
BACKGROUND_TRANSPARENT,
72+
)
73+
output_image_center = (
74+
int((output_size[0] - image.size[0]) / 2),
75+
int((output_size[1] - image.size[1]) / 2),
76+
)
6977

7078
output_image.paste(image, output_image_center)
7179

7280
else:
73-
# If output_size is None the output_image would be the same as source
81+
# If output_size is None the output_image
82+
# would be the same as source
7483
output_image = image
7584

7685
# If the file extension is JPEG, convert the output_image to RGB
77-
if extension == 'JPEG':
78-
output_image = output_image.convert('RGB')
86+
if extension == "JPEG":
87+
output_image = output_image.convert("RGB")
7988

8089
output_image.save(bytes_io, format=extension, optimize=True)
8190

8291
image_data.seek(0)
8392
image_data.file.write(bytes_io.getvalue())
8493
image_data.file.truncate()
8594

86-
elif OPTIMIZED_IMAGE_METHOD == 'tinypng':
95+
elif OPTIMIZED_IMAGE_METHOD == "tinypng":
8796
# disable warning info
8897
requests.packages.urllib3.disable_warnings()
8998

9099
# just info for people
91100
if any([output_size, resize_method]):
92-
message = '[django-image-optimizer] "output_size" and "resize_method" only for OPTIMIZED_IMAGE_METHOD="pillow"'
101+
message = (
102+
'[django-image-optimizer] "output_size" and "resize_method" '
103+
'only for OPTIMIZED_IMAGE_METHOD="pillow"'
104+
)
93105
logging.info(message)
94106

95107
tinify.key = TINYPNG_KEY
96-
optimized_buffer = tinify.from_buffer(image_data.file.read()).to_buffer()
108+
optimized_buffer = tinify.from_buffer(
109+
image_data.file.read()
110+
).to_buffer() # noqa: E501
97111
image_data.seek(0)
98112
image_data.file.write(optimized_buffer)
99113
image_data.file.truncate()
100114

101115
return image_data
116+
117+
118+
def crop_image_on_axis(image, width, height, x, y, extension):
119+
"""
120+
function to crop the image using axis (using Pillow).
121+
:param `image` is image data, e.g from request.FILES['image']
122+
:param `width` float width of image
123+
:param `height` float height of image
124+
:param `x` is float x axis
125+
:param `y` is float y axis
126+
:param `extension` is string, e.g: ".png"
127+
"""
128+
# Open the passed image
129+
img = Image.open(image)
130+
131+
# Initialise bytes io
132+
bytes_io = BytesIO()
133+
134+
# crop the image through axis
135+
img = img.crop((x, y, width + x, height + y))
136+
137+
# resize the image and optimise it for file size,
138+
# making smaller as possible
139+
img = img.resize((width, height), Image.ANTIALIAS)
140+
141+
# This line is optional, for safe side, image name should be unique.
142+
img.name = "{}.{}".format(uuid4().hex, extension)
143+
144+
# If the file extension is JPEG, convert the output_image to RGB
145+
if extension == "JPEG":
146+
img = image.convert("RGB")
147+
img.save(bytes_io, format=extension, optimize=True)
148+
149+
# return the image
150+
image.seek(0)
151+
152+
# Write back new image
153+
image.file.write(bytes_io.getvalue())
154+
155+
# truncate the file size
156+
image.file.truncate()
157+
return image
Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1-
# -*- coding: utf-8 -*-
2-
from __future__ import unicode_literals
3-
41
from django.contrib import admin
52
from app_demo.models import (
63
Post,
74
Collaborator,
5+
OtherImage,
86
)
7+
from app_demo.forms import CropImageAxisForm
98

109

1110
class PostAdmin(admin.ModelAdmin):
12-
list_display = ['title', 'created']
13-
list_filter = ['created']
11+
list_display = ["title", "created"]
12+
list_filter = ["created"]
1413

1514

1615
class CollaboratorAdmin(admin.ModelAdmin):
17-
list_display = ['name', 'created']
18-
list_filter = ['created']
16+
list_display = ["name", "created"]
17+
list_filter = ["created"]
18+
19+
20+
class CropImageAxisAdmin(admin.ModelAdmin):
21+
list_display = ["created", "image"]
22+
list_filter = ["created"]
23+
form = CropImageAxisForm
24+
25+
def get_form(self, request, *args, **kwargs):
26+
form = super().get_form(request, *args, **kwargs)
27+
form.request = request
28+
return form
1929

2030

2131
admin.site.register(Post, PostAdmin)
2232
admin.site.register(Collaborator, CollaboratorAdmin)
33+
admin.site.register(OtherImage, CropImageAxisAdmin)
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
# -*- coding: utf-8 -*-
2-
from __future__ import unicode_literals
3-
41
from django.apps import AppConfig
52

63

74
class AppDemoConfig(AppConfig):
8-
name = 'app_demo'
5+
name = "app_demo"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from django import forms
2+
from .models import OtherImage
3+
from image_optimizer.utils import crop_image_on_axis, get_file_extension
4+
5+
6+
class CropImageAxisForm(forms.ModelForm):
7+
width = forms.IntegerField()
8+
height = forms.IntegerField()
9+
x = forms.FloatField()
10+
y = forms.FloatField()
11+
12+
def save(self, commit=True):
13+
instance = super().save(commit=False)
14+
request = self.request
15+
16+
# process on create only
17+
image = request.FILES.get("image")
18+
if image is not None:
19+
width = float(request.POST["width"])
20+
height = float(request.POST["height"])
21+
x = float(request.POST["x"])
22+
y = float(request.POST["y"])
23+
extension = get_file_extension(image.name)
24+
25+
try:
26+
image = crop_image_on_axis(image, width, height, x, y, extension)
27+
except ValueError as error:
28+
raise forms.ValidationError(error)
29+
30+
instance.image = image
31+
instance.save(commit=True)
32+
return instance
33+
34+
return super().save(commit)
35+
36+
class Meta:
37+
model = OtherImage
38+
fields = ["image"]

0 commit comments

Comments
 (0)