You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wttr.in/lib/fmt/png.py

291 lines
8.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/python
#vim: encoding=utf-8
# pylint: disable=wrong-import-position,wrong-import-order,redefined-builtin
"""
This module is used to generate png-files for wttr.in queries.
The only exported function is:
* render_ansi(png_file, text, options=None)
`render_ansi` is the main function of the module,
which does rendering of stream into a PNG-file.
The module uses PIL for graphical tasks, and pyte for rendering
of ANSI stream into terminal representation.
"""
from __future__ import print_function
import sys
import io
import os
import glob
from PIL import Image, ImageFont, ImageDraw
import pyte.screens
import emoji
import grapheme
from . import unicodedata2
sys.path.insert(0, "..")
import constants
import globals
COLS = 180
ROWS = 100
CHAR_WIDTH = 8
CHAR_HEIGHT = 14
FONT_SIZE = 13
FONT_CAT = {
'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
'Greek': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
'Arabic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
'Hebrew': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
'Han': "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
'Hangul': "/usr/share/fonts/truetype/lexi/LexiGulim.ttf",
'Braille': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf",
'Emoji': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf",
}
#
# How to find font for non-standard scripts:
#
# $ fc-list :lang=ja
#
# GNU/Debian packages, that the fonts come from:
#
# * fonts-dejavu-core
# * fonts-wqy-zenhei (Han)
# * fonts-motoya-l-cedar (Hiragana/Katakana)
# * fonts-lexi-gulim (Hangul)
# * fonts-symbola (Braille/Emoji)
#
def render_ansi(text, options=None):
"""Render `text` (terminal sequence) in a PNG file
paying attention to passed command line `options`.
Return: file content
"""
screen = pyte.screens.Screen(COLS, ROWS)
screen.set_mode(pyte.modes.LNM)
stream = pyte.Stream(screen)
text, graphemes = _fix_graphemes(text)
stream.feed(text)
buf = sorted(screen.buffer.items(), key=lambda x: x[0])
buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf]
return _gen_term(buf, graphemes, options=options)
def _color_mapping(color, inverse=False):
"""Convert pyte color to PIL color
Return: tuple of color values (R,G,B)
"""
if color == 'default':
if inverse:
return 'black'
return 'lightgray'
if color in ['green', 'black', 'cyan', 'blue', 'brown']:
return color
try:
return (
int(color[0:2], 16),
int(color[2:4], 16),
int(color[4:6], 16))
except (ValueError, IndexError):
# if we do not know this color and it can not be decoded as RGB,
# print it and return it as it is (will be displayed as black)
# print color
return color
return color
def _strip_buf(buf):
"""Strips empty spaces from behind and from the right side.
(from the right side is not yet implemented)
"""
def empty_line(line):
"Returns True if the line consists from spaces"
return all(x.data == ' ' for x in line)
def line_len(line):
"Returns len of the line excluding spaces from the right"
last_pos = len(line)
while last_pos > 0 and line[last_pos-1].data == ' ':
last_pos -= 1
return last_pos
number_of_lines = 0
for line in buf[::-1]:
if not empty_line(line):
break
number_of_lines += 1
if number_of_lines:
buf = buf[:-number_of_lines]
max_len = max(line_len(x) for x in buf)
buf = [line[:max_len] for line in buf]
return buf
def _script_category(char):
"""Returns category of a Unicode character
Possible values:
default, Cyrillic, Greek, Han, Hiragana
"""
if char in emoji.UNICODE_EMOJI:
return "Emoji"
cat = unicodedata2.script_cat(char)[0]
if char == u'':
return 'Han'
if cat in ['Latin', 'Common']:
return 'default'
return cat
def _load_emojilib():
"""Load known emojis from a directory, and return dictionary
of PIL Image objects correspodent to the loaded emojis.
Each emoji is resized to the CHAR_HEIGHT size.
"""
emojilib = {}
for filename in glob.glob("share/emoji/*.png"):
character = os.path.basename(filename)[:-3]
emojilib[character] = \
Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT))
return emojilib
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
def _gen_term(buf, graphemes, options=None):
"""Renders rendered pyte buffer `buf` and list of workaround `graphemes`
to a PNG file, and return its content
"""
if not options:
options = {}
current_grapheme = 0
buf = _strip_buf(buf)
cols = max(len(x) for x in buf)
rows = len(buf)
bg_color = 0
if "background" in options:
bg_color = _color_mapping(options["background"], options.get("inverted_colors"))
image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT), color=bg_color)
buf = buf[-ROWS:]
draw = ImageDraw.Draw(image)
font = {}
for cat in FONT_CAT:
font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE)
emojilib = _load_emojilib()
x_pos = 0
y_pos = 0
for line in buf:
x_pos = 0
for char in line:
current_color = _color_mapping(char.fg, options.get("inverted_colors"))
if char.bg != 'default':
draw.rectangle(
((x_pos, y_pos),
(x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
fill=_color_mapping(char.bg, options.get("inverted_colors")))
if char.data == "!":
try:
data = graphemes[current_grapheme]
except IndexError:
pass
current_grapheme += 1
else:
data = char.data
if data:
cat = _script_category(data[0])
if cat not in font:
globals.log("Unknown font category: %s" % cat)
if cat == 'Emoji' and emojilib.get(data):
image.paste(emojilib.get(data), (x_pos, y_pos))
else:
draw.text(
(x_pos, y_pos),
data,
font=font.get(cat, font.get('default')),
fill=current_color)
x_pos += CHAR_WIDTH * constants.WEATHER_SYMBOL_WIDTH_VTE.get(data, 1)
y_pos += CHAR_HEIGHT
if 'transparency' in options:
transparency = options.get('transparency', '255')
try:
transparency = int(transparency)
except ValueError:
transparency = 255
if transparency < 0:
transparency = 0
if transparency > 255:
transparency = 255
image = image.convert("RGBA")
datas = image.getdata()
new_data = []
for item in datas:
new_item = tuple(list(item[:3]) + [transparency])
new_data.append(new_item)
image.putdata(new_data)
img_bytes = io.BytesIO()
image.save(img_bytes, format="png")
return img_bytes.getvalue()
def _fix_graphemes(text):
"""
Extract long graphemes sequences that can't be handled
by pyte correctly because of the bug pyte#131.
Graphemes are omited and replaced with placeholders,
and returned as a list.
Return:
text_without_graphemes, graphemes
"""
output = ""
graphemes = []
for gra in grapheme.graphemes(text):
if len(gra) > 1:
character = "!"
graphemes.append(gra)
else:
character = gra
output += character
return output, graphemes