Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
122 changes: 110 additions & 12 deletions frappe/utils/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
# MIT License. See license.txt

from __future__ import unicode_literals, print_function

import frappe

from frappe.utils import cint
from PIL import Image

import traceback
import frappe
import io
import os
import re
import base64

IMAGE_CACHE_EXPIRES = 900

RESAMPLING = {
"Nearest": Image.NEAREST,
Expand Down Expand Up @@ -42,9 +46,9 @@ def resize_images(path, maxdim=700):
print("resized {0}".format(os.path.join(basepath, fname)))

def image_resize_url(path, size):
return "{}?size={}".format(path.replace('/files/', '/resize/'), size)
return "/resize/{}?size={}".format(path.lstrip('/'), size)

def process_thumbnail(path, options):
def sanitize_image_path(path):

if ('.' not in path):
return False
Expand All @@ -55,14 +59,110 @@ def process_thumbnail(path, options):
if extn not in ('jpg', 'jpeg', 'png', 'gif', 'bmp'):
return False

filepath = frappe.utils.get_site_path(path)
if re.match(r"^\/?files\/", path):
filepath = frappe.utils.get_site_path(*("public/{}".format(path.lstrip('/')).split('/')))
elif path.startswith("/assets/"):
filepath = path
else:
return False

return (filepath, filename, extn)

def get_image_resize_preset(name):
resize_fields = ["width", "height", "resample", "quality"]

if frappe.db.exists("Image Resize Preset", name):
preset = frappe.get_all("Image Resize Preset", filters={"name": name}, fields=resize_fields)
else:
preset = frappe.get_all("Image Resize Preset", filters={"name": "small"}, fields=resize_fields)

return preset[0]

def image_to_base64(path, resize_preset_name=None, cache=False):

path_info = sanitize_image_path(path)
if not path_info:
return False

filepath, extn = (path_info[0], path_info[2])

cache_key = "base64_image_cache|{}|{}".format(resize_preset_name or "_", filepath)
cache_timeout = 900

if cache:
# Build cache path for this image and retrieve data
data = frappe.cache().get_value(cache_key, None, None, True)
if data:
return data

try:
img = Image.open(filepath)
except IOError:
traceback.print_exc()
return path

# Enforce image format
image_format = IMAGE_FORMAT_MAP.get(extn.upper(), "JPEG")

if resize_preset_name:
preset = get_image_resize_preset(resize_preset_name)
cache_timeout = preset.get("cache_timeout", IMAGE_CACHE_EXPIRES)
# Enforce image format
image_format = IMAGE_FORMAT_MAP.get(extn.upper(), "JPEG")
buffer = resize_image(img, preset, image_format)

if not buffer:
return False
else:
try:
img.save(buffer, format=image_format)
except Exception:
traceback.print_exc()

return False

data = "data:image/{};base64,{}".format(image_format.lower(), buffer_to_base64(buffer))

if cache:
# Build cache path for this image and retrieve data
frappe.cache().set_value(cache_key, data, None, cache_timeout)

return data

def buffer_to_base64(buffer):
"""Converts a buffer to a base64 string representation.

Useful to convert images to base64 strings."""

return base64.b64encode(buffer.getvalue()).decode()

def process_thumbnail(path, options):

path_info = sanitize_image_path(path)
if not path_info:
return False

filepath, extn = (path_info[0], path_info[2])
buffer = io.BytesIO()

try:
img = Image.open(filepath)
except IOError:
raise NotFound

# Enforce image format
image_format = IMAGE_FORMAT_MAP.get(extn.upper(), "JPEG")

buffer = resize_image(img, options, image_format)

if not buffer:
return False

return buffer.getvalue()

def resize_image(img, options, image_format):
buffer = io.BytesIO()

# capture desired image width and height
width = cint(options.width or 0) or cint(options.size or 0)
height = cint(options.height or 0) or cint(options.size or 0)
Expand All @@ -82,29 +182,27 @@ def process_thumbnail(path, options):
# Actual image resize
img.thumbnail(size, resample)

# Enforce image format
format = IMAGE_FORMAT_MAP.get(extn.upper(), "JPEG")

# default image options for PIL processing
image_options = dict(
optimize=True,
progressive=True,
quality=quality
)

if format == "GIF":
if image_format == "GIF":
# For GIF Animations only so we keep all frames
image_options["save_all"] = True

try:
img.save(buffer,
format=format,
format=image_format,
**image_options
)
except Exception:
import traceback
traceback.print_exc()

return False


# Return image bytes
return buffer
Expand Down
88 changes: 49 additions & 39 deletions frappe/utils/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import mimetypes
import os
import frappe
import traceback
from frappe import _, _dict
import frappe.model.document
import frappe.utils
Expand All @@ -23,7 +24,6 @@
from six.moves.urllib.parse import quote
from frappe.core.doctype.access_log.access_log import make_access_log


def report_error(status_code):
'''Build error. Show traceback in developer mode'''
if (cint(frappe.db.get_system_setting('allow_error_traceback'))
Expand Down Expand Up @@ -185,52 +185,62 @@ def download_private_file(path):
return send_private_file(path.split("/private", 1)[1])

def resize_image(path):
"""Processes a /resize/<image file> path with optional resize, resampling and
quality settings while keeping its aspect ratio intact.
"""Processes a /resize/<image file path> path using a preset Image Resize Preset record.

Examples:
:param path: Url path of an image

Example Usage:

/resize/myimage.jpg?size=small
/resize/files/myimage.jpg?size=small

Where size refers to the name of a predefined "Image Resize Preset" record
"""

# Transform thumbnail path to public/files/ path
file_path = os.path.join('public', 'files', *os.path.split(path)[1:])
filename = os.path.basename(file_path)

# Get image resize preset or default to small if one isn't found
image_resize_preset_name = frappe.local.form_dict.size or "small"
if frappe.db.exists("Image Resize Preset", image_resize_preset_name):
image_resize_preset = frappe.get_doc("Image Resize Preset", image_resize_preset_name)
else:
image_resize_preset = frappe.get_doc("Image Resize Preset", "small")

# build image options
options = _dict({
key: image_resize_preset.get(key) \
for key in ("width", "height", "resample", "quality")
})

# Build cache path for this image and retrieve data
cache_path = "{}?size={}".format(path, image_resize_preset_name)
buffer = frappe.cache().hget("thumbnail_cache", cache_path)

if not buffer:
from frappe.utils.image import process_thumbnail
buffer = process_thumbnail(file_path, options)

# set cache only when generating a new thumbnail
frappe.cache().hset("thumbnail_cache", cache_path, buffer)
try:
# Transform thumbnail path to public/files/ path
file_path = os.path.join('/', *path.split('/')[2:])
filename = os.path.basename(file_path)

# Get image resize preset or default to small if one isn't found
image_resize_preset_name = frappe.local.form_dict.size or "small"

# Build cache path for this image and retrieve data
cache_key = "thumbnail_cache|{}|{}".format(image_resize_preset_name, file_path)
buffer = frappe.cache().get_value(cache_key, None, None, True)

if frappe.db.exists("Image Resize Preset", image_resize_preset_name):
image_resize_preset = frappe.get_doc("Image Resize Preset", image_resize_preset_name)
else:
image_resize_preset = frappe.get_doc("Image Resize Preset", "small")

# build image options
options = _dict({
key: image_resize_preset.get(key) \
for key in ("width", "height", "resample", "quality", "cache_timeout")
})

if not buffer:

from frappe.utils.image import process_thumbnail
buffer = process_thumbnail(file_path, options)
if buffer:
# set cache only when generating a new thumbnail
frappe.cache().set_value(cache_key, buffer, None, options.get("cache_timeout", 900))

if buffer:
response = Response(buffer, headers={
"Cache-Control": "max-age={}".format(options.get("cache_timeout", 900))
}, direct_passthrough=True)
response.mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
return response

except NotFound:
raise NotFound

if buffer:
response = Response(buffer.getvalue(), direct_passthrough=True)
response.mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
return response
except Exception:
traceback.print_exc()

else:
from werkzeug.exceptions import HTTPException, NotFound
raise NotFound
raise NotFound

def send_private_file(path):
path = os.path.join(frappe.local.conf.get('private_path', 'private'), path.strip("/"))
Expand Down
18 changes: 15 additions & 3 deletions frappe/website/doctype/image_resize_preset/image_resize_preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@
// For license information, please see license.txt

frappe.ui.form.on('Image Resize Preset', {
// refresh: function(frm) {

// }
refresh: function(frm) {
// view document button
if (!frm.is_new() && !frm.is_dirty()) {
frm.add_custom_button("Clear Cache", function() {
frm.call({
method: "clear_cache",
doc: frm.doc,
freeze: true,
callback: function() {
frappe.msgprint(`Cache Cleared for ${frm.doc.name} Preset`);
}
})
});
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"sb2",
"resample",
"cb2",
"quality"
"quality",
"sb3",
"cache_timeout"
],
"fields": [
{
Expand Down Expand Up @@ -76,9 +78,20 @@
"in_list_view": 1,
"label": "Quality",
"reqd": 1
},
{
"fieldname": "sb3",
"fieldtype": "Section Break",
"label": "Caching"
},
{
"default": "900",
"fieldname": "cache_timeout",
"fieldtype": "Int",
"label": "Cache Timeout in Seconds"
}
],
"modified": "2020-11-10 10:47:14.838106",
"modified": "2020-11-16 22:04:17.811138",
"modified_by": "Administrator",
"module": "Website",
"name": "Image Resize Preset",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
# For license information, please see license.txt

from __future__ import unicode_literals
# import frappe
import frappe
from frappe.model.document import Document

class ImageResizePreset(Document):
pass
def clear_cache(self):
frappe.cache().delete_keys("base64_image_cache|{}|*".format(self.name))
frappe.cache().delete_keys("thumbnail_cache|{}|*".format(self.name))