tinyprint 0.2.1__tar.gz → 0.2.3__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.2.1 → tinyprint-0.2.3}/PKG-INFO +29 -1
- tinyprint-0.2.3/README.md +31 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/pyproject.toml +2 -2
- tinyprint-0.2.3/tests/test_gui.py +312 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tests/test_integration.py +2 -2
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/gui.py +4 -1
- tinyprint-0.2.3/tinyprint/patches/__init__.py +5 -0
- tinyprint-0.2.3/tinyprint/patches/escpos.py +172 -0
- tinyprint-0.2.3/tinyprint/patches/mako.py +26 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/preprocessor.py +17 -1
- tinyprint-0.2.3/tinyprint/printer/__init__.py +2 -0
- tinyprint-0.2.3/tinyprint/printer/manager.py +137 -0
- tinyprint-0.2.3/tinyprint/printer/profile.py +91 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint.egg-info/PKG-INFO +29 -1
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint.egg-info/SOURCES.txt +8 -1
- tinyprint-0.2.1/README.md +0 -3
- {tinyprint-0.2.1 → tinyprint-0.2.3}/LICENSE +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/setup.cfg +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tests/test_job.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tests/test_jobconfig.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/__init__.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/__main__.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/config.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/gui_misc.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/job.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint/jobconfig.py +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint.egg-info/dependency_links.txt +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint.egg-info/entry_points.txt +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint.egg-info/requires.txt +0 -0
- {tinyprint-0.2.1 → tinyprint-0.2.3}/tinyprint.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyprint
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: ui & cli to print zines with esc/pos printer
|
|
5
5
|
Author-email: Levin Eric Zimmermann <levin.eric.zimmermann@posteo.com>
|
|
6
6
|
License: GPL
|
|
@@ -23,3 +23,31 @@ Dynamic: license-file
|
|
|
23
23
|
# tinyprint
|
|
24
24
|
|
|
25
25
|
Software for printing tiny zines with ESC/POS printer.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### MacOS
|
|
32
|
+
|
|
33
|
+
Open a console and type:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)
|
|
37
|
+
brew install python
|
|
38
|
+
brew install python-tk
|
|
39
|
+
brew install pipx
|
|
40
|
+
pipx install tinyprint
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Linux
|
|
44
|
+
|
|
45
|
+
Install python, and then just use pip:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip3 install tinyprint
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Windows
|
|
52
|
+
|
|
53
|
+
I don't know, but if someone manages, please let us know.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# tinyprint
|
|
2
|
+
|
|
3
|
+
Software for printing tiny zines with ESC/POS printer.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### MacOS
|
|
10
|
+
|
|
11
|
+
Open a console and type:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)
|
|
15
|
+
brew install python
|
|
16
|
+
brew install python-tk
|
|
17
|
+
brew install pipx
|
|
18
|
+
pipx install tinyprint
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Linux
|
|
22
|
+
|
|
23
|
+
Install python, and then just use pip:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip3 install tinyprint
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Windows
|
|
30
|
+
|
|
31
|
+
I don't know, but if someone manages, please let us know.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tinyprint"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
authors = [
|
|
5
5
|
{name = "Levin Eric Zimmermann", email = "levin.eric.zimmermann@posteo.com"},
|
|
6
6
|
]
|
|
@@ -28,7 +28,7 @@ dev = [
|
|
|
28
28
|
text = "GPL"
|
|
29
29
|
|
|
30
30
|
[tool.setuptools]
|
|
31
|
-
packages = ["tinyprint"]
|
|
31
|
+
packages = ["tinyprint", "tinyprint.patches", "tinyprint.printer"]
|
|
32
32
|
|
|
33
33
|
[project.scripts]
|
|
34
34
|
tinyprint = "tinyprint.__main__:main"
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Tests for the TinyPrint GUI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pytest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
import tempfile
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
# Import the GUI components
|
|
11
|
+
from tinyprint.gui import GUI, JobConfigManager
|
|
12
|
+
from tinyprint.jobconfig import JobConfig, Resource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def temp_dir():
|
|
17
|
+
"""Create and cleanup a temporary directory for test files."""
|
|
18
|
+
temp_dir = tempfile.mkdtemp(prefix="tinyprint-test-")
|
|
19
|
+
yield temp_dir
|
|
20
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def gui_app(temp_dir):
|
|
25
|
+
"""Create a GUI instance for testing with a temporary directory."""
|
|
26
|
+
with GUI() as gui:
|
|
27
|
+
yield gui
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def job_controller(gui_app):
|
|
32
|
+
"""Return the JobController instance from the GUI."""
|
|
33
|
+
return gui_app.blk.job_controller
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_print_button_starts_job(job_controller):
|
|
37
|
+
"""Test that clicking the print button starts a print job."""
|
|
38
|
+
# Setup - mock the Job class
|
|
39
|
+
mock_job = MagicMock()
|
|
40
|
+
mock_job.is_running = False
|
|
41
|
+
|
|
42
|
+
with patch("tinyprint.gui.Job", return_value=mock_job) as mock_job_class:
|
|
43
|
+
# Simulate clicking the print button
|
|
44
|
+
job_controller.btn.print.invoke()
|
|
45
|
+
|
|
46
|
+
# Verify a job was created with the correct config
|
|
47
|
+
mock_job_class.assert_called_once()
|
|
48
|
+
assert isinstance(mock_job_class.call_args[0][0], JobConfig)
|
|
49
|
+
|
|
50
|
+
# Verify the job was started
|
|
51
|
+
mock_job.start.assert_called_once()
|
|
52
|
+
assert job_controller.job == mock_job
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_stop_button_stops_job(job_controller):
|
|
56
|
+
"""Test that clicking the stop button stops a running print job."""
|
|
57
|
+
# Setup - create a mock job
|
|
58
|
+
mock_job = MagicMock()
|
|
59
|
+
mock_job.is_running = True
|
|
60
|
+
job_controller.job = mock_job
|
|
61
|
+
|
|
62
|
+
# Simulate clicking the stop button
|
|
63
|
+
job_controller.btn.stop.invoke()
|
|
64
|
+
|
|
65
|
+
# Verify the job was stopped and cleaned up
|
|
66
|
+
mock_job.stop.assert_called_once()
|
|
67
|
+
mock_job.close.assert_called_once()
|
|
68
|
+
assert job_controller.job is None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def job_config_manager(gui_app):
|
|
73
|
+
"""Create a JobConfigManager instance for testing."""
|
|
74
|
+
gui_app.blk = MagicMock()
|
|
75
|
+
gui_app.blk.job_config_editor = MagicMock()
|
|
76
|
+
gui_app.blk.job_config_editor.sync_from_model = MagicMock()
|
|
77
|
+
gui_app.tempdir = tempfile.mkdtemp()
|
|
78
|
+
return JobConfigManager(gui_app)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_job_config_manager_initialization(job_config_manager):
|
|
82
|
+
"""Test that JobConfigManager initializes correctly."""
|
|
83
|
+
assert job_config_manager.job_config is not None
|
|
84
|
+
assert isinstance(job_config_manager.job_config, JobConfig)
|
|
85
|
+
assert len(job_config_manager.job_config.resource_list) == 1
|
|
86
|
+
assert "tmp-resource.md" in job_config_manager.job_config.resource_list[0].path
|
|
87
|
+
assert job_config_manager.filename is not None
|
|
88
|
+
assert job_config_manager.filename.endswith("tmp-print-job.json")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_job_config_manager_new_config(job_config_manager, tmp_path):
|
|
92
|
+
"""Test creating a new configuration."""
|
|
93
|
+
test_file = tmp_path / "test_config.json"
|
|
94
|
+
|
|
95
|
+
# Set up the save_as dialog to return our test file
|
|
96
|
+
with patch(
|
|
97
|
+
"tkinter.filedialog.asksaveasfilename", return_value=str(test_file)
|
|
98
|
+
) as mock_save_as:
|
|
99
|
+
# Simulate clicking the new button
|
|
100
|
+
job_config_manager.btn.new.invoke()
|
|
101
|
+
|
|
102
|
+
assert job_config_manager.filename == str(test_file)
|
|
103
|
+
assert test_file.exists()
|
|
104
|
+
|
|
105
|
+
# Verify the file contains valid JSON with expected structure
|
|
106
|
+
with open(test_file, "r") as f:
|
|
107
|
+
config_data = json.load(f)
|
|
108
|
+
assert "resource_list" in config_data
|
|
109
|
+
assert isinstance(config_data["resource_list"], list)
|
|
110
|
+
assert len(config_data["resource_list"]) == 1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@patch("tkinter.filedialog.asksaveasfilename")
|
|
114
|
+
def test_job_config_manager_save_as(mock_save_as, job_config_manager, tmp_path):
|
|
115
|
+
"""Test saving configuration to a new file."""
|
|
116
|
+
test_file = tmp_path / "test_save_as.json"
|
|
117
|
+
mock_save_as.return_value = str(test_file)
|
|
118
|
+
|
|
119
|
+
# Simulate clicking the save_as button
|
|
120
|
+
job_config_manager.btn.save_as.invoke()
|
|
121
|
+
|
|
122
|
+
mock_save_as.assert_called_once()
|
|
123
|
+
assert job_config_manager.filename == str(test_file)
|
|
124
|
+
assert test_file.exists()
|
|
125
|
+
# Verify the filename label is updated
|
|
126
|
+
assert (
|
|
127
|
+
job_config_manager.lbl.filename["textvariable"]
|
|
128
|
+
== job_config_manager.var.filename_indicator._name
|
|
129
|
+
)
|
|
130
|
+
assert job_config_manager.var.filename_indicator.get() == "test_save_as.json"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@patch("tkinter.filedialog.askopenfilename")
|
|
134
|
+
def test_job_config_manager_open_config(mock_open_file, job_config_manager, tmp_path):
|
|
135
|
+
"""Test opening an existing configuration file."""
|
|
136
|
+
# Create a test config file
|
|
137
|
+
test_file = tmp_path / "test_open.json"
|
|
138
|
+
test_config = {
|
|
139
|
+
"resource_list": [{"path": "test.txt"}],
|
|
140
|
+
"printer_config": None,
|
|
141
|
+
"copy_count": 1,
|
|
142
|
+
"cut": True,
|
|
143
|
+
"orientation": 0, # 0 = Orientation.HORIZONTAL
|
|
144
|
+
"page_size": None,
|
|
145
|
+
"sleep_time": 0.5,
|
|
146
|
+
}
|
|
147
|
+
with open(test_file, "w") as f:
|
|
148
|
+
json.dump(test_config, f)
|
|
149
|
+
|
|
150
|
+
mock_open_file.return_value = str(test_file)
|
|
151
|
+
|
|
152
|
+
# Simulate clicking the open button
|
|
153
|
+
job_config_manager.btn.open.invoke()
|
|
154
|
+
|
|
155
|
+
mock_open_file.assert_called_once()
|
|
156
|
+
assert job_config_manager.filename == str(test_file)
|
|
157
|
+
assert job_config_manager.job_config is not None
|
|
158
|
+
assert len(job_config_manager.job_config.resource_list) == 1
|
|
159
|
+
assert job_config_manager.job_config.resource_list[0].path == "test.txt"
|
|
160
|
+
assert job_config_manager.job_config.sleep_time == 0.5
|
|
161
|
+
# Verify the filename label is updated
|
|
162
|
+
assert (
|
|
163
|
+
job_config_manager.lbl.filename["textvariable"]
|
|
164
|
+
== job_config_manager.var.filename_indicator._name
|
|
165
|
+
)
|
|
166
|
+
assert job_config_manager.var.filename_indicator.get() == "test_open.json"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_job_config_manager_reload_config(job_config_manager, tmp_path):
|
|
170
|
+
"""Test reloading the current configuration from disk."""
|
|
171
|
+
# Create a test config file
|
|
172
|
+
test_file = tmp_path / "test_reload.json"
|
|
173
|
+
test_config = {
|
|
174
|
+
"resource_list": [{"path": "reload.txt"}],
|
|
175
|
+
"orientation": 0, # 0 = Orientation.HORIZONTAL
|
|
176
|
+
"sleep_time": 0.3,
|
|
177
|
+
}
|
|
178
|
+
with open(test_file, "w") as f:
|
|
179
|
+
json.dump(test_config, f)
|
|
180
|
+
|
|
181
|
+
# Set the filename and reload
|
|
182
|
+
job_config_manager.set_filename(str(test_file))
|
|
183
|
+
# Simulate clicking the reload button
|
|
184
|
+
job_config_manager.btn.reload.invoke()
|
|
185
|
+
|
|
186
|
+
# Verify the config was loaded correctly
|
|
187
|
+
assert job_config_manager.job_config is not None
|
|
188
|
+
assert len(job_config_manager.job_config.resource_list) == 1
|
|
189
|
+
assert job_config_manager.job_config.resource_list[0].path == "reload.txt"
|
|
190
|
+
assert job_config_manager.job_config.sleep_time == 0.3
|
|
191
|
+
|
|
192
|
+
# Verify the editor was synced
|
|
193
|
+
job_config_manager.gui.blk.job_config_editor.sync_from_model.assert_called_once()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_job_config_manager_set_filename(job_config_manager):
|
|
197
|
+
"""Test updating the filename and UI indicator."""
|
|
198
|
+
test_path = "/path/to/test_config.json"
|
|
199
|
+
job_config_manager.set_filename(test_path)
|
|
200
|
+
|
|
201
|
+
assert job_config_manager.filename == test_path
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_job_config_editor_initialization(gui_app):
|
|
205
|
+
"""Test that JobConfigEditor initializes correctly."""
|
|
206
|
+
editor = gui_app.blk.job_config_editor
|
|
207
|
+
assert editor is not None
|
|
208
|
+
# Verify all expected variables are created
|
|
209
|
+
assert hasattr(editor.var, "resource_list")
|
|
210
|
+
assert hasattr(editor.var, "printer_config")
|
|
211
|
+
assert hasattr(editor.var, "sleep_time")
|
|
212
|
+
# Verify widgets are created
|
|
213
|
+
assert hasattr(editor.lbl, "resource_list")
|
|
214
|
+
assert hasattr(editor.lbl, "printer_value")
|
|
215
|
+
assert hasattr(editor.lbl, "sleep_value")
|
|
216
|
+
assert hasattr(editor.btn, "select_resource")
|
|
217
|
+
assert hasattr(editor.btn, "select_printer")
|
|
218
|
+
assert hasattr(editor.cbx, "cut")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@patch("tkinter.filedialog.askopenfilenames")
|
|
222
|
+
def test_select_resource(mock_file_dialog, gui_app):
|
|
223
|
+
"""Test selecting resources updates the job config and UI."""
|
|
224
|
+
editor = gui_app.blk.job_config_editor
|
|
225
|
+
test_files = ["/path/to/file1.pdf", "/path/to/file2.jpg"]
|
|
226
|
+
mock_file_dialog.return_value = test_files
|
|
227
|
+
editor.btn.select_resource.invoke()
|
|
228
|
+
mock_file_dialog.assert_called_once()
|
|
229
|
+
assert len(editor.job_config.resource_list) == 2
|
|
230
|
+
assert editor.job_config.resource_list[0].path == test_files[0]
|
|
231
|
+
assert editor.job_config.resource_list[1].path == test_files[1]
|
|
232
|
+
assert test_files[0].split("/")[-1] in editor.var.resource_list.get()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@patch("tkinter.filedialog.askopenfilename")
|
|
236
|
+
def test_select_printer_config(mock_file_dialog, gui_app):
|
|
237
|
+
"""Test selecting a printer config updates the job config and UI."""
|
|
238
|
+
editor = gui_app.blk.job_config_editor
|
|
239
|
+
test_file = "/path/to/printer.yml"
|
|
240
|
+
mock_file_dialog.return_value = test_file
|
|
241
|
+
|
|
242
|
+
# Simulate clicking the select printer button
|
|
243
|
+
editor.btn.select_printer.invoke()
|
|
244
|
+
|
|
245
|
+
# Get the actual call arguments
|
|
246
|
+
args, kwargs = mock_file_dialog.call_args
|
|
247
|
+
assert kwargs.get("title") == "Open a file"
|
|
248
|
+
assert kwargs.get("filetypes") == (
|
|
249
|
+
("printer config", "*.yml"),
|
|
250
|
+
("all files", "*.*"),
|
|
251
|
+
)
|
|
252
|
+
assert str(kwargs.get("initialdir")) == str(Path.home())
|
|
253
|
+
|
|
254
|
+
# Verify the printer config was updated
|
|
255
|
+
assert editor.job_config.printer_config == test_file
|
|
256
|
+
assert editor.var.printer_config.get() == test_file
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_toggle_cut(gui_app):
|
|
260
|
+
"""Test toggling the cut option updates the job config."""
|
|
261
|
+
editor = gui_app.blk.job_config_editor
|
|
262
|
+
initial_cut = editor.job_config.cut
|
|
263
|
+
|
|
264
|
+
# Set the combobox to the opposite of current cut state
|
|
265
|
+
new_value = "no" if initial_cut else "yes"
|
|
266
|
+
editor.cbx.cut.set(new_value)
|
|
267
|
+
|
|
268
|
+
# Trigger the combobox selection event
|
|
269
|
+
editor.cbx.cut.event_generate("<<ComboboxSelected>>")
|
|
270
|
+
|
|
271
|
+
# Verify the cut value has been toggled
|
|
272
|
+
assert editor.job_config.cut != initial_cut
|
|
273
|
+
assert editor.job_config.cut == (new_value == "yes")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_change_sleep_time(gui_app):
|
|
277
|
+
"""Test changing the sleep time updates the job config and UI."""
|
|
278
|
+
editor = gui_app.blk.job_config_editor
|
|
279
|
+
new_sleep_time = 1.5
|
|
280
|
+
editor.scl.sleep_time.set(new_sleep_time)
|
|
281
|
+
assert editor.job_config.sleep_time == new_sleep_time
|
|
282
|
+
assert editor.var.sleep_time.get() == new_sleep_time
|
|
283
|
+
assert float(editor.lbl.sleep_value["text"]) == new_sleep_time
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_sync_from_model(gui_app):
|
|
287
|
+
"""Test that sync_from_model updates the UI correctly."""
|
|
288
|
+
editor = gui_app.blk.job_config_editor
|
|
289
|
+
job_config_manager = editor.gui.blk.job_config_manager
|
|
290
|
+
|
|
291
|
+
# Create a test config
|
|
292
|
+
test_config = JobConfig(
|
|
293
|
+
resource_list=[Resource("test.txt")],
|
|
294
|
+
printer_config="test_printer.yml",
|
|
295
|
+
cut=True,
|
|
296
|
+
sleep_time=1.0,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Set the job config and sync
|
|
300
|
+
job_config_manager.job_config = test_config
|
|
301
|
+
editor.sync_from_model()
|
|
302
|
+
|
|
303
|
+
# Verify UI is in sync with the model
|
|
304
|
+
assert "test.txt" in editor.var.resource_list.get()
|
|
305
|
+
assert editor.var.printer_config.get() == "test_printer.yml"
|
|
306
|
+
assert editor.cbx.cut.get() == "yes"
|
|
307
|
+
assert editor.var.sleep_time.get() == 1.0
|
|
308
|
+
|
|
309
|
+
# Verify filename indicator if it exists
|
|
310
|
+
if hasattr(job_config_manager.var, "filename_indicator"):
|
|
311
|
+
# The filename should be the default one set in JobConfigManager.__init__
|
|
312
|
+
assert job_config_manager.var.filename_indicator.get() == "tmp-print-job.json"
|
|
@@ -13,7 +13,7 @@ from tinyprint.job import Job
|
|
|
13
13
|
from tinyprint.jobconfig import JobConfig, Resource, Orientation
|
|
14
14
|
|
|
15
15
|
# Test data directory
|
|
16
|
-
TEST_DATA = Path(__file__).parent / "data"
|
|
16
|
+
TEST_DATA = Path(__file__).absolute().parent / "data"
|
|
17
17
|
|
|
18
18
|
# Test configurations
|
|
19
19
|
RESOURCES = {
|
|
@@ -69,4 +69,4 @@ def test_print(snapshot, resource_path_list, config_kwargs):
|
|
|
69
69
|
job.start()
|
|
70
70
|
job.wait()
|
|
71
71
|
|
|
72
|
-
assert snapshot == job.manager.printer.output
|
|
72
|
+
assert snapshot == job.manager.printer.output, resource_path_list
|
|
@@ -224,7 +224,10 @@ class JobConfigEditor(Block):
|
|
|
224
224
|
)
|
|
225
225
|
|
|
226
226
|
def select_file(self, attr, filetypes, multiple=False):
|
|
227
|
-
|
|
227
|
+
file_list = super().select_file(filetypes, multiple)
|
|
228
|
+
if multiple is False:
|
|
229
|
+
file_list = [file_list]
|
|
230
|
+
if file_list:
|
|
228
231
|
setattr(
|
|
229
232
|
self.job_config,
|
|
230
233
|
attr,
|
|
@@ -0,0 +1,172 @@
|
|
|
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 PrinterCommand:
|
|
22
|
+
"""Wrapper for ESC/POS commands with an index for tracking command sequence.
|
|
23
|
+
|
|
24
|
+
This is used to track the position of commands for special handling,
|
|
25
|
+
particularly for paper cutting between images.
|
|
26
|
+
"""
|
|
27
|
+
index: int # Position in command sequence
|
|
28
|
+
cmd: bytes # Raw ESC/POS command bytes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if 1:
|
|
33
|
+
|
|
34
|
+
def _image(
|
|
35
|
+
self,
|
|
36
|
+
img_source,
|
|
37
|
+
high_density_vertical: bool = True,
|
|
38
|
+
high_density_horizontal: bool = True,
|
|
39
|
+
impl: str = "bitImageRaster",
|
|
40
|
+
fragment_height: int = 960,
|
|
41
|
+
center: bool = False,
|
|
42
|
+
cmd_index_offset: int = 0,
|
|
43
|
+
) -> Optional[int]:
|
|
44
|
+
"""Print an image.
|
|
45
|
+
|
|
46
|
+
You can select whether the printer should print in high density or not. The default value is high density.
|
|
47
|
+
When printing in low density, the image will be stretched.
|
|
48
|
+
|
|
49
|
+
Esc/Pos supplies several commands for printing. This function supports three of them. Please try to vary the
|
|
50
|
+
implementations if you have any problems. For example the printer `IT80-002` will have trouble aligning
|
|
51
|
+
images that are not printed in Column-mode.
|
|
52
|
+
|
|
53
|
+
The available printing implementations are:
|
|
54
|
+
|
|
55
|
+
* `bitImageRaster`: prints with the `GS v 0`-command
|
|
56
|
+
* `graphics`: prints with the `GS ( L`-command
|
|
57
|
+
* `bitImageColumn`: prints with the `ESC *`-command
|
|
58
|
+
|
|
59
|
+
When trying to center an image make sure you have initialized the printer with a valid profile, that
|
|
60
|
+
contains a media width pixel field. Otherwise the centering will have no effect.
|
|
61
|
+
|
|
62
|
+
:param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp`
|
|
63
|
+
:param high_density_vertical: print in high density in vertical direction *default:* True
|
|
64
|
+
:param high_density_horizontal: print in high density in horizontal direction *default:* True
|
|
65
|
+
:param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn`
|
|
66
|
+
:param fragment_height: Images larger than this will be split into multiple fragments *default:* 960
|
|
67
|
+
:param center: Center image horizontally *default:* False
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
im = EscposImage(img_source)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
if self.profile.profile_data["media"]["width"]["pixels"] == "Unknown":
|
|
74
|
+
logging.debug(
|
|
75
|
+
"The media.width.pixel field of the printer profile is not set. "
|
|
76
|
+
+ "The center flag will have no effect."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
max_width = int(self.profile.profile_data["media"]["width"]["pixels"])
|
|
80
|
+
|
|
81
|
+
if im.width > max_width:
|
|
82
|
+
raise ImageWidthError(f"{im.width} > {max_width}")
|
|
83
|
+
|
|
84
|
+
if center:
|
|
85
|
+
im.center(max_width)
|
|
86
|
+
except KeyError:
|
|
87
|
+
# If the printer's pixel width is not known, print anyways...
|
|
88
|
+
pass
|
|
89
|
+
except ValueError:
|
|
90
|
+
# If the max_width cannot be converted to an int, print anyways...
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
if im.height > fragment_height:
|
|
94
|
+
fragments = im.split(fragment_height)
|
|
95
|
+
cmd_index_offset = 0
|
|
96
|
+
for fragment in fragments:
|
|
97
|
+
cmd_index_offset = self.image(
|
|
98
|
+
fragment,
|
|
99
|
+
high_density_vertical=high_density_vertical,
|
|
100
|
+
high_density_horizontal=high_density_horizontal,
|
|
101
|
+
impl=impl,
|
|
102
|
+
fragment_height=fragment_height,
|
|
103
|
+
cmd_index_offset=cmd_index_offset,
|
|
104
|
+
)
|
|
105
|
+
# XXX dummy printer doesn't have '_sleep_in_fragment' method
|
|
106
|
+
# self._sleep_in_fragment()
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
if impl == "bitImageRaster":
|
|
110
|
+
# GS v 0, raster format bit image
|
|
111
|
+
density_byte = (0 if high_density_horizontal else 1) + (
|
|
112
|
+
0 if high_density_vertical else 2
|
|
113
|
+
)
|
|
114
|
+
header = (
|
|
115
|
+
GS
|
|
116
|
+
+ b"v0"
|
|
117
|
+
+ bytes((density_byte,))
|
|
118
|
+
+ self._int_low_high(im.width_bytes, 2)
|
|
119
|
+
+ self._int_low_high(im.height, 2)
|
|
120
|
+
)
|
|
121
|
+
self._raw(header + im.to_raster_format())
|
|
122
|
+
|
|
123
|
+
if impl == "graphics":
|
|
124
|
+
# GS ( L raster format graphics
|
|
125
|
+
img_header = self._int_low_high(im.width, 2) + self._int_low_high(
|
|
126
|
+
im.height, 2
|
|
127
|
+
)
|
|
128
|
+
tone = b"0"
|
|
129
|
+
colors = b"1"
|
|
130
|
+
ym = b"\x01" if high_density_vertical else b"\x02"
|
|
131
|
+
xm = b"\x01" if high_density_horizontal else b"\x02"
|
|
132
|
+
header = tone + xm + ym + colors + img_header
|
|
133
|
+
raster_data = im.to_raster_format()
|
|
134
|
+
self._image_send_graphics_data(b"0", b"p", header + raster_data)
|
|
135
|
+
self._image_send_graphics_data(b"0", b"2", b"")
|
|
136
|
+
|
|
137
|
+
if impl == "bitImageColumn":
|
|
138
|
+
# ESC *, column format bit image
|
|
139
|
+
density_byte = (1 if high_density_horizontal else 0) + (
|
|
140
|
+
32 if high_density_vertical else 0
|
|
141
|
+
)
|
|
142
|
+
header = (
|
|
143
|
+
ESC
|
|
144
|
+
+ b"*"
|
|
145
|
+
+ six.int2byte(density_byte)
|
|
146
|
+
+ self._int_low_high(im.width, 2)
|
|
147
|
+
)
|
|
148
|
+
self._raw(PrinterCommand(cmd_index_offset, ESC + b"3" + six.int2byte(16)))
|
|
149
|
+
for i, blob in enumerate(im.to_column_format(high_density_vertical)):
|
|
150
|
+
cmd_index = i + 1 + cmd_index_offset
|
|
151
|
+
self._raw(PrinterCommand(cmd_index, header + blob + b"\n"))
|
|
152
|
+
self._raw(ESC + b"2")
|
|
153
|
+
return cmd_index_offset + i + 1
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
Escpos.image = _image
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if 1:
|
|
161
|
+
|
|
162
|
+
# Usb printer: reduce likelihood for timeout gibberish
|
|
163
|
+
|
|
164
|
+
_Usb_raw_original = UsbPrinter._raw
|
|
165
|
+
|
|
166
|
+
def _Usb_raw(self, *args, **kwargs) -> None:
|
|
167
|
+
_Usb_raw_original(self, *args, **kwargs)
|
|
168
|
+
# NOTE Patch to reduce likelihood for timeout gibberish
|
|
169
|
+
while self._read():
|
|
170
|
+
time.sleep(0.01)
|
|
171
|
+
|
|
172
|
+
UsbPrinter._raw = _Usb_raw
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from marko import block
|
|
2
|
+
|
|
3
|
+
# Allow multiple blank lines to create multiple blank lines
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BlankLine(block.BlockElement):
|
|
7
|
+
"""Blank lines"""
|
|
8
|
+
|
|
9
|
+
priority = 5
|
|
10
|
+
|
|
11
|
+
def __init__(self, start: int) -> None:
|
|
12
|
+
self._anchor = start
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def match(cls, source) -> bool:
|
|
16
|
+
line = source.next_line()
|
|
17
|
+
return line is not None and not line.strip()
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def parse(cls, source) -> int:
|
|
21
|
+
m = source.match
|
|
22
|
+
source.consume()
|
|
23
|
+
return m
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
block.BlankLine = BlankLine # type: ignore
|
|
@@ -4,6 +4,7 @@ import abc
|
|
|
4
4
|
from functools import cached_property
|
|
5
5
|
import mimetypes
|
|
6
6
|
import os
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from tempfile import TemporaryDirectory
|
|
8
9
|
from typing import Callable, TypeAlias, Optional
|
|
9
10
|
|
|
@@ -144,6 +145,7 @@ class MarkdownPreprocessor(Preprocessor):
|
|
|
144
145
|
md = f.read()
|
|
145
146
|
|
|
146
147
|
def _(printer):
|
|
148
|
+
self.markdown.renderer.set_markdown_path(resource.path)
|
|
147
149
|
self.markdown.renderer.set_printer(printer)
|
|
148
150
|
self.markdown.renderer.set_config(self.config)
|
|
149
151
|
self.markdown.renderer.set_printer_profile(self.printer_profile)
|
|
@@ -192,7 +194,8 @@ class _EscposRenderer(marko.renderer.Renderer):
|
|
|
192
194
|
return ""
|
|
193
195
|
|
|
194
196
|
def render_image(self, element) -> str:
|
|
195
|
-
|
|
197
|
+
path = _resolve_path(element.dest, self._markdown_path)
|
|
198
|
+
resource = Resource(path=path)
|
|
196
199
|
get_image_page(resource, self._config, self._printer_profile)(self._printer)
|
|
197
200
|
return ""
|
|
198
201
|
|
|
@@ -241,6 +244,9 @@ class _EscposRenderer(marko.renderer.Renderer):
|
|
|
241
244
|
def set_printer_profile(self, printer_profile):
|
|
242
245
|
self._printer_profile = printer_profile
|
|
243
246
|
|
|
247
|
+
def set_markdown_path(self, markdown_path):
|
|
248
|
+
self._markdown_path = markdown_path
|
|
249
|
+
|
|
244
250
|
def _set(self, *args, **kwargs):
|
|
245
251
|
if self._printer:
|
|
246
252
|
self._printer.set(*args, **kwargs)
|
|
@@ -273,3 +279,13 @@ class _EscposRenderer(marko.renderer.Renderer):
|
|
|
273
279
|
@max_char_count.setter
|
|
274
280
|
def max_char_count(self, max_char_count: int):
|
|
275
281
|
self._max_char_count = max_char_count
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _resolve_path(path: str, markdown_path: str) -> str:
|
|
285
|
+
p = Path(path)
|
|
286
|
+
|
|
287
|
+
if p.is_absolute():
|
|
288
|
+
return path
|
|
289
|
+
|
|
290
|
+
markdown_dir = Path(markdown_path).parent
|
|
291
|
+
return str((markdown_dir / p).resolve())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Printer communication manager for handling ESC/POS printer interactions."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import six
|
|
10
|
+
import usb # type: ignore
|
|
11
|
+
|
|
12
|
+
from escpos.constants import GS # type: ignore
|
|
13
|
+
from escpos.config import Config as escpos_Config # type: ignore
|
|
14
|
+
from escpos.printer import Dummy # type: ignore
|
|
15
|
+
|
|
16
|
+
from tinyprint.printer.profile import patch_printer_profile
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Manager:
|
|
20
|
+
"""Manages printer communication including connection handling and command sending."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config):
|
|
23
|
+
"""Initialize the printer manager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Configuration object containing printer settings.
|
|
27
|
+
"""
|
|
28
|
+
self.config = config
|
|
29
|
+
self._logger = logging.getLogger(f"{__name__}.Manager")
|
|
30
|
+
self._print_job: Optional[threading.Thread] = None
|
|
31
|
+
self.is_printing = False
|
|
32
|
+
|
|
33
|
+
@functools.cached_property
|
|
34
|
+
def printer(self):
|
|
35
|
+
if not self.config.printer_config:
|
|
36
|
+
self._logger.warning("no printer config: use dummy printer")
|
|
37
|
+
p = Dummy()
|
|
38
|
+
else:
|
|
39
|
+
c = escpos_Config()
|
|
40
|
+
c.load(self.config.printer_config)
|
|
41
|
+
p = c.printer()
|
|
42
|
+
patch_printer_profile(p)
|
|
43
|
+
return p
|
|
44
|
+
|
|
45
|
+
def print(self, cmd_list):
|
|
46
|
+
"""Start printing in background thread"""
|
|
47
|
+
self.is_printing = True
|
|
48
|
+
|
|
49
|
+
def _():
|
|
50
|
+
self._print(cmd_list)
|
|
51
|
+
self.is_printing = False
|
|
52
|
+
|
|
53
|
+
self._print_job = threading.Thread(target=_)
|
|
54
|
+
self._print_job.start()
|
|
55
|
+
|
|
56
|
+
def _print(self, cmd_list: list[bytes]):
|
|
57
|
+
try:
|
|
58
|
+
for cmd in cmd_list:
|
|
59
|
+
if not self.is_printing:
|
|
60
|
+
break
|
|
61
|
+
self._print1(cmd)
|
|
62
|
+
finally:
|
|
63
|
+
# No matter what happens - make final cut.
|
|
64
|
+
# Final cut
|
|
65
|
+
# 66 => move to cut position
|
|
66
|
+
# 00 => then make full cut
|
|
67
|
+
self.printer._raw(GS + b"V" + six.int2byte(66) + b"\x00")
|
|
68
|
+
|
|
69
|
+
def _print1(self, cmd: bytes):
|
|
70
|
+
printer = self.printer
|
|
71
|
+
sleep_time = self.config.sleep_time or printer.profile.sleep_time
|
|
72
|
+
try:
|
|
73
|
+
printer._raw(cmd)
|
|
74
|
+
# NOTE ( USB device printer fix )
|
|
75
|
+
#
|
|
76
|
+
# Do not lose any command:
|
|
77
|
+
# Python sends faster commands than the matrix
|
|
78
|
+
# printer can print. And the USB device doesn't
|
|
79
|
+
# block until one print command is finished, but
|
|
80
|
+
# returns immediately. If we run into a USBTimeout,
|
|
81
|
+
# this becomes a serious problem, because then
|
|
82
|
+
# we restart the printer & only resend the last command.
|
|
83
|
+
# In case the previous command wasn't processed yet,
|
|
84
|
+
# we'd lose this command (usually the paper cut).
|
|
85
|
+
# To avoid this, we wait for a little time, to better
|
|
86
|
+
# synchronize Python and the Matrix printer.
|
|
87
|
+
if sleep_time and len(cmd) > 5:
|
|
88
|
+
self._sleep(sleep_time)
|
|
89
|
+
# NOTE ( USB device printer fix )
|
|
90
|
+
# Don't give up when a time out happens - it seems USB
|
|
91
|
+
# connection is sometimes unstable & breaks.
|
|
92
|
+
except usb.core.USBTimeoutError:
|
|
93
|
+
self._logger.warning("timed out ... reset printer")
|
|
94
|
+
# We first read all data from printer to reduce the
|
|
95
|
+
# likelihood that the printer outputs glitchy gibberish
|
|
96
|
+
# to the print.
|
|
97
|
+
while printer._read():
|
|
98
|
+
time.sleep(0.01)
|
|
99
|
+
# Then reset printer to make it workable again
|
|
100
|
+
self._sleep(10)
|
|
101
|
+
printer.close()
|
|
102
|
+
self._sleep(10)
|
|
103
|
+
printer.open()
|
|
104
|
+
self._sleep(10)
|
|
105
|
+
# Finally try send command again
|
|
106
|
+
printer._raw(cmd)
|
|
107
|
+
|
|
108
|
+
def _sleep(self, total_time: float):
|
|
109
|
+
"""Sleep that returns immediatelly if no longer printing"""
|
|
110
|
+
check_interval: float = 1
|
|
111
|
+
end_time = time.monotonic() + total_time
|
|
112
|
+
|
|
113
|
+
while time.monotonic() < end_time:
|
|
114
|
+
if not self.is_printing:
|
|
115
|
+
return
|
|
116
|
+
time.sleep(min(check_interval, end_time - time.monotonic()))
|
|
117
|
+
|
|
118
|
+
def wait(self, timeout: Optional[float] = None):
|
|
119
|
+
"""Wait/block until print is finished"""
|
|
120
|
+
if self._print_job:
|
|
121
|
+
self._print_job.join(timeout)
|
|
122
|
+
self._print_job = None
|
|
123
|
+
self.is_printing = False
|
|
124
|
+
|
|
125
|
+
def stop(self):
|
|
126
|
+
"""Stop currently running printing"""
|
|
127
|
+
if self._print_job and self._print_job.is_alive():
|
|
128
|
+
self.is_printing = False
|
|
129
|
+
self.wait()
|
|
130
|
+
|
|
131
|
+
def close(self):
|
|
132
|
+
self._logger.info("close printer")
|
|
133
|
+
self.stop()
|
|
134
|
+
self.printer.close()
|
|
135
|
+
|
|
136
|
+
def __del__(self):
|
|
137
|
+
self.close()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Printer profile management for the tinyprint application.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to manage and configure printer profiles
|
|
4
|
+
for different ESC/POS compatible printers. It handles printer-specific settings
|
|
5
|
+
and configurations to ensure optimal printing results across different printer
|
|
6
|
+
models.
|
|
7
|
+
|
|
8
|
+
The module includes functionality to:
|
|
9
|
+
- Apply custom configurations to printer profiles
|
|
10
|
+
- Handle printer model-specific settings
|
|
11
|
+
- Configure connection-specific parameters
|
|
12
|
+
- Set default values for various printing parameters
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from escpos.printer.usb import Usb as UsbPrinter # type: ignore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def patch_printer_profile(printer):
|
|
19
|
+
"""Apply custom configuration to a printer's profile.
|
|
20
|
+
|
|
21
|
+
This function extends the printer's profile with additional settings and
|
|
22
|
+
configurations that are not included in the default ESC/POS profile. It
|
|
23
|
+
handles model-specific configurations and sets appropriate defaults for
|
|
24
|
+
various printing parameters.
|
|
25
|
+
"""
|
|
26
|
+
profile = printer.profile
|
|
27
|
+
|
|
28
|
+
# Default values in case printer is unknown
|
|
29
|
+
profile.img_kwargs = {}
|
|
30
|
+
profile.img_cut_index = 3
|
|
31
|
+
profile.img_scale_factor = 1
|
|
32
|
+
profile.img_x_scale_factor = 1
|
|
33
|
+
profile.img_y_scale_factor = 1
|
|
34
|
+
profile.sleep_time = 0
|
|
35
|
+
|
|
36
|
+
# How many characters fit on one line with normal font size.
|
|
37
|
+
# 42 is also default for TM-T88III.
|
|
38
|
+
profile.default_char_count = 42
|
|
39
|
+
# How many characters fit on one line with heading font size.
|
|
40
|
+
# 21 is also default for TM-T88III.
|
|
41
|
+
profile.big_char_count = 21
|
|
42
|
+
|
|
43
|
+
# Special treatment for specific printer models.
|
|
44
|
+
match profile.profile_data["name"]:
|
|
45
|
+
case "TM-T88III":
|
|
46
|
+
profile.img_kwargs = dict(
|
|
47
|
+
impl="bitImageColumn",
|
|
48
|
+
high_density_vertical=True,
|
|
49
|
+
high_density_horizontal=True,
|
|
50
|
+
center=False,
|
|
51
|
+
)
|
|
52
|
+
profile.default_char_count = 42
|
|
53
|
+
profile.big_char_count = 21
|
|
54
|
+
profile.img_cut_index = 4
|
|
55
|
+
case "TM-U220B":
|
|
56
|
+
profile.img_kwargs = dict(
|
|
57
|
+
impl="bitImageColumn",
|
|
58
|
+
high_density_vertical=False,
|
|
59
|
+
high_density_horizontal=False,
|
|
60
|
+
center=False,
|
|
61
|
+
fragment_height=32,
|
|
62
|
+
)
|
|
63
|
+
# NOTE Numbers need less space than letters in case of this
|
|
64
|
+
# printer (there is space for 40 numbers). Let's use the
|
|
65
|
+
# smaller number to make sure everything always fits in one
|
|
66
|
+
# line.
|
|
67
|
+
profile.default_char_count = 33
|
|
68
|
+
profile.big_char_count = 16
|
|
69
|
+
profile.img_cut_index = 9
|
|
70
|
+
# Profile is wrong, it's 200 dots, not 400
|
|
71
|
+
# https://files.support.epson.com/pdf/pos/bulk/tm-u220_trg_en_std_reve.pdf
|
|
72
|
+
# https://github.com/receipt-print-hq/escpos-printer-db/pull/87/commits/242299097
|
|
73
|
+
profile.profile_data["media"]["width"]["pixels"] = 200
|
|
74
|
+
|
|
75
|
+
case _:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Special treatment for specific connections to printer.
|
|
79
|
+
match printer:
|
|
80
|
+
case UsbPrinter():
|
|
81
|
+
# Usb printer immediately returns, but is usually slower
|
|
82
|
+
# than Python - improve synchronicity & sleep after each
|
|
83
|
+
# print command. Otherwise we get a lot of timeouts &
|
|
84
|
+
# printed gibberish. The time here seems to depend on the
|
|
85
|
+
# printed material. If it's more dense & blackish / colorful,
|
|
86
|
+
# then we need a higher value to not run into any trouble.
|
|
87
|
+
# More spare material seems to be less problematic. Also
|
|
88
|
+
# the problem becomes bigger if we print multiple pages.
|
|
89
|
+
profile.sleep_time = 0.3
|
|
90
|
+
case _:
|
|
91
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyprint
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: ui & cli to print zines with esc/pos printer
|
|
5
5
|
Author-email: Levin Eric Zimmermann <levin.eric.zimmermann@posteo.com>
|
|
6
6
|
License: GPL
|
|
@@ -23,3 +23,31 @@ Dynamic: license-file
|
|
|
23
23
|
# tinyprint
|
|
24
24
|
|
|
25
25
|
Software for printing tiny zines with ESC/POS printer.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### MacOS
|
|
32
|
+
|
|
33
|
+
Open a console and type:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)
|
|
37
|
+
brew install python
|
|
38
|
+
brew install python-tk
|
|
39
|
+
brew install pipx
|
|
40
|
+
pipx install tinyprint
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Linux
|
|
44
|
+
|
|
45
|
+
Install python, and then just use pip:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip3 install tinyprint
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Windows
|
|
52
|
+
|
|
53
|
+
I don't know, but if someone manages, please let us know.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
+
tests/test_gui.py
|
|
4
5
|
tests/test_integration.py
|
|
5
6
|
tests/test_job.py
|
|
6
7
|
tests/test_jobconfig.py
|
|
@@ -17,4 +18,10 @@ tinyprint.egg-info/SOURCES.txt
|
|
|
17
18
|
tinyprint.egg-info/dependency_links.txt
|
|
18
19
|
tinyprint.egg-info/entry_points.txt
|
|
19
20
|
tinyprint.egg-info/requires.txt
|
|
20
|
-
tinyprint.egg-info/top_level.txt
|
|
21
|
+
tinyprint.egg-info/top_level.txt
|
|
22
|
+
tinyprint/patches/__init__.py
|
|
23
|
+
tinyprint/patches/escpos.py
|
|
24
|
+
tinyprint/patches/mako.py
|
|
25
|
+
tinyprint/printer/__init__.py
|
|
26
|
+
tinyprint/printer/manager.py
|
|
27
|
+
tinyprint/printer/profile.py
|
tinyprint-0.2.1/README.md
DELETED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|