22
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
Please feel free to delete any sections that aren't relevant.
|
||||
-->
|
||||
|
||||
<!-- mark with x between the [ ] -->
|
||||
I have:
|
||||
- [ ] searched open and closed issues for duplicates
|
||||
|
||||
### Bug description
|
||||
|
||||
### Steps to reproduce
|
||||
- add your steps here
|
||||
- as a list
|
||||
- using hyphens
|
||||
|
||||
### Device info
|
||||
<!-- Replace examples with your info -->
|
||||
**Printer:** Manufacturer Model XVI
|
||||
|
||||
**python-escpos version:** 0.0.0
|
||||
|
||||
**operating system:**
|
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
### 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
|
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# python temporary files
|
||||
*.pyc
|
||||
|
||||
# editor autosaves, data and file browser files
|
||||
$~
|
||||
.idea/
|
||||
.directory
|
||||
|
||||
# temporary data
|
||||
temp
|
||||
|
||||
# packaging and testing
|
||||
.tox/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
*.egg
|
||||
build/
|
||||
dist/
|
||||
.coverage
|
||||
|
||||
# testing temporary directories
|
||||
test/test-cli-output/
|
10
.hgignore
@@ -1,10 +0,0 @@
|
||||
# python temporary files
|
||||
syntax: glob
|
||||
*.pyc
|
||||
|
||||
# editor autosaves
|
||||
$~
|
||||
|
||||
# temporary data
|
||||
syntax: regexp
|
||||
temp
|
35
.travis.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
language: python
|
||||
sudo: false
|
||||
cache: pip
|
||||
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.5-dev
|
||||
env: TOXENV=py35
|
||||
- python: nightly
|
||||
env: TOXENV=py36
|
||||
- python: pypy
|
||||
env: TOXENV=pypy
|
||||
- python: pypy3
|
||||
env: TOXENV=pypy3
|
||||
- python: 2.7
|
||||
env: TOXENV=docs
|
||||
allow_failures:
|
||||
- python: 3.5-dev
|
||||
- python: nightly
|
||||
before_install:
|
||||
- pip install tox codecov
|
||||
script:
|
||||
- tox
|
||||
- codecov
|
48
CHANGELOG
@@ -1,48 +0,0 @@
|
||||
CHANGELOG
|
||||
|
||||
* 2012-11-15 - Version 1.0
|
||||
- Issue #2: Added ethernet support
|
||||
- Issue #3: Added compatibility with libusb-1.0.1
|
||||
- Issue #4: Fixed typo in escpos.py
|
||||
|
||||
* 2013-03-14 - Version 1.0.1
|
||||
- Issue #8: Fixed set font
|
||||
- Added QR support
|
||||
|
||||
* 2013-12-30 - Version 1.0.2
|
||||
- Issue #5: Fixed vertical tab
|
||||
- Issue #9: Fixed identation inconsistence
|
||||
|
||||
* 2014-02-23 - Version 1.0.3
|
||||
- Issue #18: Added quad-area characters (Sent by syncman1x@gmail.com)
|
||||
- Added exception for PIL import
|
||||
|
||||
* 2014-05-20 - Version 1.0.4
|
||||
- Issue #20: Added Density support (Sent by thomas.erbacher@ragapack.de)
|
||||
- Added charcode tables
|
||||
- Fixed Horizontal Tab
|
||||
- Fixed code tabulators
|
||||
|
||||
* 2015-04-21 - Version 1.0.5
|
||||
- Merge pull request #45 from Krispy2009/master
|
||||
. Raising the right error when wrong charcode is used
|
||||
. Sent by Kristi <Krispy2009@gmail.com>
|
||||
|
||||
* 2015-07-06 - Version 1.0.6
|
||||
- Merge pull request #53 from ldos/master
|
||||
. Extended params for serial printers
|
||||
. Sent by ldos <cafeteria.ldosalzira@gmail.com>
|
||||
|
||||
* 2015-08-22 - Version 1.0.7
|
||||
- Issue #57: Fixed transparent images
|
||||
|
||||
* 2015-10-27 - Version 1.0.8
|
||||
- Merge pull request #59 from zouppen/master
|
||||
. Support for images vertically longer than 256 pixels
|
||||
. Sent by Joel Lehtonen <joel.lehtonen@koodilehto.fi>
|
||||
- Updated README
|
||||
|
||||
* 2016-01-24 - Version 1.0.9
|
||||
- fix constant definition for PC1252
|
||||
- move documentation to Sphinx
|
||||
|
101
CHANGELOG.rst
Normal file
@@ -0,0 +1,101 @@
|
||||
*********
|
||||
Changelog
|
||||
*********
|
||||
|
||||
2016-06-24 - Version 2.0.0 - "Attitude Adjuster"
|
||||
------------------------------------------------
|
||||
|
||||
This version is based on the original version of python-escpos by Manuel F Martinez. However, many contributions have
|
||||
greatly improved the old codebase. Since this version does not completely match the interface of the version published
|
||||
on PyPi and has many improvements, it will be released as version 2.0.0.
|
||||
|
||||
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
|
||||
- improve the documentation
|
||||
- extend support of barcode-codes to type B
|
||||
- add function to disable panel-buttons
|
||||
- the text-functions are now intended for unicode, the driver will automatically encode the string based on the selected
|
||||
codepage
|
||||
- the image-functions are now much more flexible
|
||||
- added a CLI
|
||||
- restructured the constants
|
||||
|
||||
contributors
|
||||
^^^^^^^^^^^^
|
||||
- Thomas van den Berg
|
||||
- Michael Billington
|
||||
- Nate Bookham
|
||||
- Davis Goglin
|
||||
- Christoph Heuel
|
||||
- Patrick Kanzler
|
||||
- Qian LinFeng
|
||||
|
||||
2016-01-24 - Version 1.0.9
|
||||
--------------------------
|
||||
|
||||
- fix constant definition for PC1252
|
||||
- move documentation to Sphinx
|
||||
|
||||
2015-10-27 - Version 1.0.8
|
||||
--------------------------
|
||||
|
||||
- Merge pull request #59 from zouppen/master
|
||||
- Support for images vertically longer than 256 pixels
|
||||
- Sent by Joel Lehtonen <joel.lehtonen@koodilehto.fi>
|
||||
- Updated README
|
||||
|
||||
2015-08-22 - Version 1.0.7
|
||||
--------------------------
|
||||
|
||||
- Issue #57: Fixed transparent images
|
||||
|
||||
2015-07-06 - Version 1.0.6
|
||||
--------------------------
|
||||
|
||||
- Merge pull request #53 from ldos/master
|
||||
- Extended params for serial printers
|
||||
- Sent by ldos <cafeteria.ldosalzira@gmail.com>
|
||||
|
||||
2015-04-21 - Version 1.0.5
|
||||
--------------------------
|
||||
|
||||
- Merge pull request #45 from Krispy2009/master
|
||||
- Raising the right error when wrong charcode is used
|
||||
- Sent by Kristi <Krispy2009@gmail.com>
|
||||
|
||||
2014-05-20 - Version 1.0.4
|
||||
--------------------------
|
||||
|
||||
- Issue #20: Added Density support (Sent by thomas.erbacher@ragapack.de)
|
||||
- Added charcode tables
|
||||
- Fixed Horizontal Tab
|
||||
- Fixed code tabulators
|
||||
|
||||
2014-02-23 - Version 1.0.3
|
||||
--------------------------
|
||||
|
||||
- Issue #18: Added quad-area characters (Sent by syncman1x@gmail.com)
|
||||
- Added exception for PIL import
|
||||
|
||||
2013-12-30 - Version 1.0.2
|
||||
--------------------------
|
||||
|
||||
- Issue #5: Fixed vertical tab
|
||||
- Issue #9: Fixed identation inconsistence
|
||||
|
||||
2013-03-14 - Version 1.0.1
|
||||
--------------------------
|
||||
|
||||
- Issue #8: Fixed set font
|
||||
- Added QR support
|
||||
|
||||
2012-11-15 - Version 1.0
|
||||
------------------------
|
||||
|
||||
- Issue #2: Added ethernet support
|
||||
- Issue #3: Added compatibility with libusb-1.0.1
|
||||
- Issue #4: Fixed typo in escpos.py
|
80
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,80 @@
|
||||
************
|
||||
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.
|
||||
|
||||
The pull requests and issues will be prefilled with templates. Please fill in your information where applicable.
|
||||
|
||||
This project uses `semantic versioning <http://semver.org/>`_ and tries to adhere to the proposed rules as
|
||||
well as possible.
|
||||
|
||||
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.
|
||||
|
||||
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 a released version
|
||||
of the package.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Thank you for your contribution!
|
8
MANIFEST
@@ -1,8 +0,0 @@
|
||||
# file GENERATED by distutils, do NOT edit
|
||||
README
|
||||
setup.py
|
||||
escpos/__init__.py
|
||||
escpos/constants.py
|
||||
escpos/escpos.py
|
||||
escpos/exceptions.py
|
||||
escpos/printer.py
|
11
MANIFEST.in
Normal file
@@ -0,0 +1,11 @@
|
||||
include *.rst
|
||||
include *.txt
|
||||
include COPYING
|
||||
include INSTALL
|
||||
include tox.ini
|
||||
recursive-include doc *.bat
|
||||
recursive-include doc *.ico
|
||||
recursive-include doc *.py
|
||||
recursive-include doc *.rst
|
||||
recursive-include doc *.txt
|
||||
recursive-include doc Makefile
|
45
README
@@ -1,45 +0,0 @@
|
||||
ESCPOS
|
||||
======
|
||||
|
||||
Python library to manipulate ESC/POS Printers.
|
||||
|
||||
------------------------------------------------------------------
|
||||
1. Description
|
||||
|
||||
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 standard usage is send raw text to the printer, but in also
|
||||
helps the user to enhance the experience with those printers by
|
||||
facilitating the bar code printing in many different standards,
|
||||
as well as manipulating images so they can be printed as brand
|
||||
logo or any other usage images migh have.
|
||||
|
||||
Text can be aligned/justified and fonts can be changed by size,
|
||||
type and weight.
|
||||
|
||||
Also, this module handles some hardware functionalities like, cut
|
||||
paper, carrier return, printer reset and others concerned to the
|
||||
carriage alignment.
|
||||
|
||||
------------------------------------------------------------------
|
||||
2. Documentation
|
||||
|
||||
Please visit project documentation at:
|
||||
https://python-escpos.readthedocs.org/en/latest
|
||||
|
||||
------------------------------------------------------------------
|
||||
3. Donations
|
||||
|
||||
There are some different prints I'd like to acquire, but unfortunately
|
||||
not all, even used, are cheaper and easy to get.
|
||||
|
||||
If you want to help funding money to get more printers or just want to
|
||||
donate because you like the project, please be in touch and I'll be
|
||||
sending my PayPal info so you can donate.
|
||||
|
||||
Thank you!
|
||||
|
||||
Manuel F Martinez <manpaz@bashlinux.com>
|
||||
|
71
README.rst
Normal file
@@ -0,0 +1,71 @@
|
||||
#############################################################
|
||||
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
|
||||
-----------
|
||||
|
||||
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,
|
||||
barcodes and qr-codes to the printer.
|
||||
|
||||
Text can be aligned/justified and fonts can be changed by size, type and weight.
|
||||
|
||||
Also, this module handles some hardware functionalities like cutting paper, control characters, printer reset
|
||||
and similar functions.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
This library makes use of:
|
||||
|
||||
* pyusb for USB-printers
|
||||
* Pillow for image printing
|
||||
* qrcode for the generation of QR-codes
|
||||
* pyserial for serial printers
|
||||
|
||||
Documentation and Usage
|
||||
-----------------------
|
||||
|
||||
The basic usage is:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from escpos import *
|
||||
|
||||
""" Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """
|
||||
Epson = escpos.Escpos(0x04b8,0x0202,0)
|
||||
Epson.text("Hello World\n")
|
||||
Epson.image("logo.gif")
|
||||
Epson.barcode('1324354657687','EAN13',64,2,'','')
|
||||
Epson.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.
|
2
doc/.gitignore
vendored
@@ -1 +1 @@
|
||||
_build
|
||||
_build/
|
||||
|
0
doc/_static/.gitignore
vendored
Normal file
46
doc/api.rst
@@ -1,46 +0,0 @@
|
||||
escpos package
|
||||
==============
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
escpos.constants module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: escpos.constants
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
escpos.escpos module
|
||||
--------------------
|
||||
|
||||
.. automodule:: escpos.escpos
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
escpos.exceptions module
|
||||
------------------------
|
||||
|
||||
.. automodule:: escpos.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
escpos.printer module
|
||||
---------------------
|
||||
|
||||
.. automodule:: escpos.printer
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: escpos
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
10
doc/api/config.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Config
|
||||
------
|
||||
Module :py:mod:`escpos.config`
|
||||
|
||||
.. automodule:: escpos.config
|
||||
:members:
|
||||
:inherited-members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
10
doc/api/constants.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Constants
|
||||
---------
|
||||
Module :py:mod:`escpos.constants`
|
||||
|
||||
.. automodule:: escpos.constants
|
||||
:members:
|
||||
:inherited-members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
10
doc/api/escpos.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Esc/Pos
|
||||
-------
|
||||
Module :py:mod:`escpos.escpos`
|
||||
|
||||
.. automodule:: escpos.escpos
|
||||
:members:
|
||||
:inherited-members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
9
doc/api/exceptions.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
Exceptions
|
||||
----------
|
||||
Module :py:mod:`escpos.exceptions`
|
||||
|
||||
.. automodule:: escpos.exceptions
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
10
doc/api/image.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Image helper
|
||||
------------
|
||||
Module :py:mod:`escpos.image`
|
||||
|
||||
.. automodule:: escpos.image
|
||||
:members:
|
||||
:inherited-members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
9
doc/api/printer.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
Printer implementations
|
||||
-----------------------
|
||||
Module :py:mod:`escpos.printer`
|
||||
|
||||
.. automodule:: escpos.printer
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
35
doc/conf.py
@@ -14,11 +14,14 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
on_rtd = os.getenv('READTHEDOCS') == 'True'
|
||||
from setuptools_scm import get_version
|
||||
|
||||
# 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('..'))
|
||||
sys.path.insert(0, os.path.abspath('../src'))
|
||||
root = os.path.relpath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
@@ -34,8 +37,19 @@ extensions = [
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
]
|
||||
|
||||
# supress warnings for external images
|
||||
suppress_warnings = [
|
||||
'image.nonlocal_uri',
|
||||
]
|
||||
|
||||
# enable todos
|
||||
todo_include_todos = True
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
@@ -50,16 +64,16 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'python-escpos'
|
||||
copyright = u'2015, Manuel F Martinez and others'
|
||||
copyright = u'2016, Manuel F Martinez and others'
|
||||
|
||||
# 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.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.0.8'
|
||||
version = get_version(root=root)
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0.8'
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -104,7 +118,16 @@ pygments_style = 'sphinx'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
if on_rtd:
|
||||
html_theme = 'default'
|
||||
else:
|
||||
try:
|
||||
import 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'
|
||||
|
||||
# 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
|
||||
@@ -128,7 +151,7 @@ html_theme = 'default'
|
||||
# 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 = None
|
||||
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,
|
||||
|
1
doc/dev/changelog.rst
Normal file
@@ -0,0 +1 @@
|
||||
.. include:: ../../CHANGELOG.rst
|
1
doc/dev/contributing.rst
Normal file
@@ -0,0 +1 @@
|
||||
.. include:: ../../CONTRIBUTING.rst
|
BIN
doc/download/barcode.bin
Normal file
@@ -3,33 +3,14 @@
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to python-escpos's documentation!
|
||||
=========================================
|
||||
.. include:: ../README.rst
|
||||
|
||||
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 standard usage is send raw text to the printer, but in also helps the user to enhance the experience with those printers by facilitating the bar code printing in many different standards,as well as manipulating images so they can be printed as brand logo or any other usage images migh have.
|
||||
|
||||
Text can be justified and fonts can be changed by size, type and weight.
|
||||
|
||||
Also, this module handles some hardware functionalists like, cut paper, cash drawer kicking, printer reset, carriage return and others concerned to the carriage alignment.
|
||||
|
||||
------------
|
||||
|
||||
There are some different printers I'd like to acquire, unfortunately
|
||||
not all, even used, are cheaper and easy to get.
|
||||
|
||||
If you want to help funding money to get more printers or just want to
|
||||
donate because you like the project, please be in touch and I'll be
|
||||
sending my PayPal info so you can donate.
|
||||
|
||||
Thank you!
|
||||
|
||||
User Documentation:
|
||||
-------------------
|
||||
Content
|
||||
-------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: User Documentation
|
||||
|
||||
user/dependencies
|
||||
user/installation
|
||||
@@ -39,13 +20,23 @@ User Documentation:
|
||||
user/todo
|
||||
user/usage
|
||||
|
||||
API:
|
||||
----
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Developer Documentation
|
||||
|
||||
dev/contributing
|
||||
dev/changelog
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
:caption: API Documentation
|
||||
|
||||
api/escpos
|
||||
api/printer
|
||||
api/constants
|
||||
api/exceptions
|
||||
api/config
|
||||
api/image
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
BIN
doc/pyescpos.ico
Normal file
After Width: | Height: | Size: 162 KiB |
@@ -1,4 +1,6 @@
|
||||
pyusb
|
||||
Pillow>=2.0
|
||||
qrcode>=4.0
|
||||
pyserial
|
||||
pyserial
|
||||
sphinx-rtd-theme
|
||||
setuptools-scm
|
||||
|
@@ -2,7 +2,8 @@
|
||||
Methods
|
||||
*******
|
||||
|
||||
.. note:: **TODO** Merge this page into the API-description.
|
||||
.. note:: **TODO** Merge this page with the API-description. (Make the API-description more pretty and then
|
||||
replace this with the API-description.)
|
||||
|
||||
Escpos class
|
||||
------------
|
||||
@@ -35,7 +36,7 @@ barcode("code", "barcode\_type", width, height, "position", "font")
|
||||
Prints a barcode.
|
||||
|
||||
* ``code`` is an alphanumeric code to be printed as bar code
|
||||
* ``barcode_type`` must be one of the following type of codes:
|
||||
* ``barcode_type`` must be one of the following type of codes for function type A:
|
||||
|
||||
* UPC-A
|
||||
* UPC-E
|
||||
@@ -44,7 +45,19 @@ Prints a barcode.
|
||||
* CODE39
|
||||
* ITF
|
||||
* NW7
|
||||
|
||||
|
||||
And for function type B:
|
||||
|
||||
* Any type above
|
||||
* CODE93
|
||||
* CODE128
|
||||
* GS1-128
|
||||
* GS1 DataBar Omnidirectional
|
||||
* GS1 DataBar Truncated
|
||||
* GS1 DataBar Limited
|
||||
* GS1 DataBar Expanded
|
||||
|
||||
|
||||
* ``width`` is a numeric value in the range between (1,255) *Default:* 64
|
||||
* ``height`` is a numeric value in the range between (2,6) *Default:* 3
|
||||
* ``position`` is where to place the code around the bars, could be one of the following values:
|
||||
@@ -57,15 +70,22 @@ Prints a barcode.
|
||||
* ``font`` is one of the 2 type of fonts, values could be:
|
||||
|
||||
* A
|
||||
* B > *Default:* A Raises ``BarcodeTypeError``, ``BarcodeSizeError``, ``BarcodeCodeError`` exceptions.
|
||||
* B > *Default:* A
|
||||
|
||||
* ``fuction_type`` chooses between ESCPOS function type A or B. A is default, B has more barcode options. Choose which one based upon your printer support and require barcode.
|
||||
|
||||
* A
|
||||
* B > *Default* A
|
||||
|
||||
* Raises ``BarcodeTypeError``, ``BarcodeSizeError``, ``BarcodeCodeError`` exceptions.
|
||||
|
||||
text("text")
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Prints raw text. Raises ``TextError`` exception.
|
||||
|
||||
set("align", "font", "type", width, height)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
set("align", "font", "type", width, height, invert, smooth, flip)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Set text properties.
|
||||
* ``align`` set horizontal position for text, the possible values are:
|
||||
@@ -78,6 +98,9 @@ Set text properties.
|
||||
* ``type`` type could be ``B`` (Bold), ``U`` (Underline) or ``normal``. *Default:* normal
|
||||
* ``width`` is a numeric value, 1 is for regular size, and 2 is twice the standard size. *Default*: 1
|
||||
* ``height`` is a numeric value, 1 is for regular size and 2 is twice the standard size. *Default*: 1
|
||||
* ``invert`` is a boolean value, True enables white on black printing. *Default*: False
|
||||
* ``smooth`` is a boolean value, True enables text smoothing. *Default*: False
|
||||
* ``flip`` is a boolean value, True enables upside-down text. *Default*: False
|
||||
|
||||
cut("mode")
|
||||
^^^^^^^^^^^
|
||||
|
@@ -7,6 +7,11 @@ This instructions were tested on Raspbian.
|
||||
Unless you have done any distro with libusb-1.0 on the Raspberry Pi, the
|
||||
following instructions should works fine on your raspberry distro.
|
||||
|
||||
.. 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
|
||||
------------
|
||||
|
||||
|
@@ -21,6 +21,7 @@ Testing
|
||||
~~~~~~~
|
||||
|
||||
* Test on many printers as possible (USB, Serial, Network)
|
||||
* automate testing
|
||||
|
||||
Design
|
||||
~~~~~~
|
||||
@@ -32,4 +33,11 @@ Design
|
||||
* Windows compatibility (hidapi instead libusb?)
|
||||
* PDF417 support
|
||||
|
||||
* use something similar to the `capabilities` in escpos-php
|
||||
|
||||
Todos in the codebase
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. todolist::
|
||||
|
||||
|
||||
|
@@ -120,6 +120,90 @@ on USB interface
|
||||
# Cut paper
|
||||
Epson.cut()
|
||||
|
||||
Configuration File
|
||||
------------------
|
||||
|
||||
You can create a configuration file for python-escpos. This will
|
||||
allow you to use the CLI, and skip some setup when using the library
|
||||
programically.
|
||||
|
||||
The default configuration file is named ``config.yaml``. It's in the YAML
|
||||
format. For windows it is probably at::
|
||||
|
||||
%appdata%/python-escpos/config.yaml
|
||||
|
||||
And for linux::
|
||||
|
||||
$HOME/.config/python-escpos/config.yaml
|
||||
|
||||
If you aren't sure, run::
|
||||
|
||||
from escpos import config
|
||||
c = config.Config()
|
||||
c.load()
|
||||
|
||||
If it can't find the configuration file in the default location, it will tell
|
||||
you where it's looking. You can always pass a path or a list of paths to
|
||||
search to the ``load()`` method.
|
||||
|
||||
|
||||
To load the configured pritner, run::
|
||||
|
||||
from escpos import config
|
||||
c = config.Config()
|
||||
printer = c.printer()
|
||||
|
||||
|
||||
The printer section
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``printer`` configuration section defines a default printer to create.
|
||||
|
||||
The only required paramter is ``type``. The value of this should be one of the
|
||||
printers defined in :doc:`/user/printers`.
|
||||
|
||||
The rest of the parameters are whatever you want to pass to the printer.
|
||||
|
||||
An example file printer::
|
||||
|
||||
printer:
|
||||
type: File
|
||||
devfile: /dev/someprinter
|
||||
|
||||
And for a network printer::
|
||||
|
||||
printer:
|
||||
type: network
|
||||
host: 127.0.0.1
|
||||
port: 9000
|
||||
|
||||
Advanced Usage: Print from binary blob
|
||||
--------------------------------------
|
||||
|
||||
Imagine you have a file with ESC/POS-commands in binary form. This could be useful for testing capabilities of your
|
||||
printer with a known working combination of commands.
|
||||
You can print this data with the following code, using the standard methods of python-escpos. (This is an
|
||||
advantage of the fact that `_raw()` accepts binary strings.)
|
||||
|
||||
::
|
||||
|
||||
from escpos import printer
|
||||
p = printer.Serial() # adapt this to your printer model
|
||||
|
||||
file = open("binary-blob.bin", "rb") # read in the file containing your commands in binary-mode
|
||||
data = file.read()
|
||||
file.close()
|
||||
|
||||
p._raw(data)
|
||||
|
||||
That's all, the printer should then print your data. You can also use this technique to let others reproduce an issue
|
||||
that you have found. (Just "print" your commands to a File-printer on your local filesystem.)
|
||||
However, please keep in mind, that often it is easier and better to just supply the code that you are using.
|
||||
|
||||
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>`_
|
||||
|
||||
How to update your code for USB printers
|
||||
----------------------------------------
|
||||
|
||||
|
@@ -1 +0,0 @@
|
||||
__all__ = ["constants","escpos","exceptions","printer"]
|
@@ -1,86 +0,0 @@
|
||||
""" ESC/POS Commands (Constants) """
|
||||
|
||||
# Feed control sequences
|
||||
CTL_LF = '\x0a' # Print and line feed
|
||||
CTL_FF = '\x0c' # Form feed
|
||||
CTL_CR = '\x0d' # Carriage return
|
||||
CTL_HT = '\x09' # Horizontal tab
|
||||
CTL_SET_HT = '\x1b\x44' # Set horizontal tab positions
|
||||
CTL_VT = '\x1b\x64\x04' # Vertical tab
|
||||
# Printer hardware
|
||||
HW_INIT = '\x1b\x40' # Clear data in buffer and reset modes
|
||||
HW_SELECT = '\x1b\x3d\x01' # Printer select
|
||||
HW_RESET = '\x1b\x3f\x0a\x00' # Reset printer hardware
|
||||
# Cash Drawer
|
||||
CD_KICK_2 = '\x1b\x70\x00' # Sends a pulse to pin 2 []
|
||||
CD_KICK_5 = '\x1b\x70\x01' # Sends a pulse to pin 5 []
|
||||
# Paper
|
||||
PAPER_FULL_CUT = '\x1d\x56\x00' # Full cut paper
|
||||
PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper
|
||||
# Text format
|
||||
TXT_NORMAL = '\x1b\x21\x00' # Normal text
|
||||
TXT_2HEIGHT = '\x1b\x21\x10' # Double height text
|
||||
TXT_2WIDTH = '\x1b\x21\x20' # Double width text
|
||||
TXT_4SQUARE = '\x1b\x21\x30' # Quad area text
|
||||
TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF
|
||||
TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON
|
||||
TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON
|
||||
TXT_BOLD_OFF = '\x1b\x45\x00' # Bold font OFF
|
||||
TXT_BOLD_ON = '\x1b\x45\x01' # Bold font ON
|
||||
TXT_FONT_A = '\x1b\x4d\x00' # Font type A
|
||||
TXT_FONT_B = '\x1b\x4d\x01' # Font type B
|
||||
TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification
|
||||
TXT_ALIGN_CT = '\x1b\x61\x01' # Centering
|
||||
TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification
|
||||
# Char code table
|
||||
CHARCODE_PC437 = '\x1b\x74\x00' # USA: Standard Europe
|
||||
CHARCODE_JIS = '\x1b\x74\x01' # Japanese Katakana
|
||||
CHARCODE_PC850 = '\x1b\x74\x02' # Multilingual
|
||||
CHARCODE_PC860 = '\x1b\x74\x03' # Portuguese
|
||||
CHARCODE_PC863 = '\x1b\x74\x04' # Canadian-French
|
||||
CHARCODE_PC865 = '\x1b\x74\x05' # Nordic
|
||||
CHARCODE_WEU = '\x1b\x74\x06' # Simplified Kanji, Hirakana
|
||||
CHARCODE_GREEK = '\x1b\x74\x07' # Simplified Kanji
|
||||
CHARCODE_HEBREW = '\x1b\x74\x08' # Simplified Kanji
|
||||
CHARCODE_PC1252 = '\x1b\x74\x10' # Western European Windows Code Set
|
||||
CHARCODE_PC866 = '\x1b\x74\x12' # Cirillic #2
|
||||
CHARCODE_PC852 = '\x1b\x74\x13' # Latin 2
|
||||
CHARCODE_PC858 = '\x1b\x74\x14' # Euro
|
||||
CHARCODE_THAI42 = '\x1b\x74\x15' # Thai character code 42
|
||||
CHARCODE_THAI11 = '\x1b\x74\x16' # Thai character code 11
|
||||
CHARCODE_THAI13 = '\x1b\x74\x17' # Thai character code 13
|
||||
CHARCODE_THAI14 = '\x1b\x74\x18' # Thai character code 14
|
||||
CHARCODE_THAI16 = '\x1b\x74\x19' # Thai character code 16
|
||||
CHARCODE_THAI17 = '\x1b\x74\x1a' # Thai character code 17
|
||||
CHARCODE_THAI18 = '\x1b\x74\x1b' # Thai character code 18
|
||||
# Barcode format
|
||||
BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF
|
||||
BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above
|
||||
BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below
|
||||
BARCODE_TXT_BTH = '\x1d\x48\x03' # HRI barcode chars both above and below
|
||||
BARCODE_FONT_A = '\x1d\x66\x00' # Font type A for HRI barcode chars
|
||||
BARCODE_FONT_B = '\x1d\x66\x01' # Font type B for HRI barcode chars
|
||||
BARCODE_HEIGHT = '\x1d\x68\x64' # Barcode Height [1-255]
|
||||
BARCODE_WIDTH = '\x1d\x77\x03' # Barcode Width [2-6]
|
||||
BARCODE_UPC_A = '\x1d\x6b\x00' # Barcode type UPC-A
|
||||
BARCODE_UPC_E = '\x1d\x6b\x01' # Barcode type UPC-E
|
||||
BARCODE_EAN13 = '\x1d\x6b\x02' # Barcode type EAN13
|
||||
BARCODE_EAN8 = '\x1d\x6b\x03' # Barcode type EAN8
|
||||
BARCODE_CODE39 = '\x1d\x6b\x04' # Barcode type CODE39
|
||||
BARCODE_ITF = '\x1d\x6b\x05' # Barcode type ITF
|
||||
BARCODE_NW7 = '\x1d\x6b\x06' # Barcode type NW7
|
||||
# Image format
|
||||
S_RASTER_N = '\x1d\x76\x30\x00' # Set raster image normal size
|
||||
S_RASTER_2W = '\x1d\x76\x30\x01' # Set raster image double width
|
||||
S_RASTER_2H = '\x1d\x76\x30\x02' # Set raster image double height
|
||||
S_RASTER_Q = '\x1d\x76\x30\x03' # Set raster image quadruple
|
||||
# Printing Density
|
||||
PD_N50 = '\x1d\x7c\x00' # Printing Density -50%
|
||||
PD_N37 = '\x1d\x7c\x01' # Printing Density -37.5%
|
||||
PD_N25 = '\x1d\x7c\x02' # Printing Density -25%
|
||||
PD_N12 = '\x1d\x7c\x03' # Printing Density -12.5%
|
||||
PD_0 = '\x1d\x7c\x04' # Printing Density 0%
|
||||
PD_P50 = '\x1d\x7c\x08' # Printing Density +50%
|
||||
PD_P37 = '\x1d\x7c\x07' # Printing Density +37.5%
|
||||
PD_P25 = '\x1d\x7c\x06' # Printing Density +25%
|
||||
PD_P12 = '\x1d\x7c\x05' # Printing Density +12.5%
|
360
escpos/escpos.py
@@ -1,360 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
@author: Manuel F Martinez <manpaz@bashlinux.com>
|
||||
@organization: Bashlinux
|
||||
@copyright: Copyright (c) 2012 Bashlinux
|
||||
@license: GNU GPL v3
|
||||
"""
|
||||
|
||||
try:
|
||||
import Image
|
||||
except ImportError:
|
||||
from PIL import Image
|
||||
|
||||
import qrcode
|
||||
import time
|
||||
|
||||
from constants import *
|
||||
from exceptions import *
|
||||
|
||||
class Escpos:
|
||||
""" ESC/POS Printer object """
|
||||
device = None
|
||||
|
||||
|
||||
def _check_image_size(self, size):
|
||||
""" Check and fix the size of the image to 32 bits """
|
||||
if size % 32 == 0:
|
||||
return (0, 0)
|
||||
else:
|
||||
image_border = 32 - (size % 32)
|
||||
if (image_border % 2) == 0:
|
||||
return (image_border / 2, image_border / 2)
|
||||
else:
|
||||
return (image_border / 2, (image_border / 2) + 1)
|
||||
|
||||
|
||||
def _print_image(self, line, size):
|
||||
""" Print formatted image """
|
||||
i = 0
|
||||
cont = 0
|
||||
buffer = ""
|
||||
|
||||
self._raw(S_RASTER_N)
|
||||
buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1]&0xff, size[1]>>8)
|
||||
self._raw(buffer.decode('hex'))
|
||||
buffer = ""
|
||||
|
||||
while i < len(line):
|
||||
hex_string = int(line[i:i+8],2)
|
||||
buffer += "%02X" % hex_string
|
||||
i += 8
|
||||
cont += 1
|
||||
if cont % 4 == 0:
|
||||
self._raw(buffer.decode("hex"))
|
||||
buffer = ""
|
||||
cont = 0
|
||||
|
||||
|
||||
def _convert_image(self, im):
|
||||
""" Parse image and prepare it to a printable format """
|
||||
pixels = []
|
||||
pix_line = ""
|
||||
im_left = ""
|
||||
im_right = ""
|
||||
switch = 0
|
||||
img_size = [ 0, 0 ]
|
||||
|
||||
|
||||
if im.size[0] > 512:
|
||||
print ("WARNING: Image is wider than 512 and could be truncated at print time ")
|
||||
if im.size[1] > 0xffff:
|
||||
raise ImageSizeError()
|
||||
|
||||
im_border = self._check_image_size(im.size[0])
|
||||
for i in range(im_border[0]):
|
||||
im_left += "0"
|
||||
for i in range(im_border[1]):
|
||||
im_right += "0"
|
||||
|
||||
for y in range(im.size[1]):
|
||||
img_size[1] += 1
|
||||
pix_line += im_left
|
||||
img_size[0] += im_border[0]
|
||||
for x in range(im.size[0]):
|
||||
img_size[0] += 1
|
||||
RGB = im.getpixel((x, y))
|
||||
im_color = (RGB[0] + RGB[1] + RGB[2])
|
||||
im_pattern = "1X0"
|
||||
pattern_len = len(im_pattern)
|
||||
switch = (switch - 1 ) * (-1)
|
||||
for x in range(pattern_len):
|
||||
if im_color <= (255 * 3 / pattern_len * (x+1)):
|
||||
if im_pattern[x] == "X":
|
||||
pix_line += "%d" % switch
|
||||
else:
|
||||
pix_line += im_pattern[x]
|
||||
break
|
||||
elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3):
|
||||
pix_line += im_pattern[-1]
|
||||
break
|
||||
pix_line += im_right
|
||||
img_size[0] += im_border[1]
|
||||
|
||||
self._print_image(pix_line, img_size)
|
||||
|
||||
|
||||
def image(self,path_img):
|
||||
""" Open image file """
|
||||
im_open = Image.open(path_img)
|
||||
|
||||
# Remove the alpha channel on transparent images
|
||||
if im_open.mode == 'RGBA':
|
||||
im_open.load()
|
||||
im = Image.new("RGB", im_open.size, (255, 255, 255))
|
||||
im.paste(im_open, mask=im_open.split()[3])
|
||||
else:
|
||||
im = im_open.convert("RGB")
|
||||
|
||||
# Convert the RGB image in printable image
|
||||
self._convert_image(im)
|
||||
|
||||
|
||||
def qr(self,text):
|
||||
""" Print QR Code for the provided string """
|
||||
qr_code = qrcode.QRCode(version=4, box_size=4, border=1)
|
||||
qr_code.add_data(text)
|
||||
qr_code.make(fit=True)
|
||||
qr_img = qr_code.make_image()
|
||||
im = qr_img._img.convert("RGB")
|
||||
|
||||
# Convert the RGB image in printable image
|
||||
self._convert_image(im)
|
||||
|
||||
|
||||
def charcode(self,code):
|
||||
""" Set Character Code Table """
|
||||
if code.upper() == "USA":
|
||||
self._raw(CHARCODE_PC437)
|
||||
elif code.upper() == "JIS":
|
||||
self._raw(CHARCODE_JIS)
|
||||
elif code.upper() == "MULTILINGUAL":
|
||||
self._raw(CHARCODE_PC850)
|
||||
elif code.upper() == "PORTUGUESE":
|
||||
self._raw(CHARCODE_PC860)
|
||||
elif code.upper() == "CA_FRENCH":
|
||||
self._raw(CHARCODE_PC863)
|
||||
elif code.upper() == "NORDIC":
|
||||
self._raw(CHARCODE_PC865)
|
||||
elif code.upper() == "WEST_EUROPE":
|
||||
self._raw(CHARCODE_WEU)
|
||||
elif code.upper() == "GREEK":
|
||||
self._raw(CHARCODE_GREEK)
|
||||
elif code.upper() == "HEBREW":
|
||||
self._raw(CHARCODE_HEBREW)
|
||||
elif code.upper() == "LATVIAN":
|
||||
self._raw(CHARCODE_PC755)
|
||||
elif code.upper() == "WPC1252":
|
||||
self._raw(CHARCODE_PC1252)
|
||||
elif code.upper() == "CIRILLIC2":
|
||||
self._raw(CHARCODE_PC866)
|
||||
elif code.upper() == "LATIN2":
|
||||
self._raw(CHARCODE_PC852)
|
||||
elif code.upper() == "EURO":
|
||||
self._raw(CHARCODE_PC858)
|
||||
elif code.upper() == "THAI42":
|
||||
self._raw(CHARCODE_THAI42)
|
||||
elif code.upper() == "THAI11":
|
||||
self._raw(CHARCODE_THAI11)
|
||||
elif code.upper() == "THAI13":
|
||||
self._raw(CHARCODE_THAI13)
|
||||
elif code.upper() == "THAI14":
|
||||
self._raw(CHARCODE_THAI14)
|
||||
elif code.upper() == "THAI16":
|
||||
self._raw(CHARCODE_THAI16)
|
||||
elif code.upper() == "THAI17":
|
||||
self._raw(CHARCODE_THAI17)
|
||||
elif code.upper() == "THAI18":
|
||||
self._raw(CHARCODE_THAI18)
|
||||
else:
|
||||
raise CharCodeError()
|
||||
|
||||
def barcode(self, code, bc, width, height, pos, font):
|
||||
""" Print Barcode """
|
||||
# Align Bar Code()
|
||||
self._raw(TXT_ALIGN_CT)
|
||||
# Height
|
||||
if height >=2 or height <=6:
|
||||
self._raw(BARCODE_HEIGHT)
|
||||
else:
|
||||
raise BarcodeSizeError()
|
||||
# Width
|
||||
if width >= 1 or width <=255:
|
||||
self._raw(BARCODE_WIDTH)
|
||||
else:
|
||||
raise BarcodeSizeError()
|
||||
# Font
|
||||
if font.upper() == "B":
|
||||
self._raw(BARCODE_FONT_B)
|
||||
else: # DEFAULT FONT: A
|
||||
self._raw(BARCODE_FONT_A)
|
||||
# Position
|
||||
if pos.upper() == "OFF":
|
||||
self._raw(BARCODE_TXT_OFF)
|
||||
elif pos.upper() == "BOTH":
|
||||
self._raw(BARCODE_TXT_BTH)
|
||||
elif pos.upper() == "ABOVE":
|
||||
self._raw(BARCODE_TXT_ABV)
|
||||
else: # DEFAULT POSITION: BELOW
|
||||
self._raw(BARCODE_TXT_BLW)
|
||||
# Type
|
||||
if bc.upper() == "UPC-A":
|
||||
self._raw(BARCODE_UPC_A)
|
||||
elif bc.upper() == "UPC-E":
|
||||
self._raw(BARCODE_UPC_E)
|
||||
elif bc.upper() == "EAN13":
|
||||
self._raw(BARCODE_EAN13)
|
||||
elif bc.upper() == "EAN8":
|
||||
self._raw(BARCODE_EAN8)
|
||||
elif bc.upper() == "CODE39":
|
||||
self._raw(BARCODE_CODE39)
|
||||
elif bc.upper() == "ITF":
|
||||
self._raw(BARCODE_ITF)
|
||||
elif bc.upper() == "NW7":
|
||||
self._raw(BARCODE_NW7)
|
||||
else:
|
||||
raise BarcodeTypeError()
|
||||
# Print Code
|
||||
if code:
|
||||
self._raw(code)
|
||||
else:
|
||||
raise exception.BarcodeCodeError()
|
||||
|
||||
|
||||
def text(self, txt):
|
||||
""" Print alpha-numeric text """
|
||||
if txt:
|
||||
self._raw(txt)
|
||||
else:
|
||||
raise TextError()
|
||||
|
||||
|
||||
def set(self, align='left', font='a', type='normal', width=1, height=1, density=9):
|
||||
""" Set text properties """
|
||||
# Width
|
||||
if height == 2 and width == 2:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_4SQUARE)
|
||||
elif height == 2 and width != 2:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_2HEIGHT)
|
||||
elif width == 2 and height != 2:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_2WIDTH)
|
||||
else: # DEFAULT SIZE: NORMAL
|
||||
self._raw(TXT_NORMAL)
|
||||
# Type
|
||||
if type.upper() == "B":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL_OFF)
|
||||
elif type.upper() == "U":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL_ON)
|
||||
elif type.upper() == "U2":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL2_ON)
|
||||
elif type.upper() == "BU":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL_ON)
|
||||
elif type.upper() == "BU2":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL2_ON)
|
||||
elif type.upper == "NORMAL":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL_OFF)
|
||||
# Font
|
||||
if font.upper() == "B":
|
||||
self._raw(TXT_FONT_B)
|
||||
else: # DEFAULT FONT: A
|
||||
self._raw(TXT_FONT_A)
|
||||
# Align
|
||||
if align.upper() == "CENTER":
|
||||
self._raw(TXT_ALIGN_CT)
|
||||
elif align.upper() == "RIGHT":
|
||||
self._raw(TXT_ALIGN_RT)
|
||||
elif align.upper() == "LEFT":
|
||||
self._raw(TXT_ALIGN_LT)
|
||||
# Density
|
||||
if density == 0:
|
||||
self._raw(PD_N50)
|
||||
elif density == 1:
|
||||
self._raw(PD_N37)
|
||||
elif density == 2:
|
||||
self._raw(PD_N25)
|
||||
elif density == 3:
|
||||
self._raw(PD_N12)
|
||||
elif density == 4:
|
||||
self._raw(PD_0)
|
||||
elif density == 5:
|
||||
self._raw(PD_P12)
|
||||
elif density == 6:
|
||||
self._raw(PD_P25)
|
||||
elif density == 7:
|
||||
self._raw(PD_P37)
|
||||
elif density == 8:
|
||||
self._raw(PD_P50)
|
||||
else:# DEFAULT: DOES NOTHING
|
||||
pass
|
||||
|
||||
|
||||
def cut(self, mode=''):
|
||||
""" Cut paper """
|
||||
# Fix the size between last line and cut
|
||||
# TODO: handle this with a line feed
|
||||
self._raw("\n\n\n\n\n\n")
|
||||
if mode.upper() == "PART":
|
||||
self._raw(PAPER_PART_CUT)
|
||||
else: # DEFAULT MODE: FULL CUT
|
||||
self._raw(PAPER_FULL_CUT)
|
||||
|
||||
|
||||
def cashdraw(self, pin):
|
||||
""" Send pulse to kick the cash drawer """
|
||||
if pin == 2:
|
||||
self._raw(CD_KICK_2)
|
||||
elif pin == 5:
|
||||
self._raw(CD_KICK_5)
|
||||
else:
|
||||
raise CashDrawerError()
|
||||
|
||||
|
||||
def hw(self, hw):
|
||||
""" Hardware operations """
|
||||
if hw.upper() == "INIT":
|
||||
self._raw(HW_INIT)
|
||||
elif hw.upper() == "SELECT":
|
||||
self._raw(HW_SELECT)
|
||||
elif hw.upper() == "RESET":
|
||||
self._raw(HW_RESET)
|
||||
else: # DEFAULT: DOES NOTHING
|
||||
pass
|
||||
|
||||
|
||||
def control(self, ctl, pos=4):
|
||||
""" Feed control sequences """
|
||||
# Set tab positions
|
||||
if pos < 1 or pos > 16:
|
||||
raise TabError()
|
||||
else:
|
||||
self._raw("".join([CTL_SET_HT,hex(pos)]))
|
||||
# Set position
|
||||
if ctl.upper() == "LF":
|
||||
self._raw(CTL_LF)
|
||||
elif ctl.upper() == "FF":
|
||||
self._raw(CTL_FF)
|
||||
elif ctl.upper() == "CR":
|
||||
self._raw(CTL_CR)
|
||||
elif ctl.upper() == "HT":
|
||||
self._raw(CTL_HT)
|
||||
elif ctl.upper() == "VT":
|
||||
self._raw(CTL_VT)
|
@@ -1,102 +0,0 @@
|
||||
""" ESC/POS Exceptions classes """
|
||||
|
||||
import os
|
||||
|
||||
class Error(Exception):
|
||||
""" Base class for ESC/POS errors """
|
||||
def __init__(self, msg, status=None):
|
||||
Exception.__init__(self)
|
||||
self.msg = msg
|
||||
self.resultcode = 1
|
||||
if status is not None:
|
||||
self.resultcode = status
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
# Result/Exit codes
|
||||
# 0 = success
|
||||
# 10 = No Barcode type defined
|
||||
# 20 = Barcode size values are out of range
|
||||
# 30 = Barcode text not supplied
|
||||
# 40 = Image height is too large
|
||||
# 50 = No string supplied to be printed
|
||||
# 60 = Invalid pin to send Cash Drawer pulse
|
||||
# 70 = Invalid number of tab positions
|
||||
# 80 = Invalid char code
|
||||
|
||||
|
||||
class BarcodeTypeError(Error):
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 10
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode type is defined"
|
||||
|
||||
class BarcodeSizeError(Error):
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 20
|
||||
|
||||
def __str__(self):
|
||||
return "Barcode size is out of range"
|
||||
|
||||
class BarcodeCodeError(Error):
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 30
|
||||
|
||||
def __str__(self):
|
||||
return "Code was not supplied"
|
||||
|
||||
class ImageSizeError(Error):
|
||||
def __init__(self, msg=""):
|
||||
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"
|
||||
|
||||
class TextError(Error):
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 50
|
||||
|
||||
def __str__(self):
|
||||
return "Text string must be supplied to the text() method"
|
||||
|
||||
|
||||
class CashDrawerError(Error):
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 60
|
||||
|
||||
def __str__(self):
|
||||
return "Valid pin must be set to send pulse"
|
||||
|
||||
|
||||
class TabError(Error):
|
||||
def __init__(self, msg=""):
|
||||
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"
|
||||
|
||||
|
||||
class CharCodeError(Error):
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 70
|
||||
|
||||
def __str__(self):
|
||||
return "Valid char code must be set"
|
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
"""
|
||||
@author: Manuel F Martinez <manpaz@bashlinux.com>
|
||||
@organization: Bashlinux
|
||||
@copyright: Copyright (c) 2012 Bashlinux
|
||||
@license: GNU GPL v3
|
||||
"""
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
import serial
|
||||
import socket
|
||||
|
||||
from escpos import *
|
||||
from constants import *
|
||||
from exceptions import *
|
||||
|
||||
class Usb(Escpos):
|
||||
""" Define USB printer """
|
||||
|
||||
def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01):
|
||||
"""
|
||||
@param idVendor : Vendor ID
|
||||
@param idProduct : Product ID
|
||||
@param interface : USB device interface
|
||||
@param in_ep : Input end point
|
||||
@param out_ep : Output end point
|
||||
"""
|
||||
self.idVendor = idVendor
|
||||
self.idProduct = idProduct
|
||||
self.interface = interface
|
||||
self.in_ep = in_ep
|
||||
self.out_ep = out_ep
|
||||
self.open()
|
||||
|
||||
|
||||
def open(self):
|
||||
""" Search device on USB tree and set is as escpos device """
|
||||
self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct)
|
||||
if self.device is None:
|
||||
print "Cable isn't 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: %s" % str(e)
|
||||
|
||||
try:
|
||||
self.device.set_configuration()
|
||||
self.device.reset()
|
||||
except usb.core.USBError as e:
|
||||
print "Could not set configuration: %s" % str(e)
|
||||
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format """
|
||||
self.device.write(self.out_ep, msg, self.interface)
|
||||
|
||||
|
||||
def __del__(self):
|
||||
""" Release USB interface """
|
||||
if self.device:
|
||||
usb.util.dispose_resources(self.device)
|
||||
self.device = None
|
||||
|
||||
|
||||
|
||||
class Serial(Escpos):
|
||||
""" Define Serial printer """
|
||||
|
||||
def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1,
|
||||
parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE,
|
||||
xonxoff=False , dsrdtr=True):
|
||||
"""
|
||||
@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)
|
||||
"""
|
||||
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 """
|
||||
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: %s" % self.devfile
|
||||
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format """
|
||||
self.device.write(msg)
|
||||
|
||||
|
||||
def __del__(self):
|
||||
""" Close Serial interface """
|
||||
if self.device is not None:
|
||||
self.device.close()
|
||||
|
||||
|
||||
|
||||
class Network(Escpos):
|
||||
""" Define Network printer """
|
||||
|
||||
def __init__(self,host,port=9100):
|
||||
"""
|
||||
@param host : Printer's hostname or IP address
|
||||
@param port : Port to write to
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.open()
|
||||
|
||||
|
||||
def open(self):
|
||||
""" Open TCP socket and set it as escpos device """
|
||||
self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.device.connect((self.host, self.port))
|
||||
|
||||
if self.device is None:
|
||||
print "Could not open socket for %s" % self.host
|
||||
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format """
|
||||
self.device.send(msg)
|
||||
|
||||
|
||||
def __del__(self):
|
||||
""" Close TCP connection """
|
||||
self.device.close()
|
||||
|
||||
|
||||
|
||||
class File(Escpos):
|
||||
""" Define Generic file printer """
|
||||
|
||||
def __init__(self, devfile="/dev/usb/lp0"):
|
||||
"""
|
||||
@param devfile : Device file under dev filesystem
|
||||
"""
|
||||
self.devfile = devfile
|
||||
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 %s" % self.devfile
|
||||
|
||||
|
||||
def _raw(self, msg):
|
||||
""" Print any command sent in raw format """
|
||||
self.device.write(msg);
|
||||
|
||||
|
||||
def __del__(self):
|
||||
""" Close system file """
|
||||
self.device.close()
|
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
-e .
|
7
setup.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[nosetests]
|
||||
verbosity=3
|
||||
with-doctest=1
|
||||
|
||||
[bdist_wheel]
|
||||
# This flag says that the code is written to work on both Python 2 and Python 3.
|
||||
universal=1
|
111
setup.py
@@ -1,29 +1,112 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from distutils.core import setup
|
||||
import os
|
||||
import sys
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as test_command
|
||||
|
||||
|
||||
base_dir = os.path.dirname(__file__)
|
||||
src_dir = os.path.join(base_dir, "src")
|
||||
|
||||
# When executing the setup.py, we need to be able to import ourselves, this
|
||||
# means that we need to add the src/ directory to the sys.path.
|
||||
sys.path.insert(0, src_dir)
|
||||
|
||||
|
||||
def read(fname):
|
||||
"""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)
|
||||
|
||||
setup(
|
||||
name='escpos',
|
||||
version='1.0.9',
|
||||
url='https://github.com/manpaz/python-escpos',
|
||||
download_url='https://github.com/manpaz/python-escpos.git',
|
||||
name='python-escpos',
|
||||
use_scm_version=True,
|
||||
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='GNU GPL v3',
|
||||
long_description=open('README').read(),
|
||||
author='Manuel F Martinez',
|
||||
long_description=read('README.rst'),
|
||||
author='Manuel F Martinez and others',
|
||||
author_email='manpaz@bashlinux.com',
|
||||
platforms=['linux'],
|
||||
packages=[
|
||||
'escpos',
|
||||
maintainer='Patrick Kanzler',
|
||||
maintainer_email='patrick.kanzler@fablab.fau.de',
|
||||
keywords=[
|
||||
'ESC/POS',
|
||||
'thermoprinter',
|
||||
'voucher printer',
|
||||
'printing',
|
||||
'receipt,',
|
||||
],
|
||||
platforms='any',
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages(where="src", exclude=["tests", "tests.*"]),
|
||||
package_data={'': ['COPYING']},
|
||||
classifiers=[
|
||||
'Development Status :: 1 - Alpha',
|
||||
'License :: OSI Approved :: GNU GPL v3',
|
||||
'Operating System :: GNU/Linux',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: MacOS',
|
||||
'Operating System :: GNU/Linux',
|
||||
'Programming Language :: Python',
|
||||
'Topic :: System :: Pheripherals',
|
||||
'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 :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: System :: Peripherals',
|
||||
'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',
|
||||
],
|
||||
setup_requires=[
|
||||
'setuptools_scm',
|
||||
],
|
||||
tests_require=['tox', 'nose', 'scripttest'],
|
||||
cmdclass={'test': Tox},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'python-escpos = escpos.cli:main'
|
||||
]
|
||||
},
|
||||
)
|
||||
|
1
src/escpos/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = ["constants", "escpos", "exceptions", "printer"]
|
545
src/escpos/cli.py
Normal file
@@ -0,0 +1,545 @@
|
||||
#!/usr/bin/env python
|
||||
""" 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.
|
||||
|
||||
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 sys
|
||||
import six
|
||||
from . import config
|
||||
|
||||
|
||||
# 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
|
||||
"""
|
||||
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')
|
||||
|
||||
# Used in demo method
|
||||
# Key: The name of escpos function and the argument passed on the CLI. Some
|
||||
# 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', }
|
||||
],
|
||||
'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_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'},
|
||||
],
|
||||
}
|
||||
|
||||
# Used to build the CLI
|
||||
# A list of dictionaries. Each dict is a CLI argument.
|
||||
# Keys:
|
||||
# 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 = [
|
||||
{
|
||||
'parser': {
|
||||
'name': 'qr',
|
||||
'help': 'Print a QR code',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'qr',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--content',),
|
||||
'help': 'Text to print as a qr code',
|
||||
'required': True,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'text',
|
||||
'help': 'Print plain text',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'text',
|
||||
},
|
||||
'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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'cut',
|
||||
'help': 'Cut the paper',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'cut',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--mode',),
|
||||
'help': 'Type of cut',
|
||||
'choices': ['FULL', 'PART'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'cashdraw',
|
||||
'help': 'Kick the cash drawer',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'cashdraw',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--pin',),
|
||||
'help': 'Which PIN to kick',
|
||||
'choices': [2, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'image',
|
||||
'help': 'Print an image',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'image',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'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': '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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'charcode',
|
||||
'help': 'Set character code table',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'charcode',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--code',),
|
||||
'help': 'Character code',
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'set',
|
||||
'help': 'Set text properties',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'set',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--align',),
|
||||
'help': 'Horizontal alignment',
|
||||
'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': ('--width',),
|
||||
'help': 'Width multiplier',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--height',),
|
||||
'help': 'Height multiplier',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'option_strings': ('--density',),
|
||||
'help': 'Print density',
|
||||
'type': int,
|
||||
},
|
||||
{
|
||||
'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': ('--flip',),
|
||||
'help': 'Text smoothing. Effective on >: 4x4 text',
|
||||
'type': str_to_bool,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'hw',
|
||||
'help': 'Hardware operations',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'hw',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--hw',),
|
||||
'help': 'Operation',
|
||||
'choices': ['INIT', 'SELECT', 'RESET'],
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'control',
|
||||
'help': 'Control sequences',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'control',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'panel_buttons',
|
||||
'help': 'Controls panel buttons',
|
||||
},
|
||||
'defaults': {
|
||||
'func': 'panel_buttons',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'option_strings': ('--enable',),
|
||||
'help': 'Feed button enabled',
|
||||
'type': str_to_bool,
|
||||
'required': True,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'parser': {
|
||||
'name': 'raw',
|
||||
'help': 'Raw data',
|
||||
},
|
||||
'defaults': {
|
||||
'func': '_raw',
|
||||
},
|
||||
'arguments': [
|
||||
{
|
||||
'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.',
|
||||
)
|
||||
|
||||
parser.register('type', 'bool', str_to_bool)
|
||||
|
||||
# Allow config file location to be passed
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
help='Altnerate 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',
|
||||
)
|
||||
|
||||
# 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.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')
|
||||
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',
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--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',
|
||||
)
|
||||
demo_group.add_argument(
|
||||
'--text',
|
||||
help='Print some demo text',
|
||||
action='store_true',
|
||||
)
|
||||
|
||||
# Get only arguments actually passed
|
||||
args_dict = vars(parser.parse_args())
|
||||
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)
|
||||
|
||||
# If there was a config path passed, grab it
|
||||
config_path = command_arguments.pop('config', None)
|
||||
|
||||
# Load the configuration and defined printer
|
||||
saved_config = config.Config()
|
||||
saved_config.load(config_path)
|
||||
printer = saved_config.printer()
|
||||
|
||||
if not printer:
|
||||
raise Exception('No printers loaded from config')
|
||||
|
||||
target_command = command_arguments.pop('func')
|
||||
|
||||
if hasattr(printer, target_command):
|
||||
# print command with args
|
||||
getattr(printer, target_command)(**command_arguments)
|
||||
if target_command in REQUIRES_NEWLINE:
|
||||
printer.text("\n")
|
||||
else:
|
||||
command_arguments['printer'] = printer
|
||||
globals()[target_command](**command_arguments)
|
||||
|
||||
|
||||
def demo(printer, **kwargs):
|
||||
"""
|
||||
Prints specificed demos. Called when CLI is passed `demo`. This function
|
||||
uses the DEMO_FUNCTIONS dictionary.
|
||||
|
||||
:param printer: A printer from escpos.printer
|
||||
:param kwargs: A dict with a key for each function you want to test. It's
|
||||
in this format since it usually comes from argparse.
|
||||
"""
|
||||
for demo_choice in kwargs.keys():
|
||||
command = getattr(
|
||||
printer,
|
||||
demo_choice
|
||||
.replace('barcodes_a', 'barcode')
|
||||
.replace('barcodes_b', 'barcode')
|
||||
)
|
||||
for params in DEMO_FUNCTIONS[demo_choice]:
|
||||
command(**params)
|
||||
printer.cut()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
118
src/escpos/config.py
Normal file
@@ -0,0 +1,118 @@
|
||||
""" ESC/POS configuration manager.
|
||||
|
||||
This module contains the implentations 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 appdirs
|
||||
import yaml
|
||||
|
||||
from . import printer
|
||||
from . import exceptions
|
||||
|
||||
|
||||
class Config(object):
|
||||
""" Configuration handler class.
|
||||
|
||||
This class loads configuration from a default or specificed directory. It
|
||||
can create your defined printer and return it to you.
|
||||
"""
|
||||
_app_name = 'python-escpos'
|
||||
_config_file = 'config.yaml'
|
||||
|
||||
def __init__(self):
|
||||
""" Initialize configuration.
|
||||
|
||||
Remember to add anything that needs to be reset between configurations
|
||||
to self._reset_config
|
||||
"""
|
||||
self._has_loaded = False
|
||||
self._printer = None
|
||||
|
||||
self._printer_name = None
|
||||
self._printer_config = None
|
||||
|
||||
def _reset_config(self):
|
||||
""" Clear the loaded configuration.
|
||||
|
||||
If we are loading a changed config, we don't want to have leftover
|
||||
data.
|
||||
"""
|
||||
self._has_loaded = False
|
||||
self._printer = None
|
||||
|
||||
self._printer_name = None
|
||||
self._printer_config = None
|
||||
|
||||
def load(self, config_path=None):
|
||||
""" 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
|
||||
)
|
||||
|
||||
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:
|
||||
config = yaml.safe_load(config_file)
|
||||
except EnvironmentError:
|
||||
raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format(
|
||||
config_path=str(config_path),
|
||||
))
|
||||
except yaml.YAMLError:
|
||||
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 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,
|
||||
)
|
||||
)
|
||||
|
||||
self._has_loaded = True
|
||||
|
||||
def printer(self):
|
||||
""" Returns a printer that was defined in the config, or throws an
|
||||
exception.
|
||||
|
||||
This method loads the default config if one hasn't beeen already loaded.
|
||||
|
||||
"""
|
||||
if not self._has_loaded:
|
||||
self.load()
|
||||
|
||||
if not self._printer_name:
|
||||
raise exceptions.ConfigSectionMissingError('printer')
|
||||
|
||||
if not self._printer:
|
||||
# We could catch init errors and make them a ConfigSyntaxError,
|
||||
# but I'll just let them pass
|
||||
self._printer = getattr(printer, self._printer_name)(**self._printer_config)
|
||||
|
||||
return self._printer
|
216
src/escpos/constants.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- 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
|
||||
moved to `capabilities` as in `escpos-php by @mike42 <https://github.com/mike42/escpos-php>`_.
|
||||
|
||||
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import six
|
||||
|
||||
# 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'
|
||||
|
||||
# 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
|
||||
|
||||
# Printer hardware
|
||||
HW_INIT = ESC + b'@' # Clear data in buffer and reset modes
|
||||
HW_SELECT = ESC + b'=\x01' # Printer select
|
||||
|
||||
HW_RESET = 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_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 []
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Text format
|
||||
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
|
||||
# Printers" and tidy up this stuff too.
|
||||
TXT_FLIP_ON = ESC + b'\x7b\x01'
|
||||
TXT_FLIP_OFF = ESC + b'\x7b\x00'
|
||||
TXT_SMOOTH_ON = GS + b'\x62\x01'
|
||||
TXT_SMOOTH_OFF = GS + b'\x62\x00'
|
||||
TXT_SIZE = GS + b'!'
|
||||
TXT_WIDTH = {1: 0x00,
|
||||
2: 0x10,
|
||||
3: 0x20,
|
||||
4: 0x30,
|
||||
5: 0x40,
|
||||
6: 0x50,
|
||||
7: 0x60,
|
||||
8: 0x70}
|
||||
TXT_HEIGHT = {1: 0x00,
|
||||
2: 0x01,
|
||||
3: 0x02,
|
||||
4: 0x03,
|
||||
5: 0x04,
|
||||
6: 0x05,
|
||||
7: 0x06,
|
||||
8: 0x07}
|
||||
TXT_NORMAL = ESC + b'!\x00' # Normal text
|
||||
TXT_2HEIGHT = ESC + b'!\x10' # Double height text
|
||||
TXT_2WIDTH = ESC + b'!\x20' # Double width text
|
||||
TXT_4SQUARE = ESC + b'!\x30' # Quad area text
|
||||
TXT_UNDERL_OFF = ESC + b'\x2d\x00' # Underline font OFF
|
||||
TXT_UNDERL_ON = ESC + b'\x2d\x01' # Underline font 1-dot ON
|
||||
TXT_UNDERL2_ON = ESC + b'\x2d\x02' # Underline font 2-dot ON
|
||||
TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF
|
||||
TXT_BOLD_ON = ESC + b'\x45\x01' # Bold font ON
|
||||
TXT_FONT_A = ESC + b'\x4d\x00' # Font type A
|
||||
TXT_FONT_B = ESC + b'\x4d\x01' # Font type B
|
||||
TXT_ALIGN_LT = ESC + b'\x61\x00' # Left justification
|
||||
TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering
|
||||
TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification
|
||||
TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON
|
||||
TXT_INVERT_OFF = GS + b'\x42\x00' # Inverse Printing OFF
|
||||
|
||||
# Char code table
|
||||
CHARCODE_PC437 = ESC + b'\x74\x00' # USA: Standard Europe
|
||||
CHARCODE_JIS = ESC + b'\x74\x01' # Japanese Katakana
|
||||
CHARCODE_PC850 = ESC + b'\x74\x02' # Multilingual
|
||||
CHARCODE_PC860 = ESC + b'\x74\x03' # Portuguese
|
||||
CHARCODE_PC863 = ESC + b'\x74\x04' # Canadian-French
|
||||
CHARCODE_PC865 = ESC + b'\x74\x05' # Nordic
|
||||
CHARCODE_WEU = ESC + b'\x74\x06' # Simplified Kanji, Hirakana
|
||||
CHARCODE_GREEK = ESC + b'\x74\x07' # Simplified Kanji
|
||||
CHARCODE_HEBREW = ESC + b'\x74\x08' # Simplified Kanji
|
||||
CHARCODE_PC1252 = ESC + b'\x74\x11' # Western European Windows Code Set
|
||||
CHARCODE_PC866 = ESC + b'\x74\x12' # Cirillic #2
|
||||
CHARCODE_PC852 = ESC + b'\x74\x13' # Latin 2
|
||||
CHARCODE_PC858 = ESC + b'\x74\x14' # Euro
|
||||
CHARCODE_THAI42 = ESC + b'\x74\x15' # Thai character code 42
|
||||
CHARCODE_THAI11 = ESC + b'\x74\x16' # Thai character code 11
|
||||
CHARCODE_THAI13 = ESC + b'\x74\x17' # Thai character code 13
|
||||
CHARCODE_THAI14 = ESC + b'\x74\x18' # Thai character code 14
|
||||
CHARCODE_THAI16 = ESC + b'\x74\x19' # Thai character code 16
|
||||
CHARCODE_THAI17 = ESC + b'\x74\x1a' # Thai character code 17
|
||||
CHARCODE_THAI18 = ESC + b'\x74\x1b' # Thai character code 18
|
||||
|
||||
# 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_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
|
||||
|
||||
BARCODE_HEIGHT = GS + b'h' # Barcode Height [1-255]
|
||||
BARCODE_WIDTH = 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)
|
||||
|
||||
# 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 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),
|
||||
# These are all the same barcode, but using different charcter sets
|
||||
'CODE128A': _SET_BARCODE_TYPE(73) + b'{A', # CODE128 character set A
|
||||
'CODE128B': _SET_BARCODE_TYPE(73) + b'{B', # CODE128 character set B
|
||||
'CODE128C': _SET_BARCODE_TYPE(73) + b'{C', # CODE128 character set C
|
||||
'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,
|
||||
}
|
||||
|
||||
# QRCode error correction levels
|
||||
QR_ECLEVEL_L = 0
|
||||
QR_ECLEVEL_M = 1
|
||||
QR_ECLEVEL_Q = 2
|
||||
QR_ECLEVEL_H = 3
|
||||
|
||||
# QRcode models
|
||||
QR_MODEL_1 = 1
|
||||
QR_MODEL_2 = 2
|
||||
QR_MICRO = 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
|
||||
|
||||
# Printing Density
|
||||
PD_N50 = GS + b'\x7c\x00' # Printing Density -50%
|
||||
PD_N37 = GS + b'\x7c\x01' # Printing Density -37.5%
|
||||
PD_N25 = GS + b'\x7c\x02' # Printing Density -25%
|
||||
PD_N12 = GS + b'\x7c\x03' # Printing Density -12.5%
|
||||
PD_0 = GS + b'\x7c\x04' # Printing Density 0%
|
||||
PD_P50 = GS + b'\x7c\x08' # Printing Density +50%
|
||||
PD_P37 = GS + b'\x7c\x07' # Printing Density +37.5%
|
||||
PD_P25 = GS + b'\x7c\x06' # Printing Density +25%
|
||||
PD_P12 = GS + b'\x7c\x05' # Printing Density +12.5%
|
746
src/escpos/escpos.py
Normal file
@@ -0,0 +1,746 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Main class
|
||||
|
||||
This module contains the 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 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import qrcode
|
||||
import textwrap
|
||||
|
||||
from .constants import *
|
||||
from .exceptions import *
|
||||
|
||||
from abc import ABCMeta, abstractmethod # abstract base class support
|
||||
from escpos.image import EscposImage
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class Escpos(object):
|
||||
""" ESC/POS Printer object
|
||||
|
||||
This class is the abstract base class for an esc/pos-printer. The printer implementations are children of this
|
||||
class.
|
||||
"""
|
||||
device = None
|
||||
codepage = None
|
||||
|
||||
def __init__(self, columns=32):
|
||||
""" Initialize ESCPOS Printer
|
||||
|
||||
:param columns: Text columns used by the printer. Defaults to 32."""
|
||||
self.columns = columns
|
||||
|
||||
def __del__(self):
|
||||
""" call self.close upon deletion """
|
||||
self.close()
|
||||
|
||||
@abstractmethod
|
||||
def _raw(self, msg):
|
||||
""" Sends raw data to the printer
|
||||
|
||||
This function has to be individually implemented by the implementations.
|
||||
|
||||
:param msg: message string to be sent to the printer
|
||||
:type msg: bytes
|
||||
"""
|
||||
pass
|
||||
|
||||
def image(self, img_source, high_density_vertical=True, high_density_horizontal=True, impl="bitImageRaster"):
|
||||
""" Print an image
|
||||
|
||||
You can select whether the printer should print in high density or not. The default value is high density.
|
||||
When printing in low density, the image will be stretched.
|
||||
|
||||
Esc/Pos supplies several commands for printing. This function supports three of them. Please try to vary the
|
||||
implementations if you have any problems. For example the printer `IT80-002` will have trouble aligning
|
||||
images that are not printed in Column-mode.
|
||||
|
||||
The available printing implementations are:
|
||||
|
||||
* `bitImageRaster`: prints with the `GS v 0`-command
|
||||
* `graphics`: prints with the `GS ( L`-command
|
||||
* `bitImageColumn`: prints with the `ESC *`-command
|
||||
|
||||
:param img_source: PIL image or filename to load: `jpg`, `gif`, `png` or `bmp`
|
||||
:param high_density_vertical: print in high density in vertical direction *default:* True
|
||||
:param high_density_horizontal: print in high density in horizontal direction *default:* True
|
||||
:param impl: choose image printing mode between `bitImageRaster`, `graphics` or `bitImageColumn`
|
||||
|
||||
"""
|
||||
im = EscposImage(img_source)
|
||||
|
||||
if impl == "bitImageRaster":
|
||||
# GS v 0, raster format bit image
|
||||
density_byte = (0 if high_density_horizontal else 1) + (0 if high_density_vertical else 2)
|
||||
header = GS + b"v0" + six.int2byte(density_byte) + self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.height, 2)
|
||||
self._raw(header + im.to_raster_format())
|
||||
|
||||
if impl == "graphics":
|
||||
# GS ( L raster format graphics
|
||||
img_header = self._int_low_high(im.width, 2) + self._int_low_high(im.height, 2)
|
||||
tone = b'0'
|
||||
colors = b'1'
|
||||
ym = six.int2byte(1 if high_density_vertical else 2)
|
||||
xm = six.int2byte(1 if high_density_horizontal else 2)
|
||||
header = tone + xm + ym + colors + img_header
|
||||
raster_data = im.to_raster_format()
|
||||
self._image_send_graphics_data(b'0', b'p', header + raster_data)
|
||||
self._image_send_graphics_data(b'0', b'2', b'')
|
||||
|
||||
if impl == "bitImageColumn":
|
||||
# ESC *, column format bit image
|
||||
density_byte = (1 if high_density_horizontal else 0) + (32 if high_density_vertical else 0)
|
||||
header = ESC + b"*" + six.int2byte(density_byte) + self._int_low_high(im.width, 2)
|
||||
outp = [ESC + b"3" + six.int2byte(16)] # Adjust line-feed size
|
||||
for blob in im.to_column_format(high_density_vertical):
|
||||
outp.append(header + blob + b"\n")
|
||||
outp.append(ESC + b"2") # Reset line-feed size
|
||||
self._raw(b''.join(outp))
|
||||
|
||||
def _image_send_graphics_data(self, m, fn, data):
|
||||
"""
|
||||
Wrapper for GS ( L, to calculate and send correct data length.
|
||||
|
||||
:param m: Modifier//variant for function. Usually '0'
|
||||
:param fn: Function number to use, as byte
|
||||
:param data: Data to send
|
||||
"""
|
||||
header = self._int_low_high(len(data) + 2, 2)
|
||||
self._raw(GS + b'(L' + header + m + fn + data)
|
||||
|
||||
def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False):
|
||||
""" Print QR Code for the provided string
|
||||
|
||||
:param content: The content of the code. Numeric data will be more efficiently compacted.
|
||||
:param ec: Error-correction level to use. One of QR_ECLEVEL_L (default), QR_ECLEVEL_M, QR_ECLEVEL_Q or
|
||||
QR_ECLEVEL_H.
|
||||
Higher error correction results in a less compact code.
|
||||
:param size: Pixel size to use. Must be 1-16 (default 3)
|
||||
:param model: QR code model to use. Must be one of QR_MODEL_1, QR_MODEL_2 (default) or QR_MICRO (not supported
|
||||
by all printers).
|
||||
:param native: True to render the code on the printer, False to render the code as an image and send it to the
|
||||
printer (Default)
|
||||
"""
|
||||
# Basic validation
|
||||
if ec not in [QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q]:
|
||||
raise ValueError("Invalid error correction level")
|
||||
if not 1 <= size <= 16:
|
||||
raise ValueError("Invalid block size (must be 1-16)")
|
||||
if model not in [QR_MODEL_1, QR_MODEL_2, QR_MICRO]:
|
||||
raise ValueError("Invalid QR model (must be one of QR_MODEL_1, QR_MODEL_2, QR_MICRO)")
|
||||
if content == "":
|
||||
# Handle edge case by printing nothing.
|
||||
return
|
||||
if not native:
|
||||
# Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image
|
||||
if model != QR_MODEL_2:
|
||||
raise ValueError("Invalid QR model for qrlib rendering (must be QR_MODEL_2)")
|
||||
python_qr_ec = {
|
||||
QR_ECLEVEL_H: qrcode.constants.ERROR_CORRECT_H,
|
||||
QR_ECLEVEL_L: qrcode.constants.ERROR_CORRECT_L,
|
||||
QR_ECLEVEL_M: qrcode.constants.ERROR_CORRECT_M,
|
||||
QR_ECLEVEL_Q: qrcode.constants.ERROR_CORRECT_Q
|
||||
}
|
||||
qr_code = qrcode.QRCode(version=None, box_size=size, border=1, error_correction=python_qr_ec[ec])
|
||||
qr_code.add_data(content)
|
||||
qr_code.make(fit=True)
|
||||
qr_img = qr_code.make_image()
|
||||
im = qr_img._img.convert("RGB")
|
||||
# Convert the RGB image in printable image
|
||||
self.image(im)
|
||||
return
|
||||
# Native 2D code printing
|
||||
cn = b'1' # Code type for QR code
|
||||
# Select model: 1, 2 or micro.
|
||||
self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0))
|
||||
# Set dot size.
|
||||
self._send_2d_code_data(six.int2byte(67), cn, six.int2byte(size))
|
||||
# Set error correction level: L, M, Q, or H
|
||||
self._send_2d_code_data(six.int2byte(69), cn, six.int2byte(48 + ec))
|
||||
# Send content & print
|
||||
self._send_2d_code_data(six.int2byte(80), cn, content.encode('utf-8'), b'0')
|
||||
self._send_2d_code_data(six.int2byte(81), cn, b'', b'0')
|
||||
|
||||
def _send_2d_code_data(self, fn, cn, data, m=b''):
|
||||
""" Wrapper for GS ( k, to calculate and send correct data length.
|
||||
|
||||
:param fn: Function to use.
|
||||
:param cn: Output code type. Affects available data.
|
||||
:param data: Data to send.
|
||||
:param m: Modifier/variant for function. Often '0' where used.
|
||||
"""
|
||||
if len(m) > 1 or len(cn) != 1 or len(fn) != 1:
|
||||
raise ValueError("cn and fn must be one byte each.")
|
||||
header = self._int_low_high(len(data) + len(m) + 2, 2)
|
||||
self._raw(GS + b'(k' + header + cn + fn + m + data)
|
||||
|
||||
@staticmethod
|
||||
def _int_low_high(inp_number, out_bytes):
|
||||
""" Generate multiple bytes for a number: In lower and higher parts, or more parts as needed.
|
||||
|
||||
:param inp_number: Input number
|
||||
:param out_bytes: The number of bytes to output (1 - 4).
|
||||
"""
|
||||
max_input = (256 << (out_bytes * 8) - 1)
|
||||
if not 1 <= out_bytes <= 4:
|
||||
raise ValueError("Can only output 1-4 byes")
|
||||
if not 0 <= inp_number <= max_input:
|
||||
raise ValueError("Number too large. Can only output up to {0} in {1} byes".format(max_input, out_bytes))
|
||||
outp = b''
|
||||
for _ in range(0, out_bytes):
|
||||
outp += six.int2byte(inp_number % 256)
|
||||
inp_number //= 256
|
||||
return outp
|
||||
|
||||
def charcode(self, code):
|
||||
""" Set Character Code Table
|
||||
|
||||
Sends the control sequence from :py:mod:`escpos.constants` to the printer
|
||||
with :py:meth:`escpos.printer.'implementation'._raw()`.
|
||||
|
||||
:param code: Name of CharCode
|
||||
:raises: :py:exc:`~escpos.exceptions.CharCodeError`
|
||||
"""
|
||||
# TODO improve this (rather unhandy code)
|
||||
# TODO check the codepages
|
||||
if code.upper() == "USA":
|
||||
self._raw(CHARCODE_PC437)
|
||||
self.codepage = 'cp437'
|
||||
elif code.upper() == "JIS":
|
||||
self._raw(CHARCODE_JIS)
|
||||
self.codepage = 'cp932'
|
||||
elif code.upper() == "MULTILINGUAL":
|
||||
self._raw(CHARCODE_PC850)
|
||||
self.codepage = 'cp850'
|
||||
elif code.upper() == "PORTUGUESE":
|
||||
self._raw(CHARCODE_PC860)
|
||||
self.codepage = 'cp860'
|
||||
elif code.upper() == "CA_FRENCH":
|
||||
self._raw(CHARCODE_PC863)
|
||||
self.codepage = 'cp863'
|
||||
elif code.upper() == "NORDIC":
|
||||
self._raw(CHARCODE_PC865)
|
||||
self.codepage = 'cp865'
|
||||
elif code.upper() == "WEST_EUROPE":
|
||||
self._raw(CHARCODE_WEU)
|
||||
self.codepage = 'latin_1'
|
||||
elif code.upper() == "GREEK":
|
||||
self._raw(CHARCODE_GREEK)
|
||||
self.codepage = 'cp737'
|
||||
elif code.upper() == "HEBREW":
|
||||
self._raw(CHARCODE_HEBREW)
|
||||
self.codepage = 'cp862'
|
||||
# elif code.upper() == "LATVIAN": # this is not listed in the constants
|
||||
# self._raw(CHARCODE_PC755)
|
||||
# self.codepage = 'cp'
|
||||
elif code.upper() == "WPC1252":
|
||||
self._raw(CHARCODE_PC1252)
|
||||
self.codepage = 'cp1252'
|
||||
elif code.upper() == "CIRILLIC2":
|
||||
self._raw(CHARCODE_PC866)
|
||||
self.codepage = 'cp866'
|
||||
elif code.upper() == "LATIN2":
|
||||
self._raw(CHARCODE_PC852)
|
||||
self.codepage = 'cp852'
|
||||
elif code.upper() == "EURO":
|
||||
self._raw(CHARCODE_PC858)
|
||||
self.codepage = 'cp858'
|
||||
elif code.upper() == "THAI42":
|
||||
self._raw(CHARCODE_THAI42)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI11":
|
||||
self._raw(CHARCODE_THAI11)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI13":
|
||||
self._raw(CHARCODE_THAI13)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI14":
|
||||
self._raw(CHARCODE_THAI14)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI16":
|
||||
self._raw(CHARCODE_THAI16)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI17":
|
||||
self._raw(CHARCODE_THAI17)
|
||||
self.codepage = 'cp874'
|
||||
elif code.upper() == "THAI18":
|
||||
self._raw(CHARCODE_THAI18)
|
||||
self.codepage = 'cp874'
|
||||
else:
|
||||
raise CharCodeError()
|
||||
|
||||
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"):
|
||||
""" Print Barcode
|
||||
|
||||
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
|
||||
be supported by the unit. Currently you have to check manually whether your barcode text is correct. Uncorrect
|
||||
barcodes may lead to unexpected printer behaviour. There are two forms of the barcode function. Type A is
|
||||
default but has fewer barcodes, while type B has some more to choose from.
|
||||
|
||||
.. todo:: Add a method to check barcode codes. Alternatively or as an addition write explanations about each
|
||||
barcode-type. Research whether the check digits can be computed autmatically.
|
||||
|
||||
Use the parameters `height` and `width` for adjusting of the barcode size. Please take notice that the barcode
|
||||
will not be printed if it is outside of the printable area. (Which should be impossible with this method, so
|
||||
this information is probably more useful for debugging purposes.)
|
||||
|
||||
.. todo:: On TM-T88II width from 1 to 6 is accepted. Try to acquire command reference and correct the code.
|
||||
.. todo:: Supplying pos does not have an effect for every barcode type. Check and document for which types this
|
||||
is true.
|
||||
|
||||
If you do not want to center the barcode you can call the method with `align_ct=False`, which will disable
|
||||
automatic centering. Please note that when you use center alignment, then the alignment of text will be changed
|
||||
automatically to centered. You have to manually restore the alignment if necessary.
|
||||
|
||||
.. todo:: If further barcode-types are needed they could be rendered transparently as an image. (This could also
|
||||
be of help if the printer does not support types that others do.)
|
||||
|
||||
:param code: alphanumeric data to be printed as bar code
|
||||
:param bc: barcode format, possible values are for type A are:
|
||||
|
||||
* UPC-A
|
||||
* UPC-E
|
||||
* EAN13
|
||||
* EAN8
|
||||
* CODE39
|
||||
* ITF
|
||||
* NW7
|
||||
|
||||
Possible values for type B:
|
||||
|
||||
* All types from function type A
|
||||
* CODE93
|
||||
* CODE128
|
||||
* GS1-128
|
||||
* GS1 DataBar Omnidirectional
|
||||
* GS1 DataBar Truncated
|
||||
* GS1 DataBar Limited
|
||||
* GS1 DataBar Expanded
|
||||
|
||||
If none is specified, the method raises :py:exc:`~escpos.exceptions.BarcodeTypeError`.
|
||||
:param height: barcode height, has to be between 1 and 255
|
||||
*default*: 64
|
||||
:type height: int
|
||||
:param width: barcode width, has to be between 2 and 6
|
||||
*default*: 3
|
||||
:type width: int
|
||||
:param pos: where to place the text relative to the barcode, *default*: BELOW
|
||||
|
||||
* ABOVE
|
||||
* BELOW
|
||||
* BOTH
|
||||
* OFF
|
||||
|
||||
:param font: select font (see ESC/POS-documentation, the device often has two fonts), *default*: A
|
||||
|
||||
* A
|
||||
* B
|
||||
|
||||
:param align_ct: If this parameter is True the barcode will be centered. Otherwise no alignment command will be
|
||||
issued.
|
||||
:type align_ct: bool
|
||||
|
||||
:param function_type: Choose between ESCPOS function type A or B, depending on printer support and desired
|
||||
barcode.
|
||||
*default*: A
|
||||
|
||||
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
||||
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
||||
:py:exc:`~escpos.exceptions.BarcodeCodeError`
|
||||
"""
|
||||
# Align Bar Code()
|
||||
if align_ct:
|
||||
self._raw(TXT_ALIGN_CT)
|
||||
# Height
|
||||
if 1 <= height <= 255:
|
||||
self._raw(BARCODE_HEIGHT + six.int2byte(height))
|
||||
else:
|
||||
raise BarcodeSizeError("height = {height}".format(height=height))
|
||||
# Width
|
||||
if 2 <= width <= 6:
|
||||
self._raw(BARCODE_WIDTH + six.int2byte(width))
|
||||
else:
|
||||
raise BarcodeSizeError("width = {width}".format(width=width))
|
||||
# Font
|
||||
if font.upper() == "B":
|
||||
self._raw(BARCODE_FONT_B)
|
||||
else: # DEFAULT FONT: A
|
||||
self._raw(BARCODE_FONT_A)
|
||||
# Position
|
||||
if pos.upper() == "OFF":
|
||||
self._raw(BARCODE_TXT_OFF)
|
||||
elif pos.upper() == "BOTH":
|
||||
self._raw(BARCODE_TXT_BTH)
|
||||
elif pos.upper() == "ABOVE":
|
||||
self._raw(BARCODE_TXT_ABV)
|
||||
else: # DEFAULT POSITION: BELOW
|
||||
self._raw(BARCODE_TXT_BLW)
|
||||
|
||||
bc_types = BARCODE_TYPES[function_type.upper()]
|
||||
if bc.upper() not in bc_types.keys():
|
||||
# TODO: Raise a better error, or fix the message of this error type
|
||||
raise BarcodeTypeError("Barcode type {bc} not valid for barcode function type {function_type}".format(
|
||||
bc=bc,
|
||||
function_type=function_type,
|
||||
))
|
||||
|
||||
self._raw(bc_types[bc.upper()])
|
||||
|
||||
if function_type.upper() == "B":
|
||||
self._raw(six.int2byte(len(code)))
|
||||
|
||||
# Print Code
|
||||
if code:
|
||||
self._raw(code.encode())
|
||||
else:
|
||||
raise BarcodeCodeError()
|
||||
|
||||
if function_type.upper() == "A":
|
||||
self._raw(NUL)
|
||||
|
||||
def text(self, txt):
|
||||
""" Print alpha-numeric text
|
||||
|
||||
The text has to be encoded in the currently selected codepage.
|
||||
The input text has to be encoded in unicode.
|
||||
|
||||
:param txt: text to be printed
|
||||
:raises: :py:exc:`~escpos.exceptions.TextError`
|
||||
"""
|
||||
if txt:
|
||||
if self.codepage:
|
||||
self._raw(txt.encode(self.codepage))
|
||||
else:
|
||||
self._raw(txt.encode())
|
||||
else:
|
||||
# TODO: why is it problematic to print an empty string?
|
||||
raise TextError()
|
||||
|
||||
def block_text(self, txt, columns=None):
|
||||
""" Text is printed wrapped to specified columns
|
||||
|
||||
Text has to be encoded in unicode.
|
||||
|
||||
:param txt: text to be printed
|
||||
:param columns: amount of columns
|
||||
:return: None
|
||||
"""
|
||||
col_count = self.columns if columns is None else columns
|
||||
self.text(textwrap.fill(txt, col_count))
|
||||
|
||||
def set(self, align='left', font='a', text_type='normal', width=1, height=1, density=9, invert=False, smooth=False,
|
||||
flip=False):
|
||||
""" Set text properties by sending them to the printer
|
||||
|
||||
:param align: horizontal position for text, possible values are:
|
||||
|
||||
* CENTER
|
||||
* LEFT
|
||||
* RIGHT
|
||||
|
||||
*default*: LEFT
|
||||
:param font: font type, possible values are A or B, *default*: A
|
||||
:param text_type: text type, possible values are:
|
||||
|
||||
* B for bold
|
||||
* U for underlined
|
||||
* U2 for underlined, version 2
|
||||
* BU for bold and underlined
|
||||
* BU2 for bold and underlined, version 2
|
||||
* NORMAL for normal text
|
||||
|
||||
*default*: NORMAL
|
||||
:param width: text width multiplier, decimal range 1-8, *default*: 1
|
||||
:param height: text height multiplier, decimal range 1-8, *default*: 1
|
||||
:param density: print density, value from 0-8, if something else is supplied the density remains unchanged
|
||||
:param invert: True enables white on black printing, *default*: False
|
||||
:param smooth: True enables text smoothing. Effective on 4x4 size text and larger, *default*: False
|
||||
:param flip: True enables upside-down printing, *default*: False
|
||||
:type invert: bool
|
||||
"""
|
||||
# Width
|
||||
if height == 2 and width == 2:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_4SQUARE)
|
||||
elif height == 2 and width == 1:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_2HEIGHT)
|
||||
elif width == 2 and height == 1:
|
||||
self._raw(TXT_NORMAL)
|
||||
self._raw(TXT_2WIDTH)
|
||||
elif width == 1 and height == 1:
|
||||
self._raw(TXT_NORMAL)
|
||||
elif 1 <= width <= 8 and 1 <= height <= 8 and isinstance(width, int) and isinstance(height, int):
|
||||
self._raw(TXT_SIZE + six.int2byte(TXT_WIDTH[width] + TXT_HEIGHT[height]))
|
||||
else:
|
||||
raise SetVariableError()
|
||||
# Upside down
|
||||
if flip:
|
||||
self._raw(TXT_FLIP_ON)
|
||||
else:
|
||||
self._raw(TXT_FLIP_OFF)
|
||||
# Smoothing
|
||||
if smooth:
|
||||
self._raw(TXT_SMOOTH_ON)
|
||||
else:
|
||||
self._raw(TXT_SMOOTH_OFF)
|
||||
# Type
|
||||
if text_type.upper() == "B":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL_OFF)
|
||||
elif text_type.upper() == "U":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL_ON)
|
||||
elif text_type.upper() == "U2":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL2_ON)
|
||||
elif text_type.upper() == "BU":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL_ON)
|
||||
elif text_type.upper() == "BU2":
|
||||
self._raw(TXT_BOLD_ON)
|
||||
self._raw(TXT_UNDERL2_ON)
|
||||
elif text_type.upper() == "NORMAL":
|
||||
self._raw(TXT_BOLD_OFF)
|
||||
self._raw(TXT_UNDERL_OFF)
|
||||
# Font
|
||||
if font.upper() == "B":
|
||||
self._raw(TXT_FONT_B)
|
||||
else: # DEFAULT FONT: A
|
||||
self._raw(TXT_FONT_A)
|
||||
# Align
|
||||
if align.upper() == "CENTER":
|
||||
self._raw(TXT_ALIGN_CT)
|
||||
elif align.upper() == "RIGHT":
|
||||
self._raw(TXT_ALIGN_RT)
|
||||
elif align.upper() == "LEFT":
|
||||
self._raw(TXT_ALIGN_LT)
|
||||
# Density
|
||||
if density == 0:
|
||||
self._raw(PD_N50)
|
||||
elif density == 1:
|
||||
self._raw(PD_N37)
|
||||
elif density == 2:
|
||||
self._raw(PD_N25)
|
||||
elif density == 3:
|
||||
self._raw(PD_N12)
|
||||
elif density == 4:
|
||||
self._raw(PD_0)
|
||||
elif density == 5:
|
||||
self._raw(PD_P12)
|
||||
elif density == 6:
|
||||
self._raw(PD_P25)
|
||||
elif density == 7:
|
||||
self._raw(PD_P37)
|
||||
elif density == 8:
|
||||
self._raw(PD_P50)
|
||||
else: # DEFAULT: DOES NOTHING
|
||||
pass
|
||||
# Invert Printing
|
||||
if invert:
|
||||
self._raw(TXT_INVERT_ON)
|
||||
else:
|
||||
self._raw(TXT_INVERT_OFF)
|
||||
|
||||
def cut(self, mode=''):
|
||||
""" Cut paper.
|
||||
|
||||
Without any arguments the paper will be cut completely. With 'mode=PART' a partial cut will
|
||||
be attempted. Note however, that not all models can do a partial cut. See the documentation of
|
||||
your printer for details.
|
||||
|
||||
.. todo:: Check this function on TM-T88II.
|
||||
|
||||
:param mode: set to 'PART' for a partial cut
|
||||
"""
|
||||
# Fix the size between last line and cut
|
||||
# TODO: handle this with a line feed
|
||||
self._raw(b"\n\n\n\n\n\n")
|
||||
if mode.upper() == "PART":
|
||||
self._raw(PAPER_PART_CUT)
|
||||
else: # DEFAULT MODE: FULL CUT
|
||||
self._raw(PAPER_FULL_CUT)
|
||||
|
||||
def cashdraw(self, pin):
|
||||
""" Send pulse to kick the cash drawer
|
||||
|
||||
Kick cash drawer on pin 2 or pin 5 according to parameter.
|
||||
|
||||
:param pin: pin number, 2 or 5
|
||||
:raises: :py:exc:`~escpos.exceptions.CashDrawerError`
|
||||
"""
|
||||
if pin == 2:
|
||||
self._raw(CD_KICK_2)
|
||||
elif pin == 5:
|
||||
self._raw(CD_KICK_5)
|
||||
else:
|
||||
raise CashDrawerError()
|
||||
|
||||
def hw(self, hw):
|
||||
""" Hardware operations
|
||||
|
||||
:param hw: hardware action, may be:
|
||||
|
||||
* INIT
|
||||
* SELECT
|
||||
* RESET
|
||||
"""
|
||||
if hw.upper() == "INIT":
|
||||
self._raw(HW_INIT)
|
||||
elif hw.upper() == "SELECT":
|
||||
self._raw(HW_SELECT)
|
||||
elif hw.upper() == "RESET":
|
||||
self._raw(HW_RESET)
|
||||
else: # DEFAULT: DOES NOTHING
|
||||
pass
|
||||
|
||||
def control(self, ctl, pos=4):
|
||||
""" Feed control sequences
|
||||
|
||||
:param ctl: string for the following control sequences:
|
||||
|
||||
* LF *for Line Feed*
|
||||
* FF *for Form Feed*
|
||||
* CR *for Carriage Return*
|
||||
* HT *for Horizontal Tab*
|
||||
* VT *for Vertical Tab*
|
||||
|
||||
:param pos: integer between 1 and 16, controls the horizontal tab position
|
||||
:raises: :py:exc:`~escpos.exceptions.TabPosError`
|
||||
"""
|
||||
# Set tab positions
|
||||
if not (1 <= pos <= 16):
|
||||
raise TabPosError()
|
||||
else:
|
||||
self._raw(CTL_SET_HT + six.int2byte(pos))
|
||||
# Set position
|
||||
if ctl.upper() == "LF":
|
||||
self._raw(CTL_LF)
|
||||
elif ctl.upper() == "FF":
|
||||
self._raw(CTL_FF)
|
||||
elif ctl.upper() == "CR":
|
||||
self._raw(CTL_CR)
|
||||
elif ctl.upper() == "HT":
|
||||
self._raw(CTL_HT)
|
||||
elif ctl.upper() == "VT":
|
||||
self._raw(CTL_VT)
|
||||
|
||||
def panel_buttons(self, enable=True):
|
||||
""" Controls the panel buttons on the printer (e.g. FEED)
|
||||
|
||||
When enable is set to False the panel buttons on the printer will be disabled. Calling the method with
|
||||
enable=True or without argument will enable the panel buttons.
|
||||
|
||||
If panel buttons are enabled, the function of the panel button, such as feeding, will be executed upon pressing
|
||||
the button. If the panel buttons are disabled, pressing them will not have any effect.
|
||||
|
||||
This command is effective until the printer is initialized, reset or power-cycled. The default is enabled panel
|
||||
buttons.
|
||||
|
||||
Some panel buttons will always work, especially when printer is opened. See for more information the manual
|
||||
of your printer and the escpos-command-reference.
|
||||
|
||||
:param enable: controls the panel buttons
|
||||
:rtype: None
|
||||
"""
|
||||
if enable:
|
||||
self._raw(PANEL_BUTTON_ON)
|
||||
else:
|
||||
self._raw(PANEL_BUTTON_OFF)
|
||||
|
||||
|
||||
class EscposIO(object):
|
||||
"""ESC/POS Printer IO object
|
||||
|
||||
Allows the class to be used together with the `with`-statement. You have to define a printer instance
|
||||
and assign it to the EsposIO-class.
|
||||
This example explains the usage:
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
with EscposIO(printer.Serial('/dev/ttyUSB0')) as p:
|
||||
p.set(font='a', height=2, align='center', text_type='bold')
|
||||
p.printer.set(align='left')
|
||||
p.printer.image('logo.gif')
|
||||
p.writelines('Big line\\n', font='b')
|
||||
p.writelines('Привет')
|
||||
p.writelines('BIG TEXT', width=2)
|
||||
|
||||
After the `with`-statement the printer automatically cuts the paper if `autocut` is `True`.
|
||||
"""
|
||||
|
||||
def __init__(self, printer, autocut=True, autoclose=True, **kwargs):
|
||||
"""
|
||||
:param printer: An EscPos-printer object
|
||||
:type printer: escpos.Escpos
|
||||
:param autocut: If True, paper is automatically cut after the `with`-statement *default*: True
|
||||
:param kwargs: These arguments will be passed to :py:meth:`escpos.Escpos.set()`
|
||||
"""
|
||||
self.printer = printer
|
||||
self.params = kwargs
|
||||
self.autocut = autocut
|
||||
self.autoclose = autoclose
|
||||
|
||||
def set(self, **kwargs):
|
||||
""" Set the printer-parameters
|
||||
|
||||
Controls which parameters will be passed to :py:meth:`Escpos.set() <escpos.escpos.Escpos.set()>`.
|
||||
For more information on the parameters see the :py:meth:`set() <escpos.escpos.Escpos.set()>`-methods
|
||||
documentation. These parameters can also be passed with this class' constructor or the
|
||||
:py:meth:`~escpos.escpos.EscposIO.writelines()`-method.
|
||||
|
||||
:param kwargs: keyword-parameters that will be passed to :py:meth:`Escpos.set() <escpos.escpos.Escpos.set()>`
|
||||
"""
|
||||
self.params.update(kwargs)
|
||||
|
||||
def writelines(self, text, **kwargs):
|
||||
params = dict(self.params)
|
||||
params.update(kwargs)
|
||||
|
||||
if isinstance(text, six.text_type):
|
||||
lines = text.split('\n')
|
||||
elif isinstance(text, list) or isinstance(text, tuple):
|
||||
lines = text
|
||||
else:
|
||||
lines = ["{0}".format(text), ]
|
||||
|
||||
# TODO check unicode handling
|
||||
# TODO flush? or on print? (this should prob rather be handled by the _raw-method)
|
||||
for line in lines:
|
||||
self.printer.set(**params)
|
||||
if isinstance(text, six.text_type):
|
||||
self.printer.text(u"{0}\n".format(line))
|
||||
else:
|
||||
self.printer.text("{0}\n".format(line))
|
||||
|
||||
def close(self):
|
||||
""" called upon closing the `with`-statement
|
||||
"""
|
||||
self.printer.close()
|
||||
|
||||
def __enter__(self, **kwargs):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
"""
|
||||
|
||||
If :py:attr:`autocut <escpos.escpos.EscposIO.autocut>` is `True` (set by this class' constructor),
|
||||
then :py:meth:`printer.cut() <escpos.escpos.Escpos.cut()>` will be called here.
|
||||
"""
|
||||
if not (type is not None and issubclass(type, Exception)):
|
||||
if self.autocut:
|
||||
self.printer.cut()
|
||||
|
||||
if self.autoclose:
|
||||
self.close()
|
241
src/escpos/exceptions.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" ESC/POS Exceptions classes
|
||||
|
||||
Result/Exit codes:
|
||||
|
||||
- `0` = success
|
||||
- `10` = No Barcode type defined :py:exc:`~escpos.exceptions.BarcodeTypeError`
|
||||
- `20` = Barcode size values are out of range :py:exc:`~escpos.exceptions.BarcodeSizeError`
|
||||
- `30` = Barcode text not supplied :py:exc:`~escpos.exceptions.BarcodeCodeError`
|
||||
- `40` = Image height is too large :py:exc:`~escpos.exceptions.ImageSizeError`
|
||||
- `50` = No string supplied to be printed :py:exc:`~escpos.exceptions.TextError`
|
||||
- `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`
|
||||
- `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
|
||||
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2012 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
""" Base class for ESC/POS errors """
|
||||
def __init__(self, msg, status=None):
|
||||
Exception.__init__(self)
|
||||
self.msg = msg
|
||||
self.resultcode = 1
|
||||
if status is not None:
|
||||
self.resultcode = status
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class BarcodeTypeError(Error):
|
||||
""" No Barcode type defined.
|
||||
|
||||
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`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 10
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode type is defined ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class BarcodeSizeError(Error):
|
||||
""" Barcode size is out of range.
|
||||
|
||||
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 returncode is `20`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
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)
|
||||
|
||||
|
||||
class BarcodeCodeError(Error):
|
||||
""" No Barcode code was supplied.
|
||||
|
||||
No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode`.
|
||||
The returncode for this exception is `30`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 30
|
||||
|
||||
def __str__(self):
|
||||
return "No Barcode code was supplied"
|
||||
|
||||
|
||||
class ImageSizeError(Error):
|
||||
""" Image height is longer than 255px and can't be printed.
|
||||
|
||||
The returncode for this exception is `40`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
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"
|
||||
|
||||
|
||||
class TextError(Error):
|
||||
""" Text string must be supplied to the `text()` method.
|
||||
|
||||
This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`.
|
||||
The returncode for this exception is `50`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 50
|
||||
|
||||
def __str__(self):
|
||||
return "Text string must be supplied to the text() method"
|
||||
|
||||
|
||||
class CashDrawerError(Error):
|
||||
""" Valid pin must be set in order to send pulse.
|
||||
|
||||
A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`.
|
||||
The returncode for this exception is `60`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 60
|
||||
|
||||
def __str__(self):
|
||||
return "Valid pin must be set to send pulse"
|
||||
|
||||
|
||||
class TabPosError(Error):
|
||||
""" Valid tab positions must be in the range 0 to 16.
|
||||
|
||||
This exception is raised by :py:meth:`escpos.escpos.Escpos.control`.
|
||||
The returncode for this exception is `70`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
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"
|
||||
|
||||
|
||||
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`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 80
|
||||
|
||||
def __str__(self):
|
||||
return "Valid char code must be set"
|
||||
|
||||
|
||||
class USBNotFoundError(Error):
|
||||
""" Device wasn't found (probably not plugged in)
|
||||
|
||||
The USB device seems to be not plugged in.
|
||||
Ths returncode for this exception is `90`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 90
|
||||
|
||||
def __str__(self):
|
||||
return "USB device not found"
|
||||
|
||||
|
||||
class SetVariableError(Error):
|
||||
""" A set method variable was out of range
|
||||
|
||||
Check set variables against minimum and maximum values
|
||||
Ths returncode for this exception is `100`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 100
|
||||
|
||||
def __str__(self):
|
||||
return "Set variable out of range"
|
||||
|
||||
|
||||
# Configuration errors
|
||||
|
||||
class ConfigNotFoundError(Error):
|
||||
""" The configuration file was not found
|
||||
|
||||
The default or passed configuration file could not be read
|
||||
Ths returncode for this exception is `200`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 200
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration not found ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ConfigSyntaxError(Error):
|
||||
""" The configuration file is invalid
|
||||
|
||||
The syntax is incorrect
|
||||
Ths returncode for this exception is `210`.
|
||||
"""
|
||||
def __init__(self, msg=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 210
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration syntax is invalid ({msg})".format(msg=self.msg)
|
||||
|
||||
|
||||
class ConfigSectionMissingError(Error):
|
||||
""" The configuration file is missing a section
|
||||
|
||||
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=""):
|
||||
Error.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.resultcode = 220
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration section is missing ({msg})".format(msg=self.msg)
|
90
src/escpos/image.py
Normal file
@@ -0,0 +1,90 @@
|
||||
""" Image format handling class
|
||||
|
||||
This module contains the image format handler :py:class:`EscposImage`.
|
||||
|
||||
: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: GNU GPL v3
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
class EscposImage(object):
|
||||
"""
|
||||
Load images in, and output ESC/POS formats.
|
||||
|
||||
The class is designed to efficiently delegate image processing to
|
||||
PIL, rather than spend CPU cycles looping over pixels.
|
||||
"""
|
||||
|
||||
def __init__(self, img_source):
|
||||
"""
|
||||
Load in an image
|
||||
|
||||
:param img_source: PIL.Image, or filename to load one from.
|
||||
"""
|
||||
if isinstance(img_source, Image.Image):
|
||||
img_original = img_source
|
||||
else:
|
||||
img_original = Image.open(img_source)
|
||||
|
||||
# Convert to white RGB background, paste over white background
|
||||
# to strip alpha.
|
||||
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
|
||||
im = im.convert("L")
|
||||
# Invert: Only works on 'L' images
|
||||
im = ImageOps.invert(im)
|
||||
# Pure black and white
|
||||
self._im = im.convert("1")
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return (self.width + 7) >> 3
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""
|
||||
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.
|
||||
|
||||
:param high_density_vertical: Printed line height in dots
|
||||
"""
|
||||
im = self._im.transpose(Image.ROTATE_270).transpose(Image.FLIP_LEFT_RIGHT)
|
||||
line_height = 24 if high_density_vertical else 8
|
||||
width_pixels, height_pixels = im.size
|
||||
top = 0
|
||||
left = 0
|
||||
while left < width_pixels:
|
||||
box = (left, top, left + line_height, top + height_pixels)
|
||||
im_slice = im.transform((line_height, height_pixels), Image.EXTENT, box)
|
||||
im_bytes = im_slice.tobytes()
|
||||
yield(im_bytes)
|
||||
left += line_height
|
||||
|
||||
def to_raster_format(self):
|
||||
"""
|
||||
Convert image to raster-format binary
|
||||
"""
|
||||
return self._im.tobytes()
|
300
src/escpos/printer.py
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/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 Bashlinux
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
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, interface=0, in_ep=0x82, out_ep=0x01, *args, **kwargs):
|
||||
"""
|
||||
:param idVendor: Vendor ID
|
||||
:param idProduct: Product ID
|
||||
:param interface: USB device interface
|
||||
:param in_ep: Input end point
|
||||
:param out_ep: Output end point
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.idVendor = idVendor
|
||||
self.idProduct = idProduct
|
||||
self.interface = interface
|
||||
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.interface)
|
||||
|
||||
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 """
|
||||
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:
|
||||
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 """
|
||||
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", *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param devfile : Device file under dev filesystem
|
||||
"""
|
||||
Escpos.__init__(self, *args, **kwargs)
|
||||
self.devfile = devfile
|
||||
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)
|
||||
|
||||
def close(self):
|
||||
""" Close system file """
|
||||
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
|
1
test/Dies ist ein Test.LF.txt
Normal file
@@ -0,0 +1 @@
|
||||
Dies ist ein Test.
|
BIN
test/resources/black_transparent.gif
Normal file
After Width: | Height: | Size: 65 B |
BIN
test/resources/black_transparent.png
Normal file
After Width: | Height: | Size: 167 B |
BIN
test/resources/black_white.gif
Normal file
After Width: | Height: | Size: 65 B |
BIN
test/resources/black_white.jpg
Normal file
After Width: | Height: | Size: 175 B |
BIN
test/resources/black_white.png
Normal file
After Width: | Height: | Size: 156 B |
BIN
test/resources/canvas_black.gif
Normal file
After Width: | Height: | Size: 72 B |
BIN
test/resources/canvas_black.jpg
Normal file
After Width: | Height: | Size: 160 B |
BIN
test/resources/canvas_black.png
Normal file
After Width: | Height: | Size: 239 B |
BIN
test/resources/canvas_white.gif
Normal file
After Width: | Height: | Size: 72 B |
BIN
test/resources/canvas_white.jpg
Normal file
After Width: | Height: | Size: 160 B |
BIN
test/resources/canvas_white.png
Normal file
After Width: | Height: | Size: 239 B |
30
test/test_abstract_base_class.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/python
|
||||
"""verifies that the metaclass abc is properly used by Escpos
|
||||
|
||||
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 Patrick Kanzler
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
|
||||
def test_abstract_base_class():
|
||||
""" test whether Escpos has the metaclass ABCMeta """
|
||||
assert issubclass(escpos.Escpos, object)
|
||||
assert type(escpos.Escpos) is ABCMeta
|
116
test/test_cli.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Test for the CLI
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
TEST_DIR = os.path.abspath('test/test-cli-output')
|
||||
|
||||
DEVFILE_NAME = 'testfile'
|
||||
|
||||
DEVFILE = os.path.join(TEST_DIR, DEVFILE_NAME)
|
||||
CONFIGFILE = 'testconfig.yaml'
|
||||
CONFIG_YAML = '''
|
||||
---
|
||||
|
||||
printer:
|
||||
type: file
|
||||
devfile: {testfile}
|
||||
'''.format(
|
||||
testfile=DEVFILE,
|
||||
)
|
||||
|
||||
|
||||
class TestCLI:
|
||||
""" Contains setups, teardowns, and tests for CLI
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
""" Initalize the tests.
|
||||
Just define some vars here since most of them get set during
|
||||
setup_class and teardown_class
|
||||
"""
|
||||
self.env = None
|
||||
self.default_args = None
|
||||
|
||||
@staticmethod
|
||||
def setup_class():
|
||||
""" Create a config file to read from """
|
||||
with open(CONFIGFILE, 'w') as config:
|
||||
config.write(CONFIG_YAML)
|
||||
|
||||
@staticmethod
|
||||
def teardown_class():
|
||||
""" Remove config file """
|
||||
os.remove(CONFIGFILE)
|
||||
|
||||
def setup(self):
|
||||
""" Create a file to print to and set up env"""
|
||||
self.env = TestFileEnvironment(
|
||||
base_path=TEST_DIR,
|
||||
cwd=os.getcwd(),
|
||||
)
|
||||
|
||||
self.default_args = (
|
||||
sys.executable,
|
||||
'-mescpos.cli',
|
||||
'-c',
|
||||
CONFIGFILE,
|
||||
)
|
||||
|
||||
fhandle = open(DEVFILE, 'a')
|
||||
try:
|
||||
os.utime(DEVFILE, None)
|
||||
finally:
|
||||
fhandle.close()
|
||||
|
||||
def teardown(self):
|
||||
""" Destroy printer file and env """
|
||||
os.remove(DEVFILE)
|
||||
self.env.clear()
|
||||
|
||||
def test_cli_help(self):
|
||||
""" Test getting help from cli """
|
||||
result = self.env.run(sys.executable, '-mescpos.cli', '-h')
|
||||
assert not result.stderr
|
||||
assert 'usage' in result.stdout
|
||||
|
||||
def test_cli_text(self):
|
||||
""" Make sure text returns what we sent it """
|
||||
test_text = 'this is some text'
|
||||
result = self.env.run(
|
||||
*(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'
|
||||
)
|
||||
|
||||
def test_cli_text_inavlid_args(self):
|
||||
""" Test a failure to send valid arguments """
|
||||
result = self.env.run(
|
||||
*(self.default_args + (
|
||||
'text',
|
||||
'--invalid-param',
|
||||
'some data'
|
||||
)),
|
||||
expect_error=True,
|
||||
expect_stderr=True
|
||||
)
|
||||
assert_equals(result.returncode, 2)
|
||||
assert 'error:' in result.stderr
|
||||
assert not result.files_updated
|
132
test/test_function_image.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/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: GNU GPL v3
|
||||
"""
|
||||
|
||||
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 PIL import Image
|
||||
|
||||
|
||||
# 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')
|
54
test/test_function_panel_button.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/python
|
||||
"""tests for panel button function
|
||||
|
||||
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from nose.tools import with_setup
|
||||
|
||||
import escpos.printer as printer
|
||||
import os
|
||||
|
||||
devfile = 'testfile'
|
||||
|
||||
|
||||
def setup_testfile():
|
||||
"""create a testfile as devfile"""
|
||||
fhandle = open(devfile, 'a')
|
||||
try:
|
||||
os.utime(devfile, None)
|
||||
finally:
|
||||
fhandle.close()
|
||||
|
||||
|
||||
def teardown_testfile():
|
||||
"""destroy testfile again"""
|
||||
os.remove(devfile)
|
||||
|
||||
|
||||
@with_setup(setup_testfile, teardown_testfile)
|
||||
def test_function_panel_button_on():
|
||||
"""test the panel button function (enabling) by comparing output"""
|
||||
instance = printer.File(devfile=devfile)
|
||||
instance.panel_buttons()
|
||||
instance.flush()
|
||||
with open(devfile, "rb") as f:
|
||||
assert(f.read() == b'\x1B\x63\x35\x00')
|
||||
|
||||
|
||||
@with_setup(setup_testfile, teardown_testfile)
|
||||
def test_function_panel_button_off():
|
||||
"""test the panel button function (disabling) by comparing output"""
|
||||
instance = printer.File(devfile=devfile)
|
||||
instance.panel_buttons(False)
|
||||
instance.flush()
|
||||
with open(devfile, "rb") as f:
|
||||
assert(f.read() == b'\x1B\x63\x35\x01')
|
99
test/test_function_qr_native.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/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: GNU GPL v3
|
||||
"""
|
||||
|
||||
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'\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'
|
||||
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)
|
45
test/test_function_text.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/python
|
||||
"""tests for the text printing function
|
||||
|
||||
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from nose.tools import with_setup
|
||||
|
||||
import escpos.printer as printer
|
||||
import os
|
||||
|
||||
import filecmp
|
||||
|
||||
devfile = 'testfile'
|
||||
|
||||
|
||||
def setup_testfile():
|
||||
"""create a testfile as devfile"""
|
||||
fhandle = open(devfile, 'a')
|
||||
try:
|
||||
os.utime(devfile, None)
|
||||
finally:
|
||||
fhandle.close()
|
||||
|
||||
|
||||
def teardown_testfile():
|
||||
"""destroy testfile again"""
|
||||
os.remove(devfile)
|
||||
|
||||
|
||||
@with_setup(setup_testfile, teardown_testfile)
|
||||
def test_function_text_dies_ist_ein_test_lf():
|
||||
"""test the text printing function with simple string and compare output"""
|
||||
instance = printer.File(devfile=devfile)
|
||||
instance.text('Dies ist ein Test.\n')
|
||||
instance.flush()
|
||||
assert(filecmp.cmp('test/Dies ist ein Test.LF.txt', devfile))
|
57
test/test_image.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
""" Image tests- Check that images from different source formats are correctly
|
||||
converted to ESC/POS column & raster formats.
|
||||
|
||||
: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: GNU GPL v3
|
||||
"""
|
||||
|
||||
from escpos.image import EscposImage
|
||||
|
||||
|
||||
def test_image_black():
|
||||
"""
|
||||
Test rendering solid black image
|
||||
"""
|
||||
for img_format in ['png', 'jpg', 'gif']:
|
||||
_load_and_check_img('canvas_black.' + img_format, 1, 1, b'\x80', [b'\x80'])
|
||||
|
||||
|
||||
def test_image_black_transparent():
|
||||
"""
|
||||
Test rendering black/transparent image
|
||||
"""
|
||||
for img_format in ['png', 'gif']:
|
||||
_load_and_check_img('black_transparent.' + img_format, 2, 2, b'\xc0\x00', [b'\x80\x80'])
|
||||
|
||||
|
||||
def test_image_black_white():
|
||||
"""
|
||||
Test rendering black/white image
|
||||
"""
|
||||
for img_format in ['png', 'jpg', 'gif']:
|
||||
_load_and_check_img('black_white.' + img_format, 2, 2, b'\xc0\x00', [b'\x80\x80'])
|
||||
|
||||
|
||||
def test_image_white():
|
||||
"""
|
||||
Test rendering solid white image
|
||||
"""
|
||||
for img_format in ['png', 'jpg', 'gif']:
|
||||
_load_and_check_img('canvas_white.' + img_format, 1, 1, b'\x00', [b'\x00'])
|
||||
|
||||
|
||||
def _load_and_check_img(filename, width_expected, height_expected, raster_format_expected, column_format_expected):
|
||||
"""
|
||||
Load an image, and test whether raster & column formatted output, sizes, etc match expectations.
|
||||
"""
|
||||
im = EscposImage('test/resources/' + filename)
|
||||
assert(im.width == width_expected)
|
||||
assert(im.height == height_expected)
|
||||
assert(im.to_raster_format() == raster_format_expected)
|
||||
i = 0
|
||||
for row in im.to_column_format(False):
|
||||
assert(row == column_format_expected[i])
|
||||
i += 1
|
41
test/test_load_module.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/python
|
||||
"""very basic test cases that load the classes
|
||||
|
||||
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from nose.tools import with_setup
|
||||
|
||||
import escpos.printer as printer
|
||||
import os
|
||||
|
||||
devfile = 'testfile'
|
||||
|
||||
|
||||
def setup_testfile():
|
||||
"""create a testfile as devfile"""
|
||||
fhandle = open(devfile, 'a')
|
||||
try:
|
||||
os.utime(devfile, None)
|
||||
finally:
|
||||
fhandle.close()
|
||||
|
||||
|
||||
def teardown_testfile():
|
||||
"""destroy testfile again"""
|
||||
os.remove(devfile)
|
||||
|
||||
|
||||
@with_setup(setup_testfile, teardown_testfile)
|
||||
def test_instantiation():
|
||||
"""test the instantiation of a escpos-printer class and basic printing"""
|
||||
instance = printer.File(devfile=devfile)
|
||||
instance.text('This is a test\n')
|
24
test/test_with_statement.py
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/python
|
||||
"""test the facility which enables usage of the with-statement
|
||||
|
||||
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
|
||||
:organization: `python-escpos <https://github.com/python-escpos>`_
|
||||
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
|
||||
:license: GNU GPL v3
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import escpos.printer as printer
|
||||
import escpos.escpos as escpos
|
||||
|
||||
|
||||
def test_with_statement():
|
||||
"""Use with statement"""
|
||||
dummy_printer = printer.Dummy()
|
||||
with escpos.EscposIO(dummy_printer) as p:
|
||||
p.writelines('Some text.\n')
|
||||
# TODO extend these tests as they don't really do anything at the moment
|
15
tox.ini
Normal file
@@ -0,0 +1,15 @@
|
||||
[tox]
|
||||
envlist = py27, py34, py35, docs
|
||||
|
||||
[testenv]
|
||||
deps = nose
|
||||
coverage
|
||||
scripttest
|
||||
commands = nosetests --with-coverage --cover-erase --cover-branches
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python
|
||||
changedir = doc
|
||||
deps = sphinx
|
||||
setuptools_scm
|
||||
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
|