You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2788 lines
135 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

'use strict';
/**
* @version 2.2.3-SNAPSHOT
* @overview QZ Tray Connector
* <p/>
* Connects a web client to the QZ Tray software.
* Enables printing and device communication from javascript.
*/
var qz = (function () {
///// POLYFILLS /////
if (!Array.isArray) {
Array.isArray = function (arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
if (!Number.isInteger) {
Number.isInteger = function (value) {
return typeof value === 'number' && isFinite(value) && Math.floor(value) === value;
};
}
///// PRIVATE METHODS /////
var _qz = {
VERSION: "2.2.3-SNAPSHOT", //must match @version above
DEBUG: false,
log: {
/** Debugging messages */
trace: function () { if (_qz.DEBUG) { console.log.apply(console, arguments); } },
/** General messages */
info: function () { console.info.apply(console, arguments); },
/** General warnings */
warn: function () { console.warn.apply(console, arguments); },
/** Debugging errors */
allay: function () { if (_qz.DEBUG) { console.warn.apply(console, arguments); } },
/** General errors */
error: function () { console.error.apply(console, arguments); }
},
//stream types
streams: {
serial: 'SERIAL', usb: 'USB', hid: 'HID', printer: 'PRINTER', file: 'FILE', socket: 'SOCKET'
},
websocket: {
/** The actual websocket object managing the connection. */
connection: null,
/** Default parameters used on new connections. Override values using options parameter on {@link qz.websocket.connect}. */
connectConfig: {
host: ["localhost", "localhost.qz.io"], //hosts QZ Tray can be running on
hostIndex: 0, //internal var - index on host array
usingSecure: true, //boolean use of secure protocol
protocol: {
secure: "wss://", //secure websocket
insecure: "ws://" //insecure websocket
},
port: {
secure: [8181, 8282, 8383, 8484], //list of secure ports QZ Tray could be listening on
insecure: [8182, 8283, 8384, 8485], //list of insecure ports QZ Tray could be listening on
portIndex: 0 //internal var - index on active port array
},
keepAlive: 60, //time between pings to keep connection alive, in seconds
retries: 0, //number of times to reconnect before failing
delay: 0 //seconds before firing a connection
},
setup: {
/** Loop through possible ports to open connection, sets web socket calls that will settle the promise. */
findConnection: function (config, resolve, reject) {
//force flag if missing ports
if (!config.port.secure.length) {
if (!config.port.insecure.length) {
reject(new Error("No ports have been specified to connect over"));
return;
} else if (config.usingSecure) {
_qz.log.error("No secure ports specified - forcing insecure connection");
config.usingSecure = false;
}
} else if (!config.port.insecure.length && !config.usingSecure) {
_qz.log.trace("No insecure ports specified - forcing secure connection");
config.usingSecure = true;
}
var deeper = function () {
config.port.portIndex++;
if ((config.usingSecure && config.port.portIndex >= config.port.secure.length)
|| (!config.usingSecure && config.port.portIndex >= config.port.insecure.length)) {
if (config.hostIndex >= config.host.length - 1) {
//give up, all hope is lost
reject(new Error("Unable to establish connection with QZ"));
return;
} else {
config.hostIndex++;
config.port.portIndex = 0;
}
}
// recursive call until connection established or all ports are exhausted
_qz.websocket.setup.findConnection(config, resolve, reject);
};
var address;
if (config.usingSecure) {
address = config.protocol.secure + config.host[config.hostIndex] + ":" + config.port.secure[config.port.portIndex];
} else {
address = config.protocol.insecure + config.host[config.hostIndex] + ":" + config.port.insecure[config.port.portIndex];
}
try {
_qz.log.trace("Attempting connection", address);
_qz.websocket.connection = new _qz.tools.ws(address);
}
catch (err) {
_qz.log.error(err);
deeper();
return;
}
if (_qz.websocket.connection != null) {
_qz.websocket.connection.established = false;
//called on successful connection to qz, begins setup of websocket calls and resolves connect promise after certificate is sent
_qz.websocket.connection.onopen = function (evt) {
if (!_qz.websocket.connection.established) {
_qz.log.trace(evt);
_qz.log.info("Established connection with QZ Tray on " + address);
_qz.websocket.setup.openConnection({ resolve: resolve, reject: reject });
if (config.keepAlive > 0) {
var interval = setInterval(function () {
if (!_qz.tools.isActive() || _qz.websocket.connection.interval !== interval) {
clearInterval(interval);
return;
}
_qz.websocket.connection.send("ping");
}, config.keepAlive * 1000);
_qz.websocket.connection.interval = interval;
}
}
};
//called during websocket close during setup
_qz.websocket.connection.onclose = function () {
// Safari compatibility fix to raise error event
if (_qz.websocket.connection && typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) {
_qz.websocket.connection.onerror();
}
};
//called for errors during setup (such as invalid ports), reject connect promise only if all ports have been tried
_qz.websocket.connection.onerror = function (evt) {
_qz.log.trace(evt);
_qz.websocket.connection = null;
deeper();
};
} else {
reject(new Error("Unable to create a websocket connection"));
}
},
/** Finish setting calls on successful connection, sets web socket calls that won't settle the promise. */
openConnection: function (openPromise) {
_qz.websocket.connection.established = true;
//called when an open connection is closed
_qz.websocket.connection.onclose = function (evt) {
_qz.log.trace(evt);
_qz.websocket.connection = null;
_qz.websocket.callClose(evt);
_qz.log.info("Closed connection with QZ Tray");
for (var uid in _qz.websocket.pendingCalls) {
if (_qz.websocket.pendingCalls.hasOwnProperty(uid)) {
_qz.websocket.pendingCalls[uid].reject(new Error("Connection closed before response received"));
}
}
//if this is set, then an explicit close call was made
if (this.promise != undefined) {
this.promise.resolve();
}
};
//called for any errors with an open connection
_qz.websocket.connection.onerror = function (evt) {
_qz.websocket.callError(evt);
};
//send JSON objects to qz
_qz.websocket.connection.sendData = function (obj) {
_qz.log.trace("Preparing object for websocket", obj);
if (obj.timestamp == undefined) {
obj.timestamp = Date.now();
if (typeof obj.timestamp !== 'number') {
obj.timestamp = new Date().getTime();
}
}
if (obj.promise != undefined) {
obj.uid = _qz.websocket.setup.newUID();
_qz.websocket.pendingCalls[obj.uid] = obj.promise;
}
// track requesting monitor
obj.position = {
x: typeof screen !== 'undefined' ? ((screen.availWidth || screen.width) / 2) + (screen.left || screen.availLeft || 0) : 0,
y: typeof screen !== 'undefined' ? ((screen.availHeight || screen.height) / 2) + (screen.top || screen.availTop || 0) : 0
};
try {
if (obj.call != undefined && obj.signature == undefined && _qz.security.needsSigned(obj.call)) {
var signObj = {
call: obj.call,
params: obj.params,
timestamp: obj.timestamp
};
//make a hashing promise if not already one
var hashing = _qz.tools.hash(_qz.tools.stringify(signObj));
if (!hashing.then) {
hashing = _qz.tools.promise(function (resolve) {
resolve(hashing);
});
}
hashing.then(function (hashed) {
return _qz.security.callSign(hashed);
}).then(function (signature) {
_qz.log.trace("Signature for call", signature);
obj.signature = signature || "";
obj.signAlgorithm = _qz.security.signAlgorithm;
_qz.signContent = undefined;
_qz.websocket.connection.send(_qz.tools.stringify(obj));
});
} else {
_qz.log.trace("Signature for call", obj.signature);
//called for pre-signed content and (unsigned) setup calls
_qz.websocket.connection.send(_qz.tools.stringify(obj));
}
}
catch (err) {
_qz.log.error(err);
if (obj.promise != undefined) {
obj.promise.reject(err);
delete _qz.websocket.pendingCalls[obj.uid];
}
}
};
//receive message from qz
_qz.websocket.connection.onmessage = function (evt) {
var returned = JSON.parse(evt.data);
if (returned.uid == null) {
if (returned.type == null) {
//incorrect response format, likely connected to incompatible qz version
_qz.websocket.connection.close(4003, "Connected to incompatible QZ Tray version");
} else {
//streams (callbacks only, no promises)
switch (returned.type) {
case _qz.streams.serial:
if (!returned.event) {
returned.event = JSON.stringify({ portName: returned.key, output: returned.data });
}
_qz.serial.callSerial(JSON.parse(returned.event));
break;
case _qz.streams.socket:
_qz.socket.callSocket(JSON.parse(returned.event));
break;
case _qz.streams.usb:
if (!returned.event) {
returned.event = JSON.stringify({ vendorId: returned.key[0], productId: returned.key[1], output: returned.data });
}
_qz.usb.callUsb(JSON.parse(returned.event));
break;
case _qz.streams.hid:
_qz.hid.callHid(JSON.parse(returned.event));
break;
case _qz.streams.printer:
_qz.printers.callPrinter(JSON.parse(returned.event));
break;
case _qz.streams.file:
_qz.file.callFile(JSON.parse(returned.event));
break;
default:
_qz.log.allay("Cannot determine stream type for callback", returned);
break;
}
}
return;
}
_qz.log.trace("Received response from websocket", returned);
var promise = _qz.websocket.pendingCalls[returned.uid];
if (promise == undefined) {
_qz.log.allay('No promise found for returned response');
} else {
if (returned.error != undefined) {
promise.reject(new Error(returned.error));
} else {
promise.resolve(returned.result);
}
}
delete _qz.websocket.pendingCalls[returned.uid];
};
//send up the certificate before making any calls
//also gives the user a chance to deny the connection
function sendCert(cert) {
if (cert === undefined) { cert = null; }
//websocket setup, query what version is connected
qz.api.getVersion().then(function (version) {
_qz.websocket.connection.version = version;
_qz.websocket.connection.semver = version.toLowerCase().replace(/-rc\./g, "-rc").split(/[\\+\\.-]/g);
for (var i = 0; i < _qz.websocket.connection.semver.length; i++) {
try {
if (i == 3 && _qz.websocket.connection.semver[i].toLowerCase().indexOf("rc") == 0) {
// Handle "rc1" pre-release by negating build info
_qz.websocket.connection.semver[i] = -(_qz.websocket.connection.semver[i].replace(/\D/g, ""));
continue;
}
_qz.websocket.connection.semver[i] = parseInt(_qz.websocket.connection.semver[i]);
}
catch (ignore) { }
if (_qz.websocket.connection.semver.length < 4) {
_qz.websocket.connection.semver[3] = 0;
}
}
//algorithm can be declared before a connection, check for incompatibilities now that we have one
_qz.compatible.algorithm(true);
}).then(function () {
_qz.websocket.connection.sendData({ certificate: cert, promise: openPromise });
});
}
_qz.security.callCert().then(sendCert).catch(function (error) {
_qz.log.warn("Failed to get certificate:", error);
sendCert(null);
});
},
/** Generate unique ID used to map a response to a call. */
newUID: function () {
var len = 6;
return (new Array(len + 1).join("0") + (Math.random() * Math.pow(36, len) << 0).toString(36)).slice(-len)
}
},
dataPromise: function (callName, params, signature, signingTimestamp) {
return _qz.tools.promise(function (resolve, reject) {
var msg = {
call: callName,
promise: { resolve: resolve, reject: reject },
params: params,
signature: signature,
timestamp: signingTimestamp
};
_qz.websocket.connection.sendData(msg);
});
},
/** Library of promises awaiting a response, uid -> promise */
pendingCalls: {},
/** List of functions to call on error from the websocket. */
errorCallbacks: [],
/** Calls all functions registered to listen for errors. */
callError: function (evt) {
if (Array.isArray(_qz.websocket.errorCallbacks)) {
for (var i = 0; i < _qz.websocket.errorCallbacks.length; i++) {
_qz.websocket.errorCallbacks[i](evt);
}
} else {
_qz.websocket.errorCallbacks(evt);
}
},
/** List of function to call on closing from the websocket. */
closedCallbacks: [],
/** Calls all functions registered to listen for closing. */
callClose: function (evt) {
if (Array.isArray(_qz.websocket.closedCallbacks)) {
for (var i = 0; i < _qz.websocket.closedCallbacks.length; i++) {
_qz.websocket.closedCallbacks[i](evt);
}
} else {
_qz.websocket.closedCallbacks(evt);
}
}
},
printing: {
/** Default options used for new printer configs. Can be overridden using {@link qz.configs.setDefaults}. */
defaultConfig: {
//value purposes are explained in the qz.configs.setDefaults docs
bounds: null,
colorType: 'color',
copies: 1,
density: 0,
duplex: false,
fallbackDensity: null,
interpolation: 'bicubic',
jobName: null,
legacy: false,
margins: 0,
orientation: null,
paperThickness: null,
printerTray: null,
rasterize: false,
rotation: 0,
scaleContent: true,
size: null,
units: 'in',
forceRaw: false,
encoding: null,
spool: null
}
},
serial: {
/** List of functions called when receiving data from serial connection. */
serialCallbacks: [],
/** Calls all functions registered to listen for serial events. */
callSerial: function (streamEvent) {
if (Array.isArray(_qz.serial.serialCallbacks)) {
for (var i = 0; i < _qz.serial.serialCallbacks.length; i++) {
_qz.serial.serialCallbacks[i](streamEvent);
}
} else {
_qz.serial.serialCallbacks(streamEvent);
}
}
},
socket: {
/** List of functions called when receiving data from network socket connection. */
socketCallbacks: [],
/** Calls all functions registered to listen for network socket events. */
callSocket: function (socketEvent) {
if (Array.isArray(_qz.socket.socketCallbacks)) {
for (var i = 0; i < _qz.socket.socketCallbacks.length; i++) {
_qz.socket.socketCallbacks[i](socketEvent);
}
} else {
_qz.socket.socketCallbacks(socketEvent);
}
}
},
usb: {
/** List of functions called when receiving data from usb connection. */
usbCallbacks: [],
/** Calls all functions registered to listen for usb events. */
callUsb: function (streamEvent) {
if (Array.isArray(_qz.usb.usbCallbacks)) {
for (var i = 0; i < _qz.usb.usbCallbacks.length; i++) {
_qz.usb.usbCallbacks[i](streamEvent);
}
} else {
_qz.usb.usbCallbacks(streamEvent);
}
}
},
hid: {
/** List of functions called when receiving data from hid connection. */
hidCallbacks: [],
/** Calls all functions registered to listen for hid events. */
callHid: function (streamEvent) {
if (Array.isArray(_qz.hid.hidCallbacks)) {
for (var i = 0; i < _qz.hid.hidCallbacks.length; i++) {
_qz.hid.hidCallbacks[i](streamEvent);
}
} else {
_qz.hid.hidCallbacks(streamEvent);
}
}
},
printers: {
/** List of functions called when receiving data from printer connection. */
printerCallbacks: [],
/** Calls all functions registered to listen for printer events. */
callPrinter: function (streamEvent) {
if (Array.isArray(_qz.printers.printerCallbacks)) {
for (var i = 0; i < _qz.printers.printerCallbacks.length; i++) {
_qz.printers.printerCallbacks[i](streamEvent);
}
} else {
_qz.printers.printerCallbacks(streamEvent);
}
}
},
file: {
/** List of functions called when receiving info regarding file changes. */
fileCallbacks: [],
/** Calls all functions registered to listen for file events. */
callFile: function (streamEvent) {
if (Array.isArray(_qz.file.fileCallbacks)) {
for (var i = 0; i < _qz.file.fileCallbacks.length; i++) {
_qz.file.fileCallbacks[i](streamEvent);
}
} else {
_qz.file.fileCallbacks(streamEvent);
}
}
},
security: {
/** Function used to resolve promise when acquiring site's public certificate. */
certHandler: function (resolve, reject) { reject(); },
/** Called to create new promise (using {@link _qz.security.certHandler}) for certificate retrieval. */
callCert: function () {
if (typeof _qz.security.certHandler.then === 'function') {
//already a promise
return _qz.security.certHandler;
} else if (_qz.security.certHandler.constructor.name === "AsyncFunction") {
//already callable as a promise
return _qz.security.certHandler();
} else {
//turn into a promise
return _qz.tools.promise(_qz.security.certHandler);
}
},
/** Function used to create promise resolver when requiring signed calls. */
signatureFactory: function () { return function (resolve) { resolve(); } },
/** Called to create new promise (using {@link _qz.security.signatureFactory}) for signed calls. */
callSign: function (toSign) {
if (_qz.security.signatureFactory.constructor.name === "AsyncFunction") {
//use directly
return _qz.security.signatureFactory(toSign);
} else {
//use in a promise
return _qz.tools.promise(_qz.security.signatureFactory(toSign));
}
},
/** Signing algorithm used on signatures */
signAlgorithm: "SHA1",
needsSigned: function (callName) {
const undialoged = [
"printers.getStatus",
"printers.stopListening",
"usb.isClaimed",
"usb.closeStream",
"usb.releaseDevice",
"hid.stopListening",
"hid.isClaimed",
"hid.closeStream",
"hid.releaseDevice",
"file.stopListening",
"getVersion"
];
return callName != null && undialoged.indexOf(callName) === -1;
}
},
tools: {
/** Create a new promise */
promise: function (resolver) {
//prefer global object for historical purposes
if (typeof RSVP !== 'undefined') {
return new RSVP.Promise(resolver);
} else if (typeof Promise !== 'undefined') {
return new Promise(resolver);
} else {
_qz.log.error("Promise/A+ support is required. See qz.api.setPromiseType(...)");
}
},
/** Stub for rejecting with an Error from withing a Promise */
reject: function (error) {
return _qz.tools.promise(function (resolve, reject) {
reject(error);
});
},
stringify: function (object) {
//old versions of prototype affect stringify
var pjson = Array.prototype.toJSON;
delete Array.prototype.toJSON;
function skipKeys(key, value) {
if (key === "promise") {
return undefined;
}
return value;
}
var result = JSON.stringify(object, skipKeys);
if (pjson) {
Array.prototype.toJSON = pjson;
}
return result;
},
hash: function (data) {
//prefer global object for historical purposes
if (typeof Sha256 !== 'undefined') {
return Sha256.hash(data);
} else {
return _qz.SHA.hash(data);
}
},
ws: typeof WebSocket !== 'undefined' ? WebSocket : null,
absolute: function (loc) {
if (typeof window !== 'undefined' && typeof document.createElement === 'function') {
var a = document.createElement("a");
a.href = loc;
return a.href;
} else if (typeof exports === 'object') {
//node.js
require('path').resolve(loc);
}
return loc;
},
relative: function (data) {
for (var i = 0; i < data.length; i++) {
if (data[i].constructor === Object) {
var absolute = false;
if (data[i].data && data[i].data.search && data[i].data.search(/data:image\/\w+;base64,/) === 0) {
//upgrade from old base64 behavior
data[i].flavor = "base64";
data[i].data = data[i].data.replace(/^data:image\/\w+;base64,/, "");
} else if (data[i].flavor) {
//if flavor is known, we can directly check for absolute flavor types
if (["FILE", "XML"].indexOf(data[i].flavor.toUpperCase()) > -1) {
absolute = true;
}
} else if (data[i].format && ["HTML", "IMAGE", "PDF", "FILE", "XML"].indexOf(data[i].format.toUpperCase()) > -1) {
//if flavor is not known, all valid pixel formats default to file flavor
//previous v2.0 data also used format as what is now flavor, so we check for those values here too
absolute = true;
} else if (data[i].type && ((["PIXEL", "IMAGE", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && !data[i].format)
|| (["HTML", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && (!data[i].format || data[i].format.toUpperCase() === "FILE")))) {
//if all we know is pixel type, then it is image's file flavor
//previous v2.0 data also used type as what is now format, so we check for those value here too
absolute = true;
}
if (absolute) {
//change relative links to absolute
data[i].data = _qz.tools.absolute(data[i].data);
}
if (data[i].options && typeof data[i].options.overlay === 'string') {
data[i].options.overlay = _qz.tools.absolute(data[i].options.overlay);
}
}
}
},
/** Performs deep copy to target from remaining params */
extend: function (target) {
//special case when reassigning properties as objects in a deep copy
if (typeof target !== 'object') {
target = {};
}
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
if (!source) { continue; }
for (var key in source) {
if (source.hasOwnProperty(key)) {
if (target === source[key]) { continue; }
if (source[key] && source[key].constructor && source[key].constructor === Object) {
var clone;
if (Array.isArray(source[key])) {
clone = target[key] || [];
} else {
clone = target[key] || {};
}
target[key] = _qz.tools.extend(clone, source[key]);
} else if (source[key] !== undefined) {
target[key] = source[key];
}
}
}
}
return target;
},
versionCompare: function (major, minor, patch, build) {
if (_qz.tools.assertActive()) {
var semver = _qz.websocket.connection.semver;
if (semver[0] != major) {
return semver[0] - major;
}
if (minor != undefined && semver[1] != minor) {
return semver[1] - minor;
}
if (patch != undefined && semver[2] != patch) {
return semver[2] - patch;
}
if (build != undefined && semver.length > 3 && semver[3] != build) {
return Number.isInteger(semver[3]) && Number.isInteger(build) ? semver[3] - build : semver[3].toString().localeCompare(build.toString());
}
return 0;
}
},
isVersion: function (major, minor, patch, build) {
return _qz.tools.versionCompare(major, minor, patch, build) == 0;
},
isActive: function () {
return _qz.websocket.connection != null && _qz.websocket.connection.established;
},
assertActive: function () {
if (_qz.tools.isActive()) {
return true;
}
// Promise won't reject on throw; yet better than 'undefined'
throw new Error("A connection to QZ has not been established yet");
},
uint8ArrayToHex: function (uint8) {
return Array.from(uint8)
.map(function (i) { return i.toString(16).padStart(2, '0'); })
.join('');
},
uint8ArrayToBase64: function (uint8) {
/**
* Adapted from Egor Nepomnyaschih's code under MIT Licence (C) 2020
* see https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
*/
var map = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U",
"V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p",
"q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
];
var result = '', i, l = uint8.length;
for (i = 2; i < l; i += 3) {
result += map[uint8[i - 2] >> 2];
result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += map[((uint8[i - 1] & 0x0F) << 2) | (uint8[i] >> 6)];
result += map[uint8[i] & 0x3F];
}
if (i === l + 1) { // 1 octet yet to write
result += map[uint8[i - 2] >> 2];
result += map[(uint8[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) { // 2 octets yet to write
result += map[uint8[i - 2] >> 2];
result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += map[(uint8[i - 1] & 0x0F) << 2];
result += "=";
}
return result;
},
},
compatible: {
/** Converts message format to a previous version's */
data: function (printData) {
// special handling for Uint8Array
if (printData.constructor === Object && printData.data instanceof Uint8Array) {
if (printData.flavor) {
var flavor = printData.flavor.toString().toUpperCase();
switch (flavor) {
case 'BASE64':
printData.data = _qz.tools.uint8ArrayToBase64(printData.data);
break;
case 'HEX':
printData.data = _qz.tools.uint8ArrayToHex(printData.data);
break;
default:
throw new Error("Uint8Array conversion to '" + flavor + "' is not supported.");
}
}
}
if (_qz.tools.isVersion(2, 0)) {
/*
2.0.x conversion
-----
type=pixel -> use format as 2.0 type (unless 'command' format, which forces 2.0 'raw' type)
type=raw -> 2.0 type has to be 'raw'
if format is 'image' -> force 2.0 'image' format, ignore everything else (unsupported in 2.0)
flavor translates straight to 2.0 format (unless forced to 'raw'/'image')
*/
_qz.log.trace("Converting print data to v2.0 for " + _qz.websocket.connection.version);
for (var i = 0; i < printData.length; i++) {
if (printData[i].constructor === Object) {
if (printData[i].type && printData[i].type.toUpperCase() === "RAW" && printData[i].format && printData[i].format.toUpperCase() === "IMAGE") {
if (printData[i].flavor && printData[i].flavor.toUpperCase() === "BASE64") {
//special case for raw base64 images
printData[i].data = "data:image/compat;base64," + printData[i].data;
}
printData[i].flavor = "IMAGE"; //forces 'image' format when shifting for conversion
}
if ((printData[i].type && printData[i].type.toUpperCase() === "RAW") || (printData[i].format && printData[i].format.toUpperCase() === "COMMAND")) {
printData[i].format = "RAW"; //forces 'raw' type when shifting for conversion
}
printData[i].type = printData[i].format;
printData[i].format = printData[i].flavor;
delete printData[i].flavor;
}
}
}
},
/* Converts config defaults to match previous version */
config: function (config, dirty) {
if (_qz.tools.isVersion(2, 0)) {
if (!dirty.rasterize) {
config.rasterize = true;
}
}
if (_qz.tools.versionCompare(2, 2) < 0) {
if (config.forceRaw !== 'undefined') {
config.altPrinting = config.forceRaw;
delete config.forceRaw;
}
}
if (_qz.tools.versionCompare(2, 1, 2, 11) < 0) {
if (config.spool) {
if (config.spool.size) {
config.perSpool = config.spool.size;
delete config.spool.size;
}
if (config.spool.end) {
config.endOfDoc = config.spool.end;
delete config.spool.end;
}
delete config.spool;
}
}
return config;
},
/** Compat wrapper with previous version **/
networking: function (hostname, port, signature, signingTimestamp, mappingCallback) {
// Use 2.0
if (_qz.tools.isVersion(2, 0)) {
return _qz.tools.promise(function (resolve, reject) {
_qz.websocket.dataPromise('websocket.getNetworkInfo', {
hostname: hostname,
port: port
}, signature, signingTimestamp).then(function (data) {
if (typeof mappingCallback !== 'undefined') {
resolve(mappingCallback(data));
} else {
resolve(data);
}
}, reject);
});
}
// Wrap 2.1
return _qz.tools.promise(function (resolve, reject) {
_qz.websocket.dataPromise('networking.device', {
hostname: hostname,
port: port
}, signature, signingTimestamp).then(function (data) {
resolve({ ipAddress: data.ip, macAddress: data.mac });
}, reject);
});
},
/** Check if QZ version supports chosen algorithm */
algorithm: function (quiet) {
//if not connected yet we will assume compatibility exists for the time being
if (_qz.tools.isActive()) {
if (_qz.tools.isVersion(2, 0)) {
if (!quiet) {
_qz.log.warn("Connected to an older version of QZ, alternate signature algorithms are not supported");
}
return false;
}
}
return true;
}
},
/**
* Adapted from Chris Veness's code under MIT Licence (C) 2002
* see http://www.movable-type.co.uk/scripts/sha256.html
*/
SHA: {
//@formatter:off - keep this block compact
hash: function (msg) {
// add trailing '1' bit (+ 0's padding) to string [§5.1.1]
msg = _qz.SHA._utf8Encode(msg) + String.fromCharCode(0x80);
// constants [§4.2.2]
var K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
// initial hash value [§5.3.1]
var H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
var l = msg.length / 4 + 2; // length (in 32-bit integers) of msg + 1 + appended length
var N = Math.ceil(l / 16); // number of 16-integer-blocks required to hold 'l' ints
var M = new Array(N);
for (var i = 0; i < N; i++) {
M[i] = new Array(16);
for (var j = 0; j < 16; j++) { // encode 4 chars per integer, big-endian encoding
M[i][j] = (msg.charCodeAt(i * 64 + j * 4) << 24) | (msg.charCodeAt(i * 64 + j * 4 + 1) << 16) |
(msg.charCodeAt(i * 64 + j * 4 + 2) << 8) | (msg.charCodeAt(i * 64 + j * 4 + 3));
} // note running off the end of msg is ok 'cos bitwise ops on NaN return 0
}
// add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]
// note: most significant word would be (len-1)*8 >>> 32, but since JS converts
// bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
M[N - 1][14] = ((msg.length - 1) * 8) / Math.pow(2, 32);
M[N - 1][14] = Math.floor(M[N - 1][14]);
M[N - 1][15] = ((msg.length - 1) * 8) & 0xffffffff;
// HASH COMPUTATION [§6.1.2]
var W = new Array(64); var a, b, c, d, e, f, g, h;
for (var i = 0; i < N; i++) {
// 1 - prepare message schedule 'W'
for (var t = 0; t < 16; t++) { W[t] = M[i][t]; }
for (var t = 16; t < 64; t++) { W[t] = (_qz.SHA._dev1(W[t - 2]) + W[t - 7] + _qz.SHA._dev0(W[t - 15]) + W[t - 16]) & 0xffffffff; }
// 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value
a = H[0]; b = H[1]; c = H[2]; d = H[3]; e = H[4]; f = H[5]; g = H[6]; h = H[7];
// 3 - main loop (note 'addition modulo 2^32')
for (var t = 0; t < 64; t++) {
var T1 = h + _qz.SHA._sig1(e) + _qz.SHA._ch(e, f, g) + K[t] + W[t];
var T2 = _qz.SHA._sig0(a) + _qz.SHA._maj(a, b, c);
h = g; g = f; f = e; e = (d + T1) & 0xffffffff;
d = c; c = b; b = a; a = (T1 + T2) & 0xffffffff;
}
// 4 - compute the new intermediate hash value (note 'addition modulo 2^32')
H[0] = (H[0] + a) & 0xffffffff; H[1] = (H[1] + b) & 0xffffffff; H[2] = (H[2] + c) & 0xffffffff; H[3] = (H[3] + d) & 0xffffffff;
H[4] = (H[4] + e) & 0xffffffff; H[5] = (H[5] + f) & 0xffffffff; H[6] = (H[6] + g) & 0xffffffff; H[7] = (H[7] + h) & 0xffffffff;
}
return _qz.SHA._hexStr(H[0]) + _qz.SHA._hexStr(H[1]) + _qz.SHA._hexStr(H[2]) + _qz.SHA._hexStr(H[3]) +
_qz.SHA._hexStr(H[4]) + _qz.SHA._hexStr(H[5]) + _qz.SHA._hexStr(H[6]) + _qz.SHA._hexStr(H[7]);
},
// Rotates right (circular right shift) value x by n positions
_rotr: function (n, x) { return (x >>> n) | (x << (32 - n)); },
// logical functions
_sig0: function (x) { return _qz.SHA._rotr(2, x) ^ _qz.SHA._rotr(13, x) ^ _qz.SHA._rotr(22, x); },
_sig1: function (x) { return _qz.SHA._rotr(6, x) ^ _qz.SHA._rotr(11, x) ^ _qz.SHA._rotr(25, x); },
_dev0: function (x) { return _qz.SHA._rotr(7, x) ^ _qz.SHA._rotr(18, x) ^ (x >>> 3); },
_dev1: function (x) { return _qz.SHA._rotr(17, x) ^ _qz.SHA._rotr(19, x) ^ (x >>> 10); },
_ch: function (x, y, z) { return (x & y) ^ (~x & z); },
_maj: function (x, y, z) { return (x & y) ^ (x & z) ^ (y & z); },
// note can't use toString(16) as it is implementation-dependant, and in IE returns signed numbers when used on full words
_hexStr: function (n) { var s = "", v; for (var i = 7; i >= 0; i--) { v = (n >>> (i * 4)) & 0xf; s += v.toString(16); } return s; },
// implementation of deprecated unescape() based on https://cwestblog.com/2011/05/23/escape-unescape-deprecated/ (and comments)
_unescape: function (str) {
return str.replace(/%(u[\da-f]{4}|[\da-f]{2})/gi, function (seq) {
if (seq.length - 1) {
return String.fromCharCode(parseInt(seq.substring(seq.length - 3 ? 2 : 1), 16))
} else {
var code = seq.charCodeAt(0);
return code < 256 ? "%" + (0 + code.toString(16)).slice(-2).toUpperCase() : "%u" + ("000" + code.toString(16)).slice(-4).toUpperCase()
}
});
},
_utf8Encode: function (str) {
return _qz.SHA._unescape(encodeURIComponent(str));
}
//@formatter:on
},
};
///// CONFIG CLASS ////
/** Object to handle configured printer options. */
function Config(printer, opts) {
this.config = _qz.tools.extend({}, _qz.printing.defaultConfig); //create a copy of the default options
this._dirtyOpts = {}; //track which config options have changed from the defaults
/**
* Set the printer assigned to this config.
* @param {string|Object} newPrinter Name of printer. Use object type to specify printing to file or host.
* @param {string} [newPrinter.name] Name of printer to send printing.
* @param {string} [newPrinter.file] DEPRECATED: Name of file to send printing.
* @param {string} [newPrinter.host] IP address or host name to send printing.
* @param {string} [newPrinter.port] Port used by &lt;printer.host>.
*/
this.setPrinter = function (newPrinter) {
if (typeof newPrinter === 'string') {
newPrinter = { name: newPrinter };
}
if (newPrinter && newPrinter.file) {
// TODO: Warn for UNC paths too https://github.com/qzind/tray/issues/730
if (newPrinter.file.indexOf("\\\\") != 0) {
_qz.log.warn("Printing to file is deprecated. See https://github.com/qzind/tray/issues/730");
}
}
this.printer = newPrinter;
};
/**
* @returns {Object} The printer currently assigned to this config.
*/
this.getPrinter = function () {
return this.printer;
};
/**
* Alter any of the printer options currently applied to this config.
* @param newOpts {Object} The options to change. See <code>qz.configs.setDefaults</code> docs for available values.
*
* @see qz.configs.setDefaults
*/
this.reconfigure = function (newOpts) {
for (var key in newOpts) {
if (newOpts[key] !== undefined) {
this._dirtyOpts[key] = true;
}
}
_qz.tools.extend(this.config, newOpts);
};
/**
* @returns {Object} The currently applied options on this config.
*/
this.getOptions = function () {
return _qz.compatible.config(this.config, this._dirtyOpts);
};
// init calls for new config object
this.setPrinter(printer);
this.reconfigure(opts);
}
/**
* Shortcut method for calling <code>qz.print</code> with a particular config.
* @param {Array<Object|string>} data Array of data being sent to the printer. See <code>qz.print</code> docs for available values.
* @param {boolean} [signature] Pre-signed signature of JSON string containing <code>call</code>, <code>params</code>, and <code>timestamp</code>.
* @param {number} [signingTimestamp] Required with <code>signature</code>. Timestamp used with pre-signed content.
*
* @example
* qz.print(myConfig, ...); // OR
* myConfig.print(...);
*
* @see qz.print
*/
Config.prototype.print = function (data, signature, signingTimestamp) {
qz.print(this, data, signature, signingTimestamp);
};
///// PUBLIC METHODS /////
/** @namespace qz */
var qz = {
/**
* Calls related specifically to the web socket connection.
* @namespace qz.websocket
*/
websocket: {
/**
* Check connection status. Active connection is necessary for other calls to run.
*
* @returns {boolean} If there is an active connection with QZ Tray.
*
* @see connect
*
* @memberof qz.websocket
*/
isActive: function () {
return _qz.tools.isActive();
},
/**
* Call to setup connection with QZ Tray on user's system.
*
* @param {Object} [options] Configuration options for the web socket connection.
* @param {string|Array<string>} [options.host=['localhost', 'localhost.qz.io']] Host running the QZ Tray software.
* @param {Object} [options.port] Config options for ports to cycle.
* @param {Array<number>} [options.port.secure=[8181, 8282, 8383, 8484]] Array of secure (WSS) ports to try
* @param {Array<number>} [options.port.insecure=[8182, 8283, 8384, 8485]] Array of insecure (WS) ports to try
* @param {boolean} [options.usingSecure=true] If the web socket should try to use secure ports for connecting.
* @param {number} [options.keepAlive=60] Seconds between keep-alive pings to keep connection open. Set to 0 to disable.
* @param {number} [options.retries=0] Number of times to reconnect before failing.
* @param {number} [options.delay=0] Seconds before firing a connection. Ignored if <code>options.retries</code> is 0.
*
* @returns {Promise<null|Error>}
*
* @memberof qz.websocket
*/
connect: function (options) {
return _qz.tools.promise(function (resolve, reject) {
if (_qz.tools.isActive()) {
reject(new Error("An open connection with QZ Tray already exists"));
return;
} else if (_qz.websocket.connection != null) {
reject(new Error("The current connection attempt has not returned yet"));
return;
}
if (!_qz.tools.ws) {
reject(new Error("WebSocket not supported by this browser"));
return;
} else if (!_qz.tools.ws.CLOSED || _qz.tools.ws.CLOSED == 2) {
reject(new Error("Unsupported WebSocket version detected: HyBi-00/Hixie-76"));
return;
}
//ensure some form of options exists for value checks
if (options == undefined) { options = {}; }
//disable secure ports if page is not secure
if (typeof location === 'undefined' || location.protocol !== 'https:') {
//respect forcing secure ports if it is defined, otherwise disable
if (typeof options.usingSecure === 'undefined') {
_qz.log.trace("Disabling secure ports due to insecure page");
options.usingSecure = false;
}
}
//ensure any hosts are passed to internals as an array
if (typeof options.host !== 'undefined' && !Array.isArray(options.host)) {
options.host = [options.host];
}
var attempt = function (count) {
var tried = false;
var nextAttempt = function () {
if (!tried) {
tried = true;
if (options && count < options.retries) {
attempt(count + 1);
} else {
_qz.websocket.connection = null;
reject.apply(null, arguments);
}
}
};
var delayed = function () {
var config = _qz.tools.extend({}, _qz.websocket.connectConfig, options);
_qz.websocket.setup.findConnection(config, resolve, nextAttempt)
};
if (count == 0) {
delayed(); // only retries will be called with a delay
} else {
setTimeout(delayed, options.delay * 1000);
}
};
attempt(0);
});
},
/**
* Stop any active connection with QZ Tray.
*
* @returns {Promise<null|Error>}
*
* @memberof qz.websocket
*/
disconnect: function () {
return _qz.tools.promise(function (resolve, reject) {
if (_qz.tools.isActive()) {
_qz.websocket.connection.close();
_qz.websocket.connection.promise = { resolve: resolve, reject: reject };
} else {
reject(new Error("No open connection with QZ Tray"))
}
});
},
/**
* List of functions called for any connections errors outside of an API call.<p/>
* Also called if {@link websocket#connect} fails to connect.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Event} event)</code> calls.
*
* @memberof qz.websocket
*/
setErrorCallbacks: function (calls) {
_qz.websocket.errorCallbacks = calls;
},
/**
* List of functions called for any connection closing event outside of an API call.<p/>
* Also called when {@link websocket#disconnect} is called.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Event} event)</code> calls.
*
* @memberof qz.websocket
*/
setClosedCallbacks: function (calls) {
_qz.websocket.closedCallbacks = calls;
},
/**
* @deprecated Since 2.1.0. Please use qz.networking.device() instead
*
* @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com"
* @param {number} [port] Port to use with custom hostname, defaults to 443
* @param {string} [signature] Pre-signed signature of hashed JSON string containing <code>call='websocket.getNetworkInfo'</code>, <code>params</code> object, and <code>timestamp</code>.
* @param {number} [signingTimestamp] Required with <code>signature</code>. Timestamp used with pre-signed content.
*
* @returns {Promise<Object<{ipAddress: string, macAddress: string}>|Error>} Connected system's network information.
*
* @memberof qz.websocket
*/
getNetworkInfo: _qz.compatible.networking,
/**
* @returns {Object<{socket: String, host: String, port: Number}>} Details of active websocket connection
*
* @memberof qz.websocket
*/
getConnectionInfo: function () {
if (_qz.tools.assertActive()) {
var url = _qz.websocket.connection.url.split(/[:\/]+/g);
return { socket: url[0], host: url[1], port: +url[2] };
}
}
},
/**
* Calls related to getting printer information from the connection.
* @namespace qz.printers
*/
printers: {
/**
* @param {string} [signature] Pre-signed signature of hashed JSON string containing <code>call='printers.getDefault</code>, <code>params</code>, and <code>timestamp</code>.
* @param {number} [signingTimestamp] Required with <code>signature</code>. Timestamp used with pre-signed content.
*
* @returns {Promise<string|Error>} Name of the connected system's default printer.
*
* @memberof qz.printers
*/
getDefault: function (signature, signingTimestamp) {
return _qz.websocket.dataPromise('printers.getDefault', null, signature, signingTimestamp);
},
/**
* @param {string} [query] Search for a specific printer. All printers are returned if not provided.
* @param {string} [signature] Pre-signed signature of hashed JSON string containing <code>call='printers.find'</code>, <code>params</code>, and <code>timestamp</code>.
* @param {number} [signingTimestamp] Required with <code>signature</code>. Timestamp used with pre-signed content.
*
* @returns {Promise<Array<string>|string|Error>} The matched printer name if <code>query</code> is provided.
* Otherwise an array of printer names found on the connected system.
*
* @memberof qz.printers
*/
find: function (query, signature, signingTimestamp) {
return _qz.websocket.dataPromise('printers.find', { query: query }, signature, signingTimestamp);
},
/**
* Provides a list, with additional information, for each printer available to QZ.
*
* @returns {Promise<Array<Object>|Object|Error>}
*
* @memberof qz.printers
*/
details: function () {
return _qz.websocket.dataPromise('printers.detail');
},
/**
* Start listening for printer status events, such as paper_jam events.
* Reported under the ACTION type in the streamEvent on callbacks.
*
* @returns {Promise<null|Error>}
* @since 2.1.0
*
* @see qz.printers.setPrinterCallbacks
*
* @param {null|string|Array<string>} printers Printer or list of printers to listen to, null listens to all.
* @param {Object|null} [options] Printer listener options
* @param {null|boolean} [options.jobData=false] Flag indicating if raw spool file content should be return as well as status information (Windows only)
* @param {null|number} [options.maxJobData=-1] Maximum number of bytes to returns for raw spooled file content (Windows only)
* @param {null|string} [options.flavor="plain"] Flavor of data format returned. Valid flavors are <code>[base64 | hex | plain*]</code> (Windows only)
*
* @memberof qz.printers
*/
startListening: function (printers, options) {
if (!Array.isArray(printers)) {
printers = [printers];
}
var params = {
printerNames: printers
};
if (options && options.jobData == true) params.jobData = true;
if (options && options.maxJobData) params.maxJobData = options.maxJobData;
if (options && options.flavor) params.flavor = options.flavor;
return _qz.websocket.dataPromise('printers.startListening', params);
},
/**
* Stop listening for printer status actions.
*
* @returns {Promise<null|Error>}
* @since 2.1.0
*
* @see qz.printers.setPrinterCallbacks
*
* @memberof qz.printers
*/
stopListening: function () {
return _qz.websocket.dataPromise('printers.stopListening');
},
/**
* Retrieve current printer status from any active listeners.
*
* @returns {Promise<null|Error>}
* @since 2.1.0
*
* @see qz.printers.startListening
*
* @memberof qz.printers
*/
getStatus: function () {
return _qz.websocket.dataPromise('printers.getStatus');
},
/**
* List of functions called for any printer status change.
* Event data will contain <code>{string} printerName</code> and <code>{string} status</code> for all types.
* For RECEIVE types, <code>{Array} output</code> (in hexadecimal format).
* For ERROR types, <code>{string} exception</code>.
* For ACTION types, <code>{string} actionType</code>.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Object} eventData)</code> calls.
* @since 2.1.0
*
* @memberof qz.printers
*/
setPrinterCallbacks: function (calls) {
_qz.printers.printerCallbacks = calls;
}
},
/**
* Calls related to setting up new printer configurations.
* @namespace qz.configs
*/
configs: {
/**
* Default options used by new configs if not overridden.
* Setting a value to NULL will use the printer's default options.
* Updating these will not update the options on any created config.
*
* @param {Object} options Default options used by printer configs if not overridden.
*
* @param {Object} [options.bounds=null] Bounding box rectangle.
* @param {number} [options.bounds.x=0] Distance from left for bounding box starting corner
* @param {number} [options.bounds.y=0] Distance from top for bounding box starting corner
* @param {number} [options.bounds.width=0] Width of bounding box
* @param {number} [options.bounds.height=0] Height of bounding box
* @param {string} [options.colorType='color'] Valid values <code>[color | grayscale | blackwhite]</code>
* @param {number} [options.copies=1] Number of copies to be printed.
* @param {number|Array<number>|Object|Array<Object>|string} [options.density=0] Pixel density (DPI, DPMM, or DPCM depending on <code>[options.units]</code>).
* If provided as an array, uses the first supported density found (or the first entry if none found).
* If provided as a string, valid values are <code>[best | draft]</code>, corresponding to highest or lowest reported density respectively.
* @param {number} [options.density.cross=0] Asymmetric pixel density for the cross feed direction.
* @param {number} [options.density.feed=0] Asymmetric pixel density for the feed direction.
* @param {boolean|string} [options.duplex=false] Double sided printing, Can specify duplex style by passing a string value: <code>[one-sided | duplex | long-edge | tumble | short-edge]</code>
* @param {number} [options.fallbackDensity=null] Value used when default density value cannot be read, or in cases where reported as "Normal" by the driver, (in DPI, DPMM, or DPCM depending on <code>[options.units]</code>).
* @param {string} [options.interpolation='bicubic'] Valid values <code>[bicubic | bilinear | nearest-neighbor]</code>. Controls how images are handled when resized.
* @param {string} [options.jobName=null] Name to display in print queue.
* @param {boolean} [options.legacy=false] If legacy style printing should be used.
* @param {Object|number} [options.margins=0] If just a number is provided, it is used as the margin for all sides.
* @param {number} [options.margins.top=0]
* @param {number} [options.margins.right=0]
* @param {number} [options.margins.bottom=0]
* @param {number} [options.margins.left=0]
* @param {string} [options.orientation=null] Valid values <code>[portrait | landscape | reverse-landscape | null]</code>.
* If set to <code>null</code>, orientation will be determined automatically.
* @param {number} [options.paperThickness=null]
* @param {string|number} [options.printerTray=null] Printer tray to pull from. The number N assumes string equivalent of 'Tray N'. Uses printer default if NULL.
* @param {boolean} [options.rasterize=false] Whether documents should be rasterized before printing.
* Specifying <code>[options.density]</code> for PDF print formats will set this to <code>true</code>.
* @param {number} [options.rotation=0] Image rotation in degrees.
* @param {boolean} [options.scaleContent=true] Scales print content to page size, keeping ratio.
* @param {Object} [options.size=null] Paper size.
* @param {number} [options.size.width=null] Page width.
* @param {number} [options.size.height=null] Page height.
* @param {string} [options.units='in'] Page units, applies to paper size, margins, and density. Valid value <code>[in | cm | mm]</code>
*
* @param {boolean} [options.forceRaw=false] Print the specified raw data using direct method, skipping the driver. Not yet supported on Windows.
* @param {string|Object} [options.encoding=null] Character set for commands. Can be provided as an object for converting encoding types for RAW types.
* @param {string} [options.encoding.from] If this encoding type is provided, RAW type commands will be parsed from this for the purpose of being converted to the <code>encoding.to</code> value.
* @param {string} [options.encoding.to] Encoding RAW type commands will be converted into. If <Code>encoding.from</code> is not provided, this will be treated as if a string was passed for encoding.
* @param {string} [options.endOfDoc=null] DEPRECATED Raw only: Character(s) denoting end of a page to control spooling.
* @param {number} [options.perSpool=1] DEPRECATED: Raw only: Number of pages per spool.
* @param {boolean} [options.retainTemp=false] Retain any temporary files used. Ignored unless <code>forceRaw</code> <code>true</code>.
* @param {Object} [options.spool=null] Advanced spooling options.
* @param {number} [options.spool.size=null] Number of pages per spool. Default is no limit. If <code>spool.end</code> is provided, defaults to <code>1</code>
* @param {string} [options.spool.end=null] Raw only: Character(s) denoting end of a page to control spooling.
*
* @memberof qz.configs
*/
setDefaults: function (options) {
_qz.tools.extend(_qz.printing.defaultConfig, options);
},
/**
* Creates new printer config to be used in printing.
*
* @param {string|object} printer Name of printer. Use object type to specify printing to file or host.
* @param {string} [printer.name] Name of printer to send printing.
* @param {string} [printer.file] Name of file to send printing.
* @param {string} [printer.host] IP address or host name to send printing.
* @param {string} [printer.port] Port used by &lt;printer.host>.
* @param {Object} [options] Override any of the default options for this config only.
*
* @returns {Config} The new config.
*
* @see configs.setDefaults
*
* @memberof qz.configs
*/
create: function (printer, options) {
return new Config(printer, options);
}
},
/**
* Send data to selected config for printing.
* The promise for this method will resolve when the document has been sent to the printer. Actual printing may not be complete.
* <p/>
* Optionally, print requests can be pre-signed:
* Signed content consists of a JSON object string containing no spacing,
* following the format of the "call" and "params" keys in the API call, with the addition of a "timestamp" key in milliseconds
* ex. <code>'{"call":"<callName>","params":{...},"timestamp":1450000000}'</code>
*
* @param {Object<Config>|Array<Object<Config>>} configs Previously created config object or objects.
* @param {Array<Object|string>|Array<Array<Object|string>>} data Array of data being sent to the printer.<br/>
* String values are interpreted as <code>{type: 'raw', format: 'command', flavor: 'plain', data: &lt;string>}</code>.
* @param {string} data.data
* @param {string} data.type Printing type. Valid types are <code>[pixel | raw*]</code>. *Default
* @param {string} data.format Format of data type used. *Default per type<p/>
* For <code>[pixel]</code> types, valid formats are <code>[html | image* | pdf]</code>.<p/>
* For <code>[raw]</code> types, valid formats are <code>[command* | html | image | pdf]</code>.
* @param {string} data.flavor Flavor of data format used. *Default per format<p/>
* For <code>[command]</code> formats, valid flavors are <code>[base64 | file | hex | plain* | xml]</code>.<p/>
* For <code>[html]</code> formats, valid flavors are <code>[file* | plain]</code>.<p/>
* For <code>[image]</code> formats, valid flavors are <code>[base64 | file*]</code>.<p/>
* For <code>[pdf]</code> formats, valid flavors are <code>[base64 | file*]</code>.
* @param {Object} [data.options]
* @param {string} [data.options.language] Required with <code>[raw]</code> type + <code>[image]</code> format. Printer language.
* @param {number} [data.options.x] Optional with <code>[raw]</code> type + <code>[image]</code> format. The X position of the image.
* @param {number} [data.options.y] Optional with <code>[raw]</code> type + <code>[image]</code> format. The Y position of the image.
* @param {string|number} [data.options.dotDensity] Optional with <code>[raw]</code> type + <code>[image]</code> format.
* @param {number} [data.precision=128] Optional with <code>[raw]</code> type <code>[image]</code> format. Bit precision of the ribbons.
* @param {boolean|string|Array<Array<number>>} [data.options.overlay=false] Optional with <code>[raw]</code> type <code>[image]</code> format.
* Boolean sets entire layer, string sets mask image, Array sets array of rectangles in format <code>[x1,y1,x2,y2]</code>.
* @param {string} [data.options.xmlTag] Required with <code>[xml]</code> flavor. Tag name containing base64 formatted data.
* @param {number} [data.options.pageWidth] Optional with <code>[html | pdf]</code> formats. Width of the rendering.
* Defaults to paper width.
* @param {number} [data.options.pageHeight] Optional with <code>[html | pdf]</code> formats. Height of the rendering.
* Defaults to paper height for <code>[pdf]</code>, or auto sized for <code>[html]</code>.
* @param {string} [data.options.pageRanges] Optional with <code>[pdf]</code> formats. Comma-separated list of page ranges to include.
* @param {boolean} [data.options.ignoreTransparency=false] Optional with <code>[pdf]</code> formats. Instructs transparent PDF elements to be ignored.
* Transparent PDF elements are known to degrade performance and quality when printing.
* @param {boolean} [data.options.altFontRendering=false] Optional with <code>[pdf]</code> formats. Instructs PDF to be rendered using PDFBOX 1.8 techniques.
* Drastically improves low-DPI PDF print quality on Windows.
* @param {...*} [arguments] Additionally three more parameters can be specified:<p/>
* <code>{boolean} [resumeOnError=false]</code> Whether the chain should continue printing if it hits an error on one the the prints.<p/>
* <code>{string|Array<string>} [signature]</code> Pre-signed signature(s) of the JSON string for containing <code>call</code>, <code>params</code>, and <code>timestamp</code>.<p/>
* <code>{number|Array<number>} [signingTimestamps]</code> Required to match with <code>signature</code>. Timestamps for each of the passed pre-signed content.
*
* @returns {Promise<null|Error>}
*
* @see qz.configs.create
*
* @memberof qz
*/
print: function (configs, data) {
var resumeOnError = false,
signatures = [],
signaturesTimestamps = [];
//find optional parameters
if (arguments.length >= 3) {
if (typeof arguments[2] === 'boolean') {
resumeOnError = arguments[2];
if (arguments.length >= 5) {
signatures = arguments[3];
signaturesTimestamps = arguments[4];
}
} else if (arguments.length >= 4) {
signatures = arguments[2];
signaturesTimestamps = arguments[3];
}
//ensure values are arrays for consistency
if (signatures && !Array.isArray(signatures)) { signatures = [signatures]; }
if (signaturesTimestamps && !Array.isArray(signaturesTimestamps)) { signaturesTimestamps = [signaturesTimestamps]; }
}
if (!Array.isArray(configs)) { configs = [configs]; } //single config -> array of configs
if (!Array.isArray(data[0])) { data = [data]; } //single data array -> array of data arrays
//clean up data formatting
for (var d = 0; d < data.length; d++) {
_qz.tools.relative(data[d]);
_qz.compatible.data(data[d]);
}
var sendToPrint = function (mapping) {
var params = {
printer: mapping.config.getPrinter(),
options: mapping.config.getOptions(),
data: mapping.data
};
return _qz.websocket.dataPromise('print', params, mapping.signature, mapping.timestamp);
};
//chain instead of Promise.all, so resumeOnError can collect each error
var chain = [];
for (var i = 0; i < configs.length || i < data.length; i++) {
(function (i_) {
var map = {
config: configs[Math.min(i_, configs.length - 1)],
data: data[Math.min(i_, data.length - 1)],
signature: signatures[i_],
timestamp: signaturesTimestamps[i_]
};
chain.push(function () { return sendToPrint(map) });
})(i);
}
//setup to catch errors if needed
var fallThrough = null;
if (resumeOnError) {
var fallen = [];
fallThrough = function (err) { fallen.push(err); };
//final promise to reject any errors as a group
chain.push(function () {
return _qz.tools.promise(function (resolve, reject) {
fallen.length ? reject(fallen) : resolve();
});
});
}
var last = null;
chain.reduce(function (sequence, link) {
last = sequence.catch(fallThrough).then(link); //catch is ignored if fallThrough is null
return last;
}, _qz.tools.promise(function (r) { r(); })); //an immediately resolved promise to start off the chain
//return last promise so users can chain off final action or catch when stopping on error
return last;
},
/**
* Calls related to interaction with serial ports.
* @namespace qz.serial
*/
serial: {
/**
* @returns {Promise<Array<string>|Error>} Communication (RS232, COM, TTY) ports available on connected system.
*
* @memberof qz.serial
*/
findPorts: function () {
return _qz.websocket.dataPromise('serial.findPorts');
},
/**
* List of functions called for any response from open serial ports.
* Event data will contain <code>{string} portName</code> for all types.
* For RECEIVE types, <code>{string} output</code>.
* For ERROR types, <code>{string} exception</code>.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({object} streamEvent)</code> calls.
*
* @memberof qz.serial
*/
setSerialCallbacks: function (calls) {
_qz.serial.serialCallbacks = calls;
},
/**
* Opens a serial port for sending and receiving data
*
* @param {string} port Name of serial port to open.
* @param {Object} [options] Serial port configurations.
* @param {number} [options.baudRate=9600] Serial port speed. Set to 0 for auto negotiation.
* @param {number} [options.dataBits=8] Serial port data bits. Set to 0 for auto negotiation.
* @param {number} [options.stopBits=1] Serial port stop bits. Set to 0 for auto negotiation.
* @param {string} [options.parity='NONE'] Serial port parity. Set to AUTO for auto negotiation. Valid values <code>[NONE | EVEN | ODD | MARK | SPACE | AUTO]</code>
* @param {string} [options.flowControl='NONE'] Serial port flow control. Set to AUTO for auto negotiation. Valid values <code>[NONE | XONXOFF | XONXOFF_OUT | XONXOFF_IN | RTSCTS | RTSCTS_OUT | RTSCTS_IN | AUTO]</code>
* @param {string} [options.encoding='UTF-8'] Character set for communications.
* @param {string} [options.start=0x0002] DEPRECATED: Legacy character denoting start of serial response. Use <code>options.rx.start</code> instead.
* @param {string} [options.end=0x000D] DEPRECATED: Legacy character denoting end of serial response. Use <code>options.rx.end</code> instead.
* @param {number} [options.width] DEPRECATED: Legacy use for fixed-width response serial communication. Use <code>options.rx.width</code> instead.
* @param {Object} [options.rx] Serial communications response definitions. If an object is passed but no options are defined, all response data will be sent back as it is received unprocessed.
* @param {string|Array<string>} [options.rx.start] Character(s) denoting start of response bytes. Used in conjunction with `end`, `width`, or `lengthbit` property.
* @param {string} [options.rx.end] Character denoting end of response bytes. Used in conjunction with `start` property.
* @param {number} [options.rx.width] Fixed width size of response bytes (not including header if `start` is set). Used alone or in conjunction with `start` property.
* @param {boolean} [options.rx.untilNewline] Returns data between newline characters (`\n` or `\r`) Truncates empty responses. Overrides `start`, `end`, `width`.
* @param {number|Object} [options.rx.lengthBytes] If a number is passed it is treated as the length index. Other values are left as their defaults.
* @param {number} [options.rx.lengthBytes.index=0] Position of the response byte (not including response `start` bytes) used to denote the length of the remaining response data.
* @param {number} [options.rx.lengthBytes.length=1] Length of response length bytes after response header.
* @param {string} [options.rx.lengthBytes.endian='BIG'] Byte endian for multi-byte length values. Valid values <code>[BIG | LITTLE]</code>
* @param {number|Object} [options.rx.crcBytes] If a number is passed it is treated as the crc length. Other values are left as their defaults.
* @param {number} [options.rx.crcBytes.index=0] Position after the response data (not including length or data bytes) used to denote the crc.
* @param {number} [options.rx.crcBytes.length=1] Length of response crc bytes after the response data length.
* @param {boolean} [options.rx.includeHeader=false] Whether any of the header bytes (`start` bytes and any length bytes) should be included in the processed response.
* @param {string} [options.rx.encoding] Override the encoding used for response data. Uses the same value as <code>options.encoding</code> otherwise.
*
* @returns {Promise<null|Error>}
*
* @memberof qz.serial
*/
openPort: function (port, options) {
var params = {
port: port,
options: options
};
return _qz.websocket.dataPromise('serial.openPort', params);
},
/**
* Send commands over a serial port.
* Any responses from the device will be sent to serial callback functions.
*
* @param {string} port An open serial port to send data.
* @param {string|Array<string>|Object} data Data to be sent to the serial device.
* @param {string} [data.type='PLAIN'] Valid values <code>[FILE | PLAIN | HEX | BASE64]</code>
* @param {string|Array<string>} data.data Data to be sent to the serial device.
* @param {Object} options Serial port configuration updates. See <code>qz.serial.openPort</code> `options` docs for available values.
* For best performance, it is recommended to only set these values on the port open call.
*
* @returns {Promise<null|Error>}
*
* @see qz.serial.setSerialCallbacks
*
* @memberof qz.serial
*/
sendData: function (port, data, options) {
if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) {
if (typeof data !== 'object') {
data = {
data: data,
type: "PLAIN"
}
}
if (data.type && data.type.toUpperCase() == "FILE") {
data.data = _qz.tools.absolute(data.data);
}
}
var params = {
port: port,
data: data,
options: options
};
return _qz.websocket.dataPromise('serial.sendData', params);
},
/**
* @param {string} port Name of port to close.
*
* @returns {Promise<null|Error>}
*
* @memberof qz.serial
*/
closePort: function (port) {
return _qz.websocket.dataPromise('serial.closePort', { port: port });
}
},
/**
* Calls related to interaction with communication sockets.
* @namespace qz.socket
*/
socket: {
/**
* Opens a network port for sending and receiving data.
*
* @param {string} host The connection hostname.
* @param {number} port The connection port number.
* @param {Object} [options] Network socket configuration.
* @param {string} [options.encoding='UTF-8'] Character set for communications.
*
* @memberof qz.socket
*/
open: function (host, port, options) {
var params = {
host: host,
port: port,
options: options
};
return _qz.websocket.dataPromise("socket.open", params);
},
/**
* @param {string} host The connection hostname.
* @param {number} port The connection port number.
*
* @memberof qz.socket
*/
close: function (host, port) {
var params = {
host: host,
port: port
};
return _qz.websocket.dataPromise("socket.close", params);
},
/**
* Send data over an open socket.
*
* @param {string} host The connection hostname.
* @param {number} port The connection port number.
* @param {string|Object} data Data to be sent over the port.
* @param {string} [data.type='PLAIN'] Valid values <code>[PLAIN]</code>
* @param {string} data.data Data to be sent over the port.
*
* @memberof qz.socket
*/
sendData: function (host, port, data) {
if (typeof data !== 'object') {
data = {
data: data,
type: "PLAIN"
};
}
var params = {
host: host,
port: port,
data: data
};
return _qz.websocket.dataPromise("socket.sendData", params);
},
/**
* List of functions called for any response from open network sockets.
* Event data will contain <code>{string} host</code> and <code>{number} port</code> for all types.
* For RECEIVE types, <code>{string} response</code>.
* For ERROR types, <code>{string} exception</code>.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Object} eventData)</code> calls.
*
* @memberof qz.socket
*/
setSocketCallbacks: function (calls) {
_qz.socket.socketCallbacks = calls;
}
},
/**
* Calls related to interaction with USB devices.
* @namespace qz.usb
*/
usb: {
/**
* List of available USB devices. Includes (hexadecimal) vendor ID, (hexadecimal) product ID, and hub status.
* If supported, also returns manufacturer and product descriptions.
*
* @param includeHubs Whether to include USB hubs.
* @returns {Promise<Array<Object>|Error>} Array of JSON objects containing information on connected USB devices.
*
* @memberof qz.usb
*/
listDevices: function (includeHubs) {
return _qz.websocket.dataPromise('usb.listDevices', { includeHubs: includeHubs });
},
/**
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @returns {Promise<Array<string>|Error>} List of available (hexadecimal) interfaces on a USB device.
*
* @memberof qz.usb
*/
listInterfaces: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('usb.listInterfaces', deviceInfo);
},
/**
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @param deviceInfo.iface Hex string of interface on the USB device to search.
* @returns {Promise<Array<string>|Error>} List of available (hexadecimal) endpoints on a USB device's interface.
*
* @memberof qz.usb
*/
listEndpoints: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
interface: arguments[2]
};
}
return _qz.websocket.dataPromise('usb.listEndpoints', deviceInfo);
},
/**
* List of functions called for any response from open usb devices.
* Event data will contain <code>{string} vendorId</code> and <code>{string} productId</code> for all types.
* For RECEIVE types, <code>{Array} output</code> (in hexadecimal format).
* For ERROR types, <code>{string} exception</code>.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Object} eventData)</code> calls.
*
* @memberof qz.usb
*/
setUsbCallbacks: function (calls) {
_qz.usb.usbCallbacks = calls;
},
/**
* Claim a USB device's interface to enable sending/reading data across an endpoint.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @param deviceInfo.interface Hex string of interface on the USB device to claim.
* @returns {Promise<null|Error>}
*
* @memberof qz.usb
*/
claimDevice: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
interface: arguments[2]
};
}
return _qz.websocket.dataPromise('usb.claimDevice', deviceInfo);
},
/**
* Check the current claim state of a USB device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @returns {Promise<boolean|Error>}
*
* @since 2.0.2
* @memberOf qz.usb
*/
isClaimed: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('usb.isClaimed', deviceInfo);
},
/**
* Send data to a claimed USB device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device.
* @param deviceInfo.data Bytes to send over specified endpoint.
* @param {string} [deviceInfo.type='PLAIN'] Valid values <code>[FILE | PLAIN | HEX | BASE64]</code>
* @returns {Promise<null|Error>}
*
* @memberof qz.usb
*/
sendData: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
endpoint: arguments[2],
data: arguments[3]
};
}
if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) {
if (typeof deviceInfo.data !== 'object') {
deviceInfo.data = {
data: deviceInfo.data,
type: "PLAIN"
}
}
if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") {
deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data);
}
}
return _qz.websocket.dataPromise('usb.sendData', deviceInfo);
},
/**
* Read data from a claimed USB device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device.
* @param deviceInfo.responseSize Size of the byte array to receive a response in.
* @returns {Promise<Array<string>|Error>} List of (hexadecimal) bytes received from the USB device.
*
* @memberof qz.usb
*/
readData: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
endpoint: arguments[2],
responseSize: arguments[3]
};
}
return _qz.websocket.dataPromise('usb.readData', deviceInfo);
},
/**
* Provides a continuous stream of read data from a claimed USB device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device.
* @param deviceInfo.responseSize Size of the byte array to receive a response in.
* @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds.
* @returns {Promise<null|Error>}
*
* @see qz.usb.setUsbCallbacks
*
* @memberof qz.usb
*/
openStream: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
endpoint: arguments[2],
responseSize: arguments[3],
interval: arguments[4]
};
}
return _qz.websocket.dataPromise('usb.openStream', deviceInfo);
},
/**
* Stops the stream of read data from a claimed USB device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device.
* @returns {Promise<null|Error>}
*
* @memberof qz.usb
*/
closeStream: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
endpoint: arguments[2]
};
}
return _qz.websocket.dataPromise('usb.closeStream', deviceInfo);
},
/**
* Release a claimed USB device to free resources after sending/reading data.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of USB device's vendor ID.
* @param deviceInfo.productId Hex string of USB device's product ID.
* @returns {Promise<null|Error>}
*
* @memberof qz.usb
*/
releaseDevice: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('usb.releaseDevice', deviceInfo);
}
},
/**
* Calls related to interaction with HID USB devices<br/>
* Many of these calls can be accomplished from the <code>qz.usb</code> namespace,
* but HID allows for simpler interaction
* @namespace qz.hid
* @since 2.0.1
*/
hid: {
/**
* List of available HID devices. Includes (hexadecimal) vendor ID and (hexadecimal) product ID.
* If available, also returns manufacturer and product descriptions.
*
* @returns {Promise<Array<Object>|Error>} Array of JSON objects containing information on connected HID devices.
* @since 2.0.1
*
* @memberof qz.hid
*/
listDevices: function () {
return _qz.websocket.dataPromise('hid.listDevices');
},
/**
* Start listening for HID device actions, such as attach / detach events.
* Reported under the ACTION type in the streamEvent on callbacks.
*
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @see qz.hid.setHidCallbacks
*
* @memberof qz.hid
*/
startListening: function () {
return _qz.websocket.dataPromise('hid.startListening');
},
/**
* Stop listening for HID device actions.
*
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @see qz.hid.setHidCallbacks
*
* @memberof qz.hid
*/
stopListening: function () {
return _qz.websocket.dataPromise('hid.stopListening');
},
/**
* List of functions called for any response from open usb devices.
* Event data will contain <code>{string} vendorId</code> and <code>{string} productId</code> for all types.
* For RECEIVE types, <code>{Array} output</code> (in hexadecimal format).
* For ERROR types, <code>{string} exception</code>.
* For ACTION types, <code>{string} actionType</code>.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Object} eventData)</code> calls.
* @since 2.0.1
*
* @memberof qz.hid
*/
setHidCallbacks: function (calls) {
_qz.hid.hidCallbacks = calls;
},
/**
* Claim a HID device to enable sending/reading data across.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @memberof qz.hid
*/
claimDevice: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('hid.claimDevice', deviceInfo);
},
/**
* Check the current claim state of a HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @returns {Promise<boolean|Error>}
*
* @since 2.0.2
* @memberOf qz.hid
*/
isClaimed: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('hid.isClaimed', deviceInfo);
},
/**
* Send data to a claimed HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @param deviceInfo.data Bytes to send over specified endpoint.
* @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID.
* Must be 0x00 for devices only supporting a single report.
* @param deviceInfo.reportId=0x00 Alias for <code>deviceInfo.endpoint</code>. Not used if endpoint is provided.
* @param {string} [deviceInfo.type='PLAIN'] Valid values <code>[FILE | PLAIN | HEX | BASE64]</code>
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @memberof qz.hid
*/
sendData: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
data: arguments[2],
endpoint: arguments[3]
};
}
if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) {
if (typeof deviceInfo.data !== 'object') {
deviceInfo.data = {
data: deviceInfo.data,
type: "PLAIN"
}
}
if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") {
deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data);
}
} else {
if (typeof deviceInfo.data === 'object') {
if (deviceInfo.data.type.toUpperCase() !== "PLAIN"
|| typeof deviceInfo.data.data !== "string") {
return _qz.tools.reject(new Error("Data format is not supported with connected QZ Tray version " + _qz.websocket.connection.version));
}
deviceInfo.data = deviceInfo.data.data;
}
}
return _qz.websocket.dataPromise('hid.sendData', deviceInfo);
},
/**
* Read data from a claimed HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @param deviceInfo.responseSize Size of the byte array to receive a response in.
* @returns {Promise<Array<string>|Error>} List of (hexadecimal) bytes received from the HID device.
* @since 2.0.1
*
* @memberof qz.hid
*/
readData: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
responseSize: arguments[2]
};
}
return _qz.websocket.dataPromise('hid.readData', deviceInfo);
},
/**
* Send a feature report to a claimed HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @param deviceInfo.data Bytes to send over specified endpoint.
* @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID.
* Must be 0x00 for devices only supporting a single report.
* @param deviceInfo.reportId=0x00 Alias for <code>deviceInfo.endpoint</code>. Not used if endpoint is provided.
* @param {string} [deviceInfo.type='PLAIN'] Valid values <code>[FILE | PLAIN | HEX | BASE64]</code>
* @returns {Promise<null|Error>}
*
* @memberof qz.hid
*/
sendFeatureReport: function (deviceInfo) {
return _qz.websocket.dataPromise('hid.sendFeatureReport', deviceInfo);
},
/**
* Get a feature report from a claimed HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @param deviceInfo.responseSize Size of the byte array to receive a response in.
* @returns {Promise<Array<string>|Error>} List of (hexadecimal) bytes received from the HID device.
*
* @memberof qz.hid
*/
getFeatureReport: function (deviceInfo) {
return _qz.websocket.dataPromise('hid.getFeatureReport', deviceInfo);
},
/**
* Provides a continuous stream of read data from a claimed HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @param deviceInfo.responseSize Size of the byte array to receive a response in.
* @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds.
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @see qz.hid.setHidCallbacks
*
* @memberof qz.hid
*/
openStream: function (deviceInfo) {
//backwards compatibility
if (typeof deviceInfo !== 'object') {
deviceInfo = {
vendorId: arguments[0],
productId: arguments[1],
responseSize: arguments[2],
interval: arguments[3]
};
}
return _qz.websocket.dataPromise('hid.openStream', deviceInfo);
},
/**
* Stops the stream of read data from a claimed HID device.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @memberof qz.hid
*/
closeStream: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('hid.closeStream', deviceInfo);
},
/**
* Release a claimed HID device to free resources after sending/reading data.
*
* @param {object} deviceInfo Config details of the HID device.
* @param deviceInfo.vendorId Hex string of HID device's vendor ID.
* @param deviceInfo.productId Hex string of HID device's product ID.
* @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present.
* @param deviceInfo.serial Serial ID of HID device.
* @returns {Promise<null|Error>}
* @since 2.0.1
*
* @memberof qz.hid
*/
releaseDevice: function (deviceInfo) {
if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility
return _qz.websocket.dataPromise('hid.releaseDevice', deviceInfo);
}
},
/**
* Calls related to interactions with the filesystem
* @namespace qz.file
* @since 2.1
*/
file: {
/**
* List of files available at the given directory.<br/>
* Due to security reasons, paths are limited to the qz data directory unless overridden via properties file.
*
* @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location.
* @param {Object} [params] Object containing file access parameters
* @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections
* @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user
* @returns {Promise<Array<String>|Error>} Array of files at the given path
*
* @memberof qz.file
*/
list: function (path, params) {
var param = _qz.tools.extend({ path: path }, params);
return _qz.websocket.dataPromise('file.list', param);
},
/**
* Reads contents of file at the given path.<br/>
* Due to security reasons, paths are limited to the qz data directory unless overridden via properties file.
*
* @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location.
* @param {Object} [params] Object containing file access parameters
* @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections
* @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user
* @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are <code>[base64 | hex | plain]</code>.
* @returns {Promise<String|Error>} String containing the file contents
*
* @memberof qz.file
*/
read: function (path, params) {
var param = _qz.tools.extend({ path: path }, params);
return _qz.websocket.dataPromise('file.read', param);
},
/**
* Writes data to the file at the given path.<br/>
* Due to security reasons, paths are limited to the qz data directory unless overridden via properties file.
*
* @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location.
* @param {Object} params Object containing file access parameters
* @param {string} params.data File data to be written
* @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections
* @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user
* @param {boolean} [params.append=false] Appends to the end of the file if set, otherwise overwrites existing contents
* @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are <code>[base64 | file | hex | plain]</code>.
* @returns {Promise<null|Error>}
*
* @memberof qz.file
*/
write: function (path, params) {
var param = _qz.tools.extend({ path: path }, params);
return _qz.websocket.dataPromise('file.write', param);
},
/**
* Deletes a file at given path.<br/>
* Due to security reasons, paths are limited to the qz data directory unless overridden via properties file.
*
* @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location.
* @param {Object} [params] Object containing file access parameters
* @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections
* @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user
* @returns {Promise<null|Error>}
*
* @memberof qz.file
*/
remove: function (path, params) {
var param = _qz.tools.extend({ path: path }, params);
return _qz.websocket.dataPromise('file.remove', param);
},
/**
* Provides a continuous stream of events (and optionally data) from a local file.
*
* @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location.
* @param {Object} [params] Object containing file access parameters
* @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections
* @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user
* @param {Object} [params.listener] If defined, file data will be returned on events
* @param {number} [params.listener.bytes=-1] Number of bytes to return or -1 for all
* @param {number} [params.listener.lines=-1] Number of lines to return or -1 for all
* @param {boolean} [params.listener.reverse] Controls whether data should be returned from the bottom of the file. Default value is true for line mode and false for byte mode.
* @param {string|Array<string>} [params.include] File patterns to match. Blank values will be ignored.
* @param {string|Array<string>} [params.exclude] File patterns to exclude. Blank values will be ignored. Takes priority over <code>params.include</code>.
* @param {boolean} [params.ignoreCase=true] Whether <code>params.include</code> or <code>params.exclude</code> are case-sensitive.
* @returns {Promise<null|Error>}
* @since 2.1.0
*
* @see qz.file.setFileCallbacks
*
* @memberof qz.file
*/
startListening: function (path, params) {
if (params && typeof params.include !== 'undefined' && !Array.isArray(params.include)) {
params.include = [params.include];
}
if (params && typeof params.exclude !== 'undefined' && !Array.isArray(params.exclude)) {
params.exclude = [params.exclude];
}
var param = _qz.tools.extend({ path: path }, params);
return _qz.websocket.dataPromise('file.startListening', param);
},
/**
* Closes listeners with the provided settings. Omitting the path parameter will result in all listeners closing.
*
* @param {string} [path] Previously opened directory path of listener to close, or omit to close all.
* @param {Object} [params] Object containing file access parameters
* @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections
* @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user
* @returns {Promise<null|Error>}
*
* @memberof qz.file
*/
stopListening: function (path, params) {
var param = _qz.tools.extend({ path: path }, params);
return _qz.websocket.dataPromise('file.stopListening', param);
},
/**
* List of functions called for any response from a file listener.
* For ERROR types event data will contain, <code>{string} message</code>.
* For ACTION types event data will contain, <code>{string} file {string} eventType {string} [data]</code>.
*
* @param {Function|Array<Function>} calls Single or array of <code>Function({Object} eventData)</code> calls.
* @since 2.1.0
*
* @memberof qz.file
*/
setFileCallbacks: function (calls) {
_qz.file.fileCallbacks = calls;
}
},
/**
* Calls related to networking information
* @namespace qz.networking
* @since 2.1.0
*/
networking: {
/**
* @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com"
* @param {number} [port] Port to use with custom hostname, defaults to 443
* @returns {Promise<Object|Error>} Connected system's network information.
*
* @memberof qz.networking
* @since 2.1.0
*/
device: function (hostname, port) {
// Wrap 2.0
if (_qz.tools.isVersion(2, 0)) {
return _qz.compatible.networking(hostname, port, null, null, function (data) {
return { ip: data.ipAddress, mac: data.macAddress };
});
}
// Use 2.1
return _qz.websocket.dataPromise('networking.device', {
hostname: hostname,
port: port
});
},
/**
* Get computer hostname
*
* @param {string} [hostname] DEPRECATED Hostname to try to connect to when determining network interfaces, defaults to "google.com"
* @param {number} [port] DEPRECATED Port to use with custom hostname, defaults to 443
* @returns {Promise<string|Error>} Connected system's hostname.
*
* @memberof qz.networking
* @since 2.2.2
*/
hostname: function (hostname, port) {
// Wrap < 2.2.2
if (_qz.tools.versionCompare(2, 2, 2) < 0) {
return _qz.tools.promise(function (resolve, reject) {
_qz.websocket.dataPromise('networking.device', { hostname: hostname, port: port }).then(function (device) {
console.log(device);
resolve(device.hostname);
});
});
} else {
return _qz.websocket.dataPromise('networking.hostname');
}
},
/**
* @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com"
* @param {number} [port] Port to use with custom hostname, defaults to 443
* @returns {Promise<Array<Object>|Error>} Connected system's network information.
*
* @memberof qz.networking
* @since 2.1.0
*/
devices: function (hostname, port) {
// Wrap 2.0
if (_qz.tools.isVersion(2, 0)) {
return _qz.compatible.networking(hostname, port, null, null, function (data) {
return [{ ip: data.ipAddress, mac: data.macAddress }];
});
}
// Use 2.1
return _qz.websocket.dataPromise('networking.devices', {
hostname: hostname,
port: port
});
}
},
/**
* Calls related to signing connection requests.
* @namespace qz.security
*/
security: {
/**
* Set promise resolver for calls to acquire the site's certificate.
*
* @param {Function|AsyncFunction|Promise<string>} promiseHandler Either a function that will be used as a promise resolver (of format <code>Function({function} resolve, {function}reject)</code>),
* an async function, or a promise. Any of which should return the public certificate via their respective <code>resolve</code> call.
*
* @memberof qz.security
*/
setCertificatePromise: function (promiseHandler) {
_qz.security.certHandler = promiseHandler;
},
/**
* Set promise factory for calls to sign API calls.
*
* @param {Function|AsyncFunction} promiseFactory Either a function that accepts a string parameter of the data to be signed
* and returns a function to be used as a promise resolver (of format <code>Function({function} resolve, {function}reject)</code>),
* or an async function that can take a string parameter of the data to be signed. Either of which should return the signed contents of
* the passed string parameter via their respective <code>resolve</code> call.
*
* @example
* qz.security.setSignaturePromise(function(dataToSign) {
* return function(resolve, reject) {
* $.ajax("/signing-url?data=" + dataToSign).then(resolve, reject);
* }
* })
*
* @memberof qz.security
*/
setSignaturePromise: function (promiseFactory) {
_qz.security.signatureFactory = promiseFactory;
},
/**
* Set which signing algorithm QZ will check signatures against.
*
* @param {string} algorithm The algorithm used in signing. Valid values: <code>[SHA1 | SHA256 | SHA512]</code>
* @since 2.1.0
*
* @memberof qz.security
*/
setSignatureAlgorithm: function (algorithm) {
//warn for incompatibilities if known
if (!_qz.compatible.algorithm()) {
return;
}
if (["SHA1", "SHA256", "SHA512"].indexOf(algorithm.toUpperCase()) < 0) {
_qz.log.error("Signing algorithm '" + algorithm + "' is not supported.");
} else {
_qz.security.signAlgorithm = algorithm;
}
},
/**
* Get the signing algorithm QZ will be checking signatures against.
*
* @returns {string} The algorithm used in signing.
* @since 2.1.0
*
* @memberof qz.security
*/
getSignatureAlgorithm: function () {
return _qz.security.signAlgorithm;
}
},
/**
* Calls related to compatibility adjustments
* @namespace qz.api
*/
api: {
/**
* Show or hide QZ api debugging statements in the browser console.
*
* @param {boolean} show Whether the debugging logs for QZ should be shown. Hidden by default.
* @returns {boolean} Value of debugging flag
* @memberof qz.api
*/
showDebug: function (show) {
return (_qz.DEBUG = show);
},
/**
* Get version of connected QZ Tray application.
*
* @returns {Promise<string|Error>} Version number of QZ Tray.
*
* @memberof qz.api
*/
getVersion: function () {
return _qz.websocket.dataPromise('getVersion');
},
/**
* Checks for the specified version of connected QZ Tray application.
*
* @param {string|number} [major] Major version to check
* @param {string|number} [minor] Minor version to check
* @param {string|number} [patch] Patch version to check
*
* @memberof qz.api
*/
isVersion: _qz.tools.isVersion,
/**
* Checks if the connected QZ Tray application is greater than the specified version.
*
* @param {string|number} major Major version to check
* @param {string|number} [minor] Minor version to check
* @param {string|number} [patch] Patch version to check
* @param {string|number} [build] Build version to check
* @returns {boolean} True if connected version is greater than the version specified.
*
* @memberof qz.api
* @since 2.1.0-4
*/
isVersionGreater: function (major, minor, patch, build) {
return _qz.tools.versionCompare(major, minor, patch, build) > 0;
},
/**
* Checks if the connected QZ Tray application is less than the specified version.
*
* @param {string|number} major Major version to check
* @param {string|number} [minor] Minor version to check
* @param {string|number} [patch] Patch version to check
* @param {string|number} [build] Build version to check
* @returns {boolean} True if connected version is less than the version specified.
*
* @memberof qz.api
* @since 2.1.0-4
*/
isVersionLess: function (major, minor, patch, build) {
return _qz.tools.versionCompare(major, minor, patch, build) < 0;
},
/**
* Change the promise library used by QZ API.
* Should be called before any initialization to avoid possible errors.
*
* @param {Function} promiser <code>Function({function} resolver)</code> called to create new promises.
*
* @memberof qz.api
*/
setPromiseType: function (promiser) {
_qz.tools.promise = promiser;
},
/**
* Change the SHA-256 hashing function used by QZ API.
* Should be called before any initialization to avoid possible errors.
*
* @param {Function} hasher <code>Function({function} message)</code> called to create hash of passed string.
*
* @memberof qz.api
*/
setSha256Type: function (hasher) {
_qz.tools.hash = hasher;
},
/**
* Change the WebSocket handler.
* Should be called before any initialization to avoid possible errors.
*
* @param {Function} ws <code>Function({function} WebSocket)</code> called to override the internal WebSocket handler.
*
* @memberof qz.api
*/
setWebSocketType: function (ws) {
_qz.tools.ws = ws;
}
},
/**
* Version of this JavaScript library
*
* @constant {string}
*
* @memberof qz
*/
version: _qz.VERSION
};
return qz;
})();
(function () {
if (typeof define === 'function' && define.amd) {
define(qz);
} else if (typeof exports === 'object') {
module.exports = qz;
} else {
window.qz = qz;
}
})();