diff --git a/addons/dine360_qz_printer/__init__.py b/addons/dine360_qz_printer/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/addons/dine360_qz_printer/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/addons/dine360_qz_printer/__manifest__.py b/addons/dine360_qz_printer/__manifest__.py
new file mode 100644
index 0000000..0d7249a
--- /dev/null
+++ b/addons/dine360_qz_printer/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ 'name': 'Dine360 QZ Tray Printer',
+ 'version': '1.0',
+ 'category': 'Point of Sale',
+ 'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.',
+ 'depends': ['point_of_sale'],
+ 'data': [
+ 'views/pos_config_views.xml',
+ ],
+ 'assets': {
+ 'point_of_sale._assets_pos': [
+ 'dine360_qz_printer/static/src/js/qz-tray.js',
+ 'dine360_qz_printer/static/src/js/qz_wrapper.js',
+ ],
+ },
+ 'installable': True,
+ 'application': False,
+ 'license': 'LGPL-3',
+}
diff --git a/addons/dine360_qz_printer/models/__init__.py b/addons/dine360_qz_printer/models/__init__.py
new file mode 100644
index 0000000..db8634a
--- /dev/null
+++ b/addons/dine360_qz_printer/models/__init__.py
@@ -0,0 +1 @@
+from . import pos_config
diff --git a/addons/dine360_qz_printer/models/pos_config.py b/addons/dine360_qz_printer/models/pos_config.py
new file mode 100644
index 0000000..d86d84d
--- /dev/null
+++ b/addons/dine360_qz_printer/models/pos_config.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+class PosConfig(models.Model):
+ _inherit = 'pos.config'
+
+ use_qz_printer = fields.Boolean("Use QZ Tray Printer", help="Print directly using QZ Tray locally")
+ qz_printer_name = fields.Char("QZ Printer Name", help="Name of the printer mapped in QZ Tray")
diff --git a/addons/dine360_qz_printer/static/src/js/qz-tray.js b/addons/dine360_qz_printer/static/src/js/qz-tray.js
new file mode 100644
index 0000000..46633a1
--- /dev/null
+++ b/addons/dine360_qz_printer/static/src/js/qz-tray.js
@@ -0,0 +1,2859 @@
+'use strict';
+
+/**
+ * @version 2.2.4
+ * @overview QZ Tray Connector
+ * @license LGPL-2.1-only
+ *
+ * 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.4", //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,
+ /** Track if a connection attempt is being cancelled. */
+ shutdown: false,
+
+ /** 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) {
+ if (_qz.websocket.shutdown) {
+ reject(new Error("Connection attempt cancelled by user"));
+ return;
+ }
+
+ //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() {
+ if (_qz.websocket.shutdown) {
+ //connection attempt was cancelled, bail out
+ reject(new Error("Connection attempt cancelled by user"));
+ return;
+ }
+
+ 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));
+ }).catch(function(err) {
+ _qz.log.error("Signing failed", err);
+
+ if (obj.promise != undefined) {
+ obj.promise.reject(new Error("Failed to sign request"));
+ delete _qz.websocket.pendingCalls[obj.uid];
+ }
+ });
+ } 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);
+
+ if (_qz.security.rejectOnCertFailure) {
+ openPromise.reject(error);
+ } else {
+ 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",
+
+ rejectOnCertFailure: false,
+
+ 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.shutdown && _qz.websocket.connection != null
+ && (_qz.websocket.connection.readyState === _qz.tools.ws.OPEN
+ || _qz.websocket.connection.readyState === _qz.tools.ws.CONNECTING);
+ },
+
+ 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
+ for(var i = 0; i < printData.length; i++) {
+ if (printData[i].constructor === Object && printData[i].data instanceof Uint8Array) {
+ if (printData[i].flavor) {
+ var flavor = printData[i].flavor.toString().toUpperCase();
+ switch(flavor) {
+ case 'BASE64':
+ printData[i].data = _qz.tools.uint8ArrayToBase64(printData[i].data);
+ break;
+ case 'HEX':
+ printData[i].data = _qz.tools.uint8ArrayToHex(printData[i].data);
+ break;
+ default:
+ throw new Error("Uint8Array conversion to '" + flavor + "' is not supported.");
+ }
+ }
+ }
+ }
+
+ if(_qz.tools.versionCompare(2, 2, 4) < 0) {
+ for(var i = 0; i < printData.length; i++) {
+ if (printData[i].constructor === Object) {
+ // dotDensity: "double-legacy|single-legacy" since 2.2.4. Fallback to "double|single"
+ if (printData[i].options && typeof printData[i].options.dotDensity === 'string') {
+ printData[i].options.dotDensity = printData[i].options.dotDensity.toLowerCase().replace("-legacy", "");
+ }
+ }
+ }
+ }
+
+ 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 <printer.host>.
+ */
+ this.setPrinter = function(newPrinter) {
+ if (typeof newPrinter === 'string') {
+ newPrinter = { name: newPrinter };
+ }
+ 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 qz.configs.setDefaults 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 qz.print with a particular config.
+ * @param {Array