openstamanager/assets/src/js/wacom/sigCaptDialog/stuCaptDialog.js

542 lines
19 KiB
JavaScript
Executable File

/**
* Capture dialog using a STU device.
**/
class StuCaptDialog {
constructor(config) {
if (config) {
this.config = config;
if (this.config.encryption) {
if (!this.config.encryption.sessionID) {
this.config.encryption.sessionID = 0xc0ffee;
}
}
if (this.config.sizeMode == undefined) {
this.config.sizeMode = "stu";
}
this.config.strokeColor = config.strokeColor ?? "#0202FE";
this.config.strokeSize = config.strokeSize ?? 6;
this.config.showWait = config.showWait ?? true;
this.config.stuDevice = config.stuDevice;
} else {
this.config = {showWait:true, sizeMode:"stu", strokeColor:"#0202FE", strokeSize:6 };
}
this.mPenData = Array();
this.onClearListeners = new Array();
this.onCancelListeners = new Array();
this.onOkListeners = new Array();
}
/**
* Connect to the first STU device found, and open the capture dialog.
* @param {string} - Name of the person who is going to sign.
* @param {string} - Reason for signing.
* @param {IntegrityType} - Hash method to maintain the signature integrity. None by default.
* @param {Hash} - Hash of an attached document. None by default.
* @param {string} - osInfo, string indicating the OS.
* @param {string} - nicInfo.
**/
async open(sigObj, who, why, where, extraData, integrityType, documentHash, osInfo, nicInfo) {
if (!this.config.stuDevice) {
let devices = await com.WacomGSS.STU.UsbDevice.requestDevices();
if (devices.length > 0) {
this.config.stuDevice = devices[0];
} else {
throw "No STU devices found";
}
}
this.mTablet = new com.WacomGSS.STU.Tablet();
this.mTablet.addTabletHandler(this);
if (this.config.encryption) {
if (this.config.encryption.encryptionHandler) {
this.mTablet.setEncryptionHandler(this.config.encryption.encryptionHandler);
}
if (this.config.encryption.encryptionHandler2) {
this.mTablet.setEncryptionHandler2(this.config.encryption.encryptionHandler2);
}
}
try {
await this.mTablet.usbConnect(this.config.stuDevice);
} catch (e) {
alert("STU Device not found");
}
this.mCapability = await this.mTablet.getCapability();
this.mInformation = await this.mTablet.getInformation();
this.mInkThreshold = await this.mTablet.getInkThreshold();
try {
await this.mTablet.setPenDataOptionMode(com.WacomGSS.STU.Protocol.PenDataOptionMode.TimeCountSequence);
} catch (e) {
}
let canvasWidth = 0;
let canvasHeight = 0;
if ((this.config.sizeMode == "fixed") && (this.config.width) && (this.config.height)) {
// fixed takes the values from the parameters
canvasWidth = parseInt(this.config.width);
canvasHeight = parseInt(this.config.height);
} else if (this.config.attachTo) {
const parentWidth = $("#"+this.config.attachTo).width();
const parentHeight = $("#"+this.config.attachTo).height();
const pixelWidth = (96*this.mCapability.tabletMaxX*0.01)/25.4;
const pixelHeight = (96*this.mCapability.tabletMaxY*0.01)/25.4;
if (this.config.sizeMode == "fit") {
canvasWidth = parentWidth;
canvasHeight = parentHeight;
} else if (this.config.sizeMode == "strech") {
if (pixelWidth > pixelHeight) {
canvasWidth = parentWidth;
canvasHeight = (pixelHeight/pixelWidth) * canvasWidth;
} else {
canvasHeight = parentHeight;
canvasWidth = (pixelWidth/pixelHeight)*canvasHeight;
}
} else {
// asume stu dimensions
canvasWidth = pixelWidth;
canvasHeight = pixelHeight;
}
} else {
// stu mode takes the size from the STU device
canvasWidth = (96*this.mCapability.tabletMaxX*0.01)/25.4;
canvasHeight = (96*this.mCapability.tabletMaxY*0.01)/25.4;
}
this.mScaleX = canvasWidth / this.mCapability.tabletMaxX;
this.mScaleY = canvasHeight / this.mCapability.tabletMaxY;
let useColor = true;
let encodingFlag = com.WacomGSS.STU.Protocol.ProtocolHelper.simulateEncodingFlag(this.mTablet.getProductId(), this.mCapability.ecodingFlag);
// Disable color if the bulk driver isn't installed (supportsWrite())
if ((encodingFlag & com.WacomGSS.STU.Protocol.EncodingFlag.EncodingFlag_24bit) != 0) {
this.mEncodingMode = this.mTablet.supportsWrite() ? com.WacomGSS.STU.Protocol.EncodingMode.EncodingMode_24bit_Bulk : com.WacomGSS.STU.Protocol.EncodingMode.EncodingMode_24bit;
} else if ((encodingFlag & com.WacomGSS.STU.Protocol.EncodingFlag.EncodingFlag_16bit) != 0) {
this.mEncodingMode = this.mTablet.supportsWrite() ? com.WacomGSS.STU.Protocol.EncodingMode.EncodingMode_16bit_Bulk : com.WacomGSS.STU.Protocol.EncodingMode.EncodingMode_16bit;
} else {
// assumes 1bit is available
this.mEncodingMode = com.WacomGSS.STU.Protocol.EncodingMode.EncodingMode_1bit;
useColor = false;
}
this.config.width = canvasWidth;
this.config.height = canvasHeight;
this.config.title = this.mInformation.modelName;
//this.config.borderColor = "#cccccc";
this.config.source = {mouse:false, touch:false, pen:false, stu:true},
this.sigCaptDialog = new SigCaptDialog(this.config);
this.sigCaptDialog.getCaptureData = this.getCaptureData.bind(this);
this.sigCaptDialog.addEventListener("clear", this.onClearBtn.bind(this));
this.sigCaptDialog.addEventListener("cancel", this.onCancelBtn.bind(this));
this.sigCaptDialog.addEventListener("ok", this.onOkBtn.bind(this));
await this.sigCaptDialog.open(sigObj, who, why, where, extraData, integrityType, documentHash, osInfo, "", nicInfo);
//store the background image in order for it to be reused when the screen is cleared
let canvas = await this.drawImageToCanvas(this.sigCaptDialog.createScreenImage(useColor));
let ctx = canvas.getContext("2d");
this.mDeviceBackgroundImage = com.WacomGSS.STU.Protocol.ProtocolHelper.resizeAndFlatten(canvas, 0, 0, canvasWidth, canvasHeight,
this.mCapability.screenWidth, this.mCapability.screenHeight, this.mEncodingMode, com.WacomGSS.STU.Protocol.ProtocolHelper.Scale.Stretch, "white", false, false);
if (this.config.encryption) {
if ((this.mTablet.isSupported(com.WacomGSS.STU.Protocol.ReportId.EncryptionStatus)) ||
(await com.WacomGSS.STU.Protocol.ProtocolHelper.supportsEncryption(this.mTablet.getProtocol()))) {
await this.mTablet.startCapture(this.config.encryption.sessionID);
this.mIsEncrypted = true;
}
}
// put color ink
if (useColor) {
let htc = await this.mTablet.getHandwritingThicknessColor();
let components = this.hexToRgb(this.config.strokeColor);
htc.penColor = this.rgb3216(components.r, components.g, components.b);
htc.penThickness = this.config.strokeSize;
await this.mTablet.setHandwritingThicknessColor(htc);
}
const reportCountLengths = this.mTablet.getReportCountLengths();
if (reportCountLengths[com.WacomGSS.STU.Protocol.ReportId.RenderingMode_$LI$()] !== undefined) {
await this.mTablet.setRenderingMode(com.WacomGSS.STU.Protocol.RenderingMode.WILL);
}
// Enable the pen data on the screen (if not already)
await this.mTablet.setInkingMode(com.WacomGSS.STU.Protocol.InkingMode.On);
// Initialize the screen
await this.clearScreen();
}
rgb3216(r, g, b) {
return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3);
}
hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
/**
* Add an event listener
* @param {string} eventType - The type of the listener, can be "clear", "cancel" or "ok".
* @param {function} listener - The function that will handle the event
**/
addEventListener(eventType, listener) {
switch (eventType) {
case "clear" : this.onClearListeners.push(listener); break;
case "cancel" : this.onCancelListeners.push(listener); break;
case "ok" : this.onOkListeners.push(listener); break;
}
}
async drawWait() {
const canvas = document.createElement("canvas");
canvas.width = this.mCapability.screenWidth;
canvas.height = this.mCapability.screenHeight;
const ctx = canvas.getContext("2d");
ctx.font = "60pt Wingdings";
const hourglass = String.fromCharCode(0x36);
const metrics = ctx.measureText(hourglass);
const width = metrics.width;
const height = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
const size = Math.max(width, height);
const lineWidth = 3;
canvas.width = size;
canvas.height = size;
ctx.fillStyle = "white";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.font = "60pt Wingdings";
ctx.fillText(hourglass, (width/2)-lineWidth, ((height+metrics.fontBoundingBoxAscent)/2)-lineWidth);
ctx.strokeStyle = "black";
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.arc(size/2, size/2, (size-lineWidth*2)/2, 0, 2 * Math.PI);
ctx.stroke();
const x = Math.floor(this.mCapability.screenWidth/2 - size/2);
const y = Math.floor(this.mCapability.screenHeight/2 - size/2);
const image = com.WacomGSS.STU.Protocol.ProtocolHelper.resizeAndFlatten(canvas, 0, 0, canvas.width, canvas.height,
canvas.width, canvas.height, this.mEncodingMode,
0, "white", false, false);
const rect = new com.WacomGSS.STU.Protocol.Rectangle(x, y, x+canvas.width-1, y+canvas.height-1);
await this.mTablet.writeImageArea(this.mEncodingMode, rect, image);
}
async clearScreen() {
this.sigCaptDialog.stopCapture();
await this.mTablet.setClearScreen();
if ((this.config.showWait) &&
(this.mTablet.isSupported(com.WacomGSS.STU.Protocol.ReportId.StartImageDataArea_$LI$()))) {
await this.drawWait();
}
await this.mTablet.writeImage(this.mEncodingMode, this.mDeviceBackgroundImage);
this.sigCaptDialog.startCapture();
}
async disconnect() {
// Ensure that you correctly disconnect from the tablet, otherwise you are
// likely to get errors when wanting to connect a second time.
if (this.mTablet != null) {
if (this.mIsEncrypted) {
await this.mTablet.endCapture();
this.mIsEncrypted = false;
}
await this.mTablet.setInkingMode(com.WacomGSS.STU.Protocol.InkingMode.Off);
await this.mTablet.setClearScreen();
await this.mTablet.disconnect();
}
}
tabletToScreen(penData) {
// Screen means LCD screen of the tablet.
return {x:penData.x * this.mScaleX, y:penData.y * this.mScaleY};
}
clear() {
if (this.sigCaptDialog) {
this.sigCaptDialog.clear();
}
}
cancel() {
if (this.sigCaptDialog) {
this.sigCaptDialog.cancel();
}
}
accept() {
if (this.sigCaptDialog) {
this.sigCaptDialog.accept();
}
}
onClearBtn() {
if (this.mPenData.length > 0) {
this.mPenData = new Array();
this.clearScreen();
}
this.sigCaptDialog.clearTimeOnSurface();
}
onCancelBtn() {
this.disconnect();
}
async onOkBtn() {
await this.disconnect();
this.onOkListeners.forEach(listener => listener());
}
onPenDataOption(penData, time) {
this.onPenData(penData, time);
}
onPenDataTimeCountSequence(penData, time) {
this.onPenData(penData, time);
}
onPenDataTimeCountSequenceEncrypted(penData, time) {
this.onPenDataTimeCountSequence(penData, time);
}
onPenDataEncryptedOption(penData, time) {
this.onPenData(penData.penData1, time);
this.onPenData(penData.penData2, time);
}
onPenDataEncrypted(penData, time) {
this.onPenData(penData.penData1, time);
this.onPenData(penData.penData2, time);
}
onPenData(penData, time) {
//console.log(JSON.stringify(penData));
if (!penData.timeCount) {
penData.timeCount = Math.trunc(time)%1000000;
}
const pt = this.tabletToScreen(penData);
const btnIndex = this.sigCaptDialog.getButton(pt);
const isDown = (penData.pressure > this.mInkThreshold.onPressureMark);
if (!this.mIsDown) {
if (isDown) {
// transition to down we save the button pressed
this.mBtnIndex = btnIndex;
if (this.mBtnIndex == -1) {
// We have put the pen down outside a button.
// Treat it as part of the signature.
this.mPenData.push(penData);
var downEvent = new PointerEvent("pointerdown", {
pointerId: 1,
pointerType: "stu",
isPrimary: true,
clientX: pt.x,
clientY: pt.y,
pressure: penData.pressure/this.mCapability.tabletMaxPressure,
buttons: 1
});
//downEvent.timeStamp = penData.timeCount;
this.sigCaptDialog.onDown(downEvent);
}
} else {
// hover point
this.mPenData.push(penData);
}
} else {
if (!isDown) {
// transition to up
if (this.mBtnIndex > -1) {
if (btnIndex == this.mBtnIndex) {
// The pen is over the same button that was pressed
this.sigCaptDialog.clickButton(this.mBtnIndex);
}
} else {
var upEvent = new PointerEvent("pointerup", {
pointerId: 1,
pointerType: "stu",
isPrimary: true,
clientX: pt.x,
clientY: pt.y,
pressure: penData.pressure/this.mCapability.tabletMaxPressure,
buttons: 1
});
//upEvent.timeStamp = penData.timeCount;
this.sigCaptDialog.onUp(upEvent);
this.mPenData.push(penData);
}
} else {
// continue inking
if (this.mBtnIndex == -1) {
var moveEvent = new PointerEvent("pointermove", {
pointerId: 1,
pointerType: "stu",
isPrimary: true,
clientX: pt.x,
clientY: pt.y,
pressure: penData.pressure/this.mCapability.tabletMaxPressure,
buttons: 1
});
//moveEvent.timeStamp = penData.timeCount;
this.sigCaptDialog.onMove(moveEvent);
this.mPenData.push(penData);
}
}
}
this.mIsDown = isDown;
}
/**
* Generate the signature from the raw data.
**/
getCaptureData() {
//Create Stroke Data
let strokeVector = new Module.StrokeVector();
let currentStroke = new Module.PointVector();
let currentStrokeID = 0;
let isDown = true;
let hasDown = false;
for (let index = 0; index < this.mPenData.length; index++) {
if (this.mPenData[index].sw == 0 && !hasDown) {
// the signature starts with the first pen down, so the hover
// points before first down are ingnored.
continue;
}
hasDown = true;
if ((isDown && this.mPenData[index].sw == 0) || (!isDown && this.mPenData[index].sw == 1)) {
isDown = (this.mPenData[index].sw == 1);
//Move the current stroke data into the strokes array
strokeVector.push_back({'points': currentStroke});
currentStroke.delete();
currentStroke = new Module.PointVector();
currentStrokeID++;
}
var point = {
'x': this.mPenData[index].x,
'y': this.mPenData[index].y,
'p': this.mPenData[index].pressure,
't': this.mPenData[index].timeCount,
'azimuth': 0, // STU has no azimuth
'altitude': 0, // STU has no altitude
'twist': 0, // STU has no twist
'is_down': this.mPenData[index].sw,
'stroke_id': currentStrokeID
};
currentStroke.push_back(point);
}
//Create capture area character
var device = {
'device_max_X': this.mCapability.tabletMaxX,
'device_max_Y': this.mCapability.tabletMaxY,
'device_max_P': this.mCapability.tabletMaxPressure,
'device_pixels_per_m_x': 100000,
'device_pixels_per_m_y': 100000,
'device_origin_X': 0,
'device_origin_Y': 1,
'device_unit_pixels': false
}
var uid2;
try {
// getUid2 will throw if pad doesn't support Uid2
uid2 = mTablet.getUid2();
}
catch (e) {
}
if (!uid2) {
uid2 = 0;
}
const digitizerInfo = "STU;'"+this.mInformation.modelName+"';"+this.mInformation.firmwareMajorVersion+"."+((parseInt(this.mInformation.firmwareMinorVersion) >> 4) & 0x0f)+"."+(parseInt(this.mInformation.firmwareMinorVersion) & 0x0f)+";"+uid2;
const timeResolution = 1000;
const myPromise = new Promise((resolve, reject) => {
try {
const promise = this.sigCaptDialog.sigObj.generateSignature(this.sigCaptDialog.signatory, this.sigCaptDialog.reason, this.sigCaptDialog.where, this.sigCaptDialog.integrityType, this.sigCaptDialog.documentHash, strokeVector, device, this.sigCaptDialog.osInfo, digitizerInfo, this.sigCaptDialog.nicInfo, timeResolution);
promise.then((value) => {
if (value) {
// put the extra data
if (this.extraData) {
for (const data of this.extraData) {
this.sigObj.setExtraData(data.name, data.value);
}
}
}
strokeVector.delete();
currentStroke.delete();
resolve(value);
});
promise.catch(error => {
strokeVector.delete();
currentStroke.delete();
reject(error);
});
} catch (exception) {
strokeVector.delete();
currentStroke.delete();
reject(exception);
}
});
return myPromise;
}
drawImageToCanvas(src){
return new Promise((resolve, reject) => {
let img = new Image()
img.onload = () => {
let canvas = document.createElement("canvas");
canvas.height = img.height;
canvas.width = img.width;
let ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
resolve(canvas);
}
img.onerror = reject;
img.src = src;
})
}
}