diff --git a/.gitignore b/.gitignore index 1e378f3..33676fa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ www/main.comp.js www/vconsole.js # https://github.com/delight-im/Android-AdvancedWebView build-android/advancedwebview +# cd wasm && npm install +wasm/node_modules # python bytecode *.pyc # releases diff --git a/.vscode/settings.json b/.vscode/settings.json index 572a386..06b3da4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "js/ts.implicitProjectConfig.strictNullChecks": false, - "js/ts.implicitProjectConfig.checkJs": false + "js/ts.implicitProjectConfig.checkJs": false, + "js/ts.implicitProjectConfig.experimentalDecorators": true } \ No newline at end of file diff --git a/0-prepare.sh b/0-prepare.sh new file mode 100755 index 0000000..a310cf6 --- /dev/null +++ b/0-prepare.sh @@ -0,0 +1,8 @@ +#!/bin/sh +cd www/lang/ +echo "opencc zh-CN to zh-TW..." +./0-opencc.sh +cd .. +echo "tsc bundle scripts..." +./0-transpile.sh +cd .. diff --git a/README.md b/README.md index aee0587..bbf30d6 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,14 @@ English | [Deutsch](./readme.i18n/README.de_DE.md) | [简体中文](./readme.i18 🐱🖨 A project that provides support to some Bluetooth "Cat Printer" models, on *many* platforms! -[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b) +[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7) ## Models -Currently: +Known to support: `GB0X, GT01, YT01` (`X` represents any digit) -| | | -|----|----| -| Known to support | `GB0X, GT01, YT01` | - -\* `X` represents any digit +You can test other models with the Web UI, in `Settings -> Test Unknown Device` +It may work! ## Features @@ -63,11 +60,15 @@ Get the newest apk release and install, then well done! It may ask for background location permission, you can deny it safely. (Foreground) Location permission is required for scanning Bluetooth devices in newer Android system. +Recommend to set scan time to 1 second. + ### Windows Get the newest release archive with "windows" in the file name, extract to somewhere and run `start.bat` +Windows typically needs longer scan time. Defaults to 4 seconds, try to find your case. + ### GNU/Linux You can get the "pure" release, extract it, fire up a terminal and run: @@ -75,6 +76,7 @@ You can get the "pure" release, extract it, fire up a terminal and run: python3 server.py ``` +It is recommended to set the scan time to 2 seconds. On Arch Linux based distros you may first install `bluez`, as it may not be installled by default ```bash sudo pacman -S bluez bluez-utils @@ -130,12 +132,15 @@ All other parts, except which have special statements, are in Public Domain (`CC -------- -## Development +## Contribution -You may interested in language support, anyway. See the translation files in directory `www/lang` and `readme.i18n`! -Note: you can correct some mistakes in them, if there are any. Also feel free to make it (truly) better! +You may interested in language support, anyway. There are translation files in directory `www/lang` and `readme.i18n/`! -Also interested in code development? See [development.md](development.md)! +You can correct mistakes here/there, if there are any. Also feel free to make it (truly) better! + +Also interested in code development? See [CONTRIBUTING.md](CONTRIBUTING.md) and [development.md](development.md)! + +After that, give yourself a credit in `www/about.html`, if you prefer. ### Credits diff --git a/TODO b/TODO index 17e99bf..696f192 100644 --- a/TODO +++ b/TODO @@ -8,16 +8,16 @@ Note: not ordered. do whatever I/you want + Even better CUPS/IPP support + Even better frontend, language-friendly text printing + Tcl/Tk frontend. More in dev-diary.txt, July 7th. -+ Re-style frontend to make it usable in e.g. Otter Browser + 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 -+ More funny i18n ++ More funny "languages" (now there's lolcat; I think the next is 文言) + Arch Linux package / AUR, package for other distros + Service for other init systems (a systemd unit file is there) + ... ? Optimize PF2 text printing? It seems a bit slow (in algorithm). +? Use Go as part of backend? It can boost things up, and build some (essential) image manipulation in, quicker. And strip some way-too-big Python libs away (for smaller Windows/Android dist). ? Built-in PostScript (Even if very basic) ? Data compression for GB03. Optional ? Plugin, for including community features (that involves usefulness but also bloatness) diff --git a/dev-diary.txt b/dev-diary.txt index 1ef9b03..ad97ae1 100644 --- a/dev-diary.txt +++ b/dev-diary.txt @@ -184,3 +184,15 @@ Tried to "fix" it, used at least 4 hours, finally found it's a matter of didn't So the Internet JavaScript memes are damned true. https://programmerhumor.io/javascript-memes/why-is-it-like-this-2/ https://programmerhumor.io/javascript-memes/sorry-dad-_-2/ + +14th + +... How silly is the above approach. This time I simply changed to Uint32Array. That became much more trivial. + +So, I've re-written the image processing "lib". I wanted to go for WebAssembly (with AssemblyScript), so made it separate (in dir `wasm`). +After finish, it really worked -- but it's ~100% slower than the equivalent JavaScript (`asc` versus `tsc`) +And that may involve unacceptable change to scripting structure (ESModule etc.), thus Wasm was given up. +But hey, in this rewrite some algorithm practial overhead was removed, thus much more efficient! Enjoy the blazing speed! + +In main.js the event handler was reworked too. No more double event dispatches. +Thanks to this, another image processing performance problem is fixed. diff --git a/printer.py b/printer.py index e7bfc60..e73cdea 100644 --- a/printer.py +++ b/printer.py @@ -270,7 +270,7 @@ class PrinterDriver(Commander): model: Model = None 'The printer model' - scan_timeout: float = 4.0 + scan_time: float = 4.0 connection_timeout : float = 5.0 @@ -351,15 +351,19 @@ class PrinterDriver(Commander): self.device.start_notify(self.rx_characteristic, notify) ) - def scan(self, identifier: str=None, *, use_result=False): + def scan(self, identifier: str=None, *, use_result=False, everything=False): ''' Scan for supported devices, optionally filter with `identifier`, which can be device model (bluetooth name), and optionally MAC address, after a comma. If `use_result` is True, connect to the first available device to driver instantly. + If `everything` is True, return all bluetooth devices found. Note: MAC address doesn't work on Apple MacOS. In place with it, You need an UUID of BLE device dynamically given by MacOS. ''' if self.fake: - return + return [] + if everything: + devices = self.loop(BleakScanner.discover(self.scan_time)) + return devices if identifier: if identifier.find(',') != -1: name, address = identifier.split(',') @@ -370,12 +374,12 @@ class PrinterDriver(Commander): if use_result: self.connect(name, address) return [BLEDevice(address, name)] - elif (identifier not in Models and + if (identifier not in Models and identifier[2::3] != ':::::' and len(identifier.replace('-', '')) != 32): error('model-0-is-not-supported-yet', identifier, exception=PrinterError) # scanner = BleakScanner() devices = [x for x in self.loop( - BleakScanner.discover(self.scan_timeout) + BleakScanner.discover(self.scan_time) ) if x.name in Models] if identifier: if identifier in Models: @@ -397,7 +401,7 @@ class PrinterDriver(Commander): self.scan(identifier, use_result=True) if self.device is None and not self.fake: error('no-available-devices-found', exception=PrinterError) - if mode == 'pbm' or mode == 'default': + if mode in ('pbm', 'default'): printer_data = PrinterData(self.model.paper_width, file) self._print_bitmap(printer_data) elif mode == 'text': @@ -443,7 +447,7 @@ class PrinterDriver(Commander): if self.quality: # well, slower makes stable heating self.set_speed(self.quality) if self.energy is not None: - self.set_energy(self.energy * 0xff) + self.set_energy(self.energy * 0x100) self.apply_energy() self.update_device() self.flush() @@ -648,7 +652,7 @@ def _main(): printer = PrinterDriver() scan_param = args.scan.split(',') - printer.scan_timeout = float(scan_param[0]) + printer.scan_time = float(scan_param[0]) identifier = ','.join(scan_param[1:]) if args.energy is not None: printer.energy = int(args.energy * 0xff) diff --git a/printer_lib/commander.py b/printer_lib/commander.py index af9b6db..27f26e4 100644 --- a/printer_lib/commander.py +++ b/printer_lib/commander.py @@ -1,7 +1,7 @@ ''' Cat-Printer Commander, way to communicate with cat printers via bluetooth -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 ''' @@ -137,7 +137,7 @@ class Commander(metaclass=ABCMeta): def set_energy(self, amount: int): ''' Set thermal energy, max to `0xffff` - By default, it's seems around `0x3000`, aka 1 / 5. + By default, it's seems around `0x3000` (1 / 5) ''' self.send( self.make_command(0xaf, int_to_bytes(amount)) ) diff --git a/printer_lib/i18n.py b/printer_lib/i18n.py index 3fb07bc..41cd580 100644 --- a/printer_lib/i18n.py +++ b/printer_lib/i18n.py @@ -1,7 +1,7 @@ ''' Minimal internationalization -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 ''' diff --git a/printer_lib/ipp.py b/printer_lib/ipp.py index 59d2fea..fc06745 100644 --- a/printer_lib/ipp.py +++ b/printer_lib/ipp.py @@ -1,7 +1,7 @@ ''' Provide *very* basic CUPS/IPP support -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 ''' diff --git a/printer_lib/pf2.py b/printer_lib/pf2.py index 46af476..84cb3af 100644 --- a/printer_lib/pf2.py +++ b/printer_lib/pf2.py @@ -1,7 +1,7 @@ ''' Python lib for reading PF2 font files: http://grub.gibibit.com/New_font_format -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 Don't forget to see how it's used in `text_print.py` diff --git a/readme.i18n/README.de_DE.md b/readme.i18n/README.de_DE.md index 5983f0a..9481515 100644 --- a/readme.i18n/README.de_DE.md +++ b/readme.i18n/README.de_DE.md @@ -3,7 +3,7 @@ 🐱🖨 Ein Projekt, das Unterstützung für einige Bluetooth-"Cat Printer"-Modelle auf *vielen* Plattformen bietet! -[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b) +[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7) ## Unterstützte Geräte diff --git a/readme.i18n/README.zh_CN.md b/readme.i18n/README.zh_CN.md index 9b7fecb..aefdbd9 100644 --- a/readme.i18n/README.zh_CN.md +++ b/readme.i18n/README.zh_CN.md @@ -3,17 +3,14 @@ 🐱🖨 猫咪打印机:此应用*跨平台地*对一些蓝牙“喵喵机”提供支持! -[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b) +[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7) ## 型号 -目前有: +已知支持:`GB0X, GT01, YT01` (`X` 表示任意数字) -| | | -|----|----| -| 已知支持 | `GB0X, GT01, YT01` | - -\* `X` 表示任意数字 +可在 Web 界面测试未列出的型号。在 `设置 -> 测试未知设备` +有概率成功! ## 特性 @@ -60,11 +57,15 @@ 应用可能请求“后台位置”权限,您可以拒绝它。 (前台)位置权限是较新版安卓系统扫描蓝牙设备所需要的。 +建议将扫描时间设为 1 秒。 + ### Windows 获取名称中有 "windows" 的版本, 解压并运行 `start.bat` +Windows 通常需要较长的扫描时间。默认为 4 秒,可按需调整。 + ### GNU/Linux 您可以获取“纯净(pure)”版,解压、在其中打开终端并输入: @@ -72,6 +73,8 @@ python3 server.py ``` +建议将扫描时间设为 2 秒。 + 在 Arch Linux 等发行版您可能需要首先安装 `bluez` ```bash sudo pacman -S bluez bluez-utils @@ -130,14 +133,18 @@ Copyright © 2021-2022 NaitLee Soft. 保留一些权利。 ## 开发 -您可能对翻译工作感兴趣。可于目录 `www/lang` 和 `readme.i18n` 中查看翻译文件! +您可能对翻译工作感兴趣。可于目录 `www/lang` 和 `readme.i18n/` 中查看翻译文件! 注: -1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。 -2. 如果(真的)有能力,您也可以纠正/改善某些翻译! +1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。 +2. 目前使用 [OpenCC](https://github.com/BYVoid/OpenCC) 转换简体到繁体(臺灣正體)。若有不当之处,请指出。 + 当前仅转换程序界面语言、暂不转换文档。 +3. 如果(真的)有能力,您也可以纠正/改善某些翻译! 还想写代码?看看 [development.md](development.md)!(英文) +那之后,可以将您的贡献概括添加至 `www/about.html` + ### 鸣谢 - 当然不能没有 Python 和 Web 技术! diff --git a/server.py b/server.py index 7d23f33..a5c6cee 100644 --- a/server.py +++ b/server.py @@ -49,6 +49,7 @@ mime_type = { 'json': 'application/json;charset=utf-8', 'png': 'image/png', 'svg': 'image/svg+xml;charset=utf-8', + 'wasm': 'application/wasm', 'octet-stream': 'application/octet-stream' } def mime(url: str): @@ -72,10 +73,10 @@ class PrinterServerHandler(BaseHTTPRequestHandler): settings = DictAsObject({ 'config_path': 'config.json', - 'version': 2, + 'version': 3, 'first_run': True, 'is_android': False, - 'scan_timeout': 4.0, + 'scan_time': 4.0, 'dry_run': False, 'energy': 0.2 }) @@ -193,11 +194,13 @@ class PrinterServerHandler(BaseHTTPRequestHandler): def update_printer(self): 'Update `PrinterDriver` state/config' self.printer.dry_run = self.settings.dry_run - self.printer.scan_timeout = self.settings.scan_timeout + self.printer.scan_time = self.settings.scan_time self.printer.fake = self.settings.fake self.printer.dump = self.settings.dump - self.printer.energy = self.settings.energy - self.printer.quality = self.settings.quality + if self.settings.energy is not None: + self.printer.energy = int(self.settings.energy) + if self.settings.quality is not None: + self.printer.quality = int(self.settings.quality) self.printer.flip_h = self.settings.flip_h or self.settings.flip self.printer.flip_v = self.settings.flip_v or self.settings.flip self.printer.rtl = self.settings.force_rtl @@ -218,7 +221,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler): devices_list = [{ 'name': device.name, 'address': device.address - } for device in self.printer.scan()] + } for device in self.printer.scan(everything=data.get('everything'))] self.api_success({ 'devices': devices_list }) @@ -313,6 +316,7 @@ class PrinterServer(HTTPServer): def finish_request(self, request, client_address): if self.handler is None: self.handler = self.handler_class(request, client_address, self) + self.handler.load_config() with open(os.path.join('www', 'all_js.txt'), 'r', encoding='utf-8') as file: for path in file.read().split('\n'): if path != '': diff --git a/version b/version index 82eaf6f..96ae783 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.6.0.1 \ No newline at end of file +0.6.0.2 \ No newline at end of file diff --git a/wasm/0-compile.sh b/wasm/0-compile.sh new file mode 100755 index 0000000..a825e53 --- /dev/null +++ b/wasm/0-compile.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [[ $1 == 1 ]]; then + npm run asbuild:release; +else + npm run asbuild:debug; +fi diff --git a/wasm/asconfig.json b/wasm/asconfig.json new file mode 100644 index 0000000..1c2971d --- /dev/null +++ b/wasm/asconfig.json @@ -0,0 +1,24 @@ +{ + "targets": { + "debug": { + "outFile": "../www/image-wasm.wasm", + "textFile": "../www/image-wasm.wat", + "sourceMap": true, + "debug": true + }, + "release": { + "outFile": "../www/image-wasm.wasm", + "textFile": "../www/image-wasm.wat", + "sourceMap": true, + "optimizeLevel": 3, + "shrinkLevel": 0, + "converge": false, + "noAssert": false + } + }, + "options": { + "exportRuntime": true, + "baseDir": ".", + "bindings": "esm" + } +} \ No newline at end of file diff --git a/wasm/image.ts b/wasm/image.ts new file mode 100644 index 0000000..fe3a7bd --- /dev/null +++ b/wasm/image.ts @@ -0,0 +1,115 @@ + +export function monoGrayscale(rgba: Uint32Array, brightness: i32, alpha_as_white: bool): Uint8ClampedArray { + let mono = new Uint8ClampedArray(rgba.length); + let r: f32 = 0.0, g: f32 = 0.0, b: f32 = 0.0, a: f32 = 0.0, m: f32 = 0.0, n: i32 = 0; + for (let i: i32 = 0; i < mono.length; ++i) { + n = rgba[i]; + // little endian + r = (n & 0xff), g = (n >> 8 & 0xff), b = (n >> 16 & 0xff); + a = (n >> 24 & 0xff) / 0xff; + if (a < 1 && alpha_as_white) { + a = 1 - a; + r += (0xff - r) * a; + g += (0xff - g) * a; + b += (0xff - b) * a; + } else { r *= a; g *= a; b *= a; } + m = r * 0.2125 + g * 0.7154 + b * 0.0721; + m += (brightness - 0x80) * (1 - m / 0xff) * (m / 0xff) * 2; + mono[i] = m; + } + return mono; +} + +/** Note: returns a `Uint32Array` */ +export function monoToRgba(mono: Uint8ClampedArray): Uint32Array { + let rgba = new Uint32Array(mono.length); + for (let i: i32 = 0; i < mono.length; ++i) { + // little endian + rgba[i] = 0xff000000 | (mono[i] << 16) | (mono[i] << 8) | mono[i]; + } + return rgba; +} + +export function monoDirect(mono: Uint8ClampedArray, w: i32, h:i32): Uint8ClampedArray { + for (let i: i32 = 0; i < mono.length; ++i) { + mono[i] = mono[i] > 0x80 ? 0xff : 0x00; + } + return mono; +} + +export function monoSteinberg(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray { + let p: i32 = 0, m: i32, n: i32, o: i32; + for (let j: i32 = 0; j < h; ++j) { + for (let i: i32 = 0; i < w; ++i) { + m = mono[p]; + n = mono[p] > 0x80 ? 0xff : 0x00; + o = m - n; + mono[p] = n; + if (i >= 0 && i < w - 1 && j >= 0 && j < h) + mono[p + 1] += (o * 7 / 16); + if (i >= 1 && i < w && j >= 0 && j < h - 1) + mono[p + w - 1] += (o * 3 / 16); + if (i >= 0 && i < w && j >= 0 && j < h - 1) + mono[p + w] += (o * 5 / 16); + if (i >= 0 && i < w - 1 && j >= 0 && j < h - 1) + mono[p + w + 1] += (o * 1 / 16); + ++p; + } + } + return mono; +} + +export function monoHalftone(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray { + const spot: i32 = 4; + const spot_h: i32 = spot / 2 + 1; + const spot_d: i32 = spot * 2; + const spot_s: i32 = spot * spot; + let i: i32, j: i32, x: i32, y: i32, o: f64 = 0.0; + for (j = 0; j < h - spot; j += spot) { + for (i = 0; i < w - spot; i += spot) { + for (x = 0; x < spot; ++x) + for (y = 0; y < spot; ++y) + o += mono[(j + y) * w + i + x]; + o = (1 - o / spot_s / 0xff) * spot; + for (x = 0; x < spot; ++x) + for (y = 0; y < spot; ++y) { + mono[(j + y) * w + i + x] = Math.abs(x - spot_h) >= o || Math.abs(y - spot_h) >= o ? 0xff : 0x00; + // mono[(j + y) * w + i + x] = Math.abs(x - spot_h) + Math.abs(y - spot_h) >= o ? 0xff : 0x00; + } + } + for (; i < w; ++i) mono[j * w + i] = 0xff; + } + for (; j < h; ++j) + for (i = 0; i < w; ++i) mono[j * w + i] = 0xff; + return mono; +} + +export function monoToPbm(data: Uint8ClampedArray): Uint8ClampedArray { + let length: i32 = (data.length / 8) | 0; + let result = new Uint8ClampedArray(length); + for (let i: i32 = 0, p: i32 = 0; i < data.length; ++p) { + result[p] = 0; + for (let d: u8 = 0; d < 8; ++i, ++d) + result[p] |= data[i] & (0b10000000 >> d); + result[p] ^= 0b11111111; + } + return result; +} + +/** Note: takes & gives `Uint32Array` */ +export function rotateRgba(before: Uint32Array, w: i32, h: i32): Uint32Array { + /** + * w h + * o------+ +---o + * h | | | | w + * +------+ | | after + * before +---+ + */ + let after = new Uint32Array(before.length); + for (let j: i32 = 0; j < h; j++) { + for (let i: i32 = 0; i < w; i++) { + after[j * w + i] = before[(w - i - 1) * h + j]; + } + } + return after; +} diff --git a/wasm/package-lock.json b/wasm/package-lock.json new file mode 100644 index 0000000..45867df --- /dev/null +++ b/wasm/package-lock.json @@ -0,0 +1,64 @@ +{ + "name": "wasm", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "assemblyscript": "^0.20.13" + } + }, + "node_modules/assemblyscript": { + "version": "0.20.13", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.20.13.tgz", + "integrity": "sha512-F4ACXdBdXCBnPEzRCl/ovFFPGmKXQPvWW4cM6U21eRezaCoREqxA0hjSUOYTz7txY3MJclyBfjwMSEJ5e9HKsw==", + "dependencies": { + "binaryen": "108.0.0-nightly.20220528", + "long": "^5.2.0" + }, + "bin": { + "asc": "bin/asc.js", + "asinit": "bin/asinit.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/assemblyscript" + } + }, + "node_modules/binaryen": { + "version": "108.0.0-nightly.20220528", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-108.0.0-nightly.20220528.tgz", + "integrity": "sha512-9biG357fx3NXmJNotIuY9agZBcCNHP7d1mgOGaTlPYVHZE7/61lt1IyHCXAL+W5jUOYgmFZ260PR4IbD19RKuA==", + "bin": { + "wasm-opt": "bin/wasm-opt", + "wasm2js": "bin/wasm2js" + } + }, + "node_modules/long": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", + "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==" + } + }, + "dependencies": { + "assemblyscript": { + "version": "0.20.13", + "resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.20.13.tgz", + "integrity": "sha512-F4ACXdBdXCBnPEzRCl/ovFFPGmKXQPvWW4cM6U21eRezaCoREqxA0hjSUOYTz7txY3MJclyBfjwMSEJ5e9HKsw==", + "requires": { + "binaryen": "108.0.0-nightly.20220528", + "long": "^5.2.0" + } + }, + "binaryen": { + "version": "108.0.0-nightly.20220528", + "resolved": "https://registry.npmjs.org/binaryen/-/binaryen-108.0.0-nightly.20220528.tgz", + "integrity": "sha512-9biG357fx3NXmJNotIuY9agZBcCNHP7d1mgOGaTlPYVHZE7/61lt1IyHCXAL+W5jUOYgmFZ260PR4IbD19RKuA==" + }, + "long": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", + "integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w==" + } + } +} diff --git a/wasm/package.json b/wasm/package.json new file mode 100644 index 0000000..1cbc814 --- /dev/null +++ b/wasm/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "assemblyscript": "^0.20.13" + }, + "scripts": { + "asbuild:debug": "asc image.ts --target debug && tsc", + "asbuild:release": "asc image.ts --target release && tsc", + "asbuild": "npm run asbuild:release" + } +} diff --git a/wasm/tsconfig.json b/wasm/tsconfig.json new file mode 100644 index 0000000..8fbb3f1 --- /dev/null +++ b/wasm/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ], + "compilerOptions": { + "experimentalDecorators": true, + "outDir": "../www/", + "module": "None", + "declaration": true + } +} \ No newline at end of file diff --git a/0-transpile.sh b/www/0-transpile.sh similarity index 85% rename from 0-transpile.sh rename to www/0-transpile.sh index bfe388f..21e7626 100755 --- a/0-transpile.sh +++ b/www/0-transpile.sh @@ -1,4 +1,2 @@ #!/bin/sh -cd www npx tsc $@ --allowJs --outFile main.comp.js $(cat all_js.txt) -cd .. diff --git a/www/about.html b/www/about.html index 7d03b57..8b5dba3 100644 --- a/www/about.html +++ b/www/about.html @@ -43,6 +43,7 @@ sync1211
Developer
+
Translator
All testers & users
diff --git a/www/accessibility.js b/www/accessibility.js index ac6de84..3d7e49e 100644 --- a/www/accessibility.js +++ b/www/accessibility.js @@ -1,5 +1,5 @@ ` -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 `; diff --git a/www/i18n-ext.js b/www/i18n-ext.js index 8de7fe3..63193cc 100644 --- a/www/i18n-ext.js +++ b/www/i18n-ext.js @@ -1,5 +1,5 @@ ` -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 `; diff --git a/www/i18n.js b/www/i18n.js index 4666688..8828d86 100644 --- a/www/i18n.js +++ b/www/i18n.js @@ -1,5 +1,5 @@ ` -Copyright © 2021-2022 NaitLee Soft. No rights reserved. +No rights reserved. License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0 `; diff --git a/www/image.d.ts b/www/image.d.ts new file mode 100644 index 0000000..f13ad34 --- /dev/null +++ b/www/image.d.ts @@ -0,0 +1,9 @@ +export declare function monoGrayscale(rgba: Uint32Array, brightness: i32, alpha_as_white: bool): Uint8ClampedArray; +/** Note: returns a `Uint32Array` */ +export declare function monoToRgba(mono: Uint8ClampedArray): Uint32Array; +export declare function monoDirect(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray; +export declare function monoSteinberg(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray; +export declare function monoHalftone(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray; +export declare function monoToPbm(data: Uint8ClampedArray): Uint8ClampedArray; +/** Note: takes & gives `Uint32Array` */ +export declare function rotateRgba(before: Uint32Array, w: i32, h: i32): Uint32Array; diff --git a/www/image.js b/www/image.js index f9f0a24..ec40885 100644 --- a/www/image.js +++ b/www/image.js @@ -1,117 +1,125 @@ - -/** - * Convert colored image to grayscale. - * @param {Uint8ClampedArray} image_data `data` property of an ImageData instance, - * i.e. `canvas.getContext('2d').getImageData(...).data` - * @param {Uint8ClampedArray} mono_data an `Uint8ClampedArray` that have the size `w * h` - * i.e. `image_data.length / 4` - * The result data will be here, as a 8-bit grayscale image data. - * @param {number} w width of image - * @param {number} h height of image - * @param {number} t brightness, historically "threshold" - * @param {boolean} transparencyAsWhite whether render opacity as white rather than black - */ -function monoGrayscale(image_data, mono_data, w, h, t, transparencyAsWhite) { - let p, q, r, g, b, a, m; +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.rotateRgba = exports.monoToPbm = exports.monoHalftone = exports.monoSteinberg = exports.monoDirect = exports.monoToRgba = exports.monoGrayscale = void 0; +function monoGrayscale(rgba, brightness, alpha_as_white) { + let mono = new Uint8ClampedArray(rgba.length); + let r = 0.0, g = 0.0, b = 0.0, a = 0.0, m = 0.0, n = 0; + for (let i = 0; i < mono.length; ++i) { + n = rgba[i]; + // little endian + r = (n & 0xff), g = (n >> 8 & 0xff), b = (n >> 16 & 0xff); + a = (n >> 24 & 0xff) / 0xff; + if (a < 1 && alpha_as_white) { + a = 1 - a; + r += (0xff - r) * a; + g += (0xff - g) * a; + b += (0xff - b) * a; + } + else { + r *= a; + g *= a; + b *= a; + } + m = r * 0.2125 + g * 0.7154 + b * 0.0721; + m += (brightness - 0x80) * (1 - m / 0xff) * (m / 0xff) * 2; + mono[i] = m; + } + return mono; +} +exports.monoGrayscale = monoGrayscale; +/** Note: returns a `Uint32Array` */ +function monoToRgba(mono) { + let rgba = new Uint32Array(mono.length); + for (let i = 0; i < mono.length; ++i) { + // little endian + rgba[i] = 0xff000000 | (mono[i] << 16) | (mono[i] << 8) | mono[i]; + } + return rgba; +} +exports.monoToRgba = monoToRgba; +function monoDirect(mono, w, h) { + for (let i = 0; i < mono.length; ++i) { + mono[i] = mono[i] > 0x80 ? 0xff : 0x00; + } + return mono; +} +exports.monoDirect = monoDirect; +function monoSteinberg(mono, w, h) { + let p = 0, m, n, o; + for (let j = 0; j < h; ++j) { + for (let i = 0; i < w; ++i) { + m = mono[p]; + n = mono[p] > 0x80 ? 0xff : 0x00; + o = m - n; + mono[p] = n; + if (i >= 0 && i < w - 1 && j >= 0 && j < h) + mono[p + 1] += (o * 7 / 16); + if (i >= 1 && i < w && j >= 0 && j < h - 1) + mono[p + w - 1] += (o * 3 / 16); + if (i >= 0 && i < w && j >= 0 && j < h - 1) + mono[p + w] += (o * 5 / 16); + if (i >= 0 && i < w - 1 && j >= 0 && j < h - 1) + mono[p + w + 1] += (o * 1 / 16); + ++p; + } + } + return mono; +} +exports.monoSteinberg = monoSteinberg; +function monoHalftone(mono, w, h) { + const spot = 4; + const spot_h = spot / 2 + 1; + const spot_d = spot * 2; + const spot_s = spot * spot; + let i, j, x, y, o = 0.0; + for (j = 0; j < h - spot; j += spot) { + for (i = 0; i < w - spot; i += spot) { + for (x = 0; x < spot; ++x) + for (y = 0; y < spot; ++y) + o += mono[(j + y) * w + i + x]; + o = (1 - o / spot_s / 0xff) * spot; + for (x = 0; x < spot; ++x) + for (y = 0; y < spot; ++y) { + mono[(j + y) * w + i + x] = Math.abs(x - spot_h) >= o || Math.abs(y - spot_h) >= o ? 0xff : 0x00; + // mono[(j + y) * w + i + x] = Math.abs(x - spot_h) + Math.abs(y - spot_h) >= o ? 0xff : 0x00; + } + } + for (; i < w; ++i) + mono[j * w + i] = 0xff; + } + for (; j < h; ++j) + for (i = 0; i < w; ++i) + mono[j * w + i] = 0xff; + return mono; +} +exports.monoHalftone = monoHalftone; +function monoToPbm(data) { + let length = (data.length / 8) | 0; + let result = new Uint8ClampedArray(length); + for (let i = 0, p = 0; i < data.length; ++p) { + result[p] = 0; + for (let d = 0; d < 8; ++i, ++d) + result[p] |= data[i] & (0b10000000 >> d); + result[p] ^= 0b11111111; + } + return result; +} +exports.monoToPbm = monoToPbm; +/** Note: takes & gives `Uint32Array` */ +function rotateRgba(before, w, h) { + /** + * w h + * o------+ +---o + * h | | | | w + * +------+ | | after + * before +---+ + */ + let after = new Uint32Array(before.length); for (let j = 0; j < h; j++) { for (let i = 0; i < w; i++) { - p = j * w + i; - q = p * 4; - [r, g, b, a] = image_data.slice(q, q + 4); - a /= 255; - if (a < 1 && transparencyAsWhite) { - a = 1 - a; - r += (255 - r) * a; - g += (255 - g) * a; - b += (255 - b) * a; - } - else { r *= a; g *= a; b *= a; } - m = Math.floor(r * 0.2125 + g * 0.7154 + b * 0.0721); - m += (t - 128) * (1 - m / 255) * (m / 255) * 2; - mono_data[p] = m; + after[j * w + i] = before[(w - i - 1) * h + j]; } } + return after; } - -/** - * The most simple monochrome algorithm, any value bigger than threshold is white, otherwise black. - * @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place** - * @param {number} w width of image - * @param {number} h height of image - */ -function monoDirect(data, w, h) { - let p, i, j; - for (j = 0; j < h; j++) { - for (i = 0; i < w; i++) { - p = j * w + i; - data[p] = data[p] > 128 ? 255 : 0; - } - } -} - -/** - * The widely used Floyd Steinberg algorithm, the most "natual" one. - * @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place** - * @param {number} w width of image - * @param {number} h height of image - */ -function monoSteinberg(data, w, h) { - let p, m, n, o, i, j; - function adjust(x, y, delta) { - if ( - x < 0 || x >= w || - y < 0 || y >= h - ) return; - p = y * w + x; - data[p] += delta; - } - for (j = 0; j < h; j++) { - for (i = 0; i < w; i++) { - p = j * w + i; - m = data[p]; - n = m > 128 ? 255 : 0; - o = m - n; - data[p] = n; - adjust(i + 1, j , o * 7 / 16); - adjust(i - 1, j + 1, o * 3 / 16); - adjust(i , j + 1, o * 5 / 16); - adjust(i + 1, j + 1, o * 1 / 16); - } - } -} - -/** - * (Work in Progress...) - */ -function monoHalftone(data, w, h, t) {} - -/** - * Convert a monochrome image data to PBM mono image file data. - * Returns a Blob containing the file data. - * @param {Uint8ClampedArray} data the data that have a size of `w * h` - * @param {number} w width of image - * @param {number} h height of image - * @returns {Blob} - */ -function mono2pbm(data, w, h) { - let result = new Uint8ClampedArray(data.length / 8); - let slice, p, i; - for (i = 0; i < result.length; i++) { - p = i * 8; - slice = data.slice(p, p + 8); - // Merge 8 bytes to 1 byte, and negate the bits - // assuming there's only 255 (0b11111111) or 0 (0b00000000) in the data - result[i] = ( - slice[0] & 0b10000000 | - slice[1] & 0b01000000 | - slice[2] & 0b00100000 | - slice[3] & 0b00010000 | - slice[4] & 0b00001000 | - slice[5] & 0b00000100 | - slice[6] & 0b00000010 | - slice[7] & 0b00000001 - ) ^ 0b11111111; - } - let pbm_data = new Blob([`P4\n${w} ${h}\n`, result]); - return pbm_data; -} +exports.rotateRgba = rotateRgba; diff --git a/www/index.html b/www/index.html index a1c28f3..65ef0ed 100644 --- a/www/index.html +++ b/www/index.html @@ -7,7 +7,7 @@ - +
@@ -30,6 +30,10 @@ Picture +