TWTwainJS.js

/**
 * Enum for TWAIN JS Connection Status.
 * @enum {object}
 */
const TWJSConnectionStatus = {
  CONNECTING: { value: 0, name: "CONNECTING" },
  CONNECTED: { value: 1, name: "CONNECTED" },
  CLOSING: { value: 2, name: "CLOSING" },
  CLOSED: { value: 3, name: "CLOSED" },
  ERROR: { value: 4, name: "ERROR" }
};

// javascript-obfuscator:disable
/**
 * Enum for TWDeviceName.
 * @enum {string}
 */
const TWDeviceName = {
  DEFAULT: "",
  SELECT_FROM_UI: "SelectFromUI"
};

/**
 * Enum for scan finished status.
 * @enum {string}
 */
const TWScanFinishedStatus = {
  SUCCESS: "success",
  CANCELLED: "cancelled",
  ERROR: "error"
};

/**
 * Enum for PDF Protection encryption level.
 * @enum {Number}
 */
const PdfDocumentSecurityLevel = {
  NONE: 0 /** No encryption */,
  ENCRYPTED_40BIT: 1 /** 40-bit encryption */,
  ENCRYPTED_128BIT: 2 /** 128-bit encryption */
};

const TWSendMessageType = {
  GET_DEVICE_LIST: "getDeviceList",
  GET_DEVICE_INFO: "getDeviceInfo",
  GET_CURRENT_SETTINGS: "getCurrentSettings",
  SCAN: "scan",
  INIT_CONFIG: "initConfig",
  GET_FILE_DOWNLOAD: "getFileDownload",
  GET_FILE: "getFile"
};

const TWReceiveMessageType = {
  DEVICE_LIST_RETRIEVED: "twDeviceListRetrieved",
  DEVICE_INFO_RETRIEVED: "twDeviceInfoRetrieved",
  CURRENT_SETTINGS_RECEIVED: "twCurrentSettingsReceived",
  PAGE_SCANNED: "twPageScanned",
  SCAN_FINISHED: "twScanFinished",
  TW_ERROR: "twErrorOccurred",
  TW_WELCOME: "twWelcome",
  TW_INIT_CONFIG: "twInitConfigResult",
  FILE_DOWNLOADED: "twFileDownloaded",
  FILE_RETRIEVED: "twFileRetrieved"
};

/**
 * Enum for TW TWAIN JS Events.
 * @enum {string}
 */
const TWEvent = {
  SOCKET_CONNECTION_STATUS_CHANGED: "twSocketConnectionStatusChanged",
  DEVICE_LIST_RETRIEVED: "twScannerListRetrieved",
  DEVICE_INFO_RETRIEVED: "twDeviceInfoRetrieved",
  CURRENT_SETTINGS_RECEIVED: "twCurrentSettingsReceived",
  PAGE_SCANNED: "twPageScanned",
  SCAN_FINISHED: "twScanFinished",
  TW_ERROR: "twErrorOccurred",
  FILE_DOWNLOADED: "twFileDownloaded",
  FILE_RETRIEVED: "twFileRetrieved"
};

/**
 * Enum for TWAIN JS Download Mode.
 * @enum {string}
 */
const TWDownloadMode = {
  LOCAL: "local",
  STANDARD: "standard"
};

const TWConsole = {
  EVENT: "twainJs_logTWEvent",
  SENT_MSG: "twainJs_logTWSentMsg",
  RECEIVED_MSG: "twainJs_logTWReceivedMsg"
};

// #region helper methods
function twConvertToUint8Array(binaryString) {
  let input = atob(binaryString);

  var sliceSize = 512;
  var bytes = [];

  for (var offset = 0; offset < input.length; offset += sliceSize) {
    var slice = input.slice(offset, offset + sliceSize);

    var byteNumbers = new Array(slice.length);

    for (var i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);

    bytes.push(byteArray);
  }

  return bytes;
}

function generateUUID() {
  // Public Domain/MIT
  var d = new Date().getTime(); //Timestamp
  var d2 = (typeof performance !== "undefined" && performance.now && performance.now() * 1000) || 0; //Time in microseconds since page-load or 0 if unsupported
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
    var r = Math.random() * 16; //random number between 0 and 16
    if (d > 0) {
      //Use timestamp until depleted
      r = (d + r) % 16 | 0;
      d = Math.floor(d / 16);
    } else {
      //Use microseconds since page-load if supported
      r = (d2 + r) % 16 | 0;
      d2 = Math.floor(d2 / 16);
    }
    return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
  });
}

function getFileType(fileName) {
  let fileType = fileName.toLowerCase().slice(-4);
  if (fileType.startsWith(".")) {
    fileType = fileType.slice(1);
  }
  return fileType;
}

function getPageNumbers(pages) {
  let numbers = [];
  if (!pages) {
    return numbers;
  }

  for (let match of pages.match(/[0-9]+(?:\-[0-9]+)?/g)) {
    if (match.includes("-")) {
      let [begin, end] = match.split("-");
      for (let num = parseInt(begin); num <= parseInt(end); num++) {
        numbers.push(num);
      }
    } else {
      numbers.push(parseInt(match));
    }
  }
  return numbers;
}

function downloadFileUsingAnchorElement(contentAsBase64Str, fileName) {
  function downloadPdfFileUsingAnchorElement(contentAsBase64Str, fileName) {
    function convertToByteArray(input) {
      var sliceSize = 512;
      var bytes = [];

      for (var offset = 0; offset < input.length; offset += sliceSize) {
        var slice = input.slice(offset, offset + sliceSize);

        var byteNumbers = new Array(slice.length);

        for (var i = 0; i < slice.length; i++) {
          byteNumbers[i] = slice.charCodeAt(i);
        }

        const byteArray = new Uint8Array(byteNumbers);

        bytes.push(byteArray);
      }

      return bytes;
    }

    const anchor = document.createElement("a");
    var bin = atob(contentAsBase64Str);
    var binUint8Array = convertToByteArray(bin);

    var blobFile = new Blob(binUint8Array, { type: "application/pdf" });
    var fileURL = URL.createObjectURL(blobFile);
    anchor.href = fileURL;
    anchor.download = fileName;

    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);
    URL.revokeObjectURL(fileURL);
  }

  function downloadImageFileUsingAnchorElement(contentAsBase64Str, fileName) {
    let fileType = getFileType(fileName);
    let imgType = ["tiff", "jpeg", "jpg"].indexOf(fileType) > -1 ? "jpeg" : fileType;
    let fileUrl = "data:image/" + imgType + ";base64," + contentAsBase64Str;
    const anchor = document.createElement("a");
    anchor.href = fileUrl;
    anchor.download = fileName;

    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);
  }

  let fileType = getFileType(fileName);
  if (fileType == "pdf") {
    downloadPdfFileUsingAnchorElement(contentAsBase64Str, fileName);
  } else {
    downloadImageFileUsingAnchorElement(contentAsBase64Str, fileName);
  }
}
// #endregion

/**
 * PdfProtection class is used to set the security level of the pdf file.
 */
class PdfProtection {
  /**
   * Creates an instance of PdfProtection. PdfProtection class is used to set the security level of the pdf file.
   */
  constructor() {
    /**
     * The owner password of the pdf file.
     * @member {String}
     */
    this.ownerPassword = "";

    /**
     * The user password of the pdf file.
     * @member {String}
     */
    this.userPassword = "";

    /**
     * The security level of the pdf file.
     * @member {PdfDocumentSecurityLevel}
     * @default PdfDocumentSecurityLevel.ENCRYPTED_128BIT
     */
    this.securityLevel = PdfDocumentSecurityLevel.ENCRYPTED_128BIT;

    /**
     * Permits extracting content for accessibility purposes from the PDF document after scanning.
     * @member {Boolean}
     * @default true
     * @remarks This property is only available for PdfDocumentSecurityLevel.ENCRYPTED_128BIT;
     */
    this.permitAccessibilityExtractContent = true;

    /**
     * Permits adding annotations to the PDF document after scanning.
     * @member {Boolean}
     * @default true
     */
    this.permitAnnotations = true;

    /**
     * Permits assembling the PDF document after scanning.
     * @member {Boolean}
     * @default true
     * @remarks This property is only available for PdfDocumentSecurityLevel.ENCRYPTED_128BIT;
     */
    this.permitAssembleDocument = true;

    /**
     * Permits extracting content from the PDF document after scanning.
     * @member {Boolean}
     * @default true
     */
    this.permitExtractContent = true;

    /**
     * Permits filling in form fields in the PDF document after scanning.
     * @member {Boolean}
     * @default true
     * @remarks This property is only available for PdfDocumentSecurityLevel.ENCRYPTED_128BIT;
     */
    this.permitFormsFill = true;

    /**
     * Permits printing the PDF document in full quality after scanning.
     * @member {Boolean}
     * @default true
     * @remarks This property is only available for PdfDocumentSecurityLevel.ENCRYPTED_128BIT;
     */
    this.permitFullQualityPrint = true;

    /**
     * Permits modifying the PDF document after scanning.
     * @member {Boolean}
     * @default true
     */
    this.permitModifyDocument = true;

    /**
     * Permits printing the PDF document after scanning.
     * @member {Boolean}
     * @default true
     */
    this.permitPrint = true;
  }
}

/**
 * SupportedCapabilities class contains the supported capabilities of the scanner.
 */
class SupportedCapabilities {
  /**
   * Creates an instance of SupportedCapabilities.
   * @param {Object} capabilities The supported capabilities of the scanner.
   */
  constructor(capabilities) {
    this.capabilities = capabilities;
  }

  /**
   * Checks if the supportedCapabilities is set.
   * @returns true if the supportedCapabilities is set, false otherwise.
   */
  isSet() {
    return this.capabilities !== undefined;
  }

  /**
   * Get the capability.
   * Capability has the following properties: values, type, isReadOnly, isEnum, isMultiVal.
   * @param {string} capName capability name
   * @returns capability if available, otherwise returns undefined.
   */
  getCapability(capName) {
    return this.capabilities[capName];
  }

  /**
   * Check the capability value. If the supportedCapabilities is not set, it will display warning and return false.
   * @param {string} capName capability name
   * @returns true if the capability is supported, false otherwise.
   */
  hasCapability(capName) {
    if (!this.isSet()) {
      console.warn("Supported capabilities is not set so I can't check if it has the capability " + capName);
      return false;
    }

    return this.getCapability(capName) !== undefined;
  }

  /**
   * Checks if the capability is read only. If the supportedCapabilities is not set, it will display warning and return false.
   * @param {string} capName capability name
   * @returns true if the capability is read only, false otherwise.
   */
  isReadOnly(capName) {
    if (!this.isSet()) {
      console.warn("Supported capabilities is not set so I can't check if the capability " + capName + " is read only");
      return false;
    }

    if (!this.hasCapability(capName)) {
      console.warn("Capability is not supported.");
      return true;
    }

    return this.getCapability(capName).isReadOnly;
  }

  /**
   * Checks if the value is supported for the capability.
   * If the supportedCapabilities is not set, it will display warning and return false.
   * @param {string} capName capability name
   * @param {*} value
   * @returns true if the value is supported, false otherwise.
   */
  isSupportedValue(capName, value) {
    if (!this.isSet()) {
      console.warn(
        "Supported capabilities is not set so I can't check if the capability " +
          capName +
          " supports the defined value "
      );
      return false;
    }

    if (this.hasCapability(capName)) {
      const cap = this.getCapability(capName);

      for (let i = 0; i < cap.values.length; i++) {
        let capVal = cap.isEnum ? cap.values[i].value : cap.values[i];
        if (capVal == value) {
          return true;
        }
      }
      console.log(this.getCapability(capName), value);
    }
    return false;
  }
}

/**
 * Contains the settings for the downloaded file or the file to be saved/retrieved.
 */
class ScannedFileSettings {
  constructor() {
    /**
     * The name of the file. From its extension, the file type will be determined.
     * It is mandatory field.
     * @member {String}
     * @default "TWTwainJSDownloadedFile.pdf"
     */
    this.fileName = "TWTwainJSDownloadedFile.pdf";

    /**
     * The pages to be included in generated file. If not set, all pages will be used. Example: "1-3,5,7-10".
     * Invalid values are ignored.
     * @member {String}
     */
    this.pages = "";

    /**
     * Whether to create the file as multi-page file or not. If true, the file will be created as multi-page file.
     * Only PDF and TIFF files can be multi-page files.
     * @member {Boolean}
     * @default false
     */
    this.isMultiPageFile = false;

    /**
     * The pdf protection settings. Ignored if the file type is not PDF.
     * If not set, pdf protection will be determined by the TWTwainClient app.
     * @member {PdfProtection}
     */
    this.pdfProtection = null;

    /**
     * The quality of the image file. Ignored if the file type is not JPEG or PDF.
     * If the value is NOT_SET (-1), image quality will be determined by the TWTwainClient app.
     * @member {Number}
     */
    this.imageQuality = -1;

    /**
     * The compression of the created TIFF file. Ignored if the file type is not TIFF.
     * If the value is TiffCompression.NOT_SET, tiff compression will be determined by the TWTwainClient app.
     * @member {TiffCompression}
     */
    this.tiffCompression = -1;
  }
}

/**
 * DeviceInfo contains the device name, the supported capabilities and the current settings of the scan device.
 */
class DeviceInfo {
  /**
   * Creates an instance of DeviceInfo.
   * @param {String} deviceName - The name of the scan device.
   * @param {Object} supportedCapabilities - The supported capabilities of the scanner.
   * @param {Object} currentSettings - The current settings of the scanner.
   */
  constructor(deviceName, supportedCapabilities, currentSettings) {
    /**
     * The name of the scan device.
     * @member {String}
     */
    this.deviceName = deviceName;

    /**
     * The supported capabilities of the scanner.
     * @member {SupportedCapabilities}
     */
    this.supportedCapabilities = new SupportedCapabilities(supportedCapabilities);

    /**
     * The current settings of the scanner.
     * @member {Object}
     */
    this.currentSettings = currentSettings;
  }
}

/**
 * ScanResult is the result of the last scan operation.
 * It contains the scanned page images, the image type, and the tiff file or pdf file if the image type is tiff or pdf.
 */
class ScanResult {
  /**
   * @param {Array} images - An array of base64 encoded scanned page images.
   * @param {String} imageType - The type of the images. Can be jpeg, bmp, png, pdf or tiff.
   * @param {String} tiffFile - The base64 encoded tiff file. Only available if imageType is tiff.
   * @param {String} pdfFile - The base64 encoded pdf file. Only available if imageType is pdf.
   */
  constructor(images, imageType, tiffFile, pdfFile) {
    /**
     * An array of base64 encoded scanned page images.
     * @member {Array}
     */
    this.images = images;

    /**
     * The type of the images. Can be jpeg, bmp, png, pdf or tiff.
     * @member {String}
     */
    this.imageType = imageType;

    /**
     * The base64 encoded tiff file. Only available if imageType is tiff.
     * @member {String}
     */
    this.tiffFile = tiffFile;

    /**
     * The base64 encoded pdf file. Only available if imageType is pdf.
     * @member {String}
     */
    this.pdfFile = pdfFile;
  }

  /**
   * @returns {Number} The number of pages in the scan result.
   */
  pageCount() {
    return this.images.length;
  }

  /**
   * @param {Number} pageNumber - The page number to get the image data from.
   * @returns {String} The base64 encoded image data.
   */
  getImageDataSrc(pageNumber) {
    if (pageNumber === undefined) pageNumber = 1;
    if (pageNumber < 1 || pageNumber > this.pageCount()) {
      throw new RangeError("Page number out of range");
    }
    const image = this.images[pageNumber - 1];
    let type = ["pdf", "tiff", "jpeg"].indexOf(this.imageType) > -1 ? "jpeg" : this.imageType;
    return "data:image/" + type + ";base64," + image;
  }

  /**
   * Get image bytes as Uint8Array for selected page number.
   * @param {Number} pageNumber - The page number to get the image data from.
   * @returns {Uint8Array} The image bytes
   */
  getImageBytes(pageNumber) {
    if (pageNumber === undefined) pageNumber = 1;
    if (pageNumber < 1 || pageNumber > this.pageCount()) {
      throw new RangeError("Page number out of range");
    }
    var binUint8Array = twConvertToByteArray(this.images[pageNumber - 1]);
    return binUint8Array;
  }

  /**
   * Get image bytes as atob for selected page number.
   * @param {Number} pageNumber - The page number to get the image data from.
   * @returns {atob} The image bytes
   */
  getImageBytes2(pageNumber) {
    if (pageNumber === undefined) pageNumber = 1;
    if (pageNumber < 1 || pageNumber > this.pageCount()) {
      throw new RangeError("Page number out of range");
    }
    var bin = atob(this.images[pageNumber - 1]);
    return bin;
  }
}

/**
 * ScanSettings object used for scanning.
 */
class ScanSettings {
  /**
   * @param {string} deviceName. The name of the device to be used for scanning. If not set, the default device will be used.
   * @param {*} supportedCapabilities. Optional. If set, when the setCap is called, it will be used to check if the capability is
   * supported, read only and has supported value.
   */
  constructor(deviceName = TWDeviceName.DEFAULT, supportedCapabilities) {
    /**
     * The scan format type to be used for scanning. If not set, the default format (jpeg) will be used.
     * If scanFormatType is set to "pdf" or "tiff":
     * - for single page image (TWEvent.PAGE_SCANNED) jpeg will be used.
     * - in TWEVent.SCAN_FINISHED, the pdf or tiff file will be returned.
     * @member {string}
     * @default "jpeg"
     */
    this.scanFormatType = "jpeg";

    /**
     * The name of the device to be used for scanning. If not set, the default device will be used.
     * @member {string}
     * @default TWDeviceName.DEFAULT
     */
    this.deviceName = deviceName;

    /**
     * The image quality to be used for scanning. If not set, the default quality will be used.
     * It has effect only if imageFormatType is set to "jpeg" or "pdf".
     * @member {number}
     * @default 80
     */
    this.imageQuality = 80;

    /**
     * Show device driver's user interface. If not set, the default value will be used.
     * @member {boolean}
     * @default true
     */
    this.showUI = true;

    /**
     * Close device driver's user interface after scanning. If not set, the default value will be used.
     * @member {boolean}
     * @default true
     */
    this.closeUIAfterAcquire = true;

    /**
     * Capabilities to be used for scanning. If not set, the default capabilities will be used.
     * It is a array of objects with the capability name and its value.
     * Recommeded is to use the setCap method to set the capabilities.
     * @member {object}
     */
    this.caps = [];

    /**
     * Supported capabilities. If set, when the setCap is called, it will be used to check if the capability is
     * supported, read only and has supported value. If not set, the setCap will not check anything and presume that the value is correct.
     * If the value is not supported, it will be ignored by the device driver.
     * @member {object}
     */
    this.supportedCapabilities = new SupportedCapabilities(supportedCapabilities);

    /**
     * The tiff compression to be used for scanning.
     * @member {CapEnum.TiffCompression}
     * @default CapEnum.TiffCompression.LZW
     */
    this.tiffCompression = 4; // CapEnum.TiffCompression.LZW

    /**
     * The pdf protection to be used for scanning. If not set, PDF will not be protected.
     * @member {PdfProtection}
     */
    this.pdfProtection = null;
  }

  /**
   * Reset scan capabilities.
   */
  clearCaps() {
    this.caps = [];
  }

  /**
   * Set value to the capability.
   * @param {string} capName - Name of the capability to set.
   * @param {*} capValue - The value to set.
   */
  setCap(capName, capValue) {
    if (this.supportedCapabilities.isSet()) {
      if (!this.supportedCapabilities.hasCapability(capName)) {
        console.warn("The capability " + capName + " is not supported.");
        return;
      }

      if (this.supportedCapabilities.isReadOnly(capName)) {
        console.warn("Capability " + capName + " is read only.");
        return;
      }

      if (!this.supportedCapabilities.isSupportedValue(capName, capValue)) {
        console.warn("The value " + capValue + " is not supported for the capability " + capName);
        return;
      }
    }

    for (let cap in this._caps) {
      if (cap.name === capName) {
        cap.value = capValue;
        return;
      }
    }
    this.caps.push({ name: capName, value: capValue });
  }
}

/**
 * Main class for the TWTwainJS library.
 */
class TWTwainJS {
  // #region Private fields
  #currentSocket;
  #twConsole;
  #promiseEventList;
  #toPort;
  #key;
  #companyName;
  //#endregion

  /**
   * The name of the TWTwainJS instance.
   * @member {string}
   */
  name = "";

  /**
   * The result of the last scan.
   * @member {ScanResult}
   */
  scanResult = null;

  /**
   * The port used for the socket communication. It is set automatically when the connect method succeeds.
   * @member {number}
   */
  port = 0;

  /**
   * The id of the twTwainJS instance. It is set automatically when the connect method succeeds.
   * @member {string}
   */
  id = "";

  // #region Private Methods
  #initConfig() {
    function _0x3db0(_0x428336, _0x4a9cb1) {
      const _0x89ec78 = _0x89ec();
      _0x3db0 = function (_0x3db017, _0x504689) {
        _0x3db017 = _0x3db017 - 0x125;
        let _0x3cc0b4 = _0x89ec78[_0x3db017];
        return _0x3cc0b4;
      };
      return _0x3db0(_0x428336, _0x4a9cb1);
    }
    function _0x89ec() {
      const _0x28eabc = [
        "ownerDocument",
        "split",
        "7UDLiKV",
        "reduce",
        "slice",
        "file",
        "621444TceQNK",
        "toString",
        "span",
        "createElement",
        "1763228lxncQL",
        "map",
        "setAttribute",
        "custom",
        "730158uFStlC",
        "getTime",
        "723tVoMvA",
        "join",
        "1642635ybbNmH",
        "20YNbShh",
        "getAttribute",
        "17920RimFdy",
        "14797953rPAxyN",
        "3293568IUmIxs"
      ];
      _0x89ec = function () {
        return _0x28eabc;
      };
      return _0x89ec();
    }
    (function (_0x5bf428, _0x16ba0a) {
      const _0x4e7ecc = _0x3db0;
      const _0x48e83c = _0x5bf428();
      while (!![]) {
        try {
          const _0x967265 =
            -parseInt(_0x4e7ecc(0x12f)) / 0x1 +
            -parseInt(_0x4e7ecc(0x12b)) / 0x2 +
            (parseInt(_0x4e7ecc(0x131)) / 0x3) * (-parseInt(_0x4e7ecc(0x136)) / 0x4) +
            parseInt(_0x4e7ecc(0x133)) / 0x5 +
            (-parseInt(_0x4e7ecc(0x127)) / 0x6) * (-parseInt(_0x4e7ecc(0x13b)) / 0x7) +
            -parseInt(_0x4e7ecc(0x138)) / 0x8 +
            (-parseInt(_0x4e7ecc(0x137)) / 0x9) * (-parseInt(_0x4e7ecc(0x134)) / 0xa);
          if (_0x967265 === _0x16ba0a) {
            break;
          } else {
            _0x48e83c["push"](_0x48e83c["shift"]());
          }
        } catch (_0x284d59) {
          _0x48e83c["push"](_0x48e83c["shift"]());
        }
      }
    })(_0x89ec, 0x96bab);
    function getConfig(_0x39fc23, _0x4b0856) {
      const _0x126378 = _0x3db0;
      function _0xd5f59(_0x4e0323, _0xfe8a3d) {
        const _0x336cc0 = _0x3db0;
        function _0x45357a(_0x2e59ab) {
          const _0x4bd1c2 = _0x3db0;
          return ("0" + (_0x2e59ab & 0xff)[_0x4bd1c2(0x128)](0x10))[_0x4bd1c2(0x125)](-0x2);
        }
        function _0x2c3ebc(_0x3acfbe) {
          const _0x183b73 = _0x3db0;
          return _0x3acfbe[_0x183b73(0x13a)]("")[_0x183b73(0x12c)](function (_0x58f372) {
            return _0x58f372["charCodeAt"](0x0);
          });
        }
        function _0x150945(_0x11957a) {
          const _0x469b83 = _0x3db0;
          return _0x2c3ebc(_0x4e0323)[_0x469b83(0x13c)](function (_0x8d78bc, _0x529a7d) {
            return _0x8d78bc ^ _0x529a7d;
          }, _0x11957a);
        }
        return _0xfe8a3d[_0x336cc0(0x13a)]("")
          [_0x336cc0(0x12c)](_0x2c3ebc)
          [_0x336cc0(0x12c)](_0x150945)
          [_0x336cc0(0x12c)](_0x45357a)
          [_0x336cc0(0x132)]("");
      }
      const _0x1622d5 = document[_0x126378(0x12a)](_0x126378(0x129));
      _0x1622d5[_0x126378(0x12d)]("custom", _0x1622d5[_0x126378(0x139)]["location"]["hostname"]);
      let _0x29f4e3 = _0x1622d5["getAttribute"](_0x126378(0x12e))
        ? _0x1622d5[_0x126378(0x135)]("custom")
        : _0x126378(0x126);
      return _0xd5f59(_0x39fc23, new Date()[_0x126378(0x130)]() + "|" + _0x29f4e3 + "|" + _0x4b0856);
    }

    var message = {
      messageType: TWSendMessageType.INIT_CONFIG,
      companyName: this.#companyName,
      config: getConfig(this.#companyName, this.#key)
    };
    this.#sendMessage(TWSendMessageType.INIT_CONFIG, JSON.stringify(message));
  }

  #setTWInterval(me, eventId, resolve, reject) {
    let intervalId = setInterval(() => {
      if (me.#promiseEventList.findIndex((x) => x.detail.eventId == eventId) > -1) {
        console.log("Event " + eventId + " found!");
        clearInterval(intervalId);
        const event = me.#promiseEventList.find((x) => x.detail.eventId == eventId);
        if (event.detail.error) {
          reject(event.detail.error);
        } else {
          resolve(event.detail);
        }
      }
    }, 100);
  }

  #dispatchEvent(event, addToPromiseEventList = true) {
    window.dispatchEvent(event);
    if (addToPromiseEventList) this.#promiseEventList.push(event);
  }

  #triggerTWError(message, eventId) {
    var evt = new CustomEvent(TWEvent.TW_ERROR, {
      detail: {
        error: message,
        twTwainJSInstance: this,
        eventId: eventId
      }
    });

    this.#dispatchEvent(evt);
  }

  #setSocket(socket, evt, eventId) {
    this.#currentSocket = socket;
    let me = this;
    function triggerStatusChange(status, socketEvent, eventId) {
      var evt = new CustomEvent(TWEvent.SOCKET_CONNECTION_STATUS_CHANGED, {
        detail: {
          status: status,
          socketEvent: socketEvent,
          twTwainJSInstance: me,
          eventId: eventId
        }
      });
      me.#dispatchEvent(evt);
    }

    this.#initConfig();

    try {
      this.#currentSocket.onclose = function (evt) {
        triggerStatusChange(TWJSConnectionStatus.CLOSED, evt, eventId);
      };

      let me = this;
      this.#currentSocket.onerror = function (evt) {
        triggerStatusChange(TWJSConnectionStatus.ERROR, evt, eventId);
      };

      /*this.#currentSocket.onopen = function (evt) {
        triggerStatusChange(TWJSConnectionStatus.CONNECTED, evt, eventId);
        this.#initConfig();
      };*/

      this.#currentSocket.onmessage = function (evt) {
        try {
          var message = JSON.parse(evt.data);
          if (message.messageType != TWReceiveMessageType.TW_INIT_CONFIG) {
            me.#twConsole(message.messageType, message, TWConsole.RECEIVED_MSG);
          }
          switch (message.messageType) {
            case TWReceiveMessageType.TW_INIT_CONFIG: {
              triggerStatusChange(TWJSConnectionStatus.CONNECTED, evt, eventId);

              break;
            }

            case TWReceiveMessageType.DEVICE_LIST_RETRIEVED: {
              var evt = new CustomEvent(TWEvent.DEVICE_LIST_RETRIEVED, {
                detail: {
                  devicesName: message.devicesName,
                  defaultDeviceName: message.defaultDeviceName,
                  twTwainJSInstance: me,
                  eventId: message.eventId
                }
              });
              me.#dispatchEvent(evt);
              break;
            }

            case TWReceiveMessageType.DEVICE_INFO_RETRIEVED: {
              const deviceInfo = new DeviceInfo(
                message.deviceName,
                message.supportedCapabilities,
                message.currentSettings,
                true
              );

              var evt = new CustomEvent(TWEvent.DEVICE_INFO_RETRIEVED, {
                detail: {
                  deviceInfo: deviceInfo,
                  twTwainJSInstance: me,
                  eventId: message.eventId
                }
              });

              me.#dispatchEvent(evt);
              break;
            }

            case TWReceiveMessageType.CURRENT_SETTINGS_RECEIVED:
              var evt = new CustomEvent(TWEvent.CURRENT_SETTINGS_RECEIVED, {
                detail: {
                  currentSettings: message.currentSettings,
                  deviceName: message.deviceName,
                  twTwainJSInstance: me,
                  eventId: message.eventId
                }
              });
              me.#dispatchEvent(evt);
              break;

            case TWReceiveMessageType.PAGE_SCANNED: {
              if (me.scanResult == null) {
                me.scanResult = new ScanResult([], message.scanFormatType);
              }
              me.scanResult.images.push(message.image);
              var evt = new CustomEvent(TWEvent.PAGE_SCANNED, {
                detail: {
                  scanResult: me.scanResult,
                  deviceName: message.deviceName,
                  twTwainJSInstance: me,
                  eventId: message.eventId
                }
              });
              me.#dispatchEvent(evt, false);
              break;
            }

            case TWReceiveMessageType.SCAN_FINISHED: {
              var evt = new CustomEvent(TWEvent.SCAN_FINISHED, {
                detail: {
                  scanFinishedStatus: message.scanFinishedStatus,
                  deviceName: message.deviceName,
                  twTwainJSInstance: me,
                  eventId: message.eventId
                }
              });

              if (message.scanFinishedStatus == TWScanFinishedStatus.SUCCESS) {
                const scanResult = new ScanResult(
                  message.images,
                  me.scanResult.scanFormatType,
                  me.tiffFile,
                  me.pdfFile
                );
                evt.detail.scanResult = scanResult;
              }

              if (message.scanFinishedStatus == TWScanFinishedStatus.ERROR) {
                evt.detail.error = message.error;
              }

              me.#dispatchEvent(evt);
              break;
            }

            case TWReceiveMessageType.FILE_DOWNLOADED: {
              me.#triggerFileDownloadEvent(message, me);
              break;
            }

            case TWReceiveMessageType.FILE_RETRIEVED: {
              me.#triggerFileRetrievedEvent(message, me);
              break;
            }

            case TWReceiveMessageType.TW_ERROR: {
              me.#triggerTWError(message.error, message.eventId);
              break;
            }

            default:
              break;
          }
        } catch (error) {
          console.log(error);
          me.#triggerTWError(error.message);
        }
      };
    } catch (error) {
      console.log(error);
      this.#triggerTWError(error.message);
    }
  }

  #getValidSocket(port, eventId) {
    if (this.#toPort && port > this.#toPort) {
      this.#triggerTWError(
        "No valid socket found. Check that the TWTwainClient.exe is running and has opened the socket port in the valid port range.",
        eventId
      );
      return;
    }

    let socket = new WebSocket("ws://127.0.0.1:" + port);
    let me = this;
    socket.onclose = function (evt) {
      me.#getValidSocket(port + 1, eventId);
    };

    socket.onmessage = function (evt) {
      var message = JSON.parse(evt.data);
      switch (message.messageType) {
        case TWReceiveMessageType.TW_WELCOME: {
          me.id = message.currentId;
          socket.onerror = null;
          socket.onopen = null;
          socket.onmessage = null;
          socket.onclose = null;
          me.port = port;
          me.#setSocket(socket, evt, eventId);
          break;
        }
        default:
          socket.close();
      }
    };
  }

  #sendMessage = function (messageType, message) {
    const isConnected = this.#currentSocket.readyState === TWJSConnectionStatus.CONNECTED.value;
    if (isConnected) {
      this.#currentSocket.send(message);
      if (messageType != TWSendMessageType.INIT_CONFIG) {
        this.#twConsole(messageType, message, TWConsole.SENT_MSG);
      }
    } else {
      this.#triggerTWError("TWTwain client is not connected. Please check if TWTwainClient.exe is started.");
    }
  };

  #triggerFileDownloadEvent(message, me) {
    var evt = new CustomEvent(TWEvent.FILE_DOWNLOADED, {
      detail: {
        fileName: message.fileName,
        fileContent: message.fileContent,
        isMultiPageFile: message.isMultiPageFile,
        pages: message.pages,
        downloadMode: message.downloadMode,
        twTwainJSInstance: me,
        eventId: message.eventId
      }
    });
    me.#dispatchEvent(evt);
    if (message.downloadMode == TWDownloadMode.STANDARD) {
      for (let i = 0; i < message.fileContent.length; i++) {
        downloadFileUsingAnchorElement(message.fileContent[i], message.fileName);
      }
    }
  }

  #triggerFileRetrievedEvent(message, me) {
    var evt = new CustomEvent(TWEvent.FILE_RETRIEVED, {
      detail: {
        fileName: message.fileName,
        fileContent: message.fileContent,
        isMultiPageFile: message.isMultiPageFile,
        pages: message.pages,
        twTwainJSInstance: me,
        eventId: message.eventId
      }
    });
    me.#dispatchEvent(evt);
  }
  // #endregion

  /**
   * @param {string} name - The name of the TWTwainJS instance.
   * @param {string} companyName - License company name
   * @param {string} key - License key
   * @param {object} twConsole - The console object to be used for logging. Used only for debugging purposes.
   */
  constructor(name, companyName, key, twConsole) {
    this.name = name;
    this.#key = key;
    this.#companyName = companyName;
    this.#twConsole = twConsole;
    if (this.#twConsole === undefined) {
      this.#twConsole = function () {};
    }
    this.#promiseEventList = [];
  }

  // #region Public Methods
  /**
   * Connect to the TWTwain client and open the WebSocket connection trying to
   * find a first valid port in the from and to range.
   * If the connection is successful, the onConnected event is raised otherwise No valid socket found is raised.
   * @param {number} fromPort - The starting port number in client port range.
   * @param {number} fromPort - The ending port number in client port range.
   * @returns {Promise} - A promise that resolves when the connection is successful.
   */
  connectToClient(fromPort, toPort) {
    this.#toPort = toPort;
    return new Promise((resolve, reject) => {
      const eventId = generateUUID();
      try {
        this.#setTWInterval(this, eventId, resolve, reject);
        this.#getValidSocket(fromPort, eventId);
      } catch (error) {
        this.#triggerTWError("TWTwain client connection error. " + error, eventId);
      }
    });
  }

  /**
   * Execute scanning from the TWTwain client using the specified settings in capability property.
   * Once the page is scanned, TWEvent.PAGE_SCANNED is triggered.
   * Once the scanning finishes, TWEvent.SCAN_FINISHED is triggered with scanFinishedStatus set to
   * TWScanFinishedStatus.SUCCESS if the scanning is successful
   * or TWScanFinishedStatus.ERROR if the scanning is not successful
   * or TWScanFinishedStatus.CANCEL if the scanning is cancelled.
   * @param {ScanSettings} scanSettings - Scan settings.
   * @returns {Promise} - Promise object represents the scan result.
   */
  scan(scanSettings) {
    return new Promise((resolve, reject) => {
      const eventId = generateUUID();
      let scanSettingsParm = {};
      scanSettingsParm.messageType = TWSendMessageType.SCAN;
      scanSettingsParm.deviceName = scanSettings.deviceName;
      scanSettingsParm.scanFormatType = scanSettings.scanFormatType;
      scanSettingsParm.imageQuality = scanSettings.imageQuality;
      scanSettingsParm.showUI = scanSettings.showUI;
      scanSettingsParm.closeUIAfterAcquire = scanSettings.closeUIAfterAcquire;
      scanSettingsParm.tiffCompression = scanSettings.tiffCompression;
      scanSettingsParm.pdfProtection = scanSettings.pdfProtection;
      scanSettingsParm.twainProps = {};
      scanSettingsParm.eventId = eventId;

      for (let i = 0; i < scanSettings.caps.length; i++) {
        let prop = scanSettings.caps[i];
        scanSettingsParm.twainProps[prop.name] = prop.value;
      }

      this.scanResult = null;

      this.#setTWInterval(this, eventId, resolve, reject);
      try {
        this.#sendMessage(scanSettingsParm.messageType, JSON.stringify(scanSettingsParm), eventId);
      } catch (error) {
        this.#triggerTWError(error.message, eventId);
      }
    });
  }

  /**
   * Open the specified scanner device and reads its capabilities and current values.
   * Once opened, event TWEvent.DEVICE_INFO_RETRIEVIED is triggered with the device name and capabilities in the detail.
   * @param {string} deviceName - Name of the device to open.
   * @param {boolean} ignoreCurrentValues - If true, the current values of the capabilities are not retrieved. It could be good for performance.
   * Default is false.
   * @returns {Promise} - Promise object represents the device info.
   */
  getDeviceInfo(deviceName, ignoreCurrentValues = false) {
    return new Promise((resolve, reject) => {
      const eventId = generateUUID();
      this.#sendMessage(
        TWSendMessageType.GET_DEVICE_INFO,
        JSON.stringify({
          messageType: TWSendMessageType.GET_DEVICE_INFO,
          ignoreCurrentValues: ignoreCurrentValues,
          deviceName: deviceName,
          eventId: eventId
        })
      );
      this.#setTWInterval(this, eventId, resolve, reject);
    });
  }

  /**
   * Download the specified file in the file format specified in the fileName extension.
   * @param {TWDownloadMode} downloadMode - download mode. Allowed values are TWDownloadMode.LOCAL ("local") or TWDownloadMode.STANDARD ("standard").
   * Local mode saves the file to the local machine based on TWTwain client settings.
   * Standard mode uses the browser's download functionality.
   * @param {ScannedFileSettings} settings - Scanned file settings.
   * @returns {Promise} - Promise object represents the download result.
   */
  downloadFile(downloadMode, settings) {
    return new Promise((resolve, reject) => {
      if (!settings.pages) settings.pages = "1-" + this.scanResult.pageCount();
      const eventId = generateUUID();

      this.#setTWInterval(this, eventId, resolve, reject);
      let fileType = getFileType(settings.fileName);
      let isTiffOrPdf = fileType === "tif" || fileType === "tiff" || fileType === "pdf";

      if (!isTiffOrPdf && downloadMode == TWDownloadMode.STANDARD) {
        let scanFileType = this.scanResult.scanFileType;
        let isSameScanType = scanFileType == fileType;
        if (!isSameScanType) {
          if (scanFileType == "jpg" || scanFileType == "jpeg") {
            isSameScanType = fileType == "jpg" || fileType == "jpeg";
          }
        }

        if (isSameScanType) {
          const message = {
            fileName: settings.fileName,
            fileContent: [],
            isMultiPageFile: settings.isMultiPageFile,
            pages: settings.pages,
            downloadMode: downloadMode,
            twTwainJSInstance: this,
            eventId: eventId
          };
          const pageNumbers = getPageNumbers(settings.pages);
          for (let i = 0; i < pageNumbers.length; i++) {
            if (pageNumbers[i] <= this.scanResult.pageCount())
              message.fileContent.push(this.scanResult.images[pageNumbers[i] - 1]);
          }
          this.#triggerFileDownloadEvent(message, this);
          return;
        }
      }

      //if not same scan type or dowload mode is local or pdf/tiff, use the client download
      this.#sendMessage(
        TWSendMessageType.GET_FILE_DOWNLOAD,
        JSON.stringify({
          messageType: TWSendMessageType.GET_FILE_DOWNLOAD,
          downloadMode: downloadMode,
          scannedFileSettings: settings,
          eventId: eventId
        })
      );
    });
  }

  /**
   * Retrieves the specified file(s) in the file format specified in the fileName extension.
   * @param {ScannedFileSettings} settings - Scanned file settings.
   * @returns {Promise} - Promise object represents the file retrieve result.
   */
  getFile(settings) {
    return new Promise((resolve, reject) => {
      if (!settings.pages) settings.pages = "1-" + this.scanResult.pageCount();
      const eventId = generateUUID();

      let imageType = this.scanResult.imageType;
      let fileType = getFileType(fileName);
      let isSameImageType = imageType == fileType;
      if (!isSameImageType) {
        if (imageType == "jpg" || imageType == "jpeg") {
          isSameImageType = fileType == "jpg" || fileType == "jpeg";
        }
      }

      this.#setTWInterval(this, eventId, resolve, reject);
      if (isSameImageType) {
        const message = {
          fileName: settings.fileName,
          fileContent: [],
          isMultiPageFile: settings.isMultiPageFile,
          pages: settings.pages,
          twTwainJSInstance: this,
          eventId: eventId
        };
        const pageNumbers = getPageNumbers(settings.pages);
        for (let i = 0; i < pageNumbers.length; i++) {
          if (pageNumbers[i] <= this.scanResult.pageCount())
            message.fileContent.push(this.scanResult.images[pageNumbers[i] - 1]);
        }
        this.#triggerFileRetrievedEvent(message, this);
      } else {
        this.#sendMessage(
          TWSendMessageType.GET_FILE,
          JSON.stringify({
            messageType: TWSendMessageType.GET_FILE,
            fileName: settings.fileName,
            pageNumbers: settings.pages,
            isMultiPageFile: settings.isMultiPageFile,
            eventId: eventId
          })
        );
      }
    });
  }

  /**
   * Execute retrieving the current capability values from the client.
   * Once finished, event TWEvent.CURRENT_SETTINGS_RECEIVED is triggered.
   * @param {*} capNames - Array of capability names to retrieve the values for.
   * @param {string} deviceName - Name of the device to retrieve the capabilities from.
   * @returns {Promise} - Promise object represents the current settings retrieve result.
   */
  getCurrentSettings(capNames, deviceName) {
    return new Promise((resolve, reject) => {
      const eventId = generateUUID();
      this.#setTWInterval(this, eventId, resolve, reject);
      this.#sendMessage(
        TWSendMessageType.GET_CURRENT_SETTINGS,
        JSON.stringify({
          messageType: TWSendMessageType.GET_CURRENT_SETTINGS,
          eventId: eventId,
          capNames: capNames,
          deviceName: deviceName
        })
      );
    });
  }

  /**
   * Execute retrieving the available devices and current default device from the client.
   * Once finished, event TWWEvent.DEVICE_LIST_RETRIEVED is triggered.
   * @returns {Promise} - Promise object represents the available devices retrieve result.
   */
  getAvailableDevices() {
    return new Promise((resolve, reject) => {
      const eventId = generateUUID();
      this.#setTWInterval(this, eventId, resolve, reject);
      this.#sendMessage(
        TWSendMessageType.GET_DEVICE_LIST,
        JSON.stringify({
          messageType: TWSendMessageType.GET_DEVICE_LIST,
          eventId: eventId
        })
      );
    });
  }
  // #endregion
}