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*
|
build-common/python-win32*
|
||||||
# dev config
|
# dev config
|
||||||
config.json
|
config.json
|
||||||
# traffic dump
|
# test files
|
||||||
*.dump
|
*.dump
|
||||||
|
*.pf2
|
||||||
|
*.pbm
|
||||||
# some other junk
|
# some other junk
|
||||||
.directory
|
.directory
|
||||||
thumbs.db
|
thumbs.db
|
||||||
|
13
.pylintrc
13
.pylintrc
@ -17,14 +17,5 @@ disable=broad-except,
|
|||||||
import-outside-toplevel
|
import-outside-toplevel
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
good-names=i,
|
good-names=i, j, k, ex, x, y, _, e,
|
||||||
j,
|
Run, do_GET, do_POST, do_HEAD, do_PUT
|
||||||
k,
|
|
||||||
ex,
|
|
||||||
Run,
|
|
||||||
_,
|
|
||||||
e,
|
|
||||||
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
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"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",
|
"name": "Python: Current File",
|
||||||
"type": "python",
|
"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
|
#!/bin/sh
|
||||||
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
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" \
|
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
|
||||||
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
||||||
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
for i in $(find | grep -E '.*\.pyc'); do rm $i; done
|
||||||
python3 bundle.py $1
|
python3 bundle.py $1
|
||||||
python3 bundle.py -w $1
|
python3 bundle.py -w $1
|
||||||
python3 bundle.py -b $1
|
python3 bundle.py -b $1
|
||||||
|
@ -38,7 +38,9 @@ additional_ignore = (
|
|||||||
'.pylintrc',
|
'.pylintrc',
|
||||||
'.gitignore',
|
'.gitignore',
|
||||||
'dev-diary.txt',
|
'dev-diary.txt',
|
||||||
'TODO'
|
'TODO',
|
||||||
|
# cache
|
||||||
|
'*.pyc',
|
||||||
# other
|
# other
|
||||||
'.directory',
|
'.directory',
|
||||||
'thumbs.db',
|
'thumbs.db',
|
||||||
|
114
printer.py
114
printer.py
@ -119,6 +119,7 @@ class PBMData():
|
|||||||
height: int
|
height: int
|
||||||
data: bytes
|
data: bytes
|
||||||
args: dict
|
args: dict
|
||||||
|
'Note: going to put this in `PrinterDriver` in the future'
|
||||||
|
|
||||||
def __init__(self, width: int, height: int, data: bytes, args: dict = None):
|
def __init__(self, width: int, height: int, data: bytes, args: dict = None):
|
||||||
self.width = width
|
self.width = width
|
||||||
@ -134,6 +135,57 @@ class PBMData():
|
|||||||
for arg in args:
|
for arg in args:
|
||||||
self.args[arg] = args[arg]
|
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():
|
class PrinterDriver():
|
||||||
'Manipulator of the printer'
|
'Manipulator of the printer'
|
||||||
@ -152,20 +204,26 @@ class PrinterDriver():
|
|||||||
feed_after = 128
|
feed_after = 128
|
||||||
'Extra paper to feed at the end of printing, by pixel'
|
'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)'
|
'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'
|
'It\'s a constant for the printer'
|
||||||
|
|
||||||
pbm_data_per_line = int(standard_width / 8) # 48
|
pbm_data_per_line = int(paper_width / 8) # 48
|
||||||
'Constant, determined by standard width & PBM data format'
|
'Determined by paper width & PBM data format'
|
||||||
|
|
||||||
characteristic = '0000ae01-0000-1000-8000-00805f9b34fb'
|
characteristic = '0000ae01-0000-1000-8000-00805f9b34fb'
|
||||||
'The BLE characteristic, a constant of the printer'
|
'The BLE characteristic, a constant of the printer'
|
||||||
|
|
||||||
mtu = 200
|
mtu = 200
|
||||||
|
|
||||||
|
text_canvas: TextCanvas = None
|
||||||
|
'A `TextCanvas` instance'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -192,7 +250,7 @@ class PrinterDriver():
|
|||||||
if line[0:1] != b'#':
|
if line[0:1] != b'#':
|
||||||
break
|
break
|
||||||
width, height = [int(x) for x in line.split(b' ')[0:2]]
|
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')
|
raise Exception('PBM image width is not 384px')
|
||||||
total_height += height
|
total_height += height
|
||||||
expected_data_size = self.pbm_data_per_line * height
|
expected_data_size = self.pbm_data_per_line * height
|
||||||
@ -205,7 +263,7 @@ class PrinterDriver():
|
|||||||
result += b'\x00' * expected_data_size
|
result += b'\x00' * expected_data_size
|
||||||
else:
|
else:
|
||||||
result += raw_data
|
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):
|
def _pbm_data_to_raw(self, data: PBMData):
|
||||||
buffer = bytearray()
|
buffer = bytearray()
|
||||||
@ -254,6 +312,9 @@ class PrinterDriver():
|
|||||||
|
|
||||||
async def send_buffer(self, buffer: bytearray, address: str = None):
|
async def send_buffer(self, buffer: bytearray, address: str = None):
|
||||||
'Send manipulation data (buffer) to the printer via bluetooth'
|
'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
|
address = address or self.address
|
||||||
client = BleakClient(address, timeout=5.0)
|
client = BleakClient(address, timeout=5.0)
|
||||||
await client.connect()
|
await client.connect()
|
||||||
@ -316,7 +377,7 @@ class PrinterDriver():
|
|||||||
if device.name == info:
|
if device.name == info:
|
||||||
self.name, self.address = device.name, device.address
|
self.name, self.address = device.name, device.address
|
||||||
break
|
break
|
||||||
elif info[2::3] == ':::::':
|
elif info[2::3] == ':::::' or len(info.replace('-', '')) == 32:
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if device.address.lower() == info.lower():
|
if device.address.lower() == info.lower():
|
||||||
self.name, self.address = device.name, device.address
|
self.name, self.address = device.name, device.address
|
||||||
@ -326,6 +387,27 @@ class PrinterDriver():
|
|||||||
self.name, self.address = device.name, device.address
|
self.name, self.address = device.name, device.address
|
||||||
return True
|
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():
|
async def _main():
|
||||||
'Main routine for direct command line execution'
|
'Main routine for direct command line execution'
|
||||||
@ -347,6 +429,10 @@ async def _main():
|
|||||||
help=i18n['communication-frequency-0.8-or-1-recommended'])
|
help=i18n['communication-frequency-0.8-or-1-recommended'])
|
||||||
parser.add_argument('-d', '--dry', required=False, action='store_true',
|
parser.add_argument('-d', '--dry', required=False, action='store_true',
|
||||||
help=i18n['dry-run-test-print-process-only'])
|
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()
|
cmdargs = parser.parse_args()
|
||||||
addr = cmdargs.address
|
addr = cmdargs.address
|
||||||
printer = PrinterDriver()
|
printer = PrinterDriver()
|
||||||
@ -367,9 +453,19 @@ async def _main():
|
|||||||
'name': device.name,
|
'name': device.name,
|
||||||
'address': device.address,
|
'address': device.address,
|
||||||
'frequency': cmdargs.freq,
|
'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'])
|
print(i18n['finished'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,6 +59,9 @@
|
|||||||
"path-to-pbm-file-dash-for-stdin": "Path to PBM file. '-' for stdin.",
|
"path-to-pbm-file-dash-for-stdin": "Path to PBM file. '-' for stdin.",
|
||||||
"scan-for-specified-seconds": "Scan for specified seconds",
|
"scan-for-specified-seconds": "Scan for specified seconds",
|
||||||
"specify-printer-mac-address": "Specify printer MAC address",
|
"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 文件的位置。“-” 作为标准输入。",
|
"path-to-pbm-file-dash-for-stdin": "PBM 文件的位置。“-” 作为标准输入。",
|
||||||
"scan-for-specified-seconds": "扫描指定的时长。",
|
"scan-for-specified-seconds": "扫描指定的时长。",
|
||||||
"specify-printer-mac-address": "指定打印机的 MAC 地址",
|
"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