diff --git a/README.md b/README.md index d8f72be..8ccb4fe 100644 --- a/README.md +++ b/README.md @@ -103,13 +103,13 @@ 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`. -If you like command-line, install [ImageMagick](https://imagemagick.org/) and [Ghostscript](https://ghostscript.com/) and enjoy the integraton. +If you like command-line, installing [ImageMagick](https://imagemagick.org/) and [Ghostscript](https://ghostscript.com/) could be very helpful. See the [releases](https://github.com/NaitLee/Cat-Printer/releases) now! ## Problems? -Please talk in Discussion if there's something in your mind! +Please use Issue or Discussion if there's something in your mind! Of course Pull Requests are welcome if you can handle them! diff --git a/printer.py b/printer.py index 2a95e5d..57f92ac 100644 --- a/printer.py +++ b/printer.py @@ -264,7 +264,7 @@ class PrinterDriver(Commander): model: Model = None 'The printer model' - scan_timeout: float = 5.0 + scan_timeout: float = 4.0 connection_timeout : float = 5.0 @@ -523,7 +523,7 @@ class PrinterDriver(Commander): self.device.stop_notify(self.rx_characteristic), self.device.disconnect() ) - except BleakError: + except (BleakError, EOFError): self.device = None if self._traffic_dump is not None: self._traffic_dump.close() @@ -694,14 +694,19 @@ def main(): 'Run the `_main` routine while catching exceptions' try: _main() - except BleakError as e: + except (BleakError, AttributeError) as e: error_message = str(e) if ( - 'not turned on' in error_message or - (isinstance(e, BleakDBusError) and + ('not turned on' in error_message) or # windows or android + (isinstance(e, BleakDBusError) and # linux/dbus/bluetoothctl getattr(e, 'dbus_error') == 'org.bluez.Error.NotReady') ): fatal(I18n['please-enable-bluetooth'], code=ExitCodes.GeneralError) + elif ( + (isinstance(e, AttributeError) and # macos, possibly? + 'CentralManagerDelegate' in error_message) + ): + fatal(I18n['please-enable-bluetooth-or-try-to-reboot'], code=ExitCodes.GeneralError) else: raise except PrinterError as e: diff --git a/readme.i18n/README.zh_CN.md b/readme.i18n/README.zh_CN.md index 4b8ab69..9131b9a 100644 --- a/readme.i18n/README.zh_CN.md +++ b/readme.i18n/README.zh_CN.md @@ -105,7 +105,7 @@ python3 server.py ## 有问题? -有想法?去 Discussion 讨论! +有想法?用 Issue 或去 Discussion 讨论! 如果能行,Pull Request 也可以! diff --git a/server.py b/server.py index ac6416f..5bc5863 100644 --- a/server.py +++ b/server.py @@ -58,7 +58,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler): 'version': 2, 'first_run': True, 'is_android': False, - 'scan_timeout': 5.0, + 'scan_timeout': 4.0, 'dry_run': False }) _settings_blacklist = ( diff --git a/www/image.js b/www/image.js index fa10d4e..f9f0a24 100644 --- a/www/image.js +++ b/www/image.js @@ -8,9 +8,10 @@ * 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, transparencyAsWhite) { +function monoGrayscale(image_data, mono_data, w, h, t, transparencyAsWhite) { let p, q, r, g, b, a, m; for (let j = 0; j < h; j++) { for (let i = 0; i < w; i++) { @@ -26,6 +27,7 @@ function monoGrayscale(image_data, mono_data, w, h, transparencyAsWhite) { } 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; } } @@ -36,14 +38,13 @@ function monoGrayscale(image_data, mono_data, w, h, transparencyAsWhite) { * @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place** * @param {number} w width of image * @param {number} h height of image - * @param {number} t threshold */ -function monoDirect(data, w, h, t) { - let p; - for (let j = 0; j < h; j++) { - for (let i = 0; i < w; i++) { +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] > t ? 255 : 0; + data[p] = data[p] > 128 ? 255 : 0; } } } @@ -53,10 +54,9 @@ function monoDirect(data, w, h, t) { * @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place** * @param {number} w width of image * @param {number} h height of image - * @param {number} t threshold */ -function monoSteinberg(data, w, h, t) { - let p, m, n, o; +function monoSteinberg(data, w, h) { + let p, m, n, o, i, j; function adjust(x, y, delta) { if ( x < 0 || x >= w || @@ -65,12 +65,12 @@ function monoSteinberg(data, w, h, t) { p = y * w + x; data[p] += delta; } - for (let j = 0; j < h; j++) { - for (let i = 0; i < w; i++) { + 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 + t; + o = m - n; data[p] = n; adjust(i + 1, j , o * 7 / 16); adjust(i - 1, j + 1, o * 3 / 16); @@ -95,12 +95,12 @@ function monoHalftone(data, w, h, t) {} */ function mono2pbm(data, w, h) { let result = new Uint8ClampedArray(data.length / 8); - let slice, p; - for (let i = 0; i < result.length; i++) { + 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 - // expecting in the data there's only 255 (0b11111111) or 0 (0b00000000) + // assuming there's only 255 (0b11111111) or 0 (0b00000000) in the data result[i] = ( slice[0] & 0b10000000 | slice[1] & 0b01000000 | diff --git a/www/index.html b/www/index.html index 2e10f6d..1d81dcf 100644 --- a/www/index.html +++ b/www/index.html @@ -44,8 +44,9 @@ Pattern -->
- - + + +
@@ -62,7 +63,7 @@
- + seconds
diff --git a/www/lang/en-US.json b/www/lang/en-US.json index 05c0dcf..f78279c 100644 --- a/www/lang/en-US.json +++ b/www/lang/en-US.json @@ -116,5 +116,8 @@ "text-printing-mode-with-options": "Text printing mode with options", "image-printing-options": "Image printing options", "convert-input-image-with-imagemagick": "Convert input image with ImageMagick", - "reset-configuration-": "Reset configuration?" + "reset-configuration-": "Reset configuration?", + "brightness-": "Brightness:", + "text-printing-mode": "Text Printing Mode", + "please-enable-bluetooth-or-try-to-reboot": "Please enable bluetooth or try to reboot" } \ No newline at end of file diff --git a/www/lang/zh-CN.json b/www/lang/zh-CN.json index 4991866..9e26846 100644 --- a/www/lang/zh-CN.json +++ b/www/lang/zh-CN.json @@ -111,5 +111,8 @@ "text-printing-mode-with-options": "启用文字打印并指定选项", "image-printing-options": "图片打印选项", "convert-input-image-with-imagemagick": "使用 ImageMagick 转换输入图片", - "reset-configuration-": "要重置配置吗?" + "reset-configuration-": "要重置配置吗?", + "brightness-": "亮度:", + "text-printing-mode": "文字打印模式", + "please-enable-bluetooth-or-try-to-reboot": "请启用蓝牙或尝试重启" } \ No newline at end of file diff --git a/www/main.css b/www/main.css index 14f80c9..2319d63 100644 --- a/www/main.css +++ b/www/main.css @@ -1,8 +1,10 @@ :root { --font-size: 1.2rem; + /* --dpi-zoom: 0.96; */ --deco-size: calc(var(--font-size) / 2); - --line-height: 1.8em; + --line-height: calc(var(--font-size) / 2 * 3); + --compact-menu-height: 2em; --span: 8px; --span-half: calc(var(--span) / 2); --span-double: calc(var(--span) * 2); @@ -94,6 +96,7 @@ button:active { } #notice { min-height: var(--font-size); + margin: var(--span-half) 0; } #notice span { display: block; @@ -159,9 +162,9 @@ main>.menu-side>.menu { } .compact-button { width: max-content; - height: 2em; + height: var(--compact-menu-height); flex-grow: 1; - line-height: 2em; + line-height: var(--compact-menu-height); text-align: center; cursor: pointer; border: none; @@ -171,8 +174,9 @@ main>.menu-side>.menu { margin: 0; } .compact-button:hover { - padding: 0; margin: 0; + padding: 0 var(--span) calc(var(--span-double)); + min-width: 6em; } .compact-button.active { border: var(--border) solid var(--fore-color); @@ -221,8 +225,9 @@ p { height: 0; } .panel.active { - height: var(--panel-height); + height: calc(var(--panel-height) - var(--compact-menu-height)); padding: var(--span-double) var(--span); + box-sizing: border-box; /* overflow-y: scroll; */ } .panel.sub.active { @@ -451,7 +456,13 @@ a { from { overflow: hidden; } to { overflow: auto; } } -@media (max-width: 800px) { + +@media (max-height: 520px) { + body, main { margin: 0 auto; } +} + +@media (max-width: 767.5px) { + /* My test shows it's Just 768 fit best */ :root { --panel-height: 16em; --font-size: 1em; @@ -466,14 +477,14 @@ a { overflow-x: hidden; overflow-y: auto; position: fixed; - top: var(--line-height); - height: calc(100% - var(--panel-height) - 2em); + top: calc(var(--line-height) + var(--span)); + height: calc(100% - var(--panel-height) - var(--compact-menu-height)); z-index: 0; } main>.canvas-side>.buttons, main>.menu-side>.buttons { position: sticky; - bottom: 2em; + bottom: var(--compact-menu-height); width: 100%; z-index: 1; } @@ -495,7 +506,7 @@ a { box-sizing: border-box; } main>.menu-side>.menu { - height: var(--panel-height); + height: calc(var(--panel-height) - var(--compact-menu-height)); margin: 0; } #notice { @@ -510,7 +521,7 @@ a { z-index: 0; } .blank { - height: 2em; + height: var(--compact-menu-height); } } @media (max-width: 384px) { @@ -520,6 +531,13 @@ a { } } +/* @media (min-resolution: 120dpi) { + :root { --font-size: calc(1.2rem * var(--dpi-zoom)); } +} +@media (min-resolution: 144dpi) { + :root { --font-size: calc(1.2rem * var(--dpi-zoom) * var(--dpi-zoom)); } +} */ + @media (prefers-color-scheme: dark) { :root { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); } body, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; } diff --git a/www/main.js b/www/main.js index a77f0b7..3269b01 100644 --- a/www/main.js +++ b/www/main.js @@ -4,7 +4,7 @@ /** * In order to debug on a phone, we load vConsole * https://www.npmjs.com/package/vconsole - * Double-tap the "Cat Printer" title to activate + * Double-tap notice bar to activate */ function debug() { const script = document.createElement('script'); @@ -12,7 +12,7 @@ function debug() { document.body.appendChild(script); script.addEventListener('load', () => new window.VConsole()); } -document.getElementById('title').addEventListener('dblclick', debug); +document.getElementById('notice').addEventListener('dblclick', debug); var hidden_area = document.getElementById('hidden'); @@ -243,6 +243,10 @@ function putEvent(selector, type, callback, thisArg) { }); })(); +/** + * Class to control Printing Canvas (manipulate, preview etc.) + * "Brightness" is historically "Threshold", while the later is kept in code + */ class CanvasController { /** @type {HTMLCanvasElement} */ preview; @@ -252,11 +256,18 @@ class CanvasController { imageUrl; isCanvas; algorithm; - threshold; - thresholdRange; + _threshold; + get threshold() { + return this._threshold; + } + set threshold(value) { + this._threshold = this._thresholdRange.value = value; + } + _thresholdRange; transparentAsWhite; previewData; static defaultHeight = 384; + static defaultThreshold = 256 / 3; _height; get height() { return this._height; @@ -269,7 +280,7 @@ class CanvasController { this.canvas = document.getElementById('control-canvas'); this.div = document.getElementById('control-document'); this.height = CanvasController.defaultHeight; - this.thresholdRange = document.getElementById('threshold'); + this._thresholdRange = document.getElementById('threshold'); this.imageUrl = null; putEvent('input[name="mode"]', 'change', (event) => this.enableMode(event.currentTarget.value), this); @@ -304,7 +315,8 @@ class CanvasController { } useAlgorithm(name) { this.algorithm = name; - this.thresholdRange.value = 128; + this.threshold = CanvasController.defaultThreshold; + this._thresholdRange.dispatchEvent(new Event('change')); this.activatePreview(); } expand(length = CanvasController.defaultHeight) { @@ -322,7 +334,7 @@ class CanvasController { let context_p = preview.getContext('2d'); let data = context_c.getImageData(0, 0, w, h); let mono_data = new Uint8ClampedArray(w * h); - monoGrayscale(data.data, mono_data, w, h, this.transparentAsWhite); + monoGrayscale(data.data, mono_data, w, h, t, this.transparentAsWhite); switch (this.algorithm) { case 'algo-direct': monoDirect(mono_data, w, h, t); diff --git a/www/polyfill.js b/www/polyfill.js index 19f3e46..d240750 100644 --- a/www/polyfill.js +++ b/www/polyfill.js @@ -1,8 +1,29 @@ +// Polyfills -/** - * Polyfill - */ -(function() { - if (!NodeList.prototype.forEach) - NodeList.prototype.forEach = Array.prototype.forEach; -})(); +// home-made minimal fetch +// Note: only useful with this application. Extend (or remove) it as needed. +// In fact I wanted to support Otter Browser for resource-concerning people +// But it still can't cope with few other JS (even after transpiled to es5) and CSS variables +if (!window.fetch) window.fetch = function(url, options) { + options = options || {}; + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open(options.method || 'GET', url); + function response(is_binary) { + return new Promise(function(resolve, reject) { + resolve(is_binary ? xhr.response : xhr.responseText); + }); + } + xhr.onload = function() { + resolve({ + text: function() { return response(false); }, + json: function() { return response(false).then(t => JSON.parse(t)); }, + ok: Math.floor(xhr.status / 100) === 2 + }); + } + xhr.send(options.body || null); + }); +} + +// and this +if (!NodeList.prototype.forEach) NodeList.prototype.forEach = Array.prototype.forEach;