From f363315476024fbef35093b03bc605aacff5b92d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:21:56 +0200 Subject: [PATCH] moved formats to format/ --- lib/format/__init__.py | 0 lib/{wttrin_png.py => format/png.py} | 315 +++++++++++++++++---------- lib/{ => format}/unicodedata2.py | 6 + 3 files changed, 204 insertions(+), 117 deletions(-) create mode 100644 lib/format/__init__.py rename lib/{wttrin_png.py => format/png.py} (56%) rename lib/{ => format}/unicodedata2.py (99%) diff --git a/lib/format/__init__.py b/lib/format/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/wttrin_png.py b/lib/format/png.py similarity index 56% rename from lib/wttrin_png.py rename to lib/format/png.py index 005a4a0..58465a3 100644 --- a/lib/wttrin_png.py +++ b/lib/format/png.py @@ -1,51 +1,59 @@ #!/usr/bin/python #vim: encoding=utf-8 -from __future__ import print_function -import sys -import os -import re -import time - """ This module is used to generate png-files for wttr.in queries. -The only exported function is: +The only exported function are: + +* render_ansi(png_file, text, options=None) +* make_wttr_in_png(png_file) + +`render_ansi` is the main function of the module, +which does rendering of stream into a PNG-file. + +The `make_wttr_in_png` function is a temporary helper function +which is a wraper around `render_ansi` and handles +such tasks as caching, name parsing etc. - make_wttr_in_png(filename) +`make_wttr_in_png` parses `png_file` name (the shortname) and extracts +the weather query from it. It saves the weather report into the specified file. -in filename (in the shortname) is coded the weather query. -The function saves the weather report in the file and returns None. +The module uses PIL for graphical tasks, and pyte for rendering +of ANSI stream into terminal representation. + +TODO: + + * remove make_wttr_in_png + * remove functions specific for wttr.in """ -import requests +from __future__ import print_function + +import sys +import os +import re +import time +import glob from PIL import Image, ImageFont, ImageDraw import pyte.screens +import emoji +import grapheme -# downloaded from https://gist.github.com/2204527 -# described/recommended here: -# -# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character -# -import unicodedata2 +import requests -MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__'))) -sys.path.append("%s/lib/" % MYDIR) -import parse_query +from . import unicodedata2 -from globals import PNG_CACHE, log +sys.path.insert(0, "..") +import constants +import parse_query +import globals COLS = 180 ROWS = 100 - -CHAR_WIDTH = 7 -CHAR_HEIGHT = 14 - -# How to find font for non-standard scripts: -# -# $ fc-list :lang=ja - -FONT_SIZE = 12 +CHAR_WIDTH = 9 +CHAR_HEIGHT = 18 +FONT_SIZE = 15 FONT_CAT = { 'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", @@ -56,30 +64,105 @@ FONT_CAT = { '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", } -def color_mapping(color): +# +# 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 make_wttr_in_png(png_name, options=None): + """ The function saves the weather report in the file and returns None. + The weather query is coded in filename (in the shortname). """ - Convert pyte color to PIL color + + parsed = _parse_wttrin_png_name(png_name) + + # if location is MyLocation it should be overriden + # with autodetected location (from options) + if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''): + del parsed['location'] + + if options is not None: + for key, val in options.items(): + if key not in parsed: + parsed[key] = val + url = _make_wttrin_query(parsed) + + timestamp = time.strftime("%Y%m%d%H", time.localtime()) + cached_basename = url[14:].replace('/', '_') + + cached_png_file = "%s/%s/%s.png" % (globals.PNG_CACHE, timestamp, cached_basename) + + dirname = os.path.dirname(cached_png_file) + if not os.path.exists(dirname): + os.makedirs(dirname) + + if os.path.exists(cached_png_file): + return cached_png_file + + headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} + text = requests.get(url, headers=headers).text + + render_ansi(cached_png_file, text, options=parsed) + + return cached_png_file + +def render_ansi(png_file, text, options=None): + """Render `text` (terminal sequence) in `png_file` + paying attention to passed command line `options` + """ + + 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] + + _gen_term(png_file, buf, graphemes, options=options) + +def _color_mapping(color): + """Convert pyte color to PIL color + + Return: tuple of color values (R,G,B) """ + if color == 'default': 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: + 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. +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) @@ -106,22 +189,47 @@ def strip_buf(buf): return buf -def script_category(char): - """ - Returns category of a Unicode character +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' - else: - return cat + return cat -def gen_term(filename, buf, options=None): - buf = strip_buf(buf) +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)[:-4] + emojilib[character] = \ + Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT)) + return emojilib + +def _gen_term(filename, buf, graphemes, options=None): + """Renders rendered pyte buffer `buf` and list of workaround `graphemes` + to a PNG file `filename`. + """ + + if not options: + options = {} + + current_grapheme = 0 + + buf = _strip_buf(buf) cols = max(len(x) for x in buf) rows = len(buf) @@ -134,30 +242,40 @@ def gen_term(filename, buf, options=None): 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) + current_color = _color_mapping(char.fg) if char.bg != 'default': draw.rectangle( ((x_pos, y_pos), (x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)), - fill=color_mapping(char.bg)) + fill=_color_mapping(char.bg)) + + if char.data == "!": + data = graphemes[current_grapheme] + current_grapheme += 1 + else: + data = char.data - if char.data: - cat = script_category(char.data) + if data: + cat = _script_category(data[0]) if cat not in font: - log("Unknown font category: %s" % cat) - draw.text( - (x_pos, y_pos), - char.data, - font=font.get(cat, font.get('default')), - fill=current_color) - #sys.stdout.write(c.data) - - x_pos += CHAR_WIDTH + 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 #sys.stdout.write('\n') @@ -165,8 +283,8 @@ def gen_term(filename, buf, options=None): transparency = options.get('transparency', '255') try: transparency = int(transparency) - except: - transparceny = 255 + except ValueError: + transparency = 255 if transparency < 0: transparency = 0 @@ -187,32 +305,36 @@ def gen_term(filename, buf, options=None): image.save(filename) -def typescript_to_one_frame(png_file, text, options=None): - """ - Render text (terminal sequence) in png_file +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. - # fixing some broken characters because of bug #... in pyte 6.0 - text = text.replace('Н', 'H').replace('Ν', 'N') - - screen = pyte.screens.Screen(COLS, ROWS) - #screen.define_charset("B", "(") + Return: + text_without_graphemes, graphemes + """ - stream = pyte.streams.ByteStream() - stream.attach(screen) + output = "" + graphemes = [] - stream.feed(text) + for gra in grapheme.graphemes(text): + if len(gra) > 1: + character = "!" + graphemes.append(gra) + else: + character = gra + output += character - 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 output, graphemes - gen_term(png_file, buf, options=options) # # wttr.in related functions # -def parse_wttrin_png_name(name): +def _parse_wttrin_png_name(name): """ Parse the PNG filename and return the result as a dictionary. For example: @@ -252,9 +374,8 @@ def parse_wttrin_png_name(name): return parsed -def make_wttrin_query(parsed): - """ - Convert parsed data into query name +def _make_wttrin_query(parsed): + """Convert parsed data into query name """ for key in ['width', 'height', 'filetype']: @@ -281,43 +402,3 @@ def make_wttrin_query(parsed): url += "?%s" % ("&".join(args)) return url - - -def make_wttr_in_png(png_name, options=None): - """ - The function saves the weather report in the file and returns None. - The weather query is coded in filename (in the shortname). - """ - - parsed = parse_wttrin_png_name(png_name) - - # if location is MyLocation it should be overriden - # with autodetected location (from options) - if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''): - del parsed['location'] - - if options is not None: - for key, val in options.items(): - if key not in parsed: - parsed[key] = val - url = make_wttrin_query(parsed) - - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cached_basename = url[14:].replace('/','_') - - cached_png_file = "%s/%s/%s.png" % (PNG_CACHE, timestamp, cached_basename) - - dirname = os.path.dirname(cached_png_file) - if not os.path.exists(dirname): - os.makedirs(dirname) - - if os.path.exists(cached_png_file): - return cached_png_file - - headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} - text = requests.get(url, headers=headers).text.replace('\n', '\r\n') - curl_output = text.encode('utf-8') - - typescript_to_one_frame(cached_png_file, curl_output, options=parsed) - - return cached_png_file diff --git a/lib/unicodedata2.py b/lib/format/unicodedata2.py similarity index 99% rename from lib/unicodedata2.py rename to lib/format/unicodedata2.py index ed9070e..45e7ec4 100644 --- a/lib/unicodedata2.py +++ b/lib/format/unicodedata2.py @@ -1,3 +1,9 @@ +# downloaded from https://gist.github.com/2204527 +# described/recommended here: +# +# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character +# + from __future__ import print_function from unicodedata import *