Skip to content

Commit

Permalink
Long awaited PF2 font zooming ("scaling"), fixes:
Browse files Browse the repository at this point in the history
Frontend will connect to printer only right before printing
Frontend option layout organized & optimized
Backend supress useless warnings
  • Loading branch information
NaitLee committed May 15, 2022
1 parent 39e29de commit 1f47d2a
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 154 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Currently:
- and also Android!

- Free, as in [freedom](https://www.gnu.org/philosophy/free-sw.html)!
- Unlike the "original" proprietary app,
- Unlike those proprietary "apps" around,
this project is for everyone that concerns *open-mind and freedom*!

- and Fun!
Expand Down Expand Up @@ -120,7 +120,7 @@ Copyright © 2021-2022 NaitLee Soft. Some rights reserved.
See file `COPYING`, `LICENSE`, and detail of used JavaScript in file `www/jslicense.html`

Particularly, `printer.py`, `server.py` and `main.js` are released under GNU GPL 3.
Excluding contributions of other people (which probably have their own copyright) and possible third-party dependencies, all other parts are in Public Domain (CC0).
All other parts, except which have special statements, are in Public Domain (CC0).

--------

Expand All @@ -134,9 +134,12 @@ Also interested in code development? See [development.md](development.md)!
### Credits

- Of course, Python & the Web!
- [Bleak](https://bleak.readthedocs.io/en/latest/) BLE lib! The overall Hero!
- [Bleak](https://bleak.readthedocs.io/en/latest/) Bluetooth-Low-Energy library! The overall Hero!
- [roddeh-i18n](https://github.com/roddeh/i18njs), the current built-in i18n is inspired by this
- [PF2 font](http://grub.gibibit.com/New_font_format), great minimal raster font idea
- ImageMagick & Ghostscript, never mention other if something useful is already in one's system
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) for saving my life from Java
- Stack Overflow & the whole Internet, you let me know Android `Activity` all from beginning
... and many other helpful ideas as well
- ... Everyone is Awesome!
2 changes: 2 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Note: not ordered. do whatever I/you want
+ Try to implement enough without more dependencies
+ ...

? Optimize PF2 text printing? It seems a bit slow (in algorithm).
? Built-in PostScript? (Even if very basic)
? Data compression for GB03. Optional
? Put Android APP on F-Droid? But it needs automatic build system...
Android guys can help this!
Expand Down
79 changes: 43 additions & 36 deletions printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ class PrinterDriver(Commander):
flip_v: bool = False
wrap: bool = False
rtl: bool = False
font_scale: int = 1

energy: int = None
quality: int = 24
Expand Down Expand Up @@ -468,7 +469,8 @@ def _print_text(self, file: io.BufferedIOBase):
text_io = io.TextIOWrapper(file, encoding='utf-8')
if self.text_canvas is None:
self.text_canvas = TextCanvas(paper_width, wrap=self.wrap,
rtl=self.rtl, font_path=self.font_family + '.pf2')
rtl=self.rtl, font_path=self.font_family + '.pf2',
scale=self.font_scale)
# with stdin you maybe trying out a typewriter
# so print a "ruler", indicating max characters in one line
if file is sys.stdin.buffer:
Expand All @@ -480,16 +482,16 @@ def _print_text(self, file: io.BufferedIOBase):
width_stats[char] = pf2[char].width
average = pf2.point_size // 2
if (width_stats[' '] == width_stats['i'] ==
width_stats['m'] == width_stats['M']):
width_stats['m'] == width_stats['M']):
# monospace
average = width_stats['A']
else:
# variable width, use a rough average
average = (width_stats['a'] + width_stats['A'] +
width_stats['0'] + width_stats['+']) // 4
width_stats['0'] + width_stats['+']) // 4
# ruler
info('-------+' * (paper_width // average // 8) +
'-' * (paper_width // average % 8))
'-' * (paper_width // average % 8))
self._prepare()
printer_data = PrinterData(paper_width)
buffer = io.BytesIO()
Expand All @@ -502,7 +504,7 @@ def _print_text(self, file: io.BufferedIOBase):
buffer.write(data)
line_count += 1
flip(buffer, self.text_canvas.width, self.text_canvas.height * line_count,
self.flip_h, self.flip_v, overwrite=True)
self.flip_h, self.flip_v, overwrite=True)
while chunk := buffer.read(paper_width // 8):
printer_data.write(chunk)
if self.dry_run:
Expand Down Expand Up @@ -550,17 +552,17 @@ def magick_text(stdin, image_width, font_size, font_family):
'Pipe an io to ImageMagick for processing text to image, return output io'
read_fd, write_fd = os.pipe()
subprocess.Popen([_MagickExe, '-background', 'white', '-fill', 'black',
'-size', f'{image_width}x', '-font', font_family, '-pointsize', str(font_size),
'caption:@-', 'pbm:-'],
stdin=stdin, stdout=io.FileIO(write_fd, 'w'))
'-size', f'{image_width}x', '-font', font_family, '-pointsize',
str(font_size), 'caption:@-', 'pbm:-'],
stdin=stdin, stdout=io.FileIO(write_fd, 'w'))
return io.FileIO(read_fd, 'r')

def magick_image(stdin, image_width, dither):
'Pipe an io to ImageMagick for processing "usual" image to pbm, return output io'
read_fd, write_fd = os.pipe()
subprocess.Popen([_MagickExe, '-', '-fill', 'white', '-opaque', 'transparent',
'-resize', f'{image_width}x', '-dither', dither, '-monochrome', 'pbm:-'],
stdin=stdin, stdout=io.FileIO(write_fd, 'w'))
'-resize', f'{image_width}x', '-dither', dither, '-monochrome', 'pbm:-'],
stdin=stdin, stdout=io.FileIO(write_fd, 'w'))
return io.FileIO(read_fd, 'r')

class HelpFormatterI18n(argparse.HelpFormatter):
Expand Down Expand Up @@ -607,29 +609,29 @@ def _main():
)
# TODO: group some switches to dedicated help
parser.add_argument('-h', '--help', action='store_true',
help=i18n('show-this-help-message'))
help=i18n('show-this-help-message'))
parser.add_argument('file', default='-', metavar='File', type=str,
help=i18n('path-to-input-file-dash-for-stdin'))
help=i18n('path-to-input-file-dash-for-stdin'))
parser.add_argument('-s', '--scan', metavar='Time[,XY01[,MacAddress]]', default='4', type=str,
help=i18n('scan-for-a-printer'))
help=i18n('scan-for-a-printer'))
parser.add_argument('-c', '--convert', metavar='text|image', type=str, default='',
help=i18n('convert-input-image-with-imagemagick'))
help=i18n('convert-input-image-with-imagemagick'))
parser.add_argument('-p', '--image', metavar='flip|fliph|flipv', type=str, default='',
help=i18n('image-printing-options'))
help=i18n('image-printing-options'))
parser.add_argument('-t', '--text', metavar='Size[,FontFamily][,pf2][,nowrap][,rtl]', type=str,
default='', help=i18n('text-printing-mode-with-options'))
default='', help=i18n('text-printing-mode-with-options'))
parser.add_argument('-e', '--energy', metavar='0.0-1.0', type=float, default=None,
help=i18n('control-printer-thermal-strength'))
help=i18n('control-printer-thermal-strength'))
parser.add_argument('-q', '--quality', metavar='1-4', type=int, default=3,
help=i18n('print-quality'))
help=i18n('print-quality'))
parser.add_argument('-d', '--dry', action='store_true',
help=i18n('dry-run-test-print-process-only'))
help=i18n('dry-run-test-print-process-only'))
parser.add_argument('-f', '--fake', metavar='XY01', type=str, default='',
help=i18n('virtual-run-on-specified-model'))
help=i18n('virtual-run-on-specified-model'))
parser.add_argument('-m', '--dump', action='store_true',
help=i18n('dump-the-traffic'))
help=i18n('dump-the-traffic'))
parser.add_argument('-n', '--nothing', action='store_true',
help=i18n('do-nothing'))
help=i18n('do-nothing'))

if len(sys.argv) < 2 or '-h' in sys.argv or '--help' in sys.argv:
parser.print_help()
Expand All @@ -644,6 +646,10 @@ def _main():
identifier = ','.join(scan_param[1:])
if args.energy is not None:
printer.energy = int(args.energy * 0xff)
elif args.convert == 'text' or args.text:
printer.energy = 96
else:
printer.energy = 64
if args.quality is not None:
printer.quality = 4 * (args.quality + 5)

Expand All @@ -665,32 +671,22 @@ def _main():

info(i18n('cat-printer'))

if args.dry:
info(i18n('dry-run-test-print-process-only'))
printer.dry_run = True
if args.fake:
printer.fake = True
printer.model = Models[args.fake]
else:
info(i18n('connecting'))
printer.scan(identifier, use_result=True)
printer.dump = args.dump

mode = 'pbm'

if args.file == '-':
file = sys.stdin.buffer
else:
file = open(args.file, 'rb')

mode = 'pbm'

if args.text:
info(i18n('text-printing-mode'))
printer.font_family = font_family or 'font'
if 'pf2' not in text_param:
# TODO: remove hardcoded width
file = magick_text(file, 384,
font_size, font_family)
font_size, font_family)
else:
printer.font_scale = font_size
mode = 'text'
elif args.convert:
file = magick_image(file, 384, (
Expand All @@ -699,6 +695,17 @@ def _main():
else 'FloydSteinberg')
)

if args.dry:
info(i18n('dry-run-test-print-process-only'))
printer.dry_run = True
if args.fake:
printer.fake = True
printer.model = Models[args.fake]
else:
info(i18n('connecting'))
printer.scan(identifier, use_result=True)
printer.dump = args.dump

if args.nothing:
global Printer
Printer = printer
Expand Down
52 changes: 52 additions & 0 deletions printer_lib/pf2.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def int16be(b: bytes):
u = uint16be(b)
return u - ((u >> 15 & 0b1) << 16)


class Character():
'A PF2 character'

Expand All @@ -39,6 +40,11 @@ class Character():
device_width: int
bitmap_data: bytes

def get_bit(self, x, y):
'Get the bit at (x, y) of this character\'s raster glyph'
char_byte = (self.width * y + x) // 8
char_bit = 7 - (self.width * y + x) % 8
return self.bitmap_data[char_byte] & (0b1 << char_bit)

class PF2():
'The PF2 class, for serializing a PF2 font file'
Expand Down Expand Up @@ -133,3 +139,49 @@ def get_char(self, char: str):

def __del__(self):
self.data_io.close()


class CharacterS(Character):
'A "scaled" character'

scale: int = 1

def get_bit(self, x, y):
'Get the bit at (x, y) of this character\'s raster glyph'
scale = self.scale
width = self.width // scale
x //= scale
y //= scale
char_byte = (width * y + x) // 8
char_bit = 7 - (width * y + x) % 8
return (self.bitmap_data[char_byte] &
(0b1 << char_bit)) >> char_bit

class PF2S(PF2):
'PF2 class with glyph scaling support'

scale: int = 1

def __init__(self, *args, scale: int=1, **kwargs):
super().__init__(*args, **kwargs)
self.scale = scale
self.point_size *= scale
self.max_width *= scale
self.max_height *= scale
self.ascent *= scale
self.descent *= scale

def get_char(self, char):
scale = self.scale
char = super().get_char(char)
chars = CharacterS()
chars.scale = scale
chars.width = char.width * scale
chars.height = char.height * scale
chars.device_width = char.device_width * scale
chars.x_offset = char.x_offset * scale
chars.y_offset = char.y_offset * scale
chars.bitmap_data = char.bitmap_data
return chars

__getitem__ = get_char
15 changes: 7 additions & 8 deletions printer_lib/text_print.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'Things used by Text Printing feature'

from .pf2 import PF2
from .pf2 import PF2S

class TextCanvas():
'Canvas for text printing, requires PF2 lib'
Expand All @@ -9,12 +9,15 @@ class TextCanvas():
canvas: bytearray = None
rtl: bool
wrap: bool
scale: int
pf2 = None
def __init__(self, width, *, wrap=False, rtl=False, font_path='font.pf2'):
self.pf2 = PF2(font_path)
def __init__(self, width, *, wrap=False, rtl=False,
font_path='font.pf2', scale=1):
self.pf2 = PF2S(font_path, scale=scale)
self.width = width
self.wrap = wrap
self.rtl = rtl
self.scale = scale
self.height = self.pf2.max_height + self.pf2.descent
self.flush_canvas()
def flush_canvas(self):
Expand Down Expand Up @@ -79,9 +82,5 @@ def puttext(self, text):
canvas_bit = 7 - (self.width * target_y + current_width + target_x) % 8
if canvas_byte < 0 or canvas_byte >= canvas_length:
continue
char_byte = (char.width * y + x) // 8
char_bit = 7 - (char.width * y + x) % 8
self.canvas[canvas_byte] |= (
char.bitmap_data[char_byte] & (0b1 << char_bit)
) >> char_bit << canvas_bit
self.canvas[canvas_byte] |= char.get_bit(x, y) << canvas_bit
current_width += char.device_width
9 changes: 6 additions & 3 deletions readme.i18n/README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
- 还有安卓!

-[自由软件](https://www.gnu.org/philosophy/free-sw.html)
- 不像“原版”专有应用,此作品为在乎*开放思想与计算自由*的人而生!
- 不像那些专有应用,此作品为在乎*开放思想与计算自由*的人而生!

- 有意思!
- 做什么都可以!
Expand Down Expand Up @@ -118,7 +118,7 @@ Copyright © 2021-2022 NaitLee Soft. 保留一些权利。
敬请查看文件 `COPYING``LICENSE`,以及在 `www/jslicense.html` 中有关 JavaScript 许可的详细内容。

具体地,`printer.py``server.py``main.js` 以 GNU GPL 3 发布。
除去来自其他人的贡献(版权可能归贡献者所有)及可能的第三方依赖,所有其余部分在公有领域(CC0)。
其余所有部分,若无特殊声明,均在公有领域(CC0)。

--------

Expand All @@ -136,8 +136,11 @@ Copyright © 2021-2022 NaitLee Soft. 保留一些权利。

- 当然不能没有 Python 和 Web 技术!
- [Bleak](https://bleak.readthedocs.io/en/latest/) 跨平台蓝牙低功耗库,牛!
- [roddeh-i18n](https://github.com/roddeh/i18njs),当前内置的国际化脚本受此启发
- [roddeh-i18n](https://github.com/roddeh/i18njs),当前内置的国际化功能受此启发
- [PF2 font](http://grub.gibibit.com/New_font_format),很好的简易像素字体格式
- ImageMagick 和 Ghostscript,有用的东西已经在系统里,就当然不用考虑别的了
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
- Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity`
……当然还有其他方面的帮助
- ……每个人都是好样的!
7 changes: 5 additions & 2 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import sys
import json
import platform
from http.server import BaseHTTPRequestHandler
import warnings

# For now we can't use `ThreadingHTTPServer`
from http.server import HTTPServer
from http.server import HTTPServer, BaseHTTPRequestHandler

# import `printer` first, to diagnostic some common errors
from printer import PrinterDriver, PrinterError, i18n, info
Expand All @@ -19,6 +19,9 @@

from printer_lib.ipp import IPP

# Supress non-sense asyncio warnings
warnings.simplefilter('ignore', RuntimeWarning, 0, True)

IsAndroid = (os.environ.get("P4A_BOOTSTRAP") is not None)

class DictAsObject(dict):
Expand Down
Loading

0 comments on commit 1f47d2a

Please sign in to comment.