tinyprint 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinyprint/__init__.py +5 -0
- tinyprint/__main__.py +254 -0
- tinyprint/config.py +5 -0
- tinyprint/job.py +306 -0
- tinyprint/jobconfig.py +73 -0
- tinyprint/preprocessor.py +132 -0
- tinyprint/xescpos.py +166 -0
- tinyprint-0.1.0.dist-info/LICENSE +674 -0
- tinyprint-0.1.0.dist-info/METADATA +21 -0
- tinyprint-0.1.0.dist-info/RECORD +13 -0
- tinyprint-0.1.0.dist-info/WHEEL +5 -0
- tinyprint-0.1.0.dist-info/entry_points.txt +2 -0
- tinyprint-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from typing import Callable, TypeAlias
|
|
5
|
+
|
|
6
|
+
from escpos.escpos import Escpos # type: ignore
|
|
7
|
+
import marko # type: ignore
|
|
8
|
+
from PIL import Image # type: ignore
|
|
9
|
+
|
|
10
|
+
from tinyprint.jobconfig import Resource
|
|
11
|
+
from tinyprint.job import Job
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Page: TypeAlias = Callable[[Escpos], None]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Preprocessor(abc.ABC):
|
|
18
|
+
def __init__(self, job: Job):
|
|
19
|
+
self.job = job
|
|
20
|
+
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
def __call__(self, resource: Resource) -> Page: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ImagePreprocessor(Preprocessor):
|
|
26
|
+
def __call__(self, resource: Resource):
|
|
27
|
+
profile = self.job.printer.profile
|
|
28
|
+
|
|
29
|
+
def _(printer):
|
|
30
|
+
|
|
31
|
+
with Image.open(resource[0]) as im:
|
|
32
|
+
max_width = profile.profile_data["media"]["width"]["pixels"]
|
|
33
|
+
assert max_width != "Unknown", "max_width of printer unknown!"
|
|
34
|
+
max_width = int(max_width * profile.img_scale_factor)
|
|
35
|
+
width = im.width
|
|
36
|
+
heigth = im.height
|
|
37
|
+
factor = max_width / width
|
|
38
|
+
im = im.resize(
|
|
39
|
+
(
|
|
40
|
+
int(width * factor * profile.img_x_scale_factor),
|
|
41
|
+
int(heigth * factor * profile.img_y_scale_factor),
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Add extra space between to make spaces between cuts even
|
|
46
|
+
# XXX Does number '10' need to change depending on printer?
|
|
47
|
+
if self.job.config.cut:
|
|
48
|
+
printer.line_spacing(10)
|
|
49
|
+
printer.ln()
|
|
50
|
+
printer.line_spacing()
|
|
51
|
+
|
|
52
|
+
img_kwargs = dict(self.job.printer.profile.img_kwargs)
|
|
53
|
+
if fragment_height := resource[1]:
|
|
54
|
+
img_kwargs["fragment_height"] = fragment_height
|
|
55
|
+
|
|
56
|
+
printer.image(
|
|
57
|
+
im,
|
|
58
|
+
**img_kwargs,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return _
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# https://github.com/Belval/pdf2image
|
|
65
|
+
# but then we depend on 1 more external program: poppler
|
|
66
|
+
class PdfPreprocessor(Preprocessor): ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MarkdownPreprocessor(Preprocessor):
|
|
70
|
+
def __init__(self, *args, **kwargs):
|
|
71
|
+
super().__init__(*args, **kwargs)
|
|
72
|
+
self.markdown = marko.Markdown(renderer=_EscposRenderer)
|
|
73
|
+
self.markdown._setup_extensions()
|
|
74
|
+
|
|
75
|
+
def __call__(self, resource: Resource):
|
|
76
|
+
with open(resource[0], "r") as f:
|
|
77
|
+
md = f.read()
|
|
78
|
+
|
|
79
|
+
def _(printer):
|
|
80
|
+
self.markdown.renderer.set_printer(printer)
|
|
81
|
+
self.markdown(md)
|
|
82
|
+
|
|
83
|
+
return _
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _EscposRenderer(marko.renderer.Renderer):
|
|
87
|
+
# Ref: https://github.com/frostming/marko/blob/master/marko/html_renderer.py
|
|
88
|
+
|
|
89
|
+
# TODO Allow images
|
|
90
|
+
# TODO Add auto line breaks
|
|
91
|
+
# TODO Add auto page breaks
|
|
92
|
+
|
|
93
|
+
def render_raw_text(self, element) -> str:
|
|
94
|
+
self._printer.text(element.children)
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
def render_emphasis(self, element) -> str:
|
|
98
|
+
self._set(bold=True)
|
|
99
|
+
self.render_children(element)
|
|
100
|
+
self._set(bold=False)
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
def render_strong_emphasis(self, element) -> str:
|
|
104
|
+
return self.render_emphasis(element)
|
|
105
|
+
|
|
106
|
+
def render_line_break(self, element) -> str:
|
|
107
|
+
self._printer.ln(2)
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
def render_paragraph(self, element) -> str:
|
|
111
|
+
self.render_children(element)
|
|
112
|
+
self._printer.ln(2)
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
def render_heading(self, element) -> str:
|
|
116
|
+
self._set(double_height=True, double_width=True)
|
|
117
|
+
self.render_children(element)
|
|
118
|
+
self._set(double_height=False, double_width=False, normal_textsize=True)
|
|
119
|
+
self._printer.ln(3)
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
def set_printer(self, printer):
|
|
123
|
+
self._printer = printer
|
|
124
|
+
|
|
125
|
+
def set_job(self, job):
|
|
126
|
+
self._job = job
|
|
127
|
+
|
|
128
|
+
def _set(self, *args, **kwargs):
|
|
129
|
+
if self._printer:
|
|
130
|
+
self._printer.set(*args, **kwargs)
|
|
131
|
+
else:
|
|
132
|
+
raise RuntimeError("printer not yet set")
|
tinyprint/xescpos.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Patching escpos library to cut between images without whitespace between
|
|
2
|
+
|
|
3
|
+
See comment at Job.print for more details.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import six # type: ignore
|
|
12
|
+
|
|
13
|
+
from escpos.image import EscposImage # type: ignore
|
|
14
|
+
from escpos.escpos import Escpos # type: ignore
|
|
15
|
+
from escpos.constants import ESC, GS # type: ignore
|
|
16
|
+
from escpos.exceptions import ImageWidthError # type: ignore
|
|
17
|
+
from escpos.printer.usb import Usb as UsbPrinter # type: ignore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Cmd:
|
|
22
|
+
index: int
|
|
23
|
+
cmd: bytes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if 1:
|
|
27
|
+
|
|
28
|
+
def _image(
|
|
29
|
+
self,
|
|
30
|
+
img_source,
|
|
31
|
+
high_density_vertical: bool = True,
|
|
32
|
+
high_density_horizontal: bool = True,
|
|
33
|
+
impl: str = "bitImageRaster",
|
|
34
|
+
fragment_height: int = 960,
|
|
35
|
+
center: bool = False,
|
|
36
|
+
cmd_index_offset: int = 0,
|
|
37
|
+
) -> Optional[int]:
|
|
38
|
+
"""Print an image.
|
|
39
|
+
|
|
40
|
+
You can select whether the printer should print in high density or not. The default value is high density.
|
|
41
|
+
When printing in low density, the image will be stretched.
|
|
42
|
+
|
|
43
|
+
Esc/Pos supplies several commands for printing. This function supports three of them. Please try to vary the
|
|
44
|
+
implementations if you have any problems. For example the printer `IT80-002` will have trouble aligning
|
|
45
|
+
images that are not printed in Column-mode.
|
|
46
|
+
|
|
47
|
+
The available printing implementations are:
|
|
48
|
+
|
|
49
|
+
* `bitImageRaster`: prints with the `GS v 0`-command
|
|
50
|
+
* `graphics`: prints with the `GS ( L`-command
|
|
51
|
+
* `bitImageColumn`: prints with the `ESC *`-command
|
|
52
|
+
|
|
53
|
+
When trying to center an image make sure you have initialized the printer with a valid profile, that
|
|
54
|
+
contains a media width pixel field. Otherwise the centering will have no effect.
|
|
55
|
+
|
|
56
|
+
:param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp`
|
|
57
|
+
:param high_density_vertical: print in high density in vertical direction *default:* True
|
|
58
|
+
:param high_density_horizontal: print in high density in horizontal direction *default:* True
|
|
59
|
+
:param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn`
|
|
60
|
+
:param fragment_height: Images larger than this will be split into multiple fragments *default:* 960
|
|
61
|
+
:param center: Center image horizontally *default:* False
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
im = EscposImage(img_source)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if self.profile.profile_data["media"]["width"]["pixels"] == "Unknown":
|
|
68
|
+
logging.debug(
|
|
69
|
+
"The media.width.pixel field of the printer profile is not set. "
|
|
70
|
+
+ "The center flag will have no effect."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
max_width = int(self.profile.profile_data["media"]["width"]["pixels"])
|
|
74
|
+
|
|
75
|
+
if im.width > max_width:
|
|
76
|
+
raise ImageWidthError(f"{im.width} > {max_width}")
|
|
77
|
+
|
|
78
|
+
if center:
|
|
79
|
+
im.center(max_width)
|
|
80
|
+
except KeyError:
|
|
81
|
+
# If the printer's pixel width is not known, print anyways...
|
|
82
|
+
pass
|
|
83
|
+
except ValueError:
|
|
84
|
+
# If the max_width cannot be converted to an int, print anyways...
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
if im.height > fragment_height:
|
|
88
|
+
fragments = im.split(fragment_height)
|
|
89
|
+
cmd_index_offset = 0
|
|
90
|
+
for fragment in fragments:
|
|
91
|
+
cmd_index_offset = self.image(
|
|
92
|
+
fragment,
|
|
93
|
+
high_density_vertical=high_density_vertical,
|
|
94
|
+
high_density_horizontal=high_density_horizontal,
|
|
95
|
+
impl=impl,
|
|
96
|
+
fragment_height=fragment_height,
|
|
97
|
+
cmd_index_offset=cmd_index_offset,
|
|
98
|
+
)
|
|
99
|
+
# XXX dummy printer doesn't have '_sleep_in_fragment' method
|
|
100
|
+
# self._sleep_in_fragment()
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
if impl == "bitImageRaster":
|
|
104
|
+
# GS v 0, raster format bit image
|
|
105
|
+
density_byte = (0 if high_density_horizontal else 1) + (
|
|
106
|
+
0 if high_density_vertical else 2
|
|
107
|
+
)
|
|
108
|
+
header = (
|
|
109
|
+
GS
|
|
110
|
+
+ b"v0"
|
|
111
|
+
+ bytes((density_byte,))
|
|
112
|
+
+ self._int_low_high(im.width_bytes, 2)
|
|
113
|
+
+ self._int_low_high(im.height, 2)
|
|
114
|
+
)
|
|
115
|
+
self._raw(header + im.to_raster_format())
|
|
116
|
+
|
|
117
|
+
if impl == "graphics":
|
|
118
|
+
# GS ( L raster format graphics
|
|
119
|
+
img_header = self._int_low_high(im.width, 2) + self._int_low_high(
|
|
120
|
+
im.height, 2
|
|
121
|
+
)
|
|
122
|
+
tone = b"0"
|
|
123
|
+
colors = b"1"
|
|
124
|
+
ym = b"\x01" if high_density_vertical else b"\x02"
|
|
125
|
+
xm = b"\x01" if high_density_horizontal else b"\x02"
|
|
126
|
+
header = tone + xm + ym + colors + img_header
|
|
127
|
+
raster_data = im.to_raster_format()
|
|
128
|
+
self._image_send_graphics_data(b"0", b"p", header + raster_data)
|
|
129
|
+
self._image_send_graphics_data(b"0", b"2", b"")
|
|
130
|
+
|
|
131
|
+
if impl == "bitImageColumn":
|
|
132
|
+
# ESC *, column format bit image
|
|
133
|
+
density_byte = (1 if high_density_horizontal else 0) + (
|
|
134
|
+
32 if high_density_vertical else 0
|
|
135
|
+
)
|
|
136
|
+
header = (
|
|
137
|
+
ESC
|
|
138
|
+
+ b"*"
|
|
139
|
+
+ six.int2byte(density_byte)
|
|
140
|
+
+ self._int_low_high(im.width, 2)
|
|
141
|
+
)
|
|
142
|
+
self._raw(Cmd(cmd_index_offset, ESC + b"3" + six.int2byte(16)))
|
|
143
|
+
for i, blob in enumerate(im.to_column_format(high_density_vertical)):
|
|
144
|
+
cmd_index = i + 1 + cmd_index_offset
|
|
145
|
+
self._raw(Cmd(cmd_index, header + blob + b"\n"))
|
|
146
|
+
self._raw(ESC + b"2")
|
|
147
|
+
return cmd_index_offset + i + 1
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
Escpos.image = _image
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if 1:
|
|
155
|
+
|
|
156
|
+
# Usb printer: reduce likelihood for timeout gibberish
|
|
157
|
+
|
|
158
|
+
_Usb_raw_original = UsbPrinter._raw
|
|
159
|
+
|
|
160
|
+
def _Usb_raw(self, *args, **kwargs) -> None:
|
|
161
|
+
_Usb_raw_original(self, *args, **kwargs)
|
|
162
|
+
# NOTE Patch to reduce likelihood for timeout gibberish
|
|
163
|
+
while self._read():
|
|
164
|
+
time.sleep(0.01)
|
|
165
|
+
|
|
166
|
+
UsbPrinter._raw = _Usb_raw
|