Usable text printing (needs code cleaning later)

This commit is contained in:
NaitLee 2022-04-14 01:24:09 +08:00
parent cc93b981f5
commit 996bc5ec99
10 changed files with 270 additions and 25 deletions

4
.gitignore vendored
View File

@ -22,8 +22,10 @@ build-common/bleak
build-common/python-win32*
# dev config
config.json
# traffic dump
# test files
*.dump
*.pf2
*.pbm
# some other junk
.directory
thumbs.db

View File

@ -17,14 +17,5 @@ disable=broad-except,
import-outside-toplevel
[BASIC]
good-names=i,
j,
k,
ex,
Run,
_,
e,
do_GET,
do_POST,
do_HEAD,
do_PUT
good-names=i, j, k, ex, x, y, _, e,
Run, do_GET, do_POST, do_HEAD, do_PUT

11
.vscode/launch.json vendored
View File

@ -4,6 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Text Print",
"type": "python",
"request": "launch",
"program": "printer.py",
"args": [
"-m", "-t", "-d", "-s", "1", "-"
],
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Python: Current File",
"type": "python",

136
additional/pf2.py Normal file
View File

@ -0,0 +1,136 @@
'Python lib to read PF2 font file: http://grub.gibibit.com/New_font_format'
import io
from typing import Dict, Tuple
def uint32be(b: bytes):
'Translate 4 bytes as unsigned big-endian 32-bit int'
return (
(b[0] << 32) +
(b[1] << 16) +
(b[2] << 8) +
b[3]
)
def int32be(b: bytes):
'Translate 4 bytes as signed big-endian 32-bit int'
u = uint32be(b)
return u - ((u >> 31 & 0b1) << 32)
def uint16be(b: bytes):
'Translate 2 bytes as big-endian unsigned 16-bit int'
return (b[0] << 8) + b[1]
def int16be(b: bytes):
'Translate 2 bytes as big-endian signed 16-bit int'
u = uint16be(b)
return u - ((u >> 15 & 0b1) << 16)
class Character():
'A PF2 character'
width: int
height: int
x_offset: int
y_offset: int
device_width: int
bitmap_data: bytes
class PF2():
'The PF2 class, for serializing a PF2 font file'
is_pf2: bool
'Sets to false if the read file is not PF2 font file'
missing_character_code: int
in_memory: bool
font_name: str
family: str
weight: str
slant: str
point_size: int
max_width: int
max_height: int
ascent: int
descent: int
character_index: Dict[int, Tuple[int, int]]
data_offset: int
data_io: io.IOBase
def __init__(self, path='font.pf2', *, read_to_mem=False, missing_character: str='?'):
self.missing_character_code = ord(missing_character)
self.in_memory = read_to_mem
file = open(path, 'rb')
self.is_pf2 = (file.read(12) == b'FILE\x00\x00\x00\x04PFF2')
if not self.is_pf2:
return
while True:
name = file.read(4)
data_length = int32be(file.read(4))
if name == b'CHIX':
self.character_index = {}
for _ in range(data_length // (4 + 1 + 4)):
code_point = int32be(file.read(4))
compression = file.read(1)[0]
offset = int32be(file.read(4))
self.character_index[code_point] = (
compression, offset
)
continue
elif name == b'DATA':
if read_to_mem:
self.data_io = io.BytesIO(file.read())
self.data_offset = -file.tell()
file.close()
else:
self.data_io = file
self.data_offset = 0
break
data = file.read(data_length)
if name == b'NAME':
self.font_name = data
elif name == b'FAMI':
self.family = data
elif name == b'WEIG':
self.weight = data
elif name == b'SLAN':
self.slant = data
elif name == b'PTSZ':
self.point_size = uint16be(data)
elif name == b'MAXW':
self.max_width = uint16be(data)
elif name == b'MAXH':
self.max_height = uint16be(data)
elif name == b'ASCE':
self.ascent = uint16be(data)
elif name == b'DESC':
self.descent = uint16be(data)
def get_char(self, char: str):
'Get a character, returning a `Character` instance'
char_point = ord(char)
info = self.character_index.get(char_point)
if info is None:
info = self.character_index[self.missing_character_code]
_compression, offset = info
data = self.data_io
data.seek(offset + self.data_offset)
char = Character()
char.width = uint16be(data.read(2))
char.height = uint16be(data.read(2))
char.x_offset = int16be(data.read(2))
char.y_offset = int16be(data.read(2))
char.device_width = int16be(data.read(2))
char.bitmap_data = data.read(
(char.width * char.height + 7) // 8
)
return char
__getitem__ = get_char
def close(self):
'Close the data IO, if it\'s a real file'
if not self.in_memory:
self.data_io.close()

View File

@ -1,6 +1,6 @@
#!/bin/sh
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
--icon=icon.png --version="0.1.0" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
--icon=icon.png --version="0.1.1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \

View File

@ -1,4 +1,5 @@
#!/bin/sh
for i in $(find | grep -E '.*\.pyc'); do rm $i; done
python3 bundle.py $1
python3 bundle.py -w $1
python3 bundle.py -b $1

View File

@ -38,7 +38,9 @@ additional_ignore = (
'.pylintrc',
'.gitignore',
'dev-diary.txt',
'TODO'
'TODO',
# cache
'*.pyc',
# other
'.directory',
'thumbs.db',

View File

@ -119,6 +119,7 @@ class PBMData():
height: int
data: bytes
args: dict
'Note: going to put this in `PrinterDriver` in the future'
def __init__(self, width: int, height: int, data: bytes, args: dict = None):
self.width = width
@ -134,6 +135,57 @@ class PBMData():
for arg in args:
self.args[arg] = args[arg]
class TextCanvas():
'Canvas for text printing, requires PF2 lib'
width: int
height: int
canvas: bytearray = None
pf2 = None
def __init__(self, width):
if self.pf2 is None:
from additional.pf2 import PF2
self.pf2 = PF2()
self.width = width
self.height = self.pf2.max_height + self.pf2.descent
self.flush_canvas()
def flush_canvas(self):
'Flush the canvas, returning the canvas data'
if self.canvas is None:
pbm_data = None
else:
pbm_data = bytearray(self.canvas)
self.canvas = bytearray(self.width * self.height // 8)
return pbm_data
def puttext(self, text):
'Put the specified text to canvas'
current_width = 0
canvas_length = len(self.canvas)
pf2 = self.pf2
for i in text:
char = pf2[i]
if (
current_width + char.width + char.x_offset > self.width or
i == '\n'
):
yield self.flush_canvas()
current_width = 0
if i in '\n': # glyphs that should not be printed out
continue
for x in range(char.width):
for y in range(char.height):
target_x = x + char.x_offset
target_y = pf2.ascent + (y - char.height) - char.y_offset
canvas_byte = (self.width * target_y + current_width + target_x) // 8
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
current_width += char.device_width
return
class PrinterDriver():
'Manipulator of the printer'
@ -152,20 +204,26 @@ class PrinterDriver():
feed_after = 128
'Extra paper to feed at the end of printing, by pixel'
dry_run = False
dry_run = None
'Is dry run (emulate print process but print nothing)'
standard_width = 384
dump = None
'Dump the traffic (and PBM image when text printing)?'
paper_width = 384
'It\'s a constant for the printer'
pbm_data_per_line = int(standard_width / 8) # 48
'Constant, determined by standard width & PBM data format'
pbm_data_per_line = int(paper_width / 8) # 48
'Determined by paper width & PBM data format'
characteristic = '0000ae01-0000-1000-8000-00805f9b34fb'
'The BLE characteristic, a constant of the printer'
mtu = 200
text_canvas: TextCanvas = None
'A `TextCanvas` instance'
def __init__(self):
pass
@ -192,7 +250,7 @@ class PrinterDriver():
if line[0:1] != b'#':
break
width, height = [int(x) for x in line.split(b' ')[0:2]]
if width != self.standard_width:
if width != self.paper_width:
raise Exception('PBM image width is not 384px')
total_height += height
expected_data_size = self.pbm_data_per_line * height
@ -205,7 +263,7 @@ class PrinterDriver():
result += b'\x00' * expected_data_size
else:
result += raw_data
return PBMData(self.standard_width, total_height, result)
return PBMData(self.paper_width, total_height, result)
def _pbm_data_to_raw(self, data: PBMData):
buffer = bytearray()
@ -254,6 +312,9 @@ class PrinterDriver():
async def send_buffer(self, buffer: bytearray, address: str = None):
'Send manipulation data (buffer) to the printer via bluetooth'
if self.dump:
with open('traffic.dump', 'wb') as file:
file.write(buffer)
address = address or self.address
client = BleakClient(address, timeout=5.0)
await client.connect()
@ -316,7 +377,7 @@ class PrinterDriver():
if device.name == info:
self.name, self.address = device.name, device.address
break
elif info[2::3] == ':::::':
elif info[2::3] == ':::::' or len(info.replace('-', '')) == 32:
for device in devices:
if device.address.lower() == info.lower():
self.name, self.address = device.name, device.address
@ -326,6 +387,27 @@ class PrinterDriver():
self.name, self.address = device.name, device.address
return True
async def print_text(self, file_io: io.IOBase):
'Print some text from `file_io`'
if self.text_canvas is None:
self.text_canvas = TextCanvas(self.paper_width)
canvas = self.text_canvas
header = b'P4\n%i %i\n'
dump = bytearray()
current_height = 0
while True:
text = file_io.readline()
if not text:
break
for data in canvas.puttext(text):
if self.dump:
dump += data
current_height += canvas.height
with open('dump.pbm', 'wb') as file:
file.write(header % (canvas.width, current_height))
file.write(dump)
await self.print_data(bytearray(header % (canvas.width, canvas.height)) + data)
async def _main():
'Main routine for direct command line execution'
@ -347,6 +429,10 @@ async def _main():
help=i18n['communication-frequency-0.8-or-1-recommended'])
parser.add_argument('-d', '--dry', required=False, action='store_true',
help=i18n['dry-run-test-print-process-only'])
parser.add_argument('-m', '--dump', required=False, action='store_true',
help=i18n['dump-the-traffic-to-printer-and-pbm-image-when-text-printing'])
parser.add_argument('-t', '--text', required=False, action='store_true',
help=i18n['text-printing-mode-input-text-from-stdin'])
cmdargs = parser.parse_args()
addr = cmdargs.address
printer = PrinterDriver()
@ -367,9 +453,19 @@ async def _main():
'name': device.name,
'address': device.address,
'frequency': cmdargs.freq,
'dry_run': cmdargs.dry
'dry_run': cmdargs.dry,
'dump': cmdargs.dump
})
await printer.print_file(cmdargs.file)
if cmdargs.text:
if cmdargs.file == '-':
file = sys.stdin
else:
file = open(cmdargs.file, 'r', encoding='utf-8')
await printer.print_text(file)
if cmdargs.file != '-':
file.close()
else:
await printer.print_file(cmdargs.file)
print(i18n['finished'])

View File

@ -59,6 +59,9 @@
"path-to-pbm-file-dash-for-stdin": "Path to PBM file. '-' for stdin.",
"scan-for-specified-seconds": "Scan for specified seconds",
"specify-printer-mac-address": "Specify printer MAC address",
"communication-frequency-0.8-or-1-recommended": "Communication frequency. 0.8 or 1 recommended."
"communication-frequency-0.8-or-1-recommended": "Communication frequency. 0.8 or 1 recommended.",
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "Dump the traffic to printer, and PBM image when text printing.",
"text-printing-mode-input-text-from-stdin": "Text printing mode, input from stdin."
}
}

View File

@ -58,6 +58,9 @@
"path-to-pbm-file-dash-for-stdin": "PBM 文件的位置。“-” 作为标准输入。",
"scan-for-specified-seconds": "扫描指定的时长。",
"specify-printer-mac-address": "指定打印机的 MAC 地址",
"communication-frequency-0.8-or-1-recommended": "通讯频率。推荐 0.8 或 1。"
"communication-frequency-0.8-or-1-recommended": "通讯频率。推荐 0.8 或 1。",
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "转储到打印机的数据,和文字打印模式的 PBM 图像。",
"text-printing-mode-input-text-from-stdin": "文字打印模式,将从标准输入读取文字。"
}
}