Compare commits
373 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0d22896689 | ||
![]() |
b66dafc90e | ||
![]() |
0c824cf295 | ||
![]() |
66a2e78e16 | ||
![]() |
06bdb56937 | ||
![]() |
91cbc264fc | ||
![]() |
dcc71ce47d | ||
![]() |
d2b213dcdc | ||
![]() |
c91eec544e | ||
![]() |
a9e49c909d | ||
![]() |
815f247df1 | ||
![]() |
5914c7c560 | ||
![]() |
86a715cf02 | ||
![]() |
ac23c083b6 | ||
![]() |
9a1699ab94 | ||
![]() |
8274833255 | ||
![]() |
33d17615a0 | ||
![]() |
90c89e66f0 | ||
![]() |
8f44ae7480 | ||
![]() |
ff5631c989 | ||
![]() |
ecf3fbcc25 | ||
![]() |
678f8d2451 | ||
![]() |
a127c7b9c4 | ||
![]() |
72d26879a0 | ||
![]() |
3b6551004f | ||
![]() |
5d3d2ca34b | ||
![]() |
d2ef5159c5 | ||
![]() |
a50a3b7167 | ||
![]() |
e7dd97554c | ||
![]() |
425d3d209f | ||
![]() |
a00b98937b | ||
![]() |
a8d789fe64 | ||
![]() |
246c4ea6c5 | ||
![]() |
3a8af8a6f5 | ||
![]() |
e4f4844983 | ||
![]() |
a70e1604d6 | ||
![]() |
5018f7f377 | ||
![]() |
ecfeeb9b13 | ||
![]() |
ba0167f1fd | ||
![]() |
87e488ce11 | ||
![]() |
6d0c475b9a | ||
![]() |
adcde8abda | ||
![]() |
89aaae0186 | ||
![]() |
f3ee049ee1 | ||
![]() |
cfa9ecf16d | ||
![]() |
8f07c1da0f | ||
![]() |
24217756f7 | ||
![]() |
e3e1500d35 | ||
![]() |
44444f3c51 | ||
![]() |
c7c01cdbff | ||
![]() |
6c49e4a057 | ||
![]() |
f23bcacabe | ||
![]() |
c11d192e65 | ||
![]() |
3177c8d411 | ||
![]() |
fbabd8ed88 | ||
![]() |
2b62c8e28d | ||
![]() |
4c2dcdfac6 | ||
![]() |
31daabcbea | ||
![]() |
4a0f5855ef | ||
![]() |
60c4f481ae | ||
![]() |
09a598883c | ||
![]() |
df9e8ff394 | ||
![]() |
9ff327d967 | ||
![]() |
e9e8b10582 | ||
![]() |
cb3f4e856b | ||
![]() |
756bf2c6c2 | ||
![]() |
3c11c1b9ab | ||
![]() |
676d2840de | ||
![]() |
e72ebc7986 | ||
![]() |
c67c077a4d | ||
![]() |
fc17391b63 | ||
![]() |
905e37e52e | ||
![]() |
66348ccc0a | ||
![]() |
2b826aa834 | ||
![]() |
3560f8b12b | ||
![]() |
83c7f5745c | ||
![]() |
7bf8d4e4b8 | ||
![]() |
6849dd3979 | ||
![]() |
25867f3196 | ||
![]() |
8bbbf7ceac | ||
![]() |
5b72985b0a | ||
![]() |
ef7d518953 | ||
![]() |
d4d3819d55 | ||
![]() |
98ee06a783 | ||
![]() |
de515a86cc | ||
![]() |
2b0d57a9c7 | ||
![]() |
1626af5def | ||
![]() |
e4a73ad5c5 | ||
![]() |
709d90809c | ||
![]() |
90c340163c | ||
![]() |
757fe3328c | ||
![]() |
0a8d8ae6c4 | ||
![]() |
70d4da1364 | ||
![]() |
cbf887ed02 | ||
![]() |
b1a1204bb8 | ||
![]() |
361afe3555 | ||
![]() |
2bc61d9bf8 | ||
![]() |
67827d8e4d | ||
![]() |
2567e0e12a | ||
![]() |
83cccf0cb9 | ||
![]() |
fe9c0573f8 | ||
![]() |
ea61f287cb | ||
![]() |
69e9dca761 | ||
![]() |
49ab3f35a8 | ||
![]() |
84a3912b34 | ||
![]() |
9a3057c8a8 | ||
![]() |
0b695eff79 | ||
![]() |
f0760ddbc0 | ||
![]() |
39826c3286 | ||
![]() |
7f9878282d | ||
![]() |
5e80764ff5 | ||
![]() |
219b43d35a | ||
![]() |
b16f44257e | ||
![]() |
5bb0642b5d | ||
![]() |
8e5c600e20 | ||
![]() |
435f2bba24 | ||
![]() |
109a5d8a92 | ||
![]() |
4a88bacd3f | ||
![]() |
19abf43448 | ||
![]() |
7b6dbc4db4 | ||
![]() |
53aab73ffc | ||
![]() |
a4e15c6ac2 | ||
![]() |
dcfb8fec30 | ||
![]() |
14edc87206 | ||
![]() |
1e3024eaa9 | ||
![]() |
15c18f225e | ||
![]() |
a526be5ddf | ||
![]() |
92fbcabdd2 | ||
![]() |
27b7ae8629 | ||
![]() |
7c82881f65 | ||
![]() |
05f68b22b4 | ||
![]() |
f9ce777057 | ||
![]() |
c2e3b79e5a | ||
![]() |
b5a999a6ea | ||
![]() |
0b2118e146 | ||
![]() |
69e9b9103e | ||
![]() |
620cf97bbf | ||
![]() |
1ad53eb642 | ||
![]() |
ed8ec0788a | ||
![]() |
dd3c768f13 | ||
![]() |
36bbb6690f | ||
![]() |
0758a79e64 | ||
![]() |
9c41cfc54f | ||
![]() |
75006f62da | ||
![]() |
58e1682448 | ||
![]() |
0057a498bb | ||
![]() |
b3e93020d4 | ||
![]() |
3962bc991f | ||
![]() |
b608d59942 | ||
![]() |
e898413464 | ||
![]() |
357558ae10 | ||
![]() |
809b4f47aa | ||
![]() |
b6516e4b42 | ||
![]() |
8a54d59dbc | ||
![]() |
1275bf90d1 | ||
![]() |
d6639d37f3 | ||
![]() |
21f19b7f7e | ||
![]() |
3c479407dc | ||
![]() |
3f04ff352e | ||
![]() |
6c89e93c22 | ||
![]() |
266b64bf2b | ||
![]() |
a2e348d04a | ||
![]() |
40c8399a68 | ||
![]() |
a279ae7152 | ||
![]() |
a2d553efb1 | ||
![]() |
84a97e5b9f | ||
![]() |
7db3d30f4a | ||
![]() |
c6f93e017a | ||
![]() |
c09c10cb2d | ||
![]() |
af4785d52f | ||
![]() |
e68cdcdac2 | ||
![]() |
18cc1a268a | ||
![]() |
5d62be2be9 | ||
![]() |
1dc79baf82 | ||
![]() |
247211d0ce | ||
![]() |
243eb48b38 | ||
![]() |
21b9dba345 | ||
![]() |
a8941b4a3e | ||
![]() |
04e0a0ce47 | ||
![]() |
8ece137789 | ||
![]() |
d390449400 | ||
![]() |
8488fafd75 | ||
![]() |
818c1313e8 | ||
![]() |
596787554b | ||
![]() |
2d798ca7e5 | ||
![]() |
24105226b2 | ||
![]() |
c57b9c35dc | ||
![]() |
0a8477a888 | ||
![]() |
e3cf088d69 | ||
![]() |
323db98318 | ||
![]() |
ee6eef6db3 | ||
![]() |
ef34cca463 | ||
![]() |
4680fb16b1 | ||
![]() |
544bb4f904 | ||
![]() |
d330dd80fd | ||
![]() |
570661b3c8 | ||
![]() |
aa7f2773eb | ||
![]() |
ff39908674 | ||
![]() |
d960eea4a8 | ||
![]() |
f7962576b4 | ||
![]() |
f4e214ad17 | ||
![]() |
9e406efc86 | ||
![]() |
b9e3827867 | ||
![]() |
cbe412cfdb | ||
![]() |
42554d836c | ||
![]() |
8ca682e3ac | ||
![]() |
ab30ef4a8c | ||
![]() |
725f1254aa | ||
![]() |
1e313cefc6 | ||
![]() |
b0ea9aec41 | ||
![]() |
ab210b5996 | ||
![]() |
f3e1db8da2 | ||
![]() |
1e7b43a92d | ||
![]() |
0e41709703 | ||
![]() |
cb30d7a881 | ||
![]() |
142fc4af71 | ||
![]() |
8f71372bb0 | ||
![]() |
81fb7caa07 | ||
![]() |
6a60b6706e | ||
![]() |
c14a414924 | ||
![]() |
faa9414da5 | ||
![]() |
2c4552a528 | ||
![]() |
c9953c2f56 | ||
![]() |
557991d80b | ||
![]() |
fe08fc1469 | ||
![]() |
6c27222aeb | ||
![]() |
6731057456 | ||
![]() |
f0b1a89c48 | ||
![]() |
fd7bd0710e | ||
![]() |
7ea58625e6 | ||
![]() |
baffd98a22 | ||
![]() |
ecbdd43dff | ||
![]() |
5b6b96d2a0 | ||
![]() |
7aa20a60e3 | ||
![]() |
95ec6d5c08 | ||
![]() |
18c3a5f298 | ||
![]() |
4836dcd486 | ||
![]() |
d9d400da6d | ||
![]() |
fe2e1a6d28 | ||
![]() |
c53575a155 | ||
![]() |
cadf448c38 | ||
![]() |
7c05404ac4 | ||
![]() |
e1e1ccb3f2 | ||
![]() |
32c56e78ea | ||
![]() |
4d106e8659 | ||
![]() |
ddab5318cf | ||
![]() |
9b60e2e3ab | ||
![]() |
34cd1ebde1 | ||
![]() |
a2db415559 | ||
![]() |
4e19b0ca51 | ||
![]() |
a3660a6366 | ||
![]() |
9fa551e6e8 | ||
![]() |
ae0a049efa | ||
![]() |
117d286371 | ||
![]() |
a555a651b4 | ||
![]() |
e350a49cad | ||
![]() |
f49c1dcb89 | ||
![]() |
cc67cb1c1e | ||
![]() |
2ee3ff7f87 | ||
![]() |
ca45d25670 | ||
![]() |
f07d5e0610 | ||
![]() |
5eeff1c16b | ||
![]() |
51d1299285 | ||
![]() |
a30c28baa5 | ||
![]() |
0c0e6b9b4c | ||
![]() |
50437cc9d2 | ||
![]() |
7c7d401f31 | ||
![]() |
2110431d4f | ||
![]() |
c0481f3a9a | ||
![]() |
6161b46d57 | ||
![]() |
c86826101d | ||
![]() |
fa140c2df5 | ||
![]() |
8b3076871f | ||
![]() |
46429b1092 | ||
![]() |
99ca096f82 | ||
![]() |
2d97c0bbbd | ||
![]() |
6c6fe9bccf | ||
![]() |
c5e46a888d | ||
![]() |
46942820a5 | ||
![]() |
e50e295acc | ||
![]() |
2d7458fa49 | ||
![]() |
a6f635c0d5 | ||
![]() |
2d0f045457 | ||
![]() |
293b8632ff | ||
![]() |
5ff73595b6 | ||
![]() |
4ecab402b8 | ||
![]() |
c56e43da84 | ||
![]() |
88af26f46e | ||
![]() |
9dd966c2a3 | ||
![]() |
a7d959428f | ||
![]() |
8bf0e08659 | ||
![]() |
5ac5a24b50 | ||
![]() |
63252515b5 | ||
![]() |
29a546821b | ||
![]() |
4ddd18279f | ||
![]() |
de761e96e3 | ||
![]() |
ed7bce6932 | ||
![]() |
edd567785c | ||
![]() |
f1054876da | ||
![]() |
490e0657dd | ||
![]() |
b4c32b5a4a | ||
![]() |
40b30225d3 | ||
![]() |
19e3ec6895 | ||
![]() |
df539da854 | ||
![]() |
4534038b39 | ||
![]() |
adf85f7784 | ||
![]() |
aaa8162967 | ||
![]() |
0461adc212 | ||
![]() |
91ff83e506 | ||
![]() |
29ef88f591 | ||
![]() |
7c01a30d6c | ||
![]() |
035c425581 | ||
![]() |
d20646b2a9 | ||
![]() |
206822ac69 | ||
![]() |
dc08792e72 | ||
![]() |
2886075ce9 | ||
![]() |
73fff6291d | ||
![]() |
d5b9d99093 | ||
![]() |
18c51358aa | ||
![]() |
52719c0b7d | ||
![]() |
0051c876bf | ||
![]() |
854759d312 | ||
![]() |
a0343c66af | ||
![]() |
6c94f88c24 | ||
![]() |
6fb23d6826 | ||
![]() |
f649814091 | ||
![]() |
47b4d41b28 | ||
![]() |
599f4f3ca5 | ||
![]() |
d085e5c467 | ||
![]() |
b418302311 | ||
![]() |
f6acb72bbe | ||
![]() |
0c9856c1f6 | ||
![]() |
a748563395 | ||
![]() |
b84e280efb | ||
![]() |
4390dc4a9c | ||
![]() |
6e09fd1e97 | ||
![]() |
100c6b5e89 | ||
![]() |
26d72a69f0 | ||
![]() |
01e28bbcf6 | ||
![]() |
2a7e2a6a36 | ||
![]() |
3c3dab95f5 | ||
![]() |
d1e7052fa1 | ||
![]() |
10e1dfe1d1 | ||
![]() |
cd1bcb57b4 | ||
![]() |
d6d12f99d4 | ||
![]() |
128221363f | ||
![]() |
6b0b1371e5 | ||
![]() |
44f01a212b | ||
![]() |
456f5b7aa6 | ||
![]() |
d78a6f1699 | ||
![]() |
5e784c060a | ||
![]() |
1439b14686 | ||
![]() |
b648cfd67f | ||
![]() |
50c627fbb0 | ||
![]() |
99034d0575 | ||
![]() |
19663ec574 | ||
![]() |
281eea125f | ||
![]() |
5bed0bfbb4 | ||
![]() |
f12470d3cd | ||
![]() |
fb0e4c28ba | ||
![]() |
af29fcca77 | ||
![]() |
f8b269d859 | ||
![]() |
c259263f26 | ||
![]() |
27c843935f | ||
![]() |
f3da6a9725 | ||
![]() |
b64b534394 | ||
![]() |
81426ab6dc | ||
![]() |
df1193ab35 | ||
![]() |
b494c9a4bd | ||
![]() |
f8a2174108 | ||
![]() |
1f57b04974 | ||
![]() |
c7080165a7 | ||
![]() |
cf0cf127fe |
3
.github/ISSUE_TEMPLATE.md
vendored
@@ -18,7 +18,8 @@ I have:
|
||||
**Printer:** Manufacturer Model XVI
|
||||
|
||||
<!-- since version 2.0.1 you can type 'python-escpos version' in your shell.
|
||||
Alternatively you could use '__version__' in module escpos. -->
|
||||
Starting with python-escpos version 3.0, please replace the information below
|
||||
with the output of `python-escpos version_extended`. -->
|
||||
**python-escpos version:** 0.0.0
|
||||
|
||||
**python version:** 0.0
|
||||
|
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,10 +1,5 @@
|
||||
### Contributor checklist
|
||||
<!-- mark with x between the brackets -->
|
||||
- [ ] I have read the CONTRIBUTING.rst
|
||||
- [ ] I have tested my contribution on these devices:
|
||||
* e.g. Epson TM-T88II
|
||||
- [ ] My contribution is ready to be merged as is
|
||||
|
||||
----------
|
||||
|
||||
### Description
|
||||
|
||||
|
||||
### Tested with
|
||||
_If applicable, please describe with which device you have tested._
|
23
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/doc"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
13
.github/workflows/black.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Lint (Black code style)
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
black-code-style:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
version: "23.12.0"
|
||||
|
66
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 1 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['python']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
32
.github/workflows/documentation.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Documentation build
|
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||
# events but only for the master branch
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "docs"
|
||||
docs:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: Install packages
|
||||
run:
|
||||
sudo apt-get update -y &&
|
||||
sudo apt-get install -y git python3-sphinx graphviz libenchant-2-2 &&
|
||||
sudo apt-get install -y gcc libcups2-dev python3-dev python3-setuptools &&
|
||||
sudo pip install tox pycups
|
||||
- name: Test doc build
|
||||
run: tox -e docs
|
55
.github/workflows/pythonpackage-windows.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Python package on Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest tox tox-gh-actions
|
||||
If (Test-Path .\requirements.txt) { pip install -r .\requirements.txt }
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E,F,W --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with tox
|
||||
run: |
|
||||
tox
|
||||
env:
|
||||
ESCPOS_CAPABILITIES_FILE: D:\a\python-escpos\python-escpos\capabilities-data\dist\capabilities.json
|
||||
- name: Test mypy with tox
|
||||
run: |
|
||||
tox -e mypy
|
||||
env:
|
||||
ESCPOS_CAPABILITIES_FILE: D:\a\python-escpos\python-escpos\capabilities-data\dist\capabilities.json
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
directory: ./coverage/reports/
|
||||
env_vars: OS,PYTHON
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml,!./cache
|
||||
flags: unittests
|
||||
name: coverage-tox-${{ matrix.python-version }}
|
||||
verbose: true
|
65
.github/workflows/pythonpackage.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y graphviz libenchant-2-2 gcc libcups2-dev python3-dev xindy
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest tox tox-gh-actions
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Check sorting of imports
|
||||
uses: isort/isort-action@master
|
||||
with:
|
||||
requirementsFiles: "requirements.txt doc/requirements.txt"
|
||||
sortPaths: "./doc ./src ./examples ./test ./setup.py"
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E,F,W --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with tox
|
||||
run: |
|
||||
tox
|
||||
env:
|
||||
ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json
|
||||
- name: Test mypy with tox
|
||||
run: |
|
||||
tox -e mypy
|
||||
env:
|
||||
ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
directory: ./coverage/reports/
|
||||
env_vars: OS,PYTHON
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml,!./cache
|
||||
flags: unittests
|
||||
name: coverage-tox-${{ matrix.python-version }}
|
||||
verbose: true
|
14
.gitignore
vendored
@@ -6,6 +6,7 @@ $~
|
||||
.idea/
|
||||
.directory
|
||||
.cache/
|
||||
settings.json
|
||||
|
||||
# temporary data
|
||||
temp
|
||||
@@ -20,6 +21,11 @@ dist/
|
||||
.coverage
|
||||
src/escpos/version.py
|
||||
.hypothesis
|
||||
.pytest_cache/
|
||||
coverage.xml
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# testing temporary directories
|
||||
test/test-cli-output/
|
||||
@@ -28,3 +34,11 @@ test/test-cli-output/
|
||||
*.swp
|
||||
*.swn
|
||||
*.swo
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
|
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "capabilities-data"]
|
||||
path = capabilities-data
|
||||
url = https://github.com/receipt-print-hq/escpos-printer-db.git
|
||||
branch = master
|
||||
|
9
.mailmap
@@ -8,4 +8,13 @@ Cody (Quantified Code Bot) <cody@quantifiedcode.com> Cody <cody@quantifiedcode.c
|
||||
Renato Lorenzi <renato.lorenzi@senior.com.br> Renato.Lorenzi <renato.lorenzi@senior.com.br>
|
||||
Ahmed Tahri <nyuubi.10@gmail.com> TAHRI Ahmed <nyuubi.10@gmail.com>
|
||||
Michael Elsdörfer <michael@elsdoerfer.com> Michael Elsdörfer <michael@elsdoerfer.info>
|
||||
Juanmi Taboada <juanmi@juanmitaboada.com> Juanmi Taboada <juanmi@juanmitaboada.com>
|
||||
csoft2k <csoft2k@hotmail.com>
|
||||
Sergio Pulgarin <sergio.pulgarin@gmail.com>
|
||||
reck31 <rakesh.gunduka@gmail.com>
|
||||
Alex Debiasio <alex.debiasio@thinkin.io> <alex.debiasio@studenti.unitn.it>
|
||||
白月秋见心 <ourweijiang@gmail.com>
|
||||
Maximilian Wagenbach <maximilian.wagenbach@native-instruments.de>
|
||||
<belono@users.noreply.github.com> <tiotil.lindeman@gmail.com>
|
||||
belono <belono@users.noreply.github.com> Benito López <belono@users.noreply.github.com>
|
||||
Alfredo Orozco <alfredoopa@gmail.com> Alfredo orozco <alfreedom@users.noreply.github.com>
|
||||
|
27
.readthedocs.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: 2
|
||||
formats:
|
||||
- epub
|
||||
- pdf
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
apt_packages:
|
||||
- graphviz
|
||||
- libenchant-2-2
|
||||
- gcc
|
||||
- libcups2-dev
|
||||
- python3-dev
|
||||
- xindy
|
||||
sphinx:
|
||||
configuration: doc/conf.py
|
||||
submodules:
|
||||
include:
|
||||
- capabilities-data
|
||||
recursive: true
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: doc/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
62
.travis.yml
@@ -1,62 +0,0 @@
|
||||
language: python
|
||||
sudo: false
|
||||
cache: pip
|
||||
git:
|
||||
depth: 100000
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- graphviz
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: TOXENV=py27
|
||||
- python: 3.3
|
||||
env: TOXENV=py33
|
||||
- python: 3.4
|
||||
env: TOXENV=py34
|
||||
- python: 3.5
|
||||
env: TOXENV=py35
|
||||
- python: 3.6
|
||||
env: TOXENV=py36
|
||||
- python: 3.6-dev
|
||||
env: TOXENV=py36
|
||||
- python: nightly
|
||||
env: TOXENV=py37
|
||||
- python: pypy
|
||||
env: TOXENV=pypy
|
||||
- python: pypy3
|
||||
env: TOXENV=pypy3
|
||||
- python: 2.7
|
||||
env: TOXENV=docs
|
||||
- python: 2.7
|
||||
env: TOXENV=flake8
|
||||
- python: 3.6
|
||||
env: TOXENV=flake8
|
||||
allow_failures:
|
||||
- python: 3.6-dev
|
||||
- python: nightly
|
||||
- python: pypy3
|
||||
before_install:
|
||||
- pip install tox codecov 'sphinx>=1.5.1'
|
||||
- ./doc/generate_authors.sh --check
|
||||
script:
|
||||
- tox
|
||||
- codecov
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: change
|
||||
deploy:
|
||||
# Github deployment
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: oiR3r5AIx9ENIRtbUKIxorRx8GMv4BxgVIZcieXbgSTN4DBZdRWdzs1Xxngu/90Xf79G0X+XGxZyXrYN7eFFNp0kUYj8kwZ1aS/dyR88scskumERWi1Hv5WUJrYGrDe7PcjNGsJ2jw0nNnRPKG87Y84aR4lQygyGBSlDcdrOBnBv0sHYJMxRvHSRkGgWpur06QIOGOk4oOipTXR/7E9cg3YQC5nvZAf2QiprwTa8IcOSFlZQPykEVRYSiAgXrgqBYcZzpX0hAGuIBv7DmPI2ORTF+t79Wbhxhnho3gGJleDv7Z96//sf1vQNCG6qOgeIc9ZY08Jm1AwXQoW0p6F1/XcEPxeyPDkXJzlojE9rjYNLCPL4gxb/LESEuUafm0U4JGMsZ6hnsBOw583yTuAdfQuJ9M+QaSyem6OVNkky3+DKAD3z0WJnl9jmGXIXigNSIxD25XhpvY+j9P0XTLBG1GT2Q+wXCIjSYJc2XnYcdgVJcLoxSWk1fKj/KPi7buAWtqwnL3tjeldpMMOZMliPUTWMM14zoGskHztt0JCkAtcotm9AQtvL8eZ2LHLDK/jyLzjv0wAwU5vzSVp14XHLZl7Q0AIoNc20p1EYGa9C/gSPd9CkrWZoG4lMOiAu3tp2PRLVrdXH3ZWSPQq4Ek5MczrUTkmB82XErNbOa8QB1Dw=
|
||||
file: .tox/dist/python-escpos*.zip
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: python-escpos/python-escpos
|
||||
branch: master
|
||||
condition: $TRAVIS_PYTHON_VERSION = "3.5"
|
19
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"esbonio.sphinx.confDir": "${workspaceFolder}/doc",
|
||||
"files.watcherExclude": {
|
||||
"**/.git/objects/**": true,
|
||||
"**/.git/subtree-cache/**": true,
|
||||
"**/node_modules/*/**": true,
|
||||
"**/.eggs/**": true,
|
||||
"**/.hypothesis/**": true,
|
||||
"**/.tox/**": true,
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"python.formatting.provider": "black",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
}
|
16
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "test with tox",
|
||||
"type": "shell",
|
||||
"command": "tox",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
27
AUTHORS
@@ -1,27 +1,54 @@
|
||||
Ahmed Tahri
|
||||
akeonly
|
||||
Alejandro Hernández
|
||||
Alexander Bougakov
|
||||
Alexandre Detiste
|
||||
Alex Debiasio
|
||||
Alfredo Orozco
|
||||
Asuki Kono
|
||||
belono
|
||||
B. Howell
|
||||
Brian
|
||||
Christoph Heuel
|
||||
Cody (Quantified Code Bot)
|
||||
csoft2k
|
||||
Curtis // mashedkeyboard
|
||||
Davis Goglin
|
||||
Dean Rispin
|
||||
dependabot[bot]
|
||||
Dmytro Katyukha
|
||||
Florent de Labarre
|
||||
Gerard Marull-Paretas
|
||||
Hark
|
||||
Joel Lehtonen
|
||||
Justin Vieira
|
||||
kennedy
|
||||
Kristi
|
||||
ldos
|
||||
Lucy Linder
|
||||
Manuel F Martinez
|
||||
Mathieu Poussin
|
||||
Maximilian Wagenbach
|
||||
Michael Billington
|
||||
Michael Elsdörfer
|
||||
mrwunderbar666
|
||||
Nathan Bookham
|
||||
Omer Akram
|
||||
Patrick Kanzler
|
||||
primax79
|
||||
Qian Linfeng
|
||||
Ramon Poca
|
||||
reck31
|
||||
Renato Lorenzi
|
||||
Ricardo Sánchez Alba
|
||||
Romain Porte
|
||||
Sam Cheng
|
||||
Sergio Pulgarin
|
||||
Stephan Sokolow
|
||||
Thijs Triemstra
|
||||
Thomas van den Berg
|
||||
tuxmaster
|
||||
vendryan
|
||||
Yaisel Hurtado
|
||||
ysuolmai
|
||||
白月秋见心
|
||||
|
309
CHANGELOG.rst
@@ -1,6 +1,299 @@
|
||||
*********
|
||||
Changelog
|
||||
*********
|
||||
=========
|
||||
|
||||
2023-12-17 - Version 3.1 - "Rubric Of Ruin"
|
||||
-------------------------------------------
|
||||
This is the minor release of the new version 3.1.
|
||||
It adds a modification of the API of the qr-method,
|
||||
hence the minor release.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- extend API of the qr-method to allow passing image
|
||||
parameters in non-native mode
|
||||
- use version 0.15 and upwards of python-barcode
|
||||
- fix an issue in the config provider that prevented
|
||||
config files to be found when only a path was supplied
|
||||
- Improve type annotations, usage of six and other
|
||||
packaging relevant parts.
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Patrick Kanzler
|
||||
- Alexandre Detiste
|
||||
- tuxmaster
|
||||
- belono
|
||||
|
||||
|
||||
2023-11-17 - Version 3.0 - "Quietly Confident"
|
||||
----------------------------------------------
|
||||
This is the major release of the new version 3.0.
|
||||
A big thank you to @belono for their many contributions
|
||||
for the finalization of v3!
|
||||
|
||||
The third major release of this library drops support for
|
||||
Python 2 and requires a Python version of at least 3.8.
|
||||
The API has been reworked to be more consistent and two
|
||||
new concepts have been introduced:
|
||||
|
||||
`Capabilities` allow the library to know which features
|
||||
the currently used printer implements and send fitting
|
||||
commands.
|
||||
`Magic Encode` is a new feature that uses the
|
||||
capability information and encodes Unicode automatically
|
||||
with the correct code page while sending also the
|
||||
code page change commands.
|
||||
|
||||
The license of the project has been changed to MIT
|
||||
in accordance with its contributors.
|
||||
|
||||
The changes listed here are a summary of the changes
|
||||
of the previous alpha releases. For details please
|
||||
read the changelog of the alpha releases.
|
||||
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- change the project's license to MIT in accordance with the contributors (see python-escpos/python-escpos#171)
|
||||
- feature: add "capabilities" which are shared with escpos-php, capabilities are stored in
|
||||
`escpos-printer-db <https://github.com/receipt-print-hq/escpos-printer-db>`_
|
||||
- feature: the driver tries now to guess the appropriate codepage and sets it automatically (called "magic encode")
|
||||
- as an alternative you can force the codepage with the old API
|
||||
- fix the encoding search so that lower encodings are found first
|
||||
- automatically handle cases where full cut or partial cut is not available
|
||||
- refactor of the set-method
|
||||
- preliminary support of POS "line display" printing
|
||||
- add support for software-based barcode-rendering
|
||||
- make feed for cut optional
|
||||
- implemented paper sensor querying command
|
||||
- Include support for CUPS based printer interfaces
|
||||
- add Win32Raw-Printer on Windows-platforms
|
||||
- add and improve Windows support of USB-class
|
||||
- pickle capabilities for faster startup
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
This is a list of contributors since the last v2 release.
|
||||
|
||||
- Ahmed Tahri
|
||||
- akeonly
|
||||
- Alexander Bougakov
|
||||
- AlexandroJaez
|
||||
- Asuki Kono
|
||||
- belono
|
||||
- brendanhowell
|
||||
- Brian
|
||||
- Christoph Heuel
|
||||
- csoft2k
|
||||
- Curtis // mashedkeyboard
|
||||
- Dmytro Katyukha
|
||||
- Foaly
|
||||
- Gerard Marull-Paretas
|
||||
- Justin Vieira
|
||||
- kedare
|
||||
- kennedy
|
||||
- Lucy Linder
|
||||
- Maximilian Wagenbach
|
||||
- Michael Billington
|
||||
- Michael Elsdörfer
|
||||
- mrwunderbar666
|
||||
- NullYing
|
||||
- Omer Akram
|
||||
- Patrick Kanzler
|
||||
- primax79
|
||||
- Ramon Poca
|
||||
- reck31
|
||||
- Romain Porte
|
||||
- Sam Cheng
|
||||
- Scott Rotondo
|
||||
- Sergio Pulgarin
|
||||
- Thijs Triemstra
|
||||
- Yaisel Hurtado
|
||||
- ysuolmai
|
||||
|
||||
and others
|
||||
|
||||
2023-05-11 - Version 3.0a9 - "Pride Comes Before A Fall"
|
||||
--------------------------------------------------------
|
||||
This release is the 10th alpha release of the new version 3.0.
|
||||
After three years hiatus, a new release is in work in order to
|
||||
finally get a version 3.0 out.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- Include support for CUPS based printer interfaces
|
||||
- Move the build tool chain to GitHub
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- belono
|
||||
- brendanhowell
|
||||
- AlexandroJaez
|
||||
- NullYing
|
||||
- kedare
|
||||
- Foaly
|
||||
- patkan
|
||||
- and others
|
||||
|
||||
2020-05-12 - Version 3.0a8 - "Only Slightly Bent"
|
||||
-------------------------------------------------
|
||||
This release is the ninth alpha release of the new version 3.0.
|
||||
Please be aware that the API is subject to change until v3.0 is
|
||||
released.
|
||||
|
||||
This release drops support for Python 2, requiring at least
|
||||
version 3.5 of Python.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- Drop support for Python 2 and mark in setuptools as only supporting 3.5 and upwards
|
||||
- remove landscape.io badge
|
||||
- replace viivakoodi with python-barcode which is maintained
|
||||
- add configuration for Visual Studio Code
|
||||
- use pkg_resources for the retrieval of the capabilities.json
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Romain Porte
|
||||
- Patrick Kanzler
|
||||
|
||||
2020-05-09 - Version 3.0a7 - "No Fixed Abode"
|
||||
---------------------------------------------
|
||||
This release is the eight alpha release of the new version 3.0.
|
||||
Please be aware that the API is subject to change until v3.0
|
||||
is released.
|
||||
|
||||
This release also marks the point at which the project transitioned
|
||||
to having only a master-branch (and not an additional development branch).
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- add Exception for NotImplementedError in detach_kernel_driver
|
||||
- update installation information
|
||||
- update and improve documentation
|
||||
- add error handling to image centering flag
|
||||
- update and fix tox and CI environment, preparing drop of support for Python 2
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Alexander Bougakov
|
||||
- Brian
|
||||
- Yaisel Hurtado
|
||||
- Maximilian Wagenbach
|
||||
- Patrick Kanzler
|
||||
|
||||
2019-06-19 - Version 3.0a6 - "Mistake not..."
|
||||
---------------------------------------------
|
||||
This release is the seventh alpha release of the new version 3.0.
|
||||
Please be aware that the API is subject to change until v3.0 is
|
||||
released.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- fix inclusion of the capabilities-file
|
||||
- execute CI jobs also on Windows and MacOS-targets
|
||||
- improve documentation
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Patrick Kanzler
|
||||
|
||||
2019-06-16 - Version 3.0a5 - "Lightly Seared On The Reality Grill"
|
||||
------------------------------------------------------------------
|
||||
This release is the sixth alpha release of the new version 3.0. Please
|
||||
be aware that the API is subject to change until v3.0 is released.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- allow arbitrary USB arguments in USB-class
|
||||
- add Win32Raw-Printer on Windows-platforms
|
||||
- add and improve Windows support of USB-class
|
||||
- use pyyaml safe_load()
|
||||
- improve doc
|
||||
- implement _read method of Network printer class
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Patrick Kanzler
|
||||
- Gerard Marull-Paretas
|
||||
- Ramon Poca
|
||||
- akeonly
|
||||
- Omer Akram
|
||||
- Justin Vieira
|
||||
|
||||
2018-05-15 - Version 3.0a4 - "Kakistocrat"
|
||||
------------------------------------------
|
||||
This release is the fifth alpha release of the new version 3.0. Please
|
||||
be aware that the API will still change until v3.0 is released.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- raise exception when TypeError occurs in cashdraw (#268)
|
||||
- Feature/clear content in dummy printer (#271)
|
||||
- fix is_online() (#282)
|
||||
- improve documentation
|
||||
- Modified submodule to always pull from master branch (#283)
|
||||
- parameter for implementation of nonnative qrcode (#289)
|
||||
- improve platform independence (#296)
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Christoph Heuel
|
||||
- Patrick Kanzler
|
||||
- kennedy
|
||||
- primax79
|
||||
- reck31
|
||||
- Thijs Triemstra
|
||||
|
||||
2017-10-08 - Version 3.0a3 - "Just Testing"
|
||||
-------------------------------------------
|
||||
This release is the fourth alpha release of the new version 3.0. Please
|
||||
be aware that the API will still change until v3.0 is released.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- minor changes in documentation, tests and examples
|
||||
- pickle capabilities for faster startup
|
||||
- first implementation of centering images and QR
|
||||
- check barcodes based on regex
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Patrick Kanzler
|
||||
- Lucy Linder
|
||||
- Romain Porte
|
||||
- Sergio Pulgarin
|
||||
|
||||
2017-08-04 - Version 3.0a2 - "It's My Party And I'll Sing If I Want To"
|
||||
-----------------------------------------------------------------------
|
||||
This release is the third alpha release of the new version 3.0. Please
|
||||
be aware that the API will still change until v3.0 is released.
|
||||
|
||||
changes
|
||||
^^^^^^^
|
||||
- refactor of the set-method
|
||||
- preliminary support of POS "line display" printing
|
||||
- improvement of tests
|
||||
- added ImageWidthError
|
||||
- list authors in repository
|
||||
- add support for software-based barcode-rendering
|
||||
- fix SerialException when trying to close device on __del__
|
||||
- added the DLE EOT querying command for USB and Serial
|
||||
- ensure QR codes have a large enough border
|
||||
- make feed for cut optional
|
||||
- fix the behavior of horizontal tabs
|
||||
- added test script for hard an soft barcodes
|
||||
- implemented paper sensor querying command
|
||||
- added weather forecast example script
|
||||
- added a method for simpler newlines
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- csoft2k
|
||||
- Patrick Kanzler
|
||||
- mrwunderbar666
|
||||
- Romain Porte
|
||||
- Ahmed Tahri
|
||||
|
||||
2017-03-29 - Version 3.0a1 - "Headcrash"
|
||||
----------------------------------------
|
||||
@@ -33,7 +326,7 @@ changes
|
||||
- feature: the driver tries now to guess the appropriate codepage and sets it automatically (called "magic encode")
|
||||
- as an alternative you can force the codepage with the old API
|
||||
- updated and improved documentation
|
||||
- changed constructor of main class due to introduction of capablities
|
||||
- changed constructor of main class due to introduction of capabilities
|
||||
- changed interface of method `blocktext`, changed behavior of multiple methods, for details refer to the documentation
|
||||
on `python-escpos.readthedocs.io <https://python-escpos.readthedocs.io>`_
|
||||
- add support for custom cash drawer sequence
|
||||
@@ -112,8 +405,8 @@ changes
|
||||
- packaging: configured the coverage-analysis codecov.io
|
||||
- GitHub: improved issues-template
|
||||
- documentation: add troubleshooting tip to network-interface
|
||||
- the module, cli and documentation is now aware of the version of python-escpos
|
||||
- the cli does now support basic tabcompletion
|
||||
- the module, CLI and documentation is now aware of the version of python-escpos
|
||||
- the CLI does now support basic tab completion
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
@@ -131,7 +424,7 @@ changes
|
||||
- refactor complete code in order to be compatible with Python 2 and 3
|
||||
- modernize packaging
|
||||
- add testing and CI
|
||||
- merge various forks into codebase, fixing multiple issues with barcode-, QR-printing, cashdraw and structure
|
||||
- merge various forks into codebase, fixing multiple issues with barcode-, QR-printing, cash-draw and structure
|
||||
- improve the documentation
|
||||
- extend support of barcode-codes to type B
|
||||
- add function to disable panel-buttons
|
||||
@@ -202,7 +495,7 @@ contributors
|
||||
--------------------------
|
||||
|
||||
- Issue #5: Fixed vertical tab
|
||||
- Issue #9: Fixed identation inconsistence
|
||||
- Issue #9: Fixed indentation inconsistency
|
||||
|
||||
2013-03-14 - Version 1.0.1
|
||||
--------------------------
|
||||
@@ -213,6 +506,6 @@ contributors
|
||||
2012-11-15 - Version 1.0
|
||||
------------------------
|
||||
|
||||
- Issue #2: Added ethernet support
|
||||
- Issue #2: Added Ethernet support
|
||||
- Issue #3: Added compatibility with libusb-1.0.1
|
||||
- Issue #4: Fixed typo in escpos.py
|
||||
|
102
CONTRIBUTING.rst
@@ -1,87 +1,83 @@
|
||||
************
|
||||
Contributing
|
||||
************
|
||||
============
|
||||
|
||||
This project is open to any kind of contribution. You can help with improving the documentation, adding fixes to the
|
||||
code, providing test cases in code or as a description or just spreading the word. Please feel free to create an
|
||||
issue or pull request.
|
||||
In order to reduce the amount of work for everyone please try to adhere to good practice.
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
The pull requests and issues will be prefilled with templates. Please fill in your information where applicable.
|
||||
This project is open to any kind of contribution.
|
||||
You can help with improving the documentation, adding fixes to the
|
||||
code, providing test cases in code or as a description or just
|
||||
spreading the word.
|
||||
Please feel free to create an issue or pull request.
|
||||
In order to reduce the amount of work for everyone please try
|
||||
to adhere to good practice.
|
||||
|
||||
This project uses `semantic versioning <http://semver.org/>`_ and tries to adhere to the proposed rules as
|
||||
well as possible.
|
||||
The pull requests and issues will be prefilled with templates.
|
||||
Please fill in your information where applicable.
|
||||
|
||||
This project uses `semantic versioning <https://semver.org/>`_
|
||||
and tries to adhere to the proposed rules as well as possible.
|
||||
|
||||
Author-list
|
||||
-----------
|
||||
|
||||
This project keeps a list of authors. This can be auto-generated by calling `./doc/generate-authors.sh`.
|
||||
When contributing the first time, please include a commit with the output of this script in place.
|
||||
Otherwise the integration-check will fail.
|
||||
This project keeps a list of authors.
|
||||
This can be auto-generated by calling `./doc/generate-authors.sh`.
|
||||
When contributing the first time, please include a commit with
|
||||
the output of this script in place.
|
||||
|
||||
When you change your username or mail-address, please also update the `.mailmap` and the authors-list.
|
||||
When you change your username or mail-address, please also
|
||||
update the `.mailmap` and the authors-list.
|
||||
You can find a good documentation on the mapping-feature in the
|
||||
`documentation of git-shortlog <https://git-scm.com/docs/git-shortlog#_mapping_authors>`_.
|
||||
|
||||
Style-Guide
|
||||
-----------
|
||||
|
||||
When writing code please try to stick to these rules.
|
||||
|
||||
Python 2 and 3
|
||||
^^^^^^^^^^^^^^
|
||||
We have rewritten the code in order to maintain compatibility with both Python 2 and Python 3.
|
||||
In order to ensure that we do not miss any accidental degradation, please add these imports to the top
|
||||
of every file of code:
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
Furthermore please be aware of the differences between Python 2 and 3. For
|
||||
example `this guide <https://docs.python.org/3/howto/pyporting.html>`_ is helpful.
|
||||
Special care has to be taken when dealing with strings and byte-strings. Please note
|
||||
that the :py:meth:`~escpos.escpos.Escpos._raw`-method only accepts byte-strings.
|
||||
Often you can achieve compatibility quite easily with a tool from the `six`-package.
|
||||
|
||||
PEP8
|
||||
^^^^
|
||||
This is not yet consequently done in every piece of code, but please try to ensure
|
||||
that your code honors PEP8.
|
||||
The checks by Landscape and QuantifiedCode that run on every PR will provide you with hints.
|
||||
Black Code Style
|
||||
^^^^^^^^^^^^^^^^
|
||||
This project is formatted with the auto formatter `black <https://github.com/psf/black>`_.
|
||||
Please format your contributions with black, otherwise they will be rejected.
|
||||
|
||||
GIT
|
||||
^^^
|
||||
The master-branch contains code that has been released to PyPi. A release is marked with a tag
|
||||
corresponding to the version. Issues are closed when they have been resolved in the development-branch.
|
||||
The master-branch contains the main development of the project.
|
||||
A release to PyPi is marked with a tag corresponding to the version.
|
||||
Issues are closed when they have been resolved by merging
|
||||
into the master-branch.
|
||||
When you have a change to make, begin by creating a new branch
|
||||
from the HEAD of `python-escpos/master`.
|
||||
|
||||
When you have a change to make, begin by creating a new branch from the HEAD of `python-escpos/development`.
|
||||
Name your branch to indicate what you are trying to achieve. Good branch names might
|
||||
be `improve/text-handling`, `feature/enable-color-printing`.
|
||||
|
||||
Please try to group your commits into logical units. If you need to tidy up your branch, you can make use of a
|
||||
git feature called an 'interactive rebase' before making a pull request. A small, self-contained change-set is
|
||||
easier to review, and improves the chance of your code being merged.
|
||||
Please also make sure that before creating your PR, your branch is rebased on a recent commit or you merged a recent
|
||||
commit into your branch. This way you can ensure that your PR is without merge conflicts.
|
||||
Please try to group your commits into logical units.
|
||||
If you need to tidy up your branch, you can make use of a
|
||||
git feature called an 'interactive rebase' before making a pull request.
|
||||
A small, self-contained change-set is easier to review, and
|
||||
improves the chance of your code being merged.
|
||||
Please also make sure that before creating your PR, your branch
|
||||
is rebased on a recent commit or you merged a recent
|
||||
commit into your branch.
|
||||
This way you can ensure that your PR is without merge conflicts.
|
||||
|
||||
Docstrings
|
||||
^^^^^^^^^^
|
||||
This project tries to have a good documentation.
|
||||
Please add a docstring to every method and class. Have a look at existing methods and classes for the style.
|
||||
Please add a docstring to every method and class.
|
||||
Have a look at existing methods and classes for the style.
|
||||
We use basically standard rst-docstrings for Sphinx.
|
||||
|
||||
Test
|
||||
^^^^
|
||||
Try to write tests whenever possible. Our goal for the future is 100% coverage.
|
||||
We are currently using `nose` but might change in the future.
|
||||
You can copy the structure from other testcases. Please remember to adapt the docstrings.
|
||||
Try to write tests whenever possible.
|
||||
Our goal for the future is 100% coverage.
|
||||
You can copy the structure from other testcases.
|
||||
Please remember to adapt the docstrings.
|
||||
|
||||
Further reading
|
||||
^^^^^^^^^^^^^^^
|
||||
For further best practices and hints on contributing please see the
|
||||
`contribution-guide <http://www.contribution-guide.org/>`_. Should there be any contradictions between this guide
|
||||
`contribution-guide <https://www.contribution-guide.org/>`_.
|
||||
Should there be any contradictions between this guide
|
||||
and the linked one, please stick to this text.
|
||||
Aside from that feel free to create an issue or write an email if anything is unclear.
|
||||
|
||||
|
23
INSTALL
@@ -1,23 +1,10 @@
|
||||
python-escpos
|
||||
=============
|
||||
|
||||
Ensure the library is installed on ${lib_arch}/${python_ver}/site-packages/escpos
|
||||
This library is available over pypi. So for most of the use-cases it should be sufficient to run
|
||||
|
||||
On CLi you must run:
|
||||
# python setup.py build
|
||||
# sudo python setup.py install
|
||||
```
|
||||
pip install python-escpos[all] --user # add --pre if you want to install pre-releases
|
||||
```
|
||||
|
||||
On Linux, ensure you belongs to the proper group so you can have access to the printer.
|
||||
This can be done, by adding yourself to 'dialout' group, this might require to re-login
|
||||
so the changes make effect.
|
||||
|
||||
Then, add the following rule to /etc/udev/rules.d/99-escpos.rules
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="04b8", ATTRS{idProduct}=="0202", MODE="0664", GROUP="dialout"
|
||||
|
||||
and restar udev rules.
|
||||
# sudo service udev restart
|
||||
|
||||
Enjoy !!!
|
||||
And please, don't forget to ALWAYS add Epson.cut() at the end of your printing :)
|
||||
|
||||
Manuel F Martinez <manpaz@bashlinux.com>
|
||||
For more information please read the documentation at https://python-escpos.readthedocs.io/en/latest/user/installation.html
|
||||
|
@@ -1,10 +1,10 @@
|
||||
include *.rst
|
||||
include *.txt
|
||||
include COPYING
|
||||
include LICENSE
|
||||
include INSTALL
|
||||
include tox.ini
|
||||
include capabilities-data/dist/capabilities.json
|
||||
include src/escpos/capabilities.json
|
||||
recursive-include doc *.bat
|
||||
recursive-include doc *.ico
|
||||
recursive-include doc *.py
|
||||
|
80
README.rst
@@ -2,34 +2,17 @@
|
||||
python-escpos - Python library to manipulate ESC/POS Printers
|
||||
#############################################################
|
||||
|
||||
.. image:: https://travis-ci.org/python-escpos/python-escpos.svg?branch=master
|
||||
:target: https://travis-ci.org/python-escpos/python-escpos
|
||||
:alt: Continous Integration
|
||||
|
||||
.. image:: https://www.quantifiedcode.com/api/v1/project/95748b89a3974700800b85e4ed3d32c4/badge.svg
|
||||
:target: https://www.quantifiedcode.com/app/project/95748b89a3974700800b85e4ed3d32c4
|
||||
:alt: Code issues
|
||||
|
||||
.. image:: https://landscape.io/github/python-escpos/python-escpos/master/landscape.svg?style=flat
|
||||
:target: https://landscape.io/github/python-escpos/python-escpos/master
|
||||
:alt: Code Health
|
||||
|
||||
.. image:: https://codecov.io/github/python-escpos/python-escpos/coverage.svg?branch=master
|
||||
:target: https://codecov.io/github/python-escpos/python-escpos?branch=master
|
||||
:alt: Code Coverage
|
||||
|
||||
.. image:: https://readthedocs.org/projects/python-escpos/badge/?version=stable
|
||||
:target: http://python-escpos.readthedocs.io/en/latest/?badge=stable
|
||||
:alt: Documentation Status
|
||||
|
||||
|
||||
Description
|
||||
-----------
|
||||
===========
|
||||
|
||||
.. image:: https://readthedocs.org/projects/python-escpos/badge/?version=latest
|
||||
:target: https://python-escpos.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
||||
Python ESC/POS is a library which lets the user have access to all those printers handled
|
||||
by ESC/POS commands, as defined by Epson, from a Python application.
|
||||
|
||||
The library tries to implement the functions provided by the ESC/POS-commandset and supports sending text, images,
|
||||
The library tries to implement the functions provided by the ESC/POS-command-set and supports sending text, images,
|
||||
barcodes and qr-codes to the printer.
|
||||
|
||||
Text can be aligned/justified and fonts can be changed by size, type and weight.
|
||||
@@ -51,7 +34,7 @@ This library makes use of:
|
||||
* `Pillow <https://github.com/python-pillow/Pillow>`_ for image printing
|
||||
* `qrcode <https://github.com/lincolnloop/python-qrcode>`_ for the generation of QR-codes
|
||||
* `pyserial <https://github.com/pyserial/pyserial>`_ for serial printers
|
||||
* `viivakoodi <https://github.com/kxepal/viivakoodi>`_ for the generation of barcodes
|
||||
* `python-barcode <https://github.com/WhyNotHugo/python-barcode>`_ for the generation of barcodes
|
||||
|
||||
Documentation and Usage
|
||||
-----------------------
|
||||
@@ -66,12 +49,55 @@ The basic usage is:
|
||||
p = Usb(0x04b8, 0x0202, 0, profile="TM-T88III")
|
||||
p.text("Hello World\n")
|
||||
p.image("logo.gif")
|
||||
p.barcode('1324354657687', 'EAN13', 64, 2, '', '')
|
||||
p.barcode('4006381333931', 'EAN13', 64, 2, '', '')
|
||||
p.cut()
|
||||
|
||||
The full project-documentation is available on `Read the Docs <https://python-escpos.readthedocs.io>`_.
|
||||
|
||||
Another example based on the Network printer class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from escpos.printer import Network
|
||||
|
||||
kitchen = Network("192.168.1.100") #Printer IP Address
|
||||
kitchen.text("Hello World\n")
|
||||
kitchen.barcode('4006381333931', 'EAN13', 64, 2, '', '')
|
||||
kitchen.cut()
|
||||
|
||||
Another example based on the Serial printer class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from escpos.printer import Serial
|
||||
|
||||
""" 9600 Baud, 8N1, Flow Control Enabled """
|
||||
p = Serial(devfile='/dev/tty.usbserial',
|
||||
baudrate=9600,
|
||||
bytesize=8,
|
||||
parity='N',
|
||||
stopbits=1,
|
||||
timeout=1.00,
|
||||
dsrdtr=True)
|
||||
|
||||
p.text("Hello World\n")
|
||||
p.qr("You can readme from your smartphone")
|
||||
p.cut()
|
||||
|
||||
|
||||
The full project-documentation is available on
|
||||
`Read the Docs <https://python-escpos.readthedocs.io>`_.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
This project is open for any contribution! Please see CONTRIBUTING.rst for more information.
|
||||
This project is open for any contribution! Please see
|
||||
`CONTRIBUTING.rst <https://python-escpos.readthedocs.io/en/latest/dev/contributing.html>`_
|
||||
for more information.
|
||||
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
None of the vendors cited in this project agree or endorse any of the
|
||||
patterns or implementations.
|
||||
Its names are used only to maintain context.
|
||||
|
14
codecov.yml
@@ -3,11 +3,15 @@ codecov:
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default: # status context
|
||||
target: auto
|
||||
threshold: "1%"
|
||||
project: off
|
||||
patch: off
|
||||
changes: off
|
||||
range: "60...100"
|
||||
|
||||
comment: off
|
||||
comment:
|
||||
layout: " diff, flags, files"
|
||||
behavior: default
|
||||
require_changes: false # if true: only post the comment if coverage changes
|
||||
require_base: false # [true :: must have a base report to post]
|
||||
require_head: true # [true :: must have a head report to post]
|
||||
|
||||
|
10
doc/Makefile
@@ -9,7 +9,7 @@ BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
@@ -19,7 +19,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext spelling
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@@ -45,6 +45,7 @@ help:
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " spelling to run the spellchecker"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
@@ -175,3 +176,8 @@ pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
||||
spelling:
|
||||
$(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling
|
||||
@echo
|
||||
@echo "Spellchecker finished."
|
||||
|
@@ -0,0 +1,28 @@
|
||||
{% for item in data.encodings %}
|
||||
{% set encoding = data.encodings[item] %}
|
||||
{% macro draw_with_underline(text, symbol='-') -%}
|
||||
{{ escape_rst(text) }}
|
||||
{{ escape_rst(text) | length * symbol }}
|
||||
{%- endmacro %}
|
||||
|
||||
{{ '.. _encoding-label-' + item + ':' }}
|
||||
|
||||
{{ draw_with_underline(encoding.name) }}
|
||||
|
||||
{{ escape_rst(encoding.notes) }}
|
||||
|
||||
Mapping Information
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
====================== ================================================================
|
||||
identifier {{ escape_rst(item) }}
|
||||
Name {{ escape_rst(encoding.name|default('Unknown')) }}
|
||||
Iconv Name {{ escape_rst(encoding.iconv|default('Unknown')) }}
|
||||
``python_encode`` Name {{ escape_rst(encoding.python_encode|default('Unknown')) }}
|
||||
====================== ================================================================
|
||||
|
||||
{% if encoding.data is defined %}
|
||||
{{ draw_with_underline('Code page data', symbol='^') }}
|
||||
{{ encoding.data }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
83
doc/capability_templates/capabilities-template.jinja
Normal file
@@ -0,0 +1,83 @@
|
||||
{% for item in data.profiles %}
|
||||
{% set printer = data.profiles[item] %}
|
||||
{% macro draw_with_underline(text, symbol='-') -%}
|
||||
{{ escape_rst(text) }}
|
||||
{{ escape_rst(text) | length * symbol }}
|
||||
{%- endmacro %}
|
||||
{% macro abort(error) %}
|
||||
{{ None['[ERROR] ' ~ error][0] }}
|
||||
{% endmacro %}
|
||||
{% macro fill_line(text, total, symbol=' ') -%}
|
||||
{%- if total < text|length -%}
|
||||
{{- abort("Line cannot be filled: must be longer") -}}
|
||||
{%- endif -%}
|
||||
{{- text + ((total - text|length ) * symbol ) -}}
|
||||
{%- endmacro %}
|
||||
|
||||
{{ '.. _printer-label-' + item + ':' }}
|
||||
|
||||
{{ draw_with_underline(printer.name) }}
|
||||
{{ escape_rst(printer.notes) }}
|
||||
|
||||
You can select this profile in python-escpos with this identifier: ``{{ item }}``.
|
||||
(Set parameter to `profile='{{ item }}'`.)
|
||||
|
||||
Basic information
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
====================== ================================================================
|
||||
Name {{ escape_rst(printer.name|default('Unknown')) }}
|
||||
Vendor {{ escape_rst(printer.vendor|default('Unknown')) }}
|
||||
Media width (mm) {{ escape_rst(printer.media.width.mm|default('Unknown')|string) }}
|
||||
Media width (pixels) {{ escape_rst(printer.media.width.pixels|default('Unknown')|string) }}
|
||||
DPI {{ escape_rst(printer.media.dpi|default('Unknown')|string) }}
|
||||
====================== ================================================================
|
||||
|
||||
Fonts
|
||||
^^^^^
|
||||
|
||||
+------------------+------------------------------+-----------------------+
|
||||
| ID | Name | Columns |
|
||||
+==================+==============================+=======================+
|
||||
{% for id in printer.fonts -%}
|
||||
| {{ fill_line(escape_rst(id), 16) }} | {{ fill_line(escape_rst(printer.fonts[id].name), 28) }} | {{ fill_line(printer.fonts[id].columns|string, 21) }} |
|
||||
+------------------+------------------------------+-----------------------+
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Colors
|
||||
^^^^^^
|
||||
|
||||
+------------------+----------------------------------------------------------------+
|
||||
| ID | Color |
|
||||
+==================+================================================================+
|
||||
{% for id in printer.colors -%}
|
||||
| {{ fill_line(escape_rst(id), 16) }} | {{ fill_line(escape_rst(printer.colors[id]), 62) }} |
|
||||
+------------------+----------------------------------------------------------------+
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Feature support
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
+-----------------------------------+----------------------------+
|
||||
| Feature | Supported |
|
||||
+===================================+============================+
|
||||
{% for feature in printer.features -%}
|
||||
| {{ fill_line(escape_rst(feature), 33) }} | {{ fill_line(escape_rst(printer.features[feature]|string), 26) }} |
|
||||
+-----------------------------------+----------------------------+
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Text code pages
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
+------------------+----------------------------------------------------------------+
|
||||
| ID | Encoding |
|
||||
+==================+================================================================+
|
||||
{% for id in printer.codePages -%}
|
||||
| {{ fill_line(escape_rst(id), 16) }} | {{ fill_line(':ref:`encoding-label-'+printer.codePages[id]+'`', 62) }} |
|
||||
+------------------+----------------------------------------------------------------+
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
129
doc/conf.py
@@ -12,19 +12,17 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
on_rtd = os.getenv('READTHEDOCS') == 'True'
|
||||
if on_rtd:
|
||||
import escpos
|
||||
else:
|
||||
from setuptools_scm import get_version
|
||||
import sys
|
||||
from importlib.metadata import version as imp_version
|
||||
|
||||
on_rtd = os.getenv("READTHEDOCS") == "True"
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../src'))
|
||||
root = os.path.relpath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, os.path.abspath("../src"))
|
||||
root = os.path.relpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
@@ -35,52 +33,55 @@ root = os.path.relpath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx_autodoc_typehints",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.graphviz",
|
||||
"sphinx.ext.inheritance_diagram",
|
||||
"sphinx.ext.imgconverter",
|
||||
"sphinxarg.ext",
|
||||
"sphinxcontrib.datatemplates",
|
||||
"sphinxcontrib.spelling",
|
||||
]
|
||||
|
||||
# mock the following modules for autodoc
|
||||
autodoc_mock_imports = ["qrcode"]
|
||||
|
||||
# supress warnings for external images
|
||||
suppress_warnings = [
|
||||
'image.nonlocal_uri',
|
||||
"image.nonlocal_uri",
|
||||
]
|
||||
|
||||
# enable todos
|
||||
todo_include_todos = True
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates", "capability_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'python-escpos'
|
||||
copyright = u'2016, Manuel F Martinez and others'
|
||||
project = "python-escpos"
|
||||
copyright = "2023, python-escpos developers"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
if on_rtd:
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = escpos.__version__
|
||||
else:
|
||||
# locally setuptools_scm should work
|
||||
release = get_version(root=root)
|
||||
release = imp_version("python-escpos")
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2]) # The short X.Y version.
|
||||
version = ".".join(release.split(".")[:2])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -94,7 +95,7 @@ version = '.'.join(release.split('.')[:2]) # The short X.Y version.
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
@@ -112,7 +113,7 @@ exclude_patterns = ['_build']
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
@@ -126,15 +127,17 @@ pygments_style = 'sphinx'
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
if on_rtd:
|
||||
html_theme = 'default'
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
print("recognized execution on RTD")
|
||||
else:
|
||||
try:
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
except ImportError:
|
||||
print("no sphinx_rtd_theme found, switching to nature")
|
||||
html_theme = 'default'
|
||||
html_theme = "default"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@@ -158,12 +161,12 @@ else:
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = 'pyescpos.ico'
|
||||
html_favicon = "pyescpos.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
@@ -212,7 +215,7 @@ html_static_path = ['_static']
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'python-escposdoc'
|
||||
htmlhelp_basename = "python-escposdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@@ -220,20 +223,25 @@ htmlhelp_basename = 'python-escposdoc'
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
latex_engine = "xelatex"
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'python-escpos.tex', u'python-escpos Documentation',
|
||||
u'Manuel F Martinez and others', 'manual'),
|
||||
(
|
||||
"index",
|
||||
"python-escpos.tex",
|
||||
"python-escpos Documentation",
|
||||
"python-escpos developers",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -262,8 +270,13 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'python-escpos', u'python-escpos Documentation',
|
||||
[u'Manuel F Martinez and others'], 1)
|
||||
(
|
||||
"index",
|
||||
"python-escpos",
|
||||
"python-escpos Documentation",
|
||||
["python-escpos developers"],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@@ -276,9 +289,15 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'python-escpos', u'python-escpos Documentation',
|
||||
u'Manuel F Martinez and others', 'python-escpos', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
"python-escpos",
|
||||
"python-escpos Documentation",
|
||||
"python-escpos developers",
|
||||
"python-escpos",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -292,3 +311,21 @@ texinfo_documents = [
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# spellchecker
|
||||
spelling_ignore_pypi_package_names = True
|
||||
spelling_ignore_wiki_words = True
|
||||
spelling_ignore_python_builtins = True
|
||||
spelling_ignore_importable_modules = True
|
||||
spelling_ignore_contributor_names = True
|
||||
spelling_word_list_filename = ["spelling_wordlist.txt", "../AUTHORS"]
|
||||
spelling_show_suggestions = True
|
||||
spelling_suggestion_limit = 3
|
||||
spelling_warning = True
|
||||
spelling_exclude_patterns = [
|
||||
"**/capabilities.json",
|
||||
"../../capabilities-data/dist/capabilities.json",
|
||||
"**/available-encodings.rst",
|
||||
"**/available-profiles.rst",
|
||||
"dev/todo.rst",
|
||||
]
|
||||
|
11
doc/dev/release-process.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
Release process
|
||||
===============
|
||||
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
* Update authors file
|
||||
* Update changelog
|
||||
* Set annotated tag for release and push to public github
|
||||
* Build wheel
|
||||
* Load wheel to PyPi
|
||||
* Prepare project for next release with an empty changelog entry
|
17
doc/dev/repository.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
.. _developer-manual-repository:
|
||||
|
||||
Repository
|
||||
==========
|
||||
|
||||
:Last Reviewed: 2023-09-05
|
||||
|
||||
This project uses sub-projects and retrieves its versioning
|
||||
information from version control.
|
||||
Therefore it is crucial that you follow these rules when
|
||||
working with the project (e.g. for packaging a
|
||||
development version).
|
||||
|
||||
* Make sure that the git project is complete. A call to git status for example should succeed.
|
||||
* Make sure that you have checked out all available sub-projects.
|
||||
* Proper initialization of submodules can be ensured with ``git submodule update --init --recursive``
|
||||
|
15
doc/dev/todo.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
TODO
|
||||
====
|
||||
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
Open points and issues of the project are tracked in the GitHub issues.
|
||||
Some annotations still remain in the code and should be moved over time
|
||||
into the issue tracker.
|
||||
|
||||
Todos in the codebase
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. todolist::
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
GENLIST=$(git shortlog -s -n | cut -f2 | sort)
|
||||
GENLIST=$(git shortlog -s -n | cut -f2 | sort -f)
|
||||
GENLIST_W_MAIL=$(git shortlog -s -e -n | cut -f2 | sort -f)
|
||||
AUTHORSFILE="$(dirname $0)/../AUTHORS"
|
||||
TEMPAUTHORSFILE="/tmp/python-escpos-authorsfile"
|
||||
|
||||
@@ -12,7 +13,9 @@ if [ "$#" -eq 1 ]
|
||||
echo "\nNew authorsfile:\n"
|
||||
cat $TEMPAUTHORSFILE
|
||||
echo "\nUsing diff on files...\n"
|
||||
diff -q --from-file $AUTHORSFILE $TEMPAUTHORSFILE
|
||||
diff --suppress-common-lines -b --from-file $AUTHORSFILE $TEMPAUTHORSFILE
|
||||
echo "Authors with mail addresses:\n"
|
||||
echo "$GENLIST_W_MAIL"
|
||||
else
|
||||
echo "$GENLIST">$AUTHORSFILE
|
||||
fi
|
||||
|
@@ -5,8 +5,15 @@
|
||||
|
||||
.. include:: ../README.rst
|
||||
|
||||
#######
|
||||
Content
|
||||
-------
|
||||
#######
|
||||
|
||||
User Documentation
|
||||
==================
|
||||
|
||||
This chapter describes the central points that
|
||||
are relevant to the user of this library.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
@@ -16,16 +23,46 @@ Content
|
||||
user/methods
|
||||
user/printers
|
||||
user/raspi
|
||||
user/todo
|
||||
user/usage
|
||||
user/cli-user
|
||||
user/barcode
|
||||
|
||||
Printer profiles
|
||||
================
|
||||
|
||||
This chapter gives a listing of the available
|
||||
printer profiles. Details are described in
|
||||
:ref:`capabilities-profile-intro`.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Printer profiles
|
||||
|
||||
printer_profiles/capabilities
|
||||
printer_profiles/available-profiles
|
||||
printer_profiles/available-encodings
|
||||
|
||||
Developer Documentation
|
||||
=======================
|
||||
|
||||
This chapter summarizes information for
|
||||
developers of this library.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Developer Documentation
|
||||
|
||||
dev/release-process
|
||||
dev/contributing
|
||||
dev/repository
|
||||
dev/changelog
|
||||
dev/todo
|
||||
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
This chapter contains an auto-generated
|
||||
documentation of the API of this library.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
@@ -43,8 +80,9 @@ Content
|
||||
api/codepages
|
||||
api/katakana
|
||||
|
||||
##################
|
||||
Indices and tables
|
||||
==================
|
||||
##################
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
|
@@ -56,7 +56,7 @@ if errorlevel 9009 (
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
echo.https://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
|
11
doc/printer_profiles/available-encodings.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
Available Encodings
|
||||
-------------------
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
If you find any issues with the described encodings,
|
||||
please open an issue in the
|
||||
`ESC/POS printer database <https://github.com/receipt-print-hq/escpos-printer-db>`_.
|
||||
The data shown here is directly taken from there.
|
||||
|
||||
.. datatemplate:json:: ../../capabilities-data/dist/capabilities.json
|
||||
:template: capabilities-template-encoding.jinja
|
18
doc/printer_profiles/available-profiles.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
.. _available-profiles:
|
||||
|
||||
Available Profiles
|
||||
------------------
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
The following list describes which printer profiles are
|
||||
available in this release.
|
||||
The existence of a profile is a hint, but no guarantee
|
||||
that this printer actually can be controlled by this library.
|
||||
|
||||
If you find any issues with the described capabilities,
|
||||
please open an issue in the
|
||||
`ESC/POS printer database <https://github.com/receipt-print-hq/escpos-printer-db>`_.
|
||||
The data shown here is directly taken from there.
|
||||
|
||||
.. datatemplate:json:: ../../capabilities-data/dist/capabilities.json
|
||||
:template: capabilities-template.jinja
|
26
doc/printer_profiles/capabilities.rst
Normal file
@@ -0,0 +1,26 @@
|
||||
.. _capabilities-profile-intro:
|
||||
|
||||
Capabilities
|
||||
------------
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
Since the used command set often differs between printers,
|
||||
a model for supporting different printers is implemented.
|
||||
This feature is called `capabilities`.
|
||||
|
||||
The `capabilities`-feature allows this library to know
|
||||
which features are supported.
|
||||
If no further information is specified, python-escpos will
|
||||
try to automatically use features based on the supplied information.
|
||||
|
||||
In order to use the `capabilities`-database, the printer instance
|
||||
simply has to be created with the parameter `profile` set to the
|
||||
relevant identifier.
|
||||
The identifier can be found in :ref:`available-profiles`.
|
||||
|
||||
This documentation describes the profiles in the database file that
|
||||
is bundled with this release.
|
||||
If another configuration is to be used, this chapter can be followed
|
||||
for information on how to side-load another `capabilities`-database:
|
||||
:ref:`advanced-usage-change-capabilities-profile`.
|
||||
|
@@ -2,6 +2,15 @@ pyusb
|
||||
Pillow>=2.0
|
||||
qrcode>=4.0
|
||||
pyserial
|
||||
sphinx-rtd-theme
|
||||
sphinx-rtd-theme==2.0.0
|
||||
setuptools
|
||||
setuptools-scm
|
||||
docutils>=0.12
|
||||
sphinxcontrib-spelling>=7.2.0
|
||||
python-barcode>=0.15.0,<1
|
||||
importlib-metadata
|
||||
importlib_resources
|
||||
sphinxcontrib.datatemplates
|
||||
sphinx-argparse
|
||||
sphinx-autodoc-typehints
|
||||
pycups
|
||||
|
130
doc/spelling_wordlist.txt
Normal file
@@ -0,0 +1,130 @@
|
||||
ESC
|
||||
Esc
|
||||
POS
|
||||
Pos
|
||||
Escpos
|
||||
Escpos
|
||||
escpos
|
||||
|
||||
bitImageColumn
|
||||
bitImageRaster
|
||||
ep
|
||||
pc
|
||||
cp
|
||||
|
||||
csoft
|
||||
Frédéric
|
||||
Headcrash
|
||||
Krispy
|
||||
primax
|
||||
Tahri
|
||||
reck
|
||||
mrwunderbar
|
||||
zouppen
|
||||
kedare
|
||||
Foaly
|
||||
brendanhowell
|
||||
der
|
||||
fvdsn
|
||||
Marull
|
||||
Paretas
|
||||
Kakistocrat
|
||||
Billington
|
||||
patkan
|
||||
Romain
|
||||
Bougakov
|
||||
Yaisel
|
||||
Hurtado
|
||||
Wagenbach
|
||||
Poca
|
||||
Akram
|
||||
Vieira
|
||||
Christoph
|
||||
Heuel
|
||||
Thijs
|
||||
Triemstra
|
||||
Linder
|
||||
Romain
|
||||
Pulgarin
|
||||
Romain
|
||||
Cheng
|
||||
Dmytro
|
||||
Katyukha
|
||||
Elsdörfer
|
||||
Asuki
|
||||
Kono
|
||||
López
|
||||
mashedkeyboard
|
||||
Thijs
|
||||
Triemstra
|
||||
Elsdörfer
|
||||
Renato
|
||||
Lorenzi
|
||||
Bookham
|
||||
Goglin
|
||||
Christoph
|
||||
Heuel
|
||||
Qian
|
||||
Lehtonen
|
||||
Kanzler
|
||||
Rotondo
|
||||
Alexandre
|
||||
Detiste
|
||||
|
||||
barcode
|
||||
barcodes
|
||||
baudrate
|
||||
Bashlinux
|
||||
capabilities
|
||||
cashdraw
|
||||
charcode
|
||||
changelog
|
||||
cheque
|
||||
codebase
|
||||
codecov
|
||||
codepages
|
||||
config
|
||||
del
|
||||
dev
|
||||
dialout
|
||||
docstrings
|
||||
ean
|
||||
Ean
|
||||
encodable
|
||||
fff
|
||||
fullimage
|
||||
io
|
||||
json
|
||||
latin
|
||||
libusb
|
||||
lp
|
||||
lsusb
|
||||
natively
|
||||
php
|
||||
pre
|
||||
prefilled
|
||||
printcap
|
||||
programmatically
|
||||
py
|
||||
pypy
|
||||
pyyaml
|
||||
px
|
||||
qrcode
|
||||
Raspbian
|
||||
rebase
|
||||
rebased
|
||||
resetted
|
||||
rst
|
||||
submodule
|
||||
submodules
|
||||
src
|
||||
testcases
|
||||
th
|
||||
Todo
|
||||
traceback
|
||||
udev
|
||||
usb
|
||||
usec
|
||||
virtualenvs
|
||||
whitespaces
|
||||
xml
|
@@ -1,17 +1,29 @@
|
||||
Printing Barcodes
|
||||
-----------------
|
||||
:Last Reviewed: 2016-07-31
|
||||
|
||||
Most ESC/POS-printers implement barcode-printing.
|
||||
The barcode-commandset is implemented in the barcode-method.
|
||||
For a list of compatible barcodes you should check the manual of your printer.
|
||||
As a rule of thumb: even older Epson-models support most 1D-barcodes.
|
||||
To be sure just try some implementations and have a look at the notices below.
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
Many printers implement barcode printing natively.
|
||||
These hardware rendered barcodes are fast but the supported
|
||||
formats are limited by the printer itself and different between models.
|
||||
However, almost all printers support printing images, so barcode
|
||||
rendering can be performed externally by software and then sent
|
||||
to the printer as an image.
|
||||
As a drawback, this operation is much slower and the user needs
|
||||
to know and choose the image implementation method supported by
|
||||
the printer's command-set.
|
||||
|
||||
barcode-method
|
||||
~~~~~~~~~~~~~~
|
||||
The barcode-method is rather low-level and orients itself on the implementation of ESC/POS.
|
||||
In the future this class could be supplemented by a high-level class that helps the user generating the payload.
|
||||
Since version 3.0, the ``barcode`` method unifies the previous
|
||||
``barcode`` (hardware) and ``soft_barcode`` (software) methods.
|
||||
It is able to choose automatically the best printer implementation
|
||||
for barcode printing based on the capabilities of the printer
|
||||
and the type of barcode desired.
|
||||
To achieve this, it relies on the information contained in the
|
||||
escpos-printer-db profiles.
|
||||
The chosen profile needs to match the capabilities of the printer
|
||||
as closely as possible.
|
||||
|
||||
.. py:currentmodule:: escpos.escpos
|
||||
|
||||
@@ -31,4 +43,5 @@ For alphanumeric CODE128 you have to preface your payload with `{B`.
|
||||
# print CODE128 012ABCDabcd
|
||||
p.barcode("{B012ABCDabcd", "CODE128", function_type="B")
|
||||
|
||||
A very good description on CODE128 is also on `Wikipedia <https://en.wikipedia.org/wiki/Code_128>`_.
|
||||
A very good description on CODE128 is also on
|
||||
`Wikipedia <https://en.wikipedia.org/wiki/Code_128>`_.
|
||||
|
10
doc/user/cli-user.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
CLI
|
||||
===
|
||||
|
||||
:Last Reviewed: 2023-09-25
|
||||
|
||||
Documentation of the command line interface, callable with ``python-escpos``.
|
||||
|
||||
.. argparse::
|
||||
:ref: escpos.cli.generate_parser
|
||||
:prog: python-escpos
|
@@ -1,25 +1,53 @@
|
||||
************
|
||||
Installation
|
||||
************
|
||||
.. _user_installation:
|
||||
|
||||
:Last Reviewed: 2016-07-23
|
||||
Installation
|
||||
============
|
||||
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
Installation with PIP
|
||||
---------------------
|
||||
Installation should be rather straight-forward. python-escpos is on PyPi, so you can simply enter:
|
||||
Installation should be rather straight-forward. python-escpos is on PyPi,
|
||||
so you can simply enter:
|
||||
|
||||
::
|
||||
|
||||
pip install python-escpos
|
||||
pip install python-escpos[all]
|
||||
|
||||
This should install all necessary dependencies. Apart from that python-escpos should also be
|
||||
available as a Debian package. If you want to always benefit from the newest stable releases you should probably
|
||||
install from PyPi.
|
||||
This should install all necessary dependencies. Apart from that
|
||||
python-escpos is for some versions also available as a Debian package.
|
||||
If you want to always benefit from the newest stable releases you should
|
||||
always install from PyPi.
|
||||
If you use the ``--pre`` parameter for ``pip``, you will get the latest
|
||||
pre-release.
|
||||
|
||||
The following installation options exist:
|
||||
|
||||
* `all`: install all packages available for this platform
|
||||
* `usb`: install packages required for USB printers
|
||||
* `serial`: install packages required for serial printers
|
||||
* `win32`: install packages required for win32 printing (only Windows)
|
||||
* `cups`: install packages required for CUPS printing
|
||||
|
||||
Other installation methods
|
||||
--------------------------
|
||||
Officially, no other installation methods are supplied.
|
||||
|
||||
If you want to install nevertheless from another source,
|
||||
please make sure that you have received the correct package
|
||||
and that the capabilities data is properly integrated.
|
||||
When packaging from source please read the developer
|
||||
information in :ref:`developer-manual-repository`.
|
||||
|
||||
If your packaging method breaks the resource system from setuptools,
|
||||
it might be necessary to supply the path to the capabilities file:
|
||||
:ref:`advanced-usage-change-capabilities-profile`.
|
||||
|
||||
Setup udev for USB-Printers
|
||||
---------------------------
|
||||
1. Get the *Product ID* and *Vendor ID* from the lsusb command
|
||||
``# lsusb Bus 002 Device 001: ID 1a2b:1a2b Device name``
|
||||
``# lsusb Bus 002 Device 001: ID 1a2b:1a2b Device name``.
|
||||
(Or whichever way your system supplies to get the PID and VID.)
|
||||
|
||||
2. Create a udev rule to let users belonging to *dialout* group use the
|
||||
printer. You can create the file
|
||||
@@ -35,7 +63,8 @@ Setup udev for USB-Printers
|
||||
|
||||
Enabling tab-completion in CLI
|
||||
------------------------------
|
||||
python-escpos has a CLI with tab-completion. This is realised with ``argcomplete``.
|
||||
python-escpos has a CLI with tab-completion.
|
||||
This is realized with ``argcomplete``.
|
||||
In order for this to work you have to enable tab-completion, which is described in
|
||||
the `manual of argcomplete <https://argcomplete.readthedocs.io>`__.
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
*******
|
||||
Methods
|
||||
*******
|
||||
:Last Reviewed: 2017-01-25
|
||||
=======
|
||||
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
Escpos class
|
||||
------------
|
||||
|
||||
The core part of this libraries API is the Escpos class.
|
||||
The core part of the API of this library is the Escpos class.
|
||||
You use it by instantiating a :doc:`printer <printers>` which is a child of Escpos.
|
||||
The following methods are available:
|
||||
|
||||
|
@@ -1,15 +1,18 @@
|
||||
********
|
||||
Printers
|
||||
********
|
||||
:Last Reviewed: 2017-01-25
|
||||
========
|
||||
|
||||
As of now there are 5 different type of printer implementations.
|
||||
:Last Reviewed: 2023-08-23
|
||||
|
||||
As of now there are 8 different types of printer implementations.
|
||||
|
||||
USB
|
||||
---
|
||||
The USB-class uses pyusb and libusb to communicate with USB-based
|
||||
printers. Note that this driver is not suited for USB-to-Serial-adapters
|
||||
and similiar devices, but only for those implementing native USB.
|
||||
printers.
|
||||
|
||||
.. note::
|
||||
This driver is not suited for USB-to-Serial-adapters
|
||||
and similar devices, but only for those implementing native USB.
|
||||
|
||||
.. autoclass:: escpos.printer.Usb
|
||||
:members:
|
||||
@@ -44,20 +47,24 @@ This driver is based on the socket class.
|
||||
|
||||
Troubleshooting
|
||||
^^^^^^^^^^^^^^^
|
||||
Problems with a network-attached printer can have numerous causes. Make sure that your device has a proper IP address.
|
||||
Often you can check the IP address by triggering the self-test of the device. As a next step try to send text
|
||||
manually to the device. You could use for example:
|
||||
Problems with a network-attached printer can have numerous causes.
|
||||
Make sure that your device has a proper IP address.
|
||||
Often you can check the IP address by triggering the self-test of the device.
|
||||
As a next step try to send text manually to the device.
|
||||
You could use for example:
|
||||
|
||||
::
|
||||
|
||||
echo "OK\n" | nc IPADDRESS 9100
|
||||
# the port number is often 9100
|
||||
|
||||
As a last resort try to reset the interface of the printer. This should be described in its manual.
|
||||
As a last resort try to reset the interface of the printer.
|
||||
This should be described in its manual.
|
||||
|
||||
File
|
||||
----
|
||||
This printer "prints" just into a file-handle. Especially on \*nix-systems this comes very handy.
|
||||
This printer "prints" just into a file-handle.
|
||||
Especially on \*nix-systems this comes very handy.
|
||||
|
||||
.. autoclass:: escpos.printer.File
|
||||
:members:
|
||||
@@ -67,11 +74,50 @@ This printer "prints" just into a file-handle. Especially on \*nix-systems this
|
||||
|
||||
Dummy
|
||||
-----
|
||||
The Dummy-printer is mainly for testing- and debugging-purposes. It stores
|
||||
all of the "output" as raw ESC/POS in a string and returns that.
|
||||
The Dummy-printer is mainly for testing- and debugging-purposes.
|
||||
It stores all of the "output" as raw ESC/POS in a string and returns that.
|
||||
|
||||
.. autoclass:: escpos.printer.Dummy
|
||||
:members:
|
||||
:member-order: bysource
|
||||
:noindex:
|
||||
|
||||
CUPS
|
||||
----
|
||||
This driver uses `pycups` in order to communicate with a CUPS server.
|
||||
Supports both local and remote CUPS printers and servers.
|
||||
The printer must be properly configured in CUPS administration.
|
||||
The connector generates a print job that is added to the CUPS queue.
|
||||
|
||||
.. autoclass:: escpos.printer.CupsPrinter
|
||||
:members:
|
||||
:member-order: bysource
|
||||
:noindex:
|
||||
|
||||
LP
|
||||
----
|
||||
This driver uses the UNIX command `lp` in order to communicate with a CUPS server.
|
||||
Supports local and remote CUPS printers.
|
||||
The printer must be properly configured in CUPS administration.
|
||||
The connector spawns a new sub-process where the command lp is executed.
|
||||
|
||||
No dependencies required, but somehow the print queue will affect some
|
||||
print job such as barcode.
|
||||
|
||||
.. autoclass:: escpos.printer.LP
|
||||
:members:
|
||||
:special-members:
|
||||
:member-order: bysource
|
||||
:noindex:
|
||||
|
||||
Win32Raw
|
||||
--------
|
||||
This driver uses a native WIN32 interface of Windows in order to print.
|
||||
Please refer to the code for documentation as this driver is currently
|
||||
not included in the documentation build.
|
||||
|
||||
.. autoclass:: escpos.printer.Win32Raw
|
||||
:members:
|
||||
:special-members:
|
||||
:member-order: bysource
|
||||
:noindex:
|
@@ -1,42 +1,26 @@
|
||||
************
|
||||
Raspberry Pi
|
||||
************
|
||||
============
|
||||
|
||||
:Last Reviewed: 2017-01-05
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
This instructions were tested on Raspbian Jessie.
|
||||
|
||||
.. warning:: You should **never** directly connect an printer with RS232-interface (serial port) directly to
|
||||
a Raspberry PI or similar interface (e.g. those simple USB-sticks without encasing). Those interfaces are
|
||||
based on 5V- or 3,3V-logic (the latter in the case of Raspberry PI). Classical RS232 uses 12V-logic and would
|
||||
**thus destroy your interface**. Connect both systems with an appropriate *level shifter*.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
First, install the packages available on Raspbian.
|
||||
|
||||
::
|
||||
|
||||
sudo apt-get install python3 python3-setuptools python3-pip libjpeg8-dev
|
||||
.. warning:: You should **never** directly connect an printer with RS232-interface
|
||||
(serial port) directly to a Raspberry PI or similar interface
|
||||
(e.g. those simple USB-sticks without encasing).
|
||||
Those interfaces are based on 5V- or 3,3V-logic
|
||||
(the latter in the case of Raspberry PI).
|
||||
Classical RS232 uses 12V-logic and would **thus destroy your interface**.
|
||||
Connect both systems with an appropriate *level shifter*.
|
||||
|
||||
Installation
|
||||
------------
|
||||
You can install by using pip3.
|
||||
|
||||
::
|
||||
|
||||
sudo pip3 install --upgrade pip
|
||||
sudo pip3 install python-escpos
|
||||
The installation should be performed as described in :ref:`user_installation`.
|
||||
|
||||
Run
|
||||
---
|
||||
You need sudo and python3 to run your program.
|
||||
You can run this software as on any other Linux system.
|
||||
|
||||
::
|
||||
|
||||
sudo python3 your-program.py
|
||||
|
||||
Now you can attach your printer and and test it with the example code in the project's set of examples.
|
||||
You can find that in the `project-repository <https://github.com/python-escpos/python-escpos>`__.
|
||||
Attach your printer and test it with the example code in the project's set of examples.
|
||||
You can find that in the
|
||||
`project-repository <https://github.com/python-escpos/python-escpos>`__.
|
||||
|
||||
For more details on this check the :doc:`installation-manual <installation>`.
|
||||
|
@@ -1,43 +0,0 @@
|
||||
****
|
||||
TODO
|
||||
****
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
python-escpos is the initial idea, from here we can start to build a
|
||||
robust library to get most of the ESC/POS printers working with this
|
||||
library.
|
||||
|
||||
Eventually, this library must be able to cover almost all the defined
|
||||
models detailed in the ESC/POS Command Specification Manual.
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
What things are planned to work on?
|
||||
|
||||
Testing
|
||||
~~~~~~~
|
||||
|
||||
* Test on many printers as possible (USB, Serial, Network)
|
||||
* automate testing
|
||||
|
||||
Design
|
||||
~~~~~~
|
||||
|
||||
* Add all those sequences which are not common, but part of the ESC/POS
|
||||
Command Specifications.
|
||||
|
||||
* Port to Python 3
|
||||
* Windows compatibility (hidapi instead libusb?)
|
||||
* PDF417 support
|
||||
|
||||
* use something similar to the `capabilities` in escpos-php
|
||||
|
||||
Todos in the codebase
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. todolist::
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
*****
|
||||
Usage
|
||||
*****
|
||||
:Last Reviewed: 2017-06-10
|
||||
=====
|
||||
|
||||
:Last Reviewed: 2023-08-10
|
||||
|
||||
Define your printer
|
||||
-------------------
|
||||
@@ -44,18 +44,20 @@ to have and the second yields the "Output Endpoint" address.
|
||||
|
||||
::
|
||||
|
||||
Epson = printer.Usb(0x04b8,0x0202)
|
||||
p = printer.Usb(0x04b8,0x0202)
|
||||
|
||||
By default the "Interface" number is "0" and the "Output Endpoint"
|
||||
address is "0x01". If you have other values then you can define them on
|
||||
your instance. So, assuming that we have another printer where in\_ep is
|
||||
on 0x81 and out\_ep=0x02, then the printer definition should look like:
|
||||
your instance. So, assuming that we have another printer, CT-S2000,
|
||||
manufactured by Citizen (with "Vendor ID" of 2730 and "Product ID" of 0fff)
|
||||
where in\_ep is on 0x81 and out\_ep=0x02, then the printer definition should
|
||||
look like:
|
||||
|
||||
**Generic USB Printer initialization**
|
||||
|
||||
::
|
||||
|
||||
Generic = printer.Usb(0x1a2b,0x1a2b,0,0x81,0x02)
|
||||
p = printer.Usb(0x2730, 0x0fff, 0, 0x81, 0x02)
|
||||
|
||||
Network printer
|
||||
^^^^^^^^^^^^^^^
|
||||
@@ -67,7 +69,7 @@ IP by DHCP or you set it manually.
|
||||
|
||||
::
|
||||
|
||||
Epson = printer.Network("192.168.1.99")
|
||||
p = printer.Network("192.168.1.99")
|
||||
|
||||
Serial printer
|
||||
^^^^^^^^^^^^^^
|
||||
@@ -81,7 +83,10 @@ to.
|
||||
|
||||
::
|
||||
|
||||
Epson = printer.Serial("/dev/tty0")
|
||||
p = printer.Serial("/dev/tty0")
|
||||
|
||||
# on a Windows OS serial devices are typically accessible as COM
|
||||
p = printer.Serial("COM1")
|
||||
|
||||
Other printers
|
||||
^^^^^^^^^^^^^^
|
||||
@@ -93,7 +98,7 @@ passing the device node name.
|
||||
|
||||
::
|
||||
|
||||
Epson = printer.File("/dev/usb/lp1")
|
||||
p = printer.File("/dev/usb/lp1")
|
||||
|
||||
The default is "/dev/usb/lp0", so if the printer is located on that
|
||||
node, then you don't necessary need to pass the node name.
|
||||
@@ -108,17 +113,22 @@ on a USB interface.
|
||||
|
||||
from escpos import *
|
||||
""" Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """
|
||||
Epson = printer.Usb(0x04b8,0x0202)
|
||||
p = printer.Usb(0x04b8,0x0202)
|
||||
# Print text
|
||||
Epson.text("Hello World\n")
|
||||
p.text("Hello World\n")
|
||||
# Print image
|
||||
Epson.image("logo.gif")
|
||||
p.image("logo.gif")
|
||||
# Print QR Code
|
||||
Epson.qr("You can readme from your smartphone")
|
||||
p.qr("You can readme from your smartphone")
|
||||
# Print barcode
|
||||
Epson.barcode('1324354657687','EAN13',64,2,'','')
|
||||
p.barcode('4006381333931','EAN13',64,2,'','')
|
||||
# Cut paper
|
||||
Epson.cut()
|
||||
p.cut()
|
||||
|
||||
Standard python constraints on libraries apply. This means especially
|
||||
that you should not name the script in which you implement these lines
|
||||
should not be named ``escpos`` as this would collide with the name of
|
||||
the library.
|
||||
|
||||
Configuration File
|
||||
------------------
|
||||
@@ -136,7 +146,7 @@ And for linux::
|
||||
|
||||
$HOME/.config/python-escpos/config.yaml
|
||||
|
||||
If you aren't sure, run::
|
||||
If you are not sure, run::
|
||||
|
||||
from escpos import config
|
||||
c = config.Config()
|
||||
@@ -158,7 +168,7 @@ The printer section
|
||||
|
||||
The ``printer`` configuration section defines a default printer to create.
|
||||
|
||||
The only required paramter is ``type``. The value of this has to be one of the
|
||||
The only required parameter is ``type``. The value of this has to be one of the
|
||||
printers defined in :doc:`/user/printers`.
|
||||
|
||||
The rest of the given parameters will be passed on to the initialization of the printer class.
|
||||
@@ -189,17 +199,19 @@ An USB-printer could be defined by::
|
||||
|
||||
Printing text right
|
||||
-------------------
|
||||
Python-escpos is designed to accept unicode. So make sure that you use ``u'strings'`` or import ``unicode_literals``
|
||||
from ``__future__`` if you are on Python 2. On Python 3 you should be fine.
|
||||
|
||||
Python-escpos is designed to accept unicode.
|
||||
|
||||
For normal usage you can simply pass your text to the printers ``text()``-function. It will automatically guess
|
||||
the right codepage and then send the encoded data to the printer. If this feature does not work, please try to
|
||||
isolate the error and then create an issue on the Github project page.
|
||||
isolate the error and then create an issue on the GitHub project page.
|
||||
|
||||
If you want or need to you can manually set the codepage. For this please use the ``charcode()``-function. You can set
|
||||
any key-value that is in ``CHARCODE``. If something is wrong, an ``CharCodeError`` will be raised.
|
||||
After you have manually set the codepage the printer won't change it anymore. You can revert to normal behaviour
|
||||
by setting charcode to ``AUTO``.
|
||||
If you want or need to you can manually set the codepage.
|
||||
For this please use the ``charcode()``-function.
|
||||
You can set any key-value that is in ``CHARCODE``.
|
||||
If something is wrong, an ``CharCodeError`` will be raised.
|
||||
After you have manually set the codepage the printer won't change it anymore.
|
||||
You can revert to normal behavior by setting charcode to ``AUTO``.
|
||||
|
||||
Advanced Usage: Print from binary blob
|
||||
--------------------------------------
|
||||
@@ -228,6 +240,8 @@ Here you can download an example, that will print a set of common barcodes:
|
||||
|
||||
* :download:`barcode.bin </download/barcode.bin>` by `@mike42 <https://github.com/mike42>`_
|
||||
|
||||
.. _advanced-usage-change-capabilities-profile:
|
||||
|
||||
Advanced Usage: change capabilities-profile
|
||||
-------------------------------------------
|
||||
|
||||
@@ -284,4 +298,18 @@ This way you could also store the code in a file and print it later.
|
||||
You could then for example print the code from another process than your main-program and thus reduce the waiting time.
|
||||
(Of course this will not make the printer print faster.)
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
This section gathers various hints on troubleshooting.
|
||||
|
||||
Print with STAR TSP100 family
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Printer of the STAR TSP100 family do not have a native ESC/POS mode, which
|
||||
is why you will not be able to directly print with this library to the printer.
|
||||
|
||||
More information on this topic can be found in the online documentation of
|
||||
`Star Micronics <https://www.starmicronics.com/help-center/knowledge-base/configure-tsp100-series-printers-esc-pos-mode/>`_
|
||||
and the `discussion in the python-escpos project <https://github.com/python-escpos/python-escpos/issues/410>`_.
|
||||
|
||||
|
||||
|
11
examples/barcodes.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Example for printing barcodes."""
|
||||
from escpos.printer import Usb
|
||||
|
||||
# Adapt to your needs
|
||||
p = Usb(0x0416, 0x5011, profile="TM-T88II")
|
||||
|
||||
# Print software and then hardware barcode with the same content
|
||||
p.barcode("123456", "CODE39", width=2, force_software=True)
|
||||
p.text("\n")
|
||||
p.text("\n")
|
||||
p.barcode("123456", "CODE39")
|
@@ -1,21 +1,27 @@
|
||||
"""Prints code page tables.
|
||||
"""
|
||||
"""Prints code page tables."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import six
|
||||
import sys
|
||||
|
||||
from escpos import printer
|
||||
from escpos.constants import CODEPAGE_CHANGE, ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT
|
||||
from escpos.constants import (
|
||||
CODEPAGE_CHANGE,
|
||||
CTL_CR,
|
||||
CTL_FF,
|
||||
CTL_HT,
|
||||
CTL_LF,
|
||||
CTL_VT,
|
||||
ESC,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Init printer and print codepage tables."""
|
||||
dummy = printer.Dummy()
|
||||
|
||||
dummy.hw('init')
|
||||
dummy.hw("init")
|
||||
|
||||
for codepage in sys.argv[1:] or ['USA']:
|
||||
for codepage in sys.argv[1:] or ["USA"]:
|
||||
dummy.set(height=2, width=2)
|
||||
dummy._raw(codepage + "\n\n\n")
|
||||
print_codepage(dummy, codepage)
|
||||
@@ -27,9 +33,10 @@ def main():
|
||||
|
||||
|
||||
def print_codepage(printer, codepage):
|
||||
"""Print a codepage."""
|
||||
if codepage.isdigit():
|
||||
codepage = int(codepage)
|
||||
printer._raw(CODEPAGE_CHANGE + six.int2byte(codepage))
|
||||
printer._raw(CODEPAGE_CHANGE + bytes((codepage,)))
|
||||
printer._raw("after")
|
||||
else:
|
||||
printer.charcode(codepage)
|
||||
@@ -37,27 +44,29 @@ def print_codepage(printer, codepage):
|
||||
sep = ""
|
||||
|
||||
# Table header
|
||||
printer.set(text_type='B')
|
||||
printer._raw(" {}\n".format(sep.join(map(lambda s: hex(s)[2:], range(0, 16)))))
|
||||
printer.set(font="b")
|
||||
printer._raw(f" {sep.join(map(lambda s: hex(s)[2:], range(0, 16)))}\n")
|
||||
printer.set()
|
||||
|
||||
# The table
|
||||
for x in range(0, 16):
|
||||
# First column
|
||||
printer.set(text_type='B')
|
||||
printer._raw("{} ".format(hex(x)[2:]))
|
||||
printer.set(font="b")
|
||||
printer._raw(f"{hex(x)[2:]} ")
|
||||
printer.set()
|
||||
|
||||
for y in range(0, 16):
|
||||
byte = six.int2byte(x * 16 + y)
|
||||
byte = bytes(
|
||||
(x * 16 + y),
|
||||
)
|
||||
|
||||
if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT):
|
||||
byte = ' '
|
||||
byte = " "
|
||||
|
||||
printer._raw(byte)
|
||||
printer._raw(sep)
|
||||
printer._raw('\n')
|
||||
printer._raw("\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
28
examples/docker-flask/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# Use the official Python image as the base image
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the requirements file
|
||||
COPY requirements.txt .
|
||||
|
||||
#Install the libcups library
|
||||
RUN apt-get update -y && apt-get install libcups2-dev -y
|
||||
# Install the Python packages
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install python-escpos --pre
|
||||
|
||||
# Install Git
|
||||
RUN apt-get update && \
|
||||
apt-get install -y git && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the Flask app
|
||||
COPY app.py .
|
||||
|
||||
# Expose the port the Flask app will run on
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the Flask app
|
||||
CMD ["python", "app.py"]
|
6
examples/docker-flask/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Simple example on how to use it inside a web service
|
||||
|
||||
```
|
||||
docker build . -t escpos-web
|
||||
docker run --network=host -p 9999:9999 escpos
|
||||
```
|
22
examples/docker-flask/app.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Example for a flask application."""
|
||||
from flask import Flask
|
||||
|
||||
from escpos.printer import CupsPrinter
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
def do_print():
|
||||
"""Print."""
|
||||
# p = Usb(0x04b8, 0x0e28, 0)
|
||||
p = CupsPrinter(host="localhost", port=631, printer_name="TM-T20III")
|
||||
p.text("Hello World\n")
|
||||
p.cut()
|
||||
p.close()
|
||||
return "OK"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=False, host="0.0.0.0", port=9999)
|
20
examples/docker-flask/requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
appdirs==1.4.4
|
||||
argcomplete==3.0.8
|
||||
blinker==1.6.2
|
||||
click==8.1.3
|
||||
Flask==2.3.2
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.2
|
||||
Pillow==10.0.1
|
||||
pycups==2.0.1
|
||||
pypng==0.20220715.0
|
||||
pyserial==3.5
|
||||
python-barcode==0.14.0
|
||||
python-escpos==3.0a9
|
||||
pyusb==1.2.1
|
||||
PyYAML==6.0
|
||||
qrcode==7.4.2
|
||||
six==1.16.0
|
||||
typing_extensions==4.5.0
|
||||
Werkzeug==3.0.1
|
BIN
examples/graphics/climacons/clear-day.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
examples/graphics/climacons/clear-night.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
examples/graphics/climacons/cloudy.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
examples/graphics/climacons/fog.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
examples/graphics/climacons/partly-cloudy-day.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
examples/graphics/climacons/partly-cloudy-night.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
examples/graphics/climacons/rain.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
10
examples/graphics/climacons/readme.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Climacons by Adam Whitcroft
|
||||
|
||||
75 climatically categorised pictographs for web and UI design by [@adamwhitcroft](https://www.twitter.com/#!/adamwhitcroft).
|
||||
|
||||
Visit the [Climacons](https://adamwhitcroft.com/climacons/) website for more information.
|
||||
|
||||
Visit [Adam Whitcroft on GitHub](https://github.com/AdamWhitcroft)
|
||||
|
||||
## License
|
||||
You are free to use any of the Climacons Icons (the "icons") in any personal or commercial work without obligation of payment (monetary or otherwise) or attribution, however a credit for the work would be appreciated. **Do not** redistribute or sell and **do not** claim creative credit. Intellectual property rights are not transferred with the download of the icons.
|
BIN
examples/graphics/climacons/sleet.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
examples/graphics/climacons/snow.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
examples/graphics/climacons/wind.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
@@ -1,13 +1,15 @@
|
||||
"""Print example QR codes."""
|
||||
import sys
|
||||
|
||||
from escpos.printer import Usb
|
||||
|
||||
|
||||
def usage():
|
||||
"""Print information on usage."""
|
||||
print("usage: qr_code.py <content>")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
usage()
|
||||
sys.exit(1)
|
||||
@@ -16,4 +18,4 @@ if __name__ == '__main__':
|
||||
|
||||
# Adapt to your needs
|
||||
p = Usb(0x0416, 0x5011, profile="POS-5890")
|
||||
p.qr(content)
|
||||
p.qr(content, center=True)
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"""Example file for software barcodes."""
|
||||
from escpos.printer import Usb
|
||||
|
||||
|
||||
# Adapt to your needs
|
||||
p = Usb(0x0416, 0x5011, profile="POS-5890")
|
||||
|
||||
# Some software barcodes
|
||||
p.soft_barcode('code128', 'Hello')
|
||||
p.soft_barcode('code39', '123456')
|
||||
p.barcode("Hello", "code128", width=2, force_software="bitImageRaster")
|
||||
p.barcode("1234", "code39", width=2, force_software=True)
|
||||
|
131
examples/weather.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/python
|
||||
"""Weather forecast example.
|
||||
|
||||
Adapted script from Adafruit
|
||||
Weather forecast for Raspberry Pi w/Adafruit Mini Thermal Printer.
|
||||
Retrieves data from DarkSky.net's API, prints current conditions and
|
||||
forecasts for next two days.
|
||||
Weather example using nice bitmaps.
|
||||
Written by Adafruit Industries. MIT license.
|
||||
Adapted and enhanced for escpos library by MrWunderbar666
|
||||
|
||||
Icons taken from https://adamwhitcroft.com/climacons/
|
||||
Check out his github: https://github.com/AdamWhitcroft/climacons
|
||||
"""
|
||||
|
||||
|
||||
import calendar
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.request import urlopen
|
||||
|
||||
from escpos.printer import Usb
|
||||
|
||||
"""Set up the main pathing."""
|
||||
this_dir, this_filename = os.path.split(__file__)
|
||||
GRAPHICS_PATH = os.path.join(this_dir, "graphics/climacons/")
|
||||
|
||||
# Adapt to your needs
|
||||
printer = Usb(0x0416, 0x5011, profile="POS-5890")
|
||||
|
||||
# You can get your API Key on www.darksky.net and register a dev account.
|
||||
# Technically you can use any other weather service, of course :)
|
||||
API_KEY = "YOUR API KEY"
|
||||
|
||||
LAT = "22.345490" # Your Location
|
||||
LONG = "114.189945" # Your Location
|
||||
|
||||
|
||||
def forecast_icon(idx):
|
||||
"""Get right icon for forecast."""
|
||||
icon = data["daily"]["data"][idx]["icon"]
|
||||
image = GRAPHICS_PATH + icon + ".png"
|
||||
return image
|
||||
|
||||
|
||||
def forecast(idx):
|
||||
"""Dump one forecast line to the printer."""
|
||||
date = datetime.fromtimestamp(int(data["daily"]["data"][idx]["time"]))
|
||||
day = calendar.day_name[date.weekday()]
|
||||
lo = data["daily"]["data"][idx]["temperatureMin"]
|
||||
hi = data["daily"]["data"][idx]["temperatureMax"]
|
||||
cond = data["daily"]["data"][idx]["summary"]
|
||||
print(date)
|
||||
print(day)
|
||||
print(lo)
|
||||
print(hi)
|
||||
print(cond)
|
||||
time.sleep(1)
|
||||
printer.set(font="a", height=2, align="left", bold=False, double_height=False)
|
||||
printer.text(day + " \n ")
|
||||
time.sleep(5) # Sleep to prevent printer buffer overflow
|
||||
printer.text("\n")
|
||||
printer.image(forecast_icon(idx))
|
||||
printer.text("low " + str(lo))
|
||||
printer.text(deg)
|
||||
printer.text("\n")
|
||||
printer.text(" high " + str(hi))
|
||||
printer.text(deg)
|
||||
printer.text("\n")
|
||||
# take care of pesky unicode dash
|
||||
printer.text(cond.replace("\u2013", "-").encode("utf-8"))
|
||||
printer.text("\n \n")
|
||||
|
||||
|
||||
def icon():
|
||||
"""Get icon."""
|
||||
icon = data["currently"]["icon"]
|
||||
image = GRAPHICS_PATH + icon + ".png"
|
||||
return image
|
||||
|
||||
|
||||
deg = " C" # Degree symbol on thermal printer, need to find a better way to use a proper degree symbol
|
||||
|
||||
# if you want Fahrenheit change units= to 'us'
|
||||
url = (
|
||||
"https://api.darksky.net/forecast/"
|
||||
+ API_KEY
|
||||
+ "/"
|
||||
+ LAT
|
||||
+ ","
|
||||
+ LONG
|
||||
+ "?exclude=[alerts,minutely,hourly,flags]&units=si"
|
||||
) # change last bit to 'us' for Fahrenheit
|
||||
response = urlopen(url)
|
||||
data = json.loads(response.read())
|
||||
|
||||
printer.print_and_feed(n=1)
|
||||
printer.control("LF")
|
||||
printer.set(font="a", height=2, align="center", bold=True, double_height=True)
|
||||
printer.text("Weather Forecast")
|
||||
printer.text("\n")
|
||||
printer.set(align="center")
|
||||
|
||||
|
||||
# Print current conditions
|
||||
printer.set(font="a", height=2, align="center", bold=True, double_height=False)
|
||||
printer.text("Current conditions: \n")
|
||||
printer.image(icon())
|
||||
printer.text("\n")
|
||||
|
||||
printer.set(font="a", height=2, align="left", bold=False, double_height=False)
|
||||
temp = data["currently"]["temperature"]
|
||||
cond = data["currently"]["summary"]
|
||||
printer.text(temp)
|
||||
printer.text(" ")
|
||||
printer.text(deg)
|
||||
printer.text(" ")
|
||||
printer.text("\n")
|
||||
printer.text("Sky: " + cond)
|
||||
printer.text("\n")
|
||||
printer.text("\n")
|
||||
|
||||
# Print forecast
|
||||
printer.set(font="a", height=2, align="center", bold=True, double_height=False)
|
||||
printer.text("Forecast: \n")
|
||||
forecast(0)
|
||||
forecast(1)
|
||||
printer.cut()
|
||||
printer.control("LF")
|
27
pyproject.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[tool.black]
|
||||
extend-exclude = 'capabilities-data'
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = "--doctest-modules --cov escpos --cov-report=xml"
|
||||
testpaths = [
|
||||
"test",
|
||||
"src",
|
||||
"src/escpos",
|
||||
"escpos",
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["pytest",
|
||||
"jaconv",
|
||||
"scripttest",
|
||||
"barcode.*",
|
||||
"qrcode",
|
||||
"usb.*",
|
||||
"cups",
|
||||
"win32print"
|
||||
]
|
||||
ignore_missing_imports = true
|
@@ -1,7 +0,0 @@
|
||||
formats:
|
||||
- pdf
|
||||
- epub
|
||||
requirements_file: doc/requirements.txt
|
||||
python:
|
||||
version: 2
|
||||
setup_py_install: true
|
82
setup.cfg
@@ -1,12 +1,80 @@
|
||||
[nosetests]
|
||||
verbosity=3
|
||||
with-doctest=1
|
||||
[metadata]
|
||||
name = python-escpos
|
||||
url = https://github.com/python-escpos/python-escpos
|
||||
description = Python library to manipulate ESC/POS Printers
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
author = python-escpos developers
|
||||
author_email = dev@pkanzler.de
|
||||
maintainer = Patrick Kanzler
|
||||
maintainer_email = dev@pkanzler.de
|
||||
keywords = ESC/POS, thermoprinter, voucher printer, printing, receipt
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: Console
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Topic :: Software Development :: Libraries :: Python Modules
|
||||
Topic :: Office/Business :: Financial :: Point-Of-Sale
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/python-escpos/python-escpos/issues
|
||||
Documentation = https://python-escpos.readthedocs.io/en/latest/
|
||||
Release Notes = https://github.com/python-escpos/python-escpos/releases
|
||||
|
||||
[bdist_wheel]
|
||||
# This flag says that the code is written to work on both Python 2 and Python 3.
|
||||
universal=1
|
||||
[options]
|
||||
python_requires = >=3.8
|
||||
zip_safe = false
|
||||
include_package_data = true
|
||||
install_requires =
|
||||
Pillow>=2.0
|
||||
qrcode>=4.0
|
||||
python-barcode>=0.15.0,<1
|
||||
setuptools
|
||||
six
|
||||
appdirs
|
||||
PyYAML
|
||||
argcomplete
|
||||
importlib_resources
|
||||
setup_requires = setuptools_scm
|
||||
tests_require =
|
||||
jaconv
|
||||
tox>=4.11
|
||||
pytest>=7.4
|
||||
pytest-cov
|
||||
pytest-mock
|
||||
scripttest
|
||||
mock
|
||||
hypothesis>=6.83
|
||||
flake8
|
||||
sphinxcontrib-spelling>=8.0.0
|
||||
|
||||
[options.extras_require]
|
||||
usb =
|
||||
pyusb>=1.0.0
|
||||
serial =
|
||||
pyserial
|
||||
cups =
|
||||
pycups; platform_system!='Windows'
|
||||
win32 =
|
||||
pywin32; platform_system=='Windows'
|
||||
all =
|
||||
pyusb>=1.0.0
|
||||
pyserial
|
||||
pycups; platform_system!='Windows'
|
||||
pywin32; platform_system=='Windows'
|
||||
|
||||
[flake8]
|
||||
exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test,src/escpos/constants.py
|
||||
max-line-length = 120
|
||||
# future-imports = absolute_import, division, print_function, unicode_literals # we are not there yet
|
||||
extend-ignore = E203, W503
|
||||
|
129
setup.py
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
"""Setup script for python package."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as test_command
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
base_dir = os.path.dirname(__file__)
|
||||
src_dir = os.path.join(base_dir, "src")
|
||||
@@ -15,130 +15,31 @@ sys.path.insert(0, src_dir)
|
||||
|
||||
|
||||
def read(fname):
|
||||
"""read file from same path as setup.py"""
|
||||
"""Read file from same path as setup.py."""
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
|
||||
class Tox(test_command):
|
||||
"""proxy class that enables tox to be run with setup.py test"""
|
||||
user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
|
||||
|
||||
def initialize_options(self):
|
||||
"""initialize the user-options"""
|
||||
test_command.initialize_options(self)
|
||||
self.tox_args = None
|
||||
|
||||
def finalize_options(self):
|
||||
"""finalize user-options"""
|
||||
test_command.finalize_options(self)
|
||||
self.test_args = []
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
"""run tox and pass on user-options"""
|
||||
# import here, cause outside the eggs aren't loaded
|
||||
import tox
|
||||
import shlex
|
||||
args = self.tox_args
|
||||
if args:
|
||||
args = shlex.split(self.tox_args)
|
||||
errno = tox.cmdline(args=args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
setuptools_scm_template = """\
|
||||
# coding: utf-8
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
\"\"\"Version identifier.
|
||||
|
||||
file generated by setuptools_scm
|
||||
don't change, don't track in version control
|
||||
\"\"\"
|
||||
|
||||
version = '{version}'
|
||||
"""
|
||||
|
||||
|
||||
setup(
|
||||
name='python-escpos',
|
||||
use_scm_version={
|
||||
'write_to': 'src/escpos/version.py',
|
||||
'write_to_template': setuptools_scm_template,
|
||||
"write_to": "src/escpos/version.py",
|
||||
"write_to_template": setuptools_scm_template,
|
||||
},
|
||||
url='https://github.com/python-escpos/python-escpos',
|
||||
download_url='https://github.com/python-escpos/python-escpos/archive/master.zip',
|
||||
description='Python library to manipulate ESC/POS Printers',
|
||||
bugtrack_url='https://github.com/python-escpos/python-escpos/issues',
|
||||
license='MIT',
|
||||
long_description=read('README.rst'),
|
||||
author='Manuel F Martinez and others',
|
||||
author_email='manpaz@bashlinux.com',
|
||||
maintainer='Patrick Kanzler',
|
||||
maintainer_email='dev@pkanzler.de',
|
||||
keywords=[
|
||||
'ESC/POS',
|
||||
'thermoprinter',
|
||||
'voucher printer',
|
||||
'printing',
|
||||
'receipt,',
|
||||
],
|
||||
platforms='any',
|
||||
platforms="any",
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages(where="src", exclude=["tests", "tests.*"]),
|
||||
package_data={'': ['COPYING', 'src/escpos/capabilities.json']},
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
'Topic :: Office/Business :: Financial :: Point-Of-Sale',
|
||||
],
|
||||
install_requires=[
|
||||
'pyusb>=1.0.0',
|
||||
'Pillow>=2.0',
|
||||
'qrcode>=4.0',
|
||||
'pyserial',
|
||||
'six',
|
||||
'appdirs',
|
||||
'pyyaml',
|
||||
'argparse',
|
||||
'argcomplete',
|
||||
'future',
|
||||
'viivakoodi>=0.8'
|
||||
],
|
||||
setup_requires=[
|
||||
'setuptools_scm',
|
||||
],
|
||||
tests_require=[
|
||||
'jaconv',
|
||||
'tox',
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
'pytest-mock',
|
||||
'nose',
|
||||
'scripttest',
|
||||
'mock',
|
||||
'hypothesis',
|
||||
'flake8'
|
||||
],
|
||||
cmdclass={'test': Tox},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'python-escpos = escpos.cli:main'
|
||||
]
|
||||
},
|
||||
package_data={"escpos": ["capabilities.json"]},
|
||||
entry_points={"console_scripts": ["python-escpos = escpos.cli:main"]},
|
||||
)
|
||||
|
@@ -1,19 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
python-escpos enables you to manipulate escpos-printers
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
"""python-escpos enables you to manipulate escpos-printers."""
|
||||
|
||||
__all__ = ["constants", "escpos", "exceptions", "printer"]
|
||||
__all__ = ["constants", "escpos", "exceptions", "printer", "__version__"]
|
||||
|
||||
try:
|
||||
from .version import version as __version__ # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError(
|
||||
'Failed to find (autogenerated) version.py. '
|
||||
'This might be because you are installing from GitHub\'s tarballs, '
|
||||
'use the PyPI ones.'
|
||||
"Failed to find (autogenerated) version.py. "
|
||||
"This might be because you are installing from GitHub's tarballs, "
|
||||
"use the PyPI ones."
|
||||
)
|
||||
|
@@ -1,118 +1,195 @@
|
||||
"""Handler for capabilities data."""
|
||||
import atexit
|
||||
import logging
|
||||
import pickle
|
||||
import platform
|
||||
import re
|
||||
import six
|
||||
import time
|
||||
from contextlib import ExitStack
|
||||
from os import environ, path
|
||||
from tempfile import mkdtemp
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
import importlib_resources
|
||||
import yaml
|
||||
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", mkdtemp())
|
||||
pickle_path = path.join(pickle_dir, f"{platform.python_version()}.capabilities.pickle")
|
||||
# get a temporary file from importlib_resources if no file is specified in env
|
||||
file_manager = ExitStack()
|
||||
atexit.register(file_manager.close)
|
||||
ref = importlib_resources.files(__name__) / "capabilities.json"
|
||||
capabilities_path = environ.get(
|
||||
"ESCPOS_CAPABILITIES_FILE",
|
||||
file_manager.enter_context(importlib_resources.as_file(ref)),
|
||||
)
|
||||
|
||||
# Load external printer database
|
||||
if 'ESCPOS_CAPABILITIES_FILE' in environ:
|
||||
file_path = environ['ESCPOS_CAPABILITIES_FILE']
|
||||
t0 = time.time()
|
||||
logger.debug("Using capabilities from file: %s", capabilities_path)
|
||||
if path.exists(pickle_path):
|
||||
if path.getmtime(capabilities_path) > path.getmtime(pickle_path):
|
||||
logger.debug("Found a more recent capabilities file")
|
||||
full_load = True
|
||||
else:
|
||||
file_path = path.join(path.dirname(__file__), 'capabilities.json')
|
||||
full_load = False
|
||||
logger.debug("Loading capabilities from pickle in %s", pickle_path)
|
||||
with open(pickle_path, "rb") as cf:
|
||||
CAPABILITIES = pickle.load(cf)
|
||||
else:
|
||||
logger.debug("Capabilities pickle file not found: %s", pickle_path)
|
||||
full_load = True
|
||||
|
||||
with open(file_path) as f:
|
||||
CAPABILITIES = yaml.load(f)
|
||||
if full_load:
|
||||
logger.debug("Loading and pickling capabilities")
|
||||
with open(capabilities_path) as cp, open(pickle_path, "wb") as pp:
|
||||
CAPABILITIES = yaml.safe_load(cp)
|
||||
if not CAPABILITIES:
|
||||
# yaml could not be loaded
|
||||
print(
|
||||
f"Capabilities yaml from {capabilities_path} could not be loaded.\n"
|
||||
"This python package seems to be broken. If it has been installed "
|
||||
"from official sources, please report an issue on GitHub.\n"
|
||||
"Currently loaded capabilities:\n"
|
||||
f"{CAPABILITIES}"
|
||||
)
|
||||
CAPABILITIES = {
|
||||
"profiles": {
|
||||
"default": {
|
||||
"name": "BrokenDefault",
|
||||
"notes": "The integrated capabilities file could not be found and has been replaced.",
|
||||
"codePages": {"0": "Broken"},
|
||||
"features": {},
|
||||
},
|
||||
},
|
||||
"encodings": {
|
||||
"Broken": {
|
||||
"name": "Broken",
|
||||
"notes": "The configuration is broken.",
|
||||
}
|
||||
},
|
||||
}
|
||||
print(
|
||||
"Created a minimal backup profile, "
|
||||
"many functionalities of the library will not work:\n"
|
||||
f"{CAPABILITIES}"
|
||||
)
|
||||
pickle.dump(CAPABILITIES, pp, protocol=2)
|
||||
|
||||
PROFILES = CAPABILITIES['profiles']
|
||||
logger.debug("Finished loading capabilities took %.2fs", time.time() - t0)
|
||||
|
||||
|
||||
class NotSupported(Exception):
|
||||
"""Raised if a requested feature is not suppored by the
|
||||
printer profile.
|
||||
"""
|
||||
"""Raised if a requested feature is not supported by the printer profile."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
BARCODE_B = 'barcodeB'
|
||||
BARCODE_B = "barcodeB"
|
||||
|
||||
|
||||
class BaseProfile(object):
|
||||
"""This respresents a printer profile.
|
||||
class BaseProfile:
|
||||
"""This represents a printer profile.
|
||||
|
||||
A printer profile knows about the number of columns, supported
|
||||
features, colors and more.
|
||||
"""
|
||||
|
||||
profile_data = {}
|
||||
profile_data: Dict[str, Any] = {}
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Get a data element from the profile."""
|
||||
return self.profile_data[name]
|
||||
|
||||
def get_font(self, font):
|
||||
"""Return the escpos index for `font`. Makes sure that
|
||||
the requested `font` is valid.
|
||||
def get_font(self, font) -> int:
|
||||
"""Return the escpos index for `font`.
|
||||
|
||||
Makes sure that the requested `font` is valid.
|
||||
"""
|
||||
font = {'a': 0, 'b': 1}.get(font, font)
|
||||
if not six.text_type(font) in self.fonts:
|
||||
raise NotSupported(
|
||||
'"{}" is not a valid font in the current profile'.format(font))
|
||||
font = {"a": 0, "b": 1}.get(font, font)
|
||||
if not str(font) in self.fonts:
|
||||
raise NotSupported(f'"{font}" is not a valid font in the current profile')
|
||||
return font
|
||||
|
||||
def get_columns(self, font):
|
||||
""" Return the number of columns for the given font.
|
||||
"""
|
||||
def get_columns(self, font) -> int:
|
||||
"""Return the number of columns for the given font."""
|
||||
font = self.get_font(font)
|
||||
return self.fonts[six.text_type(font)]['columns']
|
||||
columns = self.fonts[str(font)]["columns"]
|
||||
assert type(columns) is int
|
||||
return columns
|
||||
|
||||
def supports(self, feature):
|
||||
"""Return true/false for the given feature.
|
||||
"""
|
||||
def supports(self, feature) -> bool:
|
||||
"""Return true/false for the given feature."""
|
||||
return self.features.get(feature)
|
||||
|
||||
def get_code_pages(self):
|
||||
"""Return the support code pages as a {name: index} dict.
|
||||
"""
|
||||
def get_code_pages(self) -> Dict[str, int]:
|
||||
"""Return the support code pages as a ``{name: index}`` dict."""
|
||||
return {v: k for k, v in self.codePages.items()}
|
||||
|
||||
|
||||
def get_profile(name=None, **kwargs):
|
||||
"""Get the profile by name; if no name is given, return the
|
||||
default profile.
|
||||
def get_profile(name: Optional[str] = None, **kwargs):
|
||||
"""Get a profile by name.
|
||||
|
||||
If no name is given, return the default profile.
|
||||
"""
|
||||
if isinstance(name, Profile):
|
||||
return name
|
||||
|
||||
clazz = get_profile_class(name or 'default')
|
||||
clazz = get_profile_class(name or "default")
|
||||
return clazz(**kwargs)
|
||||
|
||||
|
||||
CLASS_CACHE = {}
|
||||
|
||||
|
||||
def get_profile_class(name):
|
||||
"""For the given profile name, load the data from the external
|
||||
def get_profile_class(name: str) -> Type[BaseProfile]:
|
||||
"""Load a profile class.
|
||||
|
||||
For the given profile name, load the data from the external
|
||||
database, then generate dynamically a class.
|
||||
"""
|
||||
if name not in CLASS_CACHE:
|
||||
profile_data = PROFILES[name]
|
||||
profiles: Dict[str, Any] = CAPABILITIES["profiles"]
|
||||
profile_data = profiles[name]
|
||||
profile_name = clean(name)
|
||||
class_name = '{}{}Profile'.format(
|
||||
profile_name[0].upper(), profile_name[1:])
|
||||
new_class = type(class_name, (BaseProfile,), {'profile_data': profile_data})
|
||||
class_name = f"{profile_name[0].upper()}{profile_name[1:]}Profile"
|
||||
new_class = type(class_name, (BaseProfile,), {"profile_data": profile_data})
|
||||
CLASS_CACHE[name] = new_class
|
||||
|
||||
return CLASS_CACHE[name]
|
||||
|
||||
|
||||
def clean(s):
|
||||
def clean(s: str) -> str:
|
||||
"""Clean profile name."""
|
||||
# Remove invalid characters
|
||||
s = re.sub('[^0-9a-zA-Z_]', '', s)
|
||||
s = re.sub("[^0-9a-zA-Z_]", "", s)
|
||||
# Remove leading characters until we find a letter or underscore
|
||||
s = re.sub('^[^a-zA-Z_]+', '', s)
|
||||
s = re.sub("^[^a-zA-Z_]+", "", s)
|
||||
return str(s)
|
||||
|
||||
|
||||
class Profile(get_profile_class('default')):
|
||||
"""
|
||||
For users, who want to provide their profile
|
||||
# mute the mypy type issue with this dynamic base class function for now (: Any)
|
||||
ProfileBaseClass: Any = get_profile_class("default")
|
||||
|
||||
|
||||
class Profile(ProfileBaseClass):
|
||||
"""Profile class for user usage.
|
||||
|
||||
For users, who want to provide their own profile.
|
||||
"""
|
||||
|
||||
def __init__(self, columns=None, features=None):
|
||||
def __init__(self, columns: Optional[int] = None, features=None) -> None:
|
||||
"""Initialize profile."""
|
||||
super(Profile, self).__init__()
|
||||
|
||||
self.columns = columns
|
||||
self.features = features or {}
|
||||
|
||||
def get_columns(self, font):
|
||||
def get_columns(self, font) -> int:
|
||||
"""Get column count of printer."""
|
||||
if self.columns is not None:
|
||||
return self.columns
|
||||
|
||||
|
@@ -1,41 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
""" CLI
|
||||
"""CLI.
|
||||
|
||||
This module acts as a command line interface for python-escpos. It mirrors
|
||||
closely the available ESCPOS commands while adding a couple extra ones for convience.
|
||||
closely the available ESCPOS commands while adding a couple extra ones for convenience.
|
||||
|
||||
It requires you to have a configuration file. See documentation for details.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
import platform
|
||||
from typing import Any, Dict, List
|
||||
|
||||
try:
|
||||
import argcomplete
|
||||
except ImportError:
|
||||
# this CLI works nevertheless without argcomplete
|
||||
pass # noqa
|
||||
import sys
|
||||
import six
|
||||
from . import config
|
||||
|
||||
from . import config, escpos
|
||||
from . import printer as escpos_printer_module
|
||||
from . import version
|
||||
|
||||
|
||||
# Must be defined before it's used in DEMO_FUNCTIONS
|
||||
def str_to_bool(string):
|
||||
""" Used as a type in argparse so that we get back a proper
|
||||
bool instead of always True
|
||||
def str_to_bool(string: str) -> bool:
|
||||
"""Convert string to bool.
|
||||
|
||||
Used as a type in argparse so that we get back a proper
|
||||
bool instead of always True.
|
||||
"""
|
||||
return string.lower() in ('y', 'yes', '1', 'true')
|
||||
return string.lower() in ("y", "yes", "1", "true")
|
||||
|
||||
|
||||
# A list of functions that work better with a newline to be sent after them.
|
||||
REQUIRES_NEWLINE = ('qr', 'barcode', 'text', 'block_text')
|
||||
REQUIRES_NEWLINE = ("qr", "barcode", "text", "block_text")
|
||||
|
||||
|
||||
# Used in demo method
|
||||
@@ -43,40 +44,46 @@ REQUIRES_NEWLINE = ('qr', 'barcode', 'text', 'block_text')
|
||||
# manual translation is done in the case of barcodes_a -> barcode.
|
||||
# Value: A list of dictionaries to pass to the escpos function as arguments.
|
||||
DEMO_FUNCTIONS = {
|
||||
'text': [
|
||||
{'txt': 'Hello, World!\n', }
|
||||
"text": [
|
||||
{
|
||||
"txt": "Hello, World!\n",
|
||||
}
|
||||
],
|
||||
'qr': [
|
||||
{'content': 'This tests a QR code'},
|
||||
{'content': 'https://en.wikipedia.org/'}
|
||||
"qr": [
|
||||
{"content": "This tests a QR code"},
|
||||
{"content": "https://en.wikipedia.org/"},
|
||||
],
|
||||
'barcodes_a': [
|
||||
{'bc': 'UPC-A', 'code': '13243546576'},
|
||||
{'bc': 'UPC-E', 'code': '132435'},
|
||||
{'bc': 'EAN13', 'code': '1324354657687'},
|
||||
{'bc': 'EAN8', 'code': '1324354'},
|
||||
{'bc': 'CODE39', 'code': 'TEST'},
|
||||
{'bc': 'ITF', 'code': '55867492279103'},
|
||||
{'bc': 'NW7', 'code': 'A00000000A'},
|
||||
"barcodes_a": [
|
||||
{"bc": "UPC-A", "code": "13243546576"},
|
||||
{"bc": "UPC-E", "code": "132435"},
|
||||
{"bc": "EAN13", "code": "4006381333931"},
|
||||
{"bc": "EAN8", "code": "1324354"},
|
||||
{"bc": "CODE39", "code": "TEST"},
|
||||
{"bc": "ITF", "code": "55867492279103"},
|
||||
{"bc": "NW7", "code": "A00000000A"},
|
||||
],
|
||||
'barcodes_b': [
|
||||
{'bc': 'UPC-A', 'code': '13243546576', 'function_type': 'B'},
|
||||
{'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'},
|
||||
{'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'},
|
||||
{'bc': 'EAN8', 'code': '1324354', 'function_type': 'B'},
|
||||
{'bc': 'CODE39', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'},
|
||||
{'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'},
|
||||
{'bc': 'CODE93', 'code': 'A00000000A', 'function_type': 'B'},
|
||||
{'bc': 'CODE93', 'code': '1324354657687', 'function_type': 'B'},
|
||||
{'bc': 'CODE128A', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'CODE128B', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'CODE128C', 'code': 'TEST', 'function_type': 'B'},
|
||||
{'bc': 'GS1-128', 'code': '00123456780000000001', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Omnidirectional', 'code': '0000000000000', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Limited', 'code': '0000000000000', 'function_type': 'B'},
|
||||
{'bc': 'GS1 DataBar Expanded', 'code': '00AAAAAAA', 'function_type': 'B'},
|
||||
"barcodes_b": [
|
||||
{"bc": "UPC-A", "code": "13243546576", "function_type": "B"},
|
||||
{"bc": "UPC-E", "code": "132435", "function_type": "B"},
|
||||
{"bc": "EAN13", "code": "4006381333931", "function_type": "B"},
|
||||
{"bc": "EAN8", "code": "1324354", "function_type": "B"},
|
||||
{"bc": "CODE39", "code": "TEST", "function_type": "B"},
|
||||
{"bc": "ITF", "code": "55867492279103", "function_type": "B"},
|
||||
{"bc": "NW7", "code": "A00000000A", "function_type": "B"},
|
||||
{"bc": "CODE93", "code": "A00000000A", "function_type": "B"},
|
||||
{"bc": "CODE93", "code": "1324354657687", "function_type": "B"},
|
||||
{"bc": "CODE128A", "code": "TEST", "function_type": "B"},
|
||||
{"bc": "CODE128B", "code": "TEST", "function_type": "B"},
|
||||
{"bc": "CODE128C", "code": "TEST", "function_type": "B"},
|
||||
{"bc": "GS1-128", "code": "00123456780000000001", "function_type": "B"},
|
||||
{
|
||||
"bc": "GS1 DataBar Omnidirectional",
|
||||
"code": "0000000000000",
|
||||
"function_type": "B",
|
||||
},
|
||||
{"bc": "GS1 DataBar Truncated", "code": "0000000000000", "function_type": "B"},
|
||||
{"bc": "GS1 DataBar Limited", "code": "0000000000000", "function_type": "B"},
|
||||
{"bc": "GS1 DataBar Expanded", "code": "00AAAAAAA", "function_type": "B"},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -86,435 +93,484 @@ DEMO_FUNCTIONS = {
|
||||
# parser: A dict of args for command_parsers.add_parser
|
||||
# defaults: A dict of args for subparser.set_defaults
|
||||
# arguments: A list of dicts of args for subparser.add_argument
|
||||
ESCPOS_COMMANDS = [
|
||||
ESCPOS_COMMANDS: List[Dict[str, Any]] = [
|
||||
{
|
||||
'parser': {
|
||||
'name': 'qr',
|
||||
'help': 'Print a QR code',
|
||||
"parser": {
|
||||
"name": "qr",
|
||||
"help": "Print a QR code",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'qr',
|
||||
"defaults": {
|
||||
"func": "qr",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--content',),
|
||||
'help': 'Text to print as a qr code',
|
||||
'required': True,
|
||||
"option_strings": ("--content",),
|
||||
"help": "Text to print as a qr code",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--size',),
|
||||
'help': 'QR code size (1-16) [default:3]',
|
||||
'required': False,
|
||||
'type': int,
|
||||
"option_strings": ("--size",),
|
||||
"help": "QR code size (1-16) [default:3]",
|
||||
"required": False,
|
||||
"type": int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"parser": {
|
||||
"name": "barcode",
|
||||
"help": "Print a barcode",
|
||||
},
|
||||
"defaults": {
|
||||
"func": "barcode",
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"option_strings": ("--code",),
|
||||
"help": "Barcode data to print",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--bc",),
|
||||
"help": "Barcode format",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--height",),
|
||||
"help": "Barcode height in px",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--width",),
|
||||
"help": "Barcode width",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--pos",),
|
||||
"help": "Label position",
|
||||
"choices": ["BELOW", "ABOVE", "BOTH", "OFF"],
|
||||
},
|
||||
{
|
||||
"option_strings": ("--font",),
|
||||
"help": "Label font",
|
||||
"choices": ["A", "B"],
|
||||
},
|
||||
{
|
||||
"option_strings": ("--align_ct",),
|
||||
"help": "Align barcode center",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--function_type",),
|
||||
"help": "ESCPOS function type",
|
||||
"choices": ["A", "B"],
|
||||
},
|
||||
{
|
||||
"option_strings": ("--force_software",),
|
||||
"help": "Force render and print barcode as an image",
|
||||
"choices": ["graphics", "bitImageColumn", "bitImageRaster"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"parser": {
|
||||
"name": "text",
|
||||
"help": "Print plain text",
|
||||
},
|
||||
"defaults": {
|
||||
"func": "text",
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"option_strings": ("--txt",),
|
||||
"help": "Plain text to print",
|
||||
"required": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'barcode',
|
||||
'help': 'Print a barcode',
|
||||
"parser": {
|
||||
"name": "block_text",
|
||||
"help": "Print wrapped text",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'barcode',
|
||||
"defaults": {
|
||||
"func": "block_text",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--code',),
|
||||
'help': 'Barcode data to print',
|
||||
'required': True,
|
||||
"option_strings": ("--txt",),
|
||||
"help": "block_text to print",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--bc',),
|
||||
'help': 'Barcode format',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--height',),
|
||||
'help': 'Barcode height in px',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--width',),
|
||||
'help': 'Barcode width',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--pos',),
|
||||
'help': 'Label position',
|
||||
'choices': ['BELOW', 'ABOVE', 'BOTH', 'OFF'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--font',),
|
||||
'help': 'Label font',
|
||||
'choices': ['A', 'B'],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--align_ct',),
|
||||
'help': 'Align barcode center',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--function_type',),
|
||||
'help': 'ESCPOS function type',
|
||||
'choices': ['A', 'B'],
|
||||
"option_strings": ("--columns",),
|
||||
"help": "Number of columns",
|
||||
"type": int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'text',
|
||||
'help': 'Print plain text',
|
||||
"parser": {
|
||||
"name": "cut",
|
||||
"help": "Cut the paper",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'text',
|
||||
"defaults": {
|
||||
"func": "cut",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--txt',),
|
||||
'help': 'Plain text to print',
|
||||
'required': True,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'block_text',
|
||||
'help': 'Print wrapped text',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'block_text',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--txt',),
|
||||
'help': 'block_text to print',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--columns',),
|
||||
'help': 'Number of columns',
|
||||
'type': int,
|
||||
"option_strings": ("--mode",),
|
||||
"help": "Type of cut",
|
||||
"choices": ["FULL", "PART"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'cut',
|
||||
'help': 'Cut the paper',
|
||||
"parser": {
|
||||
"name": "cashdraw",
|
||||
"help": "Kick the cash drawer",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'cut',
|
||||
"defaults": {
|
||||
"func": "cashdraw",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--mode',),
|
||||
'help': 'Type of cut',
|
||||
'choices': ['FULL', 'PART'],
|
||||
"option_strings": ("--pin",),
|
||||
"help": "Which PIN to kick",
|
||||
"choices": [2, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'cashdraw',
|
||||
'help': 'Kick the cash drawer',
|
||||
"parser": {
|
||||
"name": "image",
|
||||
"help": "Print an image",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'cashdraw',
|
||||
"defaults": {
|
||||
"func": "image",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--pin',),
|
||||
'help': 'Which PIN to kick',
|
||||
'choices': [2, 5],
|
||||
"option_strings": ("--img_source",),
|
||||
"help": "Path to image",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--impl",),
|
||||
"help": "Implementation to use",
|
||||
"choices": ["bitImageRaster", "bitImageColumn", "graphics"],
|
||||
},
|
||||
{
|
||||
"option_strings": ("--high_density_horizontal",),
|
||||
"help": "Image density (horizontal)",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
{
|
||||
"option_strings": ("--high_density_vertical",),
|
||||
"help": "Image density (vertical)",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'image',
|
||||
'help': 'Print an image',
|
||||
"parser": {
|
||||
"name": "fullimage",
|
||||
"help": "Print a fullimage",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'image',
|
||||
"defaults": {
|
||||
"func": "fullimage",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--img_source',),
|
||||
'help': 'Path to image',
|
||||
'required': True,
|
||||
"option_strings": ("--img",),
|
||||
"help": "Path to img",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--impl',),
|
||||
'help': 'Implementation to use',
|
||||
'choices': ['bitImageRaster', 'bitImageColumn', 'graphics'],
|
||||
"option_strings": ("--max_height",),
|
||||
"help": "Max height of image in px",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--high_density_horizontal',),
|
||||
'help': 'Image density (horizontal)',
|
||||
'type': str_to_bool,
|
||||
"option_strings": ("--width",),
|
||||
"help": "Max width of image in px",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--high_density_vertical',),
|
||||
'help': 'Image density (vertical)',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
|
||||
],
|
||||
"option_strings": ("--histeq",),
|
||||
"help": "Equalize the histogram",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'fullimage',
|
||||
'help': 'Print a fullimage',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'fullimage',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--img',),
|
||||
'help': 'Path to img',
|
||||
'required': True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--max_height',),
|
||||
'help': 'Max height of image in px',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--width',),
|
||||
'help': 'Max width of image in px',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--histeq',),
|
||||
'help': 'Equalize the histrogram',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--bandsize',),
|
||||
'help': 'Size of bands to divide into when printing',
|
||||
'type': int,
|
||||
"option_strings": ("--bandsize",),
|
||||
"help": "Size of bands to divide into when printing",
|
||||
"type": int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'charcode',
|
||||
'help': 'Set character code table',
|
||||
"parser": {
|
||||
"name": "charcode",
|
||||
"help": "Set character code table",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'charcode',
|
||||
"defaults": {
|
||||
"func": "charcode",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--code',),
|
||||
'help': 'Character code',
|
||||
'required': True,
|
||||
"option_strings": ("--code",),
|
||||
"help": "Character code",
|
||||
"required": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'set',
|
||||
'help': 'Set text properties',
|
||||
"parser": {
|
||||
"name": "set",
|
||||
"help": "Set text properties",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'set',
|
||||
"defaults": {
|
||||
"func": "set",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--align',),
|
||||
'help': 'Horizontal alignment',
|
||||
'choices': ['left', 'center', 'right'],
|
||||
"option_strings": ("--align",),
|
||||
"help": "Horizontal alignment",
|
||||
"choices": ["left", "center", "right"],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--font',),
|
||||
'help': 'Font choice',
|
||||
'choices': ['left', 'center', 'right'],
|
||||
"option_strings": ("--font",),
|
||||
"help": "Font choice",
|
||||
"choices": ["left", "center", "right"],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--text_type',),
|
||||
'help': 'Text properties',
|
||||
'choices': ['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'],
|
||||
"option_strings": ("--text_type",),
|
||||
"help": "Text properties",
|
||||
"choices": ["B", "U", "U2", "BU", "BU2", "NORMAL"],
|
||||
},
|
||||
{
|
||||
'option_strings': ('--width',),
|
||||
'help': 'Width multiplier',
|
||||
'type': int,
|
||||
"option_strings": ("--width",),
|
||||
"help": "Width multiplier",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--height',),
|
||||
'help': 'Height multiplier',
|
||||
'type': int,
|
||||
"option_strings": ("--height",),
|
||||
"help": "Height multiplier",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--density',),
|
||||
'help': 'Print density',
|
||||
'type': int,
|
||||
"option_strings": ("--density",),
|
||||
"help": "Print density",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--invert',),
|
||||
'help': 'White on black printing',
|
||||
'type': str_to_bool,
|
||||
"option_strings": ("--invert",),
|
||||
"help": "White on black printing",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--smooth',),
|
||||
'help': 'Text smoothing. Effective on >: 4x4 text',
|
||||
'type': str_to_bool,
|
||||
"option_strings": ("--smooth",),
|
||||
"help": "Text smoothing. Effective on >: 4x4 text",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--flip',),
|
||||
'help': 'Text smoothing. Effective on >: 4x4 text',
|
||||
'type': str_to_bool,
|
||||
"option_strings": ("--flip",),
|
||||
"help": "Text smoothing. Effective on >: 4x4 text",
|
||||
"type": str_to_bool,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'hw',
|
||||
'help': 'Hardware operations',
|
||||
"parser": {
|
||||
"name": "hw",
|
||||
"help": "Hardware operations",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'hw',
|
||||
"defaults": {
|
||||
"func": "hw",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--hw',),
|
||||
'help': 'Operation',
|
||||
'choices': ['INIT', 'SELECT', 'RESET'],
|
||||
'required': True,
|
||||
"option_strings": ("--hw",),
|
||||
"help": "Operation",
|
||||
"choices": ["INIT", "SELECT", "RESET"],
|
||||
"required": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'control',
|
||||
'help': 'Control sequences',
|
||||
"parser": {
|
||||
"name": "control",
|
||||
"help": "Control sequences",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'control',
|
||||
"defaults": {
|
||||
"func": "control",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--ctl',),
|
||||
'help': 'Control sequence',
|
||||
'choices': ['LF', 'FF', 'CR', 'HT', 'VT'],
|
||||
'required': True,
|
||||
"option_strings": ("--ctl",),
|
||||
"help": "Control sequence",
|
||||
"choices": ["LF", "FF", "CR", "HT", "VT"],
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--pos',),
|
||||
'help': 'Horizontal tab position (1-4)',
|
||||
'type': int,
|
||||
"option_strings": ("--pos",),
|
||||
"help": "Horizontal tab position (1-4)",
|
||||
"type": int,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'panel_buttons',
|
||||
'help': 'Controls panel buttons',
|
||||
"parser": {
|
||||
"name": "panel_buttons",
|
||||
"help": "Controls panel buttons",
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'panel_buttons',
|
||||
"defaults": {
|
||||
"func": "panel_buttons",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--enable',),
|
||||
'help': 'Feed button enabled',
|
||||
'type': str_to_bool,
|
||||
'required': True,
|
||||
"option_strings": ("--enable",),
|
||||
"help": "Feed button enabled",
|
||||
"type": str_to_bool,
|
||||
"required": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'raw',
|
||||
'help': 'Raw data',
|
||||
"parser": {
|
||||
"name": "raw",
|
||||
"help": "Raw data",
|
||||
},
|
||||
'defaults': {
|
||||
'func': '_raw',
|
||||
"defaults": {
|
||||
"func": "_raw",
|
||||
},
|
||||
'arguments': [
|
||||
"arguments": [
|
||||
{
|
||||
'option_strings': ('--msg',),
|
||||
'help': 'Raw data to send',
|
||||
'required': True,
|
||||
"option_strings": ("--msg",),
|
||||
"help": "Raw data to send",
|
||||
"required": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
|
||||
Handles loading of configuration and creating and processing of command
|
||||
line arguments. Called when run from a CLI.
|
||||
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='CLI for python-escpos',
|
||||
epilog='Printer configuration is defined in the python-escpos config'
|
||||
'file. See documentation for details.',
|
||||
def print_extended_information() -> None:
|
||||
"""Print diagnostic information for bug reports."""
|
||||
print(f"* python-escpos version: `{version.version}`")
|
||||
print(
|
||||
f"* python version: `{platform.python_implementation()} v{platform.python_version()}`"
|
||||
)
|
||||
print(f"* platform: `{platform.platform()}`")
|
||||
print(
|
||||
f"* printer driver `USB` is usable: `{escpos_printer_module.Usb.is_usable()}`"
|
||||
)
|
||||
print(
|
||||
f"* printer driver `File` is usable: `{escpos_printer_module.File.is_usable()}`"
|
||||
)
|
||||
print(
|
||||
f"* printer driver `Network` is usable: `{escpos_printer_module.Network.is_usable()}`"
|
||||
)
|
||||
print(
|
||||
f"* printer driver `Serial` is usable: `{escpos_printer_module.Serial.is_usable()}`"
|
||||
)
|
||||
print(f"* printer driver `LP` is usable: `{escpos_printer_module.LP.is_usable()}`")
|
||||
print(
|
||||
f"* printer driver `Dummy` is usable: `{escpos_printer_module.Dummy.is_usable()}`"
|
||||
)
|
||||
print(
|
||||
f"* printer driver `CupsPrinter` is usable: `{escpos_printer_module.CupsPrinter.is_usable()}`"
|
||||
)
|
||||
print(
|
||||
f"* printer driver `Win32Raw` is usable: `{escpos_printer_module.Win32Raw.is_usable()}`"
|
||||
)
|
||||
|
||||
parser.register('type', 'bool', str_to_bool)
|
||||
|
||||
def generate_parser() -> argparse.ArgumentParser:
|
||||
"""Generate an argparse parser."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="CLI for python-escpos",
|
||||
epilog="Printer configuration is defined in the python-escpos config"
|
||||
"file. See documentation for details.",
|
||||
)
|
||||
|
||||
parser.register("type", "bool", str_to_bool)
|
||||
|
||||
# Allow config file location to be passed
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
help='Alternate path to the configuration file',
|
||||
"-c",
|
||||
"--config",
|
||||
help="Alternate path to the configuration file",
|
||||
)
|
||||
|
||||
# Everything interesting runs off of a subparser so we can use the format
|
||||
# cli [subparser] -args
|
||||
command_subparsers = parser.add_subparsers(
|
||||
title='ESCPOS Command',
|
||||
dest='parser',
|
||||
title="ESCPOS Command",
|
||||
dest="parser",
|
||||
)
|
||||
# fix inconsistencies in the behaviour of some versions of argparse
|
||||
command_subparsers.required = False # force 'required' testing
|
||||
|
||||
# Build the ESCPOS command arguments
|
||||
for command in ESCPOS_COMMANDS:
|
||||
parser_command = command_subparsers.add_parser(**command['parser'])
|
||||
parser_command.set_defaults(**command['defaults'])
|
||||
for argument in command['arguments']:
|
||||
option_strings = argument.pop('option_strings')
|
||||
parser_command = command_subparsers.add_parser(**command["parser"])
|
||||
parser_command.set_defaults(**command["defaults"])
|
||||
for argument in command["arguments"]:
|
||||
option_strings = argument.pop("option_strings")
|
||||
parser_command.add_argument(*option_strings, **argument)
|
||||
|
||||
# Build any custom arguments
|
||||
parser_command_demo = command_subparsers.add_parser('demo',
|
||||
help='Demonstrates various functions')
|
||||
parser_command_demo.set_defaults(func='demo')
|
||||
parser_command_demo = command_subparsers.add_parser(
|
||||
"demo", help="Demonstrates various functions"
|
||||
)
|
||||
parser_command_demo.set_defaults(func="demo")
|
||||
demo_group = parser_command_demo.add_mutually_exclusive_group()
|
||||
demo_group.add_argument(
|
||||
'--barcodes-a',
|
||||
help='Print demo barcodes for function type A',
|
||||
action='store_true',
|
||||
"--barcodes-a",
|
||||
help="Print demo barcodes for function type A",
|
||||
action="store_true",
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--barcodes-b',
|
||||
help='Print demo barcodes for function type B',
|
||||
action='store_true',
|
||||
"--barcodes-b",
|
||||
help="Print demo barcodes for function type B",
|
||||
action="store_true",
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--qr',
|
||||
help='Print some demo QR codes',
|
||||
action='store_true',
|
||||
"--qr",
|
||||
help="Print some demo QR codes",
|
||||
action="store_true",
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--text',
|
||||
help='Print some demo text',
|
||||
action='store_true',
|
||||
"--text",
|
||||
help="Print some demo text",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser_command_version = command_subparsers.add_parser('version',
|
||||
help='Print the version of python-escpos')
|
||||
parser_command_version = command_subparsers.add_parser(
|
||||
"version", help="Print the version information of python-escpos"
|
||||
)
|
||||
parser_command_version.set_defaults(version=True)
|
||||
|
||||
parser_command_version_extended = command_subparsers.add_parser(
|
||||
"version_extended",
|
||||
help="Print the extended version information of python-escpos (for bug reports)",
|
||||
)
|
||||
parser_command_version_extended.set_defaults(version_extended=True)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Handle main entry point of CLI script.
|
||||
|
||||
Handles loading of configuration and creating and processing of command
|
||||
line arguments. Called when run from a CLI.
|
||||
"""
|
||||
parser = generate_parser()
|
||||
|
||||
# hook in argcomplete
|
||||
if 'argcomplete' in globals():
|
||||
if "argcomplete" in globals():
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
# Get only arguments actually passed
|
||||
@@ -522,16 +578,21 @@ def main():
|
||||
if not args_dict:
|
||||
parser.print_help()
|
||||
sys.exit()
|
||||
command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v is not None)
|
||||
command_arguments = dict([k, v] for k, v in args_dict.items() if v is not None)
|
||||
|
||||
# If version should be printed, do this, then exit
|
||||
print_version = command_arguments.pop('version', None)
|
||||
print_version = command_arguments.pop("version", None)
|
||||
if print_version:
|
||||
print(version.version)
|
||||
sys.exit()
|
||||
|
||||
print_version_extended = command_arguments.pop("version_extended", None)
|
||||
if print_version_extended:
|
||||
print_extended_information()
|
||||
sys.exit()
|
||||
|
||||
# If there was a config path passed, grab it
|
||||
config_path = command_arguments.pop('config', None)
|
||||
config_path = command_arguments.pop("config", None)
|
||||
|
||||
# Load the configuration and defined printer
|
||||
saved_config = config.Config()
|
||||
@@ -539,12 +600,12 @@ def main():
|
||||
printer = saved_config.printer()
|
||||
|
||||
if not printer:
|
||||
raise Exception('No printers loaded from config')
|
||||
raise Exception("No printers loaded from config")
|
||||
|
||||
target_command = command_arguments.pop('func')
|
||||
target_command = command_arguments.pop("func")
|
||||
|
||||
# remove helper-argument 'parser' from dict
|
||||
command_arguments.pop('parser', None)
|
||||
command_arguments.pop("parser", None)
|
||||
|
||||
if hasattr(printer, target_command):
|
||||
# print command with args
|
||||
@@ -552,13 +613,14 @@ def main():
|
||||
if target_command in REQUIRES_NEWLINE:
|
||||
printer.text("\n")
|
||||
else:
|
||||
command_arguments['printer'] = printer
|
||||
command_arguments["printer"] = printer
|
||||
globals()[target_command](**command_arguments)
|
||||
|
||||
|
||||
def demo(printer, **kwargs):
|
||||
"""
|
||||
Prints specificed demos. Called when CLI is passed `demo`. This function
|
||||
def demo(printer: escpos.Escpos, **kwargs) -> None:
|
||||
"""Print demos.
|
||||
|
||||
Called when CLI is passed `demo`. This function
|
||||
uses the DEMO_FUNCTIONS dictionary.
|
||||
|
||||
:param printer: A printer from escpos.printer
|
||||
@@ -568,14 +630,14 @@ def demo(printer, **kwargs):
|
||||
for demo_choice in kwargs.keys():
|
||||
command = getattr(
|
||||
printer,
|
||||
demo_choice
|
||||
.replace('barcodes_a', 'barcode')
|
||||
.replace('barcodes_b', 'barcode')
|
||||
demo_choice.replace("barcodes_a", "barcode").replace(
|
||||
"barcodes_b", "barcode"
|
||||
),
|
||||
)
|
||||
for params in DEMO_FUNCTIONS[demo_choice]:
|
||||
command(**params)
|
||||
printer.cut()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@@ -1,24 +1,28 @@
|
||||
"""Helper module for codepage handling."""
|
||||
from .capabilities import CAPABILITIES
|
||||
|
||||
|
||||
class CodePageManager:
|
||||
"""Holds information about all the code pages (as defined
|
||||
in escpos-printer-db).
|
||||
"""Holds information about all the code pages.
|
||||
|
||||
Information as defined in escpos-printer-db.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
"""Initialize codepage manager."""
|
||||
self.data = data
|
||||
|
||||
def get_all(self):
|
||||
return self.data.values()
|
||||
|
||||
@staticmethod
|
||||
def get_encoding_name(encoding):
|
||||
# TODO resolve the encoding alias
|
||||
"""Get encoding name.
|
||||
|
||||
.. todo:: Resolve the encoding alias.
|
||||
"""
|
||||
return encoding.upper()
|
||||
|
||||
def get_encoding(self, encoding):
|
||||
"""Return the encoding data."""
|
||||
return self.data[encoding]
|
||||
|
||||
|
||||
CodePages = CodePageManager(CAPABILITIES['encodings'])
|
||||
CodePages = CodePageManager(CAPABILITIES["encodings"])
|
||||
|
@@ -1,32 +1,27 @@
|
||||
"""ESC/POS configuration manager.
|
||||
|
||||
This module contains the implentations of abstract base class :py:class:`Config`.
|
||||
|
||||
This module contains the implementations of abstract base class :py:class:`Config`.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import appdirs
|
||||
import yaml
|
||||
|
||||
from . import printer
|
||||
from . import exceptions
|
||||
from . import exceptions, printer
|
||||
|
||||
|
||||
class Config(object):
|
||||
class Config:
|
||||
"""Configuration handler class.
|
||||
|
||||
This class loads configuration from a default or specificed directory. It
|
||||
This class loads configuration from a default or specified directory. It
|
||||
can create your defined printer and return it to you.
|
||||
"""
|
||||
_app_name = 'python-escpos'
|
||||
_config_file = 'config.yaml'
|
||||
|
||||
def __init__(self):
|
||||
_app_name = "python-escpos"
|
||||
_config_file = "config.yaml"
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize configuration.
|
||||
|
||||
Remember to add anything that needs to be reset between configurations
|
||||
@@ -38,7 +33,7 @@ class Config(object):
|
||||
self._printer_name = None
|
||||
self._printer_config = None
|
||||
|
||||
def _reset_config(self):
|
||||
def _reset_config(self) -> None:
|
||||
"""Clear the loaded configuration.
|
||||
|
||||
If we are loading a changed config, we don't want to have leftover
|
||||
@@ -51,64 +46,58 @@ class Config(object):
|
||||
self._printer_config = None
|
||||
|
||||
def load(self, config_path=None):
|
||||
""" Load and parse the configuration file using pyyaml
|
||||
"""Load and parse the configuration file using pyyaml.
|
||||
|
||||
:param config_path: An optional file path, file handle, or byte string
|
||||
for the configuration file.
|
||||
|
||||
"""
|
||||
|
||||
self._reset_config()
|
||||
|
||||
if not config_path:
|
||||
config_path = os.path.join(
|
||||
appdirs.user_config_dir(self._app_name),
|
||||
self._config_file
|
||||
appdirs.user_config_dir(self._app_name), self._config_file
|
||||
)
|
||||
if isinstance(config_path, pathlib.Path):
|
||||
# store string if posixpath
|
||||
config_path = config_path.as_posix()
|
||||
if not os.path.isfile(config_path):
|
||||
# supplied path is not a file --> assume default file
|
||||
config_path = os.path.join(config_path, self._config_file)
|
||||
|
||||
try:
|
||||
# First check if it's file like. If it is, pyyaml can load it.
|
||||
# I'm checking type instead of catching exceptions to keep the
|
||||
# exception handling simple
|
||||
if hasattr(config_path, 'read'):
|
||||
config = yaml.safe_load(config_path)
|
||||
else:
|
||||
# If it isn't, it's a path. We have to open it first, otherwise
|
||||
# pyyaml will try to read it as yaml
|
||||
with open(config_path, 'rb') as config_file:
|
||||
with open(config_path, "rb") as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
except EnvironmentError:
|
||||
raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format(
|
||||
config_path=str(config_path),
|
||||
))
|
||||
raise exceptions.ConfigNotFoundError(
|
||||
f"Couldn't read config at {config_path}"
|
||||
)
|
||||
except yaml.YAMLError:
|
||||
raise exceptions.ConfigSyntaxError('Error parsing YAML')
|
||||
raise exceptions.ConfigSyntaxError("Error parsing YAML")
|
||||
|
||||
if 'printer' in config:
|
||||
self._printer_config = config['printer']
|
||||
self._printer_name = self._printer_config.pop('type').title()
|
||||
if "printer" in config:
|
||||
self._printer_config = config["printer"]
|
||||
self._printer_name = self._printer_config.pop("type").title()
|
||||
|
||||
if not self._printer_name or not hasattr(printer, self._printer_name):
|
||||
raise exceptions.ConfigSyntaxError(
|
||||
'Printer type "{printer_name}" is invalid'.format(
|
||||
printer_name=self._printer_name,
|
||||
)
|
||||
f'Printer type "{self._printer_name}" is invalid'
|
||||
)
|
||||
|
||||
self._has_loaded = True
|
||||
|
||||
def printer(self):
|
||||
""" Returns a printer that was defined in the config, or throws an
|
||||
exception.
|
||||
"""Return a printer that was defined in the config.
|
||||
|
||||
This method loads the default config if one hasn't beeen already loaded.
|
||||
Throw an exception on error.
|
||||
|
||||
This method loads the default config if one has not been already loaded.
|
||||
|
||||
"""
|
||||
if not self._has_loaded:
|
||||
self.load()
|
||||
|
||||
if not self._printer_name:
|
||||
raise exceptions.ConfigSectionMissingError('printer')
|
||||
raise exceptions.ConfigSectionMissingError("printer")
|
||||
|
||||
if not self._printer:
|
||||
# We could catch init errors and make them a ConfigSyntaxError,
|
||||
|
@@ -1,139 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Set of ESC/POS Commands (Constants)
|
||||
|
||||
This module contains constants that are described in the esc/pos-documentation.
|
||||
Since there is no definitive and unified specification for all esc/pos-like printers the constants could later be
|
||||
This module contains constants that are described in the Esc/Pos-documentation.
|
||||
Since there is no definitive and unified specification for all Esc/Pos-like printers the constants could later be
|
||||
moved to `capabilities` as in `escpos-php by @mike42 <https://github.com/mike42/escpos-php>`_.
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:author: python-escpos developers
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from typing import Dict
|
||||
|
||||
import six
|
||||
|
||||
from .types import ConstTxtStyleClass
|
||||
|
||||
# Control characters
|
||||
# as labelled in http://www.novopos.ch/client/EPSON/TM-T20/TM-T20_eng_qr.pdf
|
||||
NUL = b'\x00'
|
||||
EOT = b'\x04'
|
||||
ENQ = b'\x05'
|
||||
DLE = b'\x10'
|
||||
DC4 = b'\x14'
|
||||
CAN = b'\x18'
|
||||
ESC = b'\x1b'
|
||||
FS = b'\x1c'
|
||||
GS = b'\x1d'
|
||||
# as labelled in https://www.novopos.ch/client/EPSON/TM-T20/TM-T20_eng_qr.pdf
|
||||
NUL: bytes = b"\x00"
|
||||
EOT: bytes = b"\x04"
|
||||
ENQ: bytes = b"\x05"
|
||||
DLE: bytes = b"\x10"
|
||||
DC4: bytes = b"\x14"
|
||||
CAN: bytes = b"\x18"
|
||||
ESC: bytes = b"\x1b"
|
||||
FS: bytes = b"\x1c"
|
||||
GS: bytes = b"\x1d"
|
||||
|
||||
# Feed control sequences
|
||||
CTL_LF = b'\n' # Print and line feed
|
||||
CTL_FF = b'\f' # Form feed
|
||||
CTL_CR = b'\r' # Carriage return
|
||||
CTL_HT = b'\t' # Horizontal tab
|
||||
CTL_SET_HT = ESC + b'\x44' # Set horizontal tab positions
|
||||
CTL_VT = b'\v' # Vertical tab
|
||||
CTL_LF: bytes = b"\n" #: Print and line feed
|
||||
CTL_FF: bytes = b"\f" #: Form feed
|
||||
CTL_CR: bytes = b"\r" #: Carriage return
|
||||
CTL_HT: bytes = b"\t" #: Horizontal tab
|
||||
CTL_SET_HT: bytes = ESC + b"\x44" #: Set horizontal tab positions
|
||||
CTL_VT: bytes = b"\v" #: Vertical tab
|
||||
|
||||
# Printer hardware
|
||||
HW_INIT = ESC + b'@' # Clear data in buffer and reset modes
|
||||
HW_SELECT = ESC + b'=\x01' # Printer select
|
||||
HW_INIT: bytes = ESC + b"@" # Clear data in buffer and reset modes
|
||||
HW_SELECT: bytes = ESC + b"=\x01" # Printer select
|
||||
|
||||
HW_RESET = ESC + b'\x3f\x0a\x00' # Reset printer hardware
|
||||
HW_RESET: bytes = ESC + b"\x3f\x0a\x00" # Reset printer hardware
|
||||
# (TODO: Where is this specified?)
|
||||
|
||||
# Cash Drawer (ESC p <pin> <on time: 2*ms> <off time: 2*ms>)
|
||||
_CASH_DRAWER = lambda m, t1='', t2='': ESC + b'p' + m + six.int2byte(t1) + six.int2byte(t2)
|
||||
CD_KICK_DEC_SEQUENCE = lambda esc, p, m, t1=50, t2=50: six.int2byte(esc) + six.int2byte(p) + six.int2byte(m) + six.int2byte(t1) + six.int2byte(t2)
|
||||
CD_KICK_2 = _CASH_DRAWER(b'\x00', 50, 50) # Sends a pulse to pin 2 []
|
||||
CD_KICK_5 = _CASH_DRAWER(b'\x01', 50, 50) # Sends a pulse to pin 5 []
|
||||
_CASH_DRAWER = lambda m, t1="", t2="": ESC + b"p" + m + bytes((t1, t2))
|
||||
|
||||
#: decimal cash drawer kick sequence
|
||||
CD_KICK_DEC_SEQUENCE = (
|
||||
lambda esc, p, m, t1=50, t2=50: six.int2byte(esc)
|
||||
+ six.int2byte(p)
|
||||
+ six.int2byte(m)
|
||||
+ six.int2byte(t1)
|
||||
+ six.int2byte(t2)
|
||||
)
|
||||
#: Sends a pulse to pin 2 []
|
||||
CD_KICK_2: bytes = _CASH_DRAWER(b"\x00", 50, 50)
|
||||
#: Sends a pulse to pin 5 []
|
||||
CD_KICK_5: bytes = _CASH_DRAWER(b"\x01", 50, 50)
|
||||
|
||||
# Paper Cutter
|
||||
_CUT_PAPER = lambda m: GS + b'V' + m
|
||||
PAPER_FULL_CUT = _CUT_PAPER(b'\x00') # Full cut paper
|
||||
PAPER_PART_CUT = _CUT_PAPER(b'\x01') # Partial cut paper
|
||||
_CUT_PAPER = lambda m: GS + b"V" + m
|
||||
PAPER_FULL_CUT: bytes = _CUT_PAPER(b"\x00") #: Full cut paper
|
||||
PAPER_PART_CUT: bytes = _CUT_PAPER(b"\x01") #: Partial cut paper
|
||||
|
||||
# Beep
|
||||
BEEP = b'\x07'
|
||||
# Beep (please note that the actual beep sequence may differ between devices)
|
||||
BEEP: bytes = b"\x07"
|
||||
|
||||
# Internal buzzer (only supported printers)
|
||||
BUZZER: bytes = ESC + b"\x42"
|
||||
|
||||
# Panel buttons (e.g. the FEED button)
|
||||
_PANEL_BUTTON = lambda n: ESC + b'c5' + six.int2byte(n)
|
||||
PANEL_BUTTON_ON = _PANEL_BUTTON(0) # enable all panel buttons
|
||||
PANEL_BUTTON_OFF = _PANEL_BUTTON(1) # disable all panel buttons
|
||||
_PANEL_BUTTON = lambda n: ESC + b"c5" + six.int2byte(n)
|
||||
PANEL_BUTTON_ON: bytes = _PANEL_BUTTON(0) # enable all panel buttons
|
||||
PANEL_BUTTON_OFF: bytes = _PANEL_BUTTON(1) # disable all panel buttons
|
||||
|
||||
# Line display printing
|
||||
LINE_DISPLAY_OPEN = ESC + b'\x3d\x02'
|
||||
LINE_DISPLAY_CLEAR = ESC + b'\x40'
|
||||
LINE_DISPLAY_CLOSE = ESC + b'\x3d\x01'
|
||||
LINE_DISPLAY_OPEN: bytes = ESC + b"\x3d\x02"
|
||||
LINE_DISPLAY_CLEAR: bytes = ESC + b"\x40"
|
||||
LINE_DISPLAY_CLOSE: bytes = ESC + b"\x3d\x01"
|
||||
|
||||
# Sheet modes
|
||||
SHEET_SLIP_MODE = ESC + b'\x63\x30\x04' # slip paper
|
||||
SHEET_ROLL_MODE = ESC + b'\x63\x30\x01' # paper roll
|
||||
SHEET_SLIP_MODE: bytes = ESC + b"\x63\x30\x04" # slip paper
|
||||
SHEET_ROLL_MODE: bytes = ESC + b"\x63\x30\x01" # paper roll
|
||||
|
||||
# Slip specific codes
|
||||
SLIP_EJECT: bytes = ESC + b"\x4b\xc0" # Eject the slip or cheque
|
||||
SLIP_SELECT: bytes = FS # Select the slip station as default station
|
||||
SLIP_SET_WAIT_TIME: bytes = (
|
||||
ESC + b"\x1b\x66"
|
||||
) # Set timeout waiting for a slip/cheque to be inserted
|
||||
SLIP_PRINT_AND_EJECT: bytes = (
|
||||
b"\x0c" # Print the buffer and eject (after waiting for the paper to be inserted)
|
||||
)
|
||||
|
||||
# Text format
|
||||
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
|
||||
# Printers" and tidy up this stuff too.
|
||||
TXT_SIZE = GS + b'!'
|
||||
TXT_SIZE: bytes = GS + b"!"
|
||||
|
||||
TXT_NORMAL = ESC + b'!\x00' # Normal text
|
||||
TXT_NORMAL: bytes = ESC + b"!\x00" # Normal text
|
||||
|
||||
|
||||
TXT_STYLE = {
|
||||
'bold': {
|
||||
False: ESC + b'\x45\x00', # Bold font OFF
|
||||
True: ESC + b'\x45\x01' # Bold font ON
|
||||
#: text style dictionary for :py:meth:`escpos.escpos.Escpos.set`
|
||||
TXT_STYLE: ConstTxtStyleClass = {
|
||||
"bold": {
|
||||
False: ESC + b"\x45\x00", # Bold font OFF
|
||||
True: ESC + b"\x45\x01", # Bold font ON
|
||||
},
|
||||
'underline': {
|
||||
0: ESC + b'\x2d\x00', # Underline font OFF
|
||||
1: ESC + b'\x2d\x01', # Underline font 1-dot ON
|
||||
2: ESC + b'\x2d\x02' # Underline font 2-dot ON
|
||||
"underline": {
|
||||
0: ESC + b"\x2d\x00", # Underline font OFF
|
||||
1: ESC + b"\x2d\x01", # Underline font 1-dot ON
|
||||
2: ESC + b"\x2d\x02", # Underline font 2-dot ON
|
||||
},
|
||||
'size': {
|
||||
'normal': TXT_NORMAL + ESC + b'!\x00', # Normal text
|
||||
'2h': TXT_NORMAL + ESC + b'!\x10', # Double height text
|
||||
'2w': TXT_NORMAL + ESC + b'!\x20', # Double width text
|
||||
'2x': TXT_NORMAL + ESC + b'!\x30' # Quad area text
|
||||
"size": {
|
||||
"normal": TXT_NORMAL + ESC + b"!\x00", # Normal text
|
||||
"2h": TXT_NORMAL + ESC + b"!\x10", # Double height text
|
||||
"2w": TXT_NORMAL + ESC + b"!\x20", # Double width text
|
||||
"2x": TXT_NORMAL + ESC + b"!\x30", # Quad area text
|
||||
},
|
||||
'font': {
|
||||
'a': ESC + b'\x4d\x00', # Font type A
|
||||
'b': ESC + b'\x4d\x00' # Font type B
|
||||
"font": {
|
||||
"a": ESC + b"\x4d\x00", # Font type A
|
||||
"b": ESC + b"\x4d\x00", # Font type B
|
||||
},
|
||||
'align': {
|
||||
'left': ESC + b'\x61\x00', # Left justification
|
||||
'center': ESC + b'\x61\x01', # Centering
|
||||
'right': ESC + b'\x61\x02' # Right justification
|
||||
"align": {
|
||||
"left": ESC + b"\x61\x00", # Left justification
|
||||
"center": ESC + b"\x61\x01", # Centering
|
||||
"right": ESC + b"\x61\x02", # Right justification
|
||||
},
|
||||
'invert': {
|
||||
True: GS + b'\x42\x01', # Inverse Printing ON
|
||||
False: GS + b'\x42\x00' # Inverse Printing OFF
|
||||
"invert": {
|
||||
True: GS + b"\x42\x01", # Inverse Printing ON
|
||||
False: GS + b"\x42\x00", # Inverse Printing OFF
|
||||
},
|
||||
'color': {
|
||||
'black': ESC + b'\x72\x00', # Default Color
|
||||
'red': ESC + b'\x72\x01' # Alternative Color, Usually Red
|
||||
"color": {
|
||||
"black": ESC + b"\x72\x00", # Default Color
|
||||
"red": ESC + b"\x72\x01", # Alternative Color, Usually Red
|
||||
},
|
||||
'flip': {
|
||||
True: ESC + b'\x7b\x01', # Flip ON
|
||||
False: ESC + b'\x7b\x00' # Flip OFF
|
||||
"flip": {True: ESC + b"\x7b\x01", False: ESC + b"\x7b\x00"}, # Flip ON # Flip OFF
|
||||
"density": {
|
||||
0: GS + b"\x7c\x00", # Printing Density -50%
|
||||
1: GS + b"\x7c\x01", # Printing Density -37.5%
|
||||
2: GS + b"\x7c\x02", # Printing Density -25%
|
||||
3: GS + b"\x7c\x03", # Printing Density -12.5%
|
||||
4: GS + b"\x7c\x04", # Printing Density 0%
|
||||
5: GS + b"\x7c\x08", # Printing Density +50%
|
||||
6: GS + b"\x7c\x07", # Printing Density +37.5%
|
||||
7: GS + b"\x7c\x06", # Printing Density +25%
|
||||
8: GS + b"\x7c\x05", # Printing Density +12.5%
|
||||
},
|
||||
'density': {
|
||||
0: GS + b'\x7c\x00', # Printing Density -50%
|
||||
1: GS + b'\x7c\x01', # Printing Density -37.5%
|
||||
2: GS + b'\x7c\x02', # Printing Density -25%
|
||||
3: GS + b'\x7c\x03', # Printing Density -12.5%
|
||||
4: GS + b'\x7c\x04', # Printing Density 0%
|
||||
5: GS + b'\x7c\x08', # Printing Density +50%
|
||||
6: GS + b'\x7c\x07', # Printing Density +37.5%
|
||||
7: GS + b'\x7c\x06', # Printing Density +25%
|
||||
8: GS + b'\x7c\x05' # Printing Density +12.5%
|
||||
"smooth": {
|
||||
True: GS + b"\x62\x01", # Smooth ON
|
||||
False: GS + b"\x62\x00", # Smooth OFF
|
||||
},
|
||||
'smooth': {
|
||||
True: GS + b'\x62\x01', # Smooth ON
|
||||
False: GS + b'\x62\x00' # Smooth OFF
|
||||
},
|
||||
'height': { # Custom text height
|
||||
"height": { # Custom text height
|
||||
1: 0x00,
|
||||
2: 0x01,
|
||||
3: 0x02,
|
||||
@@ -141,9 +161,9 @@ TXT_STYLE = {
|
||||
5: 0x04,
|
||||
6: 0x05,
|
||||
7: 0x06,
|
||||
8: 0x07
|
||||
8: 0x07,
|
||||
},
|
||||
'width': { # Custom text width
|
||||
"width": { # Custom text width
|
||||
1: 0x00,
|
||||
2: 0x10,
|
||||
3: 0x20,
|
||||
@@ -151,105 +171,135 @@ TXT_STYLE = {
|
||||
5: 0x40,
|
||||
6: 0x50,
|
||||
7: 0x60,
|
||||
8: 0x70
|
||||
}
|
||||
8: 0x70,
|
||||
},
|
||||
}
|
||||
|
||||
# Fonts
|
||||
SET_FONT = lambda n: ESC + b'\x4d' + n
|
||||
TXT_FONT_A = SET_FONT(b'\x00') # Font type A
|
||||
TXT_FONT_B = SET_FONT(b'\x01') # Font type B
|
||||
SET_FONT = lambda n: ESC + b"\x4d" + n
|
||||
TXT_FONT_A: bytes = SET_FONT(b"\x00") #: Font type A
|
||||
TXT_FONT_B: bytes = SET_FONT(b"\x01") #: Font type B
|
||||
|
||||
# Spacing
|
||||
LINESPACING_RESET = ESC + b'2'
|
||||
LINESPACING_FUNCS = {
|
||||
60: ESC + b'A', # line_spacing/60 of an inch, 0 <= line_spacing <= 85
|
||||
360: ESC + b'+', # line_spacing/360 of an inch, 0 <= line_spacing <= 255
|
||||
180: ESC + b'3', # line_spacing/180 of an inch, 0 <= line_spacing <= 255
|
||||
LINESPACING_RESET = ESC + b"2"
|
||||
LINESPACING_FUNCS: Dict[int, bytes] = {
|
||||
60: ESC + b"A", # line_spacing/60 of an inch, 0 <= line_spacing <= 85
|
||||
360: ESC + b"+", # line_spacing/360 of an inch, 0 <= line_spacing <= 255
|
||||
180: ESC + b"3", # line_spacing/180 of an inch, 0 <= line_spacing <= 255
|
||||
}
|
||||
|
||||
# Prefix to change the codepage. You need to attach a byte to indicate
|
||||
# the codepage to use. We use escpos-printer-db as the data source.
|
||||
CODEPAGE_CHANGE = ESC + b'\x74'
|
||||
#: Prefix to change the codepage. You need to attach a byte to indicate
|
||||
#: the codepage to use. We use escpos-printer-db as the data source.
|
||||
CODEPAGE_CHANGE: bytes = ESC + b"\x74"
|
||||
|
||||
# Barcode format
|
||||
_SET_BARCODE_TXT_POS = lambda n: GS + b'H' + n
|
||||
BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS(b'\x00') # HRI barcode chars OFF
|
||||
BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS(b'\x01') # HRI barcode chars above
|
||||
BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS(b'\x02') # HRI barcode chars below
|
||||
BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS(b'\x03') # HRI both above and below
|
||||
_SET_BARCODE_TXT_POS = lambda n: GS + b"H" + n
|
||||
BARCODE_TXT_OFF: bytes = _SET_BARCODE_TXT_POS(b"\x00") #: HRI barcode chars OFF
|
||||
BARCODE_TXT_ABV: bytes = _SET_BARCODE_TXT_POS(b"\x01") #: HRI barcode chars above
|
||||
BARCODE_TXT_BLW: bytes = _SET_BARCODE_TXT_POS(b"\x02") #: HRI barcode chars below
|
||||
BARCODE_TXT_BTH: bytes = _SET_BARCODE_TXT_POS(b"\x03") #: HRI both above and below
|
||||
|
||||
_SET_HRI_FONT = lambda n: GS + b'f' + n
|
||||
BARCODE_FONT_A = _SET_HRI_FONT(b'\x00') # Font type A for HRI barcode chars
|
||||
BARCODE_FONT_B = _SET_HRI_FONT(b'\x01') # Font type B for HRI barcode chars
|
||||
_SET_HRI_FONT = lambda n: GS + b"f" + n
|
||||
BARCODE_FONT_A: bytes = _SET_HRI_FONT(b"\x00") #: Font type A for HRI barcode chars
|
||||
BARCODE_FONT_B: bytes = _SET_HRI_FONT(b"\x01") #: Font type B for HRI barcode chars
|
||||
|
||||
BARCODE_HEIGHT = GS + b'h' # Barcode Height [1-255]
|
||||
BARCODE_WIDTH = GS + b'w' # Barcode Width [2-6]
|
||||
BARCODE_HEIGHT: bytes = GS + b"h" #: Barcode Height [1-255]
|
||||
BARCODE_WIDTH: bytes = GS + b"w" #: Barcode Width [2-6]
|
||||
|
||||
# NOTE: This isn't actually an ESC/POS command. It's the common prefix to the
|
||||
# two "print bar code" commands:
|
||||
# - Type A: "GS k <type as integer> <data> NUL"
|
||||
# - TYPE B: "GS k <type as letter> <data length> <data>"
|
||||
# The latter command supports more barcode types
|
||||
_SET_BARCODE_TYPE = lambda m: GS + b'k' + six.int2byte(m)
|
||||
_SET_BARCODE_TYPE = lambda m: GS + b"k" + six.int2byte(m)
|
||||
|
||||
# Barcodes for printing function type A
|
||||
BARCODE_TYPE_A = {
|
||||
'UPC-A': _SET_BARCODE_TYPE(0),
|
||||
'UPC-E': _SET_BARCODE_TYPE(1),
|
||||
'EAN13': _SET_BARCODE_TYPE(2),
|
||||
'EAN8': _SET_BARCODE_TYPE(3),
|
||||
'CODE39': _SET_BARCODE_TYPE(4),
|
||||
'ITF': _SET_BARCODE_TYPE(5),
|
||||
'NW7': _SET_BARCODE_TYPE(6),
|
||||
'CODABAR': _SET_BARCODE_TYPE(6), # Same as NW7
|
||||
#: Barcodes for printing function type A
|
||||
BARCODE_TYPE_A: Dict[str, bytes] = {
|
||||
"UPC-A": _SET_BARCODE_TYPE(0),
|
||||
"UPC-E": _SET_BARCODE_TYPE(1),
|
||||
"EAN13": _SET_BARCODE_TYPE(2),
|
||||
"EAN8": _SET_BARCODE_TYPE(3),
|
||||
"CODE39": _SET_BARCODE_TYPE(4),
|
||||
"ITF": _SET_BARCODE_TYPE(5),
|
||||
"NW7": _SET_BARCODE_TYPE(6),
|
||||
"CODABAR": _SET_BARCODE_TYPE(6), # Same as NW7
|
||||
}
|
||||
|
||||
# Barcodes for printing function type B
|
||||
# The first 8 are the same barcodes as type A
|
||||
BARCODE_TYPE_B = {
|
||||
'UPC-A': _SET_BARCODE_TYPE(65),
|
||||
'UPC-E': _SET_BARCODE_TYPE(66),
|
||||
'EAN13': _SET_BARCODE_TYPE(67),
|
||||
'EAN8': _SET_BARCODE_TYPE(68),
|
||||
'CODE39': _SET_BARCODE_TYPE(69),
|
||||
'ITF': _SET_BARCODE_TYPE(70),
|
||||
'NW7': _SET_BARCODE_TYPE(71),
|
||||
'CODABAR': _SET_BARCODE_TYPE(71), # Same as NW7
|
||||
'CODE93': _SET_BARCODE_TYPE(72),
|
||||
'CODE128': _SET_BARCODE_TYPE(73),
|
||||
'GS1-128': _SET_BARCODE_TYPE(74),
|
||||
'GS1 DATABAR OMNIDIRECTIONAL': _SET_BARCODE_TYPE(75),
|
||||
'GS1 DATABAR TRUNCATED': _SET_BARCODE_TYPE(76),
|
||||
'GS1 DATABAR LIMITED': _SET_BARCODE_TYPE(77),
|
||||
'GS1 DATABAR EXPANDED': _SET_BARCODE_TYPE(78),
|
||||
#: Barcodes for printing function type B
|
||||
#: The first 8 are the same barcodes as type A
|
||||
BARCODE_TYPE_B: Dict[str, bytes] = {
|
||||
"UPC-A": _SET_BARCODE_TYPE(65),
|
||||
"UPC-E": _SET_BARCODE_TYPE(66),
|
||||
"EAN13": _SET_BARCODE_TYPE(67),
|
||||
"EAN8": _SET_BARCODE_TYPE(68),
|
||||
"CODE39": _SET_BARCODE_TYPE(69),
|
||||
"ITF": _SET_BARCODE_TYPE(70),
|
||||
"NW7": _SET_BARCODE_TYPE(71),
|
||||
"CODABAR": _SET_BARCODE_TYPE(71), # Same as NW7
|
||||
"CODE93": _SET_BARCODE_TYPE(72),
|
||||
"CODE128": _SET_BARCODE_TYPE(73),
|
||||
"GS1-128": _SET_BARCODE_TYPE(74),
|
||||
"GS1 DATABAR OMNIDIRECTIONAL": _SET_BARCODE_TYPE(75),
|
||||
"GS1 DATABAR TRUNCATED": _SET_BARCODE_TYPE(76),
|
||||
"GS1 DATABAR LIMITED": _SET_BARCODE_TYPE(77),
|
||||
"GS1 DATABAR EXPANDED": _SET_BARCODE_TYPE(78),
|
||||
}
|
||||
|
||||
BARCODE_TYPES = {
|
||||
'A': BARCODE_TYPE_A,
|
||||
'B': BARCODE_TYPE_B,
|
||||
#: supported barcode formats
|
||||
BARCODE_FORMATS = {
|
||||
"UPC-A": ([(11, 12)], r"^[0-9]{11,12}$"),
|
||||
"UPC-E": ([(7, 8), (11, 12)], r"^([0-9]{7,8}|[0-9]{11,12})$"),
|
||||
"EAN13": ([(12, 13)], r"^[0-9]{12,13}$"),
|
||||
"EAN8": ([(7, 8)], r"^[0-9]{7,8}$"),
|
||||
"CODE39": ([(1, 255)], r"^([0-9A-Z \$\%\+\-\.\/]+|\*[0-9A-Z \$\%\+\-\.\/]+\*)$"),
|
||||
"ITF": ([(2, 255)], r"^([0-9]{2})+$"),
|
||||
"NW7": ([(1, 255)], r"^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"),
|
||||
"CODABAR": ([(1, 255)], r"^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), # Same as NW7
|
||||
"CODE93": ([(1, 255)], r"^[\x00-\x7F]+$"),
|
||||
"CODE128": ([(2, 255)], r"^\{[A-C][\x00-\x7F]+$"),
|
||||
"GS1-128": ([(2, 255)], r"^\{[A-C][\x00-\x7F]+$"), # same as CODE128
|
||||
"GS1 DATABAR OMNIDIRECTIONAL": ([(13, 13)], r"^[0-9]{13}$"),
|
||||
"GS1 DATABAR TRUNCATED": (
|
||||
[(13, 13)],
|
||||
r"^[0-9]{13}$",
|
||||
), # same as GS1 omnidirectional
|
||||
"GS1 DATABAR LIMITED": ([(13, 13)], r"^[01][0-9]{12}$"),
|
||||
"GS1 DATABAR EXPANDED": (
|
||||
[(2, 255)],
|
||||
r"^\([0-9][A-Za-z0-9 \!\"\%\&'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\_\{]+$",
|
||||
),
|
||||
}
|
||||
|
||||
BARCODE_TYPES: Dict[str, Dict[str, bytes]] = {
|
||||
"A": BARCODE_TYPE_A,
|
||||
"B": BARCODE_TYPE_B,
|
||||
}
|
||||
|
||||
# QRCode error correction levels
|
||||
QR_ECLEVEL_L = 0
|
||||
QR_ECLEVEL_M = 1
|
||||
QR_ECLEVEL_Q = 2
|
||||
QR_ECLEVEL_H = 3
|
||||
QR_ECLEVEL_L: int = 0
|
||||
QR_ECLEVEL_M: int = 1
|
||||
QR_ECLEVEL_Q: int = 2
|
||||
QR_ECLEVEL_H: int = 3
|
||||
|
||||
# QRcode models
|
||||
QR_MODEL_1 = 1
|
||||
QR_MODEL_2 = 2
|
||||
QR_MICRO = 3
|
||||
QR_MODEL_1: int = 1
|
||||
QR_MODEL_2: int = 2
|
||||
QR_MICRO: int = 3
|
||||
|
||||
# Image format
|
||||
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"
|
||||
# command. The constants include a fragment of the data's header.
|
||||
_PRINT_RASTER_IMG = lambda data: GS + b'v0' + data
|
||||
S_RASTER_N = _PRINT_RASTER_IMG(b'\x00') # Set raster image normal size
|
||||
S_RASTER_2W = _PRINT_RASTER_IMG(b'\x01') # Set raster image double width
|
||||
S_RASTER_2H = _PRINT_RASTER_IMG(b'\x02') # Set raster image double height
|
||||
S_RASTER_Q = _PRINT_RASTER_IMG(b'\x03') # Set raster image quadruple
|
||||
_PRINT_RASTER_IMG = lambda data: GS + b"v0" + data
|
||||
S_RASTER_N: bytes = _PRINT_RASTER_IMG(b"\x00") # Set raster image normal size
|
||||
S_RASTER_2W: bytes = _PRINT_RASTER_IMG(b"\x01") # Set raster image double width
|
||||
S_RASTER_2H: bytes = _PRINT_RASTER_IMG(b"\x02") # Set raster image double height
|
||||
S_RASTER_Q: bytes = _PRINT_RASTER_IMG(b"\x03") # Set raster image quadruple
|
||||
|
||||
# Status Command
|
||||
RT_STATUS_ONLINE = DLE + EOT + b'\x01';
|
||||
RT_MASK_ONLINE = 8;
|
||||
RT_STATUS: bytes = DLE + EOT
|
||||
RT_STATUS_ONLINE: bytes = RT_STATUS + b"\x01"
|
||||
RT_STATUS_PAPER: bytes = RT_STATUS + b"\x04"
|
||||
RT_MASK_ONLINE: int = 8
|
||||
RT_MASK_PAPER: int = 18
|
||||
RT_MASK_LOWPAPER: int = 30
|
||||
RT_MASK_NOPAPER: int = 114
|
||||
|
1164
src/escpos/escpos.py
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" ESC/POS Exceptions classes
|
||||
"""ESC/POS Exceptions classes.
|
||||
|
||||
Result/Exit codes:
|
||||
|
||||
@@ -13,34 +13,42 @@ Result/Exit codes:
|
||||
- `60` = Invalid pin to send Cash Drawer pulse :py:exc:`~escpos.exceptions.CashDrawerError`
|
||||
- `70` = Invalid number of tab positions :py:exc:`~escpos.exceptions.TabPosError`
|
||||
- `80` = Invalid char code :py:exc:`~escpos.exceptions.CharCodeError`
|
||||
- `90` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError`
|
||||
- `90` = Device not found :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
- `91` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError`
|
||||
- `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError`
|
||||
- `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError`
|
||||
- `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError`
|
||||
- `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError`
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:author: python-escpos developers
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
""" Base class for ESC/POS errors """
|
||||
def __init__(self, msg, status=None):
|
||||
"""Base class for ESC/POS errors.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.Error
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, msg: str, status: Optional[int] = None) -> None:
|
||||
"""Initialize Error object."""
|
||||
Exception.__init__(self)
|
||||
self.msg = msg
|
||||
self.resultcode = 1
|
||||
if status is not None:
|
||||
self.resultcode = status
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of Error."""
|
||||
return self.msg
|
||||
|
||||
|
||||
@@ -50,14 +58,23 @@ class BarcodeTypeError(Error):
|
||||
This exception indicates that no known barcode-type has been entered. The barcode-type has to be
|
||||
one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
The returned error code is `10`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.BarcodeTypeError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize BarcodeTypeError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 10
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode type is defined ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of BarcodeTypeError."""
|
||||
return f"No Barcode type is defined ({self.msg})"
|
||||
|
||||
|
||||
class BarcodeSizeError(Error):
|
||||
@@ -66,57 +83,94 @@ class BarcodeSizeError(Error):
|
||||
This exception indicates that the values for the barcode size are out of range.
|
||||
The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
The resulting return code is `20`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.BarcodeSizeError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize BarcodeSizeError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 20
|
||||
|
||||
def __str__(self):
|
||||
return "Barcode size is out of range ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of BarcodeSizeError."""
|
||||
return f"Barcode size is out of range ({self.msg})"
|
||||
|
||||
|
||||
class BarcodeCodeError(Error):
|
||||
""" No Barcode code was supplied.
|
||||
"""No Barcode code was supplied, or it is incorrect.
|
||||
|
||||
No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode` or the the `check` parameter
|
||||
was True and the check failed.
|
||||
The return code for this exception is `30`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.BarcodeCodeError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize BarcodeCodeError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 30
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode code was supplied ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of BarcodeCodeError."""
|
||||
return f"No Barcode code was supplied ({self.msg})"
|
||||
|
||||
|
||||
class ImageSizeError(Error):
|
||||
"""Image height is longer than 255px and can't be printed.
|
||||
|
||||
The return code for this exception is `40`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.ImageSizeError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize ImageSizeError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 40
|
||||
|
||||
def __str__(self):
|
||||
return "Image height is longer than 255px and can't be printed ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of ImageSizeError."""
|
||||
return f"Image height is longer than 255px and can't be printed ({self.msg})"
|
||||
|
||||
|
||||
class ImageWidthError(Error):
|
||||
"""Image width is too large.
|
||||
|
||||
The return code for this exception is `41`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.ImageWidthError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize ImageWidthError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 41
|
||||
|
||||
def __str__(self):
|
||||
return "Image width is too large ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of ImageWidthError."""
|
||||
return f"Image width is too large ({self.msg})"
|
||||
|
||||
|
||||
class TextError(Error):
|
||||
@@ -124,14 +178,23 @@ class TextError(Error):
|
||||
|
||||
This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`.
|
||||
The return code for this exception is `50`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.TextError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize TextError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 50
|
||||
|
||||
def __str__(self):
|
||||
return "Text string must be supplied to the text() method ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of TextError."""
|
||||
return f"Text string must be supplied to the text() method ({self.msg})"
|
||||
|
||||
|
||||
class CashDrawerError(Error):
|
||||
@@ -139,119 +202,218 @@ class CashDrawerError(Error):
|
||||
|
||||
A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`.
|
||||
The return code for this exception is `60`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.CashDrawerError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize CashDrawerError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 60
|
||||
|
||||
def __str__(self):
|
||||
return "Valid pin must be set to send pulse ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of CashDrawerError."""
|
||||
return f"Valid pin must be set to send pulse ({self.msg})"
|
||||
|
||||
|
||||
class TabPosError(Error):
|
||||
""" Valid tab positions must be set by using from 1 to 32 tabs, and between 1 and 255 tab size values.
|
||||
"""Tab position is invalid.
|
||||
|
||||
Valid tab positions must be set by using from 1 to 32 tabs, and between 1 and 255 tab size values.
|
||||
Both values multiplied must not exceed 255, since it is the maximum tab value.
|
||||
|
||||
This exception is raised by :py:meth:`escpos.escpos.Escpos.control`.
|
||||
The return code for this exception is `70`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.TabPosError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize TabPosError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 70
|
||||
|
||||
def __str__(self):
|
||||
return "Valid tab positions must be in the range 0 to 16 ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of TabPosError."""
|
||||
return f"Valid tab positions must be in the range 0 to 16 ({self.msg})"
|
||||
|
||||
|
||||
class CharCodeError(Error):
|
||||
"""Valid char code must be set.
|
||||
|
||||
The supplied charcode-name in :py:meth:`escpos.escpos.Escpos.charcode` is unknown.
|
||||
Ths returncode for this exception is `80`.
|
||||
The return code for this exception is `80`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.CharCodeError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize CharCodeError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 80
|
||||
|
||||
def __str__(self):
|
||||
return "Valid char code must be set ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of CharCodeError."""
|
||||
return f"Valid char code must be set ({self.msg})"
|
||||
|
||||
|
||||
class USBNotFoundError(Error):
|
||||
""" Device wasn't found (probably not plugged in)
|
||||
class DeviceNotFoundError(Error):
|
||||
"""Device was not found.
|
||||
|
||||
The device seems to be not accessible.
|
||||
The return code for this exception is `90`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.Error
|
||||
:parts: 1
|
||||
|
||||
The USB device seems to be not plugged in.
|
||||
Ths returncode for this exception is `90`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize DeviceNotFoundError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 90
|
||||
|
||||
def __str__(self):
|
||||
return "USB device not found ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of DeviceNotFoundError."""
|
||||
return f"Device not found ({self.msg})"
|
||||
|
||||
|
||||
class USBNotFoundError(DeviceNotFoundError):
|
||||
"""USB device was not found (probably not plugged in).
|
||||
|
||||
The USB device seems to be not plugged in.
|
||||
The return code for this exception is `91`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.USBNotFoundError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize USBNotFoundError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 91
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of USBNotFoundError."""
|
||||
return f"USB device not found ({self.msg})"
|
||||
|
||||
|
||||
class SetVariableError(Error):
|
||||
""" A set method variable was out of range
|
||||
"""A set method variable was out of range.
|
||||
|
||||
Check set variables against minimum and maximum values
|
||||
Ths returncode for this exception is `100`.
|
||||
The return code for this exception is `100`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.SetVariableError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize SetVariableError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 100
|
||||
|
||||
def __str__(self):
|
||||
return "Set variable out of range ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of SetVariableError."""
|
||||
return f"Set variable out of range ({self.msg})"
|
||||
|
||||
|
||||
# Configuration errors
|
||||
|
||||
|
||||
class ConfigNotFoundError(Error):
|
||||
""" The configuration file was not found
|
||||
"""The configuration file was not found.
|
||||
|
||||
The default or passed configuration file could not be read
|
||||
Ths returncode for this exception is `200`.
|
||||
The return code for this exception is `200`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.ConfigNotFoundError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize ConfigNotFoundError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 200
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration not found ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of ConfigNotFoundError."""
|
||||
return f"Configuration not found ({self.msg})"
|
||||
|
||||
|
||||
class ConfigSyntaxError(Error):
|
||||
""" The configuration file is invalid
|
||||
"""The configuration file is invalid.
|
||||
|
||||
The syntax is incorrect
|
||||
Ths returncode for this exception is `210`.
|
||||
The return code for this exception is `210`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.ConfigSyntaxError
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize ConfigSyntaxError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 210
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration syntax is invalid ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of ConfigSyntaxError."""
|
||||
return f"Configuration syntax is invalid ({self.msg})"
|
||||
|
||||
|
||||
class ConfigSectionMissingError(Error):
|
||||
""" The configuration file is missing a section
|
||||
"""The configuration file is missing a section.
|
||||
|
||||
The part of the config asked for does not exist in the loaded configuration
|
||||
The return code for this exception is `220`.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.exceptions.ConfigSectionMissingError
|
||||
:parts: 1
|
||||
|
||||
The part of the config asked for doesn't exist in the loaded configuration
|
||||
Ths returncode for this exception is `220`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
|
||||
def __init__(self, msg: str = "") -> None:
|
||||
"""Initialize ConfigSectionMissingError object."""
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 220
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration section is missing ({msg})".format(msg=self.msg)
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of ConfigSectionMissingError."""
|
||||
return f"Configuration section is missing ({self.msg})"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
""" Image format handling class
|
||||
"""Image format handling class.
|
||||
|
||||
This module contains the image format handler :py:class:`EscposImage`.
|
||||
|
||||
@@ -8,16 +8,14 @@ This module contains the image format handler :py:class:`EscposImage`.
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import math
|
||||
from typing import Iterator, Union
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
class EscposImage(object):
|
||||
class EscposImage:
|
||||
"""
|
||||
Load images in, and output ESC/POS formats.
|
||||
|
||||
@@ -25,9 +23,8 @@ class EscposImage(object):
|
||||
PIL, rather than spend CPU cycles looping over pixels.
|
||||
"""
|
||||
|
||||
def __init__(self, img_source):
|
||||
"""
|
||||
Load in an image
|
||||
def __init__(self, img_source: Union[Image.Image, str]) -> None:
|
||||
"""Load in an image.
|
||||
|
||||
:param img_source: PIL.Image, or filename to load one from.
|
||||
"""
|
||||
@@ -41,7 +38,7 @@ class EscposImage(object):
|
||||
|
||||
# Convert to white RGB background, paste over white background
|
||||
# to strip alpha.
|
||||
img_original = img_original.convert('RGBA')
|
||||
img_original = img_original.convert("RGBA")
|
||||
im = Image.new("RGB", img_original.size, (255, 255, 255))
|
||||
im.paste(img_original, mask=img_original.split()[3])
|
||||
# Convert down to greyscale
|
||||
@@ -52,31 +49,24 @@ class EscposImage(object):
|
||||
self._im = im.convert("1")
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""
|
||||
Width of image in pixels
|
||||
"""
|
||||
def width(self) -> int:
|
||||
"""Return width of image in pixels."""
|
||||
width_pixels, _ = self._im.size
|
||||
return width_pixels
|
||||
|
||||
@property
|
||||
def width_bytes(self):
|
||||
"""
|
||||
Width of image if you use 8 pixels per byte and 0-pad at the end.
|
||||
"""
|
||||
def width_bytes(self) -> int:
|
||||
"""Return width of image if you use 8 pixels per byte and 0-pad at the end."""
|
||||
return (self.width + 7) >> 3
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""
|
||||
Height of image in pixels
|
||||
"""
|
||||
def height(self) -> int:
|
||||
"""Height of image in pixels."""
|
||||
_, height_pixels = self._im.size
|
||||
return height_pixels
|
||||
|
||||
def to_column_format(self, high_density_vertical=True):
|
||||
"""
|
||||
Extract slices of an image as equal-sized blobs of column-format data.
|
||||
def to_column_format(self, high_density_vertical: bool = True) -> Iterator[bytes]:
|
||||
"""Extract slices of an image as equal-sized blobs of column-format data.
|
||||
|
||||
:param high_density_vertical: Printed line height in dots
|
||||
"""
|
||||
@@ -92,15 +82,12 @@ class EscposImage(object):
|
||||
yield (im_bytes)
|
||||
left += line_height
|
||||
|
||||
def to_raster_format(self):
|
||||
"""
|
||||
Convert image to raster-format binary
|
||||
"""
|
||||
def to_raster_format(self) -> bytes:
|
||||
"""Convert image to raster-format binary."""
|
||||
return self._im.tobytes()
|
||||
|
||||
def split(self, fragment_height):
|
||||
"""
|
||||
Split an image into multiple fragments after fragment_height pixels
|
||||
def split(self, fragment_height: int):
|
||||
"""Split an image into multiple fragments after fragment_height pixels.
|
||||
|
||||
:param fragment_height: height of fragment
|
||||
:return: list of PIL objects
|
||||
@@ -115,3 +102,19 @@ class EscposImage(object):
|
||||
box = (left, upper, right, lower)
|
||||
fragments.append(self.img_original.crop(box))
|
||||
return fragments
|
||||
|
||||
def center(self, max_width: int) -> None:
|
||||
"""Center image in place.
|
||||
|
||||
:param: Maximum width in order to deduce x offset for centering
|
||||
:return: None
|
||||
"""
|
||||
old_width, height = self._im.size
|
||||
new_size = (max_width, height)
|
||||
|
||||
new_im = Image.new("1", new_size)
|
||||
paste_x = int((max_width - old_width) / 2)
|
||||
|
||||
new_im.paste(self._im, (paste_x, 0))
|
||||
|
||||
self._im = new_im
|
||||
|
@@ -4,11 +4,6 @@
|
||||
I doubt that this currently works correctly.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
try:
|
||||
import jaconv
|
||||
@@ -16,7 +11,7 @@ except ImportError:
|
||||
jaconv = None
|
||||
|
||||
|
||||
def encode_katakana(text):
|
||||
def encode_katakana(text: str) -> bytes:
|
||||
"""I don't think this quite works yet."""
|
||||
encoded = []
|
||||
for char in text:
|
||||
@@ -39,69 +34,68 @@ def encode_katakana(text):
|
||||
TXT_ENC_KATAKANA_MAP = {
|
||||
# Maps UTF-8 Katakana symbols to KATAKANA Page Codes
|
||||
# TODO: has this really to be hardcoded?
|
||||
|
||||
# Half-Width Katakanas
|
||||
'。': b'\xa1',
|
||||
'「': b'\xa2',
|
||||
'」': b'\xa3',
|
||||
'、': b'\xa4',
|
||||
'・': b'\xa5',
|
||||
'ヲ': b'\xa6',
|
||||
'ァ': b'\xa7',
|
||||
'ィ': b'\xa8',
|
||||
'ゥ': b'\xa9',
|
||||
'ェ': b'\xaa',
|
||||
'ォ': b'\xab',
|
||||
'ャ': b'\xac',
|
||||
'ュ': b'\xad',
|
||||
'ョ': b'\xae',
|
||||
'ッ': b'\xaf',
|
||||
'ー': b'\xb0',
|
||||
'ア': b'\xb1',
|
||||
'イ': b'\xb2',
|
||||
'ウ': b'\xb3',
|
||||
'エ': b'\xb4',
|
||||
'オ': b'\xb5',
|
||||
'カ': b'\xb6',
|
||||
'キ': b'\xb7',
|
||||
'ク': b'\xb8',
|
||||
'ケ': b'\xb9',
|
||||
'コ': b'\xba',
|
||||
'サ': b'\xbb',
|
||||
'シ': b'\xbc',
|
||||
'ス': b'\xbd',
|
||||
'セ': b'\xbe',
|
||||
'ソ': b'\xbf',
|
||||
'タ': b'\xc0',
|
||||
'チ': b'\xc1',
|
||||
'ツ': b'\xc2',
|
||||
'テ': b'\xc3',
|
||||
'ト': b'\xc4',
|
||||
'ナ': b'\xc5',
|
||||
'ニ': b'\xc6',
|
||||
'ヌ': b'\xc7',
|
||||
'ネ': b'\xc8',
|
||||
'ノ': b'\xc9',
|
||||
'ハ': b'\xca',
|
||||
'ヒ': b'\xcb',
|
||||
'フ': b'\xcc',
|
||||
'ヘ': b'\xcd',
|
||||
'ホ': b'\xce',
|
||||
'マ': b'\xcf',
|
||||
'ミ': b'\xd0',
|
||||
'ム': b'\xd1',
|
||||
'メ': b'\xd2',
|
||||
'モ': b'\xd3',
|
||||
'ヤ': b'\xd4',
|
||||
'ユ': b'\xd5',
|
||||
'ヨ': b'\xd6',
|
||||
'ラ': b'\xd7',
|
||||
'リ': b'\xd8',
|
||||
'ル': b'\xd9',
|
||||
'レ': b'\xda',
|
||||
'ロ': b'\xdb',
|
||||
'ワ': b'\xdc',
|
||||
'ン': b'\xdd',
|
||||
'゙': b'\xde',
|
||||
'゚': b'\xdf',
|
||||
"。": b"\xa1",
|
||||
"「": b"\xa2",
|
||||
"」": b"\xa3",
|
||||
"、": b"\xa4",
|
||||
"・": b"\xa5",
|
||||
"ヲ": b"\xa6",
|
||||
"ァ": b"\xa7",
|
||||
"ィ": b"\xa8",
|
||||
"ゥ": b"\xa9",
|
||||
"ェ": b"\xaa",
|
||||
"ォ": b"\xab",
|
||||
"ャ": b"\xac",
|
||||
"ュ": b"\xad",
|
||||
"ョ": b"\xae",
|
||||
"ッ": b"\xaf",
|
||||
"ー": b"\xb0",
|
||||
"ア": b"\xb1",
|
||||
"イ": b"\xb2",
|
||||
"ウ": b"\xb3",
|
||||
"エ": b"\xb4",
|
||||
"オ": b"\xb5",
|
||||
"カ": b"\xb6",
|
||||
"キ": b"\xb7",
|
||||
"ク": b"\xb8",
|
||||
"ケ": b"\xb9",
|
||||
"コ": b"\xba",
|
||||
"サ": b"\xbb",
|
||||
"シ": b"\xbc",
|
||||
"ス": b"\xbd",
|
||||
"セ": b"\xbe",
|
||||
"ソ": b"\xbf",
|
||||
"タ": b"\xc0",
|
||||
"チ": b"\xc1",
|
||||
"ツ": b"\xc2",
|
||||
"テ": b"\xc3",
|
||||
"ト": b"\xc4",
|
||||
"ナ": b"\xc5",
|
||||
"ニ": b"\xc6",
|
||||
"ヌ": b"\xc7",
|
||||
"ネ": b"\xc8",
|
||||
"ノ": b"\xc9",
|
||||
"ハ": b"\xca",
|
||||
"ヒ": b"\xcb",
|
||||
"フ": b"\xcc",
|
||||
"ヘ": b"\xcd",
|
||||
"ホ": b"\xce",
|
||||
"マ": b"\xcf",
|
||||
"ミ": b"\xd0",
|
||||
"ム": b"\xd1",
|
||||
"メ": b"\xd2",
|
||||
"モ": b"\xd3",
|
||||
"ヤ": b"\xd4",
|
||||
"ユ": b"\xd5",
|
||||
"ヨ": b"\xd6",
|
||||
"ラ": b"\xd7",
|
||||
"リ": b"\xd8",
|
||||
"ル": b"\xd9",
|
||||
"レ": b"\xda",
|
||||
"ロ": b"\xdb",
|
||||
"ワ": b"\xdc",
|
||||
"ン": b"\xdd",
|
||||
"゙": b"\xde",
|
||||
"゚": b"\xdf",
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Magic Encode
|
||||
"""Magic Encode.
|
||||
|
||||
This module tries to convert an UTF-8 string to an encoded string for the printer.
|
||||
It uses trial and error in order to guess the right codepage.
|
||||
@@ -12,86 +12,86 @@ The code is based on the encoding-code in py-xml-escpos by @fvdsn.
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
from builtins import bytes
|
||||
from .constants import CODEPAGE_CHANGE
|
||||
from .exceptions import Error
|
||||
from .codepages import CodePages
|
||||
|
||||
import six
|
||||
|
||||
from .codepages import CodePages
|
||||
from .constants import CODEPAGE_CHANGE
|
||||
from .exceptions import Error
|
||||
|
||||
class Encoder(object):
|
||||
"""Takes a list of available code spaces. Picks the right one for a
|
||||
given character.
|
||||
|
||||
class Encoder:
|
||||
"""Take available code spaces and pick the right one for a given character.
|
||||
|
||||
Note: To determine the code page, it needs to do the conversion, and
|
||||
thus already knows what the final byte in the target encoding would
|
||||
be. Nevertheless, the API of this class doesn't return the byte.
|
||||
be. Nevertheless, the API of this class does not return the byte.
|
||||
|
||||
The caller use to do the character conversion itself.
|
||||
|
||||
$ python -m timeit -s "{u'ö':'a'}.get(u'ö')"
|
||||
100000000 loops, best of 3: 0.0133 usec per loop
|
||||
|
||||
$ python -m timeit -s "u'ö'.encode('latin1')"
|
||||
100000000 loops, best of 3: 0.0141 usec per loop
|
||||
"""
|
||||
|
||||
def __init__(self, codepage_map):
|
||||
"""Initialize encoder."""
|
||||
self.codepages = codepage_map
|
||||
self.available_encodings = set(codepage_map.keys())
|
||||
self.available_characters = {}
|
||||
self.used_encodings = set()
|
||||
|
||||
def get_sequence(self, encoding):
|
||||
"""Get a sequence."""
|
||||
return int(self.codepages[encoding])
|
||||
|
||||
def get_encoding_name(self, encoding):
|
||||
"""Given an encoding provided by the user, will return a
|
||||
"""Return a canonical encoding name.
|
||||
|
||||
Given an encoding provided by the user, will return a
|
||||
canonical encoding name; and also validate that the encoding
|
||||
is supported.
|
||||
|
||||
TODO: Support encoding aliases: pc437 instead of cp437.
|
||||
.. todo:: Support encoding aliases: pc437 instead of cp437.
|
||||
"""
|
||||
encoding = CodePages.get_encoding_name(encoding)
|
||||
if encoding not in self.codepages:
|
||||
raise ValueError((
|
||||
'Encoding "{}" cannot be used for the current profile. '
|
||||
'Valid encodings are: {}'
|
||||
).format(encoding, ','.join(self.codepages.keys())))
|
||||
raise ValueError(
|
||||
(
|
||||
f'Encoding "{encoding}" cannot be used for the current profile. '
|
||||
f'Valid encodings are: {",".join(self.codepages.keys())}'
|
||||
)
|
||||
)
|
||||
return encoding
|
||||
|
||||
@staticmethod
|
||||
def _get_codepage_char_list(encoding):
|
||||
"""Get codepage character list
|
||||
"""Get codepage character list.
|
||||
|
||||
Gets characters 128-255 for a given code page, as an array.
|
||||
|
||||
:param encoding: The name of the encoding. This must appear in the CodePage list
|
||||
"""
|
||||
codepage = CodePages.get_encoding(encoding)
|
||||
if 'data' in codepage:
|
||||
encodable_chars = list("".join(codepage['data']))
|
||||
assert(len(encodable_chars) == 128)
|
||||
if "data" in codepage:
|
||||
encodable_chars = list("".join(codepage["data"]))
|
||||
assert len(encodable_chars) == 128
|
||||
return encodable_chars
|
||||
elif 'python_encode' in codepage:
|
||||
encodable_chars = [u" "] * 128
|
||||
elif "python_encode" in codepage:
|
||||
encodable_chars = [" "] * 128
|
||||
for i in range(0, 128):
|
||||
codepoint = i + 128
|
||||
try:
|
||||
encodable_chars[i] = bytes([codepoint]).decode(codepage['python_encode'])
|
||||
encodable_chars[i] = bytes([codepoint]).decode(
|
||||
codepage["python_encode"]
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
# Non-encodable character, just skip it
|
||||
pass
|
||||
return encodable_chars
|
||||
raise LookupError("Can't find a known encoding for {}".format(encoding))
|
||||
raise LookupError(f"Can't find a known encoding for {encoding}")
|
||||
|
||||
def _get_codepage_char_map(self, encoding):
|
||||
""" Get codepage character map
|
||||
"""Get codepage character map.
|
||||
|
||||
Process an encoding and return a map of UTF-characters to code points
|
||||
in this encoding.
|
||||
@@ -104,12 +104,14 @@ class Encoder(object):
|
||||
if encoding in self.available_characters:
|
||||
return self.available_characters[encoding]
|
||||
codepage_char_list = self._get_codepage_char_list(encoding)
|
||||
codepage_char_map = dict((utf8, i + 128) for (i, utf8) in enumerate(codepage_char_list))
|
||||
codepage_char_map = dict(
|
||||
(utf8, i + 128) for (i, utf8) in enumerate(codepage_char_list)
|
||||
)
|
||||
self.available_characters[encoding] = codepage_char_map
|
||||
return codepage_char_map
|
||||
|
||||
def can_encode(self, encoding, char):
|
||||
"""Determine if a character is encodeable in the given code page.
|
||||
"""Determine if a character is encodable in the given code page.
|
||||
|
||||
:param encoding: The name of the encoding.
|
||||
:param char: The character to attempt to encode.
|
||||
@@ -127,7 +129,7 @@ class Encoder(object):
|
||||
|
||||
@staticmethod
|
||||
def _encode_char(char, charmap, defaultchar):
|
||||
""" Encode a single character with the given encoding map
|
||||
"""Encode a single character with the given encoding map.
|
||||
|
||||
:param char: char to encode
|
||||
:param charmap: dictionary for mapping characters in this code page
|
||||
@@ -138,31 +140,33 @@ class Encoder(object):
|
||||
return charmap[char]
|
||||
return ord(defaultchar)
|
||||
|
||||
def encode(self, text, encoding, defaultchar='?'):
|
||||
""" Encode text under the given encoding
|
||||
def encode(self, text, encoding, defaultchar="?"):
|
||||
"""Encode text under the given encoding.
|
||||
|
||||
:param text: Text to encode
|
||||
:param encoding: Encoding name to use (must be defined in capabilities)
|
||||
:param defaultchar: Fallback for non-encodable characters
|
||||
"""
|
||||
codepage_char_map = self._get_codepage_char_map(encoding)
|
||||
output_bytes = bytes([self._encode_char(char, codepage_char_map, defaultchar) for char in text])
|
||||
output_bytes = bytes(
|
||||
[self._encode_char(char, codepage_char_map, defaultchar) for char in text]
|
||||
)
|
||||
return output_bytes
|
||||
|
||||
def __encoding_sort_func(self, item):
|
||||
key, index = item
|
||||
return (
|
||||
key in self.used_encodings,
|
||||
index
|
||||
)
|
||||
used = key in self.used_encodings
|
||||
return (not used, index)
|
||||
|
||||
def find_suitable_encoding(self, char):
|
||||
"""The order of our search is a specific one:
|
||||
"""Search in a specific order for a suitable encoding.
|
||||
|
||||
It is the following order:
|
||||
|
||||
1. code pages that we already tried before; there is a good
|
||||
chance they might work again, reducing the search space,
|
||||
and by re-using already used encodings we might also
|
||||
reduce the number of codepage change instructiosn we have
|
||||
reduce the number of codepage change instruction we have
|
||||
to send. Still, any performance gains will presumably be
|
||||
fairly minor.
|
||||
|
||||
@@ -172,9 +176,7 @@ class Encoder(object):
|
||||
that the code page we pick for this character is actually
|
||||
supported.
|
||||
"""
|
||||
sorted_encodings = sorted(
|
||||
self.codepages.items(),
|
||||
key=self.__encoding_sort_func)
|
||||
sorted_encodings = sorted(self.codepages.items(), key=self.__encoding_sort_func)
|
||||
|
||||
for encoding, _ in sorted_encodings:
|
||||
if self.can_encode(encoding, char):
|
||||
@@ -184,7 +186,9 @@ class Encoder(object):
|
||||
|
||||
|
||||
def split_writable_text(encoder, text, encoding):
|
||||
"""Splits off as many characters from the begnning of text as
|
||||
"""Split up the writable text.
|
||||
|
||||
Splits off as many characters from the beginning of text as
|
||||
are writable with "encoding". Returns a 2-tuple (writable, rest).
|
||||
"""
|
||||
if not encoding:
|
||||
@@ -198,8 +202,10 @@ def split_writable_text(encoder, text, encoding):
|
||||
return text, None
|
||||
|
||||
|
||||
class MagicEncode(object):
|
||||
"""A helper that helps us to automatically switch to the right
|
||||
class MagicEncode:
|
||||
"""Help switching to the right code page.
|
||||
|
||||
A helper that helps us to automatically switch to the right
|
||||
code page to encode any given Unicode character.
|
||||
|
||||
This will consider the printers supported codepages, according
|
||||
@@ -209,9 +215,11 @@ class MagicEncode(object):
|
||||
If the printer does not support a suitable code page, it can
|
||||
insert an error character.
|
||||
"""
|
||||
def __init__(self, driver, encoding=None, disabled=False,
|
||||
defaultsymbol='?', encoder=None):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, driver, encoding=None, disabled=False, defaultsymbol="?", encoder=None
|
||||
):
|
||||
"""Initialize magic encode.
|
||||
|
||||
:param driver:
|
||||
:param encoding: If you know the current encoding of the printer
|
||||
@@ -223,7 +231,7 @@ class MagicEncode(object):
|
||||
:param encoder:
|
||||
"""
|
||||
if disabled and not encoding:
|
||||
raise Error('If you disable magic encode, you need to define an encoding!')
|
||||
raise Error("If you disable magic encode, you need to define an encoding!")
|
||||
|
||||
self.driver = driver
|
||||
self.encoder = encoder or Encoder(driver.profile.get_code_pages())
|
||||
@@ -233,7 +241,7 @@ class MagicEncode(object):
|
||||
self.disabled = disabled
|
||||
|
||||
def force_encoding(self, encoding):
|
||||
"""Sets a fixed encoding. The change is emitted right away.
|
||||
"""Set a fixed encoding. The change is emitted right away.
|
||||
|
||||
From now one, this buffer will switch the code page anymore.
|
||||
However, it will still keep track of the current code page.
|
||||
@@ -245,13 +253,15 @@ class MagicEncode(object):
|
||||
self.disabled = True
|
||||
|
||||
def write(self, text):
|
||||
"""Write the text, automatically switching encodings.
|
||||
"""
|
||||
|
||||
"""Write the text, automatically switching encodings."""
|
||||
if self.disabled:
|
||||
self.write_with_encoding(self.encoding, text)
|
||||
return
|
||||
|
||||
if re.findall(r"[\u4e00-\u9fa5]", text):
|
||||
self.driver._raw(text.encode("GB18030"))
|
||||
return
|
||||
|
||||
# See how far we can go into the text with the current encoding
|
||||
to_write, text = split_writable_text(self.encoder, text, self.encoding)
|
||||
if to_write:
|
||||
@@ -272,25 +282,28 @@ class MagicEncode(object):
|
||||
self.write_with_encoding(encoding, to_write)
|
||||
|
||||
def _handle_character_failed(self, char):
|
||||
"""Called when no codepage was found to render a character.
|
||||
"""Write a default symbol.
|
||||
|
||||
Called when no codepage was found to render a character.
|
||||
"""
|
||||
# Writing the default symbol via write() allows us to avoid
|
||||
# unnecesary codepage switches.
|
||||
self.write(self.defaultsymbol)
|
||||
|
||||
def write_with_encoding(self, encoding, text):
|
||||
if text is not None and type(text) is not six.text_type:
|
||||
raise Error("The supplied text has to be unicode, but is of type {type}.".format(
|
||||
type=type(text)
|
||||
))
|
||||
"""Write the text and inject necessary codepage switches."""
|
||||
if text is not None and type(text) is not str:
|
||||
raise Error(
|
||||
f"The supplied text has to be unicode, but is of type {type(text)}."
|
||||
)
|
||||
|
||||
# We always know the current code page; if the new codepage
|
||||
# is different, emit a change command.
|
||||
if encoding != self.encoding:
|
||||
self.encoding = encoding
|
||||
self.driver._raw(
|
||||
CODEPAGE_CHANGE +
|
||||
six.int2byte(self.encoder.get_sequence(encoding)))
|
||||
CODEPAGE_CHANGE + six.int2byte(self.encoder.get_sequence(encoding))
|
||||
)
|
||||
|
||||
if text:
|
||||
self.driver._raw(self.encoder.encode(text, encoding))
|
||||
|
@@ -1,312 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" This module contains the implementations of abstract base class :py:class:`Escpos`.
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
import serial
|
||||
import socket
|
||||
|
||||
from .escpos import Escpos
|
||||
from .exceptions import USBNotFoundError
|
||||
|
||||
|
||||
class Usb(Escpos):
|
||||
""" USB printer
|
||||
|
||||
This class describes a printer that natively speaks USB.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Usb
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, idVendor, idProduct, timeout=0, in_ep=0x82, out_ep=0x01, *args, **kwargs): # noqa: N803
|
||||
"""
|
||||
:param idVendor: Vendor ID
|
||||
:param idProduct: Product ID
|
||||
:param timeout: Is the time limit of the USB operation. Default without timeout.
|
||||
:param in_ep: Input end point
|
||||
:param out_ep: Output end point
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.idVendor = idVendor
|
||||
self.idProduct = idProduct
|
||||
self.timeout = timeout
|
||||
self.in_ep = in_ep
|
||||
self.out_ep = out_ep
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Search device on USB tree and set it as escpos device """
|
||||
self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct)
|
||||
if self.device is None:
|
||||
raise USBNotFoundError("Device not found or cable not plugged in.")
|
||||
|
||||
check_driver = None
|
||||
|
||||
try:
|
||||
check_driver = self.device.is_kernel_driver_active(0)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
if check_driver is None or check_driver:
|
||||
try:
|
||||
self.device.detach_kernel_driver(0)
|
||||
except usb.core.USBError as e:
|
||||
if check_driver is not None:
|
||||
print("Could not detatch kernel driver: {0}".format(str(e)))
|
||||
|
||||
try:
|
||||
self.device.set_configuration()
|
||||
self.device.reset()
|
||||
except usb.core.USBError as e:
|
||||
print("Could not set configuration: {0}".format(str(e)))
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.write(self.out_ep, msg, self.timeout)
|
||||
|
||||
def _read(self):
|
||||
""" Reads a data buffer and returns it to the caller. """
|
||||
return self.device.read(self.in_ep, 16)
|
||||
|
||||
def close(self):
|
||||
""" Release USB interface """
|
||||
if self.device:
|
||||
usb.util.dispose_resources(self.device)
|
||||
self.device = None
|
||||
|
||||
|
||||
class Serial(Escpos):
|
||||
""" Serial printer
|
||||
|
||||
This class describes a printer that is connected by serial interface.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Serial
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1,
|
||||
parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
|
||||
xonxoff=False, dsrdtr=True, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param devfile: Device file under dev filesystem
|
||||
:param baudrate: Baud rate for serial transmission
|
||||
:param bytesize: Serial buffer size
|
||||
:param timeout: Read/Write timeout
|
||||
:param parity: Parity checking
|
||||
:param stopbits: Number of stop bits
|
||||
:param xonxoff: Software flow control
|
||||
:param dsrdtr: Hardware flow control (False to enable RTS/CTS)
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.timeout = timeout
|
||||
self.parity = parity
|
||||
self.stopbits = stopbits
|
||||
self.xonxoff = xonxoff
|
||||
self.dsrdtr = dsrdtr
|
||||
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Setup serial port and set is as escpos device """
|
||||
if self.device is not None and self.device.is_open:
|
||||
self.close()
|
||||
self.device = serial.Serial(port=self.devfile, baudrate=self.baudrate,
|
||||
bytesize=self.bytesize, parity=self.parity,
|
||||
stopbits=self.stopbits, timeout=self.timeout,
|
||||
xonxoff=self.xonxoff, dsrdtr=self.dsrdtr)
|
||||
|
||||
if self.device is not None:
|
||||
print("Serial printer enabled")
|
||||
else:
|
||||
print("Unable to open serial printer on: {0}".format(str(self.devfile)))
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.write(msg)
|
||||
|
||||
def close(self):
|
||||
""" Close Serial interface """
|
||||
if self.device is not None and self.device.is_open:
|
||||
self.device.flush()
|
||||
self.device.close()
|
||||
|
||||
|
||||
class Network(Escpos):
|
||||
""" Network printer
|
||||
|
||||
This class is used to attach to a networked printer. You can also use this in order to attach to a printer that
|
||||
is forwarded with ``socat``.
|
||||
|
||||
If you have a local printer on parallel port ``/dev/usb/lp0`` then you could start ``socat`` with:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0
|
||||
|
||||
Then you should be able to attach to port ``4242`` with this class.
|
||||
Otherwise the normal usecase would be to have a printer with ethernet interface. This type of printer should
|
||||
work the same with this class. For the address of the printer check its manuals.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Network
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host, port=9100, timeout=60, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param host: Printer's hostname or IP address
|
||||
:param port: Port to write to
|
||||
:param timeout: timeout in seconds for the socket-library
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Open TCP socket with ``socket``-library and set it as escpos device """
|
||||
self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.device.settimeout(self.timeout)
|
||||
self.device.connect((self.host, self.port))
|
||||
|
||||
if self.device is None:
|
||||
print("Could not open socket for {0}".format(self.host))
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.sendall(msg)
|
||||
|
||||
def close(self):
|
||||
""" Close TCP connection """
|
||||
if self.device is not None:
|
||||
self.device.shutdown(socket.SHUT_RDWR)
|
||||
self.device.close()
|
||||
|
||||
|
||||
class File(Escpos):
|
||||
""" Generic file printer
|
||||
|
||||
This class is used for parallel port printer or other printers that are directly attached to the filesystem.
|
||||
Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable
|
||||
and produce arbitrary errors.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.File
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, devfile="/dev/usb/lp0", auto_flush=True, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param devfile : Device file under dev filesystem
|
||||
:param auto_flush: automatically call flush after every call of _raw()
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
self.auto_flush = auto_flush
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
""" Open system file """
|
||||
self.device = open(self.devfile, "wb")
|
||||
|
||||
if self.device is None:
|
||||
print("Could not open the specified file {0}".format(self.devfile))
|
||||
|
||||
def flush(self):
|
||||
""" Flush printing content """
|
||||
self.device.flush()
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self.device.write(msg)
|
||||
if self.auto_flush:
|
||||
self.flush()
|
||||
|
||||
def close(self):
|
||||
""" Close system file """
|
||||
if self.device is not None:
|
||||
self.device.flush()
|
||||
self.device.close()
|
||||
|
||||
|
||||
class Dummy(Escpos):
|
||||
""" Dummy printer
|
||||
|
||||
This class is used for saving commands to a variable, for use in situations where
|
||||
there is no need to send commands to an actual printer. This includes
|
||||
generating print jobs for later use, or testing output.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Dummy
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self._output_list = []
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
:type msg: bytes
|
||||
"""
|
||||
self._output_list.append(msg)
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
""" Get the data that was sent to this printer """
|
||||
return b''.join(self._output_list)
|
||||
|
||||
def close(self):
|
||||
pass
|
22
src/escpos/printer/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""printer implementations."""
|
||||
|
||||
from .cups import CupsPrinter
|
||||
from .dummy import Dummy
|
||||
from .file import File
|
||||
from .lp import LP
|
||||
from .network import Network
|
||||
from .serial import Serial
|
||||
from .usb import Usb
|
||||
from .win32raw import Win32Raw
|
||||
|
||||
__all__ = [
|
||||
"Usb",
|
||||
"File",
|
||||
"Network",
|
||||
"Serial",
|
||||
"LP",
|
||||
"Dummy",
|
||||
"CupsPrinter",
|
||||
"Win32Raw",
|
||||
]
|
222
src/escpos/printer/cups.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the CupsPrinter printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import tempfile
|
||||
from typing import Literal, Optional, Type, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError
|
||||
|
||||
#: keeps track if the pycups dependency could be loaded (:py:class:`escpos.printer.CupsPrinter`)
|
||||
_DEP_PYCUPS = False
|
||||
|
||||
try:
|
||||
import cups
|
||||
|
||||
_DEP_PYCUPS = True
|
||||
# Store server defaults before further configuration
|
||||
DEFAULT_HOST = cups.getServer()
|
||||
DEFAULT_PORT = cups.getPort()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# TODO: dev build mode that let's the wrapper bypass?
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
usable = False
|
||||
if _DEP_PYCUPS:
|
||||
usable = True
|
||||
return usable
|
||||
|
||||
|
||||
def dependency_pycups(func):
|
||||
"""Indicate dependency on pycups."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Throw a RuntimeError if pycups is not imported."""
|
||||
if not is_usable():
|
||||
raise RuntimeError(
|
||||
"Printing with PyCups requires the pycups library to"
|
||||
"be installed. Please refer to the documentation on"
|
||||
"what to install and install the dependencies for pycups."
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class CupsPrinter(Escpos):
|
||||
"""Simple CUPS printer connector.
|
||||
|
||||
.. note::
|
||||
|
||||
Requires ``pycups`` which in turn needs the cups development library package:
|
||||
- Ubuntu/Debian: ``libcups2-dev``
|
||||
- OpenSuse/Fedora: ``cups-devel``
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.CupsPrinter
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
@dependency_pycups
|
||||
def __init__(self, printer_name: str = "", *args, **kwargs) -> None:
|
||||
"""Class constructor for CupsPrinter.
|
||||
|
||||
:param printer_name: CUPS printer name (Optional)
|
||||
:param host: CUPS server host/ip (Optional)
|
||||
:type host: str
|
||||
:param port: CUPS server port (Optional)
|
||||
:type port: int
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.host, self.port = args or (
|
||||
kwargs.get("host", DEFAULT_HOST),
|
||||
kwargs.get("port", DEFAULT_PORT),
|
||||
)
|
||||
self.tmpfile = tempfile.NamedTemporaryFile(delete=True)
|
||||
self.printer_name = printer_name
|
||||
self.job_name = ""
|
||||
self.pending_job = False
|
||||
|
||||
self._device: Union[
|
||||
Literal[False], Literal[None], Type[cups.Connection]
|
||||
] = False
|
||||
|
||||
@property
|
||||
def printers(self) -> dict:
|
||||
"""Available CUPS printers."""
|
||||
if self.device:
|
||||
return self.device.getPrinters()
|
||||
return {}
|
||||
|
||||
def open(
|
||||
self, job_name: str = "python-escpos", raise_not_found: bool = True
|
||||
) -> None:
|
||||
"""Set up a new print job and target the printer.
|
||||
|
||||
A call to this method is required to send new jobs to
|
||||
the CUPS connection after close.
|
||||
|
||||
Defaults to default CUPS printer.
|
||||
Creates a new temporary file buffer.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
"""
|
||||
if self._device:
|
||||
self.close()
|
||||
|
||||
cups.setServer(self.host)
|
||||
cups.setPort(self.port)
|
||||
self.job_name = job_name
|
||||
if self.tmpfile.closed:
|
||||
self.tmpfile = tempfile.NamedTemporaryFile(delete=True)
|
||||
|
||||
try:
|
||||
# Open device
|
||||
self.device: Optional[Type[cups.Connection]] = cups.Connection()
|
||||
if self.device:
|
||||
# Name validation, set default if no given name
|
||||
self.printer_name = self.printer_name or self.device.getDefault()
|
||||
assert self.printer_name in self.printers, "Incorrect printer name"
|
||||
except (RuntimeError, AssertionError) as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Unable to start a print job for the printer {self.printer_name}:"
|
||||
+ f"\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"CupsPrinter printing %s not available", self.printer_name
|
||||
)
|
||||
return
|
||||
logging.info("CupsPrinter printer enabled")
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Append any command sent in raw format to temporary file.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
self.pending_job = True
|
||||
try:
|
||||
self.tmpfile.write(msg)
|
||||
except TypeError:
|
||||
self.pending_job = False
|
||||
raise TypeError("Bytes required. Printer job not opened")
|
||||
|
||||
def send(self) -> None:
|
||||
"""Send the print job to the printer."""
|
||||
assert self.device
|
||||
if self.pending_job:
|
||||
# Rewind tempfile
|
||||
self.tmpfile.seek(0)
|
||||
# Print temporary file via CUPS printer.
|
||||
self.device.printFile(
|
||||
self.printer_name,
|
||||
self.tmpfile.name,
|
||||
self.job_name,
|
||||
{"document-format": cups.CUPS_FORMAT_RAW},
|
||||
)
|
||||
self._clear()
|
||||
|
||||
def _clear(self) -> None:
|
||||
"""Finish the print job.
|
||||
|
||||
Remove temporary file.
|
||||
"""
|
||||
self.tmpfile.close()
|
||||
self.pending_job = False
|
||||
|
||||
def _read(self) -> bytes:
|
||||
"""Return a single-item array with the accepting state of the print queue.
|
||||
|
||||
states: idle = [3], printing a job = [4], stopped = [5]
|
||||
"""
|
||||
printer = self.printers.get(self.printer_name, {})
|
||||
state = printer.get("printer-state")
|
||||
if not state or state in [4, 5]:
|
||||
return b"8" # offline
|
||||
return b"0" # online
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close CUPS connection.
|
||||
|
||||
Send pending job to the printer if needed.
|
||||
"""
|
||||
if not self._device:
|
||||
return
|
||||
if self.pending_job:
|
||||
self.send()
|
||||
logging.info("Closing CUPS connection to printer %s", self.printer_name)
|
||||
self._device = False
|
70
src/escpos/printer/dummy.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the CupsPrinter printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from ..escpos import Escpos
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
return True
|
||||
|
||||
|
||||
class Dummy(Escpos):
|
||||
"""Dummy printer.
|
||||
|
||||
This class is used for saving commands to a variable, for use in situations where
|
||||
there is no need to send commands to an actual printer. This includes
|
||||
generating print jobs for later use, or testing output.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Dummy
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
"""Init with empty output list."""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self._output_list: List[bytes] = []
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Print any command sent in raw format.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
self._output_list.append(msg)
|
||||
|
||||
@property
|
||||
def output(self) -> bytes:
|
||||
"""Get the data that was sent to this printer."""
|
||||
return b"".join(self._output_list)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the buffer of the printer.
|
||||
|
||||
This method can be called if you send the contents to a physical printer
|
||||
and want to use the Dummy printer for new output.
|
||||
"""
|
||||
del self._output_list[:]
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close not implemented for Dummy printer."""
|
||||
pass
|
109
src/escpos/printer/file.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the File printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import IO, Literal, Optional, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
return True
|
||||
|
||||
|
||||
class File(Escpos):
|
||||
"""Generic file printer.
|
||||
|
||||
This class is used for parallel port printer or other printers that are directly attached to the filesystem.
|
||||
Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable
|
||||
and produce arbitrary errors.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.File
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
def __init__(self, devfile: str = "", auto_flush: bool = True, *args, **kwargs):
|
||||
"""Initialize file printer with device file.
|
||||
|
||||
:param devfile: Device file under dev filesystem
|
||||
:param auto_flush: automatically call flush after every call of _raw()
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
self.auto_flush = auto_flush
|
||||
|
||||
self._device: Union[Literal[False], Literal[None], IO[bytes]] = False
|
||||
|
||||
def open(self, raise_not_found: bool = True) -> None:
|
||||
"""Open system file.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
"""
|
||||
if self._device:
|
||||
self.close()
|
||||
|
||||
try:
|
||||
# Open device
|
||||
self.device: Optional[IO[bytes]] = open(self.devfile, "wb")
|
||||
except OSError as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Could not open the specified file {self.devfile}:\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error("File printer %s not found", self.devfile)
|
||||
return
|
||||
logging.info("File printer enabled")
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush printing content."""
|
||||
if self.device:
|
||||
self.device.flush()
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Print any command sent in raw format.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
assert self.device
|
||||
self.device.write(msg)
|
||||
if self.auto_flush:
|
||||
self.flush()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close system file."""
|
||||
if not self._device:
|
||||
return
|
||||
logging.info("Closing File connection to printer %s", self.devfile)
|
||||
if not self.auto_flush:
|
||||
self.flush()
|
||||
self._device.close()
|
||||
self._device = False
|
198
src/escpos/printer/lp.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the LP printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Literal, Optional, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
usable = False
|
||||
if not sys.platform.startswith("win"):
|
||||
usable = True
|
||||
return usable
|
||||
|
||||
|
||||
def dependency_linux_lp(func):
|
||||
"""Indicate dependency on non Windows."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Throw a RuntimeError if not on a non-Windows system."""
|
||||
if not is_usable():
|
||||
raise RuntimeError(
|
||||
"This printer driver depends on LP which is not"
|
||||
"available on Windows systems."
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class LP(Escpos):
|
||||
"""Simple UNIX lp command raw printing.
|
||||
|
||||
Thanks to `Oyami-Srk comment <https://github.com/python-escpos/python-escpos/pull/348#issuecomment-549558316>`_.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.LP
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
@dependency_linux_lp
|
||||
def __init__(self, printer_name: str = "", *args, **kwargs):
|
||||
"""LP class constructor.
|
||||
|
||||
:param printer_name: CUPS printer name (Optional)
|
||||
:param auto_flush: Automatic flush after every _raw() (Optional)
|
||||
:type auto_flush: bool (Defaults False)
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.printer_name = printer_name
|
||||
self.auto_flush = kwargs.get("auto_flush", False)
|
||||
self._flushed = False
|
||||
|
||||
self._device: Union[Literal[False], Literal[None], subprocess.Popen] = False
|
||||
|
||||
@property
|
||||
def printers(self) -> dict:
|
||||
"""Available CUPS printers."""
|
||||
p_names = subprocess.run(
|
||||
["lpstat", "-e"], # Get printer names
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
p_devs = subprocess.run(
|
||||
["lpstat", "-v"], # Get attached devices
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
# List and trim output lines
|
||||
names = [name for name in p_names.stdout.split("\n") if name]
|
||||
devs = [dev for dev in p_devs.stdout.split("\n") if dev]
|
||||
# return a dict of {printer name: attached device} pairs
|
||||
return {name: dev.split()[-1] for name in names for dev in devs if name in dev}
|
||||
|
||||
def _get_system_default_printer(self) -> str:
|
||||
"""Return the system's default printer name."""
|
||||
p_name = subprocess.run(
|
||||
["lpstat", "-d"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
name = p_name.stdout.split()[-1]
|
||||
if name not in self.printers:
|
||||
return ""
|
||||
return name
|
||||
|
||||
def open(
|
||||
self,
|
||||
job_name: str = "python-escpos",
|
||||
raise_not_found: bool = True,
|
||||
_close_opened: bool = True,
|
||||
) -> None:
|
||||
"""Invoke _lp_ in a new subprocess and wait for commands.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
"""
|
||||
if self._device and _close_opened:
|
||||
self.close()
|
||||
|
||||
self._is_closing = False
|
||||
|
||||
self.job_name = job_name
|
||||
try:
|
||||
# Name validation, set default if no given name
|
||||
self.printer_name = self.printer_name or self._get_system_default_printer()
|
||||
assert self.printer_name in self.printers, "Incorrect printer name"
|
||||
# Open device
|
||||
self.device: Optional[subprocess.Popen] = subprocess.Popen(
|
||||
["lp", "-d", self.printer_name, "-t", self.job_name, "-o", "raw"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (AssertionError, subprocess.SubprocessError) as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Unable to start a print job for the printer {self.printer_name}:"
|
||||
+ f"\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error("LP printing %s not available", self.printer_name)
|
||||
return
|
||||
logging.info("LP printer enabled")
|
||||
|
||||
def close(self) -> None:
|
||||
"""Stop the subprocess."""
|
||||
if not self._device:
|
||||
return
|
||||
logging.info("Closing LP connection to printer %s", self.printer_name)
|
||||
self._is_closing = True
|
||||
if not self.auto_flush:
|
||||
self.flush()
|
||||
self._device.terminate()
|
||||
self._device = False
|
||||
|
||||
def flush(self) -> None:
|
||||
"""End line and wait for new commands."""
|
||||
if not self.device or not self.device.stdin:
|
||||
return
|
||||
|
||||
if self._flushed:
|
||||
return
|
||||
|
||||
if self.device.stdin.writable():
|
||||
self.device.stdin.write(b"\n")
|
||||
if self.device.stdin.closed is False:
|
||||
self.device.stdin.close()
|
||||
self.device.wait()
|
||||
self._flushed = True
|
||||
if not self._is_closing:
|
||||
self.open(_close_opened=False)
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Write raw command(s) to the printer.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
assert self.device is not None
|
||||
assert self.device.stdin is not None
|
||||
if self.device.stdin.writable():
|
||||
self.device.stdin.write(msg)
|
||||
else:
|
||||
raise subprocess.SubprocessError("Not a valid pipe for lp process")
|
||||
self._flushed = False
|
||||
if self.auto_flush:
|
||||
self.flush()
|
136
src/escpos/printer/network.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the Network printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from typing import Literal, Optional, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
return True
|
||||
|
||||
|
||||
class Network(Escpos):
|
||||
"""Network printer.
|
||||
|
||||
This class is used to attach to a networked printer.
|
||||
You can also use this in order to attach to a printer that
|
||||
is forwarded with ``socat``.
|
||||
|
||||
If you have a local printer on parallel port ``/dev/usb/lp0``
|
||||
then you could start ``socat`` with:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0
|
||||
|
||||
Then you should be able to attach to port ``4242`` with this class.
|
||||
Otherwise the normal use case would be to have a printer with
|
||||
Ethernet interface.
|
||||
This type of printer should work the same with this class.
|
||||
For the address of the printer check its manuals.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Network
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = "",
|
||||
port: int = 9100,
|
||||
timeout: Union[int, float] = 60,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize network printer.
|
||||
|
||||
:param host: Printer's host name or IP address
|
||||
:param port: Port to write to
|
||||
:param timeout: timeout in seconds for the socket-library
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
|
||||
self._device: Union[Literal[False], Literal[None], socket.socket] = False
|
||||
|
||||
def open(self, raise_not_found: bool = True) -> None:
|
||||
"""Open TCP socket with ``socket``-library and set it as escpos device.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
"""
|
||||
if self._device:
|
||||
self.close()
|
||||
|
||||
try:
|
||||
# Open device
|
||||
self.device: Optional[socket.socket] = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_STREAM
|
||||
)
|
||||
self.device.settimeout(self.timeout)
|
||||
self.device.connect((self.host, self.port))
|
||||
except OSError as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Could not open socket for {self.host}:\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error("Network device %s not found", self.host)
|
||||
return
|
||||
logging.info("Network printer enabled")
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Print any command sent in raw format.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
assert self.device
|
||||
self.device.sendall(msg)
|
||||
|
||||
def _read(self) -> bytes:
|
||||
"""Read data from the TCP socket."""
|
||||
assert self.device
|
||||
return self.device.recv(16)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close TCP connection."""
|
||||
if not self._device:
|
||||
return
|
||||
logging.info("Closing Network connection to printer %s", self.host)
|
||||
try:
|
||||
self._device.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error:
|
||||
pass
|
||||
self._device.close()
|
||||
self._device = False
|
179
src/escpos/printer/serial.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the Serial printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import Literal, Optional, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError
|
||||
|
||||
#: keeps track if the pyserial dependency could be loaded (:py:class:`escpos.printer.Serial`)
|
||||
_DEP_PYSERIAL = False
|
||||
|
||||
try:
|
||||
import serial
|
||||
|
||||
_DEP_PYSERIAL = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
usable = False
|
||||
if _DEP_PYSERIAL:
|
||||
usable = True
|
||||
return usable
|
||||
|
||||
|
||||
def dependency_pyserial(func):
|
||||
"""Indicate dependency on pyserial."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Throw a RuntimeError if pyserial not installed."""
|
||||
if not is_usable():
|
||||
raise RuntimeError(
|
||||
"Printing with Serial requires the pyserial library to"
|
||||
"be installed. Please refer to the documentation on"
|
||||
"what to install and install the dependencies for pyserial."
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Serial(Escpos):
|
||||
"""Serial printer.
|
||||
|
||||
This class describes a printer that is connected by serial interface.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Serial
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
@dependency_pyserial
|
||||
def __init__(
|
||||
self,
|
||||
devfile: str = "",
|
||||
baudrate: int = 9600,
|
||||
bytesize: int = 8,
|
||||
timeout: Union[int, float] = 1,
|
||||
parity: Optional[str] = None,
|
||||
stopbits: Optional[int] = None,
|
||||
xonxoff: bool = False,
|
||||
dsrdtr: bool = True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize serial printer.
|
||||
|
||||
:param devfile: Device file under dev filesystem
|
||||
:param baudrate: Baud rate for serial transmission
|
||||
:param bytesize: Serial buffer size
|
||||
:param timeout: Read/Write timeout
|
||||
:param parity: Parity checking
|
||||
:param stopbits: Number of stop bits
|
||||
:param xonxoff: Software flow control
|
||||
:param dsrdtr: Hardware flow control (False to enable RTS/CTS)
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
self.baudrate = baudrate
|
||||
self.bytesize = bytesize
|
||||
self.timeout = timeout
|
||||
if parity:
|
||||
self.parity = parity
|
||||
else:
|
||||
self.parity = serial.PARITY_NONE
|
||||
if stopbits:
|
||||
self.stopbits = stopbits
|
||||
else:
|
||||
self.stopbits = serial.STOPBITS_ONE
|
||||
self.xonxoff = xonxoff
|
||||
self.dsrdtr = dsrdtr
|
||||
|
||||
self._device: Union[Literal[False], Literal[None], serial.Serial] = False
|
||||
|
||||
@dependency_pyserial
|
||||
def open(self, raise_not_found: bool = True) -> None:
|
||||
"""Set up serial port and set is as escpos device.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
"""
|
||||
if self._device:
|
||||
if self.device and self.device.is_open:
|
||||
self.close()
|
||||
|
||||
try:
|
||||
# Open device
|
||||
self.device: Optional[serial.Serial] = serial.Serial(
|
||||
port=self.devfile,
|
||||
baudrate=self.baudrate,
|
||||
bytesize=self.bytesize,
|
||||
parity=self.parity,
|
||||
stopbits=self.stopbits,
|
||||
timeout=self.timeout,
|
||||
xonxoff=self.xonxoff,
|
||||
dsrdtr=self.dsrdtr,
|
||||
)
|
||||
except (ValueError, serial.SerialException) as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Unable to open serial printer on {self.devfile}:\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error("Serial device %s not found", self.devfile)
|
||||
return
|
||||
logging.info("Serial printer enabled")
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Print any command sent in raw format.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
assert self.device
|
||||
self.device.write(msg)
|
||||
|
||||
def _read(self) -> bytes:
|
||||
"""Read the data buffer and return it to the caller."""
|
||||
assert self.device
|
||||
return self.device.read(16)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close Serial interface."""
|
||||
if not self._device:
|
||||
return
|
||||
logging.info("Closing Serial connection to printer %s", self.devfile)
|
||||
if self._device and self._device.is_open:
|
||||
self._device.flush()
|
||||
self._device.close()
|
||||
self._device = False
|
206
src/escpos/printer/usb.py
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the USB printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
import functools
|
||||
import logging
|
||||
from typing import Dict, Literal, Optional, Type, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError, USBNotFoundError
|
||||
|
||||
#: keeps track if the usb dependency could be loaded (:py:class:`escpos.printer.Usb`)
|
||||
_DEP_USB = False
|
||||
|
||||
try:
|
||||
import usb.core
|
||||
import usb.util
|
||||
|
||||
_DEP_USB = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
usable = False
|
||||
if _DEP_USB:
|
||||
usable = True
|
||||
return usable
|
||||
|
||||
|
||||
def dependency_usb(func):
|
||||
"""Indicate dependency on usb."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Throw a RuntimeError if usb not installed."""
|
||||
if not is_usable():
|
||||
raise RuntimeError(
|
||||
"Printing with USB connection requires a usb library to"
|
||||
"be installed. Please refer to the documentation on"
|
||||
"what to install and install the dependencies for USB."
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Usb(Escpos):
|
||||
"""USB printer.
|
||||
|
||||
This class describes a printer that natively speaks USB.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Usb
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
idVendor: Optional[int] = None,
|
||||
idProduct: Optional[int] = None,
|
||||
usb_args: Dict[str, Union[str, int]] = {},
|
||||
timeout: Union[int, float] = 0,
|
||||
in_ep: int = 0x82,
|
||||
out_ep: int = 0x01,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize USB printer.
|
||||
|
||||
:param idVendor: Vendor ID
|
||||
:param idProduct: Product ID
|
||||
:param usb_args: Optional USB arguments (e.g. custom_match)
|
||||
:param timeout: Is the time limit of the USB operation. Default without timeout.
|
||||
:param in_ep: Input end point
|
||||
:param out_ep: Output end point
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.timeout = timeout
|
||||
self.in_ep = in_ep
|
||||
self.out_ep = out_ep
|
||||
|
||||
self.usb_args = usb_args or {}
|
||||
if idVendor:
|
||||
self.usb_args["idVendor"] = idVendor
|
||||
if idProduct:
|
||||
self.usb_args["idProduct"] = idProduct
|
||||
|
||||
self._device: Union[
|
||||
Literal[False], Literal[None], Type[usb.core.Device]
|
||||
] = False
|
||||
|
||||
@dependency_usb
|
||||
def open(self, raise_not_found: bool = True) -> None:
|
||||
"""Search device on USB tree and set it as escpos device.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
:raises: :py:exc:`~escpos.exceptions.USBNotFoundError`
|
||||
"""
|
||||
if self._device:
|
||||
self.close()
|
||||
|
||||
# Open device
|
||||
try:
|
||||
self.device: Optional[Type[usb.core.Device]] = usb.core.find(
|
||||
**self.usb_args
|
||||
)
|
||||
assert self.device, USBNotFoundError(
|
||||
f"Device {tuple(self.usb_args.values())} not found"
|
||||
+ " or cable not plugged in."
|
||||
)
|
||||
self._check_driver()
|
||||
self._configure_usb()
|
||||
except (AssertionError, usb.core.USBError) as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Unable to open USB printer on {tuple(self.usb_args.values())}:"
|
||||
+ f"\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error("USB device %s not found", tuple(self.usb_args.values()))
|
||||
return
|
||||
logging.info("USB printer enabled")
|
||||
|
||||
def _check_driver(self) -> None:
|
||||
"""Check the driver.
|
||||
|
||||
pyusb has three backends: libusb0, libusb1 and openusb but
|
||||
only libusb1 backend implements the methods is_kernel_driver_active()
|
||||
and detach_kernel_driver().
|
||||
This helps enable this library to work on Windows.
|
||||
"""
|
||||
if self.device and self.device.backend.__module__.endswith("libusb1"):
|
||||
check_driver: Optional[bool] = None
|
||||
|
||||
try:
|
||||
check_driver = self.device.is_kernel_driver_active(0)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
if check_driver is None or check_driver:
|
||||
try:
|
||||
self.device.detach_kernel_driver(0)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
except usb.core.USBError as e:
|
||||
if check_driver is not None:
|
||||
logging.error("Could not detatch kernel driver: %s", str(e))
|
||||
|
||||
def _configure_usb(self) -> None:
|
||||
"""Configure USB."""
|
||||
if not self.device:
|
||||
return
|
||||
try:
|
||||
self.device.set_configuration()
|
||||
self.device.reset()
|
||||
except usb.core.USBError as e:
|
||||
logging.error("Could not set configuration: %s", str(e))
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Print any command sent in raw format.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
assert self.device
|
||||
self.device.write(self.out_ep, msg, self.timeout)
|
||||
|
||||
def _read(self) -> bytes:
|
||||
"""Read a data buffer and return it to the caller."""
|
||||
assert self.device
|
||||
return self.device.read(self.in_ep, 16)
|
||||
|
||||
@dependency_usb
|
||||
def close(self) -> None:
|
||||
"""Release USB interface."""
|
||||
if not self._device:
|
||||
return
|
||||
logging.info(
|
||||
"Closing Usb connection to printer %s", tuple(self.usb_args.values())
|
||||
)
|
||||
usb.util.dispose_resources(self._device)
|
||||
self._device = False
|
164
src/escpos/printer/win32raw.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""This module contains the implementation of the Win32Raw printer driver.
|
||||
|
||||
:author: python-escpos developers
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, Literal, Optional, Union
|
||||
|
||||
from ..escpos import Escpos
|
||||
from ..exceptions import DeviceNotFoundError
|
||||
|
||||
#: keeps track if the win32print dependency could be loaded (:py:class:`escpos.printer.Win32Raw`)
|
||||
_DEP_WIN32PRINT = False
|
||||
|
||||
|
||||
try:
|
||||
import pywintypes
|
||||
import win32print
|
||||
|
||||
_DEP_WIN32PRINT = True
|
||||
PyPrinterHANDLE: Any = win32print.OpenPrinter
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this component can be used due to dependencies."""
|
||||
usable = False
|
||||
if _DEP_WIN32PRINT:
|
||||
usable = True
|
||||
return usable
|
||||
|
||||
|
||||
def dependency_win32print(func):
|
||||
"""Indicate dependency on win32print."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Throw a RuntimeError if win32print not installed."""
|
||||
if not is_usable():
|
||||
raise RuntimeError(
|
||||
"Printing with Win32Raw requires a win32print library to"
|
||||
"be installed. Please refer to the documentation on"
|
||||
"what to install and install the dependencies for win32print."
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Win32Raw(Escpos):
|
||||
"""Printer binding for win32 API.
|
||||
|
||||
Uses the module pywin32 for printing.
|
||||
|
||||
inheritance:
|
||||
|
||||
.. inheritance-diagram:: escpos.printer.Win32Raw
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_usable() -> bool:
|
||||
"""Indicate whether this printer class is usable.
|
||||
|
||||
Will return True if dependencies are available.
|
||||
Will return False if not.
|
||||
"""
|
||||
return is_usable()
|
||||
|
||||
@dependency_win32print
|
||||
def __init__(self, printer_name: str = "", *args, **kwargs) -> None:
|
||||
"""Initialize default printer."""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.printer_name = printer_name
|
||||
self.job_name = ""
|
||||
|
||||
self._device: Union[
|
||||
Literal[False],
|
||||
Literal[None],
|
||||
"PyPrinterHANDLE",
|
||||
] = False
|
||||
|
||||
@property
|
||||
def printers(self) -> dict:
|
||||
"""Available Windows printers."""
|
||||
return {
|
||||
printer["pPrinterName"]: printer
|
||||
for printer in win32print.EnumPrinters(win32print.PRINTER_ENUM_NAME, "", 4)
|
||||
}
|
||||
|
||||
def open(
|
||||
self, job_name: str = "python-escpos", raise_not_found: bool = True
|
||||
) -> None:
|
||||
"""Open connection to default printer.
|
||||
|
||||
By default raise an exception if device is not found.
|
||||
|
||||
:param raise_not_found: Default True.
|
||||
False to log error but do not raise exception.
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
|
||||
"""
|
||||
if self._device:
|
||||
self.close()
|
||||
|
||||
self.job_name = job_name
|
||||
try:
|
||||
# Name validation, set default if no given name
|
||||
self.printer_name = self.printer_name or win32print.GetDefaultPrinter()
|
||||
assert self.printer_name in self.printers, "Incorrect printer name"
|
||||
# Open device
|
||||
self.device: Optional["PyPrinterHANDLE"] = win32print.OpenPrinter(
|
||||
self.printer_name
|
||||
)
|
||||
if self.device:
|
||||
self.current_job = win32print.StartDocPrinter(
|
||||
self.device, 1, (job_name, "", "RAW")
|
||||
)
|
||||
win32print.StartPagePrinter(self.device)
|
||||
except (AssertionError, pywintypes.error) as e:
|
||||
# Raise exception or log error and cancel
|
||||
self.device = None
|
||||
if raise_not_found:
|
||||
raise DeviceNotFoundError(
|
||||
f"Unable to start a print job for the printer {self.printer_name}:"
|
||||
+ f"\n{e}"
|
||||
)
|
||||
else:
|
||||
logging.error("Win32Raw printing %s not available", self.printer_name)
|
||||
return
|
||||
logging.info("Win32Raw printer enabled")
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close connection to default printer."""
|
||||
if self._device is False or self._device is None: # Literal False | None
|
||||
return
|
||||
logging.info("Closing Win32Raw connection to printer %s", self.printer_name)
|
||||
win32print.EndPagePrinter(self._device)
|
||||
win32print.EndDocPrinter(self._device)
|
||||
win32print.ClosePrinter(self._device)
|
||||
self._device = False
|
||||
|
||||
def _raw(self, msg: bytes) -> None:
|
||||
"""Print any command sent in raw format.
|
||||
|
||||
:param msg: arbitrary code to be printed
|
||||
"""
|
||||
if self.printer_name is None:
|
||||
raise DeviceNotFoundError("Printer not found")
|
||||
if not self.device:
|
||||
raise DeviceNotFoundError("Printer job not opened")
|
||||
win32print.WritePrinter(self.device, msg) # type: ignore
|
||||
|
||||
# there is a bug in the typeshed
|
||||
# https://github.com/mhammond/pywin32/blob/main/win32/src/win32print/win32print.cpp#L976
|
||||
# https://github.com/python/typeshed/blob/main/stubs/pywin32/win32/win32print.pyi#L27C4-L27C4
|
21
src/escpos/types/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Custom types."""
|
||||
|
||||
from typing import Dict, TypedDict
|
||||
|
||||
|
||||
class ConstTxtStyleClass(TypedDict):
|
||||
"""Describe type of :py:data:`escpos.constants.TXT_STYLES`."""
|
||||
|
||||
bold: Dict[bool, bytes]
|
||||
underline: Dict[int, bytes]
|
||||
size: Dict[str, bytes]
|
||||
font: Dict[str, bytes]
|
||||
align: Dict[str, bytes]
|
||||
invert: Dict[bool, bytes]
|
||||
color: Dict[str, bytes]
|
||||
flip: Dict[bool, bytes]
|
||||
density: Dict[int, bytes]
|
||||
smooth: Dict[bool, bytes]
|
||||
height: Dict[int, int]
|
||||
width: Dict[int, int]
|
@@ -1,7 +1,49 @@
|
||||
import pytest
|
||||
from escpos.printer import Dummy
|
||||
|
||||
from escpos.exceptions import DeviceNotFoundError
|
||||
from escpos.printer import LP, CupsPrinter, Dummy, File, Network, Serial, Usb, Win32Raw
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def driver():
|
||||
def driver() -> Dummy:
|
||||
return Dummy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def usbprinter() -> Usb:
|
||||
return Usb()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def serialprinter() -> Serial:
|
||||
return Serial()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def networkprinter() -> Network:
|
||||
return Network()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fileprinter() -> File:
|
||||
return File()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lpprinter() -> LP:
|
||||
return LP()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def win32rawprinter():
|
||||
return Win32Raw()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cupsprinter():
|
||||
return CupsPrinter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def devicenotfounderror():
|
||||
return DeviceNotFoundError
|
||||
|
@@ -1,30 +1,27 @@
|
||||
#!/usr/bin/python
|
||||
"""verifies that the metaclass abc is properly used by Escpos
|
||||
"""verifies that the metaclass abc is properly used by ESC/POS
|
||||
|
||||
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
|
||||
:author: `Patrick Kanzler <dev@pkanzler.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 Patrick Kanzler
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from nose.tools import raises
|
||||
|
||||
import escpos.escpos as escpos
|
||||
from abc import ABCMeta
|
||||
|
||||
import pytest
|
||||
|
||||
@raises(TypeError)
|
||||
def test_abstract_base_class_raises():
|
||||
"""test whether the abstract base class raises an exception for Escpos"""
|
||||
escpos.Escpos() # This call should raise TypeError because of abstractmethod _raw()
|
||||
import escpos.escpos as escpos
|
||||
|
||||
|
||||
def test_abstract_base_class():
|
||||
def test_abstract_base_class_raises() -> None:
|
||||
"""test whether the abstract base class raises an exception for ESC/POS"""
|
||||
with pytest.raises(TypeError):
|
||||
# This call should raise TypeError because of abstractmethod _raw()
|
||||
escpos.Escpos() # type: ignore [abstract]
|
||||
|
||||
|
||||
def test_abstract_base_class() -> None:
|
||||
"""test whether Escpos has the metaclass ABCMeta"""
|
||||
assert issubclass(escpos.Escpos, object)
|
||||
assert type(escpos.Escpos) is ABCMeta
|
||||
|
106
test/test_cli.py
@@ -2,117 +2,115 @@
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
from scripttest import TestFileEnvironment
|
||||
from nose.tools import assert_equals, nottest
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from scripttest import TestFileEnvironment as TFE
|
||||
|
||||
import escpos
|
||||
|
||||
TEST_DIR = os.path.abspath('test/test-cli-output')
|
||||
TEST_DIR = tempfile.mkdtemp() + "/cli-test"
|
||||
|
||||
DEVFILE_NAME = 'testfile'
|
||||
DEVFILE_NAME = "testfile"
|
||||
|
||||
DEVFILE = os.path.join(TEST_DIR, DEVFILE_NAME)
|
||||
CONFIGFILE = 'testconfig.yaml'
|
||||
CONFIG_YAML = '''
|
||||
CONFIGFILE = "testconfig.yaml"
|
||||
CONFIG_YAML = f"""
|
||||
---
|
||||
|
||||
printer:
|
||||
type: file
|
||||
devfile: {testfile}
|
||||
'''.format(
|
||||
testfile=DEVFILE,
|
||||
)
|
||||
devfile: {DEVFILE}
|
||||
"""
|
||||
|
||||
|
||||
class TestCLI:
|
||||
""" Contains setups, teardowns, and tests for CLI
|
||||
"""
|
||||
"""Contains setups, teardowns, and tests for CLI"""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
def setup_class(cls) -> None:
|
||||
"""Create a config file to read from"""
|
||||
with open(CONFIGFILE, 'w') as config:
|
||||
with open(CONFIGFILE, "w") as config:
|
||||
config.write(CONFIG_YAML)
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
def teardown_class(cls) -> None:
|
||||
"""Remove config file"""
|
||||
os.remove(CONFIGFILE)
|
||||
shutil.rmtree(TEST_DIR)
|
||||
|
||||
def setup(self):
|
||||
def setup_method(self) -> None:
|
||||
"""Create a file to print to and set up env"""
|
||||
self.env = None
|
||||
self.default_args = None
|
||||
|
||||
self.env = TestFileEnvironment(
|
||||
self.env = TFE(
|
||||
base_path=TEST_DIR,
|
||||
cwd=os.getcwd(),
|
||||
)
|
||||
|
||||
self.default_args = (
|
||||
'python-escpos',
|
||||
'-c',
|
||||
"python-escpos",
|
||||
"-c",
|
||||
CONFIGFILE,
|
||||
)
|
||||
|
||||
fhandle = open(DEVFILE, 'a')
|
||||
fhandle = open(DEVFILE, "a")
|
||||
try:
|
||||
os.utime(DEVFILE, None)
|
||||
finally:
|
||||
fhandle.close()
|
||||
|
||||
def teardown(self):
|
||||
def teardown_method(self) -> None:
|
||||
"""Destroy printer file and env"""
|
||||
os.remove(DEVFILE)
|
||||
self.env.clear()
|
||||
|
||||
def test_cli_help(self):
|
||||
def test_cli_help(self) -> None:
|
||||
"""Test getting help from cli"""
|
||||
result = self.env.run('python-escpos', '-h')
|
||||
result = self.env.run("python-escpos", "-h")
|
||||
assert not result.stderr
|
||||
assert 'usage' in result.stdout
|
||||
assert "usage" in result.stdout
|
||||
|
||||
def test_cli_version(self):
|
||||
def test_cli_version(self) -> None:
|
||||
"""Test the version string"""
|
||||
result = self.env.run('python-escpos', 'version')
|
||||
result = self.env.run("python-escpos", "version")
|
||||
assert not result.stderr
|
||||
assert_equals(escpos.__version__, result.stdout.strip())
|
||||
assert escpos.__version__ == result.stdout.strip()
|
||||
|
||||
@nottest # disable this test as it is not that easy anymore to predict the outcome of this call
|
||||
def test_cli_text(self):
|
||||
def test_cli_version_extended(self) -> None:
|
||||
"""Test the extended version information"""
|
||||
result = self.env.run("python-escpos", "version_extended")
|
||||
assert not result.stderr
|
||||
assert escpos.__version__ in result.stdout
|
||||
# test that additional information on e.g. Serial is printed
|
||||
assert "Serial" in result.stdout
|
||||
|
||||
def test_cli_text(self) -> None:
|
||||
"""Make sure text returns what we sent it"""
|
||||
test_text = 'this is some text'
|
||||
test_text = "this is some text"
|
||||
result = self.env.run(
|
||||
*(self.default_args + (
|
||||
'text',
|
||||
'--txt',
|
||||
*(
|
||||
self.default_args
|
||||
+ (
|
||||
"text",
|
||||
"--txt",
|
||||
test_text,
|
||||
))
|
||||
)
|
||||
)
|
||||
)
|
||||
assert not result.stderr
|
||||
assert DEVFILE_NAME in result.files_updated.keys()
|
||||
assert_equals(
|
||||
result.files_updated[DEVFILE_NAME].bytes,
|
||||
test_text + '\n'
|
||||
assert (
|
||||
result.files_updated[DEVFILE_NAME].bytes == "\x1bt\x00" + test_text + "\n"
|
||||
)
|
||||
|
||||
def test_cli_text_inavlid_args(self):
|
||||
def test_cli_text_invalid_args(self) -> None:
|
||||
"""Test a failure to send valid arguments"""
|
||||
result = self.env.run(
|
||||
*(self.default_args + (
|
||||
'text',
|
||||
'--invalid-param',
|
||||
'some data'
|
||||
)),
|
||||
*(self.default_args + ("text", "--invalid-param", "some data")),
|
||||
expect_error=True,
|
||||
expect_stderr=True
|
||||
expect_stderr=True,
|
||||
)
|
||||
assert_equals(result.returncode, 2)
|
||||
assert 'error:' in result.stderr
|
||||
assert result.returncode == 2
|
||||
assert "error:" in result.stderr
|
||||
assert not result.files_updated
|
||||
|
126
test/test_config.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/python
|
||||
"""tests for config module
|
||||
|
||||
:author: `Patrick Kanzler <dev@pkanzler.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2023 `python-escpos <https://github.com/python-escpos>`_
|
||||
:license: MIT
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
import appdirs
|
||||
import pytest
|
||||
|
||||
import escpos.exceptions
|
||||
|
||||
|
||||
def generate_dummy_config(path, content=None):
|
||||
"""Generate a dummy config in path"""
|
||||
dummy_config_content = content
|
||||
if not content:
|
||||
dummy_config_content = "printer:\n type: Dummy\n"
|
||||
path.write_text(dummy_config_content)
|
||||
assert path.read_text() == dummy_config_content
|
||||
|
||||
|
||||
def simple_printer_test(config):
|
||||
"""Simple test for the dummy printer."""
|
||||
p = config.printer()
|
||||
p._raw(b"1234")
|
||||
|
||||
assert p.output == b"1234"
|
||||
|
||||
|
||||
def test_config_load_with_invalid_config_yaml(tmp_path):
|
||||
"""Test the loading of a config with a invalid config file (yaml issue)."""
|
||||
# generate a dummy config
|
||||
config_file = tmp_path / "config.yaml"
|
||||
generate_dummy_config(config_file, content="}invalid}yaml}")
|
||||
|
||||
# test the config loading
|
||||
from escpos import config
|
||||
|
||||
c = config.Config()
|
||||
with pytest.raises(escpos.exceptions.ConfigSyntaxError):
|
||||
c.load(config_path=config_file)
|
||||
|
||||
|
||||
def test_config_load_with_invalid_config_content(tmp_path):
|
||||
"""Test the loading of a config with a invalid config file (content issue)."""
|
||||
# generate a dummy config
|
||||
config_file = tmp_path / "config.yaml"
|
||||
generate_dummy_config(
|
||||
config_file, content="printer:\n type: NoPrinterWithThatName\n"
|
||||
)
|
||||
|
||||
# test the config loading
|
||||
from escpos import config
|
||||
|
||||
c = config.Config()
|
||||
with pytest.raises(escpos.exceptions.ConfigSyntaxError):
|
||||
c.load(config_path=config_file)
|
||||
|
||||
|
||||
def test_config_load_with_missing_config(tmp_path):
|
||||
"""Test the loading of a config that does not exist."""
|
||||
# test the config loading
|
||||
from escpos import config
|
||||
|
||||
c = config.Config()
|
||||
with pytest.raises(escpos.exceptions.ConfigNotFoundError):
|
||||
c.load(config_path=tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
"This test creates in the actual appdir files and is therefore skipped."
|
||||
)
|
||||
def test_config_load_from_appdir() -> None:
|
||||
"""Test the loading of a config in appdir."""
|
||||
from escpos import config
|
||||
|
||||
# generate a dummy config
|
||||
config_file = (
|
||||
pathlib.Path(appdirs.user_config_dir(config.Config._app_name))
|
||||
/ config.Config._config_file
|
||||
)
|
||||
|
||||
generate_dummy_config(config_file)
|
||||
|
||||
# test the config loading
|
||||
c = config.Config()
|
||||
c.load()
|
||||
|
||||
# test the resulting printer object
|
||||
simple_printer_test(c)
|
||||
|
||||
|
||||
def test_config_load_with_file(tmp_path):
|
||||
"""Test the loading of a config with a config file."""
|
||||
# generate a dummy config
|
||||
config_file = tmp_path / "config.yaml"
|
||||
generate_dummy_config(config_file)
|
||||
|
||||
# test the config loading
|
||||
from escpos import config
|
||||
|
||||
c = config.Config()
|
||||
c.load(config_path=config_file)
|
||||
|
||||
# test the resulting printer object
|
||||
simple_printer_test(c)
|
||||
|
||||
|
||||
def test_config_load_with_path(tmp_path):
|
||||
"""Test the loading of a config with a config path."""
|
||||
# generate a dummy config
|
||||
config_file = tmp_path / "config.yaml"
|
||||
generate_dummy_config(config_file)
|
||||
|
||||
# test the config loading
|
||||
from escpos import config
|
||||
|
||||
c = config.Config()
|
||||
c.load(config_path=tmp_path)
|
||||
|
||||
# test the resulting printer object
|
||||
simple_printer_test(c)
|
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import escpos.printer as printer
|
||||
from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B
|
||||
from escpos.capabilities import Profile, BARCODE_B
|
||||
from escpos.exceptions import BarcodeTypeError
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bctype,data,expected", [
|
||||
('EAN13', '4006381333931',
|
||||
b'\x1ba\x01\x1dh@\x1dw\x03\x1df\x00\x1dH\x02\x1dk\x024006381333931\x00')
|
||||
])
|
||||
def test_barcode(bctype, data, expected):
|
||||
"""should generate different barcode types correctly.
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.barcode(data, bctype)
|
||||
assert instance.output == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bctype,supports_b", [
|
||||
('invalid', True),
|
||||
('CODE128', False),
|
||||
])
|
||||
def test_lacks_support(bctype, supports_b):
|
||||
"""should raise an error if the barcode type is not supported.
|
||||
"""
|
||||
profile = Profile(features={BARCODE_B: supports_b})
|
||||
instance = printer.Dummy(profile=profile)
|
||||
with pytest.raises(BarcodeTypeError):
|
||||
instance.barcode('test', bctype)
|
||||
|
||||
assert instance.output == b''
|
@@ -1,17 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import six
|
||||
|
||||
import escpos.printer as printer
|
||||
from escpos.constants import GS
|
||||
|
||||
|
||||
def test_cut_without_feed():
|
||||
"""Test cut without feeding paper"""
|
||||
instance = printer.Dummy()
|
||||
instance.cut(feed=False)
|
||||
expected = GS + b'V' + six.int2byte(66) + b'\x00'
|
||||
assert(instance.output == expected)
|
@@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
""" Image function tests- Check that image print commands are sent correctly.
|
||||
|
||||
:author: `Michael Billington <michael.billington@gmail.com>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import escpos.printer as printer
|
||||
from escpos.exceptions import ImageWidthError
|
||||
|
||||
|
||||
# Raster format print
|
||||
def test_bit_image_black():
|
||||
"""
|
||||
Test printing solid black bit image (raster)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/canvas_black.png', impl="bitImageRaster")
|
||||
assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x80')
|
||||
# Same thing w/ object created on the fly, rather than a filename
|
||||
instance = printer.Dummy()
|
||||
im = Image.new("RGB", (1, 1), (0, 0, 0))
|
||||
instance.image(im, impl="bitImageRaster")
|
||||
assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x80')
|
||||
|
||||
|
||||
def test_bit_image_white():
|
||||
"""
|
||||
Test printing solid white bit image (raster)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/canvas_white.png', impl="bitImageRaster")
|
||||
assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\x00')
|
||||
|
||||
|
||||
def test_bit_image_both():
|
||||
"""
|
||||
Test printing black/white bit image (raster)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_white.png', impl="bitImageRaster")
|
||||
assert(instance.output == b'\x1dv0\x00\x01\x00\x02\x00\xc0\x00')
|
||||
|
||||
|
||||
def test_bit_image_transparent():
|
||||
"""
|
||||
Test printing black/transparent bit image (raster)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_transparent.png', impl="bitImageRaster")
|
||||
assert(instance.output == b'\x1dv0\x00\x01\x00\x02\x00\xc0\x00')
|
||||
|
||||
|
||||
# Column format print
|
||||
def test_bit_image_colfmt_black():
|
||||
"""
|
||||
Test printing solid black bit image (column format)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/canvas_black.png', impl="bitImageColumn")
|
||||
assert(instance.output == b'\x1b3\x10\x1b*!\x01\x00\x80\x00\x00\x0a\x1b2')
|
||||
|
||||
|
||||
def test_bit_image_colfmt_white():
|
||||
"""
|
||||
Test printing solid white bit image (column format)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/canvas_white.png', impl="bitImageColumn")
|
||||
assert(instance.output == b'\x1b3\x10\x1b*!\x01\x00\x00\x00\x00\x0a\x1b2')
|
||||
|
||||
|
||||
def test_bit_image_colfmt_both():
|
||||
"""
|
||||
Test printing black/white bit image (column format)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_white.png', impl="bitImageColumn")
|
||||
assert(instance.output == b'\x1b3\x10\x1b*!\x02\x00\x80\x00\x00\x80\x00\x00\x0a\x1b2')
|
||||
|
||||
|
||||
def test_bit_image_colfmt_transparent():
|
||||
"""
|
||||
Test printing black/transparent bit image (column format)
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_transparent.png', impl="bitImageColumn")
|
||||
assert(instance.output == b'\x1b3\x10\x1b*!\x02\x00\x80\x00\x00\x80\x00\x00\x0a\x1b2')
|
||||
|
||||
|
||||
# Graphics print
|
||||
def test_graphics_black():
|
||||
"""
|
||||
Test printing solid black graphics
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/canvas_black.png', impl="graphics")
|
||||
assert(instance.output == b'\x1d(L\x0b\x000p0\x01\x011\x01\x00\x01\x00\x80\x1d(L\x02\x0002')
|
||||
|
||||
|
||||
def test_graphics_white():
|
||||
"""
|
||||
Test printing solid white graphics
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/canvas_white.png', impl="graphics")
|
||||
assert(instance.output == b'\x1d(L\x0b\x000p0\x01\x011\x01\x00\x01\x00\x00\x1d(L\x02\x0002')
|
||||
|
||||
|
||||
def test_graphics_both():
|
||||
"""
|
||||
Test printing black/white graphics
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_white.png', impl="graphics")
|
||||
assert(instance.output == b'\x1d(L\x0c\x000p0\x01\x011\x02\x00\x02\x00\xc0\x00\x1d(L\x02\x0002')
|
||||
|
||||
|
||||
def test_graphics_transparent():
|
||||
"""
|
||||
Test printing black/transparent graphics
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_transparent.png', impl="graphics")
|
||||
assert(instance.output == b'\x1d(L\x0c\x000p0\x01\x011\x02\x00\x02\x00\xc0\x00\x1d(L\x02\x0002')
|
||||
|
||||
|
||||
def test_large_graphics():
|
||||
"""
|
||||
Test whether 'large' graphics that induce a fragmentation are handled correctly.
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.image('test/resources/black_white.png', impl="bitImageRaster", fragment_height=1)
|
||||
assert(instance.output == b'\x1dv0\x00\x01\x00\x01\x00\xc0\x1dv0\x00\x01\x00\x01\x00\x00')
|
||||
|
||||
|
||||
def test_width_too_large():
|
||||
"""
|
||||
Test printing an image that is too large in width.
|
||||
"""
|
||||
instance = printer.Dummy()
|
||||
instance.profile.profile_data = {
|
||||
'media': {
|
||||
'width': {
|
||||
'pixels': 384
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ImageWidthError):
|
||||
instance.image(Image.new("RGB", (385, 200)))
|
||||
|
||||
instance.image(Image.new("RGB", (384, 200)))
|
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
"""test native QR code printing
|
||||
|
||||
:author: `Michael Billington <michael.billington@gmail.com>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_
|
||||
:license: MIT
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from nose.tools import raises
|
||||
import escpos.printer as printer
|
||||
from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1
|
||||
|
||||
|
||||
def test_defaults():
|
||||
"""Test QR code with defaults"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True)
|
||||
expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d' \
|
||||
b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
|
||||
assert(instance.output == expected)
|
||||
|
||||
|
||||
def test_empty():
|
||||
"""Test QR printing blank code"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("", native=True)
|
||||
assert(instance.output == b'')
|
||||
|
||||
|
||||
def test_ec():
|
||||
"""Test QR error correction setting"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True, ec=QR_ECLEVEL_H)
|
||||
expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E3\x1d' \
|
||||
b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
|
||||
assert(instance.output == expected)
|
||||
|
||||
|
||||
def test_size():
|
||||
"""Test QR box size"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True, size=7)
|
||||
expected = b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x07\x1d(k\x03\x001E0\x1d' \
|
||||
b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
|
||||
assert(instance.output == expected)
|
||||
|
||||
|
||||
def test_model():
|
||||
"""Test QR model"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True, model=QR_MODEL_1)
|
||||
expected = b'\x1d(k\x04\x001A1\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d' \
|
||||
b'(k\x07\x001P01234\x1d(k\x03\x001Q0'
|
||||
assert(instance.output == expected)
|
||||
|
||||
|
||||
@raises(ValueError)
|
||||
def test_invalid_ec():
|
||||
"""Test invalid QR error correction"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True, ec=-1)
|
||||
|
||||
|
||||
@raises(ValueError)
|
||||
def test_invalid_size():
|
||||
"""Test invalid QR size"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True, size=0)
|
||||
|
||||
|
||||
@raises(ValueError)
|
||||
def test_invalid_model():
|
||||
"""Test invalid QR model"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=True, model="Hello")
|
||||
|
||||
|
||||
def test_image():
|
||||
"""Test QR as image"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1", native=False, size=1)
|
||||
print(instance.output)
|
||||
expected = b'\x1bt\x00\n' \
|
||||
b'\x1dv0\x00\x03\x00\x17\x00\x00\x00\x00\x7f]\xfcA\x19\x04]it]et' \
|
||||
b']ItA=\x04\x7fU\xfc\x00\x0c\x00y~t4\x7f =\xa84j\xd9\xf0\x05\xd4\x90\x00' \
|
||||
b'i(\x7f<\xa8A \xd8]\'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00\x00' \
|
||||
b'\n\n'
|
||||
assert(instance.output == expected)
|
||||
|
||||
|
||||
@raises(ValueError)
|
||||
def test_image_invalid_model():
|
||||
"""Test unsupported QR model as image"""
|
||||
instance = printer.Dummy()
|
||||
instance.qr("1234", native=False, model=QR_MODEL_1)
|