Repository: 7sDream/pyqart Branch: master Commit: c40c693cdedb Files: 52 Total size: 111.5 KB Directory structure: gitextract_kd70hrty/ ├── .gitignore ├── LICENSE ├── README.md ├── README.zh.md ├── pyqart/ │ ├── __init__.py │ ├── art/ │ │ ├── __init__.py │ │ ├── bitblock.py │ │ ├── exception.py │ │ ├── qart.py │ │ ├── source.py │ │ └── target.py │ ├── common/ │ │ ├── __init__.py │ │ ├── bit_funcs.py │ │ ├── bits.py │ │ └── exception.py │ ├── qart_entry.py │ ├── qr/ │ │ ├── __init__.py │ │ ├── args/ │ │ │ ├── __init__.py │ │ │ ├── args.py │ │ │ ├── mask.py │ │ │ ├── rotation.py │ │ │ └── version.py │ │ ├── data/ │ │ │ ├── __init__.py │ │ │ ├── alphanumeric.py │ │ │ ├── base.py │ │ │ ├── data.py │ │ │ ├── exception.py │ │ │ ├── numbers.py │ │ │ └── raw.py │ │ ├── ec/ │ │ │ ├── __init__.py │ │ │ ├── gf.py │ │ │ ├── poly.py │ │ │ └── rsencoder.py │ │ ├── exception.py │ │ ├── painter/ │ │ │ ├── __init__.py │ │ │ ├── canvas.py │ │ │ ├── exception.py │ │ │ ├── painter.py │ │ │ └── point.py │ │ └── printer/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── halftone_printer.py │ │ ├── image_printer.py │ │ └── string_printer.py │ └── qr_entry.py ├── setup.py └── test/ ├── __init__.py ├── test_alphanumeric.py ├── test_bits.py ├── test_bits_utils.py ├── test_encode_numbers.py └── test_encode_raw.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Pycharm files .idea/ # personal test file test.py example.jpg # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 7sDream Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # PyQArt - QArt Python implementation [中文版 README](https://github.com/7sDream/pyqart/blob/master/README.zh.md) ## introduction QArt is a method of combining QrCode of an URL with any image, which was submitted in [an article][qart-article] writen by [Russ Cox][russ-cos-google-plus] on his personal website. An Example(come from the article): ![QArt Example][qart-example] This repo is Python implementation of it. ## Install ``` pip install pyqart ``` **Note: Support Python3 only, please make sure you are using pip of Python3.** ## Usage For code reuse, I split the lib to two part. One for generate normal QrCode, another for generate QArt. ### The Qr Part Use `pyqr` CLI to create normal QrCode. ``` pyqr -p 5 -c 102 204 255 "Hello World!" -o qr.png ``` The options: - `-p` for point size of QrQCode, by pixel, default is 3 pixel. - `-c` for color of point, default is black. Background color can be set with `-g` option, default is white. ![qr code: hello world][my-qr-img] If you want show it in terminal, just don't provide `-o` option: ```bash pyqr "Hello World" ``` Then you will see: ![qr in terminal: hello world][my-qr-terminal] The actual result you will see depends on your font setting, I'm using Dejavu Sans Mono. Yes, it is only useful for small QrCode. Run `pyqr -h` for more options and their effect. ### The Art Part Use `pyqart` CLI to create QArt. It may take a long time, please be patient :) This is an example that mix my blog url and my Github avatar: ``` pyqart -v 8 -c 102 204 255 "http://0v0.link/" photo.jpg -o qart.png ``` My Github avatar: ![][my-github-avatar] The QArt Code: ![][my-qart-img] Not meet your expectations? Try `-n` option to pick point at random(default is pick low-contrast region pixels first): ```bash pyqart -n -c 102 204 255 -v 8 "http://0v0.link/" photo.jpg -o qart-n.png ``` ![][my-qart-n-img] Still not satisfied? Use `-y` option to enhance the accuracy of the central region by giving up the control of the edge pixels: ![][my-qart-y-img] `-y` and `-n` can be used at the same time, but no obvious improvement. **Note: because that `-y` option will only use data block, ignore error correction block,it reduce many many many calculate. It has about 30x to 100x speed up compare with no `-y` option case. So I strongly recommend using `-y` option whenever you needn't make a full picture fitting.** Use `-r` option to set rotation degree, The controllable data region can be changed into a horizontal area, it will make it easier to process very wide picture. ![][my-pyqart-y-r-img] Run `pyqart -h` for more options and their effect. ### Use it in your codes as a module Documentation is in preparation. ## Gallery ![][python-qr] python.org(used -d option, means dithering, see help message for more info.) ![][github-qr] github.com ![][bilibili-qr] bilibili.com (An ACG videos website) ## Halftone and HalfArt support Halftone support added in version 0.1.0, and I made another new method which combined Halftone and QArt, so I call it HalfArt temporarily. ## Arguments for all methods The following code shows arguments to get output image of all kind of method: ```python from pyqart import QArtist, QrHalftonePrinter, QrImagePrinter, QrPainter QR_VERSION = 10 POINT_PIXEL = 3 artist = QArtist('http://www.nankai.edu.cn/', 'example.jpg', QR_VERSION) painter = QrPainter('http://www.nankai.edu.cn/', QR_VERSION) artist_data_only = QArtist('http://www.nankai.edu.cn/', 'example.jpg', QR_VERSION, only_data=True) # normal QrImagePrinter.print(painter, path='normal.png', point_width=POINT_PIXEL) # Halftone QrHalftonePrinter.print(painter, path='halftone.png', img='example.jpg', point_width=POINT_PIXEL, colorful=False) # Halftone colorful QrHalftonePrinter.print(painter, path='halftone-color.png', img='example.jpg', point_width=POINT_PIXEL) # Halftone pixel QrHalftonePrinter.print(painter, path='halftone-pixel.png', img='example.jpg', point_width=POINT_PIXEL, colorful=False, pixelization=True) # QArt QrImagePrinter.print(artist, path='qart.png', point_width=POINT_PIXEL) # QArt data only QrImagePrinter.print(artist_data_only, path='qart-data-only.png', point_width=POINT_PIXEL) # HalfArt QrHalftonePrinter.print(artist, path='halfart.png', point_width=POINT_PIXEL) # HalfArt data only QrHalftonePrinter.print(artist_data_only, path='halfart-data-only.png', point_width=POINT_PIXEL) ``` ### Result example for all method | | | | | :-: | :-: | :-: | | ![][halftone.png]| ![][halftone-color.png] | ![][halftone-pixel.png] | | Halftone | Halftone colorful | Halftone pixel | | ![][qart.png] | ![][qart-data-only.png] | | | QArt | QArt data only | | | ![][halfart.png] | ![][halfart-data-only.png] | | | HalfArt | HalfArt data only | | ## TODO - [x] Make QrPainter decided argument by itself. - [x] Art part - [x] CLI - [x] Package - [x] Halftone support - [x] self-made HalfArt method - [ ] GUI - [ ] Use Cython to accelerate Reed-Solomon error correction - [ ] Docs - [ ] Tests ## Other Implementation - Golang: [qr][qr] by [Russ Cox][russ-cos-google-plus] - Java: [qart4j][qart4j] by [dieforfree][dieforfree] ## Acknowledgements - All credit goes to [Russ Cos][russ-cos-google-plus], Thanks for his article and implement. - Thanks for [qart4j project][qart4j] by [dieforfree][dieforfree],which helps me so much on how to implement the art part. - Thanks to a series of articles named [QR Code Tutorial][tutorial] in thonky.com, It's very detailed. Whenever I faced problem about encoding or error correction, I will go to it for help. - Thanks to the Python programing language。 ## LICENSE MIT. See LICENSE. [russ-cos-google-plus]: https://plus.google.com/+RussCox-rsc [qart-article]: http://research.swtch.com/qart [qart-example]: http://ww4.sinaimg.cn/large/88e401f0gw1f6dl845naoj205g05ga9y.jpg [my-qr-img]: http://ww3.sinaimg.cn/large/88e401f0gw1f6ir3ifivzj20370370ss.jpg [my-qr-terminal]: http://ww2.sinaimg.cn/large/88e401f0gw1f6ir4taf7hj209008c3ze.jpg [my-github-avatar]: http://ww3.sinaimg.cn/large/88e401f0gw1f6iyj9nuwhj2049049q2v.jpg [my-qart-img]: http://ww3.sinaimg.cn/large/88e401f0gw1f6ir8t0mbej20490490t2.jpg [my-qart-n-img]: http://ww1.sinaimg.cn/large/88e401f0gw1f6irh15ouuj2049049mxp.jpg [my-qart-y-img]: http://ww2.sinaimg.cn/large/88e401f0gw1f6irbnfjozj20490490t4.jpg [my-pyqart-y-r-img]: http://ww3.sinaimg.cn/large/88e401f0gw1f6jd7w10r7j205l05lt91.jpg [qr]: https://code.google.com/p/rsc/source/browse/qr [dieforfree]: https://github.com/dieforfree [qart4j]: https://github.com/dieforfree/qart4j [tutorial]: http://www.thonky.com/qr-code-tutorial/ [python-qr]: http://ww1.sinaimg.cn/large/88e401f0gw1f6iz81tkwpj204x04xaaf.jpg [github-qr]: http://ww4.sinaimg.cn/large/88e401f0gw1f6izdtv2kqj204x04x0sy.jpg [bilibili-qr]: http://ww3.sinaimg.cn/large/88e401f0gw1f6j0ds93k9j204x04x74m.jpg [halftone.png]: http://rikka-10066868.image.myqcloud.com/f62cbc2f-1e38-4a94-80aa-0be1a0c32b55.png [halftone-color.png]: http://rikka-10066868.image.myqcloud.com/d96d057a-42d2-469b-9b65-0eabd2bd915f.png [halftone-pixel.png]: http://rikka-10066868.image.myqcloud.com/00da6fa8-5035-4ba6-8c33-584b54e73e2d.png [qart.png]: http://rikka-10066868.image.myqcloud.com/d2f3febb-a535-4154-8ebc-80183701c47d.png [qart-data-only.png]: http://rikka-10066868.image.myqcloud.com/59834cea-5d44-41c3-b759-780c56c9789b.png [halfart.png]: http://rikka-10066868.image.myqcloud.com/8b0847b9-c3fc-451d-b554-7bdc3a53f7e9.png [halfart-data-only.png]: http://rikka-10066868.image.myqcloud.com/9f4fd92e-99ff-4aca-a252-b6c1ab709e65.png ================================================ FILE: README.zh.md ================================================ # PyQArt - QArt 的 Python 实现 [Readme in English](https://github.com/7sDream/pyqart/blob/master/README.md) ## 简介 QArt 是由 [Russ Cox][russ-cos-google-plus] 在他个人网站的[一篇文章][qart-article]中提出的一种将包含 URL 的二维码与图像结合的方法。 示例图片(来源于 Russ Cox 的文章): ![QArt Example][qart-example] 这个库是 QArt 的 Python 实现版本。 ## 安装 ```bash pip install pyqart ``` **注:只支持 Python3,请确认你使用的是 python3 版本的 pip。** ## 使用 为便于重用,我将库分成了两部分,一部分是普通二维码生成,另一部分则将 URL 二维码与图像结合。 ### Qr 部分 使用 `pyqr` 命令行程序,可以创建普通二维码。 ``` pyqr -p 5 -c 102 204 255 "Hello World!" -o qr.png ``` 其中: - `-p` 参数指定生成的二维码图片中,每个填充点的大小,默认是 3 像素。 - `-c` 参数指定填充点的颜色,默认是黑色。背景色默认为白色,可以用 `-g` 参数设定。 ![qr code: hello world][my-qr-img] 如果你想在终端里查看的话,不提供 `-o` 参数即可: ```bash pyqr "Hello World" ``` 输出如下: ![qr in terminal: hello world][my-qr-terminal] 显示效果和你终端的字体有关,我的字体是 Dejavu Sans Mono. 当然,终端只能用于显示比较小的二维码。 有关二维码生成的更多参数和它们的作用请使用 `pyqr -h` 命令查看。 ### Art 部分 使用 `pyqart` 命令行程序创建艺术二维码。(所需时间可能较长,请耐心等待) 使用我的博客网址和 Github 头像来做例子,`-v` 参数指定二维码的大小。 ``` pyqart -v 8 -c 102 204 255 "http://0v0.link/" photo.jpg -o qart.png ``` 这是我的 Github 头像: ![][my-github-avatar] 生成的二维码如下,扫描一下就会跳转到我的博客啦: ![][my-qart-img] 可能效果不太好,试试使用 `-n` 参数来随机选取像素点(默认情况下会先处理大片相同颜色的区域): ```bash pyqart -n -c 102 204 255 -v 8 "http://0v0.link/" photo.jpg -o qart-n.png ``` ![][my-qart-n-img] 可能还是不太好?再试试 `-y` 参数,它通过放弃边缘区域来加强中间区域的逼近效果: ```bash pyqart -y -c 102 204 255 -v 8 "http://0v0.link/" photo.jpg -o qart-y.png ``` ![][my-qart-y-img] `-y` 和 `-n` 参数也可以结合起来使用,不过提升不会很明显。 **注意: `-y` 参数由于只只使用数据块而不使用纠错块,减少了很多很多很多操作,相比没有 `-y` 参数大概有 30 到 100 倍的速度提升,强烈建议在不需要全图拟合时使用 `-y` 参数。** 另外,使用 `-r` 参数指定二维码的旋转角度,可以把可控制的数据区变为横向,方便扁长图形处理: ![][my-pyqart-y-r-img] 有关 QArt 生成的更多参数和它们的作用请使用 `pyqart -h` 命令查看。 ### 作为模块使用 文档正在编写中。 ## 更多示例 ![][python-qr] Python 官网。(此示例使用了 -d 参数,请查看帮助获取更多信息) ![][github-qr] Github 首页。 ![][bilibili-qr] 哔哩哔哩。 ## Halftone 和 HalfArt 支持 0.1.0 版本增加了 Halftone 支持,另外也实现了一种结合了 QArt 和 Halftone 的新算法,我暂时命名为 HalfArt。 ## 各方法所用参数 以下代码展示了输出各种格式的所需参数: ```python from pyqart import QArtist, QrHalftonePrinter, QrImagePrinter, QrPainter QR_VERSION = 10 POINT_PIXEL = 3 artist = QArtist('http://www.nankai.edu.cn/', 'example.jpg', QR_VERSION) painter = QrPainter('http://www.nankai.edu.cn/', QR_VERSION) artist_data_only = QArtist('http://www.nankai.edu.cn/', 'example.jpg', QR_VERSION, only_data=True) # normal QrImagePrinter.print(painter, path='normal.png', point_width=POINT_PIXEL) # Halftone QrHalftonePrinter.print(painter, path='halftone.png', img='example.jpg', point_width=POINT_PIXEL, colorful=False) # Halftone colorful QrHalftonePrinter.print(painter, path='halftone-color.png', img='example.jpg', point_width=POINT_PIXEL) # Halftone pixel QrHalftonePrinter.print(painter, path='halftone-pixel.png', img='example.jpg', point_width=POINT_PIXEL, colorful=False, pixelization=True) # QArt QrImagePrinter.print(artist, path='qart.png', point_width=POINT_PIXEL) # QArt data only QrImagePrinter.print(artist_data_only, path='qart-data-only.png', point_width=POINT_PIXEL) # HalfArt QrHalftonePrinter.print(artist, path='halfart.png', point_width=POINT_PIXEL) # HalfArt data only QrHalftonePrinter.print(artist_data_only, path='halfart-data-only.png', point_width=POINT_PIXEL) ``` ### 各方法结果样例 | | | | | :-: | :-: | :-: | | ![][halftone.png]| ![][halftone-color.png] | ![][halftone-pixel.png] | | Halftone | Halftone colorful | Halftone pixel | | ![][qart.png] | ![][qart-data-only.png] | | | QArt | QArt data only | | | ![][halfart.png] | ![][halfart-data-only.png] | | | HalfArt | HalfArt data only | | ## TODO - [x] 让 QrPainter 能自己决定参数 - [x] Art 部分 - [x] CLI - [x] 打包 - [x] Halftone 支持 - [x] 自制 HalfArt 方法 - [ ] GUI - [ ] 使用 Cython 加快里德所罗门码编码速度 - [ ] 文档 - [ ] 测试 ## 其他实现版本 - Golang: [qr][qr] by [Russ Cox][russ-cos-google-plus] - Java: [qart4j][qart4j] by [dieforfree][dieforfree] ## 致谢 - 所有一切都源自 [Russ Cos][russ-cos-google-plus] 的文章,感谢他。 - 感谢 [dieforfree][dieforfree] 的 [qart4j 项目][qart4j],它给我提供了很多如何实现 art 部分参考。 - 感谢 thonky.com 的 [二维码原理指导][tutorial] 系列文章,非常详细,关于编码和纠错中不懂的地方多亏了它。 - 感谢 Python。 ## 协议 MIT。 参见 LICENSE 文件。 [russ-cos-google-plus]: https://plus.google.com/+RussCox-rsc [qart-article]: http://research.swtch.com/qart [qart-example]: http://ww4.sinaimg.cn/large/88e401f0gw1f6dl845naoj205g05ga9y.jpg [my-qr-img]: http://ww3.sinaimg.cn/large/88e401f0gw1f6ir3ifivzj20370370ss.jpg [my-qr-terminal]: http://ww2.sinaimg.cn/large/88e401f0gw1f6ir4taf7hj209008c3ze.jpg [my-github-avatar]: http://ww3.sinaimg.cn/large/88e401f0gw1f6iyj9nuwhj2049049q2v.jpg [my-qart-img]: http://ww3.sinaimg.cn/large/88e401f0gw1f6ir8t0mbej20490490t2.jpg [my-qart-n-img]: http://ww1.sinaimg.cn/large/88e401f0gw1f6irh15ouuj2049049mxp.jpg [my-qart-y-img]: http://ww2.sinaimg.cn/large/88e401f0gw1f6irbnfjozj20490490t4.jpg [my-pyqart-y-r-img]: http://ww3.sinaimg.cn/large/88e401f0gw1f6jd7w10r7j205l05lt91.jpg [qr]: https://code.google.com/p/rsc/source/browse/qr [dieforfree]: https://github.com/dieforfree [qart4j]: https://github.com/dieforfree/qart4j [tutorial]: http://www.thonky.com/qr-code-tutorial/ [python-qr]: http://ww1.sinaimg.cn/large/88e401f0gw1f6iz81tkwpj204x04xaaf.jpg [github-qr]: http://ww4.sinaimg.cn/large/88e401f0gw1f6izdtv2kqj204x04x0sy.jpg [bilibili-qr]: http://ww3.sinaimg.cn/large/88e401f0gw1f6j0ds93k9j204x04x74m.jpg [halftone.png]: http://rikka-10066868.image.myqcloud.com/f62cbc2f-1e38-4a94-80aa-0be1a0c32b55.png [halftone-color.png]: http://rikka-10066868.image.myqcloud.com/d96d057a-42d2-469b-9b65-0eabd2bd915f.png [halftone-pixel.png]: http://rikka-10066868.image.myqcloud.com/00da6fa8-5035-4ba6-8c33-584b54e73e2d.png [qart.png]: http://rikka-10066868.image.myqcloud.com/d2f3febb-a535-4154-8ebc-80183701c47d.png [qart-data-only.png]: http://rikka-10066868.image.myqcloud.com/59834cea-5d44-41c3-b759-780c56c9789b.png [halfart.png]: http://rikka-10066868.image.myqcloud.com/8b0847b9-c3fc-451d-b554-7bdc3a53f7e9.png [halfart-data-only.png]: http://rikka-10066868.image.myqcloud.com/9f4fd92e-99ff-4aca-a252-b6c1ab709e65.png ================================================ FILE: pyqart/__init__.py ================================================ from .qr import ( QrData, QrPainter, # ------------ QrBasePrinter, QrImagePrinter, QrStringPrinter, QrHalftonePrinter, # ------------ QrException, ) from .art import QArtist __version__ = '0.1.0' ================================================ FILE: pyqart/art/__init__.py ================================================ from .qart import QArtist ================================================ FILE: pyqart/art/bitblock.py ================================================ # Added at : 2016.8.3 # Author : 7sDream # Usage : A block of bits. In this class, we let each point (including EC) # as much as possible to meet the color of image in it's position. from ..common import Bits, one_at, bit_at, BIT_PER_CW from ..qr.ec import RSEncoder __all__ = ['BitBlock'] _VS_CACHE = {} def _copy(i): vs = [] for line in _VS_CACHE[i]: vs.append([x for x in line]) return vs def _create_vs(dbc, ecbc): if dbc not in _VS_CACHE: print('can\'t find in cache, calculating...', end='', flush=True) vs = [] for i in range(dbc): b = [0] * (i // 8) + [one_at(i % 8)] + [0] * (dbc // 8 - i // 8 - 1) b += RSEncoder.encode(b, ecbc // BIT_PER_CW, False) vs.append(b) print('Done') _VS_CACHE[dbc] = vs else: print('found in cache.') return _copy(dbc) class BitBlock(object): def __init__(self, bits, di, dbc, eci, ecbc): self._dbc = dbc self._bits = Bits.copy_from(bits, di, dbc) self._bits.extend(bits, eci, ecbc) self._bits = self._bits.as_int_list # vector space self._vs = _create_vs(dbc, ecbc) self._locked_index = len(self._vs) self._already_set = set() self._max_index = len(self._bits) * 8 def set(self, index, value): assert isinstance(index, int) assert isinstance(value, bool) assert 0 <= index < self._max_index if index in self._already_set: return False if len(self._already_set) >= self._dbc: return False found = False for i in range(self._locked_index): if bit_at(self._vs[i][index // 8], 8, index % 8) is False: continue if not found: found = True if i != 0: self._exchange_row(0, i) continue self._vs_xor_line(i, 0) if not found: return False for i in range(self._locked_index, len(self._vs)): if bit_at(self._vs[i][index // 8], 8, index % 8) is True: self._vs_xor_line(i, 0) if bit_at(self._bits[index // 8], 8, index % 8) is not value: self._bits_xor_with_vs(0) self._exchange_row(0, self._locked_index - 1) self._locked_index -= 1 self._already_set.add(index) return True def bits(self): return Bits.copy_from(bytearray(self._bits)) def _vs_xor_line(self, i, j): self._vs[i] = [a ^ b for a, b in zip(self._vs[i], self._vs[j])] def _bits_xor_with_vs(self, i): self._bits = [a ^ b for a, b in zip(self._bits, self._vs[i])] def _exchange_row(self, i, j): self._vs[i], self._vs[j] = self._vs[j], self._vs[i] ================================================ FILE: pyqart/art/exception.py ================================================ # Added at : 2016.8.2 # Author : 7sDream # Usage : Exceptions happened at art part. from ..qr import QrException __all__ = ['ArtException'] class ArtException(QrException): pass ================================================ FILE: pyqart/art/qart.py ================================================ # Added at : 2016.8.2 # Author : 7sDream # Usage : Accept data and source image, make a QArt. import itertools from random import randint from .source import QArtSourceImage from .bitblock import BitBlock from ..qr import QrData, QrPainter from ..qr.data.numbers import Numbers from ..qr.painter.point import QrPointType from ..qr.ec import RSEncoder from ..common import Bits, BIT_PER_CW __all__ = ['QArtist'] INF = float('inf') class QArtist(QrPainter): def __init__(self, url, img, version=None, mask=None, level=0, rotation=0, dither=False, only_data=False, rand=False, higher_first=False, dy=None, dx=None): assert isinstance(img, (str, QArtSourceImage)) if isinstance(img, str): img = QArtSourceImage(img) self.source = img self.dy = dy self.dx = dx self._only_data = bool(only_data) self._higher_first = bool(higher_first) data = QrData(url + '#', level) super().__init__(data, version, mask, rotation) args, _, _ = self.get_params() print('Processing input image...', end='', flush=True) self._targets = self.source.to_targets( self.canvas, args, bool(dither), rand, dy, dx) self.dither = dither print('Done.') self._bits = None @property def bits(self): if self._bits is not None: return self._bits args, available, used = self.get_params() cci_length = args.cci_length_of(Numbers) available_for_number = available - 4 - cci_length used += 4 + cci_length if available_for_number < 4: return super().bits else: numbers_count = available_for_number // 10 * 3 remaining = available_for_number % 10 if remaining >= 7: numbers_count += 2 remaining -= 7 elif remaining >= 4: numbers_count += 1 remaining -= 4 upper = args.dcwc * BIT_PER_CW - remaining self._data.put_numbers('0' * numbers_count) while True: bits = super().bits di = 0 eci = args.dcwc * BIT_PER_CW ecbc = args.eccwcpb * BIT_PER_CW data_bits = Bits() ec_bits = Bits() for i in range(args.bc): dbc = args.dcwcof(i) * BIT_PER_CW low = 0 high = dbc if di < used: low = used - di if low >= dbc: data_bits.extend(bits, di, dbc) ec_bits.extend(bits, eci, ecbc) di += dbc eci += ecbc continue if di + dbc > upper: high = upper - di if high <= 0: data_bits.extend(bits, di, dbc) ec_bits.extend(bits, eci, ecbc) di += dbc eci += ecbc continue if not self._only_data: print('Create BitBlock', '{i}/{bc}...'.format( i=i+1, bc=args.bc, ), end='', flush=True) block = BitBlock(bits, di, dbc, eci, ecbc) else: block = Bits.copy_from(bits, di, dbc) # Lock uncontrollable bits locked_bits = set() if not self._only_data: for j in itertools.chain(range(0, low), range(high, dbc)): assert block.set(j, bits[di + j]) else: for j in itertools.chain(range(0, low), range(high, dbc)): locked_bits.add(j) targets_index = list(range(di, di+dbc)) if not self._only_data: targets_index.extend(range(eci, eci+ecbc)) def compare(x): t = self._targets[x] if t.is_hard_zero(): if self._higher_first: return INF else: return -1 else: return t.contrast targets_index = sorted(targets_index, key=compare, reverse=self._higher_first) for target_index in targets_index: target = self._targets[target_index] point = target.point fill = target.fill if point.invert: fill = not fill if target.is_hard_zero(): fill = False if point.type is QrPointType.DATA: index = point.offset - di else: assert point.type is QrPointType.CORRECTION index = point.offset - eci + dbc if not self._only_data: block.set(index, fill) elif index not in locked_bits: block[index] = fill if not self._only_data: new_block_bits = block.bits() data_bits.extend(new_block_bits, 0, dbc) ec_bits.extend(new_block_bits, dbc, ecbc) else: data_bits.extend(block) ec_bits.extend(RSEncoder.encode(block, ecbc // 8, True)) di += dbc eci += ecbc error_count = 0 numbers = '' for i in range(0, numbers_count, 3): if i + 3 > numbers_count: count = [None, 4, 7][numbers_count - i] else: count = 10 offset = used + i // 3 * 10 value = Bits.copy_from(data_bits, offset, count) value = value.as_int if count == 10 and value >= 1000: rand_pos = randint(0, 4) hard_zero_pos = offset + rand_pos self._targets[hard_zero_pos].set_hard_zero() error_count += 1 value -= 2**(9 - rand_pos) elif count == 7 and value >= 100: rand_pos = randint(0, 1) hard_zero_pos = offset + rand_pos self._targets[hard_zero_pos].set_hard_zero() error_count += 1 value -= 2**(6 - rand_pos) elif count == 4 and value >= 10: hard_zero_pos = offset self._targets[hard_zero_pos].set_hard_zero() error_count += 1 value -= 8 numbers += str(value).rjust(count // 3, '0') print('Error count', error_count, end='') if error_count == 0: print(', send to printer.') data_bits.extend(ec_bits) self._bits = data_bits return data_bits else: print(', restart.') ================================================ FILE: pyqart/art/source.py ================================================ # Added at : 2016.8.2 # Author : 7sDream # Usage : Source image to make QArt. from random import randint import PIL.Image as Image from .target import Target from ..qr.painter.point import QrPointType __all__ = ['QArtSourceImage'] class QArtSourceImage(object): def __init__(self, path, left=None, top=None, size=None, board=None): """ :param str|file path: Image file path or file-like object. :param int left: X of start point. :param int top: Y of start point. :param int size: Size of target image region. :param int board: Board region width. """ self._img = Image.open(path) left = left or 0 top = top or 0 if size is None: size = min(self._img.width - left, self._img.height - top) self._set(left or 0, top or 0, size or 0, board or 0) self.path = path def _set(self, left, top, size, border): assert left >= 0, "left arg must > 0" assert top >= 0, "top arg must > 0" assert size >= 0, "size arg must >= 0" assert left + size <= self._img.width, "region over image" assert top + size <= self._img.height, "region over image" assert border >= 0, "border width must >= 0" self._left = int(left) self._top = int(top) self._size = int(size) self._border = int(border) def set_by_center(self, x, y, size, board): offset = (size - 1) // 2 self._set(x - offset, y - offset, size, board) @staticmethod def _calc_divider(img): res = 0 for row in range(img.height): for col in range(img.width): res += img.getpixel((row, col)) n = img.width * img.height if n == 0: return 128 return res // n @staticmethod def _calc_target_range(img, y, x, dy, dx): assert dy is None or dy > 0 assert dx is None or dx > 0 dx = dx or 3 dy = dy or 3 left = x - dx if x - dx >= 0 else 0 right = x + dx + 1 if x + dx < img.width else img.width top = y - dy if y - dy >= 0 else 0 bottom = y + dy + 1 if y + dy < img.height else img.height width = right - left height = bottom - top return left, right, top, bottom, width, height def _calc_contrast(self, img, y, x, dy, dx, rand): assert 0 <= y < img.height and 0 <= x < img.width, "Point out of image." assert isinstance(rand, bool) if rand: return randint(0, 128) + 64 * ((x + y) % 2) + 64 * ((x + y) % 3 % 2) l, r, t, b, w, h = self._calc_target_range(img, y, x, dy, dx) n = w * h sum_1 = sum_2 = 0 for y in range(t, b): for x in range(l, r): v = img.getpixel((x, y)) sum_1 += v sum_2 += v * v average = sum_1 / n return sum_2 / n - average * average def to_image(self, args, dither, dy, dx): assert dx is None or dx > 0, 'dx must >= 0.' assert dy is None or dy > 0, 'dy must >= 0.' code_part_size = self._size - 2 * self._border box_x, box_y = self._left + self._border, self._top + self._border box = (box_x, box_y, box_x + code_part_size, box_y + code_part_size) img = self._img.crop(box).resize((args.size, args.size)) if dither: img = img.convert("1") else: img = img.convert('L') divider = self._calc_divider(img) img = img.point(lambda v: 0 if v <= divider else 255, '1') return img def to_targets(self, canvas, args, dither, rand, dy, dx): """ :param QrCanvas canvas: canvas used to draw the QrCode. :param QrArgs args: The args of QrCode. :param bool dither: Make binary image with dithering or not. :param bool rand: Make contrast of target random number. :param int dy: Y offset when calc target. :param int dx: X offset when calc target. """ temp = self.to_image(args, dither, dy, dx) targets = [None] * args.cwc * 8 for y in range(temp.height): for x in range(temp.width): point = canvas.points[y][x] if point.type in {QrPointType.DATA, QrPointType.CORRECTION}: fill = temp.getpixel((x, y)) == 0 contrast = self._calc_contrast(temp, y, x, dy, dx, rand) targets[point.offset] = Target(y, x, fill, contrast, point) return targets ================================================ FILE: pyqart/art/target.py ================================================ # Added at : 2016.8.3 # Author : 7sDream # Usage : Target point(contain point and image pixel info). __all__ = ['Target'] class Target(object): def __init__(self, y, x, fill, contrast, point): self._y = y self._x = x self._fill = fill self._contrast = contrast self._point = point self._hard_zero = False @property def fill(self): return self._fill @property def contrast(self): return self._contrast @property def y(self): return self._y @property def x(self): return self._x @property def point(self): return self._point def set_hard_zero(self): self._hard_zero = True def is_hard_zero(self): return self._hard_zero def __str__(self): return "Target({fill}, {contrast:.3f})".format( fill=self.fill, contrast=self.contrast ) ================================================ FILE: pyqart/common/__init__.py ================================================ from .bit_funcs import bit_at, set_bit, one_at, zero_at from .bits import Bits from .exception import InvalidTypeException BIT_PER_CW = BIT_PER_BYTE = 8 ================================================ FILE: pyqart/common/bit_funcs.py ================================================ # Added at : 2016.07.28 # Author : 7sDream # Usage : Some common function to process data. import functools __all__ = ['bit_at', 'one_at', 'zero_at', 'set_bit'] @functools.lru_cache() def one_at(pos, size=8): """ Create a size-bit int which only has one '1' bit at specific position. example: one_at(0) -> 0b10000000 one_at(3) -> 0b00010000 one_at(5, 10) -> 0b0000010000 :param int pos: Position of '1' bit. :param int size: Length of value by bit. :rtype: int """ assert 0 <= pos < size return 1 << (size - 1 - pos) @functools.lru_cache() def zero_at(pos, size=8): """ Create a size-bit int which only has one '0' bit at specific position. :param int pos: Position of '0' bit. :param int size: Length of value by bit. :rtype: int """ assert 0 <= pos < size return 2**size - 2**(size - pos - 1) - 1 def set_bit(value, pos, bit): """ Set bit at specific position of a 8-bit value to '1' or '0' :param int value: Original value, 8 bit. :param int pos: Position of bit which will be set. :param bool bit: True for 1, False for 0. :return: New value :rtype: int """ assert 0 <= pos < 8 if bit: return value | one_at(pos) else: return value & zero_at(pos) def bit_at(value, length, pos): """ Get bit at pos of number, True for '1', False for '0': :param int value: Int value to get the bit. :param int length: Length of value by bit. :param int pos: Bit position, highest position is 0. :rtype: bool """ assert length > 0 assert 0 <= pos < length if value == 0: return False return ((value >> (length - 1 - pos)) & one_at(7)) == 1 ================================================ FILE: pyqart/common/bits.py ================================================ # Added at : 2016.7.28 # Author : 7sDream # Usage : A utility class provide some bit level operation to bit stream. from .bit_funcs import set_bit, bit_at __all__ = ['Bits'] _BIT_PER_BYTE = _BIT_PER_CW = 8 _PADDING_BITS = 0b1110110000010001 """ padding with those data when data not fill data codewords. """ class Bits(object): def __init__(self, value=None, length=None): """ Build a container that save value as a value_upper-bit size int. :param int value: Value to init the container. :value_upper int value_upper: Value value_upper by bit. """ self._length = 0 self._raw = bytearray(b'\x00') if value is not None: length = value.bit_length() if length is None else length self.append(value, length) @classmethod def copy_from(cls, other, start=0, count=None): """ Build object from other :any:`Bits`. :param Bits|bytes|bytearray other: Target object. :param int start: Where to start copy. :param int count: How many data will be added. Default is None, will add all data. """ obj = cls() obj.extend(other, start, count) return obj @property def length(self): """ :return: Data value_upper by bit. :rtype: int """ return self._length def __len__(self): return self.length @property def capacity(self): """ :return: Capacity of object by bit. :return: int """ return self.capacity_by_byte * _BIT_PER_BYTE @property def capacity_by_byte(self): """ :return: Capacity of object by byte. :return: int """ return len(self._raw) def append(self, value, length=None): """ Add a value_upper-bit int to container's end, use lower data of value. :param int value: Value to be added. :param int length: Length of value by bit, default is None, which will use value.bit_length(). """ if length is None: length = value.bit_length() for i in range(length): self.append_bit(bit_at(value, length, i)) def append_bit(self, bit): """ Add one bit to container. :param bool bit: True for 1, False for 0 """ index = self.length self._expand_capacity(self._length + 1) self._length += 1 self[index] = bit def extend(self, other, start=0, count=None): """ Add new value from other Bits. :param Bits|bytes|bytearray other: Other data source want to be added. :param int start: Where to start copy, default is 0. :param int count: how many data will be extend. default is None, will add all data. :return: How many data be extended. :rtype: int """ assert start >= 0 if isinstance(other, Bits): end = len(other) if count is None else (start + count) end = min(end, len(other)) for i in range(start, end): self.append_bit(other[i]) return max(0, end - start) elif isinstance(other, (bytes, bytearray)): end = len(other) * _BIT_PER_BYTE \ if count is None else (start + count) end = min(end, len(other) * _BIT_PER_BYTE) for i in range(start, end): self.append_bit(bit_at( other[i // _BIT_PER_BYTE], _BIT_PER_BYTE, i % _BIT_PER_BYTE)) return max(0, end - start) return 0 def xor(self, other, my_start=0, other_start=0, count=None): """ [001010001] xor [0010110], self_start at 2 other_start at 3, value_upper 3 will be [00<101>0001] xor [001<011>0] -> [00<110>0001] :param Bit other: What to xor with. :param int my_start: Where to start be xor. :param int other_start: Where to start xor with. :param int count: How many data will be xor, default is None, will xor all possible. :return: How many data be xor. :rtype: int """ my_end = self.length if count is None else (my_start + count) other_end = other.length if count is None else (other_start + count) my_end = min(self.length, my_end) other_end = min(other.length, other_end) count = 0 for i, j in zip(range(my_start, my_end), range(other_start, other_end)): self[i] = self[i] ^ other[j] count += 1 return count def pad(self, available, used): # add terminator self.append(0, min(available, 4)) # add more 0s to make last several data to a codeword if self.length % _BIT_PER_CW != 0: self.append(0, _BIT_PER_CW - self.length % _BIT_PER_CW) # add pad bytes if the data is still not fill all data codewords available = available + used - self.length while available > 0: self.append(_PADDING_BITS, min(available, 16)) available -= 16 def _expand_capacity(self, target): """ Expend capacity to ensure object can save "target" value_upper data. :param int target: Target capacity by bit """ assert target >= 0 while target > self.capacity: self._raw += b'\x00' * self.capacity_by_byte @property def as_int(self): """ :return: View those data as int start at highest position, -1 if no data. :rtype: int """ if self.length == 0: return -1 return int(self.as_string, 2) @property def as_string(self): """ :return: A string of "01" to represent those data. empty string if no data. :rtype: string """ return ''.join(['1' if x else '0' for x in self]) @property def as_bytes(self): return self._raw[:(self.length - 1) // _BIT_PER_BYTE + 1] @property def as_int_list(self): return [int(x) for x in self.as_bytes] def __str__(self): s = self.as_string return ', '.join([s[x:x + 8] for x in range(0, len(s), 8)]) def __repr__(self): return "Bits at {id}: [{self}]".format( id=id(self), self=self, ) def __iter__(self): for i in range(self.length): yield self[i] def __getitem__(self, index): if index >= self.length: raise IndexError() return bit_at( self._raw[index // _BIT_PER_BYTE], _BIT_PER_BYTE, index % _BIT_PER_BYTE ) def __setitem__(self, index, value): if index >= self.length: raise IndexError() old_value = self._raw[index // _BIT_PER_BYTE] new_value = set_bit(old_value, index % _BIT_PER_BYTE, value) self._raw[index // _BIT_PER_BYTE] = new_value ================================================ FILE: pyqart/common/exception.py ================================================ # Added at : 2016.07.30 # Author : 7sDream # Usage : Exceptions raised by common functions or classes, class InvalidTypeException(Exception): def __init__(self, excepted, given, index, function): self._excepted = excepted self._given = given self._function = function self._index = index + 1 def __str__(self): string = "Invalid type at No.{number} argument of {function}, " \ "except {excepted}, {given} given." return string.format( index=self._index, function=self._function, excepted=self._excepted, given=self._given, ) __repr__ = __str__ ================================================ FILE: pyqart/qart_entry.py ================================================ import argparse import sys import time from pyqart.art import QArtist from pyqart.qr.printer import QrImagePrinter, QrStringPrinter def main(): parser = argparse.ArgumentParser( prog="pyqart", description="A program of generate QArt Codes.", epilog="Writen by 7sDream. (https://github.com/7sDream/pyqart)", ) parser.add_argument( 'url', type=str, help="url will be encode, like http://example.com/", ) parser.add_argument( 'img', type=str, help="target image the QrCode will look like", ) parser.add_argument( '-s', '--start-point', type=int, nargs=2, help="left top point of the region of target image to use, " "default is (0, 0).", ) parser.add_argument( '-w', '--region-width', type=int, help="target region width and height, " "default will make region as bigger as possible", ) parser.add_argument( '-d', '--dither', action="store_true", help="dithering when generate binary target image", ) parser.add_argument( '-y', '--only-data', action="store_true", help="only use data bit points to approach the target image", ) parser.add_argument( '-n', '--rand', action="store_true", help="generate point contrast by random, " "if not provide, will use pixel nearby to calculate contrast", ) parser.add_argument( '-f', '--higher-first', action='store_true', help="pick pixel from higher contrast region first, " "default will pick from lower region first" ) parser.add_argument( '-x', '--yx', type=int, nargs=2, help="yx region when calculate contrast", ) parser.add_argument( '-v', '--version', type=int, help="version of QrCode, 1 to 40, " "will auto calculated from data length if not provide", ) parser.add_argument( '-l', '--level', type=int, default=0, help="QrCode error correction level, 0 to 3, default is 0", ) parser.add_argument( '-m', '--mask', type=int, help="mask of QrCode, 0 to 7, default is random value", ) parser.add_argument( '-r', '--rotation', type=int, default=0, help="rotate the QrCode(clockwise), " "0 for no rotation, 1 for 90 degree, 2 for 180, 3 for 270", ) parser.add_argument( '-p', '--point-size', type=int, default=3, help="the point width and height of one QrCode point," " by pixel, default is 3" ) parser.add_argument( '-b', '--board', type=int, help="board wide by pixel, will auto calculated " "from code size if not provide", ) parser.add_argument( '-c', '--color', type=int, nargs=3, metavar=('R', 'G', 'B'), help="front color of QrCode, 3 number as rgb color", ) parser.add_argument( '-g', '--background-color', type=int, nargs=3, metavar=('R', 'G', 'B'), help="background color of QrCode, 3 number as rgb color", ) parser.add_argument( '-o', '--output', type=str, help="output file path, code will print to terminal if not provide, " "and other arguments will be ignored" ) argv = sys.argv[1:] args = parser.parse_args(argv) if args.yx is None: args.yx = [None, None] if args.color is not None: args.color = tuple(args.color) if args.background_color is not None: args.background_color = tuple(args.background_color) start = time.time() artist = QArtist(args.url, args.img, args.version, args.mask, args.level, args.rotation, args.dither, args.only_data, args.rand, args.yx[0], args.yx[1]) if args.output is not None: QrImagePrinter.print( artist, args.output, args.point_size, args.board, args.color, args.background_color ) print('Done.') else: QrStringPrinter.print(artist, True) end = time.time() print("Used time:", end-start, 'second.') if __name__ == '__main__': main() ================================================ FILE: pyqart/qr/__init__.py ================================================ from .args import QrArgs from .data import ( QrData, QrDataInvalidException, QrEncodingException, QrSpaceNotEnoughException ) from .painter import QrPainter, QrCanvasException, QrPainterException from .printer import ( QrBasePrinter, QrImagePrinter, QrStringPrinter, QrHalftonePrinter ) from .exception import QrException ================================================ FILE: pyqart/qr/args/__init__.py ================================================ from .args import QrArgs ================================================ FILE: pyqart/qr/args/args.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : All needed args to create a empty qr code. from .mask import QrMask from .version import QrVersion from .rotation import QrRotation from ..data import Raw, AlphaNumeric, Numbers from ...common import Bits __all__ = ['QrArgs'] _FORMAT_POLY = Bits(0x537, 11) """ x^10 + x^8 + x^5 + x^4 + x^2 + x + 1 = 1 0 1 00 1 1 0 1 1 1 = 0101 0011 0111 = 0x573 Used when calculate format pattern data. """ _FORMAT_MASK_PATTERN = Bits(0x5412, 15) """ After all, format data should be xor with this mask pattern. """ _BC_ECCWCPB_TABLE = [ [(1, 7), (1, 10), (1, 13), (1, 17)], [(1, 10), (1, 16), (1, 22), (1, 28)], [(1, 15), (1, 26), (2, 18), (2, 22)], [(1, 20), (2, 18), (2, 26), (4, 16)], [(1, 26), (2, 24), (4, 18), (4, 22)], [(2, 18), (4, 16), (4, 24), (4, 28)], [(2, 20), (4, 18), (6, 18), (5, 26)], [(2, 24), (4, 22), (6, 22), (6, 26)], [(2, 30), (5, 22), (8, 20), (8, 24)], [(4, 18), (5, 26), (8, 24), (8, 28)], [(4, 20), (5, 30), (8, 28), (11, 24)], [(4, 24), (8, 22), (10, 26), (11, 28)], [(4, 26), (9, 22), (12, 24), (16, 22)], [(4, 30), (9, 24), (16, 20), (16, 24)], [(6, 22), (10, 24), (12, 30), (18, 24)], [(6, 24), (10, 28), (17, 24), (16, 30)], [(6, 28), (11, 28), (16, 28), (19, 28)], [(6, 30), (13, 26), (18, 28), (21, 28)], [(7, 28), (14, 26), (21, 26), (25, 26)], [(8, 28), (16, 26), (20, 30), (25, 28)], [(8, 28), (17, 26), (23, 28), (25, 30)], [(9, 28), (17, 28), (23, 30), (34, 24)], [(9, 30), (18, 28), (25, 30), (30, 30)], [(10, 30), (20, 28), (27, 30), (32, 30)], [(12, 26), (21, 28), (29, 30), (35, 30)], [(12, 28), (23, 28), (34, 28), (37, 30)], [(12, 30), (25, 28), (34, 30), (40, 30)], [(13, 30), (26, 28), (35, 30), (42, 30)], [(14, 30), (28, 28), (38, 30), (45, 30)], [(15, 30), (29, 28), (40, 30), (48, 30)], [(16, 30), (31, 28), (43, 30), (51, 30)], [(17, 30), (33, 28), (45, 30), (54, 30)], [(18, 30), (35, 28), (48, 30), (57, 30)], [(19, 30), (37, 28), (51, 30), (60, 30)], [(19, 30), (38, 28), (53, 30), (63, 30)], [(20, 30), (40, 28), (56, 30), (66, 30)], [(21, 30), (43, 28), (59, 30), (70, 30)], [(22, 30), (45, 28), (62, 30), (74, 30)], [(24, 30), (47, 28), (65, 30), (77, 30)], [(25, 30), (49, 28), (68, 30), (81, 30)], ] """ Table of (block count, error correction codeword count per block). Row by version, column by level. """ CCI_LENGTH_TABLE = { Raw: [(9, 8), (40, 16)], AlphaNumeric: [(9, 9), (26, 11), (40, 13)], Numbers: [(9, 10), (26, 12), (40, 14)] } """ When data encoding, the Char Count Indicator value_upper table. if version <= first_item, cci_length is second_item. """ class QrArgs(object): def __init__(self, version, level=0, mask=0, rotation=0): assert 0 <= level <= 3, "Level must between 0 and 3." self._version = QrVersion(version) self._mask = QrMask(mask) self._level = level self._rotation = QrRotation(rotation) @property def size(self): """ :return: Width and height of QrCode. :rtype: int """ return self._version.size @property def rotate_func(self): return self._rotation.rotate_func @property def align_start(self): """ :return: See :any:`QrVersion.align_start`. :rtype: int """ return self._version.align_start @property def align_step(self): """ :return: See :any:`QrVersion.align_step`. :rtype: int """ return self._version.align_step @property def version_pattern_value(self): """ :return: See :any:`QrVersion.version_pattern_value`. :rtype: int """ return self._version.version_pattern_value @property def version_number(self): """ :return: See :any:`QrVersion.number`. :rtype: int """ return self._version.number @property def level(self): """ :return: See :any:`QrLevel.index`. :rtype: int """ return self._level @property def mask_index(self): """ :return: See :any:`QrMask.index`. :rtype: int """ return self._mask.index @property def format_pattern_bits(self): """ Format pattern has 15 bit, split to 3 parts: level, mask, error correction. It's structure like bellow: 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 Lv Lv M M M C C C C C C C C C C Level part value table: ---- ----------- --------- name index value bit value ---- ----------- --------- L 00 01 M 01 00 H 10 11 Q 11 10 ---- ----------- --------- We can see: bit value = index value xor 01, 2 bit QrMask from 0(000) to 7(111), 3 bit Correction data is calculated by bch(15, 5), 10 bit :return: The format pattern value in a :any:`Bits` object. :rtype: Bits """ # level bits = Bits(self.level ^ 0b01, 2) # mask bits.append(self.mask_index, 3) # ec bits.append(0, 10) ec = Bits.copy_from(bits) for i in range(5): if ec[i]: ec.xor(_FORMAT_POLY, i, 0) for i in range(5, 15): bits[i] = ec[i] # masking bits.xor(_FORMAT_MASK_PATTERN) return bits @property def bc(self): """ :return: Block count. :rtype: int """ return _BC_ECCWCPB_TABLE[self.version_number - 1][self.level][0] @property def eccwcpb(self): """ :return: Error Correction CodeWord Count Per Block. :return: int """ return _BC_ECCWCPB_TABLE[self.version_number - 1][self.level][1] @property def cwc(self): """ :return: See :any:`QrVersion.cwc`. :rtype: int """ return self._version.cwc @property def eccwc(self): """ :return: Error Correction CodeWord Count :rtype: int """ return self.eccwcpb * self.bc @property def dcwc(self): """ :return: Data CodeWord Count. :rtype: int """ return self.cwc - self.eccwc @property def ndcwcpb(self): """ :return: Normal Data CodeWord Count Per Block. :rtype: int """ return self.dcwc // self.bc @property def edcwc(self): """ :return: Extra Data CodeWord Count. :rtype: int """ return self.dcwc - self.ndcwcpb * self.bc def dcwcof(self, index): """ :param int index: Block index. :return: Data CodeWord Count OF No.index block. :rtype: int """ assert index < self.bc return self.ndcwcpb + (0 if index < (self.bc - self.edcwc) else 1) @property def should_invert(self): """ :return: See :any:`QrMask.should_invert`. :rtype: callable """ return self._mask.should_invert def cci_length_of(self, cls): for sep, value in CCI_LENGTH_TABLE[cls]: if self.version_number <= sep: return value ================================================ FILE: pyqart/qr/args/mask.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : QrMask represent data part mask of QrCode, # which changes which modules are dark and which are light # according to a particular rule. # The purpose of this step is to modify the QR code to make it # as easy for a QR code reader to scan as possible. # There are only 8 mask pattern can be used. __all__ = ['QrMask'] _FUNCTION_LIST = [ lambda y, x: (x + y) % 2 == 0, lambda y, x: y % 2 == 0, lambda y, x: x % 3 == 0, lambda y, x: (x + y) % 3 == 0, lambda y, x: (y // 2 + x // 3) % 2 == 0, lambda y, x: x * y % 2 + x * y % 3 == 0, lambda y, x: (x * y % 2 + x * y % 3) % 2 == 0, lambda y, x: ((x + y) % 2 + (x * y) % 3) % 2 == 0, ] """ The mask function table. """ class QrMask(object): def __init__(self, mask_index): assert 0 <= mask_index <= 7, "Mask must between 0 and 7" self._index = mask_index @property def index(self): """ :return: Mask index, from 0 to 7, specific the mask pattern. :rtype: int """ return self._index @property def should_invert(self): """ :return: A function accept (y, x) to decide if point should invert. :rtype: callable """ return _FUNCTION_LIST[self.index] ================================================ FILE: pyqart/qr/args/rotation.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : QrRotation represent the rotate of QrCode: # 0 for rotate 0 degrees clockwise, 1 for 90, 2 to 180, 3 for 270. __all__ = ['QrRotation'] _ROTATE_FUNC_LIST = [ None, lambda y, x, s: (x, s-y-1), lambda y, x, s: (s-y-1, s-x-1), lambda y, x, s: (s-x-1, y) ] class QrRotation(object): def __init__(self, rotate_index): assert 0 <= rotate_index <= 3, "Rotation must between 0 and 3." self._index = rotate_index @property def index(self): return self._index @property def rotate_func(self): return _ROTATE_FUNC_LIST[self.index] ================================================ FILE: pyqart/qr/args/version.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : QrVersion represent version of QrCode, which decide: # code's size, # value_upper codeword count, # align pattern position, # version pattern value. __all__ = ['QrVersion'] _ALIGN_START_TABLE = [ 100, 16, 20, 24, 28, 32, 20, 22, 24, 26, 28, 30, 32, 24, 24, 24, 28, 28, 28, 32, 26, 24, 28, 26, 30, 28, 32, 24, 28, 24, 28, 32, 28, 32, 28, 22, 26, 30, 24, 28, ] _ALIGN_STEP_TABLE = [ 100, 100, 100, 100, 100, 100, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24, 26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28, ] _VERSION_PATTERN_VALUE_TABLE = [ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7c94, 0x85bc, 0x9a99, 0xa4d3, 0xbbf6, 0xc762, 0xd847, 0xe60d, 0xf928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, 0x27541, 0x28c69, ] _CODEWORD_COUNT_TABLE = [ 26, 44, 70, 100, 134, 172, 196, 242, 292, 346, 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085, 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706, ] class QrVersion(object): def __init__(self, version_number): assert 1 <= version_number <= 40, "Version must between 1 and 40." self._num = version_number @property def number(self): """ :return: The number represent of the version, from 1 to 40. :rtype: int """ return self._num @property def size(self): return 17 + 4 * self._num @property def align_start(self): return _ALIGN_START_TABLE[self.number - 1] @property def align_step(self): return _ALIGN_STEP_TABLE[self.number - 1] @property def version_pattern_value(self): return _VERSION_PATTERN_VALUE_TABLE[self.number - 1] @property def cwc(self): """ CodeWord Count """ return _CODEWORD_COUNT_TABLE[self.number - 1] ================================================ FILE: pyqart/qr/data/__init__.py ================================================ from .data import QrData from .raw import Raw from .numbers import Numbers from .alphanumeric import AlphaNumeric from .exception import ( QrDataInvalidException, QrEncodingException, QrSpaceNotEnoughException, ) ================================================ FILE: pyqart/qr/data/alphanumeric.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : Data mode that used to encode # big alpha (A-Z) # and numeric (0-9) # and some special symbols (space, $, %, *, +, -, ., /, :). from .exception import QrDataInvalidException from .base import BaseType from ...common import Bits __all__ = ['AlphaNumeric'] _ALPHA_NUMERIC_TABLE = { ''.join([chr(c) for c in range(ord('0'), ord('9') + 1)]): lambda c: int(c), ''.join([chr(c) for c in range(ord('A'), ord('Z') + 1)]): lambda c: ord(c) - ord('A') + 10, ' $%*+-./:': lambda c: ' $%*+-./:'.index(c) + 36 } _DOMAIN = '' for key in _ALPHA_NUMERIC_TABLE.keys(): _DOMAIN += key class AlphaNumeric(BaseType): def __init__(self, data, cci_length): super().__init__(data, cci_length) @property def _mode_indicator(self): return 0b0010 def _validate(self): for i, v in enumerate(self.data): if v not in _DOMAIN: raise QrDataInvalidException( type(self).__name__, self.data, i, ) @property def _encoded_data_part(self): bits = Bits() for i in range(0, len(self.data), 2): part = self.data[i:i + 2] if len(part) == 2: a, b = tuple(part) length = 11 else: a, b = part[0], None length = 6 bits.append(self._calc_number(a, b), length) return bits @staticmethod def _calc_number(a, b=None): number1 = number2 = -1 for k, func in _ALPHA_NUMERIC_TABLE.items(): if a in k: number1 = func(a) if b is not None and b in k: number2 = func(b) if b is None: return number1 else: return number1 * 45 + number2 @property def _encoded_data_part_length(self): return 11 * (len(self.data) // 2) + 6 * (len(self.data) % 2) ================================================ FILE: pyqart/qr/data/base.py ================================================ # Added at : 2016.7.28 # Author : 7sDream # Usage : A base data mode class for encoding data to bytes. # All specific data model inherit from this class. import abc from ...common import Bits from .exception import QrEncodingException __all__ = ['BaseType'] class BaseType(object): def __init__(self, data, cci_length): """ :param data: Data to be encoded :param int cci_length: value_upper of Char Count Indicator in bit """ assert len(data) > 0, 'Unable to encode empty data.' self._data = data self._cci_length = cci_length self._validate() @property def data(self): """ :return: provided, raw original data """ return self._data @property @abc.abstractmethod def _encoded_data_part_length(self): return 0 @property def needed_space(self): return 4 + self._cci_length + self._encoded_data_part_length @property @abc.abstractmethod def _mode_indicator(self): """ :return: A 4-bit data to indicate what model is using, Use the lower 4 data. :rtype: int """ pass @property def _char_count_indicator(self): """ :return: Placed before encoded data to indicate data value_upper, it's own value_upper is decided by :any:`cci_length`. :rtype: Bits """ bits = Bits() bits.append(0, self._cci_length - len(self.data).bit_length()) bits.append(len(self.data), len(self.data).bit_length()) return bits @abc.abstractmethod def _validate(self): """ validate data, raise :any:`QrDataInvalidException` if data is invalid, implemented by subclasses. :raise: QrDataInvalidException """ pass @property @abc.abstractmethod def _encoded_data_part(self): """ encode data to bytes use specific model, implemented by subclasses. :return: encoded data :rtype: Bits """ pass @property def output(self): """ :return: Output encoded data. :rtype: Bits """ bits = Bits() bits.append(self._mode_indicator, 4) bits.extend(self._char_count_indicator) bits.extend(self._encoded_data_part) if bits.length != self.needed_space: raise QrEncodingException( type(self), self.data, info="Encoded data value_upper does not match expectations.", exception=self.needed_space, actual=bits.length, ) return bits def __str__(self): mi = Bits() mi.append(self._mode_indicator, 4) cci = Bits() cci.extend(self._char_count_indicator) encoded_data = Bits() encoded_data.extend(self._encoded_data_part) string = "{type} at {id:x}: " \ "{{data: {data}, mi: {mi}, cci: {cci}, encode: {code}}}" return string.format( type=type(self).__name__, id=id(self), data=self.data, mi=mi, cci=cci, code=encoded_data, ) ================================================ FILE: pyqart/qr/data/data.py ================================================ # Added at : 2016.7.30 # Author : 7sDream # Usage : As a data set of (maybe) different data mode thad will be encoded. from .raw import Raw from .alphanumeric import AlphaNumeric from .numbers import Numbers from .exception import QrSpaceNotEnoughException from ...common import BIT_PER_CW class QrData(object): def __init__(self, string=None, ec_level=0): from ..args import QrArgs assert isinstance(string, str) assert 0 <= ec_level <= 3 self._data_set = [] self._ec_level = ec_level self._changed = False self._last = (1, QrArgs(1).dcwc * BIT_PER_CW, 0) if string is not None: self.put_string(string) @property def size(self): """ :return: How many data item in object. :rtype: int """ return len(self._data_set) @property def version_used_available(self): from ..args import QrArgs if self._changed is True: args = None used = 0 for i in range(1, 41): args = QrArgs(i, self._ec_level) encode_list = [cls(data, args.cci_length_of(cls)) for cls, data in self._data_set] used = sum([x.needed_space for x in encode_list]) available = args.dcwc * BIT_PER_CW - used if available >= 0: self._last = (i, available, used) self._changed = False break else: raise QrSpaceNotEnoughException( args.dcwc * BIT_PER_CW, used ) return self._last @property def ec_level(self): return self._ec_level def set_level(self, level): assert 0 <= level <= 3 if self._ec_level != level: self._ec_level = level self._changed = True def _common_put(self, data, cls): if len(self._data_set) > 0 and self._data_set[-1][0] is cls: old_data = self._data_set[-1][1] self._data_set[-1] = (cls, old_data + data) else: self._data_set.append((cls, data)) self._changed = True def put_string(self, string): """ Add string(utf-8) data to QrCode. :param str string: The string will be added. :return: A tuple: (if_success, exception). :rtype: (bool, QrException) :raise: QrDataInvalidException """ return self._common_put(string.encode('utf-8'), Raw) def put_bytes(self, data): """ Add raw bytes data to QrCode. :see-also:: :any:`put_string` for return and exception info. """ return self._common_put(data, Raw) def put_numbers(self, numbers): """ Add numbers data to QrCode. :see-also:: :any:`put_string` for return and exception info. :param int|str numbers: The number will be added, 0 start at string type numbers will be preserved. """ return self._common_put(numbers, Numbers) def put_alpha_numeric(self, string): """ Add numbers, big letters, and some special symbol data to QrCode. :see-also:: :any:`put_string` for return and exception info. :param str string: The data will be added. """ return self._common_put(string, AlphaNumeric) @property def data_set(self): return tuple(self._data_set) ================================================ FILE: pyqart/qr/data/exception.py ================================================ # Added at : 2016.7.28 # Author : 7sDream # Usage : Exception when encode data is not invalid. from ..exception import QrException __all__ = ['QrDataInvalidException', 'QrEncodingException', 'QrSpaceNotEnoughException'] class QrDataInvalidException(QrException): def __init__(self, typename, invalid_data, index=None): self.typename = typename self.invalid_data = invalid_data self.index = index def __str__(self): string = "Invalid data \"{data}\" when build a {typename} data mode." if self.index is not None: string += " first invalid position is {number}." return string.format( data=self.invalid_data, typename=self.typename, index=self.index, ) __repr__ = __str__ class QrEncodingException(QrException): def __init__(self, cls, data, **kwargs): self._cls = cls self._data = data self._kwargs = kwargs def __str__(self): string = "Error when encoding {cls}] type data [{data}]." if len(self._kwargs) > 0: string += 'Additional information: ' + str(self._kwargs) class QrSpaceNotEnoughException(QrException): def __init__(self, available, need): self._available = available self._needed = need def __str__(self): string = "There is not enough space to store the data provided, " string += "{available} bit space available, data need {need} bit." return string.format( available=self._available, need=self._needed ) __repr__ = __str__ ================================================ FILE: pyqart/qr/data/numbers.py ================================================ # Added at : 2016.7.28 # Author : 7sDream # Usage : Numbers data model, 10 bit for 3 numbers. from .base import BaseType from ...common import Bits from .exception import QrDataInvalidException class Numbers(BaseType): def __init__(self, data, cci_length): super().__init__(data, cci_length) def _validate(self): for i, value in enumerate(self.data): if not ord('0') <= ord(value) <= ord('9'): raise QrDataInvalidException( type(self).__name__, self.data, i) @property def _encoded_data_part(self): bits = Bits() split = (self.data[x:x + 3] for x in range(0, len(self.data), 3)) for i, string in enumerate(split): bits.append(int(string), 1 + 3 * len(string)) return bits @property def _mode_indicator(self): return 0b0001 @property def _encoded_data_part_length(self): return 10 * (len(self.data) // 3) + [0, 4, 7][len(self.data) % 3] ================================================ FILE: pyqart/qr/data/raw.py ================================================ # Added at : 2016.7.28 # Author : 7sDream # Usage : Raw data model, 8 bit for a byte. from .base import BaseType from .exception import QrDataInvalidException from ...common import Bits __all__ = ['Raw'] class Raw(BaseType): def __init__(self, data, cci_length): super().__init__(data, cci_length) @property def _encoded_data_part(self): bits = Bits() bits.extend(self.data) return bits @property def _mode_indicator(self): return 0b0100 def _validate(self): for i, value in enumerate(self.data): if value < 0 or value > 255: raise QrDataInvalidException( type(self).__name__, self.data, i, ) @property def _encoded_data_part_length(self): return 8 * len(self.data) ================================================ FILE: pyqart/qr/ec/__init__.py ================================================ from .rsencoder import RSEncoder ================================================ FILE: pyqart/qr/ec/gf.py ================================================ # Added at : 2016.7.31 # Author : 7sDream # Usage : Calculate GF(2^m) and all it's item a^n from ...common import bit_at __all__ = ['GF28'] _MUL_CACHE = {} _ADD_CACHE = {} class _GF2M(object): def __init__(self, m, px): self._m = m self._px = px % self.value_upper self._table, self._rev_table = self.calc() @property def value_upper(self): return 2 ** self._m @property def index_upper(self): return self.value_upper def calc(self): table = [] rev_table = [None] * self.value_upper for x in range(self.index_upper): if x < self._m: value = 1 << x elif x == self._m: value = self._px elif not bit_at(table[-1], self._m, 0): value = (table[-1] << 1) % self.value_upper else: value = (table[-1] << 1 ^ self._px) % self.value_upper table.append(value) try: rev_table[value] = x except Exception as e: print(value) raise e table.append(1) return table, rev_table def index(self, value): return self._rev_table[value] def __getitem__(self, index): return _GFItem(self, index, self._table[index]) class _GFItem(object): def __init__(self, gf, index, value): self._gf = gf self._index = index self._value = value @property def gf(self): return self._gf @property def index(self): return self._index @property def value(self): return self._value def __add__(self, other): cache_index = (self.gf, self.index, other.index) if cache_index not in _ADD_CACHE: value = self._value ^ other.value if value == 0: return None item = _GFItem(self.gf, self.gf.index(value), value) _ADD_CACHE[cache_index] = item else: item = _ADD_CACHE[cache_index] return item def __mul__(self, other): cache_index = (self.gf, self.index, other.index) if cache_index not in _MUL_CACHE: index = self.index + other.index index = index % self.gf.value_upper + int(index // self.gf.value_upper) item = self.gf[index] _MUL_CACHE[cache_index] = item else: item = _MUL_CACHE[cache_index] return item def __str__(self): return "a" + str(self.index) GF28 = _GF2M(8, 0b100011101) ================================================ FILE: pyqart/qr/ec/poly.py ================================================ # Added at : 2016.07.31 # Author : 7sDream # Usage : Provide math operator with polynomials on GF28. # Used in Reed-solomon encoder. import abc from .gf import GF28 __all__ = ['GF28Poly'] class _GFPoly(object): @classmethod @abc.abstractmethod def gf(cls): pass def __init__(self, pcmap): self._pcmap = pcmap if self._pcmap: self._max_index = max(self._pcmap.keys()) else: self._max_index = 0 @classmethod def from_index_list(cls, ilist, maxp): pcmap = {} for xi, ai in enumerate(ilist): if ai is None: continue pcmap[maxp - xi] = cls.gf()[ai] return cls(pcmap) @classmethod def from_value_list(cls, vlist, maxp): pcmap = {} for i, v in enumerate(vlist): if v == 0: continue pcmap[maxp - i] = cls.gf()[cls.gf().index(v)] return cls(pcmap) @property def pcmap(self): return self._pcmap @property def max_index(self): return self._max_index @property def as_int_list(self): int_list = [] for p in reversed(range(self.max_index + 1)): if p in self.pcmap: int_list.append(self.pcmap[p].value) else: int_list.append(0) return int_list def __mul__(self, other): new_pcmap = {} for p1, c1 in self.pcmap.items(): for p2, c2 in other.pcmap.items(): if (p1 + p2) in new_pcmap: old_value = new_pcmap[p1 + p2] new_pcmap[p1 + p2] = old_value + c1 * c2 else: new_pcmap[p1 + p2] = c1 * c2 return type(self)(new_pcmap) def __mod__(self, other): r = type(self)(self.pcmap) while r.max_index >= other.max_index: pad = r.max_index - other.max_index pad_item = type(self)({pad: r.pcmap[r.max_index]}) r += other * pad_item return r def __add__(self, other): pcmap = {} for p in range(max(self.max_index, other.max_index) + 1): if p in self.pcmap: pcmap[p] = self.pcmap[p] if p in other.pcmap: if p in pcmap: pcmap[p] += other.pcmap[p] else: pcmap[p] = other.pcmap[p] if pcmap[p] is None: del pcmap[p] return type(self)(pcmap) def __str__(self): pc_list = sorted(self.pcmap.items(), key=lambda x: x[0], reverse=True) strings = [] for p, c in pc_list: if p == 0: item = str(c) elif p == 1: item = str(c) + 'x' else: item = str(c) + 'x^' + str(p) strings.append(item) return '+'.join(strings) def __repr__(self): return "Poly at {id}: {string}".format(id=id(self), string=str(self)) class GF28Poly(_GFPoly): @classmethod def gf(cls): return GF28 ================================================ FILE: pyqart/qr/ec/rsencoder.py ================================================ from .poly import GF28Poly from ...common import Bits class _RSGenPolynomials(object): def __init__(self): self._table = [None, GF28Poly.from_index_list([0, 0], 1)] def __getitem__(self, index): while index > len(self._table) - 1: c = len(self._table) - 1 self._table.append( self._table[-1] * GF28Poly.from_index_list([0, c], 1)) return self._table[index] RSGenPolynomials = _RSGenPolynomials() class RSEncoder(object): @classmethod def encode(cls, data, ec_length, need_bits=True): assert ec_length >= 0 if ec_length == 0: return Bits() if not isinstance(data, list): data = data.as_int_list data_length = len(data) all_length = ec_length + data_length m = GF28Poly.from_value_list( data + [0] * ec_length, all_length - 1 ) g = RSGenPolynomials[ec_length] r = m % g res = r.as_int_list if len(res) < ec_length: res = [0] * (ec_length - len(res)) + res if need_bits: return Bits.copy_from(bytearray(res)) else: return res ================================================ FILE: pyqart/qr/exception.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : Base exception. __all__ = ['QrException'] class QrException(Exception): pass ================================================ FILE: pyqart/qr/painter/__init__.py ================================================ from .painter import QrPainter from .exception import QrCanvasException, QrPainterException ================================================ FILE: pyqart/qr/painter/canvas.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : A canvas to draw the QrCode. It implements function to draw # a basic QrCode whose data part is empty from QrArgs. from .exception import QrCanvasException from .point import QrPoint, QrPointType from ..args import QrArgs __all__ = ['QrCanvas'] _TYPE_CHAR_MAP = { QrPointType.UNKNOWN: '? ', QrPointType.POSITION: 'Q*', QrPointType.ALIGNMENT: 'A^', QrPointType.TIMING: 'T-', QrPointType.FORMAT: 'F<', QrPointType.VERSION_PATTERN: 'V+', QrPointType.UNUSED: 'Nu', QrPointType.DATA: '@.', QrPointType.CORRECTION: '#,', QrPointType.EXTRA: 'E~', } """ The char table use to convert QrCode to string to print. """ _BIT_PER_CW = 8 class QrCanvas(object): def __init__(self, args): assert isinstance(args, QrArgs), "args argument must be QrArgs object." self._args = args self._size = self.args.size self._points = [ [QrPoint(False) for _ in range(self.size)] for _ in range(self.size)] self._add_timing_pattern() self._add_position_patterns() self._add_align_patterns() self._add_version_pattern() self._add_unused_point() self._add_format_pattern() self._data_ec_points = self._add_empty_data_ec() self._add_mask() self._rotate() def _add_timing_pattern(self): """ Add timing line to point array, like bellow 0 1 2 3 4 5 6 7 8 9 ... -- x axis 0 @ 1 . 2 @ 3 . 4 @ 5 . 6 @ . @ . @ . @ . @ . @ . @ . @ . @ . @ . @ . @ .... 7 . 8 @ 9 . 10 @ 11 . 12 @ 13 ... | y axis The (0-6, 6) and (6, 0-5) part will be override by position box. """ # rol 6 and col 6 is timing version_pattern_value timing_position = 6 for i in range(self.size): self._points[i][timing_position].type = QrPointType.TIMING self._points[timing_position][i].type = QrPointType.TIMING if i % 2 == 0: self._points[i][timing_position].fill = True self._points[timing_position][i].fill = True def _add_position_patterns(self): self._add_position_pattern(0, 0) self._add_position_pattern(0, self.size - 7) self._add_position_pattern(self.size - 7, 0) def _add_align_patterns(self): first_special_pos = 4 start = self.args.align_start step = self.args.align_step size = self.size y = first_special_pos while y + 5 < size: x = first_special_pos while x + 5 < size: if self._check_align_box_position(y, x): self._add_align_pattern(y, x) x = start if x == first_special_pos else (x + step) y = start if y == first_special_pos else (y + step) def _add_version_pattern(self): version_block_pattern = self.args.version_pattern_value if version_block_pattern != 0: for x in range(6): for y in range(3): point_a = self._points[self.size - 11 + y][x] point_b = self._points[x][self.size - 11 + y] point_a.type = point_b.type = QrPointType.VERSION_PATTERN if version_block_pattern & 1 != 0: point_a.fill = point_b.fill = True version_block_pattern >>= 1 def _add_unused_point(self): point = self._points[self.size - 8][8] point.type = QrPointType.UNUSED point.fill = True def _add_format_pattern(self): for i, bit in enumerate(reversed(self.args.format_pattern_bits)): # top left if i < 6: point_1 = self._points[i][8] elif i < 8: point_1 = self._points[i + 1][8] elif i < 9: point_1 = self._points[i][7] else: point_1 = self._points[8][14 - i] # top right if i < 8: point_2 = self._points[8][self.size - 1 - i] # bottom left else: point_2 = self._points[self.size - 15 + i][8] point_1.type = point_2.type = QrPointType.FORMAT point_1.fill = point_2.fill = bit def _add_empty_data_ec(self): # make data and extra data dbc = self.args.dcwc * _BIT_PER_CW ecbc = self.args.eccwc * _BIT_PER_CW data_points = [QrPoint(False, QrPointType.DATA, offset) for offset in range(dbc)] ec_points = [QrPoint(False, QrPointType.CORRECTION, dbc + offset) for offset in range(ecbc)] # split into blocks data_blocks = [] ec_blocks = [] ecbpb = self.args.eccwcpb * _BIT_PER_CW di = eci = 0 for bi in range(self.args.bc): dbcpb = self.args.dcwcof(bi) * _BIT_PER_CW data_blocks.append(data_points[di:di + dbcpb]) ec_blocks.append(ec_points[eci:eci + ecbpb]) di += dbcpb eci += ecbpb if di != dbc or eci != ecbc: raise QrCanvasException( "Error when split data and ec points to blocks.") # re-sort codewords data_ec_points = [] for cwi in range(self.args.ndcwcpb + 1): # cwi for CodeWord Index for blki in range(self.args.bc): # bi for BLocK Index if cwi * _BIT_PER_CW < len(data_blocks[blki]): bi = cwi * _BIT_PER_CW data_ec_points.extend( data_blocks[blki][bi:bi + _BIT_PER_CW]) for cwi in range(self.args.eccwcpb): for blki in range(self.args.bc): bi = cwi * _BIT_PER_CW data_ec_points.extend(ec_blocks[blki][bi:bi + _BIT_PER_CW]) if len(data_ec_points) != dbc + ecbc: raise QrCanvasException("Error when resort codewords.") # add remain points # value_upper remaining data count is value_upper(0, 3, 4, 7) = 7 for i in range(7): data_ec_points.append(QrPoint(False, QrPointType.EXTRA)) # re place points to canvas ai = 0 # for All Index x = self.size def place_two_column(reverse, now_index): y_list = range(self.size) for y in (y_list if not reverse else reversed(y_list)): if self._points[y][x - 1].type is QrPointType.UNKNOWN: self._points[y][x - 1] = data_ec_points[now_index] now_index += 1 if self._points[y][x - 2].type is QrPointType.UNKNOWN: self._points[y][x - 2] = data_ec_points[now_index] now_index += 1 return now_index while x > 0: ai = place_two_column(True, ai) x -= 2 x = 6 if x == 7 else x ai = place_two_column(False, ai) x -= 2 return data_ec_points def _add_mask(self): for y in range(self.size): for x in range(self.size): point = self._points[y][x] if point.type in {QrPointType.DATA, QrPointType.CORRECTION, QrPointType.EXTRA}: point.invert = self.args.should_invert(y, x) def _add_position_pattern(self, y, x): """ add big position box to pixels array, box pattern like bellow -1 0 1 2 3 4 5 6 7 -- x(i) axis offset -1 . . . . . . . . . 0 . # @ @ @ @ @ @ . 1 . @ . . . . . @ . 2 . @ . @ @ @ . @ . 3 . @ . @ @ @ . @ . 4 . @ . @ @ @ . @ . 5 . @ . . . . . @ . 6 . @ @ @ @ @ @ @ . 7 . . . . . . . . . | y(j) axis offset . for white pixel @ for black pixel # start pixel :param x: left of start pixel :param y: top of start pixel """ # ===== generate inside 7 x 7 box ===== for i in range(7): for j in range(7): # left, right up, bottom fill inside rect point = self._points[x + i][y + j] point.type = QrPointType.POSITION if i in {0, 6} or j in {0, 6} or (2 <= i <= 4 and 2 <= j <= 4): point.fill = True # ===== generate left and right white border ===== for j in range(-1, 8): if self._check_index(y + j): if self._check_index(x - 1): # left self._points[y + j][x - 1].type = QrPointType.POSITION if self._check_index(x + 7): # right self._points[y + j][x + 7].type = QrPointType.POSITION for i in range(-1, 8): if self._check_index(x + i): if self._check_index(y - 1): # top self._points[y - 1][x + i].type = QrPointType.POSITION if self._check_index(y + 7): # bottom self._points[y + 7][x + i].type = QrPointType.POSITION def _add_align_pattern(self, y, x): """ add align box to pixels array, version_pattern_value like bellow 0 1 2 3 4 -- x(i) axis offset 0 # @ @ @ @ 1 @ . . . @ 2 @ . @ . @ 3 @ . . . @ 4 @ @ @ @ @ | y(j) axis offset :param x: left of start pixel :param y: top of start pixel """ for j in range(5): for i in range(5): point = self._points[y + j][x + i] point.type = QrPointType.ALIGNMENT if i in {0, 4} or j in {0, 4} or i == j == 2: point.fill = True def _check_index(self, x): return 0 <= x < self.size def _check_align_box_position(self, y, x): return QrPointType.POSITION not in { self._points[y][x].type, self._points[y][x + 5].type, self._points[y + 5][x].type, self._points[y + 5][x + 5].type } def _rotate(self): if self.args.rotate_func is None: return new = [[None for _ in range(self.size)] for __ in range(self.size)] for y in range(self.size): for x in range(self.size): new_y, new_x = self.args.rotate_func(y, x, self.size) new[new_y][new_x] = self.points[y][x] self._points = new @property def args(self): return self._args @property def size(self): return self._size @property def data_ec_points(self): return self._data_ec_points def load_data(self, bits): data_ec_length = len(self._data_ec_points) - 7 assert bits.length == data_ec_length for _, point in zip(range(data_ec_length), self._data_ec_points): point.fill = bits[point.offset] @property def points(self): return self._points def __str__(self): lines = [] for row in self._points: line = [] for point in row: fill = point.fill if not point.invert else not point.fill line.append(_TYPE_CHAR_MAP[point.type][0 if fill else 1]) lines.append(' '.join(line)) return '\n'.join(lines) ================================================ FILE: pyqart/qr/painter/exception.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : Exception raised when drawing or add data to QrCode. from ..exception import QrException __all__ = ['QrCanvasException', 'QrPainterException'] class QrCanvasException(QrException): pass class QrPainterException(QrException): pass ================================================ FILE: pyqart/qr/painter/painter.py ================================================ # Added at : 2016.7.30 # Author : 7sDream # Usage : A painter that draw paint data to canvas. from random import randint from .canvas import QrCanvas from .exception import QrPainterException from ..args import QrArgs from ..data import QrData from ..ec import RSEncoder from ...common import Bits, BIT_PER_CW __all__ = ['QrPainter'] class QrPainter(object): def __init__(self, data, version=None, mask=None, rotation=0): """ :param QrData|str data: Data will be paint to QrCode. :param int version: Version of QrCode, 1 to 40, None means use the minimum version can encode the data you provided. :param int mask: The mask used in data and ec parts, from 0 to 7, None for random pick. :param int rotation: QrCode rotation direction, 0 for no rotation, 1 for 90 degree clockwise, 2 for 180, 3 for 270. """ assert isinstance(data, (QrData, str)), "Data must be str or QrData." if isinstance(data, str): data = QrData(data) if mask is None: mask = randint(0, 7) self._data = data self._version = version self._mask = mask self._rotation = rotation self._get_and_test_params() def _get_and_test_params(self): min_version, available, used = self._data.version_used_available if self._version is not None and self._version < min_version: raise QrPainterException( "The {} version QrCode does not have enough space to encode " "the data your provided, minimum version is {}.".format( self._version, min_version, )) return min_version, available, used def get_params(self): min_version, available, used = self._get_and_test_params() if self._version is None: version = min_version else: version = self._version args = QrArgs(version, self._data.ec_level) encode_list = [cls(data, args.cci_length_of(cls)) for cls, data in self._data.data_set] used = sum([x.needed_space for x in encode_list]) available = args.dcwc * BIT_PER_CW - used return QrArgs(version, self._data.ec_level, self._mask, self._rotation), available, used @property def data_bits(self): if self._data.size == 0: raise QrPainterException("Unable to paint EMPTY DATA to canvas.") args, available, used = self.get_params() bits = Bits() # add encoded data for cls, data in self._data.data_set: encoding = cls(data, args.cci_length_of(cls)) bits.extend(encoding.output) # ensure encoding process as expect assert bits.length == used bits.pad(available, used) # ensure fill all data zone assert bits.length == args.dcwc * BIT_PER_CW return bits @property def bits(self): """ :rtype: Bits :raise: QrEncodingException: When encoding process error. """ bits = self.data_bits args, _, _ = self.get_params() ec_bits = Bits() ec_length = args.eccwcpb di = 0 for bi in range(args.bc): dcwc = args.dcwcof(bi) dbcob = dcwc * BIT_PER_CW block_data_bits = Bits.copy_from(bits, di, dbcob) ec_bits.extend(RSEncoder.encode(block_data_bits, ec_length)) di += dbcob bits.extend(ec_bits) return bits @property def canvas(self): args, _, _ = self.get_params() canvas = QrCanvas(args) return canvas @property def as_bool_matrix(self): res = [] canvas = self.canvas canvas.load_data(self.bits) for row in canvas.points: line = [] for point in row: line.append(point.fill if not point.invert else not point.fill) res.append(line) return res ================================================ FILE: pyqart/qr/painter/point.py ================================================ # Added at : 2016.7.29 # Author : 7sDream # Usage : QrPoint represent a point of QrCode, can be 9 type(except UNKNOWN). # Enum class QrPointType used to stand for those type. # The UNKNOWN type only be used to init the point which means # the type of point is temporarily unknown. from enum import Enum, unique __all__ = ['QrPointType', 'QrPoint'] @unique class QrPointType(Enum): UNKNOWN = 0 POSITION = 1 ALIGNMENT = 2 TIMING = 3 FORMAT = 4 VERSION_PATTERN = 5 UNUSED = 6 DATA = 7 CORRECTION = 8 EXTRA = 9 class QrPoint(object): def __init__(self, fill, type_=QrPointType.UNKNOWN, offset=-1, invert=False): self.fill = fill self.type = type_ self.offset = offset self.invert = invert ================================================ FILE: pyqart/qr/printer/__init__.py ================================================ from .base import QrBasePrinter from .image_printer import QrImagePrinter from .string_printer import QrStringPrinter from .halftone_printer import QrHalftonePrinter ================================================ FILE: pyqart/qr/printer/base.py ================================================ # Added at : 2016.7.31 # Author : 7sDream # Usage : Base printer to visualize QrCode, define the function should be # implement by subclasses. import abc from ..painter import QrPainter from ..data import QrData class QrBasePrinter(object): def __init__(self): pass @classmethod def _create_painter(cls, obj): assert isinstance( obj, (QrPainter, QrData, str, bytes, bytearray) ), "Argument must be QrPainter, QrData, str, bytes or bytearray" if isinstance(obj, QrData): obj = QrPainter(obj) elif isinstance(obj, str): obj = QrPainter(QrData(obj)) elif isinstance(obj, (bytes, bytearray)): data = QrData() obj = data.put_bytes(obj) obj = QrPainter(obj) return obj @abc.abstractmethod def print(self, *args, **kwargs): pass ================================================ FILE: pyqart/qr/printer/halftone_printer.py ================================================ from __future__ import division import PIL.ImageDraw as Draw import PIL.Image as Image from .image_printer import QrImagePrinter from ..painter.point import QrPointType from ...art.qart import QArtist class QrHalftonePrinter(QrImagePrinter): @classmethod def print(cls, obj, path=None, point_width=None, border_width=None, f_color=None, bg_color=None, file_format=None, img=None, colorful=True, pixelization=False): super_class = super(QrHalftonePrinter, cls) point_width, border_width = super_class._calc_point_border_width( point_width, border_width) if not colorful: bg_color = (255, 255, 255) f_color = (0, 0, 0) point_width = int(((point_width - 1) // 3 + 1) * 3) mask_block_width = point_width // 3 painter = cls._create_painter(obj) canvas = painter.canvas if isinstance(painter, QArtist) and img is None: img = painter.source.to_image( canvas.args, painter.dither, painter.dy, painter.dx ) pass_path = path if img is None else None qr = super_class.print( painter, pass_path, point_width, border_width, f_color, bg_color, file_format, ) qr_size = qr.size[0] - 2 * border_width if img is not None: if not isinstance(painter, QArtist): if not isinstance(img, Image.Image): img = Image.open(str(img)) if not colorful: if pixelization: img = img.convert('L') else: img = img.convert('1') if pixelization: img = img.resize((canvas.size, canvas.size)) else: return qr img = img.resize((qr_size, qr_size)) x = y = 0 mask = Image.new('1', (qr_size, qr_size), 0) drawer = Draw.Draw(mask, '1') for line in canvas.points: for point in line: if point.type in {QrPointType.DATA, QrPointType.CORRECTION}: drawer.rectangle( [x, y, x + point_width - 1, y + point_width - 1], fill=1, outline=1, ) drawer.rectangle( [ x + mask_block_width, y + mask_block_width, x + 2 * mask_block_width - 1, y + 2 * mask_block_width - 1, ], fill=0, outline=0 ) x += point_width x, y = 0, y + point_width # uncomment the following code to see mask image # mask.save(path + '.mask.bmp', format='bmp') qr.paste(img, (border_width, border_width), mask) if path is not None: qr.save(path, format=file_format) return None return qr ================================================ FILE: pyqart/qr/printer/image_printer.py ================================================ # Added at : 2016.7.31 # Author : 7sDream # Usage : A printer that print QrCode to a image. from .base import QrBasePrinter from ..data import QrData import PIL.Image as Image import PIL.ImageDraw as Draw class QrImagePrinter(QrBasePrinter): @classmethod def _calc_point_border_width(cls, point_width, border_width): point_width = int(point_width) if point_width is not None else 1 border_width = point_width if border_width is None else border_width border_width = max(1, border_width) return point_width, border_width @classmethod def print(cls, obj, path=None, point_width=None, border_width=None, f_color=None, bg_color=None, file_format=None): """ Print the QrCode to a image. :param QrPainter|QrData|str|bytes|bytearray obj: The painter that want print his/her QrCode, or a raw QrData object which contains data, or just a string or bytes(bytearray) which will used as data. :param str path: If provided, will auto save file to the path. :param int point_width: Width and Height of code part. None will be 1 pixel per point. :param border_width: Border width, None will be code width / 20. :param (int, int, int) f_color: Front color, Default is black. :param (int, int, int) bg_color: Background color, Default is white. :param str file_format: Image suffix, like png, jpeg, bmp, etc. :return: Bytes data of image **Only when file path is not provided**. :rtype: PIL.Image """ point_width, border_width = cls._calc_point_border_width( point_width, border_width) obj = cls._create_painter(obj) matrix = obj.as_bool_matrix size = len(matrix) code_width = size * point_width img_size = code_width + 2 * border_width f_color = (0, 0, 0) if f_color is None else f_color bg_color = (255, 255, 255) if bg_color is None else bg_color qr_img = Image.new('RGB', (size, size), bg_color) drawer = Draw.Draw(qr_img) fill_points = [] for y in range(size): for x in range(size): if matrix[y][x]: fill_points.append((x, y)) drawer.point(fill_points, f_color) del drawer qr_img = qr_img.resize((code_width, code_width)) img = Image.new('RGB', (img_size, img_size), bg_color) img.paste(qr_img, (border_width, border_width)) if path is not None: img.save(path, format=file_format) return None return img ================================================ FILE: pyqart/qr/printer/string_printer.py ================================================ # Added at : 2016.8.1 # Author : 7sDream # Usage : A printer that print QrCode to a string, can be show in shell. from .base import QrBasePrinter WHITE_ALL = '\u2588' WHITE_BLACK = '\u2580' BLACK_WHITE = '\u2584' BLACK_ALL = ' ' MAP = { (True, True): BLACK_ALL, (True, False): BLACK_WHITE, (False, True): WHITE_BLACK, (False, False): WHITE_ALL, } class QrStringPrinter(QrBasePrinter): @classmethod def print(cls, obj, print_out=True, *args, **kwargs): """ :param obj: See :any:`QrImagePrinter` :param bool print_out: Whether to print QrCode out. :return: The string that can be print out like a QrCode. :type: string """ painter = cls._create_painter(obj) matrix = painter.as_bool_matrix matrix = [[False] + x + [False] for x in matrix] size = len(matrix) + 2 matrix.insert(0, [False] * size) matrix.append([False] * size) matrix.append([True] * size) lines = [] for row in range(0, size, 2): line = [] for col in range(0, size): line.append(MAP[(matrix[row][col], matrix[row + 1][col])]) lines.append(''.join(line)) string = '\n'.join(lines) if print_out: print(string) return string ================================================ FILE: pyqart/qr_entry.py ================================================ import argparse import sys from pyqart.qr.data import QrData from pyqart.qr.painter import QrPainter from pyqart.qr.printer import QrImagePrinter, QrStringPrinter def main(): parser = argparse.ArgumentParser( prog="pyqr", description="A program of generate QrCode.", epilog="Writen by 7sDream. (https://github.com/7sDream/pyqart)", ) parser.add_argument( 'string', type=str, help="string will be encode" ) parser.add_argument( '-v', '--version', type=int, help="version of QrCode, 1 to 40, " "will auto calculated from data length if not provide" ) parser.add_argument( '-l', '--level', type=int, default=0, help="QrCode error correction level, 0 to 3, default is 0" ) parser.add_argument( '-m', '--mask', type=int, help="mask of QrCode, 0 to 7, default is random value" ) parser.add_argument( '-r', '--rotation', type=int, default=0, help="rotate the QrCode(clockwise), " "0 for no rotation, 1 for 90 degree, 2 for 180, 3 for 270" ) parser.add_argument( '-p', '--point-size', type=int, default=3, help="the point width and height of one QrCode point," " by pixel, default is 3" ) parser.add_argument( '-b', '--board', type=int, help="board wide by pixel, will auto calculated " "from code size if not provide", ) parser.add_argument( '-c', '--color', type=int, nargs=3, metavar=('R', 'G', 'B'), help="front color of QrCode, 3 number as rgb color", ) parser.add_argument( '-g', '--background-color', type=int, nargs=3, metavar=('R', 'G', 'B'), help="background color of QrCode, 3 number as rgb color", ) parser.add_argument( '-o', '--output', type=str, help="output file path, code will print to terminal if not provide, " "and other arguments will be ignored" ) argv = sys.argv[1:] args = parser.parse_args(argv) data = QrData(args.string, args.level) painter = QrPainter(data, args.version, args.mask, args.rotation) if args.color is not None: args.color = tuple(args.color) if args.background_color is not None: args.background_color = tuple(args.background_color) if args.output is not None: QrImagePrinter.print( painter, args.output, args.point_size, args.board, args.color, args.background_color ) else: QrStringPrinter.print(painter, True) if __name__ == '__main__': main() ================================================ FILE: setup.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from setuptools import setup, find_packages import pyqart packages = find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]) setup( name='pyqart', keywords=['qrcode', 'qart'], version=pyqart.__version__, description='QArt Python implementation, ' 'see http://research.swtch.com/qart for details.', author='7sDream', author_email='7seconddream@gmail.com', license='MIT', url='https://github.com/7sDream/pyqart', install_requires=['pillow'], packages=packages, classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Multimedia :: Graphics', 'Topic :: Software Development :: Libraries :: Python Modules', ], entry_points={ 'console_scripts': [ 'pyqr = pyqart.qr_entry:main', 'pyqart = pyqart.qart_entry:main', ] } ) ================================================ FILE: test/__init__.py ================================================ ================================================ FILE: test/test_alphanumeric.py ================================================ import unittest from pyqart.qr.data.alphanumeric import AlphaNumeric class TestAlphaNumeric(unittest.TestCase): def test_alphanumeric_odd(self): an = AlphaNumeric('AC-42', 9) self.assertEqual( an.output.as_string, '0010' + '000000101' + '00111001110' + '11100111001' + '000010' ) def test_alphanumeric_even(self): an = AlphaNumeric('7S DREAM', 9) self.assertEqual( an.output.as_string, '0010' + '000001000' + '00101010111' + '11001100001' + '10011001101' + '00111011000' ) ================================================ FILE: test/test_bits.py ================================================ import unittest from pyqart.common.bits import Bits class TestBits(unittest.TestCase): def test_bits_as_string_when_no_data(self): b = Bits() self.assertEqual(b.as_string, '') def test_bits_as_string_when_has_data(self): b = Bits() b.append(0b11010011, 8) self.assertEqual(b.as_string, '11010011') b.append(0b1000, 4) self.assertEqual(b.as_string, '110100111000') def test_bits_as_int_when_no_data(self): b = Bits() self.assertEqual(b.as_int, -1) def test_bits_as_int_when_less_than_a_byte(self): b = Bits() b.append(1, 1) self.assertEqual(b.as_int, 1) def test_bits_as_int_when_between_one_and_two_byte(self): b = Bits() b.append(0b111100111, 9) self.assertEqual(b.as_int, 0b111100111) def test_bits_append_bit(self): b = Bits() b.append_bit(True) b.append_bit(True) b.append_bit(False) b.append_bit(False) self.assertEqual(b.as_int, 0b1100) b.append_bit(True) b.append_bit(False) b.append_bit(False) b.append_bit(False) self.assertEqual(b.as_int, 0b11001000) def test_bits_append(self): b = Bits() b.append(0xAC, 8) self.assertEqual(b.as_int, 0xAC) b.append(0xF, 4) self.assertEqual(b.as_int, 0x0ACF) def test_bits_extend_all(self): b = Bits() b.extend(b'\xAC') self.assertEqual(b.as_int, 0xAC) b.extend(bytearray(b'\x1F')) self.assertEqual(b.as_int, 0x0AC1F) def test_bits_extend_other_bits_all(self): b = Bits() b.extend(b'\xAC') other_bits = Bits.copy_from(b) b.extend(other_bits) self.assertEqual(b.as_int, 0xACAC) def test_bits_extend_other_bytes_0_to_not_end(self): b = Bits() b.extend(b'\x0F', count=6) self.assertEqual(b.as_int, 3) def test_bits_extend_other_bytes_not_start_to_end(self): b = Bits() b.extend(b'\x0F', 4) self.assertEqual(b.as_int, 15) def test_bits_extend_other_bytes_not_start_to_not_end(self): b = Bits() b.extend(b'\x0F', 4, 2) self.assertEqual(b.as_int, 3) def test_bits_xor(self): b = Bits(0b001010001, 9) o = Bits(0b0010110, 7) b.xor(o, 2, 3, 3) self.assertEqual(b.as_string, '001100001') b = Bits(0b001010001, 9) o = Bits(0b0010110, 7) b.xor(o, 2, 3) self.assertEqual(b.as_string, '001100001') ================================================ FILE: test/test_bits_utils.py ================================================ import unittest from pyqart.common.bit_funcs import one_at, zero_at, set_bit, bit_at class TestBitUtils(unittest.TestCase): def test_one_at_normal(self): self.assertEqual(one_at(0), 0b10000000) self.assertEqual(one_at(3), 0b00010000) self.assertEqual(one_at(4), 0b00001000) self.assertEqual(one_at(7), 0b00000001) self.assertEqual(one_at(0, 12), 0b100000000000) self.assertEqual(one_at(5, 10), 0b0000010000) self.assertEqual(one_at(8, 10), 0b0000000010) def test_one_at_fail(self): with self.assertRaises(AssertionError): one_at(-1) with self.assertRaises(AssertionError): one_at(-2) with self.assertRaises(AssertionError): one_at(8) with self.assertRaises(AssertionError): one_at(10, 8) with self.assertRaises(AssertionError): one_at(10, 9) def test_zero_at_normal(self): self.assertEqual(zero_at(0), 0b01111111) self.assertEqual(zero_at(3), 0b11101111) self.assertEqual(zero_at(4), 0b11110111) self.assertEqual(zero_at(7), 0b11111110) self.assertEqual(zero_at(10, 12), 0b111111111101) self.assertEqual(zero_at(0, 12), 0b011111111111) self.assertEqual(zero_at(7, 8), 0b11111110) def test_zero_at_fail(self): with self.assertRaises(AssertionError): zero_at(-1) with self.assertRaises(AssertionError): zero_at(-2) with self.assertRaises(AssertionError): zero_at(8) with self.assertRaises(AssertionError): zero_at(10) with self.assertRaises(AssertionError): zero_at(10, 9) with self.assertRaises(AssertionError): zero_at(10, 10) def test_set_bit_normal(self): self.assertEqual(set_bit(0b01001010, 2, True), 0b01101010) self.assertEqual(set_bit(0b01001010, 6, False), 0b01001000) def test_set_bit_fail(self): with self.assertRaises(AssertionError): set_bit(0, -1, True) with self.assertRaises(AssertionError): set_bit(0, -2, True) with self.assertRaises(AssertionError): set_bit(0, 8, False) with self.assertRaises(AssertionError): set_bit(0, 9, False) def test_bit_at_normal(self): self.assertEqual(bit_at(0b01100, 5, 0), False) self.assertEqual(bit_at(0b01100, 5, 1), True) self.assertEqual(bit_at(0b101101, 6, 1), False) self.assertEqual(bit_at(0b101101, 6, 5), True) self.assertEqual(bit_at(0b11110111, 8, 4), False) self.assertEqual(bit_at(0b11110111, 8, 7), True) def test_bit_at_fail(self): # value_upper = 0 with self.assertRaises(AssertionError): self.assertEqual(bit_at(0, 0, 0), False) # value_upper < 0 with self.assertRaises(AssertionError): self.assertEqual(bit_at(0, -1, 0), False) with self.assertRaises(AssertionError): self.assertEqual(bit_at(0, -5, 0), False) ================================================ FILE: test/test_encode_numbers.py ================================================ import unittest from pyqart.qr.data.numbers import Numbers class TestNumbers(unittest.TestCase): def test_numbers_length_mod_3_is_0(self): numbers = Numbers('923576', 10) self.assertEqual( numbers.output.as_string, '0001' + '0000000110' + '1110011011' + '1001000000', ) def test_numbers_length_mod_3_is_1(self): numbers = Numbers('0123456789012345', 10) self.assertEqual( numbers.output.as_string, '0001' + '0000010000' + '0000001100' + '0101011001' + '1010100110' + '1110000101' + '0011101010' + '0101', ) def test_numbers_length_mod_3_is_2(self): numbers = Numbers('01234567', 10) self.assertEqual( numbers.output.as_string, '0001' + '0000001000' + '000000110001010110011000011', ) ================================================ FILE: test/test_encode_raw.py ================================================ import unittest from pyqart.qr.data.raw import Raw class TestRaw(unittest.TestCase): def test_raw(self): raw = Raw(b'Hello, world!', 8) self.assertEqual( raw.output.as_string, '0100' + '00001101' + '01001000' + '01100101' + '01101100' + '01101100' + '01101111' + '00101100' + '00100000' + '01110111' + '01101111' + '01110010' + '01101100' + '01100100' + '00100001', )