Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fdfda82
Set pillow as the default OPTIMIZED_IMAGE_METHOD
mcueto May 21, 2019
315426b
Add optimized_image_output_size field argument to allow image resizing
mcueto May 21, 2019
31fb535
Add python-resize-image to install_requires on setup.py
mcueto May 21, 2019
0ea06ee
Bugfix: __init__() got multiple values for argument 'verbose_name'
mcueto May 21, 2019
e2f2c74
Revert "Bugfix: __init__() got multiple values for argument 'verbose_…
mcueto May 21, 2019
14ece13
Bugfix: OptimizedImageField __init__ with optinal argument
mcueto May 21, 2019
cfe7fc7
Add optimized_image_output_size argument
mcueto Jun 18, 2019
7817163
Add deconstruct method to OptimizedImageField
mcueto Jun 18, 2019
cf044da
Set None as default on image_optimizer util optional arguments
mcueto Jun 24, 2019
c1ceca2
Add docstring to OptimizedImageField init
mcueto Jun 24, 2019
ad908dc
Minor refactor to app_demo app models on image_optimizer_demo folder
mcueto Jun 24, 2019
80b5d91
Add docstring to Post model
mcueto Jun 24, 2019
f4cefa9
Change Post.photo field upload_to argument
mcueto Jun 24, 2019
21d1ac2
Create Collaborator model to illustrate image resizing
mcueto Jun 24, 2019
81ae9eb
Register Collaborator model to django admin site
mcueto Jun 24, 2019
a9bfc46
Merge pull request #3 from mcueto/master
agusmakmun Jun 24, 2019
377b289
Merge branch 'master' into fix/image-extension
alissonmuller Jun 25, 2019
88f3f95
Merge pull request #4 from alissonmuller/fix/image-extension
agusmakmun Jun 25, 2019
30fe24d
update docstring
agusmakmun Mar 19, 2020
0398d8c
first commit for image crop on axis. added method.
Aug 7, 2022
4458a95
demo app for pull request created
Aug 10, 2022
1327229
fix: implement image crop on axis in django admin, update black, etc.
Aug 10, 2022
b4e591b
Merge pull request #8 from agusmakmun/mr-7
agusmakmun Aug 11, 2022
9d52d9b
Make it possible to use all resize methods
Vayel Oct 17, 2022
d71ca1b
Merge pull request #10 from Vayel/resize_methods
agusmakmun Oct 18, 2022
323659d
feat: bump version, fix save with commit arg, implement black
Oct 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist/
*backup*
db.sqlite3
.vscode
image_optimizer_demo/media/*
48 changes: 43 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ django-image-optimizer |pypi version|
---------------------------------------

.. |pypi version|
image:: https://img.shields.io/pypi/v/django-image-optimizer.svg?style=flat-square
image:: https://img.shields.io/pypi/v/django-image-optimizer.svg
:target: https://pypi.python.org/pypi/django-image-optimizer

.. image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
.. image:: https://img.shields.io/badge/license-MIT-blue.svg
:target: https://raw.githubusercontent.com/agusmakmun/django-image-optimizer/master/LICENSE

.. image:: https://img.shields.io/pypi/pyversions/django-image-optimizer.svg?style=flat-square
.. image:: https://img.shields.io/pypi/pyversions/django-image-optimizer.svg
:target: https://pypi.python.org/pypi/django-image-optimizer

.. image:: https://img.shields.io/badge/Django-1.8,%201.9,%201.10,%201.11,%202.0-green.svg?style=flat-square
.. image:: https://img.shields.io/badge/Django-1.8%20%3E=%203.0-green.svg
:target: https://www.djangoproject.com


Expand Down Expand Up @@ -80,13 +80,51 @@ file. Note: it is a good idea to keep this secret
image = OptimizedImageField()


class MyModel2(models.Model):
"""
If you using OPTIMIZED_IMAGE_METHOD = 'pillow'
You can use this optional arguments.

This model represents a MyModel2 with a few
fields including a `image` field which is an OptimizedImageField
instance with `optimized_image_output_size` and
`optimized_image_resize_method` arguments set.

This means that image would be a resized
version of the source image, meant to keep a given screen resolution,
in this case (400, 300) pixels.
"""
image = OptimizedImageField(
upload_to="uploads/%Y/%m/%d",
optimized_image_output_size=(400, 300),
optimized_image_resize_method="cover" # "crop", "cover", "contain", "width", "height", "thumbnail" or None
)


and saving images into it, the same way you would to a Django ``ImageField``.
The optimized image will be saved into the ``url`` field in place of the
unoptimized image.


5. Or you can directly use the ``image_optimizer`` function from utils.

::

from image_optimizer.utils import image_optimizer


def post_image(request):
image_data = request.FILES.get('image')
image_data = image_optimizer(image_data=image_data,
output_size=(400, 300),
resize_method='cover')
....


**P.S:**

Note about TinyPNG API keys: If you obtain the free TinyPNG API token, you are limited to 500
image optimizations per month, so this function may fail if you have a
lot of images. You may either obtain a paid API key, or wait until next month.

This project also taken from: https://github.com/dchukhin/django_optimized_image
This project forked from: https://github.com/dchukhin/django_optimized_image
58 changes: 52 additions & 6 deletions image_optimizer/fields.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db.models import ImageField
from .utils import image_optimizer


class OptimizedImageField(ImageField):
Expand All @@ -10,9 +8,57 @@ class OptimizedImageField(ImageField):
def save_form_data(self, instance, data):
"""Remove the OptimizedNotOptimized object on clearing the image."""
# Are we updating an image?
updating_image = True if data and getattr(instance, self.name) != data else False
updating_image = (
True if data and getattr(instance, self.name) != data else False
)

if updating_image:
from .utils import image_optimizer
data = image_optimizer(data)
data = image_optimizer(
data,
self.optimized_image_output_size,
self.optimized_image_resize_method,
)

super().save_form_data(instance, data)

def __init__(
self,
optimized_image_output_size=None,
optimized_image_resize_method=None,
*args,
**kwargs
):
"""
Initialize OptimizedImageField instance.

set up the `optimized_image_output_size` and
`optimized_image_resize_method` arguments for the current
`OptimizedImageField` instance.
"""
# Set the optimized_image_output_size specified on your
# OptimizedImageField model instances
self.optimized_image_output_size = optimized_image_output_size

# Set the optimized_image_resize_method specified on your
# OptimizedImageField model instances
self.optimized_image_resize_method = optimized_image_resize_method

super().__init__(**kwargs)

def deconstruct(self):
"""
Deconstruct method.

deconstruct the field, allowing us to handle the field data, useful
in cases where you want to add optional arguments to your custom
field but you need to exclude them from migrations.
"""
name, path, args, kwargs = super().deconstruct()

if kwargs.get("optimized_image_output_size"):
del kwargs["optimized_image_output_size"]

if kwargs.get("optimized_image_resize_method"):
del kwargs["optimized_image_resize_method"]

return name, path, args, kwargs
7 changes: 2 additions & 5 deletions image_optimizer/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.conf import settings

OPTIMIZED_IMAGE_METHOD = getattr(settings, 'OPTIMIZED_IMAGE_METHOD', 'tinypng')
TINYPNG_KEY = getattr(settings, 'TINYPNG_KEY', None)
OPTIMIZED_IMAGE_METHOD = getattr(settings, "OPTIMIZED_IMAGE_METHOD", "pillow")
TINYPNG_KEY = getattr(settings, "TINYPNG_KEY", None)
140 changes: 127 additions & 13 deletions image_optimizer/utils.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,146 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import tinify
import logging
import requests
from io import BytesIO
from PIL import Image
from resizeimage import resizeimage
from uuid import uuid4

from .settings import OPTIMIZED_IMAGE_METHOD, TINYPNG_KEY


BACKGROUND_TRANSPARENT = (255, 255, 255, 0)

import tinify
import requests

from .settings import (OPTIMIZED_IMAGE_METHOD, TINYPNG_KEY)
def get_file_name(image_data):
return image_data.name


def image_optimizer(image_data):
"""Optimize an image that has not been saved to a file."""
if OPTIMIZED_IMAGE_METHOD == 'pillow':
def get_file_extension(file_name):
extension = None
# Get image file extension
if file_name.split(".")[-1].lower() != "jpg":
extension = file_name.split(".")[-1].upper()
else:
extension = "JPEG"
return extension


def get_image_extension(image):
return image.format


def image_optimizer(image_data, output_size=None, resize_method=None):
"""
Optimize an image that has not been saved to a file.
:param `image_data` is image data, e.g from request.FILES['image']
:param `output_size` is float pixel scale of image (width, height) or None, for example: (400, 300) # noqa: E501
:param `resize_method` is string resize method, choices are:
None or resizeimage.resize() method argument values,
i.e: "crop", "cover", "contain", "width", "height", "thumbnail"
:return optimized image data.
"""
if OPTIMIZED_IMAGE_METHOD == "pillow":
image = Image.open(image_data)
bytes_io = BytesIO()
extension = image.format
image.save(bytes_io, format=extension, optimize=True)

extension = get_image_extension(image)

# If output_size is set, resize the image with the selected
# resize_method. 'thumbnail' is used by default
if output_size is not None:
if resize_method:
image = resizeimage.resize(
method=resize_method,
image=image,
size=output_size,
)

output_image = Image.new(
"RGBA",
output_size,
BACKGROUND_TRANSPARENT,
)
output_image_center = (
int((output_size[0] - image.size[0]) / 2),
int((output_size[1] - image.size[1]) / 2),
)
output_image.paste(image, output_image_center)
else:
# If output_size is None the output_image
# would be the same as source
output_image = image

# If the file extension is JPEG, convert the output_image to RGB
if extension == "JPEG":
output_image = output_image.convert("RGB")

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

image_data.seek(0)
image_data.file.write(bytes_io.getvalue())
image_data.file.truncate()
elif OPTIMIZED_IMAGE_METHOD == 'tinypng':

elif OPTIMIZED_IMAGE_METHOD == "tinypng":
# disable warning info
requests.packages.urllib3.disable_warnings()

# just info for people
if any([output_size, resize_method]):
message = (
'[django-image-optimizer] "output_size" and "resize_method" '
'only for OPTIMIZED_IMAGE_METHOD="pillow"'
)
logging.info(message)

tinify.key = TINYPNG_KEY
optimized_buffer = tinify.from_buffer(image_data.file.read()).to_buffer()
optimized_buffer = tinify.from_buffer(
image_data.file.read()
).to_buffer() # noqa: E501
image_data.seek(0)
image_data.file.write(optimized_buffer)
image_data.file.truncate()

return image_data


def crop_image_on_axis(image, width, height, x, y, extension):
"""
function to crop the image using axis (using Pillow).
:param `image` is image data, e.g from request.FILES['image']
:param `width` float width of image
:param `height` float height of image
:param `x` is float x axis
:param `y` is float y axis
:param `extension` is string, e.g: ".png"
"""
# Open the passed image
img = Image.open(image)

# Initialise bytes io
bytes_io = BytesIO()

# crop the image through axis
img = img.crop((x, y, width + x, height + y))

# resize the image and optimise it for file size,
# making smaller as possible
img = img.resize((width, height), Image.ANTIALIAS)

# This line is optional, for safe side, image name should be unique.
img.name = "{}.{}".format(uuid4().hex, extension)

# If the file extension is JPEG, convert the output_image to RGB
if extension == "JPEG":
img = image.convert("RGB")
img.save(bytes_io, format=extension, optimize=True)

# return the image
image.seek(0)

# Write back new image
image.file.write(bytes_io.getvalue())

# truncate the file size
image.file.truncate()
return image
32 changes: 26 additions & 6 deletions image_optimizer_demo/app_demo/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.contrib import admin
from app_demo.models import Post
from app_demo.models import (
Post,
Collaborator,
OtherImage,
)
from app_demo.forms import CropImageAxisForm


class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'created']
list_filter = ['created']
list_display = ["title", "created"]
list_filter = ["created"]


class CollaboratorAdmin(admin.ModelAdmin):
list_display = ["name", "created"]
list_filter = ["created"]


class CropImageAxisAdmin(admin.ModelAdmin):
list_display = ["created", "image"]
list_filter = ["created"]
form = CropImageAxisForm

def get_form(self, request, *args, **kwargs):
form = super().get_form(request, *args, **kwargs)
form.request = request
return form


admin.site.register(Post, PostAdmin)
admin.site.register(Collaborator, CollaboratorAdmin)
admin.site.register(OtherImage, CropImageAxisAdmin)
5 changes: 1 addition & 4 deletions image_optimizer_demo/app_demo/apps.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.apps import AppConfig


class AppDemoConfig(AppConfig):
name = 'app_demo'
name = "app_demo"
Loading