Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Printing Text #50

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# EditorConfig is awesome: http://EditorConfig.org

# https://github.com/jokeyrhyme/standard-editorconfig

# top-most EditorConfig file
root = true

# defaults
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# Bazel: https://bazel.build/
# https://github.com/bazelbuild/buildtools/blob/master/BUILD.bazel
[*.{bazel,bzl}]
indent_size = 4
indent_style = space

# CSS
# https://google.github.io/styleguide/htmlcssguide.xml#General_Formatting_Rules
# http://cssguidelin.es/#syntax-and-formatting
[*.css]
indent_size = 2
indent_style = space
trim_trailing_whitespace = true

# GNU make
# https://www.gnu.org/software/make/manual/html_node/Recipe-Syntax.html
[Makefile]
indent_style = tab

# Go
# https://golang.org/cmd/gofmt/
[{go.mod,*.go}]
indent_style = tab

# GraphQL
# https://graphql.org/learn/
# https://prettier.io
[*.graphql]
indent_size = 2
indent_style = space

# HTML
# https://google.github.io/styleguide/htmlcssguide.xml#General_Formatting_Rules
[*.{htm,html}]
indent_size = 2
indent_style = space
trim_trailing_whitespace = true

# Java
# https://google.github.io/styleguide/javaguide.html#s4.2-block-indentation
[*.java]
indent_size = 2
indent_style = space

# JavaScript, JSON, JSX, JavaScript Modules, TypeScript
# https://github.com/feross/standard
# https://prettier.io
[*.{cjs,js,json,jsx,mjs,ts,tsx}]
indent_size = 2
indent_style = space

# Kotlin
# https://android.github.io/kotlin-guides/style.html#indentation
[*.{kt,kts}]
indent_size = 4
indent_style = space

# LESS
# https://github.com/less/less-docs#less-standards
[*.less]
indent_size = 2
indent_style = space

# PHP
# http://www.php-fig.org/psr/psr-2/
[*.php]
indent_size = 4
indent_style = space

# Python
# https://www.python.org/dev/peps/pep-0008/#code-lay-out
[*.py]
indent_size = 4
indent_style = space

# Ruby
# http://www.caliban.org/ruby/rubyguide.shtml#indentation
[*.rb]
indent_size = 2
indent_style = space

# Rust
# https://github.com/rust-lang/rust/blob/master/src/doc/style/style/whitespace.md
[*.rs]
indent_size = 4
indent_style = space
insert_final_newline = false
trim_trailing_whitespace = true

# SASS
# https://sass-guidelin.es/#syntax--formatting
[*.{sass,scss}]
indent_size = 2
indent_style = space

# Shell
# https://google.github.io/styleguide/shell.xml#Indentation
[*.{bash,sh,zsh}]
indent_size = 2
indent_style = space

# Svelte
# https://github.com/sveltejs/svelte/blob/master/.editorconfig
[*.svelte]
indent_size = 2
indent_style = tab

# TOML
# https://github.com/toml-lang/toml/tree/master/examples
[*.toml]
indent_size = 2
indent_style = space

# YAML
# http://yaml.org/spec/1.2/2009-07-21/spec.html#id2576668
[*.{yaml,yml}]
indent_size = 2
indent_style = space
43 changes: 18 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,32 @@ $ pip install -r requirements.txt

# Usage
```bash
$ ./print.py --help
usage: print.py [-h] [-l {debug,info,warn,error}]
[-b {mean-threshold,floyd-steinberg,halftone,none}] [-s]
[-d DEVICE] [-t]
filename
$ python print.py --help
usage: print.py [-h] [--filename FILENAME] [--text TEXT] [--log-level {debug,info,warn,error}]
[--img-binarization-algo {mean-threshold,floyd-steinberg,halftone,none}] [--show-preview]
[--devicename DEVICENAME] [--darker]

prints an image on your cat thermal printer

positional arguments:
filename

optional arguments:
options:
-h, --help show this help message and exit
-l {debug,info,warn,error}, --log-level {debug,info,warn,error}
-b {mean-threshold,floyd-steinberg,halftone,none}, --img-binarization-algo {mean-threshold,floyd-steinberg,halftone,none}
Which image binarization algorithm to use. If 'none'
is used, no binarization will be used. In this case
the image has to have a width of 384 px.
-s, --show-preview If set, displays the final image and asks the user for
confirmation before printing.
-d DEVICE, --device DEVICE
The printer's Bluetooth Low Energy (BLE) address (MAC
address on Linux; UUID on macOS) or advertisement name
(e.g.: "GT01", "GB02", "GB03"). If omitted, the the
script will try to auto discover the printer based on
its advertised BLE services.
-t, --darker Print the image in text mode. This leads to more
contrast, but slower speed.
--filename FILENAME Path to image file to print.
--text TEXT Prints provided text messages. Either --filename or --text should be provided, but not both.
--log-level {debug,info,warn,error}
--img-binarization-algo {mean-threshold,floyd-steinberg,halftone,none}
Which image binarization algorithm to use. If 'none' is used, no binarization will be used. In this
case the image has to have a width of 384 px.
--show-preview If set, displays the final image and asks the user for confirmation before printing.
--devicename DEVICENAME
Specify the Bluetooth Low Energy (BLE) device name to search for. If not specified, the script will
try to auto discover the printer based on its advertised BLE service UUIDs. Common names are similar
to "GT01", "GB02", "GB03".
--darker Print the image in text mode. This leads to more contrast, but slower speed.
```

# Example
```bash
% ./print.py --show-preview test.png
% python print.py --show-preview --filename test.png
⏳ Applying Floyd-Steinberg dithering to image...
✅ Done.
ℹ️ Displaying preview.
Expand Down
8 changes: 6 additions & 2 deletions catprinter/cmds.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

PRINT_WIDTH = 384

# How much extra paper to feed in order to clear the catprinter's enclosure.
POST_PRINT_FEED_LENGTH = 25

def to_unsigned_byte(val):
'''Converts a byte in signed representation to unsigned. Assumes val is encoded in two's
Expand Down Expand Up @@ -167,9 +169,11 @@ def cmd_print_row(img_row):
return b_arr


def cmds_print_img(img, dark_mode=False):
def cmds_print_img(img: bytes, dark_mode: bool=False) -> bytearray:
"""Given the `img` this wraps it in a payload the catprinter understands."""

PRINTER_MODE = CMD_PRINT_TEXT if dark_mode else CMD_PRINT_IMG
# Not sure what the intent was, but commenting out as its not used.
# PRINTER_MODE = CMD_PRINT_TEXT if dark_mode else CMD_PRINT_IMG

data = \
CMD_GET_DEV_STATE + \
Expand Down
18 changes: 12 additions & 6 deletions catprinter/img.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import cv2
import io
from math import ceil

import cv2
import numpy as np

from catprinter import logger
Expand Down Expand Up @@ -90,11 +92,15 @@ def square_avg_value(square):


def read_img(
filename,
print_width,
img_binarization_algo,
):
im = cv2.imread(filename, cv2.IMREAD_GRAYSCALE)
file: io.BytesIO,
print_width,
logger,
img_binarization_algo,
):
"""Takes `file` and rescales/converts image to be just right for the
catprinter. This returns `bytes`, ready to transmit to the printer."""
img_as_array = np.asarray(file, dtype="uint8")
im = cv2.imdecode(img_as_array, cv2.IMREAD_GRAYSCALE)
height = im.shape[0]
width = im.shape[1]
factor = print_width / width
Expand Down
25 changes: 25 additions & 0 deletions catprinter/txt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import tempfile

from PIL import Image, ImageDraw, ImageFont


def text_to_image(text):
# Fix newline characters that argparse escaped
text = text.replace("\\n", "\n")
font_path = "fonts/Roboto-Regular.ttf"
font_size = 72
width_scalar = 0.25

font = ImageFont.truetype(font_path, font_size, encoding="utf-8")
w, h = font.getsize_multiline(text)
image = Image.new("L", (w, int(h+h*width_scalar)), 255)
draw = ImageDraw.Draw(image)
draw.text((0,0), text, fill="black", font=font)

bits = None
with tempfile.TemporaryFile() as fp:
image.save(fp, "PNG")
fp.seek(0)
bits = fp.read()

return bits
Binary file added fonts/Roboto-Regular.ttf
Binary file not shown.
55 changes: 36 additions & 19 deletions print.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
#!/usr/bin/env python
import argparse
import asyncio
import io
import logging
import sys
import os
import sys

from catprinter import logger
from catprinter.cmds import PRINT_WIDTH, cmds_print_img
from catprinter.ble import run_ble
from catprinter.img import read_img, show_preview
from catprinter import ble, cmds, img, logger, txt


def parse_args():
args = argparse.ArgumentParser(
description='prints an image on your cat thermal printer')
args.add_argument('filename', type=str)
args.add_argument('--filename', type=str, default="", help="Path to image file to print.")
args.add_argument('--text', type=str, help="Prints provided text messages. Linebreaks can be added with a newline character \\n Either --filename or --text should be provided, but not both.")
args.add_argument('-l', '--log-level', type=str,
choices=['debug', 'info', 'warn', 'error'], default='info')
args.add_argument('-b', '--img-binarization-algo', type=str,
choices=['mean-threshold',
'floyd-steinberg', 'halftone', 'none'],
default='floyd-steinberg',
help=f'Which image binarization algorithm to use. If \'none\' \
is used, no binarization will be used. In this case the \
image has to have a width of {PRINT_WIDTH} px.')
args.add_argument('-s', '--show-preview', action='store_true',
help=f'Which image binarization algorithm to use. If \'none\' is used, no binarization will be used. In this case the image has to have a width of {cmds.PRINT_WIDTH} px.')
args.add_argument('--show-preview', action='store_true',
help='If set, displays the final image and asks the user for \
confirmation before printing.')
args.add_argument('-d', '--device', type=str, default='',
Expand Down Expand Up @@ -55,28 +52,48 @@ def main():
configure_logger(log_level)

filename = args.filename
if not os.path.exists(filename):
text = args.text

if not filename and not text:
logger.info('🛑 Both a filename and text are missing. Please provide one of these parameters. Exiting')
return

if filename and text:
logger.info('🛑 Both a filename and text were provided, only one of these parameters is allowed at a time. Exiting')
return

if not text and not os.path.exists(filename):
logger.info('🛑 File not found. Exiting.')
return

try:
bin_img = read_img(
args.filename,
PRINT_WIDTH,
args.img_binarization_algo,
)
bin_img = None
if filename:
with open(filename, "rb") as f:
img_arr = bytearray(f.read())
bin_img = img.read_img(img_arr, cmds.PRINT_WIDTH,
logger, args.img_binarization_algo,)
elif text:
txt_arr = bytearray(txt.text_to_image(text))
bin_img = img.read_img(txt_arr, cmds.PRINT_WIDTH,
logger, args.img_binarization_algo,)

if bin_img is None:
logger.info(f'🛑 No image generated. Exiting.')
return

if args.show_preview:
show_preview(bin_img)
img.show_preview(bin_img)
except RuntimeError as e:
logger.error(f'🛑 {e}')
return

logger.info(f'✅ Read image: {bin_img.shape} (h, w) pixels')
data = cmds_print_img(bin_img, dark_mode=args.darker)
data = cmds.cmds_print_img(bin_img, dark_mode=args.darker)
logger.info(f'✅ Generated BLE commands: {len(data)} bytes')

# Try to autodiscover a printer if --device is not specified.
asyncio.run(run_ble(data, device=args.device))
asyncio.run(ble.run_ble(data, device=args.device))


if __name__ == '__main__':
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bleak~=0.14.2
numpy<2.0
opencv-python<5.0
opencv-python<5.0
Pillow==9.1.0