mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-15 23:00:15 -07:00
A new start!
This commit is contained in:
parent
01eb4c6153
commit
1de73c79d9
166
.gitignore
vendored
166
.gitignore
vendored
@ -1,140 +1,28 @@
|
||||
|
||||
bleak
|
||||
www/skin.css
|
||||
www/fabric.js
|
||||
www/fabric.min.js
|
||||
www/html2canvas.js
|
||||
www/html2canvas.min.js
|
||||
www/qrcode.js
|
||||
www/qrcode.min.js
|
||||
# python cache
|
||||
__pycache__
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# 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/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# Compatibility version of script, for old-old webView,
|
||||
# generated by typescript tsc
|
||||
www/main.comp.js
|
||||
# https://github.com/roddeh/i18njs
|
||||
www/i18n.js
|
||||
www/i18n.d.ts
|
||||
# https://www.npmjs.com/package/vconsole
|
||||
www/vconsole.js
|
||||
# https://github.com/delight-im/Android-AdvancedWebView
|
||||
build-android/advancedwebview
|
||||
# python bytecode
|
||||
*.pyc
|
||||
# releases
|
||||
build-android/dist
|
||||
*.apk
|
||||
cat-printer*.zip
|
||||
# bleak, the bare pip package as a folder
|
||||
build-common/bleak
|
||||
# python embeddable package, with bleak_winrt inside
|
||||
build-common/python-win32*
|
||||
# dev config
|
||||
config.json
|
||||
# some other junk
|
||||
.directory
|
||||
thumbs.db
|
||||
thumbs.db:encryptable
|
||||
|
30
.pylintrc
Normal file
30
.pylintrc
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
# Apply this pylint-rc for better experience
|
||||
# Configurable in VSCode settings `python.linting.pylintArgs`
|
||||
# $ pylint --rcfile=.pylint-rc
|
||||
|
||||
[MASTER]
|
||||
jobs=4
|
||||
|
||||
[BASIC]
|
||||
class-const-naming-style=snake_case
|
||||
const-naming-style=snake_case
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=broad-except,
|
||||
global-statement,
|
||||
fixme,
|
||||
import-outside-toplevel
|
||||
|
||||
[BASIC]
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_,
|
||||
e,
|
||||
do_GET,
|
||||
do_POST,
|
||||
do_HEAD,
|
||||
do_PUT
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Web Interface",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "server.py",
|
||||
"args": ["-a", "-s"],
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
4
0-transpile.sh
Executable file
4
0-transpile.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
cd www
|
||||
npx tsc --allowJs --outFile main.comp.js polyfill.js i18n.js image.js main.js
|
||||
cd ..
|
6
COPYING
Normal file
6
COPYING
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
118
README.md
118
README.md
@ -1,85 +1,107 @@
|
||||
English | [简体中文](README.zh-CN.md)
|
||||
|
||||
# Cat-Printer
|
||||
|
||||
*A friendly cat (kitty) printer App/driver for everyone (GB01,GB02,GT01)*
|
||||
A project that provides support to some Bluetooth "Cat Printer" models, on *many* platforms!
|
||||
|
||||

|
||||
## Models
|
||||
|
||||
(According to [official website](http://office.frogtosea.com/jjfa), maybe there are also normal-, piggy- and frog-shaped printers with these models)
|
||||
Currently:
|
||||
GB01, GB02, and GT01
|
||||
|
||||
## Features
|
||||
|
||||
- Print jpg/png images directly to cat printer from a web interface
|
||||
- Print a document (.doc, .docx, .odt etc) by copy-paste
|
||||
- Custom print content, put text, image, QRcode on a canvas
|
||||
- (more will be here...)
|
||||
- Simple!
|
||||
- Operate via a Web UI just in browser,
|
||||
- or get the Android release!
|
||||
- ~~Feature-rich~~
|
||||
- Currently it is in Alpha stage. More will be there soon!
|
||||
- You can still use the legacy version (0.0.2), with some more editing features
|
||||
- Friendly!
|
||||
- Language support! You can participate in translation!
|
||||
- Good user interface, with PC/mobile/light/dark mode variants! (system config adaptive)
|
||||
- Cross platform!
|
||||
- Newer Windows 10 and above
|
||||
- GNU/Linux
|
||||
- MacOS *(Needs testing)*
|
||||
- and a lot of extra efforts for Android!
|
||||
- Free, as in [freedom](https://www.gnu.org/philosophy/free-sw.html)!
|
||||
- Unlike the "official" proprietary app,
|
||||
this project is for everyone that concerns *open-mind and freedom*!
|
||||
- and Fun!
|
||||
- Do whatever you like!
|
||||
|
||||
## How to use
|
||||
## Get Started
|
||||
|
||||
On Windows 10:
|
||||
### Android
|
||||
|
||||
- Get a release, extract, open `start.bat`.
|
||||
- Make sure bluetooth of your computer is opened and cat printer is launched.
|
||||
Get the newest apk release and install, then well done!
|
||||
|
||||
On GNU/Linux:
|
||||
It may ask for background location permission, which is mysterious to me.
|
||||
You can deny it safely.
|
||||
|
||||
- You can also use a Windows release, or prepare dependencies according to developer note.
|
||||
- Open `server.py` in `printer` folder with `python3`.
|
||||
### Windows:
|
||||
|
||||
Notes:
|
||||
Get the newest release archive with "windows" in the file name,
|
||||
extract to somewhere and run `start.bat`
|
||||
|
||||
- Newest Firefox users need to manually allow the permission of extracting canvas data, at left side of address bar after clicking preview button
|
||||
- Windows version needs to be at least 10 (`10.0.16299`)
|
||||
- GNU/Linux needs BlueZ (`bluetoothctl`)
|
||||
- Maybe also compatible to Mac (Darwin) with CoreBluetooth Framework
|
||||
### GNU/Linux
|
||||
|
||||
## Why?
|
||||
You can get the "pure" release, extract it, fire a terminal inside and run:
|
||||
```bash
|
||||
python3 server.py
|
||||
```
|
||||
|
||||
These bluetooth cat printers, with model name GB01, GB02 and GT01, have poor support at applications.
|
||||
On Arch Linux based distros you may first install `bluez`, as it's often missing
|
||||
```bash
|
||||
sudo pacman -S bluez bluez-utils
|
||||
```
|
||||
|
||||
Official apps are, proprietary, also have only mobile version.
|
||||
### MacOS
|
||||
|
||||
I hate both proprietary and platform-binding things. So I decided to make this.
|
||||
For MacOS please install [Python 3](https://www.python.org/).
|
||||
|
||||
Thankfully, people here are really warm-hearted, logged their experiences online in a [central repo](https://github.com/JJJollyjim/catprinter), and I am able to walk further 😃
|
||||
Fetch a "pure" release and do the same in a shell:
|
||||
```bash
|
||||
python3 server.py
|
||||
```
|
||||
|
||||
## Trivial
|
||||
Currently in Mac the browser will not pop up automatically. Please run manually and go to `http://127.0.0.1:8095`
|
||||
|
||||
- Many one choose these cat thermal printers because they are cute... or, just cheap 🙃
|
||||
|
||||
- Here we tell "**Cat Printer**" because other developers also call the printer as this, but what oversea shops call is "**Kitty Printer**". Search engines, please optimize it 😝
|
||||
### Note
|
||||
|
||||
- The official app is protected by law & copyright. I don't know if my work is not good...
|
||||
For all supported platforms,
|
||||
You can also use "pure" edition once you have [Python 3](https://www.python.org/) installed,
|
||||
or "bare" edition if you also managed to install `bleak` via `pip`.
|
||||
|
||||
## Developer Note
|
||||
See the [releases](./releases) now! (`0.0.*` versions are legacy/deprecated)
|
||||
|
||||
This application uses server/client model, and have fewest possible dependencies on server side.
|
||||
## Problems?
|
||||
|
||||
### Prepare
|
||||
Please open an issue if there's something in your mind!
|
||||
|
||||
- Python3 & Browser
|
||||
- [fabric.min.js](https://github.com/fabricjs/fabric.js/tree/master/dist)
|
||||
- [html2canvas.min.js](https://html2canvas.hertzen.com/)
|
||||
- [qrcode.min.js](https://davidshimjs.github.io/qrcodejs/)
|
||||
- (Optional) Any css for plain webpage, e.g. [minicss](https://minicss.org/), rename to `skin.css`
|
||||
Of course PRs are welcome if you can handle them!
|
||||
|
||||
Put any web-related files to folder `www`.
|
||||
## License
|
||||
|
||||
### Supported Platforms
|
||||
Copyright © 2022 NaitLee Soft. Some rights reserved.
|
||||
|
||||
Support for both Windows and GNU/Linux are included. And Windows release package will contain all needed things for a **normal** user to play with.
|
||||
See file `COPYING`, `LICENSE`, and detail of used JavaScript in file `www/jslicense.html`
|
||||
|
||||
### Plans
|
||||
--------
|
||||
|
||||
- Smoother mono-color converting
|
||||
- Make remote-print by web interface more standard/compatible/secure
|
||||
## Development
|
||||
|
||||
Possible features:
|
||||
You may interested in language support, anyway. See the translation files in directory `www/lang`!
|
||||
|
||||
- Remote print with printer protocols
|
||||
Also interested in code development? See [development.md](development.md)!
|
||||
|
||||
### Files
|
||||
### Credits
|
||||
|
||||
- `server.py`: Contains a BaseHTTP server that hooks user actions and printer driver
|
||||
- `printer.py`: Contains the driver of bluetooth cat printer, which depends on bleak. You can also run this file in commandline.
|
||||
- Of course, Python & the Web!
|
||||
- [Bleak](https://bleak.readthedocs.io/en/latest/) BLE lib! The overall Hero!
|
||||
- [roddeh-i18n](https://github.com/roddeh/i18njs), good work!
|
||||
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles
|
||||
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) for saving my life from Java
|
||||
- Stack Overflow & the whole Internet, you let me know Android `Activity` all from empty
|
||||
- ... Everyone is Awesome!
|
||||
|
@ -1,85 +0,0 @@
|
||||
[English](README.md) | 简体中文
|
||||
|
||||
# 猫咪打印机 Cat-Printer
|
||||
|
||||
*一个友好的猫咪打印机 App/驱动,为用户而生 (GB01,GB02,GT01)*
|
||||
|
||||

|
||||
|
||||
(根据[官网](http://office.frogtosea.com/jjfa),可能也有普通/猪猪/青蛙外观的打印机拥有此种型号)
|
||||
|
||||
## 功能
|
||||
|
||||
- 直接从网页界面打印 jpg/png 图像到猫咪打印机
|
||||
- 复制粘贴文档内容(.doc, .docx, .odt 等)以打印
|
||||
- 自定义打印内容,在画布上放置文字、图片、二维码
|
||||
- (会有更多……)
|
||||
|
||||
## 如何使用
|
||||
|
||||
在 Windows 10 上:
|
||||
|
||||
- 获取一份 release,解压,打开 `start.bat`
|
||||
- 确保电脑蓝牙开启且猫咪打印机已启动。
|
||||
|
||||
在 GNU/Linux:
|
||||
|
||||
- 您也可以使用 Windows release,或者依开发者注记准备依赖。
|
||||
- 使用 `python3` 打开位于 `printer` 文件夹的 `server.py`。
|
||||
|
||||
备注:
|
||||
|
||||
- 最新的 Firefox 用户需要手动允许提取画布信息的权限(点击预览后,在地址栏左方)
|
||||
- Windows 版本至少为 10 (`10.0.16299`)
|
||||
- GNU/Linux 需要 BlueZ (`bluetoothctl`)
|
||||
- 可能也兼容有 CoreBluetooth 框架的 Mac (Darwin)
|
||||
|
||||
## 为什么?
|
||||
|
||||
这些蓝牙猫咪打印机,型号为 GB01, GB02 和 GT01,没有足够的应用支持。
|
||||
|
||||
官方应用是专有的,且只有手机版本。
|
||||
|
||||
我讨厌专有软件和平台绑架。所以我做了这个。
|
||||
|
||||
幸运的是,这里的热心肠网友将他们的经验记录到了一个[中心仓库](https://github.com/JJJollyjim/catprinter),因此我可以走得更远 😃
|
||||
|
||||
## 花絮
|
||||
|
||||
- 很多人选择此种热敏打印机,因为它可爱……或者,只是便宜 🙃
|
||||
|
||||
- 其他开发者叫它“猫打印机”("Cat Printer"),但国外商店叫它“猫咪打印机”("Kitty Printer")。在英文文档有差别,提出以优化搜索引擎结果 😝
|
||||
|
||||
- 官方 App 受法律、版权、专利保护。不知道此 repo 是否合理……
|
||||
|
||||
## 开发者注记
|
||||
|
||||
此 App 使用服务器/客户端模型,且拥有尽可能少的服务端依赖。
|
||||
|
||||
### 准备
|
||||
|
||||
- Python3 与浏览器
|
||||
- [fabric.min.js](https://github.com/fabricjs/fabric.js/tree/master/dist)
|
||||
- [html2canvas.min.js](https://html2canvas.hertzen.com/)
|
||||
- [qrcode.min.js](https://davidshimjs.github.io/qrcodejs/)
|
||||
- (可选)任何纯网页可用的 css,如 [minicss](https://minicss.org/),重命名为 `skin.css`
|
||||
|
||||
将 web 相关的文件放在 `www` 文件夹中。
|
||||
|
||||
### 支持的平台
|
||||
|
||||
它同时包含对 Windows 和 GNU/Linux 的支持。Windows 发行包将包含一个**普通**用户所需要的所有。
|
||||
|
||||
### 计划
|
||||
|
||||
- 更好的双色转换
|
||||
- 使 web 界面的远程打印标准化/兼容/安全
|
||||
|
||||
可能的功能:
|
||||
|
||||
- 使用打印协议的远程打印
|
||||
|
||||
## 文件
|
||||
|
||||
- `server.py`: 包含一个 BaseHTTP 服务器,关联用户操作与打印机驱动
|
||||
- `printer.py`: 包含蓝牙猫咪打印机的驱动,依赖 bleak。您也可以在命令行中运行此文件。
|
9
TODO
Normal file
9
TODO
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
+ Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
||||
+ More frontend usability, more functions
|
||||
+ A better layout for mobile?
|
||||
+ Better Canvas mode, (re-)consider fabric.js
|
||||
+ Implement Document mode, (re-)consider html2canvas.js
|
||||
+ Write good help/manual
|
||||
+ Make error notice short while let users see detailed help/manual for what-to-do
|
||||
+ ...
|
7
build-android/0-build-android.sh
Executable file
7
build-android/0-build-android.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
||||
--icon=icon.png --version="0.1.0" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
|
||||
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
|
||||
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
||||
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
||||
--permission=BLUETOOTH_ADMIN --permission=ACCESS_FINE_LOCATION --permission=ACCESS_COARSE_LOCATION
|
2
build-android/1-adb-install.sh
Executable file
2
build-android/1-adb-install.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
adb install cat-printer*.apk
|
2
build-android/2-clean-up-build.sh
Executable file
2
build-android/2-clean-up-build.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
p4a clean_builds && p4a clean_dists
|
10
build-android/3-formal-build.sh
Executable file
10
build-android/3-formal-build.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
rm -rf "dist"
|
||||
unzip -q "../cat-printer-bare-$1.zip"
|
||||
mv "cat-printer" "dist"
|
||||
p4a apk --private "dist" --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
||||
--icon=icon.png --version="$1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
|
||||
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a \
|
||||
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
||||
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
||||
--permission=BLUETOOTH_ADMIN --permission=ACCESS_FINE_LOCATION --permission=ACCESS_COARSE_LOCATION
|
3
build-android/8-build-push.sh
Executable file
3
build-android/8-build-push.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
./0-build-android.sh
|
||||
./1-adb-install.sh
|
2
build-android/9-adb-logcat.sh
Executable file
2
build-android/9-adb-logcat.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
adb logcat | grep -E 'python|chromium'
|
111
build-android/blacklist.txt
Normal file
111
build-android/blacklist.txt
Normal file
@ -0,0 +1,111 @@
|
||||
|
||||
# dev
|
||||
.vscode
|
||||
.git
|
||||
.gitignore
|
||||
.pylintrc
|
||||
?-*.sh
|
||||
|
||||
# junk
|
||||
__pycache__
|
||||
.directory
|
||||
thumbs.db
|
||||
thumbs.db:encryptable
|
||||
|
||||
# other dist
|
||||
cat-printer-windows-*.zip
|
||||
cat-printer-pure-*.zip
|
||||
cat-printer-bare-*.zip
|
||||
build*/*
|
||||
|
||||
# prevent user to include invalid extensions
|
||||
*.apk
|
||||
*.aab
|
||||
*.apks
|
||||
*.pxd
|
||||
|
||||
# eggs
|
||||
*.egg-info
|
||||
|
||||
# unit test
|
||||
unittest/*
|
||||
|
||||
# python config
|
||||
config/makesetup
|
||||
|
||||
# unused kivy files (platform specific)
|
||||
kivy/input/providers/wm_*
|
||||
kivy/input/providers/mactouch*
|
||||
kivy/input/providers/probesysfs*
|
||||
kivy/input/providers/mtdev*
|
||||
kivy/input/providers/hidinput*
|
||||
kivy/core/camera/camera_videocapture*
|
||||
kivy/core/spelling/*osx*
|
||||
kivy/core/video/video_pyglet*
|
||||
kivy/tools
|
||||
kivy/tests/*
|
||||
kivy/*/*.h
|
||||
kivy/*/*.pxi
|
||||
|
||||
# unused encodings
|
||||
lib-dynload/*codec*
|
||||
encodings/cp*.pyo
|
||||
encodings/tis*
|
||||
encodings/shift*
|
||||
encodings/bz2*
|
||||
encodings/iso*
|
||||
encodings/undefined*
|
||||
encodings/johab*
|
||||
encodings/p*
|
||||
encodings/m*
|
||||
encodings/euc*
|
||||
encodings/k*
|
||||
encodings/unicode_internal*
|
||||
encodings/quo*
|
||||
encodings/gb*
|
||||
encodings/big5*
|
||||
encodings/hp*
|
||||
encodings/hz*
|
||||
|
||||
# unused python modules
|
||||
bsddb/*
|
||||
wsgiref/*
|
||||
hotshot/*
|
||||
pydoc_data/*
|
||||
tty.pyo
|
||||
anydbm.pyo
|
||||
nturl2path.pyo
|
||||
LICENCE.txt
|
||||
macurl2path.pyo
|
||||
dummy_threading.pyo
|
||||
audiodev.pyo
|
||||
antigravity.pyo
|
||||
dumbdbm.pyo
|
||||
sndhdr.pyo
|
||||
__phello__.foo.pyo
|
||||
sunaudio.pyo
|
||||
os2emxpath.pyo
|
||||
multiprocessing/dummy*
|
||||
|
||||
# unused binaries python modules
|
||||
lib-dynload/termios.so
|
||||
lib-dynload/_lsprof.so
|
||||
lib-dynload/*audioop.so
|
||||
lib-dynload/_hotshot.so
|
||||
lib-dynload/_heapq.so
|
||||
lib-dynload/_json.so
|
||||
lib-dynload/grp.so
|
||||
lib-dynload/resource.so
|
||||
lib-dynload/pyexpat.so
|
||||
lib-dynload/_ctypes_test.so
|
||||
lib-dynload/_testcapi.so
|
||||
|
||||
# odd files
|
||||
plat-linux3/regen
|
||||
|
||||
#>sqlite3
|
||||
# conditionnal include depending if some recipes are included or not.
|
||||
sqlite3/*
|
||||
lib-dynload/_sqlite3.so
|
||||
#<sqlite3
|
||||
|
BIN
build-android/blank.png
Normal file
BIN
build-android/blank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 B |
BIN
build-android/icon.png
Normal file
BIN
build-android/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
98
build-android/icon.svg
Normal file
98
build-android/icon.svg
Normal file
@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
inkscape:export-ydpi="255.49001"
|
||||
inkscape:export-xdpi="255.49001"
|
||||
inkscape:export-filename="/home/nait/图片/aaaa.png"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
sodipodi:docname="aaaa.svg"
|
||||
id="svg3"
|
||||
version="1.1"
|
||||
viewBox="0 0 300 300"
|
||||
height="96"
|
||||
width="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs7" />
|
||||
<sodipodi:namedview
|
||||
id="namedview5"
|
||||
pagecolor="#eeeeee"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="in"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.6568543"
|
||||
inkscape:cx="46.934212"
|
||||
inkscape:cy="39.067649"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="999"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="40"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3"
|
||||
inkscape:snap-global="false"
|
||||
units="px" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect25408"
|
||||
width="300"
|
||||
height="300"
|
||||
x="0"
|
||||
y="0"
|
||||
ry="30" />
|
||||
<path
|
||||
style="fill:none;stroke:#101010;stroke-width:3.125px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 129.17447,162.72362 c -0.12,14.38 13.76,23.38 20.88,4.13 8.38,17.25 22.25,13.37 21.62,-5.13"
|
||||
id="path857" />
|
||||
<path
|
||||
style="fill:#101010;fill-opacity:1;stroke:#101010;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 97.971924,155.06364 c -1.859329,-0.289 -4.633499,-1.13426 -6.045924,-1.84212 -7.029665,-3.52301 -11.163386,-11.67658 -10.120473,-19.96215 0.903676,-7.17941 4.27553,-12.60514 9.533554,-15.34067 6.115005,-3.18137 15.694519,-2.92063 21.600319,0.58794 5.79394,3.44211 8.94004,10.65859 8.28563,19.00543 -0.72596,9.2594 -5.83859,15.36854 -14.43623,17.25001 -1.78814,0.39131 -7.075946,0.57217 -8.816876,0.30156 z m 4.785856,-6.90573 c 0.92722,-0.49623 1.31028,-2.47068 0.70603,-3.63918 -0.49167,-0.95078 -1.86663,-1.27278 -3.04573,-0.71326 -0.819556,0.38891 -1.125521,1.02825 -1.125521,2.35191 0,0.85861 0.09491,1.11686 0.593895,1.61584 0.485846,0.48585 0.765496,0.59389 1.537166,0.59389 0.51879,0 1.11917,-0.0941 1.33416,-0.20919 z m -8.826554,-0.49733 c 2.299279,-0.68281 3.532763,-2.60527 3.532763,-5.50601 0,-2.43596 -1.106359,-4.00165 -3.284807,-4.64858 -2.182585,-0.64816 -4.281211,-0.32469 -5.684062,0.87609 -3.641992,3.11742 -1.315378,9.47948 3.49707,9.56264 0.510513,0.009 1.383079,-0.11903 1.939036,-0.28415 z m 18.617794,-3.13527 c 3.30127,-1.28241 5.47639,-5.31708 5.15803,-9.56772 -0.21891,-2.9227 -1.17891,-4.91372 -3.04749,-6.32037 -2.73438,-2.05843 -7.72392,-2.13228 -10.68031,-0.1581 -2.60847,1.74187 -4.055476,5.28086 -3.47178,8.49098 0.70013,3.85038 2.77955,6.66135 5.73815,7.75685 1.48641,0.55039 4.626,0.44995 6.3034,-0.20165 z"
|
||||
id="path1376"
|
||||
inkscape:export-filename="/home/nait/图片/path1376.png"
|
||||
inkscape:export-xdpi="300.00012"
|
||||
inkscape:export-ydpi="300.00012" />
|
||||
<path
|
||||
style="fill:#101010;fill-opacity:1;stroke:#101010;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 202.99373,155.06364 c 1.85933,-0.289 4.63349,-1.13426 6.04592,-1.84212 7.02967,-3.52301 11.16339,-11.67658 10.12047,-19.96215 -0.90367,-7.17941 -4.27553,-12.60514 -9.53355,-15.34067 -6.11501,-3.18137 -15.69453,-2.92063 -21.60032,0.58794 -5.79394,3.44211 -8.94005,10.65859 -8.28563,19.00543 0.72596,9.2594 5.83858,15.36854 14.43623,17.25001 1.78814,0.39131 7.07595,0.57217 8.81688,0.30156 z m -4.78586,-6.90573 c -0.92723,-0.49623 -1.31029,-2.47068 -0.70603,-3.63918 0.49167,-0.95078 1.86663,-1.27278 3.04573,-0.71326 0.81956,0.38891 1.12552,1.02825 1.12552,2.35191 0,0.85861 -0.0949,1.11686 -0.5939,1.61584 -0.48584,0.48585 -0.7655,0.59389 -1.53716,0.59389 -0.5188,0 -1.11917,-0.0941 -1.33416,-0.20919 z m 8.82655,-0.49733 c -2.29927,-0.68281 -3.53276,-2.60527 -3.53276,-5.50601 0,-2.43596 1.10636,-4.00165 3.28481,-4.64858 2.18258,-0.64816 4.28121,-0.32469 5.68406,0.87609 3.64199,3.11742 1.31538,9.47948 -3.49707,9.56264 -0.51051,0.009 -1.38308,-0.11903 -1.93904,-0.28415 z m -18.61779,-3.13527 c -3.30128,-1.28241 -5.4764,-5.31708 -5.15803,-9.56772 0.2189,-2.9227 1.17891,-4.91372 3.04748,-6.32037 2.73439,-2.05843 7.72393,-2.13228 10.68032,-0.1581 2.60847,1.74187 4.05548,5.28086 3.47178,8.49098 -0.70013,3.85038 -2.77955,6.66135 -5.73816,7.75686 -1.48641,0.55038 -4.626,0.44994 -6.30339,-0.20166 z"
|
||||
id="path1569" />
|
||||
<path
|
||||
id="path1570"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
d="m 59.000064,91.161504 c 11.999998,-34 48.499996,-50.5 76.499996,-7 m 30,0 c 17.5,-30.5 60.5,-38.5 75.5,5.5"
|
||||
style="stroke:#101010;stroke-width:6;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:#101010;fill-opacity:1;stroke:#101010;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 175.11938,86.255381 c 13.12597,-23.368892 45.37837,-29.498438 56.62922,4.214061 z"
|
||||
id="path1754"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:#101010;fill-opacity:1;stroke:#101010;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 69.03556,92.038425 c 9.000668,-26.050567 36.3777,-38.692754 57.37925,-5.363351 z"
|
||||
id="path1635"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<g
|
||||
id="g24602"
|
||||
transform="translate(22.00006,20.818541)">
|
||||
<rect
|
||||
style="fill:none;fill-opacity:1;stroke:#101010;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect22570"
|
||||
width="206.15567"
|
||||
height="7.2529359"
|
||||
x="24.922165"
|
||||
y="172.40291" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#cccccc;stroke-width:0.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect22674"
|
||||
width="180.68272"
|
||||
height="48.131855"
|
||||
x="37.658638"
|
||||
y="175.78719" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
4
build-common/0-bundle-all.sh
Executable file
4
build-common/0-bundle-all.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
python3 bundle.py $1
|
||||
python3 bundle.py -w $1
|
||||
python3 bundle.py -b $1
|
122
build-common/bundle.py
Normal file
122
build-common/bundle.py
Normal file
@ -0,0 +1,122 @@
|
||||
'Bundle script'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
bundle_name = 'cat-printer-%s-%s.zip'
|
||||
edition = 'pure'
|
||||
version = 'dev'
|
||||
bundle_sub_dir = 'cat-printer'
|
||||
|
||||
if '-w' in sys.argv:
|
||||
edition = 'windows'
|
||||
elif '-b' in sys.argv:
|
||||
edition = 'bare'
|
||||
|
||||
if not sys.argv[-1].startswith('-'):
|
||||
version = sys.argv[-1]
|
||||
|
||||
bundle_name %= (edition, version)
|
||||
|
||||
ignore_whitelist = (
|
||||
'www/i18n.js',
|
||||
'www/main.comp.js'
|
||||
)
|
||||
|
||||
additional_ignore = (
|
||||
# prevent recurse
|
||||
bundle_name,
|
||||
# build helpers
|
||||
'build-*',
|
||||
'?-*.sh',
|
||||
# no need
|
||||
'.git',
|
||||
'.vscode',
|
||||
'.pylintrc',
|
||||
'.gitignore',
|
||||
'dev-diary.txt',
|
||||
'TODO'
|
||||
# other
|
||||
'.directory',
|
||||
'thumbs.db',
|
||||
'thumbs.db:encryptable'
|
||||
)
|
||||
|
||||
def wildcard_to_regexp(wildcard):
|
||||
'Turn a "wildcard" string to a regular expression string'
|
||||
return (
|
||||
wildcard
|
||||
.replace('/', os.path.sep.replace('\\', '\\\\'))
|
||||
.replace('.', r'\.')
|
||||
.replace('*', r'.*')
|
||||
.replace('?', r'.?')
|
||||
)
|
||||
|
||||
os.chdir('../')
|
||||
|
||||
ignored = []
|
||||
|
||||
for i in additional_ignore:
|
||||
ignored.append(
|
||||
re.compile(
|
||||
wildcard_to_regexp(i)
|
||||
)
|
||||
)
|
||||
|
||||
if os.path.isfile('.gitignore'):
|
||||
with open('.gitignore', 'r', encoding='utf-8') as file:
|
||||
while True:
|
||||
line = file.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
if (
|
||||
line.startswith('#') or
|
||||
line in ignore_whitelist or
|
||||
not line
|
||||
):
|
||||
continue
|
||||
pattern = re.compile(
|
||||
wildcard_to_regexp(line)
|
||||
)
|
||||
ignored.append(pattern)
|
||||
|
||||
with zipfile.ZipFile(bundle_name, 'w', zipfile.ZIP_DEFLATED) as bundle:
|
||||
for path, dirs, files in os.walk('.'):
|
||||
for name in files:
|
||||
fullpath = os.path.join(path, name)
|
||||
if name == bundle_name:
|
||||
continue
|
||||
for pattern in ignored:
|
||||
if re.search(pattern, fullpath) is not None:
|
||||
break
|
||||
else: # if didn't break
|
||||
bundle.write(fullpath, os.path.join(bundle_sub_dir, fullpath))
|
||||
os.chdir('build-common')
|
||||
if edition != 'bare':
|
||||
for path, dirs, files in os.walk('bleak'):
|
||||
if path.endswith('__pycache__'):
|
||||
continue
|
||||
for name in files:
|
||||
fullpath = os.path.join(path, name)
|
||||
bundle.write(fullpath, os.path.join(bundle_sub_dir, fullpath))
|
||||
if edition == 'windows':
|
||||
os.chdir('python-win32-amd64-embed')
|
||||
for path, dirs, files in os.walk('.'):
|
||||
if path.endswith('__pycache__'):
|
||||
continue
|
||||
for name in files:
|
||||
fullpath = os.path.join(path, name)
|
||||
bundle.write(fullpath, os.path.join(bundle_sub_dir, fullpath))
|
||||
os.chdir('..')
|
||||
bundle.write('start.bat')
|
||||
bundle.comment = (
|
||||
b'Cat Printer "%s" bundle\n%s' % (
|
||||
edition.encode('utf-8'),
|
||||
str(datetime.datetime.now()).encode('utf-8'),
|
||||
)
|
||||
)
|
||||
bundle.close()
|
5
build-common/start.bat
Normal file
5
build-common/start.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
color f0
|
||||
title Cat Printer - Console
|
||||
cd cat-printer
|
||||
python server.py
|
57
dev-diary.txt
Normal file
57
dev-diary.txt
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
YEAR 2022
|
||||
|
||||
MARCH
|
||||
|
||||
31st
|
||||
|
||||
Well, frontend development is just ongoing.
|
||||
|
||||
APRIL
|
||||
|
||||
1st
|
||||
|
||||
Costed one hour try to find an event propagation problem,
|
||||
but finally found it's a matter of CSS selector.
|
||||
|
||||
BTW frontend can communicate with backend now.
|
||||
|
||||
2nd
|
||||
|
||||
(Formally) Started playing with Android.
|
||||
|
||||
Costed 4 hours to find why Java class (on android) always can't be found,
|
||||
but finally realized pyjnius can't work with python thread/subprocess,
|
||||
while I'm using ThreadingHTTPServer.
|
||||
|
||||
See "Note" around:
|
||||
https://python-for-android.readthedocs.io/en/latest/buildoptions/#webview
|
||||
|
||||
NEVER MISS ANY SENTENCE OF A DOCUMENT
|
||||
|
||||
3rd
|
||||
|
||||
Costed the whole day to solve WebView problem --
|
||||
it doesn't do reaction to <input type="file"> by default.
|
||||
|
||||
Finally done with library AdvancedWebView, but still with some hacks.
|
||||
|
||||
... It's now 4th 2:00 a.m. though
|
||||
|
||||
4th
|
||||
|
||||
Purged many small problems.
|
||||
|
||||
Wished to release it today, but no luck...
|
||||
|
||||
5th
|
||||
|
||||
Bundle script, optimized project structure.
|
||||
|
||||
It's finally ready...
|
||||
|
||||
6th
|
||||
|
||||
Documentation.
|
||||
|
||||
What else? First Release!
|
115
development.md
Normal file
115
development.md
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
# Development
|
||||
|
||||
## Overview
|
||||
|
||||
This application have a Client/Server module, but it's just locally.
|
||||
|
||||
The backend is in Python 3, aiming to have fewest dependencies, and in fact currently have just `bleak`.
|
||||
This can ensure the simplicity of the core part.
|
||||
|
||||
And the frontend is in a "old good" way, that use no "framework".
|
||||
It needs [roddeh-i18n](https://www.npmjs.com/package/roddeh-i18n) lib for localization, and optionally [vConsole](https://www.npmjs.com/package/vconsole) for debugging on mobile.
|
||||
|
||||
My workspace stack is Linux/GNU/Artix/KDE/VSCodium, if you're interested.
|
||||
For Android, GNU/Linux is required, though.
|
||||
|
||||
The Android version is built with [python-for-android](https://python-for-android.readthedocs.io/en/latest/).
|
||||
In our case it's **NOT** something easy, don't go blindly if you don't want to waste your time.
|
||||
There are too many hacks to be done, before and after. Let me summarize them later...
|
||||
By the way, feel free to look at file `dev-diary.txt`
|
||||
|
||||
## Get Dependencies
|
||||
|
||||
### Basic
|
||||
|
||||
Just clone this repo first!
|
||||
|
||||
1. Get Bleak BLE lib:
|
||||
`pip install bleak`
|
||||
2. Grab i18n.js [here](https://github.com/roddeh/i18njs/tree/master/dist), put to `www` as `i18n.js`
|
||||
|
||||
You are already well done for basic development. See [files](#files) section for what all the files do.
|
||||
For more, read on...
|
||||
|
||||
### Additional
|
||||
|
||||
Sorry, I'm not a dev package manager enthusiast.
|
||||
|
||||
If there are something better to organize these, feel free to discuss in issue.
|
||||
|
||||
- Install TypeScript on Node.js `npm`
|
||||
`npm --global install typescript`
|
||||
You may need root privilege on GNU/Linux (i.e. prefix `sudo`)
|
||||
Now the `0-transpile.sh` will work, you're ready to deal with compatibility
|
||||
- Put the Bleak pip installation as `build-common/bleak`
|
||||
- You need this in order to bundle a "pure" or "windows" release
|
||||
- See [Files](#files) section about `bundle.py`
|
||||
- Get an Windows 64-bit embeddable Python, extract to `build-common/python-win32-amd64-embed`
|
||||
- You may remove the "bloated" parts inside, notably `libssl`, `libcrypto`, `sqlite3` and `pydoc`, of both `dll`/`pyd` files and in `python<version>.zip`, if have any.
|
||||
- Now you're able to bundle a "windows" edition, via `python3 bundle.py -w`
|
||||
- Grab i18n.js typings `index.d.ts` from [here](https://github.com/roddeh/i18njs/tree/master/typings), put to `www` as `i18n.d.ts`
|
||||
In the file, replace the last line:
|
||||
`export = roddeh_i18n;`
|
||||
with:
|
||||
`declare var i18n = roddeh_i18n;`
|
||||
Now you are ready to do more with i18n lib with the typing hint
|
||||
- Get a [vConsole](https://www.npmjs.com/package/vconsole) script, put to `www` as `vconsole.js`
|
||||
Now you're ready to debug in browsers without a dev panel, by double-tapping "Cat Printer" title in the UI
|
||||
|
||||
## Files
|
||||
|
||||
- `server.py` - A Web server that:
|
||||
- Is single threaded, to work with Android/pyjnius
|
||||
- Serves static Web files, that are in folder `www`
|
||||
- Opens a Web browser once launched, unless specify the `-s` command-line parameter
|
||||
- Only listen to localhost, unless specify the `-a` command-line parameter
|
||||
- Handles API requests via `POST`
|
||||
- Handles frontend configuration
|
||||
- Interacts with `printer.py`, for the printer driver
|
||||
- `printer.py` - The core printer driver:
|
||||
- Have the `PrinterDriver` class, to be reused
|
||||
- Have a command-line interface. Can be invoked in a shell, to do things directly
|
||||
- `.pylintrc` - Pylint RC file:
|
||||
- Include it for better experience browsing the code
|
||||
|
||||
- `www/main.js` - Main frontend script:
|
||||
- The script for direct modification in development
|
||||
- No need to care "compatibility". Transpile the scripts when release.
|
||||
- `www/image.js` - Image manipulation functions:
|
||||
- Implementations for some grayscale/monochrome filters on a image (HTML `<canvas>` `ImageData`)
|
||||
- And PBM image file format, a very simple mono bitmap format.
|
||||
- `www/main.comp.js` - Compatibility script:
|
||||
- Transpiled with TypeScript, for fallback on old browsers
|
||||
- Bundled all required scripts, see file `0-transpile.sh`
|
||||
- Is not there by default. Transpile it yourself
|
||||
- `www/*.js` - Other scripts:
|
||||
- Small but useful, just look at them directly
|
||||
- `www/jslicense.html` - Dedicated JavaScript License information
|
||||
- `www/lang/*.json` - Languages
|
||||
|
||||
- `N-*.sh` - Shell files:
|
||||
- Helpers for development convenience
|
||||
- Quickly invoke with `./N<tab><enter>`
|
||||
- `build-common/bundle.py` - Bundler for "windows", "pure" and "bare" editions
|
||||
- You can define what to include or not in this script, just modify directly, while trying to not alter other
|
||||
- To do the builds you should be in the build dir: `cd build-common`
|
||||
- With `bleak` there you're able to bundle a "pure" edition via just `python3 bundle.py`
|
||||
- In any case you're able to bundle a "bare" edition, via `python3 bundle.py -b`
|
||||
- Bundle a "windows" edition with `-w` switch in place of `-b`
|
||||
- You may put a version code as last parameter
|
||||
- Resulting zip files will be in repo's root directory
|
||||
- `build-common/0-bundle-all.sh` - Bundle all editions at once
|
||||
|
||||
- `build-android/0-build-android.sh` - The dev build script:
|
||||
- Invokes `python-for-android`
|
||||
- Defines many things
|
||||
- Just builds using the current repo state
|
||||
- **Doesn't** work out-of-the-box. Again, please wait for me to summarize the hacks...
|
||||
- `build-android/3-formal-build.sh` - The "formal" build script:
|
||||
- Unlike the dev version, this needs a pre-built "bare" edition zip, and should be passed a version id (like `0.1.0`)
|
||||
- Also unlike dev, this doesn't enforce the custom blacklist, since "bare" is already minimal
|
||||
|
||||
### Be aware that...
|
||||
|
||||
If there are intermediate development files that are not meant to be in this public repo, please add to `.gitignore`
|
5
main.py
Normal file
5
main.py
Normal file
@ -0,0 +1,5 @@
|
||||
'For python-for-android entry point'
|
||||
|
||||
from server import serve
|
||||
|
||||
serve()
|
284
printer.py
284
printer.py
@ -1,6 +1,16 @@
|
||||
'Cat-Printer'
|
||||
|
||||
import io
|
||||
import sys
|
||||
import argparse
|
||||
import asyncio
|
||||
from bleak import BleakClient
|
||||
import sys, os, io
|
||||
from bleak import BleakClient, BleakScanner
|
||||
from bleak.exc import BleakError, BleakDBusError
|
||||
|
||||
class PrinterError(Exception):
|
||||
'Error of Printer driver'
|
||||
|
||||
models = ('GB01', 'GB02', 'GT01')
|
||||
|
||||
crc8_table = [
|
||||
0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31,
|
||||
@ -27,28 +37,62 @@ crc8_table = [
|
||||
0xfa, 0xfd, 0xf4, 0xf3
|
||||
]
|
||||
|
||||
|
||||
def crc8(data):
|
||||
'crc8 hash'
|
||||
crc = 0
|
||||
for byte in data:
|
||||
crc = crc8_table[(crc ^ byte) & 0xFF]
|
||||
return crc & 0xFF
|
||||
|
||||
|
||||
def set_attr_if_not_none(obj, attrs):
|
||||
''' set the attribute of `obj` if the value is not `None`
|
||||
`attrs` is `dict` of attr-value pair
|
||||
'''
|
||||
for name in attrs:
|
||||
value = attrs[name]
|
||||
if value is not None:
|
||||
setattr(obj, name, value)
|
||||
|
||||
|
||||
def reverse_binary(value):
|
||||
'Get the binary value of `value` and return the binary-reversed form of it as an `int`'
|
||||
return int(f"{bin(value)[2:]:0>8}"[::-1], 2)
|
||||
|
||||
|
||||
def make_command(command, payload):
|
||||
'Make a `bytes` with command data, which can be sent to printer directly to operate'
|
||||
if len(payload) > 0x100:
|
||||
raise Exception('Too large payload')
|
||||
message = bytearray([0x51, 0x78, command, 0x00, len(payload), 0x00])
|
||||
message += payload
|
||||
message.append(crc8(payload))
|
||||
message.append(0xFF)
|
||||
return bytes(message)
|
||||
|
||||
|
||||
class PrinterCommands():
|
||||
'Constants of command flags used by the printer'
|
||||
RetractPaper = 0xA0 # Data: Number of steps to go back
|
||||
FeedPaper = 0xA1 # Data: Number of steps to go forward
|
||||
DrawBitmap = 0xA2 # Data: Line to draw. 0 bit -> don't draw pixel, 1 bit -> draw pixel
|
||||
# Data: Line to draw. 0 bit -> don't draw pixel, 1 bit -> draw pixel
|
||||
DrawBitmap = 0xA2
|
||||
DrawingMode = 0xBE # Data: 1 for Text, 0 for Images
|
||||
SetEnergy = 0xAF # Data: 1 - 0xFFFF
|
||||
SetQuality = 0xA4 # Data: 1 - 5
|
||||
UpdateDevice = 0xA9 # Data: 0x00
|
||||
LatticeControl = 0xA6
|
||||
|
||||
|
||||
class PBMData():
|
||||
'Extract/Serialize PBM data'
|
||||
width: int
|
||||
height: int
|
||||
data: bytes
|
||||
args: dict
|
||||
def __init__(self, width=384, height=0, data=b'', args={}):
|
||||
|
||||
def __init__(self, width: int, height: int, data: bytes, args: dict = None):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.data = data
|
||||
@ -57,109 +101,114 @@ class PBMData():
|
||||
PrinterCommands.SetEnergy: b'\xE0\x2E',
|
||||
PrinterCommands.SetQuality: b'\x05'
|
||||
}
|
||||
for i in args:
|
||||
self.args[i] = args[i]
|
||||
if args:
|
||||
for arg in args:
|
||||
self.args[arg] = args[arg]
|
||||
|
||||
|
||||
class PrinterDriver():
|
||||
'Manipulator of the printer'
|
||||
|
||||
mtu: int
|
||||
frequency = 0.8
|
||||
''' Time to wait between communication to printer, in seconds,
|
||||
too low value will cause gaps/tearing of printed content,
|
||||
while too high value will make printer slow/clumsy
|
||||
'''
|
||||
feed_after = 128
|
||||
'Extra paper to feed at the end of printing, by pixel'
|
||||
|
||||
feed_after: int
|
||||
dry_run = False
|
||||
'Is dry run (emulate print process but print nothing)'
|
||||
|
||||
standard_width = 384
|
||||
'It\'s a constant for the printer'
|
||||
|
||||
standard_pbm_data_length_per_line = int(standard_width / 8) # 48
|
||||
pbm_data_per_line = int(standard_width / 8) # 48
|
||||
'Constant, determined by standard width & PBM data format'
|
||||
|
||||
characteristic = '0000ae01-0000-1000-8000-00805f9b34fb'
|
||||
'The BLE characteristic, a constant of the printer'
|
||||
|
||||
def __init__(self, mtu=200, feed_after=128):
|
||||
self.mtu = mtu
|
||||
self.feed_after = feed_after
|
||||
mtu = 200
|
||||
|
||||
def _reverse_binary(self, value):
|
||||
return int(f"{bin(value)[2:]:0>8}"[::-1], 2)
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _make_command(self, command, payload):
|
||||
if len(payload) > 0x100:
|
||||
raise Exception('Too large payload')
|
||||
message = bytearray([0x51, 0x78, command, 0x00, len(payload), 0x00])
|
||||
message += payload
|
||||
message.append(crc8(payload))
|
||||
message.append(0xFF)
|
||||
return bytes(message)
|
||||
|
||||
def _read_pbm(self, path='', data=b''):
|
||||
if path != '':
|
||||
f = open(path, 'rb')
|
||||
elif data != b'':
|
||||
f = io.BytesIO(data)
|
||||
def _read_pbm(self, path: str = None, data: bytes = None):
|
||||
if path is not None and path != '-':
|
||||
file = open(path, 'rb')
|
||||
elif data is not None:
|
||||
file = io.BytesIO(data)
|
||||
else:
|
||||
f = sys.stdin.buffer
|
||||
signature = f.readline()
|
||||
file = sys.stdin.buffer
|
||||
signature = file.readline()
|
||||
if signature != b'P4\n':
|
||||
raise Exception('Specified file is not a PBM image')
|
||||
width, height = self.standard_width, 0
|
||||
args = {}
|
||||
while True:
|
||||
l = f.readline()[0:-1]
|
||||
if l[0:1] == b'#':
|
||||
if l[1:2] == b'!':
|
||||
inline_args = l[2:].split(b',')
|
||||
args[PrinterCommands.DrawingMode] = bytes([int(inline_args[0], 16)])
|
||||
args[PrinterCommands.SetEnergy] = bytes([int(inline_args[1], 16)])
|
||||
args[PrinterCommands.SetQuality] = bytes([int(inline_args[2], 16)])
|
||||
continue
|
||||
width, height = [int(x) for x in l.split(b' ')[0:2]]
|
||||
if width != self.standard_width:
|
||||
raise Exception('PBM image width is not 384px')
|
||||
break
|
||||
data = f.read()
|
||||
len_data = len(data)
|
||||
if len_data != height * self.standard_pbm_data_length_per_line:
|
||||
# There can be comments. Skip them
|
||||
line = file.readline()[0:-1]
|
||||
if line[0:1] != b'#':
|
||||
break
|
||||
width, height = [int(x) for x in line.split(b' ')[0:2]]
|
||||
if width != self.standard_width:
|
||||
raise Exception('PBM image width is not 384px')
|
||||
expected_data_size = self.pbm_data_per_line * height
|
||||
data = file.read()
|
||||
if path is not None and path != '-':
|
||||
file.close()
|
||||
data_size = len(data)
|
||||
if data_size != expected_data_size:
|
||||
raise Exception('Broken PBM file data')
|
||||
return PBMData(width, height, data, args)
|
||||
if self.dry_run:
|
||||
# Dry run: put blank data
|
||||
data = b'\x00' * expected_data_size
|
||||
return PBMData(width, height, data)
|
||||
|
||||
def _pbm_data_to_raw(self, data: PBMData):
|
||||
buffer = bytearray()
|
||||
for i in data.args:
|
||||
buffer += self._make_command(i, data.args[i])
|
||||
buffer += self._make_command(
|
||||
for key in data.args:
|
||||
buffer += make_command(key, data.args[key])
|
||||
buffer += make_command(
|
||||
PrinterCommands.LatticeControl,
|
||||
bytearray([0xAA, 0x55, 0x17, 0x38, 0x44, 0x5F, 0x5F, 0x5F, 0x44, 0x38, 0x2C])
|
||||
bytearray([0xAA, 0x55, 0x17, 0x38, 0x44,
|
||||
0x5F, 0x5F, 0x5F, 0x44, 0x38, 0x2C])
|
||||
)
|
||||
for i in range(data.height):
|
||||
data_for_a_line = data.data[
|
||||
i * self.standard_pbm_data_length_per_line :
|
||||
(i + 1) * self.standard_pbm_data_length_per_line
|
||||
i * self.pbm_data_per_line:
|
||||
(i + 1) * self.pbm_data_per_line
|
||||
]
|
||||
if i % 200 == 0:
|
||||
buffer += self._make_command(
|
||||
buffer += make_command(
|
||||
PrinterCommands.LatticeControl,
|
||||
bytearray([0xAA, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17])
|
||||
bytearray([0xAA, 0x55, 0x17, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x17])
|
||||
)
|
||||
# buffer += self._make_command(
|
||||
# buffer += make_command(
|
||||
# PrinterCommands.UpdateDevice,
|
||||
# bytes([0x00])
|
||||
# )
|
||||
buffer += self._make_command(
|
||||
buffer += make_command(
|
||||
PrinterCommands.DrawBitmap,
|
||||
bytes([self._reverse_binary(x) for x in data_for_a_line])
|
||||
bytes([reverse_binary(x) for x in data_for_a_line])
|
||||
)
|
||||
buffer += self._make_command(
|
||||
buffer += make_command(
|
||||
PrinterCommands.LatticeControl,
|
||||
bytearray([0xAA, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17])
|
||||
bytearray([0xAA, 0x55, 0x17, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x17])
|
||||
)
|
||||
if self.feed_after > 0:
|
||||
buffer += self._make_command(
|
||||
buffer += make_command(
|
||||
PrinterCommands.FeedPaper,
|
||||
bytes([self.feed_after % 256, self.feed_after // 256])
|
||||
)
|
||||
return buffer
|
||||
|
||||
async def _send_buffer(self, buffer: bytearray, address: str):
|
||||
client = BleakClient(address)
|
||||
async def send_buffer(self, buffer: bytearray, address: str):
|
||||
'Send manipulation data (buffer) to the printer via bluetooth'
|
||||
client = BleakClient(address, timeout=5.0)
|
||||
await client.connect()
|
||||
# await client.write_gatt_char(self.characteristic, self._make_command(PrinterCommands.FeedPaper, bytearray([0, 0])))
|
||||
count = 0
|
||||
total = len(buffer) // self.mtu
|
||||
while True:
|
||||
@ -168,37 +217,104 @@ class PrinterDriver():
|
||||
if count < total:
|
||||
await client.write_gatt_char(self.characteristic, buffer[start:end])
|
||||
if count % 16 == 0:
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(self.frequency)
|
||||
count += 1
|
||||
else:
|
||||
await client.write_gatt_char(self.characteristic, buffer[start:])
|
||||
break
|
||||
await client.disconnect()
|
||||
|
||||
async def search_all_printers(self, timeout):
|
||||
''' Search for all printers around with bluetooth.
|
||||
Only known-working models will show up.
|
||||
'''
|
||||
timeout = timeout or 3
|
||||
devices = await BleakScanner.discover(timeout)
|
||||
result = []
|
||||
for device in devices:
|
||||
if device.name in models:
|
||||
result.append(device)
|
||||
return result
|
||||
async def search_printer(self, timeout):
|
||||
'Search for a printer, returns `None` if not found'
|
||||
timeout = timeout or 3
|
||||
devices = await self.search_all_printers(timeout)
|
||||
if len(devices) != 0:
|
||||
return devices[0]
|
||||
return None
|
||||
|
||||
async def print_file(self, path: str, address: str):
|
||||
'Method to print the specified PBM image at `path` with printer at specified MAC `address`'
|
||||
pbm_data = self._read_pbm(path)
|
||||
buffer = self._pbm_data_to_raw(pbm_data)
|
||||
await self._send_buffer(buffer, address)
|
||||
await self.send_buffer(buffer, address)
|
||||
|
||||
async def print_data(self, data: bytes, address: str):
|
||||
pbm_data = self._read_pbm('', data)
|
||||
'Method to print the specified PBM image `data` with printer at specified MAC `address`'
|
||||
pbm_data = self._read_pbm(None, data)
|
||||
buffer = self._pbm_data_to_raw(pbm_data)
|
||||
await self._send_buffer(buffer, address)
|
||||
await self.send_buffer(buffer, address)
|
||||
|
||||
|
||||
async def _main():
|
||||
'Main routine for direct command line execution'
|
||||
parser = argparse.ArgumentParser(
|
||||
description='''Print an PBM image to a Cat/Kitty Printer, of model GB01, GB02 or GT01.'''
|
||||
)
|
||||
parser.add_argument('file', default='-', metavar='FILE', type=str,
|
||||
help='PBM image file to print, use \'-\' to read from stdin')
|
||||
exgr = parser.add_mutually_exclusive_group()
|
||||
exgr.add_argument('-s', '--scan', metavar='DELAY', default=3.0, required=False, type=float,
|
||||
help='Scan for a printer for specified seconds')
|
||||
exgr.add_argument('-a', '--address', metavar='xx:xx:xx:xx:xx:xx', required=False, type=str,
|
||||
help='The printer\'s bluetooth MAC address')
|
||||
parser.add_argument('-p', '--feed', required=False, type=int,
|
||||
help='Extra paper to feed after printing')
|
||||
parser.add_argument('-f', '--freq', required=False, type=float,
|
||||
help='Communication frequency, in seconds. ' +
|
||||
'set a bit higher (eg. 1 or 1.2) if printed content is teared/have gaps')
|
||||
parser.add_argument('-d', '--dry', required=False, action='store_true',
|
||||
help='Emulate the printing process, but actually print nothing ("dry run")')
|
||||
parser.add_argument('-m', '--mtu', required=False, type=int,
|
||||
help='MTU of bluetooth packet (Advanced)')
|
||||
cmdargs = parser.parse_args()
|
||||
addr = cmdargs.address
|
||||
printer = PrinterDriver()
|
||||
if not addr:
|
||||
print('Cat Printer :3')
|
||||
print(f' * Finding printer devices via bluetooth in {cmdargs.scan} seconds')
|
||||
device = await printer.search_printer(cmdargs.scan)
|
||||
if device is not None:
|
||||
print(f' * Will print through {device.name} {device.address}')
|
||||
else:
|
||||
print(' ! No device found. Please check if the printer is powered on.')
|
||||
print(' ! Or try to scan longer with \'-s 6.0\'')
|
||||
sys.exit(1)
|
||||
if cmdargs.dry:
|
||||
print(' * DRY RUN')
|
||||
set_attr_if_not_none(printer, {
|
||||
'feed_after': cmdargs.feed,
|
||||
'frequency': cmdargs.freq,
|
||||
'mtu': cmdargs.mtu,
|
||||
'dry': cmdargs.dry
|
||||
})
|
||||
await printer.print_file(cmdargs.file, addr)
|
||||
|
||||
async def main():
|
||||
'Run the `_main` routine while catching exceptions'
|
||||
try:
|
||||
await _main()
|
||||
except BleakError as e:
|
||||
error_message = str(e)
|
||||
if (
|
||||
'not turned on' in error_message or
|
||||
(isinstance(e, BleakDBusError) and
|
||||
getattr(e, 'dbus_error') == 'org.bluez.Error.NotReady')
|
||||
):
|
||||
print(' ! Please enable bluetooth on this machine :3')
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
len_argv = len(sys.argv)
|
||||
printer = PrinterDriver()
|
||||
loop = asyncio.get_event_loop()
|
||||
if len_argv == 1:
|
||||
print(
|
||||
'Usage: %s <xx:xx:xx:xx:xx:xx> [PBM files to print...]\n' % os.path.basename(__file__) +
|
||||
'\tPrint PBM files to a Cat Printer\n' +
|
||||
'\tInput MAC address and file paths\n' +
|
||||
'\tInputing file to stdin is also supported'
|
||||
)
|
||||
else:
|
||||
if len_argv == 2:
|
||||
loop.run_until_complete(printer.print_file('', sys.argv[1]))
|
||||
elif len_argv >= 3:
|
||||
for i in sys.argv[2:]:
|
||||
loop.run_until_complete(printer.print_file(i, sys.argv[1]))
|
||||
asyncio.run(main())
|
||||
|
431
server.py
431
server.py
@ -1,215 +1,256 @@
|
||||
'Cat Printer - Serve a Web UI'
|
||||
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
import socketserver, threading, urllib, os, asyncio, tempfile, platform
|
||||
# if pylint is annoying you, see file .pylint-rc
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import asyncio
|
||||
import platform
|
||||
# Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer #, ThreadingHTTPServer
|
||||
from bleak.exc import BleakDBusError, BleakError
|
||||
from printer import PrinterDriver
|
||||
import bleak
|
||||
|
||||
def urlvar(path):
|
||||
a = path.split('?')
|
||||
d = []
|
||||
f = {}
|
||||
if len(a) > 1:
|
||||
b = a[1].split('&')
|
||||
for i in b:
|
||||
d.append(i.split('='))
|
||||
for i in d:
|
||||
if len(i) == 1:
|
||||
i.append('1')
|
||||
f[i[0]] = i[1]
|
||||
return f
|
||||
class DictAsObject(dict):
|
||||
""" Let you use a dict like an object in JavaScript.
|
||||
"""
|
||||
def __getattr__(self, key):
|
||||
return self.get(key, None)
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
mimetypes = {
|
||||
'html': 'text/html',
|
||||
'txt': 'text/plain',
|
||||
'js': 'text/javascript',
|
||||
'css': 'text/css'
|
||||
class PrinterServerError(Exception):
|
||||
'Error of PrinterServer'
|
||||
code: int
|
||||
name: str
|
||||
details: str
|
||||
def __init__(self, *args, code=1):
|
||||
super().__init__(*args)
|
||||
len_args = len(args)
|
||||
self.code = code
|
||||
if len_args > 0:
|
||||
self.name = args[0]
|
||||
if len_args > 1:
|
||||
self.details = args[1]
|
||||
|
||||
Printer = PrinterDriver()
|
||||
server = None
|
||||
|
||||
def log(message):
|
||||
'For logging a message'
|
||||
print(message)
|
||||
|
||||
mime_type = {
|
||||
'html': 'text/html;charset=utf-8',
|
||||
'css': 'text/css;charset=utf-8',
|
||||
'js': 'text/javascript;charset=utf-8',
|
||||
'txt': 'text/plain;charset=utf-8',
|
||||
'json': 'application/json;charset=utf-8',
|
||||
'png': 'image/png',
|
||||
'octet-stream': 'application/octet-stream'
|
||||
}
|
||||
def getmime(path):
|
||||
global mimetypes
|
||||
ext = path.split('.')[-1]
|
||||
return mimetypes.get(ext, 'application/octet-stream')
|
||||
def mime(url: str):
|
||||
'Get pre-defined MIME type of a certain url by extension name'
|
||||
return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream'])
|
||||
|
||||
class PrinterServer(BaseHTTPRequestHandler):
|
||||
'(Local) server for Cat Printer Web interface'
|
||||
buffer = 4 * 1024 * 1024
|
||||
driver = PrinterDriver()
|
||||
max_payload = buffer * 16
|
||||
printer_address: str = None
|
||||
settings = DictAsObject({
|
||||
'config_path': 'config.json',
|
||||
'is_android': False,
|
||||
'printer_address': None,
|
||||
'scan_time': 3,
|
||||
'frequency': 0.8,
|
||||
'dry_run': False
|
||||
})
|
||||
def log_request(self, _code=200, _size=0):
|
||||
pass
|
||||
def log_error(self, *_args):
|
||||
pass
|
||||
def do_GET(self):
|
||||
'Called when server get a GET http request'
|
||||
path = 'www' + self.path
|
||||
if self.path == '/':
|
||||
self.path = '/index.html'
|
||||
path = urllib.parse.unquote(self.path)
|
||||
# v = urlvar(path)
|
||||
path = path.split('?')[0]
|
||||
if len(path) >= 2:
|
||||
if path[0:2] == '/~':
|
||||
action = path[2:]
|
||||
if action == 'getdevices':
|
||||
try:
|
||||
devices = asyncio.run(bleak.BleakScanner.discover())
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write('\n'.join([('%s,%s' % (x.name, x.address)) for x in devices]).encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(str(e).encode('utf-8'))
|
||||
else:
|
||||
# local file
|
||||
path = 'www/' + path[1:]
|
||||
if os.path.exists(path):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', getmime(path))
|
||||
# self.send_header('Cache-Control', 'public, max-age=86400')
|
||||
self.end_headers()
|
||||
with open(path, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(self.buffer)
|
||||
if data:
|
||||
self.wfile.write(data)
|
||||
else:
|
||||
break
|
||||
return
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'Not Found')
|
||||
return
|
||||
def do_POST(self):
|
||||
if self.headers.get('Content-Type', '') == 'application/ipp':
|
||||
# https://datatracker.ietf.org/doc/html/rfc8010
|
||||
self.handle_ipp()
|
||||
path += 'index.html'
|
||||
if '/..' in path:
|
||||
return
|
||||
path = urllib.parse.unquote(self.path)
|
||||
v = urlvar(path)
|
||||
path = path.split('?')[0]
|
||||
if len(path) >= 2:
|
||||
if path[0:2] == '/~':
|
||||
action = path[2:]
|
||||
if action == 'print':
|
||||
if 'mtu' in v:
|
||||
self.driver.mtu = v['mtu']
|
||||
if 'feed_after' in v:
|
||||
self.driver.feed_after = v['feed_after']
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length'))
|
||||
data = self.rfile.read(content_length)
|
||||
asyncio.run(self.driver.print_data(data, v['address']))
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'OK')
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(str(e).encode('utf-8'))
|
||||
else:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-Type', 'text/plain')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'Bad Request')
|
||||
def handle_ipp(self):
|
||||
path = urllib.parse.unquote(self.path)
|
||||
printer_name = path[1:]
|
||||
data = self.rfile.read(int(self.headers.get('Content-Length', 0)))
|
||||
# len_data = len(data)
|
||||
# ipp_version_number = data[0:2]
|
||||
# ipp_operation_id = data[2:4]
|
||||
# ipp_request_id = data[4:8]
|
||||
ipp_operation_attributes_tag = data[8]
|
||||
attributes = {}
|
||||
data_to_print = b''
|
||||
# b'\x01'[0] == int(1)
|
||||
if ipp_operation_attributes_tag == b'\x01'[0]:
|
||||
pointer = 9
|
||||
next_name_length_at = 10
|
||||
next_value_length_at = 10
|
||||
name = b''
|
||||
value = b''
|
||||
while data[pointer] != b'\x03'[0]:
|
||||
tag = data[pointer:pointer + 1]
|
||||
pointer += 1
|
||||
if tag[0] < 0x10: # delimiter-tag
|
||||
continue
|
||||
next_name_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2
|
||||
pointer += 2
|
||||
while pointer < next_name_length_at:
|
||||
name = name + data[pointer:pointer + 1]
|
||||
pointer += 1
|
||||
next_value_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2
|
||||
pointer += 2
|
||||
while pointer < next_value_length_at:
|
||||
value = value + data[pointer:pointer + 1]
|
||||
pointer += 1
|
||||
attributes[name] = (tag, value)
|
||||
name = b''
|
||||
value = b''
|
||||
pointer += 1
|
||||
data_to_print = data[pointer:]
|
||||
if data_to_print == b'':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/ipp')
|
||||
if not os.path.isfile(path):
|
||||
self.send_response(404)
|
||||
self.send_header('Content-Type', mime('txt'))
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
b'\x01\x01\x00\x00\x00\x00\x00\x01\x01\x03'
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', mime(path))
|
||||
# self.send_header('Content-Size', str(os.stat(path).st_size))
|
||||
self.end_headers()
|
||||
with open(path, 'rb') as file:
|
||||
while True:
|
||||
chunk = file.read(self.buffer)
|
||||
if not self.wfile.write(chunk):
|
||||
break
|
||||
return
|
||||
def api_success(self):
|
||||
'Called when a simple API call is being considered successful'
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', mime('json'))
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{}')
|
||||
def api_fail(self, error_json, error=None):
|
||||
'Called when an API call is failed'
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', mime('json'))
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(error_json).encode('utf-8'))
|
||||
self.wfile.flush()
|
||||
if isinstance(error, Exception):
|
||||
raise error
|
||||
def load_config(self):
|
||||
'Load config file, or if not exist, create one with default'
|
||||
if os.environ.get("P4A_BOOTSTRAP") is not None:
|
||||
self.settings['is_android'] = True
|
||||
from android.storage import app_storage_path # pylint: disable=import-error
|
||||
settings_path = app_storage_path()
|
||||
os.makedirs(settings_path, exist_ok=True)
|
||||
self.settings['config_path'] = os.path.join(
|
||||
settings_path, 'config.json'
|
||||
)
|
||||
if os.path.exists(self.settings.config_path):
|
||||
with open(self.settings.config_path, 'r', encoding='utf-8') as file:
|
||||
self.settings = DictAsObject(json.load(file))
|
||||
else:
|
||||
self.save_config()
|
||||
def save_config(self):
|
||||
'Save config file'
|
||||
with open(self.settings.config_path, 'w', encoding='utf-8') as file:
|
||||
json.dump(self.settings, file, indent=4)
|
||||
def handle_api(self):
|
||||
'Handle API request from POST'
|
||||
content_length = int(self.headers.get('Content-Length'))
|
||||
body = self.rfile.read(content_length)
|
||||
api = self.path[1:]
|
||||
if api == 'print':
|
||||
if self.settings.printer_address is None:
|
||||
# usually can't encounter, though
|
||||
raise PrinterServerError('No printer address specified')
|
||||
Printer.dry_run = self.settings.dry_run
|
||||
Printer.frequency = float(self.settings.frequency)
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
devices = loop.run_until_complete(
|
||||
Printer.print_data(body, self.settings.printer_address)
|
||||
)
|
||||
self.api_success()
|
||||
finally:
|
||||
loop.close()
|
||||
return
|
||||
data = DictAsObject(json.loads(body))
|
||||
if api == 'devices':
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
devices = loop.run_until_complete(
|
||||
Printer.search_all_printers(float(self.settings.scan_time))
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
devices_list = [{
|
||||
'name': device.name,
|
||||
'address': device.address
|
||||
} for device in devices]
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', mime('json'))
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({
|
||||
'devices': devices_list
|
||||
}).encode('utf-8'))
|
||||
return
|
||||
if api == 'query':
|
||||
self.load_config()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', mime('json'))
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(self.settings).encode('utf-8'))
|
||||
return
|
||||
if api == 'set':
|
||||
for key in data:
|
||||
self.settings[key] = data[key]
|
||||
self.save_config()
|
||||
self.api_success()
|
||||
return
|
||||
if api == 'exit':
|
||||
self.api_success()
|
||||
self.save_config()
|
||||
# Only usable when using ThreadingHTTPServer
|
||||
# server.shutdown()
|
||||
sys.exit(0)
|
||||
def do_POST(self):
|
||||
'Called when server get a POST http request'
|
||||
content_length = int(self.headers.get('Content-Length', -1))
|
||||
if (content_length == -1 or
|
||||
content_length > self.max_payload
|
||||
):
|
||||
self.send_response(400)
|
||||
self.send_header('Content-Type', mime('txt'))
|
||||
self.end_headers()
|
||||
return
|
||||
try:
|
||||
devices = asyncio.run(bleak.BleakScanner.discover())
|
||||
target_device = ''
|
||||
for i in devices:
|
||||
if i.name == printer_name:
|
||||
target_device = i.address
|
||||
if target_device != '':
|
||||
platform_system = platform.system()
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_file_ps = os.path.join(temp_dir, 'temp.ps')
|
||||
temp_file_pbm = os.path.join(temp_dir, 'temp.pbm')
|
||||
f = open(temp_file_ps, 'wb')
|
||||
f.write(data_to_print)
|
||||
f.close()
|
||||
# https://ghostscript.com/doc/9.54.0/Use.htm#Output_device
|
||||
ghostscript_exe = 'gs'
|
||||
if platform_system == 'Windows':
|
||||
ghostscript_exe = 'gswin32c.exe'
|
||||
elif platform_system == 'Linux':
|
||||
ghostscript_exe = 'gs'
|
||||
elif platform_system == 'OS/2':
|
||||
ghostscript_exe = 'gsos2'
|
||||
return_code = os.system('%s -q -sDEVICE=pbmraw -dNOPAUSE -dBATCH -dSAFER -dFIXEDMEDIA -g384x543 -r46.4441219158x46.4441219158 -dFitPage -sOutputFile="%s" "%s"' % (ghostscript_exe, temp_file_pbm, temp_file_ps))
|
||||
if return_code == 0:
|
||||
asyncio.run(self.driver.print_file(temp_file_pbm, target_device))
|
||||
else:
|
||||
raise Exception('Error on invoking Ghostscript')
|
||||
# print(data_to_print)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/ipp')
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
b'\x01\x01\x00\x00\x00\x00\x00\x01\x01\x03'
|
||||
)
|
||||
except Exception as _:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'application/ipp')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'')
|
||||
self.handle_api()
|
||||
return
|
||||
except BleakDBusError as e:
|
||||
self.api_fail({
|
||||
'code': -2,
|
||||
'name': e.dbus_error,
|
||||
'details': e.dbus_error_details
|
||||
})
|
||||
except BleakError as e:
|
||||
self.api_fail({
|
||||
'code': -3,
|
||||
'name': 'BleakError',
|
||||
'details': str(e)
|
||||
})
|
||||
except PrinterServerError as e:
|
||||
self.api_fail({
|
||||
'code': e.code,
|
||||
'name': e.name,
|
||||
'details': e.details
|
||||
})
|
||||
except Exception as e:
|
||||
self.api_fail({
|
||||
'code': -1,
|
||||
'name': 'Exception',
|
||||
'details': str(e)
|
||||
}, e)
|
||||
|
||||
|
||||
class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
|
||||
""" Handle requests in a separate thread. """
|
||||
|
||||
if __name__ == '__main__':
|
||||
address, port = '', 8095
|
||||
server = ThreadedHTTPServer((address, port), PrinterServer)
|
||||
def serve():
|
||||
'Start server'
|
||||
address, port = '127.0.0.1', 8095
|
||||
listen_all = False
|
||||
if '-a' in sys.argv:
|
||||
print('Will listen on ALL addresses')
|
||||
listen_all = True
|
||||
global server
|
||||
# Again, Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
||||
server = HTTPServer(('' if listen_all else address, port), PrinterServer)
|
||||
service_url = f'http://{address}:{port}/'
|
||||
if '-s' in sys.argv:
|
||||
print(service_url)
|
||||
else:
|
||||
operating_system = platform.uname().system
|
||||
if operating_system == 'Windows':
|
||||
os.system(f'start {service_url} > NUL')
|
||||
elif operating_system == 'Linux':
|
||||
os.system(f'xdg-open {service_url} &> /dev/null')
|
||||
# TODO: I don't know about macOS
|
||||
# elif operating_system == 'macOS':
|
||||
else:
|
||||
print(f'Will serve application at: {service_url}')
|
||||
try:
|
||||
# Start a thread with the server -- that thread will then start one
|
||||
# more thread for each request
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
# Exit the server thread when the main thread terminates
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
print('http://localhost:8095/')
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
serve()
|
||||
|
28
www/_load.html
Normal file
28
www/_load.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- For replacing python-for-android webview bootstrap _load.html -->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Python Webview Loading</title>
|
||||
<style>
|
||||
:root {
|
||||
--fore-color: #111;
|
||||
--back-color: #eee;
|
||||
}
|
||||
body {
|
||||
background-color: var(--back-color);
|
||||
color: var(--fore-color);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--fore-color: #eee;
|
||||
--back-color: #333;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,91 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom Print</title>
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
<link rel="stylesheet" href="skin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p id="notice">
|
||||
<noscript>Javascript should be enabled</noscript>
|
||||
</p>
|
||||
<h2>Custom Print</h2>
|
||||
<p>
|
||||
<input style="display: none;" type="text" id="bluetooth_address_input" value="" />
|
||||
<span>Select device:</span><select id="device_selection"></select><button id="refresh_device">Refresh</button>
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
<span>Operate directly on the box canvas</span>
|
||||
<button id="preview_button">Preview</button>
|
||||
<button id="print_button">Print</button>
|
||||
<br />
|
||||
<span>Canvas Height:</span>
|
||||
<input type="number" value="400" min="0" max="114514" step="50" id="canvas_height" />
|
||||
<br />
|
||||
<span>Threshold:</span>
|
||||
<input type="range" min="0" max="2.0" step="0.05" value="0.6" id="filter_threshold" />
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
<span>Insert:</span>
|
||||
<button id="action_insert_text">Text</button>
|
||||
<button id="action_insert_image">Image</button>
|
||||
<button id="action_insert_qr">QRCode</button>
|
||||
<br />
|
||||
<span>Action:</span>
|
||||
<button id="action_make_bold">Bold</button>
|
||||
<button id="action_make_italic">Italic</button>
|
||||
<button id="action_make_underline">Underline</button>
|
||||
<button id="action_switch_paint">Switch Paint</button>
|
||||
<button id="action_delete">Delete</button>
|
||||
</span>
|
||||
</p>
|
||||
<div style="margin: auto; width: 386px;">
|
||||
<canvas id="work_canvas" style="border: 1px solid currentColor; width: 384px; min-height: 3em;" width="384" height="384"></canvas>
|
||||
</div>
|
||||
<p>
|
||||
<span>Preview</span><br />
|
||||
<canvas id="image_preview" style="width: 384px;" width="384" height="0"></canvas>
|
||||
</p>
|
||||
</main>
|
||||
<textarea id="i18-N" style="display: none;">
|
||||
[zh-CN]
|
||||
Custom Print=自定义打印
|
||||
Javascript should be enabled=需启用 Javascript
|
||||
Select device:=选择设备:
|
||||
Refresh=刷新
|
||||
Operate directly on the box canvas=直接在框内画布操作
|
||||
Preview=预览
|
||||
Print=打印
|
||||
Threshold:=阈值:
|
||||
Please select a device=请选择一个设备
|
||||
Printing, please wait.=打印中,请稍候。
|
||||
Please preview image first=请先预览图像
|
||||
Searching devices. Please wait for 5 seconds.=正在查找设备。请等候 5 秒。
|
||||
OK=完成
|
||||
Insert:=插入:
|
||||
Text=文本
|
||||
Image=图像
|
||||
QRCode=二维码
|
||||
Double click to edit=双击以修改
|
||||
Action:=操作:
|
||||
Bold=粗体
|
||||
Italic=斜体
|
||||
Underline=下划线
|
||||
Delete=删除
|
||||
Switch Paint=切换绘画
|
||||
Canvas Height:=画布高度:
|
||||
Content of QRCode:=二维码的内容:
|
||||
</textarea>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="qrcode.min.js"></script>
|
||||
<script src="fabric.min.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script src="custom-print.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,141 +0,0 @@
|
||||
///<reference path="main.js" />
|
||||
///<reference path="main.d.ts" />
|
||||
|
||||
class CustomPrinter {
|
||||
WIDTH = 384;
|
||||
threshold = 0.6;
|
||||
bluetoothMACInput = document.getElementById('bluetooth_address_input');
|
||||
thresholdInput = document.getElementById('filter_threshold');
|
||||
fileSelection = document.createElement('input');
|
||||
dummyImage = new Image();
|
||||
canvasPreview = document.getElementById('image_preview');
|
||||
previewButton = document.getElementById('preview_button');
|
||||
printButton = document.getElementById('print_button');
|
||||
actionInsertText = document.getElementById('action_insert_text');
|
||||
actionInsertImage = document.getElementById('action_insert_image');
|
||||
actionInsertQR = document.getElementById('action_insert_qr');
|
||||
canvasHeightInput = document.getElementById('canvas_height');
|
||||
fabricCanvas = new fabric.Canvas('work_canvas', {
|
||||
backgroundColor: 'white'
|
||||
});
|
||||
monoMethod = imageDataColorToMonoSquare;
|
||||
insertImage() {
|
||||
this.fileSelection.click();
|
||||
}
|
||||
preview() {
|
||||
let context = this.fabricCanvas.getContext('2d');
|
||||
let imagedata = context.getImageData(0, 0, this.WIDTH, this.fabricCanvas.height);
|
||||
this.canvasPreview.height = this.fabricCanvas.height;
|
||||
this.canvasPreview.getContext('2d').putImageData(this.monoMethod(imagedata, this.threshold), 0, 0);
|
||||
}
|
||||
constructor() {
|
||||
this.fileSelection.type = 'file';
|
||||
this.monoMethod = imageDataColorToMonoSquare;
|
||||
this.fileSelection.addEventListener('input', this.preview.bind(this));
|
||||
this.previewButton.addEventListener('click', this.preview.bind(this));
|
||||
this.thresholdInput.onchange = event => {
|
||||
this.threshold = this.thresholdInput.value;
|
||||
}
|
||||
this.canvasHeightInput.onchange = event => {
|
||||
this.fabricCanvas.setHeight(this.canvasHeightInput.value);
|
||||
}
|
||||
this.actionInsertText.addEventListener('click', event => {
|
||||
let text = new fabric.Textbox(i18N.get('Double click to edit'), {
|
||||
color: 'black',
|
||||
fontSize: 24,
|
||||
});
|
||||
this.fabricCanvas.add(text);
|
||||
});
|
||||
this.fileSelection.addEventListener('input', event => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = event1 => {
|
||||
this.dummyImage.src = event1.target.result;
|
||||
let fimage = new fabric.Image(this.dummyImage, {});
|
||||
fimage.scale(this.WIDTH / this.dummyImage.width);
|
||||
this.fabricCanvas.add(fimage);
|
||||
}
|
||||
reader.readAsDataURL(this.fileSelection.files[0]);
|
||||
});
|
||||
this.actionInsertImage.addEventListener('click', this.insertImage.bind(this));
|
||||
this.actionInsertQR.addEventListener('click', event => {
|
||||
let div = document.createElement('div');
|
||||
new QRCode(div, prompt(i18N.get('Content of QRCode:')));
|
||||
// QRCode generation is async, currently have no better way than waiting for a while
|
||||
setTimeout(() => {
|
||||
let fimage = new fabric.Image(div.lastChild, {
|
||||
left: this.WIDTH / 4,
|
||||
top: this.WIDTH / 4
|
||||
});
|
||||
fimage.scale((this.WIDTH / 2) / div.lastChild.width);
|
||||
this.fabricCanvas.add(fimage);
|
||||
}, 1000);
|
||||
});
|
||||
this.printButton.addEventListener('click', event => {
|
||||
// this.preview();
|
||||
if (this.canvasPreview.height == 0) {
|
||||
notice(i18N.get('Please preview image first'));
|
||||
return;
|
||||
}
|
||||
let mac_address = this.bluetoothMACInput.value;
|
||||
if (mac_address == '') {
|
||||
notice(i18N.get('Please select a device'));
|
||||
return;
|
||||
}
|
||||
notice(i18N.get('Printing, please wait.'));
|
||||
let context = this.canvasPreview.getContext('2d');
|
||||
let pbm_data = imageDataMonoToPBM(context.getImageData(0, 0, this.WIDTH, this.canvasPreview.height));
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/~print?address=' + mac_address);
|
||||
xhr.setRequestHeader('Content-Type', 'application-octet-stream');
|
||||
xhr.onload = () => {
|
||||
notice(i18N.get(xhr.responseText));
|
||||
}
|
||||
xhr.send(pbm_data);
|
||||
});
|
||||
let boldFunction = () => {
|
||||
let object = this.fabricCanvas.getActiveObject();
|
||||
if (!object) return;
|
||||
if (object.type == 'textbox') {
|
||||
if (object.fontWeight == 'normal') object.fontWeight = 'bold';
|
||||
else if (object.fontWeight == 'bold') object.fontWeight = 'normal';
|
||||
this.fabricCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
document.getElementById('action_make_bold').addEventListener('click', boldFunction.bind(this));
|
||||
let italicFunction = () => {
|
||||
let object = this.fabricCanvas.getActiveObject();
|
||||
if (!object) return;
|
||||
if (object.type == 'textbox') {
|
||||
if (object.fontStyle == 'normal') object.fontStyle = 'italic';
|
||||
else if (object.fontStyle == 'italic') object.fontStyle = 'normal';
|
||||
this.fabricCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
document.getElementById('action_make_italic').addEventListener('click', italicFunction.bind(this));
|
||||
document.getElementById('action_make_underline').addEventListener('click', event => {
|
||||
let object = this.fabricCanvas.getActiveObject();
|
||||
if (!object) return;
|
||||
if (object.type == 'textbox') {
|
||||
object.underline = !object.underline;
|
||||
// Seems there's a bug in fabric, underline cannot be rendered before changing bold/italic
|
||||
boldFunction();
|
||||
this.fabricCanvas.renderAll();
|
||||
boldFunction();
|
||||
this.fabricCanvas.renderAll();
|
||||
}
|
||||
});
|
||||
document.getElementById('action_delete').addEventListener('click', event => {
|
||||
let object = this.fabricCanvas.getActiveObject();
|
||||
if (!object) return;
|
||||
this.fabricCanvas.remove(object);
|
||||
this.fabricCanvas.renderAll();
|
||||
});
|
||||
this.fabricCanvas.freeDrawingBrush.color = 'black';
|
||||
this.fabricCanvas.freeDrawingBrush.width = 6;
|
||||
document.getElementById('action_switch_paint').addEventListener('click', event => {
|
||||
this.fabricCanvas.isDrawingMode = !this.fabricCanvas.isDrawingMode;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var custom_printer = new CustomPrinter();
|
@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Help</title>
|
||||
<link rel="stylesheet" href="../skin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Help</h1>
|
||||
<p>Here is some useful information.</p>
|
||||
<h2>Note</h2>
|
||||
<ul>
|
||||
<li>Preview (and printed) image is mono-colored. Only black and white.</li>
|
||||
<li>Threshold is the "darkness" required for image pixels to turning black. The higher it is, the image is brighter.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2>Function</h2>
|
||||
<h3>Print Image</h3>
|
||||
<p>You can print an image to cat printer in that page.</p>
|
||||
<p>Select a photo, click preview, check the preview and adjust threshold.</p>
|
||||
<p>When satisfied, select printer device and print!</p>
|
||||
<h3>Print Document</h3>
|
||||
<p>Simply copy-paste your document content from office software to that page, the format is preserved and the result can be previewed.</p>
|
||||
<p>Image can't be there because of security policy. Please use "Custom Print" if necessory.</p>
|
||||
<h3>Custom Print</h3>
|
||||
<p>You can freely arrange the content of a canvas then print it. Insert/edit text, image, or QRCode.</p>
|
||||
<hr />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>帮助</title>
|
||||
<link rel="stylesheet" href="../skin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>帮助</h1>
|
||||
<p>这里有一些有用的信息。</p>
|
||||
<h2>注记</h2>
|
||||
<ul>
|
||||
<li>预览与打印出的图像是单色的。只有黑色和白色。</li>
|
||||
<li>阈值是像素变为黑色所需的“黑度”。它的值越高,图像越亮。</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2>功能</h2>
|
||||
<h3>打印图片</h3>
|
||||
<p>您可以在此页将图像打印至猫咪打印机。</p>
|
||||
<p>选择图片,点击预览,检查预览并调整阈值。</p>
|
||||
<p>感觉满意后,选择打印机设备并打印!</p>
|
||||
<h3>打印文档</h3>
|
||||
<p>只需把文档从办公软件复制粘贴到此页,文档格式被保留且可以预览。</p>
|
||||
<p>由于安全策略,无法粘贴图像。如果需要请使用“自定义打印”。</p>
|
||||
<h3>自定义打印</h3>
|
||||
<p>您可以自由排布画布上的内容并打印。插入/编辑文字、图像或二维码。</p>
|
||||
<hr />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Help</title>
|
||||
<link rel="stylesheet" href="../skin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Help Documents</h1>
|
||||
<nav>
|
||||
<a href="help.en-US.html">English</a>
|
||||
<a href="help.zh-CN.html">简体中文</a>
|
||||
</nav>
|
||||
<main></main>
|
||||
</body>
|
||||
</html>
|
BIN
www/icon.png
Normal file
BIN
www/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
211
www/image.js
Normal file
211
www/image.js
Normal file
@ -0,0 +1,211 @@
|
||||
|
||||
/**
|
||||
* Convert colored image to grayscale.
|
||||
* @param {Uint8ClampedArray} image_data `data` property of an ImageData instance,
|
||||
* i.e. `canvas.getContext('2d').getImageData(...).data`
|
||||
* @param {Uint8ClampedArray} mono_data an `Uint8ClampedArray` that have the size `w * h`
|
||||
* i.e. `image_data.length / 4`
|
||||
* The result data will be here, as a 8-bit grayscale image data.
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @param {boolean} transparencyAsWhite whether render opacity as white rather than black
|
||||
*/
|
||||
function monoGrayscale(image_data, mono_data, w, h, transparencyAsWhite) {
|
||||
let p, q, r, g, b, a, m;
|
||||
for (let j = 0; j < h; j++) {
|
||||
for (let i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
q = p * 4;
|
||||
[r, g, b, a] = image_data.slice(q, q + 4);
|
||||
a /= 255;
|
||||
if (a < 1 && transparencyAsWhite) {
|
||||
a = 1 - a;
|
||||
r += (255 - r) * a;
|
||||
g += (255 - g) * a;
|
||||
b += (255 - b) * a;
|
||||
}
|
||||
else { r *= a; g *= a; b *= a; }
|
||||
m = Math.floor(r * 0.2125 + g * 0.7154 + b * 0.0721);
|
||||
mono_data[p] = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The most simple monochrome algorithm, any value bigger than threshold is white, otherwise black.
|
||||
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @param {number} t threshold
|
||||
*/
|
||||
function monoDirect(data, w, h, t) {
|
||||
let p;
|
||||
for (let j = 0; j < h; j++) {
|
||||
for (let i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
data[p] = data[p] > t ? 255 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The widely used Floyd Steinberg algorithm, the most "natual" one.
|
||||
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @param {number} t threshold
|
||||
*/
|
||||
function monoSteinberg(data, w, h, t) {
|
||||
let p, m, n, o;
|
||||
function adjust(x, y, delta) {
|
||||
if (
|
||||
x < 0 || x >= w ||
|
||||
y < 0 || y >= h
|
||||
) return;
|
||||
p = y * w + x;
|
||||
data[p] += delta;
|
||||
}
|
||||
for (let j = 0; j < h; j++) {
|
||||
for (let i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
m = data[p];
|
||||
n = m > t ? 255 : 0;
|
||||
o = m - n;
|
||||
data[p] = n;
|
||||
adjust(i + 1, j , o * 7 / 16);
|
||||
adjust(i - 1, j + 1, o * 3 / 16);
|
||||
adjust(i , j + 1, o * 5 / 16);
|
||||
adjust(i + 1, j + 1, o * 1 / 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Work in Progress...)
|
||||
*/
|
||||
function monoHalftone(data, w, h, t) {}
|
||||
|
||||
/**
|
||||
* My own toy algorithm used in old versions. Not so natual.
|
||||
* It have 2 pass, horizonally and vertically.
|
||||
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @param {number} t threshold
|
||||
*/
|
||||
function monoLegacy(data, w, h, t) {
|
||||
let data_h = data.slice();
|
||||
let data_v = data.slice();
|
||||
monoLegacyH(data_h, w, h, t);
|
||||
monoLegacyV(data_v, w, h, t);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] = data_h[i] & data_v[i];
|
||||
}
|
||||
}
|
||||
function monoLegacyH(data, w, h, t) {
|
||||
let v = 0, p;
|
||||
for (let j = 0; j < h; j++) {
|
||||
for (let i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
v += data[p];
|
||||
if (v >= t) {
|
||||
data[p] = 255;
|
||||
v = 0;
|
||||
} else data[p] = 0;
|
||||
}
|
||||
v = 0;
|
||||
}
|
||||
}
|
||||
function monoLegacyV(data, w, h, t) {
|
||||
let v = 0, p;
|
||||
for (let i = 0; i < w; i++) {
|
||||
for (let j = 0; j < h; j++) {
|
||||
p = j * w + i;
|
||||
v += data[p];
|
||||
if (v >= t) {
|
||||
data[p] = 255;
|
||||
v = 0;
|
||||
} else data[p] = 0;
|
||||
}
|
||||
v = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly modified from `monoLegacy`, but still messy.
|
||||
* But, try the horizonal and vertical sub algorithm!
|
||||
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @param {number} t threshold
|
||||
*/
|
||||
function monoNew(data, w, h, t) {
|
||||
let data_h = data.slice();
|
||||
let data_v = data.slice();
|
||||
monoNewH(data_h, w, h, t);
|
||||
monoNewV(data_v, w, h, t);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] = data_h[i] & data_v[i];
|
||||
}
|
||||
}
|
||||
function monoNewH(data, w, h, t) {
|
||||
t = (t - 127) / 4 + 1;
|
||||
let v = 0, p;
|
||||
for (let j = 0; j < w; j++) {
|
||||
for (let i = 0; i < h; i++) {
|
||||
p = j * h + i;
|
||||
v += data[p] + t;
|
||||
if (v >= 255) {
|
||||
data[p] = 255;
|
||||
v -= 255;
|
||||
} else data[p] = 0;
|
||||
}
|
||||
v = 0;
|
||||
}
|
||||
}
|
||||
function monoNewV(data, w, h, t) {
|
||||
t = (t - 127) / 4 + 1;
|
||||
let v = -1, p;
|
||||
for (let i = 0; i < h; i++) {
|
||||
for (let j = 0; j < w; j++) {
|
||||
p = j * h + i;
|
||||
v += data[p] + t;
|
||||
if (v >= 255) {
|
||||
data[p] = 255;
|
||||
v -= 255;
|
||||
} else data[p] = 0;
|
||||
}
|
||||
v = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a monochrome image data to PBM mono image file data.
|
||||
* Returns a Blob containing the file data.
|
||||
* @param {Uint8ClampedArray} data the data that have a size of `w * h`
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function mono2pbm(data, w, h) {
|
||||
let result = new Uint8ClampedArray(data.length / 8);
|
||||
let slice, p;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
p = i * 8;
|
||||
slice = data.slice(p, p + 8);
|
||||
// Merge 8 bytes to 1 byte, and negate the bits
|
||||
// expecting in the data there's only 255 (0b11111111) or 0 (0b00000000)
|
||||
result[i] = (
|
||||
slice[0] & 0b10000000 |
|
||||
slice[1] & 0b01000000 |
|
||||
slice[2] & 0b00100000 |
|
||||
slice[3] & 0b00010000 |
|
||||
slice[4] & 0b00001000 |
|
||||
slice[5] & 0b00000100 |
|
||||
slice[6] & 0b00000010 |
|
||||
slice[7] & 0b00000001
|
||||
) ^ 0b11111111;
|
||||
}
|
||||
let pbm_data = new Blob([`P4\n${w} ${h}\n`, result]);
|
||||
return pbm_data;
|
||||
}
|
143
www/index.html
143
www/index.html
@ -2,30 +2,133 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Printer Center</title>
|
||||
<title>Cat Printer</title>
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
<link rel="stylesheet" href="skin.css" />
|
||||
<link rel="icon" href="icon.png" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="mainpage">
|
||||
<h1>Printer Center</h1>
|
||||
<nav>
|
||||
<a href="print-image.html" target="_blank">Print Image</a>
|
||||
<a href="print-document.html" target="_blank">Print Document</a>
|
||||
<a href="custom-print.html" target="_blank">Custom Print</a>
|
||||
<a href="help/index.html" target="_blank">Help</a>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="left">
|
||||
<h1 id="title" data-i18n="cat-printer">Cat Printer</h1>
|
||||
<div id="notice">
|
||||
<noscript>
|
||||
<span class="noscript">Please enable JavaScript!</span>
|
||||
</noscript>
|
||||
</div>
|
||||
<div class="panel" id="panel-print">
|
||||
<a href="#" data-i18n="print" data-default>Print</a>
|
||||
<label for="device-options" data-i18n="device-">Device:</label>
|
||||
<select id="device-options">
|
||||
<!-- Initialized by script -->
|
||||
</select>
|
||||
<button id="device-refresh" data-i18n="refresh">Refresh</button>
|
||||
<br />
|
||||
<label data-i18n="mode-">Mode:</label>
|
||||
<label>
|
||||
<input type="radio" name="mode" value="mode-canvas" checked />
|
||||
<span data-i18n="canvas">Canvas</span>
|
||||
</label>
|
||||
<!-- <label>
|
||||
<input type="radio" name="mode" value="mode-document" />
|
||||
<span data-i18n="document">Document</span>
|
||||
</label><br /> -->
|
||||
<button id="insert-picture" data-i18n="insert-picture">Insert Picture</button><br />
|
||||
</div>
|
||||
<div class="panel expanded" id="panel-help">
|
||||
<a href="#" data-i18n="help">Help</a>
|
||||
<div>
|
||||
<p data-i18n="coming-soon-">Coming Soon...</p>
|
||||
<p>
|
||||
<!-- LibreJS doesn't work with dynamically inserted script tag. Going to complain -->
|
||||
<a href="jslicense.html" data-jslicense="1"
|
||||
data-i18n="javascript-license-information">JavaScript License Information</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" id="panel-settings">
|
||||
<a href="#" data-i18n="settings">Settings</a>
|
||||
<div class="sub panel">
|
||||
<a href="#" data-i18n="image">Image</a>
|
||||
<label data-i18n="monochrome-algorithm-">Monochrome Algorithm:</label><br />
|
||||
<label>
|
||||
<input type="radio" name="algo" value="algo-direct" />
|
||||
<span data-i18n="direct">Direct</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="algo" value="algo-steinberg" checked />
|
||||
<span data-i18n="floyd-steinberg">Floyd Steinberg</span>
|
||||
</label><br />
|
||||
<!-- <label>
|
||||
<input type="radio" name="algo" value="algo-halftone" />
|
||||
<span data-i18n="halftone">Halftone</span>
|
||||
</label> -->
|
||||
<label>
|
||||
<input type="radio" name="algo" value="algo-new-h" />
|
||||
<span data-i18n="wave">Wave</span>
|
||||
</label>
|
||||
<!-- <label>
|
||||
<input type="radio" name="algo" value="algo-new-v" />
|
||||
<span data-i18n="fall">Fall</span>
|
||||
</label> -->
|
||||
<label>
|
||||
<input type="radio" name="algo" value="algo-legacy" />
|
||||
<span data-i18n="legacy">Legacy</span>
|
||||
</label><br />
|
||||
<label for="threshold" data-i18n="threshold-">Threshold:</label>
|
||||
<input type="range" min="0" max="255" value="128" id="threshold" step="8" data-default />
|
||||
<br />
|
||||
<input type="checkbox" name="transparent-as-white" id="transparent-as-white" checked />
|
||||
<label for="transparent-as-white" data-i18n="transparent-as-white">Transparent as White</label>
|
||||
</div>
|
||||
<div class="sub panel">
|
||||
<a href="#" data-i18n="printer">Printer</a>
|
||||
<label for="scan-time" data-i18n="scan-time-">Scan Time:</label>
|
||||
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" />
|
||||
<span data-i18n="-seconds">seconds</span>
|
||||
<br />
|
||||
<label for="frequency" data-i18n="transmission-speed-">Data Speed:</label>
|
||||
<select id="frequency">
|
||||
<option value="1.0" data-i18n="low">Low</option>
|
||||
<option value="0.8" data-i18n="moderate" selected>Moderate</option>
|
||||
<option value="0.6" data-i18n="high">High</option>
|
||||
</select>
|
||||
<br />
|
||||
<input type="checkbox" name="dry-run" id="dry-run" />
|
||||
<label for="dry-run" data-i18n="dry-run">Dry Run</label>
|
||||
</div>
|
||||
<div class="sub panel">
|
||||
<a href="#" data-i18n="system">System</a>
|
||||
<input type="checkbox" name="no-animation" id="no-animation" />
|
||||
<label for="no-animation" data-i18n="disable-page-animation">Disable Page Animation</label>
|
||||
</div>
|
||||
<div class="center">
|
||||
<button id="button-exit" data-i18n="exit">Exit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel hidden" id="panel-error">
|
||||
<a href="#" data-i18n="error-message">Error Message</a>
|
||||
<p data-i18n="you-can-seek-for-help-with-detailed-info-below">You can seek for help with detailed info below.</p>
|
||||
<div id="error-record"></div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<!-- <button id="button-preview" data-i18n="preview">Preview</button> -->
|
||||
<button id="button-print" data-i18n="print">Print</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<canvas id="control-canvas" class="disabled" width="384" height="384"></canvas>
|
||||
<div id="control-document" class="disabled" contenteditable="true"></div>
|
||||
<canvas id="preview" width="384" height="384"></canvas>
|
||||
<div class="center">
|
||||
<!-- <button id="canvas-expand" data-i18n="expand">Expand</button>
|
||||
<button id="canvas-crop" data-i18n="crop">Crop</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<textarea id="i18-N" style="display: none;">
|
||||
[zh-CN]
|
||||
Printer Center=打印机中心
|
||||
Print Image=打印图片
|
||||
Print Document=打印文档
|
||||
Custom Print=自定义打印
|
||||
Help=帮助
|
||||
</textarea>
|
||||
<script src="i18n.js"></script>
|
||||
<div id="hidden" class="hidden">
|
||||
<!-- Hidden area for putting elements -->
|
||||
</div>
|
||||
<script src="loader.js"></script>
|
||||
</body>
|
||||
</html>
|
92
www/jslicense.html
Normal file
92
www/jslicense.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JavaScript License Information</title>
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
<link rel="icon" href="icon.png" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div>
|
||||
<h1>JavaScript License Information</h1>
|
||||
<p>You can see all JavaScript used along with this application are Free Software.<sup><a href="#footer-1">[1]</sup></a></p>
|
||||
<p class="center">
|
||||
<a href="/">Go Back</a>
|
||||
</p>
|
||||
<div class="table-wrap">
|
||||
<table id="jslicense-labels1">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Resource</td>
|
||||
<td>License</td>
|
||||
<td>Source</td>
|
||||
<td>Description</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="loader.js">loader.js</a></td>
|
||||
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
|
||||
<td><a href="loader.js">loader.js</a></td>
|
||||
<td>For dynamically loading other scripts, and fallback if there are problems.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="image.js">image.js</a></td>
|
||||
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
|
||||
<td><a href="image.js">image.js</a></td>
|
||||
<td>Contains functions for image manipulation and public algorithms of image monochrome filters.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="i18n.js">i18n.js</a></td>
|
||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
||||
<td><a href="https://github.com/roddeh/i18njs">i18n.js</a></td>
|
||||
<td>For internationalization (language support)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="main.js">main.js</a></td>
|
||||
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU-GPL-3.0-or-later</a></td>
|
||||
<td><a href="main.js">main.js</a></td>
|
||||
<td>The main script for Cat-Printer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="polyfill.js">polyfill.js</a></td>
|
||||
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
|
||||
<td><a href="polyfill.js">polyfill.js</a></td>
|
||||
<td>Put features that are not supported by old browsers.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="main.comp.js">main.comp.js</a></td>
|
||||
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU-GPL-3.0-or-later</a></td>
|
||||
<td><a href="main.js">main.js</a></td>
|
||||
<td>A bundle of transpiled scripts (polyfill.js, image.js, i18n.js, main.js), for compatibility to old browsers.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="vconsole.js">vconsole.js</a></td>
|
||||
<td><a href="http://opensource.org/licenses/MIT">Expat</a></td>
|
||||
<td><a href="https://github.com/Tencent/vConsole">vconsole.js</a></td>
|
||||
<td>A mini console for debugging on mobile, will only load when explicitly invoke.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<hr />
|
||||
<footer>
|
||||
<dl id="footer-1">
|
||||
<dt>
|
||||
<span>[1]</span>
|
||||
<a href="https://www.gnu.org/philosophy/free-sw.html">Free Software</a>
|
||||
</dt>
|
||||
<dd>
|
||||
<span>Software that respects your freedom.</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
56
www/lang/en-US.json
Normal file
56
www/lang/en-US.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"values": {
|
||||
"cat-printer": "Cat Printer",
|
||||
"printer": "Printer",
|
||||
"device-": "Device:",
|
||||
"refresh": "Refresh",
|
||||
"mode-": "Mode:",
|
||||
"canvas": "Canvas",
|
||||
"document": "Document",
|
||||
"insert-picture": "Insert Picture",
|
||||
"help": "Help",
|
||||
"javascript-license-information": "JavaScript License Information",
|
||||
"settings": "Settings",
|
||||
"image": "Image",
|
||||
"monochrome-algorithm-": "Monochrome Algorithm:",
|
||||
"direct": "Direct",
|
||||
"floyd-steinberg": "Floyd Steinberg",
|
||||
"halftone": "Halftone",
|
||||
"wave": "Wave",
|
||||
"fall": "Fall",
|
||||
"legacy": "Legacy",
|
||||
"threshold-": "Threshold",
|
||||
"transmission-speed-": "Transmission Speed:",
|
||||
"low": "Low",
|
||||
"moderate": "Moderate",
|
||||
"high": "High",
|
||||
"transparent-as-white": "Transparent as White",
|
||||
"misc": "Misc",
|
||||
"system": "System",
|
||||
"disable-page-animation": "Disable Page Animation",
|
||||
"exit": "Exit",
|
||||
"error-message": "Error Message",
|
||||
"preview": "Preview",
|
||||
"print": "Print",
|
||||
"expand": "Expand",
|
||||
"crop": "Crop",
|
||||
"scanning-for-devices": "Scanning for devices...",
|
||||
"scan-time-": "Scan time:",
|
||||
"-seconds": "seconds",
|
||||
"no-available-devices-found": "No available devices found",
|
||||
"found-1-available-devices": [
|
||||
[1, 1, "Found 1 available device"],
|
||||
[2, null, "Found %n available devices"]
|
||||
],
|
||||
"please-check-if-the-printer-is-down": "Please check if the printer is down",
|
||||
"printing": "Printing...",
|
||||
"finished": "Finished",
|
||||
"coming-soon-": "Coming Soon...",
|
||||
"dry-run": "Dry Run",
|
||||
"dry-run-test-print-process-only": "Dry Run: test print process only",
|
||||
"you-can-close-this-page-manually": "You can close this page manually",
|
||||
"please-enable-bluetooth": "Please enable Bluetooth",
|
||||
"error-happened-please-check-error-message": "Error happened, please check error message",
|
||||
"you-can-seek-for-help-with-detailed-info-below": "You can seek for help with detailed info below."
|
||||
}
|
||||
}
|
55
www/lang/zh-CN.json
Normal file
55
www/lang/zh-CN.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"values": {
|
||||
"cat-printer": "猫咪打印机",
|
||||
"printer": "打印机",
|
||||
"device-": "设备:",
|
||||
"refresh": "刷新",
|
||||
"mode-": "模式:",
|
||||
"canvas": "画布",
|
||||
"document": "文档",
|
||||
"insert-picture": "插入图片",
|
||||
"help": "帮助",
|
||||
"javascript-license-information": "JavaScript 许可证信息",
|
||||
"settings": "设置",
|
||||
"monochrome-algorithm-": "单色化算法:",
|
||||
"direct": "直接",
|
||||
"image": "图像",
|
||||
"floyd-steinberg": "科学",
|
||||
"halftone": "点状",
|
||||
"wave": "波纹",
|
||||
"fall": "下落",
|
||||
"legacy": "旧版",
|
||||
"threshold-": "阈值:",
|
||||
"transmission-speed-": "传输速度:",
|
||||
"low": "低",
|
||||
"moderate": "适中",
|
||||
"high": "高",
|
||||
"transparent-as-white": "透明为白色",
|
||||
"misc": "杂项",
|
||||
"system": "系统",
|
||||
"disable-page-animation": "禁用页面动画",
|
||||
"exit": "退出",
|
||||
"error-message": "错误消息",
|
||||
"preview": "预览",
|
||||
"print": "打印",
|
||||
"expand": "扩大",
|
||||
"crop": "裁减",
|
||||
"scanning-for-devices": "正在扫描设备……",
|
||||
"scan-time-": "扫描时间:",
|
||||
"-seconds": "秒",
|
||||
"no-available-devices-found": "未发现可用设备",
|
||||
"found-1-available-devices": [
|
||||
[1, null, "发现 %n 个可用设备"]
|
||||
],
|
||||
"please-check-if-the-printer-is-down": "请检查打印机是否已关闭",
|
||||
"printing": "打印中……",
|
||||
"finished": "完成",
|
||||
"coming-soon-": "即将到来……",
|
||||
"dry-run": "干运行",
|
||||
"dry-run-test-print-process-only": "干运行:仅测试打印流程",
|
||||
"you-can-close-this-page-manually": "您可手动关闭此页面",
|
||||
"please-enable-bluetooth": "请启用蓝牙",
|
||||
"error-happened-please-check-error-message": "发生错误,请检查错误消息",
|
||||
"you-can-seek-for-help-with-detailed-info-below": "您可以使用以下详细信息寻求帮助。"
|
||||
}
|
||||
}
|
39
www/loader.js
Normal file
39
www/loader.js
Normal file
@ -0,0 +1,39 @@
|
||||
|
||||
/**
|
||||
* Satisfy both development and old-old webView need
|
||||
*/
|
||||
(function() {
|
||||
|
||||
var fallbacks = [
|
||||
// main scripts, which we will directly modify
|
||||
'i18n.js', 'image.js', 'main.js',
|
||||
// "compatibility" script, produced with eg. typescript tsc
|
||||
'main.comp.js'
|
||||
];
|
||||
var trial_count = 0;
|
||||
/**
|
||||
* Try to load next "fallback" script,
|
||||
* until we see the "main" variable (ie. success)
|
||||
* fail if nothing is left to load.
|
||||
* This is recursive. Just call once.
|
||||
*/
|
||||
function try_load() {
|
||||
var script = document.createElement('script');
|
||||
script.addEventListener('load', function() {
|
||||
if (typeof main === 'undefined') {
|
||||
script.remove();
|
||||
try_load();
|
||||
} else {
|
||||
console.log('Success');
|
||||
}
|
||||
});
|
||||
var path = fallbacks[trial_count++];
|
||||
if (!path) throw new Error('All fallback scripts were tried');
|
||||
script.src = path;
|
||||
console.log('Trying script: ' + path);
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
try_load();
|
||||
|
||||
})();
|
317
www/main.css
317
www/main.css
@ -1,46 +1,297 @@
|
||||
|
||||
:root {
|
||||
--font-size: 1.2em;
|
||||
--span: 8px;
|
||||
--span-half: calc(var(--span) / 2);
|
||||
--span-double: calc(var(--span) * 2);
|
||||
--border: 1px;
|
||||
--border-double: calc(var(--border) * 2);
|
||||
--paper-width: 384px;
|
||||
--anim-time: 0.5s;
|
||||
--fore-color: #111;
|
||||
--back-color: #eee;
|
||||
--canvas-back: #fff;
|
||||
--curve: cubic-bezier(.08,.82,.17,1);
|
||||
--panel-height: 20em;
|
||||
--target-color: rgba(0, 255, 0, 0.2);
|
||||
--notice-color: rgba(0, 0, 255, 0.2);
|
||||
--notice-warning: rgba(255, 128, 0, 0.2);
|
||||
--notice-error: rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body.no-animation *,
|
||||
body.no-animation *::before,
|
||||
body.no-animation *::after {
|
||||
transition-duration: 0ms !important;
|
||||
transition: none;
|
||||
animation-timing-function: steps(1);
|
||||
animation-duration: 0ms !important;
|
||||
}
|
||||
|
||||
body {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: large;
|
||||
border: none;
|
||||
background-color: var(--back-color);
|
||||
color: var(--fore-color);
|
||||
font-size: var(--font-size);
|
||||
font-family: 'Noto Sans', 'Segoe UI', sans-serif;
|
||||
overflow: auto;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
main.mainpage nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
margin: var(--span-half) 0;
|
||||
}
|
||||
|
||||
main.mainpage nav a {
|
||||
display: inline-block;
|
||||
font-size: larger;
|
||||
width: 10em;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: blue;
|
||||
color: #33f;
|
||||
}
|
||||
a:hover, a:active {
|
||||
color: darkblue;
|
||||
color: #22f;
|
||||
}
|
||||
|
||||
main input[type="range"] {
|
||||
width: 16em;
|
||||
a {
|
||||
transition: all var(--anim-time) ease-out;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
button, input, select, textarea {
|
||||
font: inherit;
|
||||
color: var(--fore-color);
|
||||
}
|
||||
button, input[type="number"], input[type="text"], select {
|
||||
margin: var(--span-half) var(--span);
|
||||
border: var(--border) solid var(--fore-color);
|
||||
padding: var(--span-half) var(--span);
|
||||
background-color: transparent;
|
||||
transition: all var(--anim-time) var(--curve);
|
||||
cursor: pointer;
|
||||
min-width: 6em;
|
||||
display: inline-block;
|
||||
}
|
||||
input[type="number"], input[type="text"] {
|
||||
width: 6em;
|
||||
cursor: text;
|
||||
}
|
||||
button:hover {
|
||||
margin: 0;
|
||||
padding: var(--span) calc(var(--span-double));
|
||||
min-width: calc(6em + var(--span-double));
|
||||
}
|
||||
button:active {
|
||||
box-shadow: 0 0 var(--span) inset var(--fore-color);
|
||||
}
|
||||
|
||||
#notice {
|
||||
height: 2em;
|
||||
border-top: 1px dotted currentColor;
|
||||
border-bottom: 1px dotted currentColor;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 8px 0;
|
||||
min-height: var(--font-size);
|
||||
background-color: var(--notice-color);
|
||||
/* border: var(--border) solid var(--fore-color); */
|
||||
}
|
||||
#notice.warning, #button-exit {
|
||||
background-color: var(--notice-warning);
|
||||
}
|
||||
#notice.error {
|
||||
background-color: var(--notice-error);
|
||||
}
|
||||
.noscript {
|
||||
background-color: var(--notice-error);
|
||||
}
|
||||
main, header, footer {
|
||||
max-width: 45em;
|
||||
margin: 1em auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* flex-wrap: wrap; */
|
||||
}
|
||||
main>.left {
|
||||
flex-grow: 1;
|
||||
/* position: sticky; */
|
||||
/* top: 0; */
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
margin: var(--span);
|
||||
min-width: 12em;
|
||||
}
|
||||
main>.right {
|
||||
flex-grow: 0;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
min-width: calc(var(--paper-width) + var(--border-double) + var(--span-double));
|
||||
}
|
||||
canvas#preview, canvas#control-canvas, #control-document {
|
||||
border: var(--border) solid var(--fore-color);
|
||||
background-color: var(--canvas-back);
|
||||
width: var(--paper-width);
|
||||
display: inline-block;
|
||||
}
|
||||
#control-document {
|
||||
font: initial;
|
||||
font-size: 16px;
|
||||
font-family: 'Unifont';
|
||||
text-align: initial;
|
||||
color: #000;
|
||||
}
|
||||
#control-document.normal-font {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
canvas#preview {
|
||||
z-index: 0;
|
||||
}
|
||||
canvas#control-canvas,
|
||||
#control-document {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity var(--anim-time) var(--curve);
|
||||
z-index: 1;
|
||||
}
|
||||
canvas#control-canvas:hover,
|
||||
#control-document:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
canvas#control-canvas.disabled,
|
||||
#control-document.disabled {
|
||||
display: none;
|
||||
}
|
||||
p {
|
||||
margin: var(--span) 0;
|
||||
}
|
||||
.panel {
|
||||
border: var(--border) solid currentColor;
|
||||
height: calc(var(--font-size) + 8px);
|
||||
overflow: hidden;
|
||||
/* scrollbar-width: thin; */
|
||||
transition: height var(--anim-time) var(--curve);
|
||||
padding: 0 var(--span);
|
||||
margin: var(--span) 0;
|
||||
}
|
||||
.panel::before {
|
||||
float: left;
|
||||
}
|
||||
.panel:not(.sub)::before { content: '📌'; }
|
||||
.panel.sub::before { content: '📎'; }
|
||||
.panel.sub {
|
||||
border-width: var(--border) 0 0 0;
|
||||
}
|
||||
.panel.expanded {
|
||||
height: var(--panel-height);
|
||||
animation: delay-scrollable var(--anim-time) steps(1) 0s 1 forwards;
|
||||
/* overflow-y: scroll; */
|
||||
}
|
||||
.panel.sub.expanded {
|
||||
height: calc(var(--panel-height) / 2);
|
||||
}
|
||||
.panel>:nth-child(1),
|
||||
.panel>:nth-child(1):link,
|
||||
.panel>:nth-child(1):visited {
|
||||
display: block;
|
||||
margin: var(--span-half);
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
color: var(--fore-color);
|
||||
}
|
||||
.panel>:nth-child(1)::before {
|
||||
content: '>>';
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: visible;
|
||||
right: 2.5em;
|
||||
transition: all var(--anim-time) var(--curve);
|
||||
}
|
||||
.panel>:nth-child(1):hover::before,
|
||||
.panel>:nth-child(1):active::before {
|
||||
opacity: 1;
|
||||
right: 1.5em;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 10em;
|
||||
vertical-align: middle;
|
||||
content: attr(value);
|
||||
}
|
||||
@keyframes hint {
|
||||
0% { box-shadow: 0 0 var(--span-) inset transparent; }
|
||||
50% { box-shadow: 0 0 var(--span) inset var(--fore-color); }
|
||||
100% { box-shadow: 0 0 var(--span) inset transparent; }
|
||||
}
|
||||
.hint {
|
||||
animation: hint 3s ease-out 0.1s infinite;
|
||||
}
|
||||
#hidden, .hidden { display: none; }
|
||||
#error-record {
|
||||
font-family: 'DejaVu Sans Mono', 'Consolas', monospace;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
height: calc(var(--panel-height) - var(--border-double) * 4);
|
||||
}
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.reset_styles {
|
||||
font-family: initial;
|
||||
font-size: initial;
|
||||
text-align: initial;
|
||||
table#jslicense-labels1 {
|
||||
min-width: 40em;
|
||||
}
|
||||
table#jslicense-labels1 td {
|
||||
padding: var(--span-half) var(--span);
|
||||
}
|
||||
*:target {
|
||||
background-color: var(--target-color);
|
||||
}
|
||||
dl {
|
||||
margin: var(--span) 0;
|
||||
display: block;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: var(--border) solid var(--fore-color);
|
||||
}
|
||||
|
||||
@keyframes delay-scrollable {
|
||||
from { overflow: hidden; }
|
||||
to { overflow: auto; }
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
:root {
|
||||
--panel-height: 16em;
|
||||
/* --font-size: 1em; */
|
||||
}
|
||||
main {
|
||||
flex-direction: column;
|
||||
}
|
||||
main>.left {
|
||||
/* height: 16em; */
|
||||
overflow: auto;
|
||||
width: calc(100% - var(--span-double) - var(--border-double));
|
||||
}
|
||||
main>.right {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
@media (max-width: 384px) {
|
||||
canvas#preview, canvas#control-canvas, #control-document {
|
||||
width: calc(100% - var(--border-double));
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--fore-color: #eee;
|
||||
--back-color: #333;
|
||||
/* --canvas-back: #666; */
|
||||
}
|
||||
a:link, a:visited {
|
||||
color: #66f;
|
||||
}
|
||||
a:hover, a:active {
|
||||
color: #77f;
|
||||
}
|
||||
canvas#preview, canvas#control-canvas, #control-document {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Unifont';
|
||||
src: local('Unifont') url('unifont.ttf') url('unifont.otf');
|
||||
}
|
||||
|
34
www/main.d.ts
vendored
34
www/main.d.ts
vendored
@ -1,34 +0,0 @@
|
||||
|
||||
declare interface i18NProto {
|
||||
get(originaltext: string, language: string): string;
|
||||
force(language: string): void;
|
||||
recover(): void;
|
||||
}
|
||||
|
||||
declare var i18N: i18NProto;
|
||||
|
||||
declare interface ImagePrinter {
|
||||
noticeElement: HTMLParagraphElement;
|
||||
thresholdInput: HTMLInputElement;
|
||||
bluetoothMACInput: HTMLInputElement;
|
||||
fileSelection: HTMLInputElement;
|
||||
dummyImage: HTMLImageElement;
|
||||
canvasPreview: HTMLCanvasElement;
|
||||
previewButton: HTMLButtonElement;
|
||||
printButton: HTMLButtonElement;
|
||||
monoMethod: Function;
|
||||
}
|
||||
|
||||
declare interface DocumentPrinter {
|
||||
thresholdInput: HTMLInputElement;
|
||||
bluetoothMACInput: HTMLInputElement;
|
||||
container: HTMLDivElement;
|
||||
printButton: HTMLButtonElement;
|
||||
canvasPreview: HTMLDivElement;
|
||||
monoMethod: Function;
|
||||
}
|
||||
|
||||
declare function notice(message: string): void;
|
||||
declare function imageDataColorToMonoSquare(data: ImageData, threshold: number): ImageData;
|
||||
declare function imageDataColorToMonoDiamond(data: ImageData, threshold: number): ImageData;
|
||||
declare function imageDataMonoToPBM(data: ImageData): Blob;
|
704
www/main.js
704
www/main.js
@ -1,181 +1,547 @@
|
||||
///<reference path="main.d.ts" />
|
||||
|
||||
let notice_element = document.getElementById('notice');
|
||||
function notice(message) {
|
||||
notice_element.innerText = message;
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* In order to debug on a phone, we load vConsole
|
||||
* https://www.npmjs.com/package/vconsole
|
||||
* Double-tap the "Cat Printer" title to activate
|
||||
*/
|
||||
function debug() {
|
||||
let script = document.createElement('script');
|
||||
script.src = 'vconsole.js';
|
||||
document.body.appendChild(script);
|
||||
script.addEventListener('load', () => new window.VConsole());
|
||||
}
|
||||
let device_selection = document.getElementById('device_selection');
|
||||
let refresh_device_button = document.getElementById('refresh_device');
|
||||
let bluetooth_mac_input = document.getElementById('bluetooth_address_input');
|
||||
function switchDevice() {
|
||||
bluetooth_mac_input.value = device_selection.selectedOptions[0].value;
|
||||
}
|
||||
device_selection.addEventListener('input', switchDevice);
|
||||
if (device_selection != null && refresh_device_button != null && bluetooth_mac_input != null) {
|
||||
refresh_device_button.addEventListener('click', event => {
|
||||
notice(i18N.get('Searching devices. Please wait for 5 seconds.'));
|
||||
device_selection.childNodes.forEach(e => e.remove());
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/~getdevices');
|
||||
xhr.onload = () => {
|
||||
for (let i of xhr.responseText.split('\n')) {
|
||||
let [name, address] = i.split(',');
|
||||
if (address == undefined) continue;
|
||||
let option = document.createElement('option');
|
||||
option.value = address;
|
||||
option.innerText = `${name} - ${address}`;
|
||||
device_selection.appendChild(option);
|
||||
}
|
||||
device_selection.selectedIndex = 0;
|
||||
switchDevice();
|
||||
document.getElementById('title').addEventListener('dblclick', debug);
|
||||
|
||||
var hidden_area = document.getElementById('hidden');
|
||||
|
||||
const hint = (function() {
|
||||
let hints = [];
|
||||
let callback = (event) => {
|
||||
event.stopPropagation();
|
||||
event.currentTarget.classList.remove('hint');
|
||||
event.currentTarget.removeEventListener('click', callback);
|
||||
}
|
||||
return function(selector) {
|
||||
hints.forEach(element => element.classList.remove('hint'));
|
||||
hints = document.querySelectorAll(selector);
|
||||
hints.forEach(element => {
|
||||
element.classList.add('hint');
|
||||
element.addEventListener('click', callback);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
class _Notice {
|
||||
element;
|
||||
constructor() {
|
||||
this.element = document.getElementById('notice');
|
||||
}
|
||||
_message(message, ...args) {
|
||||
this.element.innerText = i18n(message, ...args) || message;
|
||||
}
|
||||
makeLogger(class_name) {
|
||||
return (message, ...args) => {
|
||||
this.element.classList.value = class_name;
|
||||
this._message(message, ...args);
|
||||
}
|
||||
}
|
||||
notice = this.makeLogger('notice');
|
||||
warn = this.makeLogger('warning');
|
||||
error = this.makeLogger('error');
|
||||
}
|
||||
|
||||
const Notice = new _Notice();
|
||||
|
||||
class _ErrorHandler {
|
||||
recordElement;
|
||||
constructor() {
|
||||
this.recordElement = document.getElementById('error-record');
|
||||
}
|
||||
/**
|
||||
* @param {Error} error
|
||||
* @param {string} output
|
||||
*/
|
||||
report(error, output) {
|
||||
Notice.error('error-happened-please-check-error-message');
|
||||
let hidden_panel = this.recordElement.parentElement;
|
||||
if (hidden_panel) hidden_panel.classList.remove('hidden');
|
||||
let div = document.createElement('div');
|
||||
div.innerText = (error.stack || (error.name + ': ' + error.message)) + '\n' + output;
|
||||
this.recordElement.appendChild(div);
|
||||
hint('#panel-error');
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorHandler = new _ErrorHandler();
|
||||
|
||||
/**
|
||||
* Call server API
|
||||
* @param {string} path API entry, as a path
|
||||
* @param {any} body JSON to send
|
||||
* @param {(response: Response) => Promise<any>} errorPreHandler
|
||||
* An async function for handling the problem where `response.ok` is false.
|
||||
* Omit or use `return Promise.reject()` to do final failure, or return something else to circumstance
|
||||
*/
|
||||
async function callApi(path, body, errorPreHandler) {
|
||||
body = body || {};
|
||||
return await fetch(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
}).then(async (response) => {
|
||||
if (response.ok) return response.json()
|
||||
else {
|
||||
try {
|
||||
// forgive this dirty trick
|
||||
let json = response.json();
|
||||
response.json = () => json;
|
||||
if (errorPreHandler) return await errorPreHandler(response);
|
||||
else throw new Error('API Failure');
|
||||
} catch (error) {
|
||||
ErrorHandler.report(
|
||||
error,
|
||||
JSON.stringify(await response.json(), undefined, 4)
|
||||
)
|
||||
return Promise.reject('API Failure');
|
||||
}
|
||||
}
|
||||
xhr.send();
|
||||
});
|
||||
refresh_device_button.click();
|
||||
}
|
||||
function imageDataColorToMonoSquare(data, threshold) {
|
||||
let newdata_horizonal = new Uint8ClampedArray(data.data.length);
|
||||
let newdata_vertical = new Uint8ClampedArray(data.data.length);
|
||||
let darkness = 0;
|
||||
for (let j = 0; j < data.height; j++) {
|
||||
for (let i = 0; i < data.width; i++) {
|
||||
let index = (j * data.width + i) * 4;
|
||||
let [r, g, b, a] = data.data.slice(index, index + 4);
|
||||
let visibility = 1 - ((r * 0.2125) + (g * 0.7154) + (b * 0.0721)) * (a / 255) / 255;
|
||||
darkness += visibility;
|
||||
if (darkness >= threshold) {
|
||||
newdata_horizonal[index] = 0;
|
||||
newdata_horizonal[index + 1] = 0;
|
||||
newdata_horizonal[index + 2] = 0;
|
||||
newdata_horizonal[index + 3] = 255;
|
||||
darkness = 0;
|
||||
} else {
|
||||
newdata_horizonal[index] = 255;
|
||||
newdata_horizonal[index + 1] = 255;
|
||||
newdata_horizonal[index + 2] = 255;
|
||||
newdata_horizonal[index + 3] = 255;
|
||||
/**
|
||||
* call addEventListener on all selected elements by `seletor`,
|
||||
* with each element itself as `this` unless specifyed `thisArg`,
|
||||
* with type `type` and a function `callback`.
|
||||
* If an element have attribute `data-default` or `checked`, dispatch event immediately on it.
|
||||
* You can of course assign resulting object to a variable for futher use.
|
||||
*/
|
||||
class EventPutter {
|
||||
elements;
|
||||
callback;
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} type
|
||||
* @param {(event?: Event) => void} callback
|
||||
* @param {any} thisArg
|
||||
*/
|
||||
constructor(selector, type, callback, thisArg) {
|
||||
let elements = this.elements = document.querySelectorAll(selector);
|
||||
if (elements.length === 0) return;
|
||||
this.callback = callback;
|
||||
elements.forEach(element => {
|
||||
element.addEventListener(type, function(event) {
|
||||
event.stopPropagation();
|
||||
event.cancelBubble = true;
|
||||
callback.call(thisArg || element, event);
|
||||
});
|
||||
if (element.hasAttribute('data-default') || element.checked) {
|
||||
element.dispatchEvent(new Event(type));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {string} type
|
||||
* @param {(event?: Event) => void} callback
|
||||
* @param {any} thisArg
|
||||
*/
|
||||
function putEvent(selector, type, callback, thisArg) {
|
||||
return new EventPutter(selector, type, callback, thisArg);
|
||||
}
|
||||
|
||||
class PanelController {
|
||||
last;
|
||||
panels;
|
||||
outerPanels;
|
||||
subPanels;
|
||||
constructor(selector = '.panel') {
|
||||
const class_expanded = 'expanded';
|
||||
const class_sub = 'sub';
|
||||
let panels = this.panels = [... document.querySelectorAll(selector)];
|
||||
let outer_panels = this.outerPanels = panels.filter(e => !e.classList.contains(class_sub));
|
||||
let sub_panels = this.subPanels = panels.filter(e => e.classList.contains(class_sub));
|
||||
const expand = (panel) => panel.classList.add(class_expanded);
|
||||
const fold = (panel) => {
|
||||
panel.classList.remove(class_expanded);
|
||||
}
|
||||
const fold_all_outer = () => outer_panels.forEach(e => fold(e));
|
||||
const fold_all_sub = () => sub_panels.forEach(e => fold(e));
|
||||
// const fold_all = () => panels.forEach(e => e.classList.remove(class_expanded));
|
||||
fold_all_outer();
|
||||
putEvent(selector + '>:nth-child(1)', 'click', event => {
|
||||
event.stopPropagation();
|
||||
event.cancelBubble = true;
|
||||
let current = event.currentTarget.parentElement,
|
||||
last = this.last;
|
||||
this.last = current;
|
||||
if (!last) {
|
||||
expand(current);
|
||||
this.last = current;
|
||||
return;
|
||||
}
|
||||
let is_sub = current.classList.contains(class_sub),
|
||||
last_is_sub = last.classList.contains(class_sub);
|
||||
if (current.classList.contains(class_expanded)) {
|
||||
fold(current);
|
||||
return;
|
||||
}
|
||||
fold_all_outer();
|
||||
if (is_sub && last_is_sub) {
|
||||
fold(last);
|
||||
expand(current.parentElement);
|
||||
last.scrollTo(0, 0);
|
||||
} else if (is_sub && !last_is_sub) {
|
||||
fold_all_sub();
|
||||
expand(last);
|
||||
} else if (!is_sub && last_is_sub) {
|
||||
last.parentElement.scrollTo(0, 0);
|
||||
}
|
||||
expand(current);
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
class CanvasController {
|
||||
/** @type {HTMLCanvasElement} */
|
||||
preview;
|
||||
/** @type {HTMLCanvasElement} */
|
||||
canvas;
|
||||
div;
|
||||
isCanvas;
|
||||
algorithm;
|
||||
threshold;
|
||||
transparentAsWhite;
|
||||
previewData;
|
||||
static defaultHeight = 384;
|
||||
_height;
|
||||
get height() {
|
||||
return this._height;
|
||||
}
|
||||
set height(value) {
|
||||
this.div.style.height = (this.canvas.height = this.preview.height = this._height = value) + 'px';
|
||||
}
|
||||
constructor() {
|
||||
this.preview = document.getElementById('preview');
|
||||
this.canvas = document.getElementById('control-canvas');
|
||||
this.div = document.getElementById('control-document');
|
||||
this.height = CanvasController.defaultHeight;
|
||||
|
||||
putEvent('input[name="mode"]', 'change', (event) => this.enableMode(event.currentTarget.value), this);
|
||||
putEvent('input[name="algo"]', 'change', (event) => this.useAlgorithm(event.currentTarget.value), this);
|
||||
putEvent('#button-preview' , 'click', this.activatePreview , this);
|
||||
putEvent('#canvas-expand' , 'click', this.expand , this);
|
||||
putEvent('#canvas-crop' , 'click', this.crop , this);
|
||||
putEvent('#insert-picture' , 'click', this.insertPicture , this);
|
||||
|
||||
putEvent('#threshold', 'change', (event) => {
|
||||
this.threshold = parseInt(event.currentTarget.value);
|
||||
this.activatePreview();
|
||||
}, this);
|
||||
putEvent('#transparent-as-white', 'change', (event) => {
|
||||
this.transparentAsWhite = event.currentTarget.checked;
|
||||
this.activatePreview();
|
||||
}, this);
|
||||
}
|
||||
enableMode(mode) {
|
||||
switch (mode) {
|
||||
case 'mode-document':
|
||||
this.div.classList.remove('disabled');
|
||||
this.canvas.classList.add('disabled');
|
||||
this.isCanvas = false;
|
||||
break;
|
||||
case 'mode-canvas':
|
||||
this.canvas.classList.remove('disabled');
|
||||
this.div.classList.add('disabled');
|
||||
this.isCanvas = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
useAlgorithm(name) {
|
||||
this.algorithm = name;
|
||||
this.activatePreview();
|
||||
}
|
||||
expand(length = CanvasController.defaultHeight) {
|
||||
this.height += length;
|
||||
}
|
||||
crop() {}
|
||||
activatePreview() {
|
||||
let preview = this.preview;
|
||||
let t = this.threshold;
|
||||
if (this.isCanvas) {
|
||||
let canvas = this.canvas;
|
||||
let w = canvas.width, h = canvas.height;
|
||||
let context_c = canvas.getContext('2d');
|
||||
let context_p = preview.getContext('2d');
|
||||
let data = context_c.getImageData(0, 0, w, h);
|
||||
let mono_data = new Uint8ClampedArray(w * h);
|
||||
monoGrayscale(data.data, mono_data, w, h, this.transparentAsWhite);
|
||||
switch (this.algorithm) {
|
||||
case 'algo-direct':
|
||||
monoDirect(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-steinberg':
|
||||
monoSteinberg(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-halftone':
|
||||
// monoHalftone(mono_data, w, h, t);
|
||||
// Sorry, do it later
|
||||
break;
|
||||
case 'algo-new':
|
||||
monoNew(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-new-h':
|
||||
monoNewH(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-new-v':
|
||||
monoNewV(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-legacy':
|
||||
monoLegacy(mono_data, w, h, t);
|
||||
break;
|
||||
}
|
||||
let new_data = context_p.createImageData(w, h);
|
||||
let p;
|
||||
for (let i = 0; i < mono_data.length; i++) {
|
||||
p = i * 4;
|
||||
new_data.data.fill(mono_data[i], p, p + 3);
|
||||
new_data.data[p + 3] = 255;
|
||||
}
|
||||
this.previewData = mono_data;
|
||||
context_p.putImageData(new_data, 0, 0);
|
||||
}
|
||||
}
|
||||
insertPicture() {
|
||||
const put_image = (url) => {
|
||||
if (this.isCanvas) {
|
||||
let img = document.createElement('img');
|
||||
img.src = url;
|
||||
hidden_area.appendChild(img);
|
||||
img.addEventListener('load', () => {
|
||||
let canvas = this.canvas;
|
||||
let rate = img.height / img.width;
|
||||
this.height = canvas.width * rate;
|
||||
let context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
this.crop();
|
||||
this.activatePreview();
|
||||
hint('#button-print, #panel-settings');
|
||||
});
|
||||
}
|
||||
}
|
||||
darkness = 0;
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.addEventListener('change', () => {
|
||||
let url = URL.createObjectURL(input.files[0]);
|
||||
put_image(url);
|
||||
});
|
||||
hidden_area.appendChild(input);
|
||||
input.click();
|
||||
}
|
||||
for (let i = 0; i < data.width; i++) {
|
||||
for (let j = 0; j < data.height; j++) {
|
||||
let index = (j * data.width + i) * 4;
|
||||
let [r, g, b, a] = data.data.slice(index, index + 4);
|
||||
let visibility = 1 - ((r * 0.2125) + (g * 0.7154) + (b * 0.0721)) * (a / 255) / 255;
|
||||
darkness += visibility;
|
||||
if (darkness >= threshold) {
|
||||
newdata_vertical[index] = 0;
|
||||
newdata_vertical[index + 1] = 0;
|
||||
newdata_vertical[index + 2] = 0;
|
||||
newdata_vertical[index + 3] = 255;
|
||||
darkness = 0;
|
||||
} else {
|
||||
newdata_vertical[index] = 255;
|
||||
newdata_vertical[index + 1] = 255;
|
||||
newdata_vertical[index + 2] = 255;
|
||||
newdata_vertical[index + 3] = 255;
|
||||
makePbm() {
|
||||
let blob = mono2pbm(this.previewData, this.preview.width, this.preview.height);
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
|
||||
class Main {
|
||||
promise;
|
||||
/** @type {PanelController} */
|
||||
panelController;
|
||||
/** @type {CanvasController} */
|
||||
canvasController;
|
||||
deviceOptions;
|
||||
/** An object containing configuration, fetched from server */
|
||||
settings;
|
||||
/** @type {{ [key: string]: EventPutter }} */
|
||||
setters;
|
||||
/**
|
||||
* There are race conditions in initialization query/set,
|
||||
* use this flag to avoid
|
||||
*/
|
||||
allowSet;
|
||||
constructor() {
|
||||
this.allowSet = false;
|
||||
this.deviceOptions = document.getElementById('device-options');
|
||||
this.settings = {};
|
||||
this.setters = {};
|
||||
// window.addEventListener('unload', () => this.exit());
|
||||
this.promise = new Promise(async (resolve, reject) => {
|
||||
await this.initI18n();
|
||||
this.panelController = new PanelController();
|
||||
this.canvasController = new CanvasController();
|
||||
putEvent('#button-exit', 'click', this.exit, this);
|
||||
putEvent('#button-print', 'click', this.print, this);
|
||||
putEvent('#device-refresh', 'click', this.searchDevices, this);
|
||||
this.attachSetter('#scan-time', 'change', 'scan_time');
|
||||
this.attachSetter('#device-options', 'input', 'printer_address');
|
||||
this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm');
|
||||
this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white');
|
||||
this.attachSetter('#dry-run', 'change', 'dry_run',
|
||||
(checked) => checked && Notice.notice('dry-run-test-print-process-only')
|
||||
);
|
||||
this.attachSetter('#no-animation', 'change', 'no_animation',
|
||||
(checked) => checked ? document.body.classList.add('no-animation')
|
||||
: document.body.classList.remove('no-animation')
|
||||
);
|
||||
this.attachSetter('#threshold', 'change', 'threshold',
|
||||
(value) => this.canvasController.threshold = value
|
||||
);
|
||||
this.attachSetter('#frequency', 'change', 'frequency');
|
||||
await this.loadConfig();
|
||||
this.searchDevices();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
query(key) { return this.settings[key]; }
|
||||
/** Sync setting(s) to server ("set") */
|
||||
async set(body, errorPreHandler) {
|
||||
if (this.allowSet) return await callApi('/set', body, errorPreHandler);
|
||||
else return null;
|
||||
}
|
||||
/**
|
||||
* Load saved config from server, and activate all setters with corresponding values in settings.
|
||||
* Please do `attachSetter` on all desired elements/inputs before calling.
|
||||
* After the load, will save config to server again in order to sync default values.
|
||||
* Then, if permitted, every single change will sync to server instantly
|
||||
*/
|
||||
async loadConfig() {
|
||||
this.settings = await callApi('/query');
|
||||
for (let key in this.settings) {
|
||||
let value = this.settings[key];
|
||||
if (this.setters[key] === undefined) continue;
|
||||
// Set the *reasonable* value
|
||||
this.setters[key].elements.forEach(element => {
|
||||
switch (element.type) {
|
||||
case 'checkbox':
|
||||
element.checked = value;
|
||||
break;
|
||||
case 'radio':
|
||||
// Only dispatch on the selected one
|
||||
if (element.value !== value) return;
|
||||
element.checked = value;
|
||||
break;
|
||||
default:
|
||||
element.value = value;
|
||||
}
|
||||
element.dispatchEvent(new Event('change'));
|
||||
});
|
||||
}
|
||||
this.allowSet = true;
|
||||
await this.set(this.settings);
|
||||
}
|
||||
/**
|
||||
* Create an event handler and attach to selected elements, that change/reflect `settings`
|
||||
* @param {string} attribute The setting to change, i.e. `this.setting[attribute] = value;`
|
||||
* @param {(value: any) => any} callback Optional additinal post-procedure to call, with a *reasonable* value as parameter
|
||||
*/
|
||||
attachSetter(selector, type, attribute, callback) {
|
||||
this.setters[attribute] = putEvent(selector, type, (event => {
|
||||
event.stopPropagation();
|
||||
event.cancelBubble = true;
|
||||
let input = event.currentTarget;
|
||||
let value;
|
||||
// Get the *reasonable* value
|
||||
switch (input.type) {
|
||||
case 'number':
|
||||
case 'range':
|
||||
value = parseFloat(input.value); break;
|
||||
case 'checkbox':
|
||||
value = input.checked; break;
|
||||
case 'radio':
|
||||
if (input.checked) value = input.value; break;
|
||||
default:
|
||||
value = input.value;
|
||||
}
|
||||
this.settings[attribute] = value;
|
||||
this.set({ [attribute]: value });
|
||||
return callback ? callback(value) : undefined;
|
||||
}).bind(this), this);
|
||||
}
|
||||
async exit() {
|
||||
await this.set(this.settings);
|
||||
await callApi('/exit');
|
||||
window.close();
|
||||
// Browser may block the exit
|
||||
Notice.notice('you-can-close-this-page-manually');
|
||||
}
|
||||
/** @param {Response} response */
|
||||
async bluetoothProblemHandler(response) {
|
||||
// Not complete yet, it's different across other platforms
|
||||
let error_details = await response.json();
|
||||
if (
|
||||
error_details.name === 'org.bluez.Error.NotReady' ||
|
||||
error_details.details.indexOf('not turned on') !== -1 ||
|
||||
error_details.details.indexOf('WinError -2147020577') !== -1
|
||||
) Notice.warn('please-enable-bluetooth');
|
||||
else throw new Error('Unknown Bluetooth Problem');
|
||||
return null;
|
||||
}
|
||||
async searchDevices() {
|
||||
Notice.notice('scanning-for-devices');
|
||||
let search_result = await callApi('/devices', null, this.bluetoothProblemHandler);
|
||||
if (search_result === null) return;
|
||||
let devices = search_result.devices;
|
||||
this.deviceOptions.childNodes.forEach(e => e.remove());
|
||||
if (devices.length === 0) {
|
||||
Notice.notice('no-available-devices-found');
|
||||
hint('#device-refresh');
|
||||
return;
|
||||
}
|
||||
Notice.notice('found-1-available-devices', devices.length);
|
||||
hint('#insert-picture');
|
||||
devices.forEach(device => {
|
||||
let option = document.createElement('option');
|
||||
option.value = device.address;
|
||||
option.innerText = `${device.name}-${device.address.slice(12, 14)}${device.address.slice(15)}`;
|
||||
this.deviceOptions.appendChild(option);
|
||||
});
|
||||
this.deviceOptions.dispatchEvent(new Event('input'));
|
||||
}
|
||||
async print() {
|
||||
Notice.notice('printing');
|
||||
await fetch('/print', {
|
||||
method: 'POST',
|
||||
body: this.canvasController.makePbm()
|
||||
}).then(async (response) => {
|
||||
if (response.ok) Notice.notice('finished')
|
||||
else {
|
||||
let error_data = await response.json();
|
||||
if (/address.+not found/.test(error_data.details))
|
||||
Notice.error('please-check-if-the-printer-is-down');
|
||||
else
|
||||
ErrorHandler.report(
|
||||
new Error('API Failure'),
|
||||
JSON.stringify(await response.json(), undefined, 4)
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
async initI18n() {
|
||||
if (typeof i18n === 'undefined') return;
|
||||
let language_list = navigator.languages;
|
||||
let loaded_languages = [];
|
||||
let data;
|
||||
for (let i = language_list.length - 1; i >= 0; i--) {
|
||||
data = await fetch(`/lang/${language_list[i]}.json`)
|
||||
.then(response => response.ok ? response.json() : null);
|
||||
if (data !== null) {
|
||||
i18n.translator.add(data);
|
||||
loaded_languages.unshift(language_list[i]);
|
||||
}
|
||||
}
|
||||
darkness = 0;
|
||||
console.log('Language stack:', loaded_languages);
|
||||
let elements = document.querySelectorAll('*[data-i18n]');
|
||||
let i18n_data, translated_string;
|
||||
elements.forEach(element => {
|
||||
i18n_data = element.getAttribute('data-i18n');
|
||||
translated_string = i18n(i18n_data);
|
||||
if (translated_string === i18n_data) return;
|
||||
// element.innerText = translated_string;
|
||||
if (element.firstChild.textContent !== translated_string)
|
||||
element.firstChild.textContent = translated_string;
|
||||
});
|
||||
}
|
||||
let newdata_intersection = new Uint8ClampedArray(data.data.length);
|
||||
for (let i = 0; i < data.data.length; i += 4) {
|
||||
if (newdata_horizonal[i] == 0 && newdata_vertical[i] == 0) {
|
||||
newdata_intersection[i] = 0;
|
||||
newdata_intersection[i + 1] = 0;
|
||||
newdata_intersection[i + 2] = 0;
|
||||
newdata_intersection[i + 3] = 255;
|
||||
} else {
|
||||
newdata_intersection[i] = 255;
|
||||
newdata_intersection[i + 1] = 255;
|
||||
newdata_intersection[i + 2] = 255;
|
||||
newdata_intersection[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
return new ImageData(newdata_intersection, data.width, data.height);
|
||||
}
|
||||
function imageDataColorToMonoDiamond(data, threshold) {
|
||||
// Not completed yet, but in theory will be beautiful
|
||||
let newdata_lefttop_to_rightbottom = new Uint8ClampedArray(data.data.length);
|
||||
let newdata_leftbottom_to_righttop = new Uint8ClampedArray(data.data.length);
|
||||
let darkness = 0;
|
||||
let is_odd = false;
|
||||
for (let j = 0; j < data.height; j++) {
|
||||
for (let i = 0; i < data.width; i++) {
|
||||
let index = (j * data.width + i + (is_odd ? 1 : 0)) * 4;
|
||||
let [r, g, b, a] = data.data.slice(index, index + 4);
|
||||
let visibility = 1 - ((r * 0.2125) + (g * 0.7154) + (b * 0.0721)) * (a / 255) / 255;
|
||||
darkness += visibility;
|
||||
if (darkness >= threshold) {
|
||||
newdata_lefttop_to_rightbottom[index] = 0;
|
||||
newdata_lefttop_to_rightbottom[index + 1] = 0;
|
||||
newdata_lefttop_to_rightbottom[index + 2] = 0;
|
||||
newdata_lefttop_to_rightbottom[index + 3] = 255;
|
||||
darkness = 0;
|
||||
} else {
|
||||
newdata_lefttop_to_rightbottom[index] = 255;
|
||||
newdata_lefttop_to_rightbottom[index + 1] = 255;
|
||||
newdata_lefttop_to_rightbottom[index + 2] = 255;
|
||||
newdata_lefttop_to_rightbottom[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
darkness = 0;
|
||||
is_odd = !is_odd;
|
||||
}
|
||||
for (let i = 0; i < data.width; i++) {
|
||||
for (let j = 0; j < data.height; j++) {
|
||||
let index = (j * data.width + i + (is_odd ? 1 : 0)) * 4;
|
||||
let [r, g, b, a] = data.data.slice(index, index + 4);
|
||||
let visibility = 1 - ((r * 0.2125) + (g * 0.7154) + (b * 0.0721)) * (a / 255) / 255;
|
||||
darkness += visibility;
|
||||
if (darkness >= threshold) {
|
||||
newdata_leftbottom_to_righttop[index] = 0;
|
||||
newdata_leftbottom_to_righttop[index + 1] = 0;
|
||||
newdata_leftbottom_to_righttop[index + 2] = 0;
|
||||
newdata_leftbottom_to_righttop[index + 3] = 255;
|
||||
darkness = 0;
|
||||
} else {
|
||||
newdata_leftbottom_to_righttop[index] = 255;
|
||||
newdata_leftbottom_to_righttop[index + 1] = 255;
|
||||
newdata_leftbottom_to_righttop[index + 2] = 255;
|
||||
newdata_leftbottom_to_righttop[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
darkness = 0;
|
||||
is_odd = !is_odd;
|
||||
}
|
||||
let newdata_intersection = new Uint8ClampedArray(data.data.length);
|
||||
for (let i = 0; i < data.data.length; i += 4) {
|
||||
if (newdata_lefttop_to_rightbottom[i] == 0 && newdata_leftbottom_to_righttop[i] == 0) {
|
||||
newdata_intersection[i] = 0;
|
||||
newdata_intersection[i + 1] = 0;
|
||||
newdata_intersection[i + 2] = 0;
|
||||
newdata_intersection[i + 3] = 255;
|
||||
} else {
|
||||
newdata_intersection[i] = 255;
|
||||
newdata_intersection[i + 1] = 255;
|
||||
newdata_intersection[i + 2] = 255;
|
||||
newdata_intersection[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
return new ImageData(newdata_intersection, data.width, data.height);
|
||||
}
|
||||
function imageDataMonoToPBM(data) {
|
||||
let result = new ArrayBuffer(data.data.length / 4 / 8);
|
||||
let view = new DataView(result);
|
||||
for (let i = 0; i < data.data.length; i += 8 * 4) {
|
||||
let code = 0;
|
||||
if (data.data[i + 0 * 4] == 0) code += 0b10000000;
|
||||
if (data.data[i + 1 * 4] == 0) code += 0b01000000;
|
||||
if (data.data[i + 2 * 4] == 0) code += 0b00100000;
|
||||
if (data.data[i + 3 * 4] == 0) code += 0b00010000;
|
||||
if (data.data[i + 4 * 4] == 0) code += 0b00001000;
|
||||
if (data.data[i + 5 * 4] == 0) code += 0b00000100;
|
||||
if (data.data[i + 6 * 4] == 0) code += 0b00000010;
|
||||
if (data.data[i + 7 * 4] == 0) code += 0b00000001;
|
||||
view.setInt8(i / 4 / 8, code);
|
||||
}
|
||||
let pbm_data = new Blob([`P4\n${data.width} ${data.height}\n`, result]);
|
||||
return pbm_data;
|
||||
}
|
||||
|
||||
var main = new Main();
|
||||
|
8
www/polyfill.js
Normal file
8
www/polyfill.js
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
/**
|
||||
* Polyfill
|
||||
*/
|
||||
(function() {
|
||||
if (!NodeList.prototype.forEach)
|
||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||||
})();
|
@ -1,57 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Print Document</title>
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
<link rel="stylesheet" href="skin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p id="notice">
|
||||
<noscript>Javascript should be enabled</noscript>
|
||||
</p>
|
||||
<h2>Print Document</h2>
|
||||
<p>
|
||||
<input style="display: none;" type="text" id="bluetooth_address_input" value="" />
|
||||
<span>Select device:</span><select id="device_selection"></select><button id="refresh_device">Refresh</button>
|
||||
</p>
|
||||
<p>
|
||||
<span>Copy & paste document to box</span><button id="preview_button">Preview</button>
|
||||
<button id="print_button">Print</button><br />
|
||||
<span>Threshold:</span><input type="range" min="0" max="2" step="0.05" value="0.2" id="filter_threshold" />
|
||||
</p>
|
||||
<div style="margin: auto; border: 1px solid black; width: 384px; min-height: 3em;">
|
||||
<div id="container" style="padding: 0.5em 0;">
|
||||
<div class="reset_styles" contenteditable="true" style="width: 384px; min-height: 3em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p><i>From Webpage/MS Office/WPS/LibreOffice to here, pasted data shall be formatted</i></p>
|
||||
<p>Preview</p>
|
||||
<div id="image_preview" style="width: 384px; margin: auto;"></div>
|
||||
</main>
|
||||
<textarea id="i18-N" style="display: none;">
|
||||
[zh-CN]
|
||||
Print Document=打印文档
|
||||
Javascript should be enabled=需启用 Javascript
|
||||
Select device:=选择设备:
|
||||
Refresh=刷新
|
||||
Copy & paste document to box=复制粘贴文档至框内
|
||||
Preview=预览
|
||||
Print=打印
|
||||
Threshold:=阈值:
|
||||
From Webpage/MS Office/WPS/LibreOffice to here, pasted data shall be formatted=从网页/MS Office/WPS/LibreOffice 到这里,粘贴的数据格式会保留
|
||||
Please select a device=请选择一个设备
|
||||
Printing, please wait.=打印中,请稍候。
|
||||
Please preview image first=请先预览图像
|
||||
Searching devices. Please wait for 5 seconds.=正在查找设备。请等候 5 秒。
|
||||
OK=完成
|
||||
</textarea>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="html2canvas.min.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script src="print-document.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,57 +0,0 @@
|
||||
///<reference path="main.js" />
|
||||
///<reference path="main.d.ts" />
|
||||
|
||||
class DocumentPrinter {
|
||||
WIDTH = 384;
|
||||
thresholdInput = document.getElementById('filter_threshold');
|
||||
bluetoothMACInput = document.getElementById('bluetooth_address_input');
|
||||
container = document.getElementById('container');
|
||||
printButton = document.getElementById('print_button');
|
||||
previewButton = document.getElementById('preview_button');
|
||||
threshold = 0.2;
|
||||
canvasPreview = document.getElementById('image_preview');
|
||||
monoMethod = imageDataColorToMonoSquare;
|
||||
constructor() {
|
||||
this.thresholdInput.onchange = event => {
|
||||
this.threshold = this.thresholdInput.value;
|
||||
}
|
||||
this.printButton.addEventListener('click', event => {
|
||||
let mac_address = this.bluetoothMACInput.value;
|
||||
if (mac_address == '') {
|
||||
notice(i18N.get('Please select a device'));
|
||||
return;
|
||||
}
|
||||
if (this.canvasPreview.children.length == 0) {
|
||||
notice(i18N.get('Please preview image first'));
|
||||
return;
|
||||
}
|
||||
html2canvas(this.container).then(canvas => {
|
||||
notice(i18N.get('Printing, please wait.'));
|
||||
let context = canvas.getContext('2d');
|
||||
let imagedata = context.getImageData(0, 0, this.WIDTH, canvas.height);
|
||||
let mono_imagedata = this.monoMethod(imagedata, this.threshold);
|
||||
context.putImageData(mono_imagedata, 0, 0);
|
||||
let pbm_data = imageDataMonoToPBM(mono_imagedata);
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/~print?address=' + mac_address);
|
||||
xhr.setRequestHeader('Content-Type', 'application-octet-stream');
|
||||
xhr.onload = () => {
|
||||
notice(i18N.get(xhr.responseText));
|
||||
}
|
||||
xhr.send(pbm_data);
|
||||
});
|
||||
});
|
||||
this.previewButton.addEventListener('click', event => {
|
||||
if (this.canvasPreview.children[0] != null) this.canvasPreview.children[0].remove();
|
||||
html2canvas(this.container).then(canvas => {
|
||||
let context = canvas.getContext('2d');
|
||||
let imagedata = context.getImageData(0, 0, this.WIDTH, canvas.height);
|
||||
let mono_imagedata = this.monoMethod(imagedata, this.threshold);
|
||||
context.putImageData(mono_imagedata, 0, 0);
|
||||
this.canvasPreview.appendChild(canvas);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var document_printer = new DocumentPrinter();
|
@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Print Image</title>
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
<link rel="stylesheet" href="skin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p id="notice">
|
||||
<noscript>Javascript should be enabled</noscript>
|
||||
</p>
|
||||
<h2>Print Image</h2>
|
||||
<p>
|
||||
<input style="display: none;" type="text" id="bluetooth_address_input" value="" />
|
||||
<span>Select device:</span><select id="device_selection"></select><button id="refresh_device">Refresh</button>
|
||||
</p>
|
||||
<p>
|
||||
<input type="file" id="file_selection" /><button id="preview_button">Preview</button><button id="print_button">Print</button><br />
|
||||
<span>Threshold:</span><input type="range" min="0" max="2" step="0.05" value="0.6" id="filter_threshold" /><br />
|
||||
<canvas id="image_preview" width="384" height="0"></canvas>
|
||||
</p>
|
||||
</main>
|
||||
<textarea id="i18-N" style="display: none;">
|
||||
[zh-CN]
|
||||
Print Image=打印图片
|
||||
Javascript should be enabled=需启用 Javascript
|
||||
Select device:=选择设备:
|
||||
Refresh=刷新
|
||||
Preview=预览
|
||||
Print=打印
|
||||
Threshold:=阈值:
|
||||
Please preview image first=请先预览图像
|
||||
Searching devices. Please wait for 5 seconds.=正在查找设备。请等候 5 秒。
|
||||
OK=完成
|
||||
Printing, please wait.=打印中,请稍候。
|
||||
</textarea>
|
||||
<script src="i18n.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script src="print-image.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,60 +0,0 @@
|
||||
///<reference path="main.js" />
|
||||
///<reference path="main.d.ts" />
|
||||
|
||||
class ImagePrinter {
|
||||
WIDTH = 384;
|
||||
threshold = 0.6;
|
||||
bluetoothMACInput = document.getElementById('bluetooth_address_input');
|
||||
thresholdInput = document.getElementById('filter_threshold');
|
||||
fileSelection = document.getElementById('file_selection');
|
||||
dummyImage = new Image();
|
||||
canvasPreview = document.getElementById('image_preview');
|
||||
previewButton = document.getElementById('preview_button');
|
||||
printButton = document.getElementById('print_button');
|
||||
preview() {
|
||||
let reader = new FileReader();
|
||||
reader.onload = event1 => {
|
||||
this.dummyImage.src = event1.target.result;
|
||||
let height = this.WIDTH / this.dummyImage.width * this.dummyImage.height;
|
||||
this.canvasPreview.width = this.WIDTH;
|
||||
this.canvasPreview.height = height;
|
||||
let context = this.canvasPreview.getContext('2d');
|
||||
context.drawImage(this.dummyImage, 0, 0, this.WIDTH, height);
|
||||
let data = context.getImageData(0, 0, this.WIDTH, height);
|
||||
context.putImageData(this.monoMethod(data, this.threshold), 0, 0);
|
||||
}
|
||||
reader.readAsDataURL(this.fileSelection.files[0]);
|
||||
}
|
||||
constructor() {
|
||||
this.monoMethod = imageDataColorToMonoSquare;
|
||||
this.fileSelection.addEventListener('input', this.preview.bind(this));
|
||||
this.previewButton.addEventListener('click', this.preview.bind(this));
|
||||
this.thresholdInput.onchange = event => {
|
||||
this.threshold = this.thresholdInput.value;
|
||||
}
|
||||
this.printButton.addEventListener('click', event => {
|
||||
// this.preview();
|
||||
if (this.canvasPreview.height == 0) {
|
||||
notice(i18N.get('Please preview image first'));
|
||||
return;
|
||||
}
|
||||
let mac_address = this.bluetoothMACInput.value;
|
||||
if (mac_address == '') {
|
||||
notice(i18N.get('Please select a device'));
|
||||
return;
|
||||
}
|
||||
notice(i18N.get('Printing, please wait.'));
|
||||
let context = this.canvasPreview.getContext('2d');
|
||||
let pbm_data = imageDataMonoToPBM(context.getImageData(0, 0, this.WIDTH, this.canvasPreview.height));
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/~print?address=' + mac_address);
|
||||
xhr.setRequestHeader('Content-Type', 'application-octet-stream');
|
||||
xhr.onload = () => {
|
||||
notice(i18N.get(xhr.responseText));
|
||||
}
|
||||
xhr.send(pbm_data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var image_printer = new ImagePrinter();
|
Loading…
x
Reference in New Issue
Block a user