Backend Big Update (document later)

This commit is contained in:
NaitLee 2022-04-17 03:20:37 +08:00
parent 1f3c16cf16
commit 205f5ad5ca
34 changed files with 1567 additions and 980 deletions

6
.gitignore vendored
View File

@ -3,9 +3,6 @@ __pycache__
# Compatibility version of script, for old-old webView, # Compatibility version of script, for old-old webView,
# generated by typescript tsc # generated by typescript tsc
www/main.comp.js www/main.comp.js
# https://github.com/roddeh/i18njs
www/i18n.js
www/i18n.d.ts
# https://www.npmjs.com/package/vconsole # https://www.npmjs.com/package/vconsole
www/vconsole.js www/vconsole.js
# https://github.com/delight-im/Android-AdvancedWebView # https://github.com/delight-im/Android-AdvancedWebView
@ -15,6 +12,7 @@ build-android/advancedwebview
# releases # releases
build-android/dist build-android/dist
*.apk *.apk
*.apk.*
cat-printer*.zip cat-printer*.zip
# bleak, the bare pip package as a folder # bleak, the bare pip package as a folder
build-common/bleak build-common/bleak
@ -22,6 +20,8 @@ build-common/bleak
build-common/python-win32* build-common/python-win32*
# dev config # dev config
config.json config.json
# dev backup
*.bak
# test files # test files
*.dump *.dump
*.pf2 *.pf2

View File

@ -7,15 +7,15 @@
jobs=4 jobs=4
[BASIC] [BASIC]
class-const-naming-style=snake_case class-const-naming-style=PascalCase
const-naming-style=snake_case const-naming-style=PascalCase
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable=broad-except, disable=broad-except,
global-statement, global-statement,
fixme, fixme, too-few-public-methods,
import-outside-toplevel import-outside-toplevel
[BASIC] [BASIC]
good-names=i, j, k, ex, x, y, _, e, good-names=i, j, k, ex, x, y, _, e, b, u, s,
Run, do_GET, do_POST, do_HEAD, do_PUT Run, do_GET, do_POST, do_HEAD, do_PUT

13
.vscode/launch.json vendored
View File

@ -4,13 +4,24 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Python: Bitmap Print",
"type": "python",
"request": "launch",
"program": "printer.py",
"args": [
"-m", "-s", "1", "dump.pbm"
],
"console": "integratedTerminal",
"justMyCode": true
},
{ {
"name": "Python: Text Print", "name": "Python: Text Print",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"program": "printer.py", "program": "printer.py",
"args": [ "args": [
"-m", "-t", "-d", "-s", "1", "-" "-mt", "-f", "GB02", "COPYING"
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true "justMyCode": true

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
cd www cd www
npx tsc --allowJs --outFile main.comp.js polyfill.js i18n.js image.js main.js npx tsc --allowJs --outFile main.comp.js polyfill.js i18n-ext.js i18n.js image.js main.js
cd .. cd ..

View File

@ -11,34 +11,35 @@ English | [Deutsch](./readme.i18n/README.de_DE.md) | [简体中文](./readme.i18
Currently: Currently:
| | | | | |
|-------------|-------------------| |----|----|
| Supported | GB01, GB02, GT01 | | Supported | GB01, GB02, GT01, GB03 |
| Maybe | GB03 | <!-- | Maybe | N/A | -->
| Planned | N/A | <!-- | Planned | N/A | -->
## Features ## Features
*Currently it's in development. More will be here soon!* *Currently it's in development. More will be here soon!*
| Available | Partial | Planned |
|-----------------|-----------|---------------|
| Web Interface | CUPS/IPP* | Visual Editor |
| Print a Picture | | Help/Manual |
| Command-line | | Text Printing |
<!-- May comment the line below if there are no * -->
\* In development code. Will be released in a short period.
*Along with...*
- Simple! - Simple!
- Operate via a Web UI just in browser, - Operate via Web UI just in browser,
- or get the Android release! - or get the Android release!
- Even no problem with command line hackers!
- Friendly! - Friendly!
- Language support! You can participate in translation! - Language support! You can participate in translation!
- Good user interface, adaptive to PC/mobile and light/dark theme! - Good user interface, adaptive to PC/mobile and light/dark theme!
- Feature-rich!
- Web UI, for most people!
- Take full control of printer config
- Print picture, or just test if it works
- Command line, for geeks & hackers!
- Control printer with a few parameters
- Simplified text printing
- Make use of every part of the program
- Some other goodies!
- Server program is also CUPS/IPP capable
- Cross platform! - Cross platform!
- Newer Windows 10 and above - Newer Windows 10 and above
- GNU/Linux - GNU/Linux
@ -78,33 +79,36 @@ On Arch Linux based distros you may first install `bluez`, as it's often missing
sudo pacman -S bluez bluez-utils sudo pacman -S bluez bluez-utils
``` ```
*Packaging is also on the way!*
### MacOS ### MacOS
For MacOS please install [Python 3](https://www.python.org/). For MacOS please install [Python 3](https://www.python.org/),
then install `pyobjc` and `bleak` via `pip` in terminal:
```bash
pip3 install pyobjc bleak
```
Fetch a "pure" release and do the same in a shell: After that, fetch a "bare" release and do the same in a shell:
```bash ```bash
python3 server.py python3 server.py
``` ```
Currently in Mac the browser will not pop up automatically. Please run manually and go to `http://127.0.0.1:8095`, or just click [here](http://127.0.0.1:8095). Currently in Mac the browser will not pop up automatically. Please run manually and go to `http://127.0.0.1:8095`, or just click [here](http://127.0.0.1:8095).
### Worth to Note ### Worth to Note
For all supported platforms, For all supported platforms,
You can also use "pure" edition once you have [Python 3](https://www.python.org/) installed, 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`. or "bare" edition if you also managed to install `bleak` via `pip`.
Command line hackers? Just use `printer.py`!
See the [releases](https://github.com/NaitLee/Cat-Printer/releases) now! See the [releases](https://github.com/NaitLee/Cat-Printer/releases) now!
## Problems? ## Problems?
Please open an issue if there's something in your mind! Please talk in Discussion if there's something in your mind!
Of course PRs are welcome if you can handle them! Of course Pull Requests are welcome if you can handle them!
## License ## License
@ -125,7 +129,7 @@ Also interested in code development? See [development.md](development.md)!
- Of course, Python & the Web! - Of course, Python & the Web!
- [Bleak](https://bleak.readthedocs.io/en/latest/) BLE lib! The overall Hero! - [Bleak](https://bleak.readthedocs.io/en/latest/) BLE lib! The overall Hero!
- [roddeh-i18n](https://github.com/roddeh/i18njs), good work! - [roddeh-i18n](https://github.com/roddeh/i18njs), the current built-in i18n is inspired by this
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles - [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 - [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 beginning - Stack Overflow & the whole Internet, you let me know Android `Activity` all from beginning

31
TODO
View File

@ -1,26 +1,23 @@
Note: not ordered. do whatever I/you want Note: not ordered. do whatever I/you want
+ Check GB03, again + Cookbook of basic things
+ Hacky text printing, typewriter-like, with PF2 font
ok I won't forget frontend, but it's different
+ Better CLI, e.g. invoke imagemagick if input is not PBM
+ Consider better MTU adaption
+ Consider better BLE traffic regulation, with BLE notification
+ Consider the printer 'text' mode, it can make things faster
+ Consider more control to something like 'energy'
+ Consider keeping server backend (more) secure to be used by IPP
+ Clean up CUPS/IPP code
+ Make a build guide for android:
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 + Write good help/manual
+ Make error notice short while let users see detailed help/manual for what-to-do + Make error notice short while let users see detailed help/manual for what-to-do
+ Even Better CLI, e.g. invoke imagemagick if input is not PBM
+ Even better CUPS/IPP support
+ Even better frontend usability, more functions
+ A better layout for mobile?
+ Make a build guide for android:
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
+ Try to implement enough without more dependencies
+ Better Canvas mode, (re-)consider fabric.js
+ Implement Document mode, (re-)consider html2canvas.js
+ ... + ...
? Compression for GB03 data ? Consider more control to something like 'energy'
This have no effect on my GB02
? Data compression for GB03. Optional
? Put Android APP on F-Droid? But it needs automatic build system... ? Put Android APP on F-Droid? But it needs automatic build system...
Android guys can help this! Android guys can help this!
? ... Or put to APKPure?

View File

@ -1,66 +0,0 @@
'Minimal internationalization lib'
import os
import math
import json
import locale
class I18n():
''' Minimal implementation of current frontend i18n in Python
Not Complete (yet)!
'''
lang: str
fallback: str
data: dict = {
'values': {},
'contexts': []
}
def __init__(self, search_path='lang', lang=None, fallback=None):
self.lang = lang or locale.getdefaultlocale()[0]
self.fallback = fallback or 'en_US'
self.load_file(os.path.join(search_path, self.fallback.replace('_', '-') + '.json'))
for name in os.listdir(search_path):
if name == self.lang.replace('_', '-') + '.json':
self.load_file(os.path.join(search_path, name))
def load_file(self, name):
'Load an i18n json file'
with open(name, 'r', encoding='utf-8') as file:
self.load_data(file.read())
def load_data(self, raw_json):
'Load i18n json data (from str)'
data = json.loads(raw_json)
for key in data['values']:
self.data['values'][key] = data['values'][key]
if data.get('contexts') is not None:
self.data['contexts'] = data['contexts']
def __getitem__(self, keys):
if not isinstance(keys, tuple):
keys = (keys, )
data = self.data['values'].get(keys[0], keys[0])
string = data[0][2] if isinstance(data, list) else data
for i in keys:
if isinstance(i, (int, float)):
if string is None:
string = data
if isinstance(data, list):
for j in data:
if j[0] is None:
j[0] = -math.inf
if j[1] is None:
j[1] = math.inf
if j[0] < i < j[1]:
template = j[2]
break
string = template.replace('%%n', i).replace('-%%n', -i)
elif isinstance(i, dict):
# not verified if would work
if string is None:
string = data
for j in i:
string = string.replace(f'%%{j}', i[j])
return string

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \ p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
--icon=icon.png --version="0.1.1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \ --icon=icon.png --version="0.2.0" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \ --blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \ --presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \ --permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \

View File

@ -4,7 +4,7 @@ unzip -q "../cat-printer-bare-$1.zip"
mv "cat-printer" "dist" mv "cat-printer" "dist"
p4a apk --private "dist" --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \ 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 \ --icon=icon.png --version="$1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a \ --blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --release \
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \ --presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \ --permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
--permission=BLUETOOTH_ADMIN --permission=ACCESS_FINE_LOCATION --permission=ACCESS_COARSE_LOCATION --permission=BLUETOOTH_ADMIN --permission=ACCESS_FINE_LOCATION --permission=ACCESS_COARSE_LOCATION

View File

@ -22,23 +22,20 @@ if not sys.argv[-1].startswith('-'):
bundle_name %= (edition, version) bundle_name %= (edition, version)
ignore_whitelist = ( ignore_whitelist = (
'www/i18n.js',
'www/main.comp.js' 'www/main.comp.js'
) )
additional_ignore = ( additional_ignore = (
# prevent recurse # prevent recurse
bundle_name, bundle_name,
# non-production (yet)
'PKGBUILD', 'systemd',
# build helpers # build helpers
'build-*', 'build-*', '?-*.sh',
'?-*.sh',
# no need # no need
'.git', '.git', '.gitignore',
'.vscode', '.vscode', '.pylintrc',
'.pylintrc', 'dev-diary.txt', 'TODO',
'.gitignore',
'dev-diary.txt',
'TODO',
# cache # cache
'*.pyc', '*.pyc',
# other # other

View File

@ -55,3 +55,40 @@ It's finally ready...
Documentation. Documentation.
What else? First Release! What else? First Release!
... ...
(some day)
Determined to make backend better,
but resulting in full rewrite.
... ... (worked hard)
15th
Feeling it's there.
Oh, asyncio always quits like a mad cat, throwing bleak there
and just ends everything, uncleanly.
16th
Solved many things left yesterday.
Thought i18n needs be universal across there, so made one by myself.
Tried to implement flip, rtl and wrap in text printing,
wasted some time, but didn't regret.
Well, slightly update document and try compiling,
give everyone a surprise.
Try --release on p4a build. It worked. 5.9MiB apk, satisfied now?
Don't forget -Djava.net.useSystemProxies=true on gradle anymore,
when a proxy to google is needed.
Phone says a release apk should be signed to be installed.
Satisfy it. https://stackoverflow.com/questions/4853011/how-to-sign-an-android-apk-file
Okay, it's 17th 3 a.m. publish it and sleep.

View File

@ -1,6 +1,8 @@
# Development # Development
**Note: Some maybe outdated at the moment**
## Overview ## Overview
This application have a Client/Server module, but it's just locally. This application have a Client/Server module, but it's just locally.
@ -9,7 +11,6 @@ The backend is in Python 3, aiming to have fewest dependencies, and in fact curr
This can ensure the simplicity of the core part. This can ensure the simplicity of the core part.
And the frontend is in a "old good" way, that use no "framework". 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. My workspace stack is Linux/GNU/Artix/KDE/VSCodium, if you're interested.
For Android, GNU/Linux is required, though. For Android, GNU/Linux is required, though.
@ -27,12 +28,11 @@ Just clone this repo first!
1. Get Bleak BLE lib: 1. Get Bleak BLE lib:
`pip install bleak` `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. Alright, you are already well done for basic development. See [files](#files) section for what all the files do.
For more, read on... For more, read on...
### Additional ### Optional
Sorry, I'm not a dev package manager enthusiast. Sorry, I'm not a dev package manager enthusiast.
@ -48,19 +48,13 @@ If there are something better to organize these, feel free to discuss in issue.
- Get an Windows 64-bit embeddable Python, extract to `build-common/python-win32-amd64-embed` - 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. - 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` - 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` - 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 Now you're ready to debug in browsers without a dev panel, by double-tapping "Cat Printer" title in the UI
## Files ## Files
- `server.py` - A Web server that: - `server.py` - A Web server that:
- Is single threaded, to work with Android/pyjnius - Is single threaded & with static handler, for some reasons
- Serves static Web files, that are in folder `www` - Serves static Web files, that are in folder `www`
- Opens a Web browser once launched, unless specify the `-s` command-line parameter - Opens a Web browser once launched, unless specify the `-s` command-line parameter
- Only listen to localhost, unless specify the `-a` command-line parameter - Only listen to localhost, unless specify the `-a` command-line parameter
@ -83,6 +77,10 @@ If there are something better to organize these, feel free to discuss in issue.
- Transpiled with TypeScript, for fallback on old browsers - Transpiled with TypeScript, for fallback on old browsers
- Bundled all required scripts, see file `0-transpile.sh` - Bundled all required scripts, see file `0-transpile.sh`
- Is not there by default. Transpile it yourself - Is not there by default. Transpile it yourself
- `www/i18n*` - Scripts about I18n:
- TODO. In fact it worth a dedicated document to describe it
- (Mostly) Depends on "extensions" to work in the correct way,
feel free to extend, as it's *your* turn
- `www/*.js` - Other scripts: - `www/*.js` - Other scripts:
- Small but useful, just look at them directly - Small but useful, just look at them directly
- `www/jslicense.html` - Dedicated JavaScript License information - `www/jslicense.html` - Dedicated JavaScript License information
@ -93,6 +91,7 @@ If there are something better to organize these, feel free to discuss in issue.
- Quickly invoke with `./N<tab><enter>` - Quickly invoke with `./N<tab><enter>`
- `build-common/bundle.py` - Bundler for "windows", "pure" and "bare" editions - `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 - You can define what to include or not in this script, just modify directly, while trying to not alter other
- Adviced to transpile scripts before bundling
- To do the builds you should be in the build dir: `cd build-common` - 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` - 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` - In any case you're able to bundle a "bare" edition, via `python3 bundle.py -b`

File diff suppressed because it is too large Load Diff

133
printer_lib/commander.py Normal file
View File

@ -0,0 +1,133 @@
'Printer Commander'
from abc import ABCMeta, abstractmethod
crc8_table = [
0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31,
0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65,
0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9,
0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1,
0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe,
0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,
0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16,
0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42,
0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80,
0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8,
0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c,
0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10,
0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,
0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f,
0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7,
0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83,
0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef,
0xfa, 0xfd, 0xf4, 0xf3
]
def crc8(data):
'crc8 checksum'
crc = 0
for byte in data:
crc = crc8_table[(crc ^ byte) & 0xff]
return crc & 0xff
def reverse_bits(i: int):
'Reverse the bits of this byte (as `int`)'
return (
(i & 0b10000000) >> 7 |
(i & 0b01000000) >> 5 |
(i & 0b00100000) >> 3 |
(i & 0b00010000) >> 1 |
(i & 0b00001000) << 1 |
(i & 0b00000100) << 3 |
(i & 0b00000010) << 5 |
(i & 0b00000001) << 7
)
def int_to_bytes(i: int, big_endian=False):
''' Turn `int` into `bytearray`, that have
least bytes possible to represent the int
'''
result = bytearray()
while i != 0:
result.append(i & 0xff)
i >>= 8
if big_endian:
result.reverse()
return result
class Commander(metaclass=ABCMeta):
'Semi-abstract class, to be inherited by `PrinterDriver`'
dry_run: bool = False
def make_command(self, command_bit, payload: bytearray, *,
prefix=bytearray(), suffix=bytearray()):
'Make bytes that to be used to control printer'
payload_size = len(payload)
if payload_size > 0xff:
raise ValueError(f'Command payload too big ({payload_size} > 255)')
return prefix + bytearray(
[ 0x51, 0x78, command_bit, 0x00, payload_size, 0x00 ]
) + payload + bytearray( [ crc8(payload), 0xff ] ) + suffix
def start_printing(self):
'Start printing'
self.send( bytearray([0x51, 0x78, 0xa3, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff]) )
def start_printing_new(self):
'Start printing on newer printers'
self.send( bytearray([0x12, 0x51, 0x78, 0xa3, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff]) )
def image_mode(self):
'Enable Image Mode on the printer. Lighter, slower.'
self.send( self.make_command(0xbe, int_to_bytes(0x00)) )
def text_mode(self):
'Enable Text Mode on the printer. Darker, faster.'
self.send( self.make_command(0xbe, int_to_bytes(0x01)) )
def update_device(self):
'(unknown)'
self.send( self.make_command(0xa9, int_to_bytes(0x00)) )
def start_lattice(self):
'Start rolling paper'
self.send( self.make_command(0xa6, bytearray(
[0xaa, 0x55, 0x17, 0x38, 0x44, 0x5f, 0x5f, 0x5f, 0x44, 0x38, 0x2c]
)) )
def end_lattice(self):
'End rolling paper'
self.send( self.make_command(0xa6, bytearray(
[ 0xaa, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17 ]
)) )
def retract_paper(self, steps: int):
'Retract the paper for `steps`'
self.send( self.make_command(0xa0, int_to_bytes(steps)) )
def feed_paper(self, steps: int):
'Feed the paper for `steps`'
self.send( self.make_command(0xa1, int_to_bytes(steps)) )
def set_energy(self, amount: int):
'Set thermal energy, max to `0xffff`'
self.send( self.make_command(0xbe, int_to_bytes(amount)) )
def draw_bitmap(self, bitmap_data: bytearray):
'Print `bitmap_data`. Also does the bit-reversing job.'
data = bytearray( map(reverse_bits, bitmap_data) )
self.send( self.make_command(0xa2, data) )
def draw_compressed_bitmap(self, bitmap_data: bytearray):
'TODO. Print `bitmap_data`, compress if worthy so'
self.draw_bitmap(bitmap_data)
@abstractmethod
def send(self, data):
'Send data to device, or whatever'
...

38
printer_lib/i18n.py Normal file
View File

@ -0,0 +1,38 @@
'Minimal internationalization lib'
import os
import json
import locale
class I18nLib():
''' Minimal implementation of current frontend i18n in Python.
Not Complete (yet)!
'''
lang: str
fallback: str
data: dict = {}
def __init__(self, search_path='lang', lang=None, fallback=None):
self.lang = lang or locale.getdefaultlocale()[0].replace('_', '-')
self.fallback = fallback or 'en-US'
with open(os.path.join(search_path, self.fallback + '.json'),
'r', encoding='utf-8') as file:
self.data = json.load(file)
path = self.lang + '.json'
if path in os.listdir(search_path):
with open(os.path.join(search_path, path), 'r', encoding='utf-8') as file:
data = json.load(file)
for key in data:
self.data[key] = data[key]
def __getitem__(self, keys):
if not isinstance(keys, tuple):
keys = (keys, )
string = self.data.get(keys[0], keys[0])
if len(keys) > 1:
if isinstance(keys[-1], dict):
string = string.format(*keys[1:-1], **keys[-1])
else:
string = string.format(*keys[1:])
return string

View File

@ -1,56 +1,45 @@
''' Provide *very* basic CUPS/IPP support ''' Provide *very* basic CUPS/IPP support
Extracted from version 0.0.2, do more cleaning later...
''' '''
import io
import platform import platform
import subprocess import subprocess
from .pf2 import int16be, int32be
def int8(b: bytes):
'Translate 1 byte as signed 8-bit int'
u = b[0]
return u - ((u >> 7 & 0b1) << 8)
class IPP(): class IPP():
'https://datatracker.ietf.org/doc/html/rfc8010' 'https://datatracker.ietf.org/doc/html/rfc8010'
server = None server = None
printer = None def __init__(self, server):
def __init__(self, server, printer):
self.server = server self.server = server
self.printer = printer def handle_ipp(self):
async def handle_ipp(self, data):
'Handle an IPP protocol request' 'Handle an IPP protocol request'
server = self.server server = self.server
# len_data = len(data) content_length = int(server.headers.get('Content-Length'))
# ipp_version_number = data[0:2] buffer = io.BytesIO(server.rfile.read(content_length))
# ipp_operation_id = data[2:4] _ipp_version = (int8(buffer.read(1)), int8(buffer.read(1)))
# ipp_request_id = data[4:8] _ipp_operation_id = int16be(buffer.read(2))
ipp_operation_attributes_tag = data[8] _ipp_request_id = int32be(buffer.read(4))
ipp_operation_attributes_tag = int8(buffer.read(1))
attributes = {} attributes = {}
data_to_print = b'' data = b''
# this is silly. i want to use io.BytesIO
if ipp_operation_attributes_tag == 0x01: if ipp_operation_attributes_tag == 0x01:
pointer = 9 while int8(buffer.read(1)) != 0x03:
next_name_length_at = 10 buffer.seek(-1, 1)
next_value_length_at = 10 tag = int8(buffer.read(1))
name = b'' if tag < 0x10: # delimiter-tag
value = b''
while data[pointer] != 0x03:
tag = data[pointer:pointer + 1]
pointer += 1
if tag[0] < 0x10: # delimiter-tag
continue continue
next_name_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2 name = buffer.read(int16be(buffer.read(2)))
pointer += 2 value = buffer.read(int16be(buffer.read(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) attributes[name] = (tag, value)
name = b'' data = buffer.read()
value = b''
pointer += 1
data_to_print = data[pointer:]
# there are hard coded minimal response. this "just works" on cups # there are hard coded minimal response. this "just works" on cups
if data_to_print == b'': if data == b'':
try: try:
server.send_response(200) server.send_response(200)
server.send_header('Content-Type', 'application/ipp') server.send_header('Content-Type', 'application/ipp')
@ -61,6 +50,14 @@ class IPP():
except BrokenPipeError: except BrokenPipeError:
pass pass
return return
if data.startswith(b'%!PS-Adobe'):
self.handle_postscript(data)
else:
identifier = server.path[1:]
server.printer.print(io.BytesIO(data), mode='text', identifier=identifier)
def handle_postscript(self, data):
'Print PostScript data to printer, converting to PBM first with GhostScript `gs`'
server = self.server
platform_system = platform.system() platform_system = platform.system()
# https://ghostscript.com/doc/9.54.0/Use.htm#Output_device # https://ghostscript.com/doc/9.54.0/Use.htm#Output_device
if platform_system == 'Windows': if platform_system == 'Windows':
@ -76,15 +73,12 @@ class IPP():
'-dFitPage', '-dFitPage', '-dFitPage', '-dFitPage',
'-sOutputFile=-', '-' '-sOutputFile=-', '-'
], executable=gsexe, stdin=subprocess.PIPE, stdout=subprocess.PIPE) ], executable=gsexe, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
pbm_data, _ = gsproc.communicate(data_to_print) pbm_data, _ = gsproc.communicate(data)
try: try:
if gsproc.wait() == 0: if gsproc.wait() == 0:
info = server.path[1:] identifier = server.path[1:]
is_found = await server.printer.filter_device(info, server.settings.scan_time) # TODO: Make IPP can report some errors
if not is_found: server.printer.print(io.BytesIO(pbm_data), mode='pbm', identifier=identifier)
... # TODO: Make IPP can report some errors
raise Exception(f'No printer found with info: {info}')
await server.printer.print_data(pbm_data)
else: else:
raise Exception('Error on invoking Ghostscript') raise Exception('Error on invoking Ghostscript')
server.send_response(200) server.send_response(200)
@ -97,4 +91,3 @@ class IPP():
server.send_response(500) server.send_response(500)
server.send_header('Content-Type', 'application/ipp') server.send_header('Content-Type', 'application/ipp')
server.end_headers() server.end_headers()
server.wfile.write(b'')

16
printer_lib/models.py Normal file
View File

@ -0,0 +1,16 @@
'Printer model specifications'
class Model():
'A printer model'
paper_width: int
is_new_kind: bool
def __init__(self, width, is_new):
self.paper_width = width
self.is_new_kind = is_new
Models = {
'GB01': Model(384, False),
'GB02': Model(384, False),
'GB03': Model(384, True),
'GT01': Model(384, False),
}

View File

@ -1,4 +1,7 @@
'Python lib to read PF2 font file: http://grub.gibibit.com/New_font_format' ''' Python lib for reading PF2 font files: http://grub.gibibit.com/New_font_format
I'd like to put it in Public Domain.
Don't forget to see how it's used in `text_print.py`
'''
import io import io
from typing import Dict, Tuple from typing import Dict, Tuple
@ -57,12 +60,16 @@ class PF2():
descent: int descent: int
character_index: Dict[int, Tuple[int, int]] character_index: Dict[int, Tuple[int, int]]
data_offset: int data_offset: int
data_io: io.IOBase data_io: io.BufferedIOBase
def __init__(self, path='font.pf2', *, read_to_mem=False, missing_character: str='?'): def __init__(self, path='font.pf2', *, read_to_mem=True, missing_character: str='?'):
self.missing_character_code = ord(missing_character) self.missing_character_code = ord(missing_character)
self.in_memory = read_to_mem self.in_memory = read_to_mem
file = open(path, 'rb') file = open(path, 'rb')
if read_to_mem:
self.data_io = io.BytesIO(file.read())
file.close()
file = self.data_io
self.is_pf2 = (file.read(12) == b'FILE\x00\x00\x00\x04PFF2') self.is_pf2 = (file.read(12) == b'FILE\x00\x00\x00\x04PFF2')
if not self.is_pf2: if not self.is_pf2:
return return
@ -80,13 +87,7 @@ class PF2():
) )
continue continue
elif name == b'DATA': elif name == b'DATA':
if read_to_mem: file.seek(0)
self.data_io = io.BytesIO(file.read())
self.data_offset = -file.tell()
file.close()
else:
self.data_io = file
self.data_offset = 0
break break
data = file.read(data_length) data = file.read(data_length)
if name == b'NAME': if name == b'NAME':
@ -116,7 +117,7 @@ class PF2():
info = self.character_index[self.missing_character_code] info = self.character_index[self.missing_character_code]
_compression, offset = info _compression, offset = info
data = self.data_io data = self.data_io
data.seek(offset + self.data_offset) data.seek(offset)
char = Character() char = Character()
char.width = uint16be(data.read(2)) char.width = uint16be(data.read(2))
char.height = uint16be(data.read(2)) char.height = uint16be(data.read(2))
@ -130,7 +131,5 @@ class PF2():
__getitem__ = get_char __getitem__ = get_char
def close(self): def __del__(self):
'Close the data IO, if it\'s a real file'
if not self.in_memory:
self.data_io.close() self.data_io.close()

87
printer_lib/text_print.py Normal file
View File

@ -0,0 +1,87 @@
'Things used by Text Printing feature'
from .pf2 import PF2
class TextCanvas():
'Canvas for text printing, requires PF2 lib'
width: int
height: int
canvas: bytearray = None
rtl: bool
wrap: bool
pf2 = None
def __init__(self, width, *, wrap=False, rtl=False):
self.pf2 = PF2()
self.width = width
self.wrap = wrap
self.rtl = rtl
self.height = self.pf2.max_height + self.pf2.descent
self.flush_canvas()
def flush_canvas(self):
'Flush the canvas, returning the canvas data'
if self.canvas is None:
pbm_data = None
else:
pbm_data = bytearray(self.canvas)
self.canvas = bytearray(self.width * self.height // 8)
return pbm_data
def puttext(self, text):
''' Put the specified text to canvas.
It's a generator, will `yield` the data produced, per line.
'''
text = text.replace('\t', ' ' * 4)
canvas_length = len(self.canvas)
pf2 = self.pf2
current_width = 0
last_space_at = -1
width_at_last_space = 0
break_points = set()
characters = {}
for i, s in enumerate(text):
if s not in characters:
characters[s] = pf2[s]
char = characters[s]
if s == ' ':
last_space_at = i
width_at_last_space = current_width
if (current_width > self.width and
last_space_at != -1):
break_points.add(last_space_at)
current_width -= width_at_last_space
if s == '\n':
current_width = 0
continue
current_width += pf2.point_size // 2 # + char.x_offset
current_width = 0
for i, s in enumerate(text):
char = characters[s]
if (i in break_points or s == '\n' or
current_width + char.width > self.width):
# print(current_width, end=' ')
yield self.flush_canvas()
current_width = 0
if s in ' ':
continue
if ord(s) in range(0x00, 0x20): # glyphs that should not be printed out
continue
for x in range(char.width):
for y in range(char.height):
rtl_current_width = self.width - current_width - char.width - 1
target_x = x + char.x_offset
target_y = pf2.ascent + (y - char.height) - char.y_offset
canvas_byte = (self.width * target_y + current_width + target_x) // 8
canvas_bit = 7 - (self.width * target_y + current_width + target_x) % 8
if self.rtl:
canvas_byte = (self.width * target_y + rtl_current_width + target_x) // 8
canvas_bit = 7 - (self.width * target_y + rtl_current_width + target_x) % 8
else:
canvas_byte = (self.width * target_y + current_width + target_x) // 8
canvas_bit = 7 - (self.width * target_y + current_width + target_x) % 8
if canvas_byte < 0 or canvas_byte >= canvas_length:
continue
char_byte = (char.width * y + x) // 8
char_bit = 7 - (char.width * y + x) % 8
self.canvas[canvas_byte] |= (
char.bitmap_data[char_byte] & (0b1 << char_bit)
) >> char_bit << canvas_bit
current_width += char.device_width

View File

@ -10,26 +10,15 @@
Gegenwärtig: Gegenwärtig:
| | | | | |
|-------------|-------------------| |----|----|
| Supported | GB01, GB02, GT01 | | Supported | GB01, GB02, GT01, GB03 |
| Maybe | GB03 | <!-- | Maybe | N/A | -->
| Planned | N/A | <!-- | Planned | N/A | -->
## Funktionen ## Funktionen
*Derzeit befindet sich die Software im Alpha-Stadium. Mehr wird es bald geben!* *Derzeit befindet sich die Software im Alpha-Stadium. Mehr wird es bald geben!*
| Available | Partial | Planned |
|-----------------|-----------|---------------|
| Web Interface | CUPS/IPP* | Visual Editor |
| Print a Picture | | Help/Manual |
| Command-line | | Text Printing |
<!-- May comment the line below if there are none -->
\* In development code. Will be released in a short period.
*Along with…*
- Simple! - Simple!
- Bedienung über eine Web-UI direkt im Browser, - Bedienung über eine Web-UI direkt im Browser,
- oder besorgen Sie sich die Android-Version! - oder besorgen Sie sich die Android-Version!

View File

@ -10,26 +10,13 @@
目前有: 目前有:
| | | | | |
|-----------|-------------------| |----|----|
| 支持 | GB01, GB02, GT01 | | 支持 | GB01, GB02, GT01, GB03 |
| 可能支持 | GB03 |
| 计划 | 暂无 |
## 特性 ## 特性
*当前仍在继续开发。以后会有更多!* *当前仍在继续开发。以后会有更多!*
| 可用 | 部分 | 计划 |
|-----------|-----------|---------------|
| 网页界面 | CUPS/IPP* | 可视化编辑器 |
| 打印图片 | | 帮助/文档 |
| 命令行 | | 文本打印 |
<!-- 若没有 *,可注释下一句 -->
\* 存在于开发代码中。将在短时间内发布。
*当然还有……*
- 简易! - 简易!
- 在网页界面进行操作, - 在网页界面进行操作,
- 或者获取安卓应用! - 或者获取安卓应用!
@ -38,6 +25,17 @@
- 语言支持!您可参与翻译! - 语言支持!您可参与翻译!
- 良好的用户界面,可适应桌面/移动端/明暗主题! - 良好的用户界面,可适应桌面/移动端/明暗主题!
- 功能丰富!
- 网页界面,所有人都可以用!
- 控制打印机配置
- 打印照片,或单纯地进行测试
- 命令行,技术爱好者必备!
- 使用一些参数控制打印机
- 简易、简化的文字打印
- 让程序的每一部分发挥作用
- 其他一些好东西!
- 服务器也具有 CUPS/IPP 能力
- 跨平台! - 跨平台!
- 较新的 Windows 10 及以上 - 较新的 Windows 10 及以上
- GNU/Linux - GNU/Linux
@ -76,11 +74,17 @@ python3 server.py
sudo pacman -S bluez bluez-utils sudo pacman -S bluez bluez-utils
``` ```
*以后将有软件包!*
### MacOS ### MacOS
MacOS 用户请首先安装 [Python 3](https://www.python.org/)。 MacOS 用户请首先安装 [Python 3](https://www.python.org/)
然后在终端使用 `pip` 安装 `pyobjc``bleak`
```bash
pip3 install pyobjc bleak
```
然后获取“纯净(pure)”版,并做同样的事情: 然后获取“单一(bare)”版,并做同样的事情:
```bash ```bash
python3 server.py python3 server.py
``` ```
@ -94,15 +98,13 @@ python3 server.py
当已安装 [Python 3](https://www.python.org/) 时,您可以直接获取“纯净(pure)”版, 当已安装 [Python 3](https://www.python.org/) 时,您可以直接获取“纯净(pure)”版,
或在已使用 `pip` 安装 `bleak` 时使用“单一(bare)”版。 或在已使用 `pip` 安装 `bleak` 时使用“单一(bare)”版。
命令行高手?直接用 `printer.py`
查看所有[发布版本](https://github.com/NaitLee/Cat-Printer/releases) 查看所有[发布版本](https://github.com/NaitLee/Cat-Printer/releases)
## 有问题? ## 有问题?
有想法?开个 issue 有想法?去 Discussion 讨论
如果能行PR 也可以! 如果能行Pull Request 也可以!
## 许可证 ## 许可证
@ -126,7 +128,7 @@ Copyright © 2022 NaitLee Soft. 保留一些权利。
- 当然不能没有 Python 和 Web 技术! - 当然不能没有 Python 和 Web 技术!
- [Bleak](https://bleak.readthedocs.io/en/latest/) 跨平台蓝牙低功耗库,牛! - [Bleak](https://bleak.readthedocs.io/en/latest/) 跨平台蓝牙低功耗库,牛!
- [roddeh-i18n](https://github.com/roddeh/i18njs)很好! - [roddeh-i18n](https://github.com/roddeh/i18njs)当前内置的国际化脚本受此启发
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方 - [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命 - [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
- Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity` - Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity`

210
server.py
View File

@ -1,16 +1,25 @@
'Cat Printer - Serve a Web UI' 'Cat Printer - Serve a Web UI'
# if pylint is annoying you, see file .pylint-rc # if pylint is annoying you, see file .pylintrc
import os import os
import io
import sys import sys
import json import json
import asyncio
import platform import platform
# Don't use ThreadingHTTPServer if you're going to use pyjnius! from http.server import BaseHTTPRequestHandler
from http.server import BaseHTTPRequestHandler, HTTPServer #, ThreadingHTTPServer
from bleak.exc import BleakDBusError, BleakError # For now we can't use `ThreadingHTTPServer`
from printer import PrinterDriver from http.server import HTTPServer
# import `printer` first, to diagnostic some common errors
from printer import PrinterDriver, PrinterError, I18n, info
from bleak.exc import BleakDBusError, BleakError # pylint: disable=wrong-import-order
from printer_lib.ipp import IPP
IsAndroid = (os.environ.get("P4A_BOOTSTRAP") is not None)
class DictAsObject(dict): class DictAsObject(dict):
""" Let you use a dict like an object in JavaScript. """ Let you use a dict like an object in JavaScript.
@ -20,23 +29,8 @@ class DictAsObject(dict):
def __setattr__(self, key, value): def __setattr__(self, key, value):
self[key] = value self[key] = value
class PrinterServerError(Exception): class PrinterServerError(PrinterError):
'Error of PrinterServer' '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]
def log(message):
'For logging a message'
print(message)
mime_type = { mime_type = {
'html': 'text/html;charset=utf-8', 'html': 'text/html;charset=utf-8',
@ -51,24 +45,34 @@ def mime(url: str):
'Get pre-defined MIME type of a certain url by extension name' 'Get pre-defined MIME type of a certain url by extension name'
return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream']) return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream'])
class PrinterServer(BaseHTTPRequestHandler): class PrinterServerHandler(BaseHTTPRequestHandler):
'(Local) server for Cat Printer Web interface' '(Local) server handler for Cat Printer Web interface'
buffer = 4 * 1024 * 1024 buffer = 4 * 1024 * 1024
max_payload = buffer * 16 max_payload = buffer * 16
settings = DictAsObject({ settings = DictAsObject({
'config_path': 'config.json', 'config_path': 'config.json',
'version': 1,
'is_android': False, 'is_android': False,
'printer': None, 'scan_timeout': 5.0,
'scan_time': 3,
'frequency': 0.8,
'dry_run': False 'dry_run': False
}) })
printer = PrinterDriver() _settings_blacklist = (
ipp = None 'printer', 'is_android'
)
printer: PrinterDriver = PrinterDriver()
ipp: IPP = None
def log_request(self, _code=200, _size=0): def log_request(self, _code=200, _size=0):
pass pass
def log_error(self, *_args): def log_error(self, *_args):
pass pass
def do_GET(self): def do_GET(self):
'Called when server get a GET http request' 'Called when server get a GET http request'
path = 'www' + self.path path = 'www' + self.path
@ -91,24 +95,28 @@ class PrinterServer(BaseHTTPRequestHandler):
if not self.wfile.write(chunk): if not self.wfile.write(chunk):
break break
return return
def api_success(self):
'Called when a simple API call is being considered successful' def api_success(self, body_json=None):
'Called when an API call is being considered successful'
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', mime('json')) self.send_header('Content-Type', mime('json'))
self.end_headers() self.end_headers()
if body_json is None:
self.wfile.write(b'{}') self.wfile.write(b'{}')
def api_fail(self, error_json, error=None): else:
self.wfile.write(json.dumps(body_json).encode('utf-8'))
def api_fail(self, error_json):
'Called when an API call is failed' 'Called when an API call is failed'
self.send_response(500) self.send_response(500)
self.send_header('Content-Type', mime('json')) self.send_header('Content-Type', mime('json'))
self.end_headers() self.end_headers()
self.wfile.write(json.dumps(error_json).encode('utf-8')) self.wfile.write(json.dumps(error_json).encode('utf-8'))
self.wfile.flush() self.wfile.flush()
if isinstance(error, Exception):
raise error
def load_config(self): def load_config(self):
'Load config file, or if not exist, create one with default' 'Load config file, or if not exist, create one with default'
if os.environ.get("P4A_BOOTSTRAP") is not None: if IsAndroid:
self.settings['is_android'] = True self.settings['is_android'] = True
from android.storage import app_storage_path # pylint: disable=import-error from android.storage import app_storage_path # pylint: disable=import-error
settings_path = app_storage_path() settings_path = app_storage_path()
@ -118,19 +126,38 @@ class PrinterServer(BaseHTTPRequestHandler):
) )
if os.path.exists(self.settings.config_path): if os.path.exists(self.settings.config_path):
with open(self.settings.config_path, 'r', encoding='utf-8') as file: with open(self.settings.config_path, 'r', encoding='utf-8') as file:
self.settings = DictAsObject(json.load(file)) settings = DictAsObject(json.load(file))
if (settings.version is None or
settings.version < self.settings.version):
# Version too old, start over
# TODO: selective?
self.save_config()
return
self.settings = settings
else: else:
self.save_config() self.save_config()
def save_config(self): def save_config(self):
'Save config file' 'Save config file'
with open(self.settings.config_path, 'w', encoding='utf-8') as file: with open(self.settings.config_path, 'w', encoding='utf-8') as file:
json.dump(self.settings, file, indent=4) settings = {}
for i in self.settings:
if i not in self._settings_blacklist:
settings[i] = self.settings[i]
json.dump(settings, file, indent=4)
def update_printer(self): def update_printer(self):
'Update `PrinterDriver` state/config' 'Update `PrinterDriver` state/config'
self.printer.dry_run = self.settings.dry_run self.printer.dry_run = self.settings.dry_run
self.printer.frequency = float(self.settings.frequency) self.printer.scan_timeout = self.settings.scan_timeout
self.printer.fake = self.settings.fake
self.printer.dump = self.settings.dump
self.printer.flip_h = self.settings.flip_h
self.printer.flip_v = self.settings.flip_v
if self.settings.printer is not None: if self.settings.printer is not None:
self.printer.name, self.printer.address = self.settings.printer.split(',') name, address = self.settings.printer.split(',')
self.printer.connect(name, address)
def handle_api(self): def handle_api(self):
'Handle API request from POST' 'Handle API request from POST'
content_length = int(self.headers.get('Content-Length')) content_length = int(self.headers.get('Content-Length'))
@ -138,121 +165,110 @@ class PrinterServer(BaseHTTPRequestHandler):
api = self.path[1:] api = self.path[1:]
if api == 'print': if api == 'print':
self.update_printer() self.update_printer()
loop = asyncio.new_event_loop() self.printer.print(io.BytesIO(body))
try:
devices = loop.run_until_complete(
self.printer.print_data(body)
)
self.api_success() self.api_success()
finally:
loop.close()
return return
data = DictAsObject(json.loads(body)) data = DictAsObject(json.loads(body))
if api == 'devices': if api == 'devices':
loop = asyncio.new_event_loop() self.printer.connect(None)
try:
devices = loop.run_until_complete(
self.printer.search_all_printers(float(self.settings.scan_time))
)
finally:
loop.close()
devices_list = [{ devices_list = [{
'name': device.name, 'name': device.name,
'address': device.address 'address': device.address
} for device in devices] } for device in self.printer.scan()]
self.send_response(200) self.api_success({
self.send_header('Content-Type', mime('json'))
self.end_headers()
self.wfile.write(json.dumps({
'devices': devices_list 'devices': devices_list
}).encode('utf-8')) })
return return
if api == 'query': if api == 'query':
self.load_config() self.load_config()
self.send_response(200) self.api_success(self.settings)
self.send_header('Content-Type', mime('json'))
self.end_headers()
self.wfile.write(json.dumps(self.settings).encode('utf-8'))
return return
if api == 'set': if api == 'set':
for key in data: for key in data:
self.settings[key] = data[key] self.settings[key] = data[key]
self.save_config() self.save_config()
self.update_printer()
self.api_success() self.api_success()
return return
if api == 'exit': if api == 'exit':
self.api_success() self.api_success()
self.exit()
def exit(self):
'Stop correctly & cleanly'
self.save_config() self.save_config()
# Only usable when using ThreadingHTTPServer self.printer.unload()
# server.shutdown()
sys.exit(0) sys.exit(0)
def do_POST(self): def do_POST(self):
'Called when server get a POST http request' 'Called when server get a POST http request'
content_length = int(self.headers.get('Content-Length', -1)) content_length = int(self.headers.get('Content-Length', -1))
if (content_length == -1 or if (content_length < -1 or
content_length > self.max_payload content_length > self.max_payload
): ):
self.send_response(400)
self.send_header('Content-Type', mime('txt'))
self.end_headers()
return return
if self.headers.get('Content-Type') == 'application/ipp': if self.headers.get('Content-Type') == 'application/ipp':
if self.ipp is None: if self.ipp is None:
try: self.ipp = IPP(self)
from additional.ipp import IPP self.ipp.handle_ipp()
self.load_config()
except ImportError:
# TODO: Better response?
return
self.ipp = IPP(self, self.printer)
self.update_printer()
body = self.rfile.read(content_length)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(self.ipp.handle_ipp(body))
finally:
loop.close()
return return
try: try:
self.handle_api() self.handle_api()
return return
except BleakDBusError as e: except BleakDBusError as e:
# TODO: better error reporting
self.api_fail({ self.api_fail({
'code': -2,
'name': e.dbus_error, 'name': e.dbus_error,
'details': e.dbus_error_details 'details': e.dbus_error_details
}) })
except BleakError as e: except BleakError as e:
self.api_fail({ self.api_fail({
'code': -3,
'name': 'BleakError', 'name': 'BleakError',
'details': str(e) 'details': str(e)
}) })
except PrinterServerError as e: except PrinterError as e:
self.api_fail({ self.api_fail({
'code': e.code, 'name': e.message,
'name': e.name, 'details': e.message_localized
'details': e.details
}) })
except Exception as e: except Exception as e:
self.api_fail({ self.api_fail({
'code': -1,
'name': 'Exception', 'name': 'Exception',
'details': str(e) 'details': str(e)
}, e) })
raise
class PrinterServer(HTTPServer):
''' (local) server for Cat Printer Web Interface
The reason to override is to only init the handler once,
avoiding confliction, and stop cleanly
'''
handler: PrinterServerHandler = None
def finish_request(self, request, client_address):
if self.handler is None:
self.handler = PrinterServerHandler(request, client_address, self)
return
self.handler.__init__(request, client_address, self)
def server_close(self):
if self.handler is not None:
self.handler.exit()
super().server_close()
def serve(): def serve():
'Start server' 'Start server'
address, port = '127.0.0.1', 8095 address, port = '127.0.0.1', 8095
listen_all = False listen_all = False
if '-a' in sys.argv: if '-a' in sys.argv:
print('Will listen on ALL addresses') info(I18n['will-listen-on-all-addresses'])
listen_all = True listen_all = True
# Again, Don't use ThreadingHTTPServer if you're going to use pyjnius! server = PrinterServer(('' if listen_all else address, port), PrinterServer)
server = HTTPServer(('' if listen_all else address, port), PrinterServer)
service_url = f'http://{address}:{port}/' service_url = f'http://{address}:{port}/'
if '-s' in sys.argv: if '-s' in sys.argv:
print(service_url) info(I18n['serving-at-0', service_url])
else: else:
operating_system = platform.uname().system operating_system = platform.uname().system
if operating_system == 'Windows': if operating_system == 'Windows':
@ -262,11 +278,11 @@ def serve():
# TODO: I don't know about macOS # TODO: I don't know about macOS
# elif operating_system == 'macOS': # elif operating_system == 'macOS':
else: else:
print(f'Will serve application at: {service_url}') info(I18n['serving-at-0', service_url])
try: try:
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
pass server.server_close()
if __name__ == '__main__': if __name__ == '__main__':
serve() serve()

49
www/i18n-ext.js Normal file
View File

@ -0,0 +1,49 @@
/**
* Methods to know which string to use, per language
*/
var I18nExtensions = (function() {
/** @type {ExtensionOf<'en-US'>} */
function english(things, conditions) {
let text = conditions;
for (let index in things) {
let value = things[index];
if (value == 1) text = conditions['single'];
else text = conditions['multiple'];
if (!text && typeof value === 'number') {
if (value < 10 || value > 20) {
if (value % 10 === 1) text = conditions['1st'];
else if (value % 10 === 2) text = conditions['2nd'];
else if (value % 10 === 3) text = conditions['3rd'];
else text = conditions['nth'];
} else text = conditions['nth'];
}
}
return text;
}
/** @type {ExtensionOf<'zh-CN'>} */
function chinese(things, conditions) {
let text = conditions;
return text;
}
/** @type {ExtensionOf<'de-DE'>} */
function german(things, conditions) {
let text = conditions;
for (let index in things) {
let value = things[index];
if (value == 1) text = conditions['single'];
else text = conditions['multiple'];
}
return text;
}
return {
'en-US': english,
'zh-CN': chinese,
'de-DE': german
}
})();

32
www/i18n.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
type DictOf<T> = { [key: string]: T };
type Conditions = DictOf<string>;
type ConditionsOf<K extends Languages> = AllConditions[K];
type LanguageData = DictOf<Conditions | string>;
type Things = { [index: number | string]: number | string } | Array<number | string>;
type Extension = (things: Things, conditions: Conditions) => string;
type ExtensionOf<K extends Languages> = (things: Things, conditions: ConditionsOf<K>) => string;
type Languages = keyof AllConditions;
/**
* All known possible condition keys, per language
*/
type AllConditions = {
'en-US': {
'single': string,
'multiple': string,
'1st': string,
'2nd': string,
'3rd': string,
'nth': string
},
'de-DE': {
'single': string,
'multiple': string
},
'zh-CN': {}
};
interface I18nCallable extends I18n {
(text: string, things: Things, can_change_things: boolean): string;
}

109
www/i18n.js Normal file
View File

@ -0,0 +1,109 @@
'use strict';
/**
* Yet another i18n solution
*/
class I18n {
/** @type {DictOf<LanguageData>} */
database;
/** @type {DictOf<Extension>} */
extensions;
/** @type {Languages} */
language;
constructor(language = 'en') {
this.database = {};
this.extensions = {};
this.useLanguage(language);
}
/**
* Use this language as main language
* @param {Languages} language
*/
useLanguage(language) {
this.language = language;
if (!this.database[language])
this.database[language] = {};
}
/**
* Add data as corresponding language, also to
* other (added) languages as fallback
* @param {Languages} language
* @param {LanguageData} data
*/
add(language, data) {
if (!this.database[language])
this.database[language] = {};
for (let key in data) {
let value = data[key];
this.database[language][key] = value;
for (let lang in this.database)
if (!this.database[lang][key])
this.database[lang][key] = value;
}
}
/**
* Use extension in the language
* @param {Languages} language
* @param {Extension} extension
*/
extend(language, extension) {
this.extensions[language] = extension;
}
/**
* Alias a language code to another, usually formal/more used/as fallback
* @param {DictOf<Languages>} aliases
*/
alias(aliases) {
for (let alt_code in aliases) {
let code = aliases[alt_code];
this.database[alt_code] = this.database[code];
this.extensions[alt_code] = this.extensions[code];
}
}
/**
* Translate a string ("text"), using "things" such as numbers
* @param {string} text
* @param {Things} things
* @param {boolean} can_change_things
*/
translate(text, things, can_change_things = true) {
let conditions = this.database[this.language][text] || text;
if (!things) return conditions;
if (!can_change_things) things = { ... things };
if (this.extensions[this.language] && typeof conditions !== 'string')
text = this.extensions[this.language](things, conditions);
else text = conditions;
for (let key in things) {
text = text.replace(`{${key}}`, things[key].toString());
}
return text;
}
}
/**
* A i18n instance that is directly callable
* @type {I18nCallable}
*/
var i18n = (function() {
let instance = new I18n();
/**
* @param {string} text
* @param {Things} things
* @param {boolean} can_change_things
*/
let i18n_callable = function(text, things, can_change_things = true) {
return instance.translate.call(i18n_callable, text, things, can_change_things);
}
Object.setPrototypeOf(i18n_callable, instance);
if (typeof I18nExtensions === 'object') {
for (let key in I18nExtensions)
instance.extend(key, I18nExtensions[key]);
}
return i18n_callable;
})();

View File

@ -87,15 +87,15 @@
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" /> <input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" />
<span data-i18n="-seconds">seconds</span> <span data-i18n="-seconds">seconds</span>
<br /> <br />
<label for="frequency" data-i18n="transmission-speed-">Data Speed:</label> <input type="checkbox" name="flip-h" id="flip-h" />
<select id="frequency"> <label for="flip-h" data-i18n="flip-horizontally">Flip Horizontally</label>
<option value="1.0" data-i18n="low">Low</option> <input type="checkbox" name="flip-v" id="flip-v" />
<option value="0.8" data-i18n="moderate" selected>Moderate</option> <label for="flip-v" data-i18n="flip-vertically">Flip Vertically</label>
<option value="0.6" data-i18n="high">High</option>
</select>
<br /> <br />
<input type="checkbox" name="dry-run" id="dry-run" /> <input type="checkbox" name="dry-run" id="dry-run" />
<label for="dry-run" data-i18n="dry-run">Dry Run</label> <label for="dry-run" data-i18n="dry-run">Dry Run</label>
<input type="checkbox" name="dump" id="dump" />
<label for="dump" data-i18n="dump-traffic">Dump Traffic</label>
</div> </div>
<div class="sub panel"> <div class="sub panel">
<a href="#" data-i18n="system">System</a> <a href="#" data-i18n="system">System</a>

View File

@ -42,10 +42,16 @@
</tr> </tr>
<tr> <tr>
<td><a href="i18n.js">i18n.js</a></td> <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="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
<td><a href="https://github.com/roddeh/i18njs">i18n.js</a></td> <td><a href="i18n.js">i18n.js</a></td>
<td>For internationalization (language support)</td> <td>For internationalization (language support)</td>
</tr> </tr>
<tr>
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td>I18n "extensions"</td>
</tr>
<tr> <tr>
<td><a href="main.js">main.js</a></td> <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="https://www.gnu.org/licenses/gpl-3.0.html">GNU-GPL-3.0-or-later</a></td>

View File

@ -1,5 +1,4 @@
{ {
"values": {
"cat-printer": "Cat Printer", "cat-printer": "Cat Printer",
"printer": "Drucker", "printer": "Drucker",
"device-": "Gerät:", "device-": "Gerät:",
@ -38,10 +37,10 @@
"scan-time-": "Scanzeit:", "scan-time-": "Scanzeit:",
"-seconds": "Sekunden", "-seconds": "Sekunden",
"no-available-devices-found": "Keine verfügbaren Geräte gefunden", "no-available-devices-found": "Keine verfügbaren Geräte gefunden",
"found-1-available-devices": [ "found-0-available-devices": {
[1, 1, "1 verfügbares Gerät gefunden"], "single": "{0} verfügbares Gerät gefunden",
[2, null, "%n verfügbare Geräte gefunden"] "multiple": "{0} verfügbare Geräte gefunden"
], },
"please-check-if-the-printer-is-down": "Bitte prüfe, ob der Drucker ausgeschaltet ist", "please-check-if-the-printer-is-down": "Bitte prüfe, ob der Drucker ausgeschaltet ist",
"printing": "Drucken…", "printing": "Drucken…",
"finished": "Fertiggestellt", "finished": "Fertiggestellt",
@ -51,15 +50,12 @@
"you-can-close-this-page-manually": "Sie können diese Seite manuell schließen", "you-can-close-this-page-manually": "Sie können diese Seite manuell schließen",
"please-enable-bluetooth": "Bitte aktivieren Sie Bluetooth", "please-enable-bluetooth": "Bitte aktivieren Sie Bluetooth",
"error-happened-please-check-error-message": "Fehler aufgetreten, bitte Fehlermeldung einsehen", "error-happened-please-check-error-message": "Fehler aufgetreten, bitte Fehlermeldung einsehen",
"you-can-seek-for-help-with-detailed-info-below": "Sie können mit den nachstehenden ausführlichen Informationen Hilfe bekommen.", "you-can-seek-for-help-with-detailed-info-below": "Sie können mit den nachstehenden ausführlichen Informationen Hilfe bekommen",
"or-try-to-scan-longer": "Oder versuchen Sie, länger zu suchen", "or-try-to-scan-longer": "Oder versuchen Sie, länger zu suchen",
"print-pbm-image-to-cat-printer": "PBM-Bild auf Cat Printer drucken.", "print-to-cat-printer": "PBM-Bild auf Cat Printer drucken",
"supported-models-": "Unterstützte Modelle:", "supported-models-": "Unterstützte Modelle:",
"path-to-pbm-file-dash-for-stdin": "Pfad zur PBM-Datei. '-' für stdin.", "path-to-input-file-dash-for-stdin": "Pfad zur Datei. '-' für stdin",
"scan-for-specified-seconds": "Suchlauf für die angegebenen Sekunden", "scan-for-specified-seconds": "Suchlauf für die angegebenen Sekunden",
"specify-printer-mac-address": "Geben Sie die MAC-Adresse des Druckers an", "dump-the-traffic": "Den Datenverkehr auf dem Drucker ausgeben und PBM-Bild beim Textdruck",
"communication-frequency-0.8-or-1-recommended": "Übermittlungsfrequenz. 0,8 oder 1 empfohlen.", "text-printing-mode": "Textdruckmodus"
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "Den Datenverkehr auf dem Drucker ausgeben und PBM-Bild beim Textdruck.",
"text-printing-mode-input-text-from-stdin": "Textdruckmodus, Eingabe von stdin."
}
} }

View File

@ -1,5 +1,4 @@
{ {
"values": {
"cat-printer": "Cat Printer", "cat-printer": "Cat Printer",
"printer": "Printer", "printer": "Printer",
"device-": "Device:", "device-": "Device:",
@ -38,10 +37,10 @@
"scan-time-": "Scan time:", "scan-time-": "Scan time:",
"-seconds": "seconds", "-seconds": "seconds",
"no-available-devices-found": "No available devices found", "no-available-devices-found": "No available devices found",
"found-1-available-devices": [ "found-0-available-devices": {
[1, 1, "Found 1 available device"], "single": "Found {0} available device",
[2, null, "Found %n available devices"] "multiple": "Found {0} available devices"
], },
"please-check-if-the-printer-is-down": "Please check if the printer is down", "please-check-if-the-printer-is-down": "Please check if the printer is down",
"printing": "Printing…", "printing": "Printing…",
"finished": "Finished", "finished": "Finished",
@ -51,17 +50,38 @@
"you-can-close-this-page-manually": "You can close this page manually", "you-can-close-this-page-manually": "You can close this page manually",
"please-enable-bluetooth": "Please enable Bluetooth", "please-enable-bluetooth": "Please enable Bluetooth",
"error-happened-please-check-error-message": "Error happened, please check error message", "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.", "you-can-seek-for-help-with-detailed-info-below": "You can seek for help with detailed info below",
"or-try-to-scan-longer": "Or try to scan longer", "or-try-to-scan-longer": "Or try to scan longer",
"print-pbm-image-to-cat-printer": "Print PBM image to Cat Printer.", "print-to-cat-printer": "Print to Cat Printer",
"supported-models-": "Supported models:", "supported-models-": "Supported models:",
"path-to-pbm-file-dash-for-stdin": "Path to PBM file. '-' for stdin.", "path-to-input-file-dash-for-stdin": "Path to input file. '-' for stdin",
"scan-for-specified-seconds": "Scan for specified seconds", "scan-for-specified-seconds": "Scan for specified seconds",
"specify-printer-mac-address": "Specify printer MAC address",
"communication-frequency-0.8-or-1-recommended": "Communication frequency. 0.8 or 1 recommended.",
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "Dump the traffic to printer, and PBM image when text printing.", "dump-the-traffic": "Dump the traffic",
"text-printing-mode-input-text-from-stdin": "Text printing mode, input from stdin." "text-printing-mode": "Text printing mode",
}
"please-install-pyobjc-via-pip": "Please install `pyobjc` via pip",
"please-install-bleak-via-pip": "Please install `bleak` via pip",
"folder-printer_lib-is-incomplete-or-missing-please-check": "Folder `printer_lib` is incomplete or missing, please check",
"input-is-not-pbm-image": "Input is not PBM image",
"unsuitable-image-width-expected-0-got-1": "Unsuitable image width, expected {0}, got {1}",
"broken-pbm-image": "Broken PBM image",
"input-is-not-text-file": "Input is not text file",
"match-printer-with-this-name-or-address": "Match printer with this name or address",
"virtual-run-on-specified-model": "Virtual run on specified model",
"font-size-0": "Font size {0}",
"stopping": "Stopping",
"connecting": "Connecting",
"model-0-is-not-supported-yet": "Model '{0}' is not supported yet",
"invalid-address-0": "Invalid address: '{0}'",
"will-listen-on-all-addresses": "Will listen on ALL addresses",
"serving-at-0": "Serving at {0}",
"disconnecting-from-printer": "Disconnecting from printer",
"connected-to-0-1": "Connected to {0} {1}",
"flip-horizontally": "Flip Horizontally",
"flip-vertically": "Flip Vertically",
"dump-traffic": "Dump Traffic",
"right-to-left-text-order": "Right-to-left text order",
"auto-wrap-line": "Auto wrap line"
} }

View File

@ -1,5 +1,4 @@
{ {
"values": {
"cat-printer": "猫咪打印机", "cat-printer": "猫咪打印机",
"printer": "打印机", "printer": "打印机",
"device-": "设备:", "device-": "设备:",
@ -38,9 +37,7 @@
"scan-time-": "扫描时间:", "scan-time-": "扫描时间:",
"-seconds": "秒", "-seconds": "秒",
"no-available-devices-found": "未发现可用设备", "no-available-devices-found": "未发现可用设备",
"found-1-available-devices": [ "found-0-available-devices": "发现 {0} 个可用设备",
[1, null, "发现 %n 个可用设备"]
],
"please-check-if-the-printer-is-down": "请检查打印机是否已关闭", "please-check-if-the-printer-is-down": "请检查打印机是否已关闭",
"printing": "打印中……", "printing": "打印中……",
"finished": "完成", "finished": "完成",
@ -50,17 +47,37 @@
"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": "您可以使用以下详细信息寻求帮助",
"or-try-to-scan-longer": "或者尝试延长扫描时间", "or-try-to-scan-longer": "或者尝试延长扫描时间",
"print-pbm-image-to-cat-printer": "打印 PBM 图片到猫咪打印机。", "print-to-cat-printer": "打印到猫咪打印机。",
"supported-models-": "支持的型号:", "supported-models-": "支持的型号:",
"path-to-pbm-file-dash-for-stdin": "PBM 文件的位置。“-” 作为标准输入。", "path-to-input-file-dash-for-stdin": "输入文件的位置。使用 '-' 作为标准输入",
"scan-for-specified-seconds": "扫描指定的时长。", "scan-for-specified-seconds": "扫描指定的时长",
"specify-printer-mac-address": "指定打印机的 MAC 地址",
"communication-frequency-0.8-or-1-recommended": "通讯频率。推荐 0.8 或 1。",
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "转储到打印机的数据,和文字打印模式的 PBM 图像。", "dump-the-traffic": "转储数据",
"text-printing-mode-input-text-from-stdin": "文字打印模式,将从标准输入读取文字。" "text-printing-mode": "文字打印模式",
}
"please-install-pyobjc-via-pip": "请从 pip 安装 `pyobjc`",
"please-install-bleak-via-pip": "请从 pip 安装 `bleak`",
"folder-printer_lib-is-incomplete-or-missing-please-check": "文件夹 `printer_lib` 不完整或丢失,请检查",
"input-is-not-pbm-image": "输入不是 PBM 图像",
"unsuitable-image-width-expected-0-got-1": "不适合的图像宽度,需要 {0}, 输入为 {1}",
"broken-pbm-image": "损坏的 PBM 图像",
"input-is-not-text-file": "输入不是文本文件",
"match-printer-with-this-name-or-address": "使用符合此名称或地址的打印机",
"virtual-run-on-specified-model": "在指定的型号模拟运行",
"font-size-0": "字体大小 {0}",
"stopping": "停止中",
"connecting": "正在连接",
"model-0-is-not-supported-yet": "型号 '{0}' 仍未支持",
"invalid-address-0": "无效的地址:'{0}'",
"will-listen-on-all-addresses": "将接受所有地址的连接",
"serving-at-0": "服务器在 {0}",
"disconnecting-from-printer": "正在从打印机断开连接",
"flip-horizontally": "水平翻转",
"flip-vertically": "垂直翻转",
"dump-traffic": "转储数据",
"right-to-left-text-order": "从右到左的文字顺序",
"auto-wrap-line": "自动折行"
} }

View File

@ -6,7 +6,7 @@
var fallbacks = [ var fallbacks = [
// main scripts, which we will directly modify // main scripts, which we will directly modify
'i18n.js', 'image.js', 'main.js', 'i18n-ext.js', 'i18n.js', 'image.js', 'main.js',
// "compatibility" script, produced with eg. typescript tsc // "compatibility" script, produced with eg. typescript tsc
'main.comp.js' 'main.comp.js'
]; ];

View File

@ -38,13 +38,13 @@ class _Notice {
constructor() { constructor() {
this.element = document.getElementById('notice'); this.element = document.getElementById('notice');
} }
_message(message, ...args) { _message(message, things) {
this.element.innerText = i18n(message, ...args) || message; this.element.innerText = i18n(message, things) || message;
} }
makeLogger(class_name) { makeLogger(class_name) {
return (message, ...args) => { return (message, things) => {
this.element.classList.value = class_name; this.element.classList.value = class_name;
this._message(message, ...args); this._message(message, things);
} }
} }
notice = this.makeLogger('notice'); notice = this.makeLogger('notice');
@ -373,7 +373,7 @@ class Main {
putEvent('#button-exit', 'click', this.exit, this); putEvent('#button-exit', 'click', this.exit, this);
putEvent('#button-print', 'click', this.print, this); putEvent('#button-print', 'click', this.print, this);
putEvent('#device-refresh', 'click', this.searchDevices, this); putEvent('#device-refresh', 'click', this.searchDevices, this);
this.attachSetter('#scan-time', 'change', 'scan_time'); this.attachSetter('#scan-time', 'change', 'scan_timeout');
this.attachSetter('#device-options', 'input', 'printer'); this.attachSetter('#device-options', 'input', 'printer');
this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm'); this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm');
this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white'); this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white');
@ -387,7 +387,9 @@ class Main {
this.attachSetter('#threshold', 'change', 'threshold', this.attachSetter('#threshold', 'change', 'threshold',
(value) => this.canvasController.threshold = value (value) => this.canvasController.threshold = value
); );
this.attachSetter('#frequency', 'change', 'frequency'); this.attachSetter('#flip-h', 'change', 'flip_h');
this.attachSetter('#flip-v', 'change', 'flip_v');
this.attachSetter('#dump', 'change', 'dump');
await this.loadConfig(); await this.loadConfig();
this.searchDevices(); this.searchDevices();
resolve(); resolve();
@ -488,7 +490,7 @@ class Main {
hint('#device-refresh'); hint('#device-refresh');
return; return;
} }
Notice.notice('found-1-available-devices', devices.length); Notice.notice('found-0-available-devices', [devices.length]);
hint('#insert-picture'); hint('#insert-picture');
devices.forEach(device => { devices.forEach(device => {
let option = document.createElement('option'); let option = document.createElement('option');
@ -519,18 +521,15 @@ class Main {
} }
async initI18n() { async initI18n() {
if (typeof i18n === 'undefined') return; if (typeof i18n === 'undefined') return;
let language_list = navigator.languages; i18n.useLanguage(navigator.languages[0]);
let loaded_languages = []; for (let language of navigator.languages) {
let data; let data = await fetch(`/lang/${language}.json`)
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); .then(response => response.ok ? response.json() : null);
if (data !== null) { if (data !== null) {
i18n.translator.add(data); i18n.add(language, data);
loaded_languages.unshift(language_list[i]); console.log('Loaded language:', language);
} }
} }
console.log('Language stack:', loaded_languages);
let elements = document.querySelectorAll('*[data-i18n]'); let elements = document.querySelectorAll('*[data-i18n]');
let i18n_data, translated_string; let i18n_data, translated_string;
elements.forEach(element => { elements.forEach(element => {