Commit 63593eed authored by Gabriel Monnerat's avatar Gabriel Monnerat

erp5_document_scanner: Initial gadget to scanner documents using mobile

For now, this is chrome only
parent 5cff0e99
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Folder" module="OFS.Folder"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_document_scanner</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
/*!
* Cropper.js v1.5.1
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2019-03-10T09:55:50.492Z
*/.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>cropper.min.css</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/css</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>cropper.min.css</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>cropper.min.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/x-javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>cropper.min.js</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
.device-selector {
text-align: center;
font-size: 19px;
}
.video, .photo, .output {
width: auto;
height: 360px;
filter: brightness(1);
}
.canvas {
display: none;
filter: brightness(1);
}
.output {
float: right;
}
.camera {
float: left;
padding-right: 2em;
}
.camera, .output {
display:inline-block;
}
.startbutton, .crop-button {
display:block;
position:relative;
margin: 0 auto;
bottom: 3em;
background-color: rgba(0, 150, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);
font-size: 14px;
font-family: "Lucida Grande", "Arial", sans-serif;
color: rgba(255, 255, 255, 1.0);
}
.contentarea {
font-size: 16px;
font-family: "Lucida Grande", "Arial", sans-serif;
width: 760px;
}
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_document_scanner.css</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/css</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<link rel="stylesheet" href="gadget_document_scanner.css">
<link rel="stylesheet" href="cropper.min.css">
<script type="text/javascript" src="cropper.min.js"></script>
<script type="text/javascript" src="gadget_document_scanner.js?foo=005"></script>
<title>Gadget Document Scanner</title>
</head>
<body>
<div class="device-selector">
<select>
<option value="">--------</option>
</select>
</div>
<div style="display: inline-block;">
<div class="camera">
<video class="video">Webcam is not available</video>
<button type="button" class="startbutton">Take a picture!</button>
</div>
<canvas class="canvas"></canvas>
<div class="output" style="display: none;">
<img class="photo" alt="Photo">
<input class="photoInput" type="hidden">
<button class="crop-button">Crop</button>
</div>
</div>
</body>
</html>
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_document_scanner.html</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
(function (rJS, RSVP, window, document, navigator, Cropper, console, alert, FileReader, URL) {
"use strict";
var imageWidth,
imageHeight,
cropper,
video,
canvas,
photo,
startbutton,
photoInput,
imageCapture;
function gotStream(mediaStream) {
imageCapture = new window.ImageCapture(mediaStream.getVideoTracks()[0]);
video.srcObject = mediaStream;
return imageCapture.getPhotoCapabilities();
}
function contrastImage(input, output, contrast) {
var outputContext,
inputContext = input.getContext("2d"),
imageData = inputContext.getImageData(0, 0, input.width, input.height),
data = imageData.data,
factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
for (var i=0;i<data.length;i+=4) {
data[i] = factor * (data[i] - 128) + 128;
data[i+1] = factor * (data[i+1] - 128) + 128;
data[i+2] = factor * (data[i+2] - 128) + 128;
}
outputContext = output.getContext("2d");
outputContext.putImageData(imageData, 0, 0);
}
function grayscale (input, output) {
var gray,
outputContext,
inputContext = input.getContext("2d"),
imageData = inputContext.getImageData(0, 0, input.width, input.height),
data = imageData.data,
arraylength = input.width * input.height * 4;
//gray = 0.3*R + 0.59*G + 0.11*B
// http://www.tannerhelland.com/3643/grayscale-image-algorithm-vb6/
for (var i=arraylength-1; i>0;i-=4) {
gray = 0.3 * data[i-3] + 0.59 * data[i-2] + 0.11 * data[i-1];
data[i-3] = gray;
data[i-2] = gray;
data[i-1] = gray;
}
outputContext = output.getContext("2d");
outputContext.putImageData(imageData, 0, 0);
}
function startup(gadget, device_id) {
var media, mediaList = [];
video = gadget.querySelector(".video");
canvas = gadget.querySelector(".canvas");
photo = gadget.querySelector(".photo");
photoInput = gadget.querySelector(".photoInput");
startbutton = gadget.querySelector(".startbutton");
navigator.mediaDevices.getUserMedia({video: {deviceId: {exact: device_id}}})
.then(gotStream)
.then(function(photoCapabilities) {
imageWidth = photoCapabilities.imageWidth.max;
imageHeight = photoCapabilities.imageHeight.max;
document.querySelector("textarea[name='field_your_description']").value = "Max => " + imageWidth + "x" + imageHeight;
video.play();
})
.catch(function(e) {
console.log(e);
});
startbutton.addEventListener("click", function(evt){
evt.preventDefault();
takePicture(gadget);
}, false);
}
function clearphoto() {
var data, context = canvas.getContext("2d");
context.fillRect(0, 0, canvas.width, canvas.height);
data = canvas.toDataURL("image/png");
photo.setAttribute("src", data);
}
function takePicture(gadget) {
imageCapture.takePhoto({imageWidth: imageWidth})
.then(function(blob){
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
photoInput.setAttribute("value", reader.result.split(",")[1]);
photo.setAttribute("src", reader.result);
photo.setAttribute("width", imageWidth);
photo.setAttribute("height", imageHeight);
return drawCanvas(gadget, photo);
};
});
}
function drawCanvas(gadget, img) {
var ratio, x, y, data;
canvas.width = imageWidth;
canvas.height = imageHeight;
ratio = Math.min(canvas.width / img.width, canvas.height / img.height);
x = (canvas.width - img.width * ratio) / 2;
y = (canvas.height - img.height * ratio) / 2;
document.querySelector("textarea[name='field_your_description']").value += "\nImage size " + img.width + "x" + img.height;
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height, x, y, img.width * ratio, img.height * ratio);
//grayscale(canvas, canvas);
//contrastImage(canvas, canvas, 10);
gadget.querySelector(".output").style.display = "";
if (cropper) {
cropper.destroy();
}
cropper = new Cropper(gadget.querySelector('.photo'), {});
gadget.querySelector(".crop-button").addEventListener("click", function(evt) {
var canvasData;
evt.preventDefault();
canvasData = cropper.getCanvasData();
cropper.getCroppedCanvas().toBlob(function(blob){
var reader = new window.FileReader(),
photo = gadget.querySelector(".photo");
reader.readAsDataURL(blob);
reader.onloadend = function () {
var base64data = reader.result,
block = base64data.split(";"),
contentType = block[0].split(":")[1],
realData = block[1].split(",")[1];
photo.style.width = canvasData.width + "px";
photo.style.height = canvasData.height + "px";
photo.src = base64data;
photoInput.value = realData;
cropper.destroy();
};
});
});
}
rJS(window)
.declareMethod('render', function (options) {
var el,
root,
selector;
return this.getElement()
.push(function (element) {
root = element;
selector = element.querySelector("select");
return navigator.mediaDevices.enumerateDevices();
})
.push(function (info_list) {
var j,
device,
len = info_list.length;
for (j = 0; j < len; j += 1) {
device = info_list[j];
if (device.kind === 'videoinput') {
el = document.createElement("option");
el.value = device.deviceId;
el.innerText = device.label;
selector.appendChild(el);
}
}
})
.push(function() {
selector.addEventListener("change", function(evt) {
if (video) {
video.pause();
}
if (evt.target.value) {
return startup(root, evt.target.value);
}
});
});
})
.declareMethod('getContent', function () {
var input = this.element.querySelector('.photoInput'),
result = {};
result.field_your_document_scanner_gadget = input.value;
return result;
});
}(rJS, RSVP, window, document, navigator, Cropper, console, alert, FileReader, URL));
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="File" module="OFS.Image"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>__name__</string> </key>
<value> <string>gadget_document_scanner.js</string> </value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>application/javascript</string> </value>
</item>
<item>
<key> <string>precondition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
erp5_dms
erp5_accounting
\ No newline at end of file
GPL
\ No newline at end of file
erp5_document_scanner
\ No newline at end of file
erp5_document_scanner
\ No newline at end of file
0.1
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment