diff --git a/print/start.py b/print/start.py index ff1fec6..022abbe 100644 --- a/print/start.py +++ b/print/start.py @@ -1,31 +1,21 @@ # https://github.com/python-escpos/python-escpos # https://python-escpos.readthedocs.io/en/latest/ -import sys -import os -import re import flask from flask import request import flask_apscheduler -import urllib -import json -from datetime import date, datetime -from escpos.escpos import Escpos +import urllib, json from escpos import printer +from tools.televex import televex # Create the application. APP = flask.Flask(__name__) # Connect to the printer -# Get the printer's USB initializing code with $ sudo lsbusb -lp = printer.Usb(0x4b8,0xe15) -buffer = printer.Dummy() +lp, lp_printer = televex.connect() -# Define the JSON databased for storing printed items -db = 'printed.json' -if not os.path.exists(db): - with open(db, 'w') as f: - f.write(json.dumps({ "printed" : [] }, indent=4)) +# Set up a JSON database for storing printed items +db = televex.database('printed.json') # Check the Multifeeder regulary (every 10 min) # https://github.com/viniciuschiele/flask-apscheduler @@ -35,32 +25,8 @@ scheduler.api_enabled = False scheduler.init_app(APP) scheduler.start() -def update_db(item): - - try: - items = json.loads(open(db).read()) - except: - # Hmm ... - items = { "printed" : [] } - - if item not in items['printed']: - new_item = True - with open(db, 'w') as out: - items['printed'].append(item) - out.write(json.dumps(items, indent=4)) - out.close() - else: - new_item = False - - return new_item - -def html2plain(html): - plaintext = re.sub('<[^<]+?>', '', html) - return plaintext - -@scheduler.task('interval', id='check', minutes=10) +@scheduler.task('interval', id='check', minutes=1) def check(): - try: url = 'https://multi.vvvvvvaria.org/API/latest/10' response = urllib.request.urlopen(url).read() @@ -69,59 +35,23 @@ def check(): feed = [] for post in feed: - - # add post to database - year = post['published_parsed'][0] - month = post['published_parsed'][1] - day = post['published_parsed'][2] - post_date = date(year, month, day) - post_date_str = post_date.strftime('%Y-%m-%d') - plaintext = html2plain(post['summary']) - - item = { - 'source' : post['feed_details']['rss'], - 'date' : post_date_str, - 'printed' : plaintext - } - new_item = update_db(item) - - # add RSS post to printing buffer - if new_item == True: - buffer.text(plaintext) - buffer.cut() - break - - # print - lp._raw(buffer.output) - - + source = post['feed_details']['rss'] + txt = post['summary'] + televex.print_now(txt, source, db, post=post, lp=lp, lp_printer=lp_printer) + @APP.route('/print/', methods=['GET', 'POST']) def print(): + txt = request.args.get('printing', '') - plaintext = request.args.get('printing', '') - - if plaintext: - - # add plaintext to printing buffer - buffer.text(plaintext) - buffer.cut() - - # add plaintext to database - date = datetime.now() - date_str = date.strftime('%Y-%m-%d_%H-%M-%S') - item = { - 'source' : 'TeleVex', - 'date' : date_str, - 'printed' : plaintext - } - update_db(item) - - # print - lp._raw(buffer.output) + if txt: + source = 'TeleVex' + televex.print_now(txt, source, db, lp=lp, lp_printer=lp_printer) + return flask.redirect(flask.url_for('print')) + return flask.render_template('index.html') -@APP.route('/printed/', methods=['GET', 'POST']) +@APP.route('/printed/', methods=['GET']) def printed(): items = json.loads(open(db).read()) return flask.render_template('printed.html', items=items) diff --git a/print/static/stylesheet.css b/print/static/stylesheet.css index 9423eb9..1d82120 100644 --- a/print/static/stylesheet.css +++ b/print/static/stylesheet.css @@ -13,4 +13,11 @@ div#multifeeder div.multipost{ } div#webcam img{ width:500px; +} +pre{ + margin: 1em 0; + padding: 0; + font-size: 14px; + word-break: break-all; + white-space: pre-wrap; } \ No newline at end of file diff --git a/print/templates/printed.html b/print/templates/printed.html index e22ed5e..66ee395 100644 --- a/print/templates/printed.html +++ b/print/templates/printed.html @@ -8,23 +8,24 @@
-

print, printed

- +

print, printed

- -

TeleVex recently printed:

-
(last 25)
-
+

TeleVex recently printed:

+
+---
+(last 25)
+(this is a de-bugging tool that most likely will disappear again)
+---
{% set count = 25 %} {% for x in range(1, 25) %} {% set item = items["printed"][-x] %} {% if item %} -
- Source: {{ item.source }}
- Date: {{ item.date }}
- Printed: {{ item.printed }}
-
-
+
+ Source: {{ item.source }}
+ Date: {{ item.date }}
+ Printed: +
{{ item.printed | replace('\n', '
') | safe }}
+
{% endif %} {% set count = count - 1 %} {% endfor %} diff --git a/print/tools/asciiWriter/__init__.py b/print/tools/asciiWriter/__init__.py new file mode 100644 index 0000000..63f77b6 --- /dev/null +++ b/print/tools/asciiWriter/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 + diff --git a/print/tools/asciiWriter/asciiWriter.py b/print/tools/asciiWriter/asciiWriter.py new file mode 100644 index 0000000..03e16e1 --- /dev/null +++ b/print/tools/asciiWriter/asciiWriter.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import utils +import patterns +import marks +import text \ No newline at end of file diff --git a/print/tools/asciiWriter/draw.py b/print/tools/asciiWriter/draw.py new file mode 100644 index 0000000..5dee52e --- /dev/null +++ b/print/tools/asciiWriter/draw.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from utils import make_lines, merge, print_lines, rotate, visit, visit_horizontal +from patterns import diagonal, horizontal, vertical, sinus_horizontal, sinus_vertical, image +from marks import random_mark, sequence_mark, space +from random import choice, random +import math + + + + +# marks = ['┼', '│', '░', '▓', 'X', '■', '≡', '·', '¦', ' '] +# blank = ' ' +width = 75 +height = 50 +mark = sequence_mark('O.P.E.N D.E.S.I.G.N C.O.U.R.S.E ') + + +layers = [] + +# layers.append(visit(make_lines(width, height), image('blobs-small.png'), mark, space(' '))) + +for offset in range(-50, 50, 15): + lines = [[] for l in range(height)] + sinus = sinus_vertical(period=50, amplitude=25, offset=offset, offset_t=random()) + layers.append(visit(make_lines(width, height), sinus, sequence_mark(' K A S K G E N T '), space())) + +for offset in range(-43, 57, 15): + lines = [[] for l in range(height)] + sinus = sinus_vertical(period=40, amplitude=10, offset=offset, offset_t=.5+random()) + layers.append(visit(make_lines(width, height), sinus, mark, space())) + +print_lines(merge(width, height, space()(), layers)) + +# for line in overlay(50, 50, ' ', [rotate(merged), merged]): +# stdout.write('{}\n'.format(''.join(line))) + +# sinus = sinus_horizontal(period=30, amplitude=8) +# for x in range(width): +# for y in range(height): +# lines[y].append(sinus(x, y, width, height, mark, space())) + +# for line in lines: +# stdout.write('{}\n'.format(''.join(line))) + +# lines = [[draw(x, y, marks) for x in range(width)] for y in range(height)] + +# sys.sdout.write('\n'.join([''.join(line) for line in lines])) + +# sys.sdout.write('\n'.join([''.join([draw(x, y, marks) for x in range(width)]) for y in range(height)])) diff --git a/print/tools/asciiWriter/marks.py b/print/tools/asciiWriter/marks.py new file mode 100644 index 0000000..7610339 --- /dev/null +++ b/print/tools/asciiWriter/marks.py @@ -0,0 +1,24 @@ +from random import choice + +def random (marks=['']): + def func (): + return choice(marks) + + return func + +def sentence (text): + chars = list(text) + def f(): + char = chars.pop(0) + chars.append(char) + return char + return f + +def single (char): + def f(): + return char + + return f + +def space (space=' '): + return single(space) diff --git a/print/tools/asciiWriter/patterns.py b/print/tools/asciiWriter/patterns.py new file mode 100644 index 0000000..2c7a817 --- /dev/null +++ b/print/tools/asciiWriter/patterns.py @@ -0,0 +1,67 @@ +import math + +# # Linear +def diagonal(): + def f (x, y, width, height, mark, blank): + if x == math.floor((y / float(height)) * width): + return mark() + else: + return blank() + + return f + +# Cross +def cross (): + def f (x, y, width, height, mark, blank): + pos = math.floor((y / float(height)) * width) + + if x == pos or (width - 1) - pos == x: + return mark() + else: + return blank() + return f + +def horizontal (position): + def f (x, y, width, height, mark, blank): + return mark() if position == y else blank() + return f + +def vertical (position): + def f (x, y, width, height, mark, blank): + return mark() if position == x else blank() + return f + +# Sinus +def sinus_vertical (period=0.2, amplitude=0.5, offset_t=0, offset=0): + period = (period / (math.pi * 2)) + + def f (x, y, width, height, mark, blank): + middle = (width - 1) * .5 + to_mark = math.floor(middle + math.sin(offset_t + y / period) * amplitude) + return mark() if (x + offset) == to_mark else blank() + return f + +def sinus_horizontal (period=0.2, amplitude=0.5, offset_t=0, offset=0): + period = (period / (math.pi * 2)) + def f (x, y, width, height, mark, blank): + middle = (height - 1) * .5 + to_mark = math.floor(middle + math.sin(offset_t + x / period) * amplitude) + return mark() if y + offset == to_mark else blank() + + return f + +def image (path, threshold=128): + from PIL import Image + + im = Image.open(path).convert('L') + image_width, image_height = im.size + pixels = im.load() + + def f (x, y, width, height, mark, blank): + if x < image_width and y < image_height: + if pixels[x, y] < threshold: + return mark() + + return blank() + + return f diff --git a/print/tools/asciiWriter/text.py b/print/tools/asciiWriter/text.py new file mode 100644 index 0000000..5fa330b --- /dev/null +++ b/print/tools/asciiWriter/text.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from .wrap_single_line import wrap_single_line +from .utils import translate, merge + +def make_column(text, line_width=50, height=200, use_hyphenator=None, line_offset=0): + + lines = [] + remaining = text + + while remaining and len(lines) < height: + + if callable(line_width): + width = line_width(len(lines), height) + else: + width = line_width + + if callable(line_offset): + offset = line_offset(len(lines), height) + else: + offset = line_offset + + line, remaining = wrap_single_line(remaining, width, use_hyphenator=use_hyphenator, replace_whitespace=False, drop_whitespace=True) + + line = list(line) + + if offset != 0: + line = [None for _ in range(offset)] + line + + lines.append(line) + + return lines, remaining + +def make_multi_column(text, height=200, column_width=40, column_count=2, column_gap=5, use_hyphenator=None, space_char=None): + # todo: vertical offset? + + remaining = text + i = 0 + + columns = [] + + while remaining and i < column_count: + column, remaining = make_column(remaining, line_width=column_width, height=height, use_hyphenator=use_hyphenator) + if i > 0: + offset = (column_width + column_gap) * i + column = translate(column, x=offset, y=0) + columns.append(column) + i += 1 + + width = (column_width + column_gap) * column_count + lines = merge(width, height, space_char, columns) + return lines, remaining + + +if __name__ == '__main__': + print(make_column('Hello world!', line_width=25, height=10)) \ No newline at end of file diff --git a/print/tools/asciiWriter/utils.py b/print/tools/asciiWriter/utils.py new file mode 100644 index 0000000..ec7473b --- /dev/null +++ b/print/tools/asciiWriter/utils.py @@ -0,0 +1,61 @@ +from sys import stdout + +def rotate(layer): + new_width = len(layer) + new_height = len(layer[0]) + rotated = [['' for x in range(new_width)] for l in range(new_height)] + + for y in range(len(layer)): + for x in range(len(layer[y])): + rotated[x][y] = layer[y][x] + + return rotated + +def merge(width, height, space_char, layers): + output = [[space_char for x in range(width)] for y in range(height)] + + for layer in layers: + for y in range(min(len(layer), height)): + for x in range(min(len(layer[y]), width)): + if layer[y][x] and layer[y][x] != space_char: + output[y][x] = layer[y][x] + + return output + +# Make a multidimensional array +# with the given dimensions +def make_lines (width, height, fill_char = ''): + return [[ fill_char for _ in range(width) ] for __ in range(height)] + +def visit (lines, callback, mark, blank): + height = len(lines) + width = len(lines[0]) + + for y in range(height): + for x in range(width): + lines[y][x] = callback(x, y, width, height, mark, blank) + + return lines + +def visit_horizontal (lines, callback, mark, blank): + height = len(lines) + width = len(lines[0]) + + for x in range(width): + for y in range(height): + lines[y][x] = callback(x, y, width, height, mark, blank) + + return lines + +def print_lines (lines): + for line in lines: + stdout.write('{}\n'.format(''.join(line))) + +def translate(shape, x=0, y=0): + ## TODO implement a negative translation? + translated = [[] for _ in range(y)] + + for line in shape: + translated.append([None for _ in range(x)] + line) + + return translated \ No newline at end of file diff --git a/print/tools/asciiWriter/wrap_single_line.py b/print/tools/asciiWriter/wrap_single_line.py new file mode 100644 index 0000000..b06f6e4 --- /dev/null +++ b/print/tools/asciiWriter/wrap_single_line.py @@ -0,0 +1,137 @@ +""" + Based on the textwrap2 module included in the PyHyphen library: + https://pypi.org/project/PyHyphen +""" + +import textwrap + +class TextWrapper(textwrap.TextWrapper): + """ + This class extends the Python 3 standard library's TextWrapper and adds an optional + use_hyphenator to its constructor arguments. + """ + + def __init__(self, *args, **kwargs): + self.use_hyphenator = kwargs.pop("use_hyphenator", None) + super().__init__(*args, **kwargs) + + def _wrap_chunks(self, chunks): + + lines = [] + if (chunks): + """Override the mother class method. + + Most of that method is directly copied from the original class, except + for the part with use_hyphenator. + """ + if self.width <= 0: + raise ValueError("invalid width %r (must be > 0)" % self.width) + if self.max_lines is not None: + if self.max_lines > 1: + indent = self.subsequent_indent + else: + indent = self.initial_indent + if len(indent) + len(self.placeholder.lstrip()) > self.width: + raise ValueError("placeholder too large for max width") + + # Arrange in reverse order so items can be efficiently popped + # from a stack of chucks. + chunks.reverse() + + # Start the list of chunks that will make up the current line. + # cur_len is just the length of all the chunks in cur_line. + cur_line = [] + cur_len = 0 + + # Figure out which static string will prefix this line. + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + # Maximum width for this line. + width = self.width - len(indent) + + # First chunk on line is whitespace -- drop it, unless this + # is the very beginning of the text (ie. no lines started yet). + if self.drop_whitespace and chunks[-1].strip() == '' and chunks[-1][0] != '\n': # and lines: + del chunks[-1] + + while chunks: + l = len(chunks[-1]) + + # Check for a newline character + if chunks[-1] == '\n': + return indent + ''.join(cur_line), ''.join(reversed(chunks[:-1])) + elif chunks[-1][0] == '\n': + return indent + ''.join(cur_line), ''.join(reversed(chunks[:-1] + [chunks[-1][1:]])) + + # Can at least squeeze this chunk onto the current line. + if cur_len + l <= width: + cur_line.append(chunks.pop()) + cur_len += l + + # Nope, this line is full. + # But try hyphenation. + else: + if self.use_hyphenator and (width - cur_len >= 2): + hyphenated_chunk = self.use_hyphenator.wrap(chunks[-1], width - cur_len) + if hyphenated_chunk: + cur_line.append(hyphenated_chunk[0]) + chunks[-1] = hyphenated_chunk[1] + + break + + # The current line is full, and the next chunk is too big to + # fit on *any* line (not just this one). + if chunks and len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + cur_len = sum(map(len, cur_line)) + + # If the last chunk on this line is all whitespace, drop it. + if self.drop_whitespace and cur_line and cur_line[-1].strip() == '': + cur_len -= len(cur_line[-1]) + del cur_line[-1] + + if cur_line: + if (self.max_lines is None or + len(lines) + 1 < self.max_lines or + (not chunks or + self.drop_whitespace and + len(chunks) == 1 and + not chunks[0].strip()) and cur_len <= width): + # Convert current line back to a string and store it in + # list of all lines (return value). + return indent + ''.join(cur_line), ''.join(reversed(chunks)) + else: + while cur_line: + if (cur_line[-1].strip() and + cur_len + len(self.placeholder) <= width): + cur_line.append(self.placeholder) + + return indent + ''.join(cur_line), ''.join(reversed(chunks)) + + cur_len -= len(cur_line[-1]) + del cur_line[-1] + else: + if lines: + prev_line = lines[-1].rstrip() + if (len(prev_line) + len(self.placeholder) <= + self.width): + lines[-1] = prev_line + self.placeholder + + return indent + self.placeholder.lstrip(), ''.join(reversed(chunks)) + else: + return '', '' + +def wrap_single_line (text, width=70, **kwargs): + w = TextWrapper(width=width, **kwargs) + return w.wrap(text) + +if __name__ == '__main__': + from hyphen import Hyphenator + h_en = Hyphenator('en_US') + + line, remaining = wrap_single_line('https://stackoverflow.com/questions/3940128/how-can-i-reverse-a-list-in-python', width=46, use_hyphenator=h_en) + + print(line, remaining) \ No newline at end of file diff --git a/print/tools/televex/televex.py b/print/tools/televex/televex.py new file mode 100644 index 0000000..a15c4ea --- /dev/null +++ b/print/tools/televex/televex.py @@ -0,0 +1,95 @@ +import sys, os, re, json +from datetime import datetime, date +import html2text +from escpos import printer + +from tools.asciiWriter.text import make_column +from tools.asciiWriter.utils import merge, print_lines, make_lines, translate + +def connect(): + # Get the printer's USB initializing code with $ sudo lsbusb + try: + lp = printer.Usb(0x4b8,0xe15) + lp_printer = True + except: + lp = None + lp_printer = False + + return lp, lp_printer + +def database(db): + if not os.path.exists(db): + with open(db, 'w') as f: + f.write(json.dumps({ "printed" : [] }, indent=4)) + + return db + +def html2plain(html): + # remove HTML tags + txt = re.sub(r'<.*?>', '', html) + + # make line breaks + # https://git.vvvvvvaria.org/mb/ascii-art-but-with-unicode + width = 48 + layers = [] + lines, remaining = make_column(txt, line_width=width) + layers.append(lines) + merged = merge(width, len(lines), ' ', layers) + txt = '' + for line in merged: + txt += '{}\n'.format(''.join(line)) + + return txt + +def update_db(txt, source, db, post=None): + try: + items = json.loads(open(db).read()) + except: + items = { "printed" : [] } # Hmm ... + + # save the publishing date of this post + if post: + year = post['published_parsed'][0] + month = post['published_parsed'][1] + day = post['published_parsed'][2] + post_date = date(year, month, day) + date_str = post_date.strftime('%Y-%m-%d') + post = None + else: + post_date = datetime.now() + date_str = post_date.strftime('%Y-%m-%d_%H-%M-%S') + + # clean txt from HTML tags + txt = html2plain(txt) + + # add txt to database + # and check if this item is new + item = { + 'source' : source, + 'date' : date_str, + 'printed' : txt + } + if item not in items['printed']: + new_item = True + with open(db, 'w') as out: + items['printed'].append(item) + out.write(json.dumps(items, indent=4)) + out.close() + else: + new_item = False + + return new_item, txt + +def print_now(txt, source, db, post=None, lp=None, lp_printer=None): + # check if this is a new item + new_item, txt = update_db(txt, source, db, post=post) + + # if this item is new + # then print! + if new_item == True: + if lp_printer == True: + lp.text(txt) + lp.cut() + else: + # or print in the terminal! + sys.stderr.write('Printing output to the terminal:\n\n'+txt+'\n\n') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4f8a0fb..5b41a8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pypandoc python-escpos flask Flask-APScheduler +html2text \ No newline at end of file