diff --git a/31/bbelderbos/.gitignore b/31/bbelderbos/.gitignore new file mode 100644 index 000000000..2fc346020 --- /dev/null +++ b/31/bbelderbos/.gitignore @@ -0,0 +1,5 @@ +*swp +*pyc +__pycache__ +images +out.png diff --git a/31/bbelderbos/README.md b/31/bbelderbos/README.md new file mode 100644 index 000000000..630d6b0d6 --- /dev/null +++ b/31/bbelderbos/README.md @@ -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 ... diff --git a/31/bbelderbos/app.py b/31/bbelderbos/app.py new file mode 100644 index 000000000..67c82efe6 --- /dev/null +++ b/31/bbelderbos/app.py @@ -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) diff --git a/31/bbelderbos/assets/SourceSansPro-Regular.otf b/31/bbelderbos/assets/SourceSansPro-Regular.otf new file mode 100644 index 000000000..bdcfb27a4 Binary files /dev/null and b/31/bbelderbos/assets/SourceSansPro-Regular.otf differ diff --git a/31/bbelderbos/assets/Ubuntu-R.ttf b/31/bbelderbos/assets/Ubuntu-R.ttf new file mode 100644 index 000000000..45a038bad Binary files /dev/null and b/31/bbelderbos/assets/Ubuntu-R.ttf differ diff --git a/31/bbelderbos/assets/pillow-logo.png b/31/bbelderbos/assets/pillow-logo.png new file mode 100644 index 000000000..2738c0e98 Binary files /dev/null and b/31/bbelderbos/assets/pillow-logo.png differ diff --git a/31/bbelderbos/assets/pybites/article.png b/31/bbelderbos/assets/pybites/article.png new file mode 100644 index 000000000..50d360c36 Binary files /dev/null and b/31/bbelderbos/assets/pybites/article.png differ diff --git a/31/bbelderbos/assets/pybites/challenge.png b/31/bbelderbos/assets/pybites/challenge.png new file mode 100644 index 000000000..ee5aa68ac Binary files /dev/null and b/31/bbelderbos/assets/pybites/challenge.png differ diff --git a/31/bbelderbos/assets/pybites/news.png b/31/bbelderbos/assets/pybites/news.png new file mode 100644 index 000000000..ca229cd7d Binary files /dev/null and b/31/bbelderbos/assets/pybites/news.png differ diff --git a/31/bbelderbos/assets/pybites/special.png b/31/bbelderbos/assets/pybites/special.png new file mode 100644 index 000000000..548d0daff Binary files /dev/null and b/31/bbelderbos/assets/pybites/special.png differ diff --git a/31/bbelderbos/assets/readme/example1a.png b/31/bbelderbos/assets/readme/example1a.png new file mode 100644 index 000000000..a69bbe219 Binary files /dev/null and b/31/bbelderbos/assets/readme/example1a.png differ diff --git a/31/bbelderbos/assets/readme/example1b.png b/31/bbelderbos/assets/readme/example1b.png new file mode 100644 index 000000000..607bf575f Binary files /dev/null and b/31/bbelderbos/assets/readme/example1b.png differ diff --git a/31/bbelderbos/assets/readme/example2a.png b/31/bbelderbos/assets/readme/example2a.png new file mode 100644 index 000000000..09a9a2c61 Binary files /dev/null and b/31/bbelderbos/assets/readme/example2a.png differ diff --git a/31/bbelderbos/assets/readme/example2b.png b/31/bbelderbos/assets/readme/example2b.png new file mode 100644 index 000000000..59a97738d Binary files /dev/null and b/31/bbelderbos/assets/readme/example2b.png differ diff --git a/31/bbelderbos/banner/.gitignore b/31/bbelderbos/banner/.gitignore new file mode 100644 index 000000000..ffc8b4d4e --- /dev/null +++ b/31/bbelderbos/banner/.gitignore @@ -0,0 +1 @@ +out*png diff --git a/31/bbelderbos/banner/__init__.py b/31/bbelderbos/banner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/31/bbelderbos/banner/banner.py b/31/bbelderbos/banner/banner.py new file mode 100644 index 000000000..883f81535 --- /dev/null +++ b/31/bbelderbos/banner/banner.py @@ -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) diff --git a/31/bbelderbos/forms.py b/31/bbelderbos/forms.py new file mode 100644 index 000000000..43a1d0d01 --- /dev/null +++ b/31/bbelderbos/forms.py @@ -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) diff --git a/31/bbelderbos/requirements.txt b/31/bbelderbos/requirements.txt new file mode 100644 index 000000000..d91f15c20 --- /dev/null +++ b/31/bbelderbos/requirements.txt @@ -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 diff --git a/31/bbelderbos/static/style.css b/31/bbelderbos/static/style.css new file mode 100644 index 000000000..4e6511d1a --- /dev/null +++ b/31/bbelderbos/static/style.css @@ -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"); +} diff --git a/31/bbelderbos/templates/_formhelpers.html b/31/bbelderbos/templates/_formhelpers.html new file mode 100644 index 000000000..630274711 --- /dev/null +++ b/31/bbelderbos/templates/_formhelpers.html @@ -0,0 +1,13 @@ +{% macro render_field(field) %} +
+ {{ field.label }} + {{ field(**kwargs)|safe }} + {% if field.errors %} + + {% endif %} +
+{% endmacro %} diff --git a/31/bbelderbos/templates/base.html b/31/bbelderbos/templates/base.html new file mode 100644 index 000000000..25efad58c --- /dev/null +++ b/31/bbelderbos/templates/base.html @@ -0,0 +1,25 @@ + + + + + PyBites Banner Generator + + + + + +
+

PyBites Banner Generator

+
+
+ {% block content %} + {% endblock %} +
+
+
+ + + + diff --git a/31/bbelderbos/templates/imageform.html b/31/bbelderbos/templates/imageform.html new file mode 100644 index 000000000..7fafd11c5 --- /dev/null +++ b/31/bbelderbos/templates/imageform.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block content %} + + {% from "_formhelpers.html" import render_field %} +
+
+ {{ render_field(form.image_url1) }} + {{ render_field(form.image_url2) }} + {{ render_field(form.text) }} + {{ render_field(form.background) }} + +
+
+ +{% endblock %}