Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for signatures and previews #2

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
156 changes: 139 additions & 17 deletions diploma.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,167 @@
from template import import_templates
from PIL import Image, ImageDraw, ImageFont, ImageColor

FONT_NAME = 'DejaVuSans.ttf'

def draw_centered_full_size(context, size, field):
width, height = size
text = field.value
box_width = int(width * field.w)
box_height = int(height * field.h)
box_x = int(width * field.x)
box_y = int(height * field.y)

class UnknownFields(ValueError):
pass


class MissingFields(ValueError):
pass


def draw_box_with_name(context, size, field, name):
"""Draws a rectangle where the field should be placed"""
rect = get_rect(field, size)
color = faded_background = ImageColor.getrgb(field.color)
if len(color) == 4:
# Fade the text color if possible
faded_background = color[0:2] + color[3] / 4
else:
# Transparent white
faded_background = (255, 255, 255, 64)
context.rectangle(rect, outline=color, fill=faded_background)

font = get_font_that_fits_in_box(name, rect)
center = center_text_in_rect(font.getsize(name), rect)
context.text(center, name, font=font, fill=ImageColor.getrgb(field.color))


def get_font_that_fits_in_box(text, rect):
"""Returns a font that can be used to draw text into a given rect
without overflowing its boundaries."""
x1, y1, x2, y2 = rect
width = x2 - x1
height = y2 - y1
fontsize = 1
# Dirty hack to find the largest text we can fit in this box
font = ImageFont.truetype('DejaVuSans.ttf', fontsize)
while font.getsize(text)[1] < field.h * height and font.getsize(text)[0] < field.w * width:
font = ImageFont.truetype(FONT_NAME, fontsize)
while font.getsize(text)[1] < height and font.getsize(text)[0] < width:
fontsize += 1
font = ImageFont.truetype('DejaVuSans.ttf', fontsize)
font = ImageFont.truetype(FONT_NAME, fontsize)
fontsize -= 1
font = ImageFont.truetype('DejaVuSans.ttf', fontsize)
return ImageFont.truetype(FONT_NAME, fontsize)


def get_rect(field, size):
"""Returns two points that define the upper left and bottom right points
in a rectangle for the given field."""
width, height = size
x1 = int(width * field.x)
y1 = int(height * field.y)
x2 = x1 + int(width * field.w)
y2 = y1 + int(height * field.h)

return (x1, y1, x2, y2)

text_width, text_height = font.getsize(text)
center = (box_x + (box_width - text_width) / 2, box_y + (box_height - text_height) / 2)

def center_text_in_rect(textsize, rect):
w, h = textsize
x1, y1, x2, y2 = rect
rect_w, rect_h = x2 - x1, y2 - y1
return (x1 + (rect_w - w) / 2, y1 + (rect_h - h) / 2)


def draw_centered_full_size(context, size, field):
"""Draws the provided fields value onto the context.
The value will be centered and shown using using the largest
font size that won't overflow its bounding box."""
text = field.value
rect = get_rect(field, size)
font = get_font_that_fits_in_box(text, rect)
center = center_text_in_rect(font.getsize(text), rect)
context.text(center, field.value, font=font, fill=ImageColor.getrgb(field.color))


def draw_scaled_signature(image, signature):
"""Draws the signature field onto the image.
The signature will be scaled to fit into its area
while preserving its aspect ratio."""
x1, y1, x2, y2 = get_rect(signature, image.size)
size = x2 - x1, y2 - y1
signature = Image \
.open(signature.value) \
.convert('RGBA')
signature.thumbnail(size)
image.paste(signature, box=(x1, y1))


def create_diploma_image(template):
"""Turns a valid template into a PNG image diploma by drawing
the fields (and signature, if applicable) onto the template base
"""
base_template = Image.open(template.path).convert('RGBA')
text = Image.new('RGBA', base_template.size, (255, 255, 255, 0))
context = ImageDraw.Draw(text)

for field in template.fields.values():
for name, field in template.fields.items():
draw_centered_full_size(context, base_template.size, field)

if template.signature and template.signature.value:
draw_scaled_signature(text, template.signature)

return Image.alpha_composite(base_template, text)


def create_template_preview(template, size=None):
"""Creates a preview version of the template where all fields are
marked with a semi-transparent rectangle containing the field name.
If the size argument is provided the it is used to constrain
the size of the image while preserving aspect ratio.
"""
base_template = Image.open(template.path).convert('RGBA')
if size:
base_template.thumbnail(size)
text = Image.new('RGBA', base_template.size, (255, 255, 255, 0))
context = ImageDraw.Draw(text)

for name, field in template.fields.items():
draw_box_with_name(context, base_template.size, field, name)

if template.signature is not None:
draw_box_with_name(context, base_template.size, template.signature, 'signature')

return Image.alpha_composite(base_template, text)


def create_signature_preview(signature, size):
"""Creates a preview version of the signature.
If the size argument is provided the it is used to constrain
the size of the image while preserving aspect ratio.
"""
base_signature = Image.open(signature).convert('RGBA')
if size:
base_signature.thumbnail(size)
return base_signature


def generate_diploma(template_name, **fields):
t = import_templates('templates/templates.json')[template_name]
"""Creates a diploma by matching the items in the fields
dictionary to the values contained in the template. Raises MissingFields
if the dict does not contain all of the required fields and KeyError
if the dictionary contains a field that does not fit into this template.
Returns an instance of PIL.Image."""
t = import_templates()[template_name]

path = fields.pop('signature', None)
if path and t.signature is not None:
t.signature = path

for k, v in fields.items():
t.fields[k].value = v

if not t.valid:
raise ValueError(','.join(t.missing))
if not t.valid and t.empty_fields:
raise MissingFields(','.join(t.empty_fields))

return create_diploma_image(t)


def preview_template(template_name, size):
template = import_templates()[template_name]
return create_template_preview(template, size)


def preview_signature(signature_path, size):
return create_signature_preview(signature_path, size)
Binary file added favicon.ico
Binary file not shown.
78 changes: 66 additions & 12 deletions server.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,97 @@
from io import BytesIO
from flask import Flask, send_file, request
from json import dumps
from diploma import generate_diploma
from template import valid_template_names
from diploma import generate_diploma, UnknownFields, \
MissingFields, preview_template, preview_signature
from template import import_templates, BadSignature, \
frontend_templates_as_json, frontend_signatures_as_json
import os

app = Flask(__name__)


def serve_diploma_image(diploma_image):
def get_size_from_query_params():
width = request.args.get('width', None)
height = request.args.get('height', None)

if not (width or height):
size = None
elif not width and height:
size = (height, height)
elif not height and width:
size = (width, width)
else:
size = (height, width)

if size:
size = tuple(int(dim) for dim in size)

return size


def serve_image(diploma_image):
iostream = BytesIO()
diploma_image.save(iostream, 'PNG')
iostream.seek(0)
return send_file(iostream, mimetype='image/png')


@app.route('/templates.json')
def serve_templates():
return send_file('templates/templates.json')
@app.route('/<data>.json')
def serve_json_file(data):
if data == "templates":
payload = frontend_templates_as_json()
elif data == "signatures":
payload = frontend_signatures_as_json()
else:
return my404('not found')

return (payload, 200, {"content-type": "application/json"})


@app.route('/preview/signature/<filename>')
def serve_signature_preview(filename):
signature_path = f"signatures/{filename}.png"
size = get_size_from_query_params()
return serve_image(preview_signature(signature_path, size))


@app.route('/preview/template/<filename>')
def serve_preview_image(filename):
size = get_size_from_query_params()
return serve_image(preview_template(filename, size))


@app.route('/<template_name>')
def serve_diploma(template_name):
template_name = template_name.lower()
if template_name not in valid_template_names():
return my404('whoops')

kwargs = {k: ' '.join(v) for k, v in dict(request.args).items()}
dip = generate_diploma(template_name, **kwargs)
return serve_diploma_image(dip)
finished_diploma = generate_diploma(template_name, **kwargs)
return serve_image(finished_diploma)


@app.route('/favicon.ico')
def favicon():
return send_file(
'favicon.ico',
mimetype='image/vnd.microsoft.icon'
)


@app.errorhandler(BadSignature)
def bad_signature(error):
payload = dumps({"bad_signature": str(error)})
return (payload, 422, {"content-type": "application/json"})


@app.errorhandler(ValueError)
@app.errorhandler(MissingFields)
def missing_query_params(error):
missing_fields = str(error).split(',')
payload = dumps({"missing_fields": missing_fields})
return (payload, 422, {"content-type": "application/json"})


@app.errorhandler(KeyError)
@app.errorhandler(UnknownFields)
def superfluous_query_params(error):
superfluous_fields = str(error)[1:-1].split(',')
payload = dumps({"superfluous_fields": superfluous_fields})
Expand Down
Binary file added signatures/donaldtrump.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added signatures/gavinbelson.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added signatures/harrypotter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions signatures/signatures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[{
"id": "gavinbelson",
"name": "Gavin Belson"
},
{
"id": "waltdisney",
"name": "Walt Disney"
},
{
"id": "donaldtrump",
"name": "Donald Trump"
},
{
"id": "harrypotter",
"name": "Harry Potter"
}]
Binary file added signatures/waltdisney.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading