|
|
|
@ -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
|