tinyprint 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → tinyprint-0.2.0}/PKG-INFO +4 -2
- {tinyprint-0.1.0 → tinyprint-0.2.0}/pyproject.toml +2 -1
- tinyprint-0.2.0/tests/test_jobconfig.py +12 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint/__init__.py +3 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint/__main__.py +15 -6
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint/job.py +17 -2
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint/jobconfig.py +6 -3
- tinyprint-0.2.0/tinyprint/patches.py +28 -0
- tinyprint-0.2.0/tinyprint/preprocessor.py +231 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint.egg-info/PKG-INFO +4 -2
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint.egg-info/SOURCES.txt +1 -1
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint.egg-info/requires.txt +1 -0
- tinyprint-0.1.0/tests/test_jobconfig.py +0 -11
- tinyprint-0.1.0/tests/test_markdown_preprocessor.py +0 -27
- tinyprint-0.1.0/tinyprint/preprocessor.py +0 -132
- {tinyprint-0.1.0 → tinyprint-0.2.0}/LICENSE +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/README.md +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/setup.cfg +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tests/test_job.py +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint/config.py +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint/xescpos.py +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint.egg-info/dependency_links.txt +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint.egg-info/entry_points.txt +0 -0
- {tinyprint-0.1.0 → tinyprint-0.2.0}/tinyprint.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyprint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: ui & cli to print zines with esc/pos printer
|
|
5
5
|
Author-email: Levin Eric Zimmermann <levin.eric.zimmermann@posteo.eu>
|
|
6
6
|
License: GPL
|
|
@@ -10,11 +10,13 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Dist: python-escpos>=3.1
|
|
11
11
|
Requires-Dist: dataclasses-json>=0.6.0
|
|
12
12
|
Requires-Dist: marko>=2.1.2
|
|
13
|
+
Requires-Dist: pdf2image>=1.17.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: pytest; extra == "dev"
|
|
15
16
|
Requires-Dist: mypy; extra == "dev"
|
|
16
17
|
Requires-Dist: types-six; extra == "dev"
|
|
17
18
|
Requires-Dist: types-pillow; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
18
20
|
|
|
19
21
|
# tinyprint
|
|
20
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tinyprint"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{name = "Levin Eric Zimmermann", email = "levin.eric.zimmermann@posteo.eu"},
|
|
6
6
|
]
|
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
|
11
11
|
'python-escpos>=3.1',
|
|
12
12
|
'dataclasses-json>=0.6.0',
|
|
13
13
|
'marko>=2.1.2',
|
|
14
|
+
'pdf2image>=1.17.0',
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from tinyprint.jobconfig import JobConfig, PageSize
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_io(tmpdir):
|
|
5
|
+
# json_path = f"{tmpdir}/test.json"
|
|
6
|
+
# config = JobConfig(
|
|
7
|
+
# [("myfile.pdf", None)], "myprinter.yml", page_size=PageSize(70, 30)
|
|
8
|
+
# )
|
|
9
|
+
# config.to_file(json_path)
|
|
10
|
+
# c2 = JobConfig.from_file(json_path)
|
|
11
|
+
# assert c2 == config
|
|
12
|
+
pass
|
|
@@ -17,7 +17,7 @@ from tkinter.filedialog import (
|
|
|
17
17
|
asksaveasfilename,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from tinyprint.jobconfig import JobConfig
|
|
20
|
+
from tinyprint.jobconfig import JobConfig, Resource
|
|
21
21
|
from tinyprint.job import Job
|
|
22
22
|
|
|
23
23
|
|
|
@@ -35,7 +35,10 @@ def _main(tempdir: str):
|
|
|
35
35
|
frm = ttk.Frame(root, padding=15)
|
|
36
36
|
frm.grid()
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
try: # support macos
|
|
39
|
+
_no_hidden_files(root)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
39
42
|
|
|
40
43
|
jcm = JobConfigManager(
|
|
41
44
|
JobConfigFields(frm), tempdir, filename_indicator=tk.StringVar(value="")
|
|
@@ -123,7 +126,7 @@ class JobConfigFields:
|
|
|
123
126
|
|
|
124
127
|
def update(self, job_config: JobConfig):
|
|
125
128
|
resource_list = "; ".join(
|
|
126
|
-
[r
|
|
129
|
+
[r.path.split(os.sep)[-1] for r in job_config.resource_list]
|
|
127
130
|
)
|
|
128
131
|
if len(resource_list) > 25:
|
|
129
132
|
resource_list = resource_list[:30] + " ..."
|
|
@@ -151,7 +154,7 @@ class JobConfigManager:
|
|
|
151
154
|
self.new(f"{self.tempdir}/tmp-print-job.json")
|
|
152
155
|
|
|
153
156
|
def new(self, *args, **kwargs):
|
|
154
|
-
self.job_config = JobConfig([(self.temp_resource
|
|
157
|
+
self.job_config = JobConfig([Resource(self.temp_resource)])
|
|
155
158
|
self.save_as(*args, **kwargs)
|
|
156
159
|
self.updatefields()
|
|
157
160
|
|
|
@@ -205,7 +208,7 @@ class JobConfigManager:
|
|
|
205
208
|
if not file_tuple:
|
|
206
209
|
return
|
|
207
210
|
|
|
208
|
-
resource_list = [(p
|
|
211
|
+
resource_list = [Resource(p) for p in file_tuple]
|
|
209
212
|
|
|
210
213
|
self.job_config.resource_list = resource_list
|
|
211
214
|
self.updatefields()
|
|
@@ -216,7 +219,9 @@ class JobConfigManager:
|
|
|
216
219
|
("all files", "*.*"),
|
|
217
220
|
)
|
|
218
221
|
filename = askopenfilename(
|
|
219
|
-
title="Open a file",
|
|
222
|
+
title="Open a file",
|
|
223
|
+
initialdir=initialdir(self.job_config.printer_config),
|
|
224
|
+
filetypes=filetypes,
|
|
220
225
|
)
|
|
221
226
|
if filename:
|
|
222
227
|
self.job_config.printer_config = filename
|
|
@@ -252,3 +257,7 @@ class JobManager:
|
|
|
252
257
|
if self.job:
|
|
253
258
|
self.job.stop()
|
|
254
259
|
self.job = None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def initialdir(var):
|
|
263
|
+
return (var and str(Path(var).parent)) or str(Path.home())
|
|
@@ -130,7 +130,7 @@ class Job:
|
|
|
130
130
|
page_list = []
|
|
131
131
|
for resource in self.config.resource_list:
|
|
132
132
|
preprocessor = self.resource_to_preprocessor(resource)
|
|
133
|
-
page_list.
|
|
133
|
+
page_list.extend(preprocessor(resource))
|
|
134
134
|
return tuple(page_list)
|
|
135
135
|
|
|
136
136
|
def _send_to_printer(self, cmd_list: list[bytes]):
|
|
@@ -202,7 +202,7 @@ class Job:
|
|
|
202
202
|
return logger
|
|
203
203
|
|
|
204
204
|
def resource_to_preprocessor(self, resource: Resource):
|
|
205
|
-
path = resource
|
|
205
|
+
path = resource.path
|
|
206
206
|
p = None
|
|
207
207
|
if _is_binary_file(path):
|
|
208
208
|
m = mimetypes.guess_type(path)
|
|
@@ -230,6 +230,13 @@ def patch_printer_profile(printer):
|
|
|
230
230
|
profile.img_y_scale_factor = 1
|
|
231
231
|
profile.sleep_time = 0
|
|
232
232
|
|
|
233
|
+
# How many characters fit on one line with normal font size.
|
|
234
|
+
# 42 is also default for TM-T88III.
|
|
235
|
+
profile.default_char_count = 42
|
|
236
|
+
# How many characters fit on one line with heading font size.
|
|
237
|
+
# 21 is also default for TM-T88III.
|
|
238
|
+
profile.big_char_count = 21
|
|
239
|
+
|
|
233
240
|
# Special treatment for specific printer models.
|
|
234
241
|
match profile.profile_data["name"]:
|
|
235
242
|
case "TM-T88III":
|
|
@@ -239,6 +246,8 @@ def patch_printer_profile(printer):
|
|
|
239
246
|
high_density_horizontal=True,
|
|
240
247
|
center=False,
|
|
241
248
|
)
|
|
249
|
+
profile.default_char_count = 42
|
|
250
|
+
profile.big_char_count = 21
|
|
242
251
|
profile.img_cut_index = 4
|
|
243
252
|
case "TM-U220B":
|
|
244
253
|
profile.img_kwargs = dict(
|
|
@@ -248,6 +257,12 @@ def patch_printer_profile(printer):
|
|
|
248
257
|
center=False,
|
|
249
258
|
fragment_height=32,
|
|
250
259
|
)
|
|
260
|
+
# NOTE Numbers need less space than letters in case of this
|
|
261
|
+
# printer (there is space for 40 numbers). Let's use the
|
|
262
|
+
# smaller number to make sure everything always fits in one
|
|
263
|
+
# line.
|
|
264
|
+
profile.default_char_count = 33
|
|
265
|
+
profile.big_char_count = 16
|
|
251
266
|
profile.img_cut_index = 9
|
|
252
267
|
# Profile is wrong, it's 200 dots, not 400
|
|
253
268
|
# https://files.support.epson.com/pdf/pos/bulk/tm-u220_trg_en_std_reve.pdf
|
|
@@ -48,9 +48,12 @@ class PageSize(DataClassJsonMixin):
|
|
|
48
48
|
raise NotImplementedError(f"can't parse {obj} to PageSize")
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
@dataclass
|
|
52
|
+
class Resource:
|
|
53
|
+
path: str
|
|
54
|
+
fragment_height: Optional[int] = None
|
|
55
|
+
# Either 0 (= black) or 1 (= red)
|
|
56
|
+
color: int = 0
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
@dataclass
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Monkey patches"""
|
|
2
|
+
|
|
3
|
+
from marko import block
|
|
4
|
+
|
|
5
|
+
# Allow multiple blank lines to create multiple blank lines
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BlankLine(block.BlockElement):
|
|
9
|
+
"""Blank lines"""
|
|
10
|
+
|
|
11
|
+
priority = 5
|
|
12
|
+
|
|
13
|
+
def __init__(self, start: int) -> None:
|
|
14
|
+
self._anchor = start
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def match(cls, source) -> bool:
|
|
18
|
+
line = source.next_line()
|
|
19
|
+
return line is not None and not line.strip()
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def parse(cls, source) -> int:
|
|
23
|
+
m = source.match
|
|
24
|
+
source.consume()
|
|
25
|
+
return m
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
block.BlankLine = BlankLine
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
6
|
+
from typing import Callable, TypeAlias, Optional
|
|
7
|
+
|
|
8
|
+
from escpos.escpos import Escpos # type: ignore
|
|
9
|
+
from escpos.constants import TXT_STYLE
|
|
10
|
+
import marko # type: ignore
|
|
11
|
+
from PIL import Image # type: ignore
|
|
12
|
+
from pdf2image import convert_from_path # type: ignore
|
|
13
|
+
|
|
14
|
+
from tinyprint.jobconfig import Resource, Orientation
|
|
15
|
+
from tinyprint.job import Job
|
|
16
|
+
|
|
17
|
+
Page: TypeAlias = Callable[[Escpos], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Preprocessor(abc.ABC):
|
|
21
|
+
def __init__(self, job: Job):
|
|
22
|
+
self.job = job
|
|
23
|
+
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
def __call__(self, resource: Resource) -> tuple[Page, ...]: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ImagePreprocessor(Preprocessor):
|
|
29
|
+
def __call__(self, resource: Resource):
|
|
30
|
+
return (get_image_printer(resource, self.job),)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_image_printer(resource, job):
|
|
34
|
+
path, fragment_height = resource.path, resource.fragment_height
|
|
35
|
+
profile = job.printer.profile
|
|
36
|
+
|
|
37
|
+
# Immediately load & process image - in case image path is volatile
|
|
38
|
+
# (as it's the case if PdfPreprocessor parses us an img path), we
|
|
39
|
+
# don't loose the image. The downside of this is that we need to keep
|
|
40
|
+
# the image in our RAM. But because we resize, and the resolution of
|
|
41
|
+
# the ESC/POS printer is usually quite low, this is acceptable and
|
|
42
|
+
# should only create problem in case of very large books.
|
|
43
|
+
with Image.open(path) as im:
|
|
44
|
+
if job.config.orientation != Orientation.HORIZONTAL:
|
|
45
|
+
im = im.rotate(angle=-90, expand=True)
|
|
46
|
+
max_width = profile.profile_data["media"]["width"]["pixels"]
|
|
47
|
+
assert max_width != "Unknown", "max_width of printer unknown!"
|
|
48
|
+
max_width = int(max_width * profile.img_scale_factor)
|
|
49
|
+
width = im.width
|
|
50
|
+
heigth = im.height
|
|
51
|
+
factor = max_width / width
|
|
52
|
+
final_im = im.resize(
|
|
53
|
+
(
|
|
54
|
+
int(width * factor * profile.img_x_scale_factor),
|
|
55
|
+
int(heigth * factor * profile.img_y_scale_factor),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
color = ("black", "red")[resource.color]
|
|
60
|
+
|
|
61
|
+
def _(printer):
|
|
62
|
+
printer._raw(TXT_STYLE["color"][color])
|
|
63
|
+
try:
|
|
64
|
+
# Add extra space between to make spaces between cuts even
|
|
65
|
+
# XXX Does number '10' need to change depending on printer?
|
|
66
|
+
if job.config.cut:
|
|
67
|
+
printer.line_spacing(10)
|
|
68
|
+
printer.ln()
|
|
69
|
+
printer.line_spacing()
|
|
70
|
+
|
|
71
|
+
img_kwargs = dict(job.printer.profile.img_kwargs)
|
|
72
|
+
if fragment_height:
|
|
73
|
+
img_kwargs["fragment_height"] = fragment_height
|
|
74
|
+
|
|
75
|
+
printer.image(
|
|
76
|
+
final_im,
|
|
77
|
+
**img_kwargs,
|
|
78
|
+
)
|
|
79
|
+
finally:
|
|
80
|
+
final_im.close()
|
|
81
|
+
|
|
82
|
+
return _
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PdfPreprocessor(ImagePreprocessor):
|
|
86
|
+
def __call__(self, resource: Resource):
|
|
87
|
+
pdf_path = resource.path
|
|
88
|
+
with TemporaryDirectory() as tmp_path:
|
|
89
|
+
image_list = convert_from_path(pdf_path, output_folder=tmp_path, dpi=600)
|
|
90
|
+
if self.job.config.orientation == Orientation.HORIZONTAL:
|
|
91
|
+
image_list = [im.rotate(angle=-90, expand=True) for im in image_list]
|
|
92
|
+
page_list = []
|
|
93
|
+
for i, img in enumerate(image_list):
|
|
94
|
+
p = f"{tmp_path}/{i}.png"
|
|
95
|
+
img.save(p)
|
|
96
|
+
page_list.extend(super().__call__(Resource(p, 0)))
|
|
97
|
+
return tuple(page_list)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MarkdownPreprocessor(Preprocessor):
|
|
101
|
+
def __init__(self, *args, **kwargs):
|
|
102
|
+
super().__init__(*args, **kwargs)
|
|
103
|
+
self.markdown = marko.Markdown(renderer=_EscposRenderer)
|
|
104
|
+
self.markdown._setup_extensions()
|
|
105
|
+
|
|
106
|
+
def __call__(self, resource: Resource):
|
|
107
|
+
with open(resource.path, "r") as f:
|
|
108
|
+
md = f.read()
|
|
109
|
+
|
|
110
|
+
def _(printer):
|
|
111
|
+
self.markdown.renderer.set_printer(printer)
|
|
112
|
+
self.markdown.renderer.set_job(self.job)
|
|
113
|
+
self.markdown(md)
|
|
114
|
+
|
|
115
|
+
return (_,)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class _EscposRenderer(marko.renderer.Renderer):
|
|
119
|
+
# Ref: https://github.com/frostming/marko/blob/master/marko/html_renderer.py
|
|
120
|
+
|
|
121
|
+
# TODO Add auto page breaks
|
|
122
|
+
|
|
123
|
+
def render_raw_text(self, element) -> str:
|
|
124
|
+
self._printer.text(element.children)
|
|
125
|
+
# XXX: Temporarily disable auto-line break until it can be configured in UI
|
|
126
|
+
# word_list = element.children.split(" ")
|
|
127
|
+
# max_char_count = self.max_char_count
|
|
128
|
+
# for word in word_list:
|
|
129
|
+
# char_count = len(word)
|
|
130
|
+
# # If word is too big for one line, just skip line breaking.
|
|
131
|
+
# if char_count > max_char_count:
|
|
132
|
+
# self._printer.text(word)
|
|
133
|
+
# self.line_position = self.line_position % max_char_count
|
|
134
|
+
# continue
|
|
135
|
+
# add_whitespace = self.line_position > 0
|
|
136
|
+
# new_pos = self.line_position + char_count + add_whitespace
|
|
137
|
+
# diff = new_pos - max_char_count
|
|
138
|
+
# if diff <= 0:
|
|
139
|
+
# if add_whitespace:
|
|
140
|
+
# self._printer.text(" ")
|
|
141
|
+
# self._printer.text(word)
|
|
142
|
+
# self.line_position = new_pos
|
|
143
|
+
# else: # diff > 0
|
|
144
|
+
# space_count = max_char_count - self.line_position
|
|
145
|
+
# if space_count:
|
|
146
|
+
# self._printer.text(" " * space_count)
|
|
147
|
+
# self._printer.text(word)
|
|
148
|
+
# self.line_position = char_count
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
def render_emphasis(self, element) -> str:
|
|
152
|
+
self._set(bold=True)
|
|
153
|
+
self.render_children(element)
|
|
154
|
+
self._set(bold=False)
|
|
155
|
+
return ""
|
|
156
|
+
|
|
157
|
+
def render_image(self, element) -> str:
|
|
158
|
+
resource = Resource(path=element.dest)
|
|
159
|
+
get_image_printer(resource, self._job)(self._printer)
|
|
160
|
+
return ""
|
|
161
|
+
|
|
162
|
+
def render_strong_emphasis(self, element) -> str:
|
|
163
|
+
return self.render_emphasis(element)
|
|
164
|
+
|
|
165
|
+
def render_blank_line(self, element=None):
|
|
166
|
+
self.render_line_break(line_count=1)
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
def render_line_break(self, element=None, line_count: Optional[int] = None) -> str:
|
|
170
|
+
soft = element.soft if element else True
|
|
171
|
+
if line_count is None:
|
|
172
|
+
if soft:
|
|
173
|
+
line_count = 1
|
|
174
|
+
else:
|
|
175
|
+
line_count = 2
|
|
176
|
+
self._printer.ln(line_count)
|
|
177
|
+
self.line_position = 0
|
|
178
|
+
return ""
|
|
179
|
+
|
|
180
|
+
def render_paragraph(self, element) -> str:
|
|
181
|
+
self.render_children(element)
|
|
182
|
+
self.render_line_break()
|
|
183
|
+
return ""
|
|
184
|
+
|
|
185
|
+
def render_heading(self, element) -> str:
|
|
186
|
+
self.max_char_count = self.printer_profile.big_char_count
|
|
187
|
+
self._set(double_height=True, double_width=True)
|
|
188
|
+
self.render_children(element)
|
|
189
|
+
self._set(double_height=False, double_width=False, normal_textsize=True)
|
|
190
|
+
self.render_line_break(line_count=3)
|
|
191
|
+
self.max_char_count = self.printer_profile.default_char_count
|
|
192
|
+
return ""
|
|
193
|
+
|
|
194
|
+
def set_printer(self, printer):
|
|
195
|
+
self._printer = printer
|
|
196
|
+
|
|
197
|
+
def set_job(self, job):
|
|
198
|
+
self._job = job
|
|
199
|
+
|
|
200
|
+
def _set(self, *args, **kwargs):
|
|
201
|
+
if self._printer:
|
|
202
|
+
self._printer.set(*args, **kwargs)
|
|
203
|
+
else:
|
|
204
|
+
raise RuntimeError("printer not yet set")
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def line_position(self):
|
|
208
|
+
try:
|
|
209
|
+
return self._line_position
|
|
210
|
+
except AttributeError:
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
@line_position.setter
|
|
214
|
+
def line_position(self, line_position: int):
|
|
215
|
+
self._line_position = line_position
|
|
216
|
+
|
|
217
|
+
@cached_property
|
|
218
|
+
def printer_profile(self):
|
|
219
|
+
return self._job.printer.profile
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def max_char_count(self):
|
|
223
|
+
try:
|
|
224
|
+
return self._max_char_count
|
|
225
|
+
except AttributeError:
|
|
226
|
+
self.max_char_count = self.printer_profile.default_char_count
|
|
227
|
+
return self.max_char_count
|
|
228
|
+
|
|
229
|
+
@max_char_count.setter
|
|
230
|
+
def max_char_count(self, max_char_count: int):
|
|
231
|
+
self._max_char_count = max_char_count
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyprint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: ui & cli to print zines with esc/pos printer
|
|
5
5
|
Author-email: Levin Eric Zimmermann <levin.eric.zimmermann@posteo.eu>
|
|
6
6
|
License: GPL
|
|
@@ -10,11 +10,13 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Dist: python-escpos>=3.1
|
|
11
11
|
Requires-Dist: dataclasses-json>=0.6.0
|
|
12
12
|
Requires-Dist: marko>=2.1.2
|
|
13
|
+
Requires-Dist: pdf2image>=1.17.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: pytest; extra == "dev"
|
|
15
16
|
Requires-Dist: mypy; extra == "dev"
|
|
16
17
|
Requires-Dist: types-six; extra == "dev"
|
|
17
18
|
Requires-Dist: types-pillow; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
18
20
|
|
|
19
21
|
# tinyprint
|
|
20
22
|
|
|
@@ -3,12 +3,12 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
tests/test_job.py
|
|
5
5
|
tests/test_jobconfig.py
|
|
6
|
-
tests/test_markdown_preprocessor.py
|
|
7
6
|
tinyprint/__init__.py
|
|
8
7
|
tinyprint/__main__.py
|
|
9
8
|
tinyprint/config.py
|
|
10
9
|
tinyprint/job.py
|
|
11
10
|
tinyprint/jobconfig.py
|
|
11
|
+
tinyprint/patches.py
|
|
12
12
|
tinyprint/preprocessor.py
|
|
13
13
|
tinyprint/xescpos.py
|
|
14
14
|
tinyprint.egg-info/PKG-INFO
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from tinyprint.jobconfig import JobConfig, PageSize
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_io(tmpdir):
|
|
5
|
-
json_path = f"{tmpdir}/test.json"
|
|
6
|
-
config = JobConfig(
|
|
7
|
-
[("myfile.pdf", None)], "myprinter.yml", page_size=PageSize(70, 30)
|
|
8
|
-
)
|
|
9
|
-
config.to_file(json_path)
|
|
10
|
-
c2 = JobConfig.from_file(json_path)
|
|
11
|
-
assert c2 == config
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from tinyprint.job import Job
|
|
2
|
-
from tinyprint.jobconfig import JobConfig
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_markdown_preprocessor(tmpdir):
|
|
6
|
-
md_file_path = f"{tmpdir}/test.md"
|
|
7
|
-
with open(md_file_path, "w") as f:
|
|
8
|
-
f.write(md)
|
|
9
|
-
|
|
10
|
-
config = JobConfig([[md_file_path, 0]])
|
|
11
|
-
job = Job(config)
|
|
12
|
-
page_tuple = job._generate_page_tuple()
|
|
13
|
-
for p in page_tuple:
|
|
14
|
-
p(job.printer)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
md = r"""
|
|
18
|
-
|
|
19
|
-
# hello world
|
|
20
|
-
|
|
21
|
-
this is a test.
|
|
22
|
-
|
|
23
|
-
this is still a *test*.
|
|
24
|
-
|
|
25
|
-
[abc](https://codeberg.org)
|
|
26
|
-
|
|
27
|
-
"""
|
|
@@ -1,132 +0,0 @@
|
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|