mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-15 23:00:15 -07:00
Usable text printing (needs code cleaning later)
This commit is contained in:
parent
cc93b981f5
commit
996bc5ec99
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
|
||||
|
13
.pylintrc
13
.pylintrc
@ -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
11
.vscode/launch.json
vendored
@ -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
136
additional/pf2.py
Normal 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()
|
@ -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 \
|
||||
|
@ -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
|
||||
|
@ -38,7 +38,9 @@ additional_ignore = (
|
||||
'.pylintrc',
|
||||
'.gitignore',
|
||||
'dev-diary.txt',
|
||||
'TODO'
|
||||
'TODO',
|
||||
# cache
|
||||
'*.pyc',
|
||||
# other
|
||||
'.directory',
|
||||
'thumbs.db',
|
||||
|
114
printer.py
114
printer.py
@ -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'])
|
||||
|
||||
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
@ -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": "文字打印模式,将从标准输入读取文字。"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user