mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-15 23:00:15 -07:00
Backend Big Update (document later)
This commit is contained in:
parent
1f3c16cf16
commit
205f5ad5ca
6
.gitignore
vendored
6
.gitignore
vendored
@ -3,9 +3,6 @@ __pycache__
|
||||
# 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
|
||||
@ -15,6 +12,7 @@ build-android/advancedwebview
|
||||
# releases
|
||||
build-android/dist
|
||||
*.apk
|
||||
*.apk.*
|
||||
cat-printer*.zip
|
||||
# bleak, the bare pip package as a folder
|
||||
build-common/bleak
|
||||
@ -22,6 +20,8 @@ build-common/bleak
|
||||
build-common/python-win32*
|
||||
# dev config
|
||||
config.json
|
||||
# dev backup
|
||||
*.bak
|
||||
# test files
|
||||
*.dump
|
||||
*.pf2
|
||||
|
@ -7,15 +7,15 @@
|
||||
jobs=4
|
||||
|
||||
[BASIC]
|
||||
class-const-naming-style=snake_case
|
||||
const-naming-style=snake_case
|
||||
class-const-naming-style=PascalCase
|
||||
const-naming-style=PascalCase
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=broad-except,
|
||||
global-statement,
|
||||
fixme,
|
||||
fixme, too-few-public-methods,
|
||||
import-outside-toplevel
|
||||
|
||||
[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
|
||||
|
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@ -4,13 +4,24 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "printer.py",
|
||||
"args": [
|
||||
"-m", "-t", "-d", "-s", "1", "-"
|
||||
"-mt", "-f", "GB02", "COPYING"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
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 ..
|
||||
|
52
README.md
52
README.md
@ -11,34 +11,35 @@ English | [Deutsch](./readme.i18n/README.de_DE.md) | [简体中文](./readme.i18
|
||||
Currently:
|
||||
|
||||
| | |
|
||||
|-------------|-------------------|
|
||||
| Supported | GB01, GB02, GT01 |
|
||||
| Maybe | GB03 |
|
||||
| Planned | N/A |
|
||||
|----|----|
|
||||
| Supported | GB01, GB02, GT01, GB03 |
|
||||
<!-- | Maybe | N/A | -->
|
||||
<!-- | Planned | N/A | -->
|
||||
|
||||
## Features
|
||||
|
||||
*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!
|
||||
- Operate via a Web UI just in browser,
|
||||
- Operate via Web UI just in browser,
|
||||
- or get the Android release!
|
||||
- Even no problem with command line hackers!
|
||||
|
||||
- Friendly!
|
||||
- Language support! You can participate in translation!
|
||||
- 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!
|
||||
- Newer Windows 10 and above
|
||||
- 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
|
||||
```
|
||||
|
||||
*Packaging is also on the way!*
|
||||
|
||||
### 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
|
||||
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).
|
||||
|
||||
|
||||
### Worth to Note
|
||||
|
||||
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`.
|
||||
|
||||
Command line hackers? Just use `printer.py`!
|
||||
|
||||
See the [releases](https://github.com/NaitLee/Cat-Printer/releases) now!
|
||||
|
||||
## 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
|
||||
|
||||
@ -125,7 +129,7 @@ Also interested in code development? See [development.md](development.md)!
|
||||
|
||||
- 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!
|
||||
- [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
|
||||
- [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
|
||||
|
31
TODO
31
TODO
@ -1,26 +1,23 @@
|
||||
|
||||
Note: not ordered. do whatever I/you want
|
||||
|
||||
+ Check GB03, again
|
||||
+ 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
|
||||
+ Cookbook of basic things
|
||||
+ Write good help/manual
|
||||
+ 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...
|
||||
Android guys can help this!
|
||||
? ... Or put to APKPure?
|
||||
|
@ -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
|
@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
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" \
|
||||
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
||||
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
||||
|
@ -4,7 +4,7 @@ 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 \
|
||||
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --release \
|
||||
--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
|
||||
|
@ -22,23 +22,20 @@ if not sys.argv[-1].startswith('-'):
|
||||
bundle_name %= (edition, version)
|
||||
|
||||
ignore_whitelist = (
|
||||
'www/i18n.js',
|
||||
'www/main.comp.js'
|
||||
)
|
||||
|
||||
additional_ignore = (
|
||||
# prevent recurse
|
||||
bundle_name,
|
||||
# non-production (yet)
|
||||
'PKGBUILD', 'systemd',
|
||||
# build helpers
|
||||
'build-*',
|
||||
'?-*.sh',
|
||||
'build-*', '?-*.sh',
|
||||
# no need
|
||||
'.git',
|
||||
'.vscode',
|
||||
'.pylintrc',
|
||||
'.gitignore',
|
||||
'dev-diary.txt',
|
||||
'TODO',
|
||||
'.git', '.gitignore',
|
||||
'.vscode', '.pylintrc',
|
||||
'dev-diary.txt', 'TODO',
|
||||
# cache
|
||||
'*.pyc',
|
||||
# other
|
||||
|
@ -55,3 +55,40 @@ It's finally ready...
|
||||
Documentation.
|
||||
|
||||
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.
|
||||
|
@ -1,6 +1,8 @@
|
||||
|
||||
# Development
|
||||
|
||||
**Note: Some maybe outdated at the moment**
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@ -27,12 +28,11 @@ 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.
|
||||
Alright, you are already well done for basic development. See [files](#files) section for what all the files do.
|
||||
For more, read on...
|
||||
|
||||
### Additional
|
||||
### Optional
|
||||
|
||||
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`
|
||||
- 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
|
||||
- Is single threaded & with static handler, for some reasons
|
||||
- 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
|
||||
@ -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
|
||||
- Bundled all required scripts, see file `0-transpile.sh`
|
||||
- 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:
|
||||
- Small but useful, just look at them directly
|
||||
- `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>`
|
||||
- `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
|
||||
- Adviced to transpile scripts before bundling
|
||||
- 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`
|
||||
|
961
printer.py
961
printer.py
File diff suppressed because it is too large
Load Diff
133
printer_lib/commander.py
Normal file
133
printer_lib/commander.py
Normal 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
38
printer_lib/i18n.py
Normal 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
|
@ -1,56 +1,45 @@
|
||||
''' Provide *very* basic CUPS/IPP support
|
||||
Extracted from version 0.0.2, do more cleaning later...
|
||||
'''
|
||||
|
||||
import io
|
||||
import platform
|
||||
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():
|
||||
'https://datatracker.ietf.org/doc/html/rfc8010'
|
||||
server = None
|
||||
printer = None
|
||||
def __init__(self, server, printer):
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
self.printer = printer
|
||||
async def handle_ipp(self, data):
|
||||
def handle_ipp(self):
|
||||
'Handle an IPP protocol request'
|
||||
server = self.server
|
||||
# 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]
|
||||
content_length = int(server.headers.get('Content-Length'))
|
||||
buffer = io.BytesIO(server.rfile.read(content_length))
|
||||
_ipp_version = (int8(buffer.read(1)), int8(buffer.read(1)))
|
||||
_ipp_operation_id = int16be(buffer.read(2))
|
||||
_ipp_request_id = int32be(buffer.read(4))
|
||||
ipp_operation_attributes_tag = int8(buffer.read(1))
|
||||
attributes = {}
|
||||
data_to_print = b''
|
||||
# this is silly. i want to use io.BytesIO
|
||||
data = b''
|
||||
if ipp_operation_attributes_tag == 0x01:
|
||||
pointer = 9
|
||||
next_name_length_at = 10
|
||||
next_value_length_at = 10
|
||||
name = b''
|
||||
value = b''
|
||||
while data[pointer] != 0x03:
|
||||
tag = data[pointer:pointer + 1]
|
||||
pointer += 1
|
||||
if tag[0] < 0x10: # delimiter-tag
|
||||
while int8(buffer.read(1)) != 0x03:
|
||||
buffer.seek(-1, 1)
|
||||
tag = int8(buffer.read(1))
|
||||
if tag < 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
|
||||
name = buffer.read(int16be(buffer.read(2)))
|
||||
value = buffer.read(int16be(buffer.read(2)))
|
||||
attributes[name] = (tag, value)
|
||||
name = b''
|
||||
value = b''
|
||||
pointer += 1
|
||||
data_to_print = data[pointer:]
|
||||
data = buffer.read()
|
||||
# there are hard coded minimal response. this "just works" on cups
|
||||
if data_to_print == b'':
|
||||
if data == b'':
|
||||
try:
|
||||
server.send_response(200)
|
||||
server.send_header('Content-Type', 'application/ipp')
|
||||
@ -61,6 +50,14 @@ class IPP():
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
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()
|
||||
# https://ghostscript.com/doc/9.54.0/Use.htm#Output_device
|
||||
if platform_system == 'Windows':
|
||||
@ -76,15 +73,12 @@ class IPP():
|
||||
'-dFitPage', '-dFitPage',
|
||||
'-sOutputFile=-', '-'
|
||||
], executable=gsexe, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
pbm_data, _ = gsproc.communicate(data_to_print)
|
||||
pbm_data, _ = gsproc.communicate(data)
|
||||
try:
|
||||
if gsproc.wait() == 0:
|
||||
info = server.path[1:]
|
||||
is_found = await server.printer.filter_device(info, server.settings.scan_time)
|
||||
if not is_found:
|
||||
... # TODO: Make IPP can report some errors
|
||||
raise Exception(f'No printer found with info: {info}')
|
||||
await server.printer.print_data(pbm_data)
|
||||
identifier = server.path[1:]
|
||||
# TODO: Make IPP can report some errors
|
||||
server.printer.print(io.BytesIO(pbm_data), mode='pbm', identifier=identifier)
|
||||
else:
|
||||
raise Exception('Error on invoking Ghostscript')
|
||||
server.send_response(200)
|
||||
@ -97,4 +91,3 @@ class IPP():
|
||||
server.send_response(500)
|
||||
server.send_header('Content-Type', 'application/ipp')
|
||||
server.end_headers()
|
||||
server.wfile.write(b'')
|
16
printer_lib/models.py
Normal file
16
printer_lib/models.py
Normal 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),
|
||||
}
|
@ -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
|
||||
from typing import Dict, Tuple
|
||||
@ -57,12 +60,16 @@ class PF2():
|
||||
descent: int
|
||||
character_index: Dict[int, Tuple[int, 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.in_memory = read_to_mem
|
||||
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')
|
||||
if not self.is_pf2:
|
||||
return
|
||||
@ -80,13 +87,7 @@ class PF2():
|
||||
)
|
||||
continue
|
||||
elif name == b'DATA':
|
||||
if read_to_mem:
|
||||
self.data_io = io.BytesIO(file.read())
|
||||
self.data_offset = -file.tell()
|
||||
file.close()
|
||||
else:
|
||||
self.data_io = file
|
||||
self.data_offset = 0
|
||||
file.seek(0)
|
||||
break
|
||||
data = file.read(data_length)
|
||||
if name == b'NAME':
|
||||
@ -116,7 +117,7 @@ class PF2():
|
||||
info = self.character_index[self.missing_character_code]
|
||||
_compression, offset = info
|
||||
data = self.data_io
|
||||
data.seek(offset + self.data_offset)
|
||||
data.seek(offset)
|
||||
char = Character()
|
||||
char.width = uint16be(data.read(2))
|
||||
char.height = uint16be(data.read(2))
|
||||
@ -130,7 +131,5 @@ class PF2():
|
||||
|
||||
__getitem__ = get_char
|
||||
|
||||
def close(self):
|
||||
'Close the data IO, if it\'s a real file'
|
||||
if not self.in_memory:
|
||||
def __del__(self):
|
||||
self.data_io.close()
|
87
printer_lib/text_print.py
Normal file
87
printer_lib/text_print.py
Normal 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
|
@ -10,26 +10,15 @@
|
||||
Gegenwärtig:
|
||||
|
||||
| | |
|
||||
|-------------|-------------------|
|
||||
| Supported | GB01, GB02, GT01 |
|
||||
| Maybe | GB03 |
|
||||
| Planned | N/A |
|
||||
|----|----|
|
||||
| Supported | GB01, GB02, GT01, GB03 |
|
||||
<!-- | Maybe | N/A | -->
|
||||
<!-- | Planned | N/A | -->
|
||||
|
||||
## Funktionen
|
||||
|
||||
*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!
|
||||
- Bedienung über eine Web-UI direkt im Browser,
|
||||
- oder besorgen Sie sich die Android-Version!
|
||||
|
@ -10,26 +10,13 @@
|
||||
目前有:
|
||||
|
||||
| | |
|
||||
|-----------|-------------------|
|
||||
| 支持 | GB01, GB02, GT01 |
|
||||
| 可能支持 | GB03 |
|
||||
| 计划 | 暂无 |
|
||||
|----|----|
|
||||
| 支持 | GB01, GB02, GT01, GB03 |
|
||||
|
||||
## 特性
|
||||
|
||||
*当前仍在继续开发。以后会有更多!*
|
||||
|
||||
| 可用 | 部分 | 计划 |
|
||||
|-----------|-----------|---------------|
|
||||
| 网页界面 | CUPS/IPP* | 可视化编辑器 |
|
||||
| 打印图片 | | 帮助/文档 |
|
||||
| 命令行 | | 文本打印 |
|
||||
|
||||
<!-- 若没有 *,可注释下一句 -->
|
||||
\* 存在于开发代码中。将在短时间内发布。
|
||||
|
||||
*当然还有……*
|
||||
|
||||
- 简易!
|
||||
- 在网页界面进行操作,
|
||||
- 或者获取安卓应用!
|
||||
@ -38,6 +25,17 @@
|
||||
- 语言支持!您可参与翻译!
|
||||
- 良好的用户界面,可适应桌面/移动端/明暗主题!
|
||||
|
||||
- 功能丰富!
|
||||
- 网页界面,所有人都可以用!
|
||||
- 控制打印机配置
|
||||
- 打印照片,或单纯地进行测试
|
||||
- 命令行,技术爱好者必备!
|
||||
- 使用一些参数控制打印机
|
||||
- 简易、简化的文字打印
|
||||
- 让程序的每一部分发挥作用
|
||||
- 其他一些好东西!
|
||||
- 服务器也具有 CUPS/IPP 能力
|
||||
|
||||
- 跨平台!
|
||||
- 较新的 Windows 10 及以上
|
||||
- GNU/Linux
|
||||
@ -76,11 +74,17 @@ python3 server.py
|
||||
sudo pacman -S bluez bluez-utils
|
||||
```
|
||||
|
||||
*以后将有软件包!*
|
||||
|
||||
### 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
|
||||
python3 server.py
|
||||
```
|
||||
@ -94,15 +98,13 @@ python3 server.py
|
||||
当已安装 [Python 3](https://www.python.org/) 时,您可以直接获取“纯净(pure)”版,
|
||||
或在已使用 `pip` 安装 `bleak` 时使用“单一(bare)”版。
|
||||
|
||||
命令行高手?直接用 `printer.py`!
|
||||
|
||||
查看所有[发布版本](https://github.com/NaitLee/Cat-Printer/releases)!
|
||||
|
||||
## 有问题?
|
||||
|
||||
有想法?开个 issue!
|
||||
有想法?去 Discussion 讨论!
|
||||
|
||||
如果能行,PR 也可以!
|
||||
如果能行,Pull Request 也可以!
|
||||
|
||||
## 许可证
|
||||
|
||||
@ -126,7 +128,7 @@ Copyright © 2022 NaitLee Soft. 保留一些权利。
|
||||
|
||||
- 当然不能没有 Python 和 Web 技术!
|
||||
- [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/),虽然有些麻烦的地方
|
||||
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
|
||||
- Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity`
|
||||
|
210
server.py
210
server.py
@ -1,16 +1,25 @@
|
||||
'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 io
|
||||
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
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
|
||||
# For now we can't use `ThreadingHTTPServer`
|
||||
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):
|
||||
""" Let you use a dict like an object in JavaScript.
|
||||
@ -20,23 +29,8 @@ class DictAsObject(dict):
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
class PrinterServerError(Exception):
|
||||
class PrinterServerError(PrinterError):
|
||||
'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 = {
|
||||
'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'
|
||||
return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream'])
|
||||
|
||||
class PrinterServer(BaseHTTPRequestHandler):
|
||||
'(Local) server for Cat Printer Web interface'
|
||||
class PrinterServerHandler(BaseHTTPRequestHandler):
|
||||
'(Local) server handler for Cat Printer Web interface'
|
||||
|
||||
buffer = 4 * 1024 * 1024
|
||||
|
||||
max_payload = buffer * 16
|
||||
|
||||
settings = DictAsObject({
|
||||
'config_path': 'config.json',
|
||||
'version': 1,
|
||||
'is_android': False,
|
||||
'printer': None,
|
||||
'scan_time': 3,
|
||||
'frequency': 0.8,
|
||||
'scan_timeout': 5.0,
|
||||
'dry_run': False
|
||||
})
|
||||
printer = PrinterDriver()
|
||||
ipp = None
|
||||
_settings_blacklist = (
|
||||
'printer', 'is_android'
|
||||
)
|
||||
|
||||
printer: PrinterDriver = PrinterDriver()
|
||||
|
||||
ipp: IPP = None
|
||||
|
||||
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
|
||||
@ -91,24 +95,28 @@ class PrinterServer(BaseHTTPRequestHandler):
|
||||
if not self.wfile.write(chunk):
|
||||
break
|
||||
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_header('Content-Type', mime('json'))
|
||||
self.end_headers()
|
||||
if body_json is None:
|
||||
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'
|
||||
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:
|
||||
if IsAndroid:
|
||||
self.settings['is_android'] = True
|
||||
from android.storage import app_storage_path # pylint: disable=import-error
|
||||
settings_path = app_storage_path()
|
||||
@ -118,19 +126,38 @@ class PrinterServer(BaseHTTPRequestHandler):
|
||||
)
|
||||
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))
|
||||
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:
|
||||
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)
|
||||
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):
|
||||
'Update `PrinterDriver` state/config'
|
||||
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:
|
||||
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):
|
||||
'Handle API request from POST'
|
||||
content_length = int(self.headers.get('Content-Length'))
|
||||
@ -138,121 +165,110 @@ class PrinterServer(BaseHTTPRequestHandler):
|
||||
api = self.path[1:]
|
||||
if api == 'print':
|
||||
self.update_printer()
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
devices = loop.run_until_complete(
|
||||
self.printer.print_data(body)
|
||||
)
|
||||
self.printer.print(io.BytesIO(body))
|
||||
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(
|
||||
self.printer.search_all_printers(float(self.settings.scan_time))
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
self.printer.connect(None)
|
||||
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({
|
||||
} for device in self.printer.scan()]
|
||||
self.api_success({
|
||||
'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'))
|
||||
self.api_success(self.settings)
|
||||
return
|
||||
if api == 'set':
|
||||
for key in data:
|
||||
self.settings[key] = data[key]
|
||||
self.save_config()
|
||||
self.update_printer()
|
||||
self.api_success()
|
||||
return
|
||||
if api == 'exit':
|
||||
self.api_success()
|
||||
self.exit()
|
||||
|
||||
def exit(self):
|
||||
'Stop correctly & cleanly'
|
||||
self.save_config()
|
||||
# Only usable when using ThreadingHTTPServer
|
||||
# server.shutdown()
|
||||
self.printer.unload()
|
||||
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
|
||||
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
|
||||
if self.headers.get('Content-Type') == 'application/ipp':
|
||||
if self.ipp is None:
|
||||
try:
|
||||
from additional.ipp import 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()
|
||||
self.ipp = IPP(self)
|
||||
self.ipp.handle_ipp()
|
||||
return
|
||||
try:
|
||||
self.handle_api()
|
||||
return
|
||||
except BleakDBusError as e:
|
||||
# TODO: better error reporting
|
||||
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:
|
||||
except PrinterError as e:
|
||||
self.api_fail({
|
||||
'code': e.code,
|
||||
'name': e.name,
|
||||
'details': e.details
|
||||
'name': e.message,
|
||||
'details': e.message_localized
|
||||
})
|
||||
except Exception as e:
|
||||
self.api_fail({
|
||||
'code': -1,
|
||||
'name': 'Exception',
|
||||
'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():
|
||||
'Start server'
|
||||
address, port = '127.0.0.1', 8095
|
||||
listen_all = False
|
||||
if '-a' in sys.argv:
|
||||
print('Will listen on ALL addresses')
|
||||
info(I18n['will-listen-on-all-addresses'])
|
||||
listen_all = True
|
||||
# Again, Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
||||
server = HTTPServer(('' if listen_all else address, port), PrinterServer)
|
||||
server = PrinterServer(('' if listen_all else address, port), PrinterServer)
|
||||
service_url = f'http://{address}:{port}/'
|
||||
if '-s' in sys.argv:
|
||||
print(service_url)
|
||||
info(I18n['serving-at-0', service_url])
|
||||
else:
|
||||
operating_system = platform.uname().system
|
||||
if operating_system == 'Windows':
|
||||
@ -262,11 +278,11 @@ def serve():
|
||||
# TODO: I don't know about macOS
|
||||
# elif operating_system == 'macOS':
|
||||
else:
|
||||
print(f'Will serve application at: {service_url}')
|
||||
info(I18n['serving-at-0', service_url])
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
server.server_close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
serve()
|
||||
|
49
www/i18n-ext.js
Normal file
49
www/i18n-ext.js
Normal 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
32
www/i18n.d.ts
vendored
Normal 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
109
www/i18n.js
Normal 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;
|
||||
})();
|
@ -87,15 +87,15 @@
|
||||
<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>
|
||||
<input type="checkbox" name="flip-h" id="flip-h" />
|
||||
<label for="flip-h" data-i18n="flip-horizontally">Flip Horizontally</label>
|
||||
<input type="checkbox" name="flip-v" id="flip-v" />
|
||||
<label for="flip-v" data-i18n="flip-vertically">Flip Vertically</label>
|
||||
<br />
|
||||
<input type="checkbox" name="dry-run" id="dry-run" />
|
||||
<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 class="sub panel">
|
||||
<a href="#" data-i18n="system">System</a>
|
||||
|
@ -42,10 +42,16 @@
|
||||
</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><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
|
||||
<td><a href="i18n.js">i18n.js</a></td>
|
||||
<td>For internationalization (language support)</td>
|
||||
</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>
|
||||
<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>
|
||||
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"values": {
|
||||
"cat-printer": "Cat Printer",
|
||||
"printer": "Drucker",
|
||||
"device-": "Gerät:",
|
||||
@ -38,10 +37,10 @@
|
||||
"scan-time-": "Scanzeit:",
|
||||
"-seconds": "Sekunden",
|
||||
"no-available-devices-found": "Keine verfügbaren Geräte gefunden",
|
||||
"found-1-available-devices": [
|
||||
[1, 1, "1 verfügbares Gerät gefunden"],
|
||||
[2, null, "%n verfügbare Geräte gefunden"]
|
||||
],
|
||||
"found-0-available-devices": {
|
||||
"single": "{0} verfügbares Gerät gefunden",
|
||||
"multiple": "{0} verfügbare Geräte gefunden"
|
||||
},
|
||||
"please-check-if-the-printer-is-down": "Bitte prüfe, ob der Drucker ausgeschaltet ist",
|
||||
"printing": "Drucken…",
|
||||
"finished": "Fertiggestellt",
|
||||
@ -51,15 +50,12 @@
|
||||
"you-can-close-this-page-manually": "Sie können diese Seite manuell schließen",
|
||||
"please-enable-bluetooth": "Bitte aktivieren Sie Bluetooth",
|
||||
"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",
|
||||
"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:",
|
||||
"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",
|
||||
"specify-printer-mac-address": "Geben Sie die MAC-Adresse des Druckers an",
|
||||
"communication-frequency-0.8-or-1-recommended": "Übermittlungsfrequenz. 0,8 oder 1 empfohlen.",
|
||||
"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."
|
||||
}
|
||||
"dump-the-traffic": "Den Datenverkehr auf dem Drucker ausgeben und PBM-Bild beim Textdruck",
|
||||
"text-printing-mode": "Textdruckmodus"
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"values": {
|
||||
"cat-printer": "Cat Printer",
|
||||
"printer": "Printer",
|
||||
"device-": "Device:",
|
||||
@ -38,10 +37,10 @@
|
||||
"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"]
|
||||
],
|
||||
"found-0-available-devices": {
|
||||
"single": "Found {0} available device",
|
||||
"multiple": "Found {0} available devices"
|
||||
},
|
||||
"please-check-if-the-printer-is-down": "Please check if the printer is down",
|
||||
"printing": "Printing…",
|
||||
"finished": "Finished",
|
||||
@ -51,17 +50,38 @@
|
||||
"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.",
|
||||
"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": "Print PBM image to Cat Printer.",
|
||||
"print-to-cat-printer": "Print to Cat Printer",
|
||||
"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",
|
||||
"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.",
|
||||
"text-printing-mode-input-text-from-stdin": "Text printing mode, input from stdin."
|
||||
}
|
||||
"dump-the-traffic": "Dump the traffic",
|
||||
"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"
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"values": {
|
||||
"cat-printer": "猫咪打印机",
|
||||
"printer": "打印机",
|
||||
"device-": "设备:",
|
||||
@ -38,9 +37,7 @@
|
||||
"scan-time-": "扫描时间:",
|
||||
"-seconds": "秒",
|
||||
"no-available-devices-found": "未发现可用设备",
|
||||
"found-1-available-devices": [
|
||||
[1, null, "发现 %n 个可用设备"]
|
||||
],
|
||||
"found-0-available-devices": "发现 {0} 个可用设备",
|
||||
"please-check-if-the-printer-is-down": "请检查打印机是否已关闭",
|
||||
"printing": "打印中……",
|
||||
"finished": "完成",
|
||||
@ -50,17 +47,37 @@
|
||||
"you-can-close-this-page-manually": "您可手动关闭此页面",
|
||||
"please-enable-bluetooth": "请启用蓝牙",
|
||||
"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": "或者尝试延长扫描时间",
|
||||
"print-pbm-image-to-cat-printer": "打印 PBM 图片到猫咪打印机。",
|
||||
"print-to-cat-printer": "打印到猫咪打印机。",
|
||||
"supported-models-": "支持的型号:",
|
||||
"path-to-pbm-file-dash-for-stdin": "PBM 文件的位置。“-” 作为标准输入。",
|
||||
"scan-for-specified-seconds": "扫描指定的时长。",
|
||||
"specify-printer-mac-address": "指定打印机的 MAC 地址",
|
||||
"communication-frequency-0.8-or-1-recommended": "通讯频率。推荐 0.8 或 1。",
|
||||
"path-to-input-file-dash-for-stdin": "输入文件的位置。使用 '-' 作为标准输入",
|
||||
"scan-for-specified-seconds": "扫描指定的时长",
|
||||
|
||||
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "转储到打印机的数据,和文字打印模式的 PBM 图像。",
|
||||
"text-printing-mode-input-text-from-stdin": "文字打印模式,将从标准输入读取文字。"
|
||||
}
|
||||
"dump-the-traffic": "转储数据",
|
||||
"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": "自动折行"
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
|
||||
var fallbacks = [
|
||||
// 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
|
||||
'main.comp.js'
|
||||
];
|
||||
|
29
www/main.js
29
www/main.js
@ -38,13 +38,13 @@ class _Notice {
|
||||
constructor() {
|
||||
this.element = document.getElementById('notice');
|
||||
}
|
||||
_message(message, ...args) {
|
||||
this.element.innerText = i18n(message, ...args) || message;
|
||||
_message(message, things) {
|
||||
this.element.innerText = i18n(message, things) || message;
|
||||
}
|
||||
makeLogger(class_name) {
|
||||
return (message, ...args) => {
|
||||
return (message, things) => {
|
||||
this.element.classList.value = class_name;
|
||||
this._message(message, ...args);
|
||||
this._message(message, things);
|
||||
}
|
||||
}
|
||||
notice = this.makeLogger('notice');
|
||||
@ -373,7 +373,7 @@ class Main {
|
||||
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('#scan-time', 'change', 'scan_timeout');
|
||||
this.attachSetter('#device-options', 'input', 'printer');
|
||||
this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm');
|
||||
this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white');
|
||||
@ -387,7 +387,9 @@ class Main {
|
||||
this.attachSetter('#threshold', 'change', 'threshold',
|
||||
(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();
|
||||
this.searchDevices();
|
||||
resolve();
|
||||
@ -488,7 +490,7 @@ class Main {
|
||||
hint('#device-refresh');
|
||||
return;
|
||||
}
|
||||
Notice.notice('found-1-available-devices', devices.length);
|
||||
Notice.notice('found-0-available-devices', [devices.length]);
|
||||
hint('#insert-picture');
|
||||
devices.forEach(device => {
|
||||
let option = document.createElement('option');
|
||||
@ -519,18 +521,15 @@ class Main {
|
||||
}
|
||||
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`)
|
||||
i18n.useLanguage(navigator.languages[0]);
|
||||
for (let language of navigator.languages) {
|
||||
let data = await fetch(`/lang/${language}.json`)
|
||||
.then(response => response.ok ? response.json() : null);
|
||||
if (data !== null) {
|
||||
i18n.translator.add(data);
|
||||
loaded_languages.unshift(language_list[i]);
|
||||
i18n.add(language, data);
|
||||
console.log('Loaded language:', language);
|
||||
}
|
||||
}
|
||||
console.log('Language stack:', loaded_languages);
|
||||
let elements = document.querySelectorAll('*[data-i18n]');
|
||||
let i18n_data, translated_string;
|
||||
elements.forEach(element => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user