diff --git a/common/common/images.py b/common/common/images.py index 5cf5471..8825ded 100644 --- a/common/common/images.py +++ b/common/common/images.py @@ -1,27 +1,52 @@ +import json import os from io import BytesIO from PIL import Image -# The region of the frame image to place on the template. -# Format is (top_left_x, top_left_y, bottom_right_x, bottom_right_y). -# The frame is scaled to the size of the template before this is done. -FRAME_CROP = None # no crop +""" +A template is two files: + NAME.png + NAME.json +The image is the template image itself. +The JSON file contains the following: + { + crop: BOX, + location: BOX, + } +where BOX is a 4-tuple [left x, top y, right x, bottom y] describing a rectangle in image coordinates. -# The location in the template to place the frame image after cropping. -# Format is (x, y) of top-left corner. -FRAME_LOCATION = (0, 90) +To create a thumbnail, the input frame is first cropped to the bounds of the "crop" box, +then resized to the size of the "location" box, then pasted underneath the template image at that +location within the template image. +For example, a JSON file of: + { + "crop": [50, 100, 1870, 980], + "location": [320, 180, 1600, 900] + } +would crop the input frame from (50, 100) to (1870, 980), resize it to 720x1280, +and place it at (320, 180). + +If the original frame and the template differ in size, the frame is first resized to the template. +This allows you to work with a consistent coordinate system regardless of the input frame size. +""" def compose_thumbnail_template(base_dir, template_name, frame_data): template_path = os.path.join(base_dir, "thumbnail_templates", f"{template_name}.png") + info_path = os.path.join(base_dir, "thumbnail_templates", f"{template_name}.json") + template = Image.open(template_path) # PIL can't load an image from a byte string directly, we have to pretend to be a file frame = Image.open(BytesIO(frame_data)) - # The parameters of how we overlay the template are hard-coded for now. - # We can make this configurable later if needed. + with open(info_path) as f: + info = json.load(f) + crop = info['crop'] + loc_left, loc_top, loc_right, loc_bottom = info['location'] + location = loc_left, loc_top + location_size = loc_right - loc_left, loc_bottom - loc_top # Create a new blank image of the same size as the template result = Image.new('RGBA', template.size) @@ -30,10 +55,11 @@ def compose_thumbnail_template(base_dir, template_name, frame_data): # and we don't really care about performance. if frame.size != template.size: frame = frame.resize(template.size, Image.LANCZOS) - # Insert the frame at the desired location and cropping - if FRAME_CROP is not None: - frame = frame.crop(FRAME_CROP) - result.paste(frame, FRAME_LOCATION) + # Insert the frame at the desired location, cropping and scaling. + # Technically we might end up resizing twice here which is bad for quality, + # but the case of frame size != template size should be rare enough that it doesn't matter. + frame = frame.crop(crop).resize(location_size, Image.LANCZOS) + result.paste(frame, location) # Place the template "on top", letting the frame be seen only where the template's alpha # lets it through. result.alpha_composite(template)