Layout fix; better dithering; misc corrections

This commit is contained in:
NaitLee 2022-05-03 00:55:20 +08:00
parent db54d6004d
commit 002072b586
11 changed files with 118 additions and 55 deletions

View File

@ -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!

View File

@ -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:

View File

@ -105,7 +105,7 @@ python3 server.py
## 有问题?
有想法?去 Discussion 讨论!
有想法?用 Issue 或去 Discussion 讨论!
如果能行Pull Request 也可以!

View File

@ -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 = (

View File

@ -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 |

View File

@ -44,8 +44,9 @@
<input type="radio" name="algo" value="algo-halftone" data-key />
<span data-i18n="pattern">Pattern</span>
</label> --><br />
<label for="threshold" data-i18n="threshold-">Threshold:</label>
<input type="range" min="0" max="256" value="128" step="8" id="threshold" data-key data-default />
<!-- "brightness" is historically "threshold" -->
<label for="threshold" data-i18n="brightness-">Brightness:</label>
<input type="range" min="0" max="256" value="86" step="1" id="threshold" data-key data-default />
<br />
<input type="checkbox" name="transparent-as-white" id="transparent-as-white" data-key checked />
<label for="transparent-as-white" data-i18n="transparent-as-white">Transparent as White</label>
@ -62,7 +63,7 @@
</div>
<div class="panel" id="panel-settings">
<label for="scan-time" data-i18n="scan-time-">Scan Time:</label>
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" data-key />
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="4" data-key />
<span data-i18n="-seconds">seconds</span>
<br />
<input type="checkbox" name="flip-h" id="flip-h" data-key />

View File

@ -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"
}

View File

@ -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": "请启用蓝牙或尝试重启"
}

View File

@ -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; }

View File

@ -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);

View File

@ -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;