Merge pull request 'Webcam support to use it as a photomaton ( photobooth )' (#2) from webcam into master
Reviewed-on: http://git.n07070.xyz/n07070/littleprynter/pulls/2
This commit is contained in:
commit
9359c8e503
@ -1,13 +1,31 @@
|
|||||||
Adafruit-Thermal>=1.1.0
|
Adafruit-Thermal~=1.1.0
|
||||||
Click>=7.0
|
appdirs~=1.4.4
|
||||||
Flask>=1.0.2
|
argcomplete~=2.0.0
|
||||||
Flask-Limiter>=1.0.1
|
click~=8.1.3
|
||||||
itsdangerous>=0.24
|
commonmark~=0.9.1
|
||||||
Jinja2>=2.10
|
Deprecated~=1.2.13
|
||||||
limits>=1.3
|
escpos~=1.9
|
||||||
MarkupSafe>=1.0
|
Flask~=2.1.2
|
||||||
Pillow>=5.3.0
|
Flask-Limiter~=2.4.5.1
|
||||||
pyserial>=3.4
|
future~=0.18.2
|
||||||
six>=1.11.0
|
itsdangerous~=2.1.2
|
||||||
Unidecode>=1.0.22
|
Jinja2~=3.1.2
|
||||||
Werkzeug>=0.14.1
|
limits~=2.6.1
|
||||||
|
MarkupSafe~=2.1.1
|
||||||
|
packaging~=21.3
|
||||||
|
Pillow~=9.1.0
|
||||||
|
Pygments~=2.12.0
|
||||||
|
pyparsing~=3.0.8
|
||||||
|
pyserial~=3.5
|
||||||
|
python-barcode~=0.13.1
|
||||||
|
pyusb~=1.2.1
|
||||||
|
PyYAML~=6.0
|
||||||
|
qrcode~=7.3.1
|
||||||
|
rich~=12.4.1
|
||||||
|
six~=1.16.0
|
||||||
|
toml~=0.10.2
|
||||||
|
typing_extensions~=4.2.0
|
||||||
|
Unidecode~=1.3.4
|
||||||
|
viivakoodi~=0.8.0
|
||||||
|
Werkzeug~=2.1.2
|
||||||
|
wrapt~=1.14.1
|
||||||
|
4
run.sh
4
run.sh
@ -1,3 +1,7 @@
|
|||||||
|
virtualenv .
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
export FLASK_APP=src/main.py
|
export FLASK_APP=src/main.py
|
||||||
export FLASK_ENV=development
|
export FLASK_ENV=development
|
||||||
flask run --host 192.168.0.42 --debugger --eager-loading
|
flask run --host 192.168.0.42 --debugger --eager-loading
|
||||||
|
22
src/main.py
22
src/main.py
@ -51,6 +51,7 @@ app.secret_key = configuration_file["secrets"]["flask_secret_key"]
|
|||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
app.config['ALLOWED_EXTENSIONS'] = ALLOWED_EXTENSIONS
|
app.config['ALLOWED_EXTENSIONS'] = ALLOWED_EXTENSIONS
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 3 * 1000 * 1000 # Maximum 3Mb for a file upload
|
app.config['MAX_CONTENT_LENGTH'] = 3 * 1000 * 1000 # Maximum 3Mb for a file upload
|
||||||
|
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||||
|
|
||||||
# Printer connection
|
# Printer connection
|
||||||
# Uses the class defined in the printer.py file
|
# Uses the class defined in the printer.py file
|
||||||
@ -64,7 +65,7 @@ web = Web(app, printer)
|
|||||||
limiter = Limiter(
|
limiter = Limiter(
|
||||||
app,
|
app,
|
||||||
key_func=get_remote_address,
|
key_func=get_remote_address,
|
||||||
default_limits=["200 per day", "50 per hour"]
|
default_limits=["1500 per day", "500 per hour"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@ -73,6 +74,12 @@ def index():
|
|||||||
app.logger.debug("Loading index")
|
app.logger.debug("Loading index")
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/webcam')
|
||||||
|
@limiter.limit("1/second", override_defaults=False)
|
||||||
|
def webcam():
|
||||||
|
app.logger.debug("Loading webcam interface")
|
||||||
|
return render_template('webcam.html')
|
||||||
|
|
||||||
# API routes
|
# API routes
|
||||||
# The api has the following methods
|
# The api has the following methods
|
||||||
# api/print/{sms,img,letter,qr,barcode}
|
# api/print/{sms,img,letter,qr,barcode}
|
||||||
@ -115,17 +122,24 @@ def api_print_image():
|
|||||||
sign = request.form["signature"]
|
sign = request.form["signature"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(str(e),'error')
|
flash(str(e),'error')
|
||||||
redirect(url_for('index'))
|
app.logger.error(str(e) + " - Whoops, no forms submitted or missing signature.")
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# check if the post request has the file part
|
# check if the post request has the file part
|
||||||
|
try:
|
||||||
if 'img' not in request.files:
|
if 'img' not in request.files:
|
||||||
flash('No file found. Did you use the good form ?', 'error')
|
flash('No file found. Did you use the good form ?', 'error')
|
||||||
|
app.logger.error("No file found. Did you use the good form ?")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
else:
|
else:
|
||||||
file = request.files['img']
|
file = request.files['img']
|
||||||
|
except Exception as e:
|
||||||
|
if sign is not None and photo is not None:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
flash(str(e), 'error')
|
||||||
|
app.logger.error("Couldn't get an image nor signature : " + str(e))
|
||||||
|
|
||||||
# If the user does not select a file, the browser submits an
|
# If the user does not select a file, the browser submits an
|
||||||
# empty file without a filename.
|
# empty file without a filename.
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
body {
|
/* body {
|
||||||
margin-left: 20%;
|
margin-left: 20%;
|
||||||
margin-right: 20%;
|
margin-right: 20%;
|
||||||
}
|
} */
|
||||||
|
|
||||||
button {
|
|
||||||
margin-top: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*// Small devices (landscape phones, 576px and up)*/
|
/*// Small devices (landscape phones, 576px and up)*/
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 576px) {
|
||||||
@ -14,8 +10,8 @@ button {
|
|||||||
|
|
||||||
/*// Medium devices (tablets, 768px and up)*/
|
/*// Medium devices (tablets, 768px and up)*/
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
margin-left: 0%;
|
margin-left: 0;
|
||||||
margin-right: 0%;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*// Large devices (desktops, 992px and up)*/
|
/*// Large devices (desktops, 992px and up)*/
|
||||||
|
275
src/static/js/webcam.js
Normal file
275
src/static/js/webcam.js
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
let streaming;
|
||||||
|
var current_stream;
|
||||||
|
var current_camera_is;
|
||||||
|
|
||||||
|
var width = document.getElementById("video").parentNode.parentElement.clientWidth;
|
||||||
|
var height = width / (4 / 3);
|
||||||
|
|
||||||
|
function startup(){
|
||||||
|
video = document.getElementById('video');
|
||||||
|
canvas = document.getElementById('canvas');
|
||||||
|
photo = document.getElementById('photo');
|
||||||
|
switch_cameras = document.getElementById('flip')
|
||||||
|
printButton = document.getElementById('print_button');
|
||||||
|
|
||||||
|
if (check_webcam() === true ){
|
||||||
|
setup_events();
|
||||||
|
clear_canvas();
|
||||||
|
} else {
|
||||||
|
no_webcam_error();
|
||||||
|
console.log("Seems like it's impossible to get a webcam.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_webcam(){
|
||||||
|
console.log("Cheking for a camera...");
|
||||||
|
if (get_front_webcam()) {
|
||||||
|
console.log("Got front camera !");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_any_webcam()) {
|
||||||
|
console.log("Got a webcam !");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log("Nope");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_events(){
|
||||||
|
|
||||||
|
// When the video is ready, we start streaming it to the canvas.
|
||||||
|
// The canvas is hidden, but it still exists in the browser.
|
||||||
|
video.addEventListener('canplay', function(ev) {
|
||||||
|
if (!streaming) {
|
||||||
|
height = video.videoHeight / (video.videoWidth / width);
|
||||||
|
|
||||||
|
if (isNaN(height)) {
|
||||||
|
height = width / (4 / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
video.setAttribute('width', width);
|
||||||
|
video.setAttribute('height', height);
|
||||||
|
canvas.setAttribute('width', width);
|
||||||
|
canvas.setAttribute('height', height);
|
||||||
|
|
||||||
|
photo.setAttribute('width', width);
|
||||||
|
photo.setAttribute('height', height);
|
||||||
|
|
||||||
|
|
||||||
|
streaming = true;
|
||||||
|
printButton.removeAttribute("disabled","");
|
||||||
|
switch_cameras.removeAttribute("disabled","");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
switch_cameras.addEventListener('click', function(ev) {
|
||||||
|
flip_cameras();
|
||||||
|
}, false );
|
||||||
|
|
||||||
|
printButton.addEventListener('click', function(ev){
|
||||||
|
data = take_picture();
|
||||||
|
try {
|
||||||
|
print_picture(data);
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to print a picture because : " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear_canvas() {
|
||||||
|
var context = canvas.getContext('2d');
|
||||||
|
context.fillStyle = "#AAA";
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
var data = canvas.toDataURL('image/png');
|
||||||
|
photo.setAttribute('src', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataURLtoFile(dataurl, filename) {
|
||||||
|
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
|
||||||
|
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||||
|
while(n--){
|
||||||
|
u8arr[n] = bstr.charCodeAt(n);
|
||||||
|
}
|
||||||
|
return new File([u8arr], filename, {type:mime});
|
||||||
|
}
|
||||||
|
|
||||||
|
function take_picture(){
|
||||||
|
var context = canvas.getContext('2d');
|
||||||
|
if (width && height) {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
context.drawImage(video, 0, 0, width, height);
|
||||||
|
|
||||||
|
var data = canvas.toDataURL('image/png');
|
||||||
|
photo.setAttribute('src', data);
|
||||||
|
} else {
|
||||||
|
clear_canvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function print_picture(data){
|
||||||
|
var url = "/api/print/img"
|
||||||
|
var picture = dataURLtoFile(data);
|
||||||
|
const formData = new FormData();
|
||||||
|
let currentDate = new Date();
|
||||||
|
let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
|
||||||
|
|
||||||
|
formData.set("img", picture, "picture.png");
|
||||||
|
formData.set("signature", "Printed via the webcam @ " + time)
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST', // or 'PUT'
|
||||||
|
body: formData,
|
||||||
|
// headers:{
|
||||||
|
// 'Content-Type': 'multipart/form-data'
|
||||||
|
// }
|
||||||
|
}).then(function(response) { console.log('Success:', response); alert("Picture printed."); } , true)
|
||||||
|
.catch(error => console.error('Error:', error), false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip_cameras(){
|
||||||
|
switch (current_camera_is) {
|
||||||
|
case "front":
|
||||||
|
try {
|
||||||
|
get_any_webcam();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not get another camera");
|
||||||
|
get_front_webcam();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "any":
|
||||||
|
try {
|
||||||
|
get_front_webcam();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not get another camera");
|
||||||
|
get_any_webcam();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Impossible to switch cameras : none is selected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop_video_streams(){
|
||||||
|
console.log("Stopping existing video streams.");
|
||||||
|
// Stop the tracks
|
||||||
|
try {
|
||||||
|
if (current_stream) {
|
||||||
|
const tracks = current_stream.getTracks();
|
||||||
|
tracks.forEach(track => track.stop());
|
||||||
|
console.log("Stopped playing current videostreams.");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log("No streams currently playing.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Could not stop playing current streams." + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_webcam(options){
|
||||||
|
stop_video_streams();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.mediaDevices.getUserMedia(options)
|
||||||
|
.then(function(stream) {
|
||||||
|
// on success, stream it in video tag
|
||||||
|
// the video tag is hidden, as is the canvas.
|
||||||
|
console.log("Got a camera ( generic )");
|
||||||
|
|
||||||
|
printButton.removeAttribute("disabled","");
|
||||||
|
current_stream = stream;
|
||||||
|
video.srcObject = stream;
|
||||||
|
video.play();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.log("Didn't manage to get a camera :" + err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Didn't manage to get a camera :" + err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_any_webcam(){
|
||||||
|
var camera_options = {
|
||||||
|
video: {
|
||||||
|
facingMode: 'environment', // Or 'environment' if we want a camera facing away
|
||||||
|
},
|
||||||
|
audio: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if(get_webcam(camera_options)){
|
||||||
|
console.log("Got any camera, or environment camera.");
|
||||||
|
current_camera_is = "any";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_front_webcam(){
|
||||||
|
// We try to start with the front facing camera,
|
||||||
|
// if we have no support, we switch back to a normal camera.
|
||||||
|
try {
|
||||||
|
const supports = navigator.mediaDevices.getSupportedConstraints();
|
||||||
|
if (!supports['facingMode']) {
|
||||||
|
throw new Error("This browser does not support facingMode!");
|
||||||
|
} else {
|
||||||
|
var camera_options = {
|
||||||
|
video: {
|
||||||
|
facingMode: 'user', // Or 'environment' if we want a camera facing away
|
||||||
|
},
|
||||||
|
audio: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Resetting to default camera : " + e);
|
||||||
|
var camera_options = {
|
||||||
|
video: true,
|
||||||
|
audio: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if(get_webcam(camera_options)){
|
||||||
|
console.log("Got the front camera");
|
||||||
|
current_camera_is = "front";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function no_webcam_error(){
|
||||||
|
console.log("Seems like they is no webcam available.")
|
||||||
|
|
||||||
|
// We disable the print button is it cannot be clicked.
|
||||||
|
printButton.setAttribute("disabled","");
|
||||||
|
|
||||||
|
// We create an alert message.
|
||||||
|
const frame_div = document.getElementById("image_dither");
|
||||||
|
frame_div.removeAttribute('class');
|
||||||
|
|
||||||
|
const alert_div = document.createElement("div");
|
||||||
|
alert_div.setAttribute("class", "alert alert-warning");
|
||||||
|
alert_div.setAttribute("role", "alert");
|
||||||
|
|
||||||
|
var alert_message = document.createTextNode("We where unable to get a Webcam device, this page will not work.");
|
||||||
|
alert_div.appendChild(alert_message);
|
||||||
|
frame_div.appendChild(alert_div);
|
||||||
|
throw new Error("Unable to get a video device, stopping the photobooth.");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', startup, false);
|
21
src/templates/banner.html
Normal file
21
src/templates/banner.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<body class="container">
|
||||||
|
<div class="col-md-7 offset-md-2">
|
||||||
|
<a href="/"><img class="rounded" width="100px" src="{{ url_for('static', filename='images/little-printer.png') }}" alt="LittlePrynter icon"></a><h1 class="card-title font-weight-bold">Little Prynter</h1>
|
||||||
|
<hr>
|
||||||
|
<div class="alert alert-dark" role="alert">LittlePrynter is under heavy developpement, you may encounter bugs ! If so, try again. Thanks !</div>
|
||||||
|
<br>
|
||||||
|
{% with messages = get_flashed_messages(category_filter=('error')) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-danger" role="alert">⚠️ {{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(category_filter=('info')) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-info" role="alert">ℹ️ {{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
59
src/templates/base.html
Normal file
59
src/templates/base.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>LittlePrynter</title>
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static',filename='css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="container">
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="/"><img class="rounded" width="100px" src="{{ url_for('static', filename='images/little-printer.png') }}" alt="LittlePrynter icon"></a><h1 class="card-title font-weight-bold">Little Prynter</h1>
|
||||||
|
<hr>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/webcam">Webcam</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="alert alert-dark" role="alert">LittlePrynter is under heavy developpement, you may encounter bugs ! If so, try again. Thanks !</div>
|
||||||
|
<br>
|
||||||
|
{% with messages = get_flashed_messages(category_filter=('error')) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-danger" role="alert">⚠️ {{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(category_filter=('info')) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-info" role="alert">ℹ️ {{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<footer class="row">
|
||||||
|
<p class=" text-center">Little Prynter is built by <a href="https://n07070.xyz/about-me/">n07070</a> because it's fun :) | <a href="https://git.n07070.xyz/n07070/littleprynter">Source code - AGPLv3</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,5 @@
|
|||||||
|
<hr>
|
||||||
|
<footer>Little Prynter is built by <a href="https://n07070.xyz/about-me/">n07070</a> because it's fun :) | <a href="https://git.n07070.xyz/n07070/littleprynter/src/branch/epson-tm-t20iii">Source code - AGPLv3</a></footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>LittlePrynter</title>
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static',filename='css/style.css') }}">
|
||||||
|
</head>
|
@ -1,48 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'base.html' %}
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>LittlePrynter</title>
|
|
||||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static',filename='css/style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body class="container">
|
|
||||||
<div class="col-md-7 offset-md-2">
|
|
||||||
<a href="/"><img class="rounded" width="100px" src="{{ url_for('static', filename='images/little-printer.png') }}" alt="LittlePrynter icon"></a><h1 class="card-title font-weight-bold">Little Prynter</h1>
|
|
||||||
<hr>
|
|
||||||
<div class="alert alert-dark" role="alert">LittlePrynter is under heavy developpement, you may encounter bugs ! If so, try again. Thanks !</div>
|
|
||||||
<br>
|
|
||||||
{% with messages = get_flashed_messages(category_filter=('error')) %}
|
|
||||||
{% if messages %}
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert alert-danger" role="alert">⚠️ {{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(category_filter=('info')) %}
|
{% block content %}
|
||||||
{% if messages %}
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert alert-info" role="alert">ℹ️ {{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<!-- <br>
|
|
||||||
<h3>Imprimer une image aléatoire</h3>
|
|
||||||
<a class="btn btn-primary btn-lg" href="/api/print/image"> Imprimer</a>
|
|
||||||
<hr>
|
|
||||||
<br> -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-header">Print a short message</h3>
|
<h3 class="card-header">Print a short message</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<form class="form-group" action="/api/print/sms" method="post">
|
<form class="form-group" action="/api/print/sms" method="post">
|
||||||
<input class="form-control" type="text" name="txt" placeholder="200 chars or less " maxlength="200" required><br>
|
<input class="form-control" type="text" name="txt" placeholder="200 chars or less " maxlength="200" required><br>
|
||||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||||
|
|
||||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -59,13 +24,5 @@
|
|||||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div id="pbar" style="display:none">
|
|
||||||
<img src="{{ url_for('static',filename='load.gif') }}" alt="loading wheel">
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
{% endblock %}
|
||||||
<footer>Little Prynter is built by <a href="https://n07070.xyz/about-me/">n07070</a> because it's fun :) | <a href="https://git.n07070.xyz/n07070/littleprynter/src/branch/epson-tm-t20iii">Source code - AGPLv3</a></footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
104
src/templates/webcam.html
Normal file
104
src/templates/webcam.html
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<noscript class="d-block">
|
||||||
|
<div class="alert alert-warning " role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bug-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.978.855a.5.5 0 1 0-.956.29l.41 1.352A4.985 4.985 0 0 0 3 6h10a4.985 4.985 0 0 0-1.432-3.503l.41-1.352a.5.5 0 1 0-.956-.29l-.291.956A4.978 4.978 0 0 0 8 1a4.979 4.979 0 0 0-2.731.811l-.29-.956z"/>
|
||||||
|
<path d="M13 6v1H8.5v8.975A5 5 0 0 0 13 11h.5a.5.5 0 0 1 .5.5v.5a.5.5 0 1 0 1 0v-.5a1.5 1.5 0 0 0-1.5-1.5H13V9h1.5a.5.5 0 0 0 0-1H13V7h.5A1.5 1.5 0 0 0 15 5.5V5a.5.5 0 0 0-1 0v.5a.5.5 0 0 1-.5.5H13zm-5.5 9.975V7H3V6h-.5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 0-1 0v.5A1.5 1.5 0 0 0 2.5 7H3v1H1.5a.5.5 0 0 0 0 1H3v1h-.5A1.5 1.5 0 0 0 1 11.5v.5a.5.5 0 1 0 1 0v-.5a.5.5 0 0 1 .5-.5H3a5 5 0 0 0 4.5 4.975z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
This page will need Javascript and your authorization to use the webcam to work. It is not possible because Javascript seems to be deactived on this page.
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">Photomaton</h3>
|
||||||
|
<div class="card-body row">
|
||||||
|
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
<video id="video" class="">Video stream not available.</video>
|
||||||
|
|
||||||
|
<div class="container-sm">
|
||||||
|
<div class="row">
|
||||||
|
<button class="col-sm col-lg-3 offset-lg-2 btn btn-primary justify-content-center" name="print_picture" id="print_button" disabled="">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-printer" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"/>
|
||||||
|
<path d="M5 1a2 2 0 0 0-2 2v2H2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-1h1a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1V3a2 2 0 0 0-2-2H5zM4 3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2H4V3zm1 5a2 2 0 0 0-2 2v1H2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v-1a2 2 0 0 0-2-2H5zm7 2v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="col-sm col-lg-3 offset-lg-2 btn btn-secondary justify-content-center" name="flip" id="flip" disabled="">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
|
||||||
|
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/>
|
||||||
|
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="image_dither" id="image_dither">
|
||||||
|
<picture>
|
||||||
|
<img id="photo" class="justify-content-center">
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>This is a as-close-as-possible reprensentation of what is printed on the thermal printer.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style media="screen">
|
||||||
|
canvas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* video {
|
||||||
|
display: none;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.image_dither {
|
||||||
|
/* width: 300px;
|
||||||
|
height: 400px; */
|
||||||
|
image-rendering: smooth;
|
||||||
|
filter: saturate(0) contrast(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
picture {
|
||||||
|
image-rendering: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_dither > picture::after {
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
display: block;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAOklEQVR4nC3KQQFAQAAAwd0EIohANA0uiiiugQgiiLA+5j0AVBewVa/VUGd1qBNgrR5gqW6r8x+7Oj8/xCBur9LU/wAAAABJRU5ErkJggg==');
|
||||||
|
background-repeat: repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#photo {
|
||||||
|
background: linear-gradient(90deg, #00C9FF 0%, #92FE9D 100%);
|
||||||
|
padding: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="{{ url_for('static',filename='js/webcam.js') }}"></script>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user