Last active
May 19, 2017 17:28
-
-
Save feluxe/e4cb86e2ea3ca72a0e821b3dfa07e175 to your computer and use it in GitHub Desktop.
Example for functions with multi-line parameters in Python.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import hug | |
import os | |
from typing import Optional, Tuple, Union | |
from image_server.constants import DATA_DIR, MAXIMUM_WIDTH_LIMIT | |
from PIL import Image | |
from PIL import ImageFont | |
from PIL import ImageDraw | |
def _make_str_from_render_query(render_query: dict): | |
""" | |
This function takes the data from the render query and produces a string that is used in the | |
file name of the requested image. An example of such string: 'w=1100&q=18' | |
""" | |
render_query_str: str = ''.join( | |
['&' + str(key) + '=' + str(val) for key, val in render_query.items() if val]) | |
if render_query_str != '': | |
render_query_str: str = '?' + render_query_str[1:] | |
return render_query_str | |
def _get_path_to_requested_image( | |
item_info: dict, | |
render_query: dict | |
) -> str: | |
"""""" | |
render_query_str: str = _make_str_from_render_query(render_query) | |
image_name: str = item_info['main_dir'] + '/' + item_info['sub_dir'] + '/' + item_info[ | |
'basename'] + render_query_str + item_info['ext'] | |
img_file: str = os.path.join(DATA_DIR, image_name) | |
return img_file | |
def _get_path_to_source_image(item_info: dict) -> str: | |
return DATA_DIR + '/' + item_info['main_dir'] + '/' + item_info['sub_dir'] + '/' + \ | |
item_info['basename'] + item_info['ext'] | |
def _draw_width_as_text_on_image( | |
loaded_image: Image, | |
new_w: int, | |
new_h: int | |
) -> None: | |
"""This function draws @new_w onto the image. (This is used for debugging.)""" | |
draw = ImageDraw.Draw(loaded_image) | |
font = ImageFont.truetype("Como-Regular.otf", 45) | |
text_w, text_h = draw.textsize(str(new_w), font=font) | |
draw.text(((new_w - text_w) / 2, (new_h - text_h) / 2), str(new_w), (255, 0, 0), font=font) | |
def _get_scaled_dims( | |
native_w: int, | |
native_h: int, | |
target_w: Optional[int] = None, | |
target_h: Optional[int] = None | |
) -> Tuple[int, int]: | |
"""Calculate scaled image dimensions.""" | |
invalid_args_msg = 'Please provide a value for "target_width" OR "target_height".' | |
if target_w and target_h: | |
raise Exception(invalid_args_msg) | |
elif not target_w and not target_h: | |
raise Exception(invalid_args_msg) | |
elif target_w: | |
return target_w, int((target_w / native_w) * native_h) | |
elif target_h: | |
return int((target_h / native_h) * native_w), target_h | |
def _prepare_target_width( | |
target_width: Optional[int], | |
maximum_width_limit: Optional[int], | |
native_width: int | |
) -> int: | |
""" | |
If no width was requested in the URL query use native width from image. | |
If the resulting width exceeds @maximum_width_limit, use @maximum_width_limit. | |
If the resulting width exceeds the native width of the image, use native width. | |
""" | |
target_w: int = target_width or native_width | |
target_w: int = maximum_width_limit if maximum_width_limit and \ | |
target_w > maximum_width_limit else target_w | |
target_w: int = native_width if native_width < target_w else target_w | |
return target_w | |
def _get_new_dimensions( | |
loaded_image: Image, | |
requested_w: Optional[int], | |
maximum_width_limit: Optional[int] | |
) -> tuple: | |
""" | |
Calculate new image dimensions. | |
This takes @requested_w and @maximum_with_limit into account. | |
""" | |
dims: tuple = loaded_image.size | |
native_w: int = dims[0] | |
native_h: int = dims[1] | |
target_w: int = _prepare_target_width(requested_w, maximum_width_limit, native_w) | |
dims_after_scale: tuple = _get_scaled_dims(native_w, native_h, target_w) | |
scaled_w: int = dims_after_scale[0] | |
scaled_h: int = dims_after_scale[1] | |
return scaled_w, scaled_h | |
def _validate_jpg_quality(input_quality: int): | |
return input_quality in range(0, 100) | |
def _process_image( | |
source_image: str, | |
requested_image: str, | |
render_query: dict | |
): | |
""" | |
Load @source_image, then use data from @render_query to render a new image. Save the new image | |
at the path that is @requested_image. | |
""" | |
requested_w: Optional[int] = render_query['w'] | |
requested_q: Optional[int] = render_query['q'] | |
maximum_width_limit: Optional[int] = render_query['maximum_width_limit'] | |
draw_width: bool = render_query['draw_width'] | |
loaded_image: Image = Image.open(source_image) | |
img_format: str = loaded_image.format | |
dest: str = requested_image | |
if requested_w or draw_width: | |
new_dims: tuple = _get_new_dimensions(loaded_image, requested_w, maximum_width_limit) | |
new_w: int = new_dims[0] | |
new_h: int = new_dims[1] | |
if requested_w: | |
loaded_image.thumbnail((new_w, new_h), Image.ANTIALIAS) | |
if draw_width: | |
_draw_width_as_text_on_image(loaded_image, new_w, new_h) | |
if 'JPEG' in img_format: | |
quality = int(requested_q) if requested_q and _validate_jpg_quality(requested_q) else False | |
loaded_image.save(dest, optimize=True, quality=quality, progressive=True) | |
elif 'PNG' in img_format: | |
loaded_image.save(dest, optimize=True) | |
else: | |
loaded_image.save(dest) | |
def _create_item_info_object( | |
main_dir: str, | |
sub_dir: str, | |
img_name: str | |
) -> dict: | |
""" | |
This dict contains data with which you can create file paths for the source file and output | |
file. | |
""" | |
splitted_filename: tuple = os.path.splitext(img_name) | |
item_info = { | |
'main_dir': main_dir, # type: str | |
'sub_dir': sub_dir, # type: str | |
'filename': img_name, # type: str | |
'basename': splitted_filename[0], # type: str | |
'ext': splitted_filename[1], # type: str | |
} | |
return item_info | |
def _create_render_query_object( | |
w: Optional[str], | |
q: Optional[str], | |
draw_width: Optional[str] | |
) -> dict: | |
""" | |
This dict contains the data that defines how the image should be rendered. | |
""" | |
make_int_or_none = lambda item: int(item) if item and item.isdigit() else None | |
render_query = { | |
'w': make_int_or_none(w), # type: Optional[int] | |
'q': make_int_or_none(q), # type: Optional[int] | |
'draw_width': draw_width, | |
'maximum_width_limit': make_int_or_none(MAXIMUM_WIDTH_LIMIT) # type: Optional[int] | |
} | |
return render_query | |
@hug.get('/images/{main_dir}/{sub_dir}/{filename}', output=hug.output_format.file) | |
def get_image( | |
main_dir: str, | |
sub_dir: str, | |
filename: str, | |
w: str = None, | |
q: str = None, | |
draw_width: str = None | |
): | |
""" | |
Serve requested image. | |
If the image does not exist render it according to the query, then serve it. | |
Future requests with the same query will skip the rendering and load the image immediately. | |
""" | |
item_info: dict = _create_item_info_object(main_dir, sub_dir, filename) | |
render_query: dict = _create_render_query_object(w, q, draw_width) | |
requested_image: str = _get_path_to_requested_image(item_info, render_query) | |
if not os.path.isfile(requested_image): | |
source_image: str = _get_path_to_source_image(item_info) | |
_process_image(source_image, requested_image, render_query) | |
return requested_image |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment