Skip to content
Merged
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
5 changes: 5 additions & 0 deletions 31/bbelderbos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*swp
*pyc
__pycache__
images
out.png
41 changes: 41 additions & 0 deletions 31/bbelderbos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## PyBites Code Challenge 31 - Image Manipulation With Pillow

### Submission: PyBites Banner Generator

I made this utility to create quick but nice banners for PyBites Articles / Challenges / News / Special. However I think the code can easily be extended or modified to use it for your own needs.

It takes an existing PyBites logo as the first image, and downloads (caches) the second image.

*Text for Banner* is for the text to be put on the banner. `SourceSansPro-Regular.otf` gave me a weird char for newline (`\n`) so I switched to `Ubuntu-R.ttf`. Of course you could build this out to also let the user choose the font type and more things. The underlying `banner.py` script should make this easy: all font settings are collected into a namedtuple before passing it to the image creating class.

The last option of the form controls the location of the second image titled *Use Second Image as Background?*. By default this is turned on and the second image serves as a background image (example 2). If you disable it, the image will be resized to thumbnail and aligned to the right (example 1 / as in [the article](https://pybit.es/pillow-banner-image.html)).

#### Example 1. - make a PyBites Code Challenge banner

1. Choose *challenge* as first image, provide URL to the second image, add banner text, disable *Use Second Image as Background?*:

![example1a.png](assets/readme/example1a.png)

2. Click *Generate banner*:

![example1b.png](assets/readme/example1b.png)

3. Right-click and save the image.

#### Example 2. - make a PyBites News banner

1. Choose *news* as first image, provide URL to the second image, add banner text, leave *Use Second Image as Background?* enabled:

![example2a.png](assets/readme/example2a.png)

2. Click *Generate banner*:

![example2b.png](assets/readme/example2b.png)

3. Right-click and save the image.

### PyBites articles:

* [Using Pillow to Create Nice Banners For Your Site](https://pybit.es/pillow-banner-image.html)

* This week I will write a part 2 how I wrapped the original `banner.py` (command line) script into this Flask app. If I don't get to update this readme, the previous article will link to it ...
57 changes: 57 additions & 0 deletions 31/bbelderbos/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
import requests

from flask import Flask, abort, render_template, request, send_file

from forms import ImageForm
from banner.banner import generate_banner
from banner.banner import DEFAULT_OUTPUT_FILE as outfile

IMAGES = 'images'

app = Flask(__name__)


def _download_image(from_url, to_file, chunk_size=2000):
r = requests.get(from_url, stream=True)

with open(to_file, 'wb') as fd:
for chunk in r.iter_content(chunk_size):
fd.write(chunk)


def get_image(image_url):
basename = os.path.basename(image_url)
local_image = os.path.join(IMAGES, basename)

if not os.path.isfile(local_image):
_download_image(image_url, local_image)

return local_image


@app.route('/', methods=['GET', 'POST'])
def image_inputs():
form = ImageForm(request.form)

if request.method == 'POST' and form.validate():
image1 = form.image_url1.data
image2 = get_image(form.image_url2.data)
text = form.text.data
print(text)
background = form.background.data

args = [image1, image2, text, background]

generate_banner(args)

if os.path.isfile(outfile):
return send_file(outfile, mimetype='image/png')
else:
abort(400)

return render_template('imageform.html', form=form)


if __name__ == "__main__":
app.run(debug=True)
Binary file added 31/bbelderbos/assets/SourceSansPro-Regular.otf
Binary file not shown.
Binary file added 31/bbelderbos/assets/Ubuntu-R.ttf
Binary file not shown.
Binary file added 31/bbelderbos/assets/pillow-logo.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 31/bbelderbos/assets/pybites/article.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 31/bbelderbos/assets/pybites/challenge.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 31/bbelderbos/assets/pybites/news.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 31/bbelderbos/assets/pybites/special.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 31/bbelderbos/assets/readme/example1a.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 31/bbelderbos/assets/readme/example1b.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 31/bbelderbos/assets/readme/example2a.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 31/bbelderbos/assets/readme/example2b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions 31/bbelderbos/banner/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
out*png
Empty file.
129 changes: 129 additions & 0 deletions 31/bbelderbos/banner/banner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from collections import namedtuple
import os
import sys

from PIL import Image, ImageDraw, ImageFont

ASSET_DIR = 'assets'
DEFAULT_WIDTH = 600
DEFAULT_HEIGHT = 150
DEFAULT_CANVAS_SIZE = (DEFAULT_WIDTH, DEFAULT_HEIGHT)
DEFAULT_OUTPUT_FILE = 'out.png'
RESIZE_PERCENTAGE = 0.8
DEFAULT_TOP_MARGIN = int(((1 - 0.8) * DEFAULT_HEIGHT) / 2)
WHITE, BLACK = (255, 255, 255), (0, 0, 0)
WHITE_TRANSPARENT_OVERLAY = (255, 255, 255, 178)
TEXT_SIZE = 24
TEXT_FONT_TYPE = os.path.join(ASSET_DIR, 'Ubuntu-R.ttf')
TEXT_PADDING_HOR, TEXT_PADDING_VERT = 10, 40

Font = namedtuple('Font', 'ttf text color size offset')
ImageDetails = namedtuple('Image', 'left top size')


class Banner:
def __init__(self, size=DEFAULT_CANVAS_SIZE,
bgcolor=WHITE, output_file=DEFAULT_OUTPUT_FILE):
'''Creating a new canvas'''
self.size = size
self.width = size[0]
self.height = size[1]
self.bgcolor = bgcolor
self.output_file = output_file
self.image = Image.new('RGBA', self.size, self.bgcolor)
self.image_coords = []

def _image_gt_canvas_size(self, img):
return img.size[0] > self.image.size[0] or \
img.size[1] > self.image.size[1]

def add_image(self, image, resize=False,
top=DEFAULT_TOP_MARGIN, left=0, right=False):
'''Adds (pastes) image on canvas
If right is given calculate left, else take left
Returns added img size'''
img = Image.open(image)

if resize or self._image_gt_canvas_size(img):
size = self.height * RESIZE_PERCENTAGE
img.thumbnail((size, size), Image.ANTIALIAS)

if right:
left = self.image.size[0] - img.size[0]

offset = (left, top)
self.image.paste(img.convert('RGBA'), offset, mask=img.convert('RGBA'))

img_details = ImageDetails(left=left, top=top, size=img.size)
self.image_coords.append(img_details)

def add_text(self, font):
'''Adds text on a given image object'''
draw = ImageDraw.Draw(self.image)
pillow_font = ImageFont.truetype(font.ttf, font.size)

if font.offset:
offset = font.offset
else:
# if no offset given put text alongside first image
left_image_px = min(img.left + img.size[0]
for img in self.image_coords)
offset = (left_image_px + TEXT_PADDING_HOR,
TEXT_PADDING_VERT)

draw.text(offset, font.text, font.color, font=pillow_font)

def add_background(self, image, resize=False):
img = Image.open(image).convert('RGBA')

overlay = Image.new('RGBA', img.size, WHITE_TRANSPARENT_OVERLAY)
bg_img = Image.alpha_composite(img, overlay)

if resize:
bg_size = (self.width * RESIZE_PERCENTAGE, self.height)
bg_img.thumbnail(bg_size, Image.ANTIALIAS)
left = self.width - bg_img.size[0]
self.image.paste(bg_img, (left, 0))
else:
self.image.paste(bg_img.resize(DEFAULT_CANVAS_SIZE,
Image.ANTIALIAS), (0, 0))

def save_image(self):
self.image.save(self.output_file)


def generate_banner(args):
image1 = args[0]
image2 = args[1]
text = args[2]
bg = bool(args[3]) if len(args) == 4 else False

banner = Banner()

if bg:
banner.add_background(image2)
else:
banner.add_image(image2, resize=True, right=True)

banner.add_image(image1)

font = Font(ttf=TEXT_FONT_TYPE,
text=text,
color=BLACK,
size=TEXT_SIZE,
offset=None)

banner.add_text(font)

banner.save_image()


if __name__ == '__main__':
script = sys.argv.pop(0)
args = sys.argv

if len(args) not in [3, 4]:
print('Usage: {} img1 img2 text (opt bool: 2nd img is bg)'.format(script)) # noqa E501
sys.exit(1)

generate_banner(args)
18 changes: 18 additions & 0 deletions 31/bbelderbos/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import glob
import os

from wtforms import Form, BooleanField, StringField, SelectField, TextAreaField, validators

pybites_logos = glob.glob(os.path.join('assets', 'pybites', '*png'))

option_field = lambda img: os.path.splitext(os.path.basename(img))[0]


class ImageForm(Form):
image_url1 = SelectField(
'PyBites Logo Theme',
choices=[(img, option_field(img)) for img in pybites_logos]
)
image_url2 = StringField('Second Image URL', [validators.DataRequired()])
text = TextAreaField('Text for Banner', [validators.DataRequired()])
background = BooleanField('Use Second Image as Background?', default=True)
15 changes: 15 additions & 0 deletions 31/bbelderbos/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
certifi==2017.7.27.1
chardet==3.0.4
click==6.7
Flask==0.12.2
Flask-WTF==0.14.2
idna==2.5
itsdangerous==0.24
Jinja2==2.9.6
MarkupSafe==1.0
olefile==0.44
Pillow==4.2.1
requests==2.18.3
urllib3==1.22
Werkzeug==0.12.2
WTForms==2.1
34 changes: 34 additions & 0 deletions 31/bbelderbos/static/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
body {
background-color: #fff;
width: 800px;
margin: 0 auto;
font-family: 'Open Sans', sans-serif;
}
form {
margin: 20px 0;
}
div {
padding: 10px 0;
}
input, select, textarea {
width: 300px;
}
textarea {
height: 150px;
}
button {
clear: both;
margin-top: 20px;
}
select#image_url1 option[value="article"] {
background-image:url("../assets/pybites/article.png");
}
select#image_url1 option[value="challenge"] {
background-image:url("../assets/pybites/challenge.png");
}
select#image_url1 option[value="news"] {
background-image:url("../assets/pybites/news.png");
}
select#image_url1 option[value="special"] {
background-image:url("../assets/pybites/special.png");
}
13 changes: 13 additions & 0 deletions 31/bbelderbos/templates/_formhelpers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% macro render_field(field) %}
<div>
{{ field.label }}
{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
25 changes: 25 additions & 0 deletions 31/bbelderbos/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PyBites Banner Generator</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/1.0.0/pure-min.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<h1>PyBites Banner Generator</h1>
</header>
<main>
{% block content %}
{% endblock %}
</main>
<br>
<hr>
<footer>
<p>Created by <a href="https://pybit.es" target="_blank">PyBites</a> for <a href="https://pybit.es/codechallenge31.html" target="_blank">Code Challenge 31</a></p>
</footer>
</body>
</html>

15 changes: 15 additions & 0 deletions 31/bbelderbos/templates/imageform.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}

{% from "_formhelpers.html" import render_field %}
<form class="pure-form pure-form-stacked" method=post>
<fieldset>
{{ render_field(form.image_url1) }}
{{ render_field(form.image_url2) }}
{{ render_field(form.text) }}
{{ render_field(form.background) }}
<button type=submit class="pure-button pure-button-primary">Generate banner</button>
</fieldset>
</form>

{% endblock %}