|
|
|
@ -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)
|
|
|
|
|