Images¶
Introduction¶
Websauna core doesn’t provide any special support for images. Images are served as any other static assets.
Dynamic image resizes¶
Below is a recipe for generating dynamic image rescales from URL sources. Images are cached in redis and served with proper HTTP caching headers.
resizer.py
:
from io import BufferedIOBase
from io import BytesIO
import logging
from pyramid.httpexceptions import HTTPNotImplemented
from pyramid.response import Response
from PIL import Image
from websauna.system.core.redis import get_redis
from websauna.system.http import Request
logger = logging.getLogger(__name__)
def resized_image(request: Request, cache_key: str, source: BufferedIOBase, width: int, height: int, cache_timeout=30*24*3600, source_content_type=None, format="png") -> Response:
"""Create a resized image version.
Results are cached in redis.
:param request: HTTP request related to this
:param source: Image source as byte stream
:param width: Desired with
:param height: Desired height
:param format: Output format. Either "png" or "jpeg".
:param cache_timeout: How long cached result is stored in Redis in seconds
:param source_format: Binary format hint for Pillow to decode image
:return: Cacheable HTTP response for the image
"""
# Sanity check and potentially prevent some abuse
if source_content_type:
if source_content_type not in ("image/jpeg", "image/png"):
logger.warning("Unsupported image rescale content type: %s", source_content_type)
return HTTPNotImplemented()
# Allow override cache for testing on when giving URL like:
# http://localhost:6543/cosmos/logo_small?redraw=true
# This is to fix potential single caes error
redraw = "redraw" in request.params
if redraw:
logger.warning("Forced redraw of image scale")
redis = get_redis(request)
full_cache_key = "image_resize_{}_{}_{}_{}".format(cache_key, width, height, format)
data = redis.get(full_cache_key)
if not data or redraw:
size = (width, height)
img = Image.open(source)
img = img.convert('RGBA')
img.thumbnail(size)
# Alternative cropper implementation
# img = ImageOps.fit(img, size, Image.ANTIALIAS)
buf = BytesIO()
img.save(buf, format=format)
data = buf.getvalue()
redis.set(full_cache_key, data, ex=cache_timeout)
buf.seek(0)
else:
buf = BytesIO(data)
# Streamable response so we don't cause a clog in the series of tubes
resp = Response(content_type="image/" + format, body_file=buf)
# Set cache headers for downstream web server
resp.cache_expires = cache_timeout
resp.cache_control.public = True
resp.headers["Content-length"] = str(len(data))
return resp
Example usage:
@view_config(context=AssetDescription, route_name="network", name="logo_small.png")
def logo_small(asset_desc: AssetDescription, request: Request):
"""Create a downscaled logo version for an asset.
.. note::
.png suffix in URL is required by some proxies (CloudFlare) to make the response caching to follow the normal caching rules.
"""
# We have a logo image URL for an item we wish to display
logo_url = asset_desc.asset.other_data.get("logo")
if not logo_url:
return HTTPNotFound()
# http://stackoverflow.com/a/37547880/315168
resp = requests.get(logo_url, stream=True)
resp.raise_for_status()
resp.raw.decode_content = True
source_content_type = resp.headers["Content-type"]
# Cache logos by asset human readable id
return resized_image(request, "logo_small_" + str(asset_desc.asset.slug), source=resp.raw, source_content_type=source_content_type, width=256, height=256, format="png")
Then in templates:
<td class="col-logo">
<a class=logo-link href="{{ asset_resource|model_url }}">
<img src="{{ asset_resource|model_url('logo_small') }}">
</a>
<a href="{{ asset_resource|model_url }}">
{{ asset_resource.asset.name }}
</a>
</td>