Custom print feature; fixes

This commit is contained in:
NaitLee 2021-09-08 16:51:48 +08:00
parent f9194a03f8
commit 2a36344375
11 changed files with 213 additions and 23 deletions

2
.gitignore vendored
View File

@ -5,6 +5,8 @@ www/fabric.js
www/fabric.min.js
www/html2canvas.js
www/html2canvas.min.js
www/qrcode.js
www/qrcode.min.js
__pycache__
# Byte-compiled / optimized / DLL files

View File

@ -37,7 +37,8 @@ This application uses server/client module, and have fewest possible dependencie
- Python3 & Browser
- [fabric.min.js](https://github.com/fabricjs/fabric.js/tree/master/dist)
- [html2canvas.min.js](https://html2canvas.hertzen.com/)
- Any css for plain webpage, e.g. [minicss](https://minicss.org/), rename to `skin.css`
- [qrcode.min.js](https://davidshimjs.github.io/qrcodejs/)
- (Optional) Any css for plain webpage, e.g. [minicss](https://minicss.org/), rename to `skin.css`
Put any web-related files to folder `www`.
@ -47,7 +48,7 @@ Support for both Windows and GNU/Linux are included. And Windows release package
### Plans
- Support rich edit features as the official app for cat printers (iPrint & 精准学习)
- Smoother mono-color converting
- Make remote-print by web interface more standard/compatible/secure
Possible features:

View File

@ -37,7 +37,8 @@ TODO
- Python3 与浏览器
- [fabric.min.js](https://github.com/fabricjs/fabric.js/tree/master/dist)
- [html2canvas.min.js](https://html2canvas.hertzen.com/)
- 任何纯网页可用的 css如 [minicss](https://minicss.org/),重命名为 `skin.css`
- [qrcode.min.js](https://davidshimjs.github.io/qrcodejs/)
- (可选)任何纯网页可用的 css如 [minicss](https://minicss.org/),重命名为 `skin.css`
将 web 相关的文件放在 `www` 文件夹中。
@ -47,7 +48,7 @@ TODO
### 计划
- 支持高级编辑,如同官方应用 (iPrint & 精准学习)
- 更好的双色转换
- 使 web 界面的远程打印标准化/兼容/安全
可能的功能:

View File

@ -18,6 +18,40 @@
<input style="display: none;" type="text" id="bt_mac" value="" />
<span>Select device:</span><select id="device_selection"></select><button id="refresh_device">Refresh</button>
</p>
<p>
<span>
<span>Operate directly on the box canvas</span>
<button id="preview_button">Preview</button>
<button id="print_button">Print</button>
<br />
<span>Canvas Height:</span>
<input type="number" value="400" min="0" max="114514" step="50" id="canvas_height" />
<br />
<span>Threshold:</span>
<input type="range" min="0" max="2.0" step="0.05" value="0.6" id="filter_threshold" />
</span>
<br />
<span>
<span>Insert:</span>
<button id="action_insert_text">Text</button>
<button id="action_insert_image">Image</button>
<button id="action_insert_qr">QRCode</button>
<br />
<span>Action:</span>
<button id="action_make_bold">Bold</button>
<button id="action_make_italic">Italic</button>
<button id="action_make_underline">Underline</button>
<button id="action_switch_paint">Switch Paint</button>
<button id="action_delete">Delete</button>
</span>
</p>
<div style="margin: auto; width: 386px;">
<canvas id="work_canvas" style="border: 1px solid currentColor; width: 384px; min-height: 3em;" width="384" height="384"></canvas>
</div>
<p>
<span>Preview</span><br />
<canvas id="image_preview" style="width: 384px;" width="384" height="0"></canvas>
</p>
</main>
<textarea id="i18-N" style="display: none;">
[zh-CN]
@ -25,6 +59,7 @@ Custom Print=自定义打印
Javascript should be enabled=需启用 Javascript
Select device:=选择设备:
Refresh=刷新
Operate directly on the box canvas=直接在框内画布操作
Preview=预览
Print=打印
Threshold:=阈值:
@ -33,8 +68,22 @@ Printing, please wait.=打印中,请稍候。
Please preview image first=请先预览图像
Searching devices. Please wait for 5 seconds.=正在查找设备。请等候 5 秒。
OK=完成
Insert:=插入:
Text=文本
Image=图像
QRCode=二维码
Double click to edit=双击以修改
Action:=操作:
Bold=粗体
Italic=斜体
Underline=下划线
Delete=删除
Switch Paint=切换绘画
Canvas Height:=画布高度:
Content of QRCode:=二维码的内容:
</textarea>
<script src="i18n.js"></script>
<script src="qrcode.min.js"></script>
<script src="fabric.min.js"></script>
<script src="main.js"></script>
<script src="custom-print.js"></script>

View File

@ -0,0 +1,138 @@
///<reference path="main.js" />
///<reference path="main.d.ts" />
class CustomPrinter {
WIDTH = 384;
threshold = 0.6;
bluetoothMACInput = document.getElementById('bt_mac');
thresholdInput = document.getElementById('filter_threshold');
fileSelection = document.createElement('input');
dummyImage = new Image();
canvasPreview = document.getElementById('image_preview');
previewButton = document.getElementById('preview_button');
printButton = document.getElementById('print_button');
actionInsertText = document.getElementById('action_insert_text');
actionInsertImage = document.getElementById('action_insert_image');
actionInsertQR = document.getElementById('action_insert_qr');
canvasHeightInput = document.getElementById('canvas_height');
fabricCanvas = new fabric.Canvas('work_canvas', {
backgroundColor: 'white'
});
monoMethod = imageDataColorToMonoSquare;
insertImage() {
this.fileSelection.click();
}
preview() {
let context = this.fabricCanvas.getContext('2d');
let imagedata = context.getImageData(0, 0, this.WIDTH, this.fabricCanvas.height);
this.canvasPreview.height = this.fabricCanvas.height;
this.canvasPreview.getContext('2d').putImageData(this.monoMethod(imagedata, this.threshold), 0, 0);
}
constructor() {
this.fileSelection.type = 'file';
this.monoMethod = imageDataColorToMonoSquare;
this.fileSelection.addEventListener('input', this.preview.bind(this));
this.previewButton.addEventListener('click', this.preview.bind(this));
this.thresholdInput.onchange = event => {
this.threshold = this.thresholdInput.value;
}
this.canvasHeightInput.onchange = event => {
this.fabricCanvas.setHeight(this.canvasHeightInput.value);
}
this.actionInsertText.addEventListener('click', event => {
let text = new fabric.Textbox(i18N.get('Double click to edit'), {
color: 'black',
fontSize: 24,
});
this.fabricCanvas.add(text);
});
this.fileSelection.addEventListener('input', event => {
let reader = new FileReader();
reader.onload = event1 => {
this.dummyImage.src = event1.target.result;
let fimage = new fabric.Image(this.dummyImage, {});
fimage.scale(this.WIDTH / this.dummyImage.width);
this.fabricCanvas.add(fimage);
}
reader.readAsDataURL(this.fileSelection.files[0]);
});
this.actionInsertImage.addEventListener('click', this.insertImage.bind(this));
this.actionInsertQR.addEventListener('click', event => {
let div = document.createElement('div');
new QRCode(div, prompt(i18N.get('Content of QRCode:')));
// QRCode generation is async, currently no better way than wait for a while
setTimeout(() => {
let fimage = new fabric.Image(div.lastChild, {});
fimage.scale(this.WIDTH / div.lastChild.width);
this.fabricCanvas.add(fimage);
}, 1000);
});
this.printButton.addEventListener('click', event => {
// this.preview();
if (this.canvasPreview.height == 0) {
notice(i18N.get('Please preview image first'));
return;
}
let mac_address = this.bluetoothMACInput.value;
if (mac_address == '') {
notice(i18N.get('Please select a device'));
return;
}
notice(i18N.get('Printing, please wait.'));
let context = this.canvasPreview.getContext('2d');
let pbm_data = imageDataMonoToPBM(context.getImageData(0, 0, this.WIDTH, this.canvasPreview.height));
let xhr = new XMLHttpRequest();
xhr.open('POST', '/~print?address=' + mac_address);
xhr.setRequestHeader('Content-Type', 'application-octet-stream');
xhr.onload = () => {
notice(i18N.get(xhr.responseText));
}
xhr.send(pbm_data);
});
let boldFunction = () => {
let object = this.fabricCanvas.getActiveObject();
if (!object) return;
if (object.type == 'textbox') {
if (object.fontWeight == 'normal') object.fontWeight = 'bold';
else if (object.fontWeight == 'bold') object.fontWeight = 'normal';
this.fabricCanvas.renderAll();
}
}
document.getElementById('action_make_bold').addEventListener('click', boldFunction.bind(this));
let italicFunction = () => {
let object = this.fabricCanvas.getActiveObject();
if (!object) return;
if (object.type == 'textbox') {
if (object.fontStyle == 'normal') object.fontStyle = 'italic';
else if (object.fontStyle == 'italic') object.fontStyle = 'normal';
this.fabricCanvas.renderAll();
}
}
document.getElementById('action_make_italic').addEventListener('click', italicFunction.bind(this));
document.getElementById('action_make_underline').addEventListener('click', event => {
let object = this.fabricCanvas.getActiveObject();
if (!object) return;
if (object.type == 'textbox') {
object.underline = !object.underline;
// Seems there's a bug in fabric, underline cannot be rendered before changing bold/italic
boldFunction();
this.fabricCanvas.renderAll();
boldFunction();
this.fabricCanvas.renderAll();
}
});
document.getElementById('action_delete').addEventListener('click', event => {
let object = this.fabricCanvas.getActiveObject();
if (!object) return;
this.fabricCanvas.remove(object);
this.fabricCanvas.renderAll();
});
this.fabricCanvas.freeDrawingBrush.color = 'black';
this.fabricCanvas.freeDrawingBrush.width = 6;
document.getElementById('action_switch_paint').addEventListener('click', event => {
this.fabricCanvas.isDrawingMode = !this.fabricCanvas.isDrawingMode;
})
}
}
var custom_printer = new CustomPrinter();

View File

@ -67,8 +67,8 @@
}
function get(originaltext, language) {
if (lang[language] == undefined) return originaltext;
language = language || userlang || defaultlang;
if (lang[language] == undefined) return originaltext;
return lang[language][originaltext] || originaltext;
}

View File

@ -15,6 +15,7 @@ main.mainpage p a {
display: inline-block;
font-size: larger;
width: 10em;
padding: 0.5em 0;
}
a:link, a:visited {

4
www/main.d.ts vendored
View File

@ -13,7 +13,7 @@ declare interface ImagePrinter {
bluetoothMACInput: HTMLInputElement;
fileSelection: HTMLInputElement;
dummyImage: HTMLImageElement;
imagePreview: HTMLCanvasElement;
canvasPreview: HTMLCanvasElement;
previewButton: HTMLButtonElement;
printButton: HTMLButtonElement;
monoMethod: Function;
@ -24,7 +24,7 @@ declare interface DocumentPrinter {
bluetoothMACInput: HTMLInputElement;
container: HTMLDivElement;
printButton: HTMLButtonElement;
imagePreview: HTMLDivElement;
canvasPreview: HTMLDivElement;
monoMethod: Function;
}

View File

@ -28,11 +28,9 @@
<div class="reset_styles" contenteditable="true" style="width: 384px; min-height: 3em;"></div>
</div>
</div>
<p style="font-style: italic; color: gray;">From MS Office/WPS/LibreOffice to here, pasted data shall be formatted</p>
<p style="font-style: italic; color: gray;">From Webpage/MS Office/WPS/LibreOffice to here, pasted data shall be formatted</p>
<p>Preview</p>
<div style="margin: auto; border: 1px solid gray; width: 384px; min-height: 3em;">
<div id="image_preview" style="width: 384px; min-height: 3em;"></div>
</div>
<div id="image_preview" style="width: 384px; margin: auto;"></div>
</main>
<textarea id="i18-N" style="display: none;">
[zh-CN]
@ -44,7 +42,7 @@ Copy & paste document to box=复制粘贴文档至框内
Preview=预览
Print=打印
Threshold:=阈值:
From MS Office/WPS/LibreOffice to here, pasted data shall be formatted=从 MS Office/WPS/LibreOffice 到这里,粘贴的数据格式会保留
From Webpage/MS Office/WPS/LibreOffice to here, pasted data shall be formatted=从网页/MS Office/WPS/LibreOffice 到这里,粘贴的数据格式会保留
Please select a device=请选择一个设备
Printing, please wait.=打印中,请稍候。
Please preview image first=请先预览图像

View File

@ -9,7 +9,7 @@ class DocumentPrinter {
printButton = document.getElementById('print_button');
previewButton = document.getElementById('preview_button');
threshold = 0.2;
imagePreview = document.getElementById('image_preview');
canvasPreview = document.getElementById('image_preview');
monoMethod = imageDataColorToMonoSquare;
constructor() {
this.thresholdInput.onchange = event => {
@ -21,7 +21,7 @@ class DocumentPrinter {
notice(i18N.get('Please select a device'));
return;
}
if (this.imagePreview.children.length == 0) {
if (this.canvasPreview.children.length == 0) {
notice(i18N.get('Please preview image first'));
return;
}
@ -42,13 +42,13 @@ class DocumentPrinter {
});
});
this.previewButton.addEventListener('click', event => {
if (this.imagePreview.children[0] != null) this.imagePreview.children[0].remove();
if (this.canvasPreview.children[0] != null) this.canvasPreview.children[0].remove();
html2canvas(this.container).then(canvas => {
let context = canvas.getContext('2d');
let imagedata = context.getImageData(0, 0, this.WIDTH, canvas.height);
let mono_imagedata = this.monoMethod(imagedata, this.threshold);
context.putImageData(mono_imagedata, 0, 0);
this.imagePreview.appendChild(canvas);
this.canvasPreview.appendChild(canvas);
});
})
}

View File

@ -8,7 +8,7 @@ class ImagePrinter {
thresholdInput = document.getElementById('filter_threshold');
fileSelection = document.getElementById('file_selection');
dummyImage = new Image();
imagePreview = document.getElementById('image_preview');
canvasPreview = document.getElementById('image_preview');
previewButton = document.getElementById('preview_button');
printButton = document.getElementById('print_button');
preview() {
@ -16,9 +16,9 @@ class ImagePrinter {
reader.onload = event1 => {
this.dummyImage.src = event1.target.result;
let height = this.WIDTH / this.dummyImage.width * this.dummyImage.height;
this.imagePreview.width = this.WIDTH;
this.imagePreview.height = height;
let context = this.imagePreview.getContext('2d');
this.canvasPreview.width = this.WIDTH;
this.canvasPreview.height = height;
let context = this.canvasPreview.getContext('2d');
context.drawImage(this.dummyImage, 0, 0, this.WIDTH, height);
let data = context.getImageData(0, 0, this.WIDTH, height);
context.putImageData(this.monoMethod(data, this.threshold), 0, 0);
@ -34,7 +34,7 @@ class ImagePrinter {
}
this.printButton.addEventListener('click', event => {
// this.preview();
if (this.imagePreview.height == 0) {
if (this.canvasPreview.height == 0) {
notice(i18N.get('Please preview image first'));
return;
}
@ -44,8 +44,8 @@ class ImagePrinter {
return;
}
notice(i18N.get('Printing, please wait.'));
let context = this.imagePreview.getContext('2d');
let pbm_data = imageDataMonoToPBM(context.getImageData(0, 0, this.WIDTH, this.imagePreview.height));
let context = this.canvasPreview.getContext('2d');
let pbm_data = imageDataMonoToPBM(context.getImageData(0, 0, this.WIDTH, this.canvasPreview.height));
let xhr = new XMLHttpRequest();
xhr.open('POST', '/~print?address=' + mac_address);
xhr.setRequestHeader('Content-Type', 'application-octet-stream');