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.
@@ -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