UNPKG

scanonweb

Version:

ScanOnWeb - 扫描控件 JavaScript SDK,支持 v2 协议、scanSource 和增量图像事件

661 lines (657 loc) 22.8 kB
/*! * scanonweb v2.0.1 * ScanOnWeb - 扫描控件 JavaScript SDK,支持 v2 协议、scanSource 和增量图像事件 * https://www.brainysoft.cn * * Copyright (c) 2026 BrainySoft * Licensed under the MIT license */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ScanOnWeb = factory()); })(this, (function () { 'use strict'; /** * ScanOnWeb - JavaScript SDK for the local ScanOnWeb tray service. * https://www.brainysoft.cn * @version 2.0.1 */ class ScanOnWeb { constructor() { this.protocolVersion = 2; this.serverProtocolVersion = null; this.imageCount = 0; this.scaner_work_config = { showUI: false, dpi_x: 300, dpi_y: 300, deviceIndex: 0, showDialog: false, scanSource: "flatbed", autoFeed: false, dupxMode: false, autoDeskew: false, autoBorderDetection: false, colorMode: "RGB", transMode: "memory" }; this.h5socket = null; this.tryConnect(); } getConnectedServer(wssUrls) { console.log("尝试连接托盘扫描服务websocket服务器..."); return new Promise((resolve, reject) => { const server = new WebSocket(wssUrls[0]); server.onopen = () => { resolve(server); }; server.onerror = err => { reject(err); }; }).then(server => { console.log("连接websocket服务器成功!"); this.initWebsocketCallback(server); console.log("尝试获取扫描设备列表..."); this.loadDevices(); return server; }, err => { if (wssUrls.length > 1) { return this.getConnectedServer(wssUrls.slice(1)); } throw err; }); } tryConnect() { const wssUrls = ["ws://127.0.0.1:2001", "ws://127.0.0.1:3001", "ws://127.0.0.1:4001", "ws://127.0.0.1:5001"]; this.getConnectedServer(wssUrls); } initWebsocketCallback(server) { this.h5socket = server; this.h5socket.onerror = this.onSocketError.bind(this); this.h5socket.onmessage = this.onSocketMessage.bind(this); } onSocketError(event) { alert("无法连接扫描服务程序,请检查扫描服务程序是否已经启动!"); console.log("WebSocket error: " + event.data); } isCallbackExist(f) { if (!f || typeof f === "undefined" || f === undefined) { return false; } return typeof f === "function"; } dispatchCallback(name, msg) { if (this.isCallbackExist(this[name])) { this[name](msg); } } isUploadCommandType(cmdType) { return cmdType === "uploadAllImageAsPdfToUrl" || cmdType === "uploadAllImageAsTiffToUrl" || cmdType === "uploadJpgImageByIndex"; } extractUploadResult(msg) { if (!msg || typeof msg !== "object") { return null; } const candidates = [msg.uploadResultJson, msg.uploadResult]; for (const candidate of candidates) { if (!candidate) { continue; } if (typeof candidate === "object") { return candidate; } if (typeof candidate === "string") { try { return JSON.parse(candidate); } catch (_error) { // 兼容旧服务端返回纯文本 uploadResult,此时退回顶层 message。 } } } if (typeof msg.message === "string" || typeof msg.msg === "string" || typeof msg.error === "string") { return { success: msg.success === true || msg.ok === true, message: msg.message || msg.msg || msg.error || "", error: msg.error || msg.message || msg.msg || "", raw: typeof msg.uploadResult === "string" ? msg.uploadResult : null }; } return null; } resolveUploadMessage(msg, fallback) { const uploadResult = this.extractUploadResult(msg); const candidates = [uploadResult && uploadResult.message, uploadResult && uploadResult.error, msg && msg.message, msg && msg.msg, msg && msg.error]; for (const candidate of candidates) { if (typeof candidate === "string" && candidate.trim()) { return candidate.trim(); } } return typeof fallback === "string" ? fallback : ""; } normalizeUploadHeaderValue(value, fieldName = "header value") { if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean") { return String(value); } throw new Error(`${fieldName} must be a string, number, or boolean`); } normalizeUploadRequestHeadersInput(headers) { if (headers == null) { return []; } if (Array.isArray(headers)) { return headers.map((header, index) => { if (!header || typeof header !== "object") { throw new Error(`headers[${index}] must be an object`); } const name = typeof header.name === "string" ? header.name.trim() : ""; if (!name) { throw new Error(`headers[${index}].name must be a non-empty string`); } return { name, value: this.normalizeUploadHeaderValue(header.value, `headers[${index}].value`) }; }); } if (typeof headers === "object") { return Object.keys(headers).map(name => ({ name, value: this.normalizeUploadHeaderValue(headers[name], `headers['${name}']`) })); } throw new Error("upload request headers must be an object, array, or null"); } normalizeUploadMessage(msg) { if (!msg || typeof msg !== "object" || !this.isUploadCommandType(msg.cmd_type)) { return msg; } const uploadResult = this.extractUploadResult(msg); if (!uploadResult) { return msg; } msg.uploadResultJson = uploadResult; msg.uploadSucceeded = typeof uploadResult.success === "boolean" ? uploadResult.success : msg.success === true || msg.ok === true; const uploadMessage = this.resolveUploadMessage(msg, ""); if (uploadMessage) { msg.uploadMessage = uploadMessage; if (!msg.message) { msg.message = uploadMessage; } if (!msg.msg) { msg.msg = uploadMessage; } if (msg.uploadSucceeded === false && !msg.error) { msg.error = uploadMessage; } } return msg; } toBoolCompat(value) { if (typeof value === "boolean") { return value; } if (typeof value === "number") { return value !== 0; } if (typeof value === "string") { const normalized = value.trim().toLowerCase(); return ["1", "true", "yes", "y", "on", "adf"].includes(normalized); } return !!value; } toIntCompat(value, fallback) { const parsed = Number(value); return Number.isInteger(parsed) ? parsed : fallback; } normalizeScanSource(scanSource, legacyAutoFeedEnable) { if (typeof scanSource === "string") { const normalized = scanSource.trim().toLowerCase(); if (normalized === "adf" || normalized === "feeder" || normalized === "documentfeeder" || normalized === "document_feeder" || normalized === "automatic_document_feeder") { return "adf"; } if (normalized === "flatbed" || normalized === "flat_bed" || normalized === "platen") { return "flatbed"; } } if (this.toBoolCompat(legacyAutoFeedEnable)) { return "adf"; } return "flatbed"; } getLegacyAutoFeedEnable(scanSource) { return this.normalizeScanSource(scanSource) === "adf"; } shouldIncludeLegacyAutoFeedEnable() { return this.serverProtocolVersion === null || this.serverProtocolVersion < 2; } normalizeScanConfig(config, options) { const merged = Object.assign({}, this.scaner_work_config, config || {}); const includeLegacyAutoFeedEnable = !options || options.includeLegacyAutoFeedEnable !== false; const normalized = { showUI: this.toBoolCompat(merged.showUI), dpi_x: this.toIntCompat(merged.dpi_x, 300), dpi_y: this.toIntCompat(merged.dpi_y, 300), deviceIndex: this.toIntCompat(merged.deviceIndex, 0), showDialog: this.toBoolCompat(merged.showDialog), scanSource: this.normalizeScanSource(merged.scanSource, merged.autoFeedEnable), autoFeed: this.toBoolCompat(merged.autoFeed), dupxMode: this.toBoolCompat(merged.dupxMode), autoDeskew: this.toBoolCompat(merged.autoDeskew), autoBorderDetection: this.toBoolCompat(merged.autoBorderDetection), colorMode: merged.colorMode || "RGB", transMode: merged.transMode || "memory" }; if (includeLegacyAutoFeedEnable) { normalized.autoFeedEnable = this.getLegacyAutoFeedEnable(normalized.scanSource); } return normalized; } recordServerProtocolVersion(msg) { const version = this.toIntCompat(msg && msg.protocolVersion, 1); this.serverProtocolVersion = version; return version; } applyIncomingScanConfig(msg) { this.scaner_work_config.deviceIndex = this.toIntCompat(msg.currentIndex, this.scaner_work_config.deviceIndex); if (Object.prototype.hasOwnProperty.call(msg, "showDialog")) { this.scaner_work_config.showDialog = this.toBoolCompat(msg.showDialog); } this.scaner_work_config.scanSource = this.normalizeScanSource(msg.scanSource, msg.autoFeedEnable); if (Object.prototype.hasOwnProperty.call(msg, "autoFeed")) { this.scaner_work_config.autoFeed = this.toBoolCompat(msg.autoFeed); } if (Object.prototype.hasOwnProperty.call(msg, "dupxMode")) { this.scaner_work_config.dupxMode = this.toBoolCompat(msg.dupxMode); } if (Object.prototype.hasOwnProperty.call(msg, "autoDeskew")) { this.scaner_work_config.autoDeskew = this.toBoolCompat(msg.autoDeskew); } if (Object.prototype.hasOwnProperty.call(msg, "autoBorderDetection")) { this.scaner_work_config.autoBorderDetection = this.toBoolCompat(msg.autoBorderDetection); } if (typeof msg.colorMode === "string" && msg.colorMode) { this.scaner_work_config.colorMode = msg.colorMode; } if (typeof msg.transMode === "string" && msg.transMode) { this.scaner_work_config.transMode = msg.transMode; } this.scaner_work_config.autoFeedEnable = this.getLegacyAutoFeedEnable(this.scaner_work_config.scanSource); } normalizeIncomingMessage(rawMsg) { const msg = Object.assign({}, rawMsg || {}); const protocolVersion = this.recordServerProtocolVersion(msg); const rawType = typeof msg.cmd_type === "string" ? msg.cmd_type : ""; if (!msg.scanSource) { msg.scanSource = this.normalizeScanSource(msg.scanSource, msg.autoFeedEnable); } switch (rawType) { case "getAllImage": msg.cmd_type = "imageListSnapshot"; break; case "getImageById": msg.cmd_type = "imageSnapshot"; break; case "imageDrap": msg.cmd_type = "imageMoved"; break; } if (!msg.protocolVersion) { msg.protocolVersion = protocolVersion; } this.normalizeUploadMessage(msg); return msg; } onSocketMessage(event) { const rawMsg = JSON.parse(event.data); const msg = this.normalizeIncomingMessage(rawMsg); if (typeof msg.imageCount === "number") { this.imageCount = msg.imageCount; } switch (msg.cmd_type) { case "getDevicesList": this.dispatchCallback("onGetDevicesListEvent", msg); break; case "scanComplete": this.dispatchCallback("onScanFinishedEvent", msg); break; case "selectScanDevice": this.applyIncomingScanConfig(msg); this.dispatchCallback("onSelectScanDeviceEvent", msg); break; case "getImageCount": this.dispatchCallback("onGetImageCountEvent", msg); break; case "imageListSnapshot": this.dispatchCallback("onImageListSnapshotEvent", msg); this.dispatchCallback("onGetAllImageEvent", msg); break; case "imageSnapshot": this.dispatchCallback("onImageSnapshotEvent", msg); this.dispatchCallback("onGetImageByIdEvent", msg); break; case "scanPageAdded": this.dispatchCallback("onScanPageAddedEvent", msg); break; case "loadImageFromUrl": this.dispatchCallback("onLoadImageFromUrlEvent", msg); break; case "loadImageFromBase64": this.dispatchCallback("onLoadImageFromBase64Event", msg); break; case "rotateImage": this.dispatchCallback("onRotateImageEvent", msg); break; case "getImageSize": this.dispatchCallback("onGetImageSizeEvent", msg); break; case "uploadAllImageAsPdfToUrl": this.dispatchCallback("onUploadAllImageAsPdfToUrlEvent", msg); break; case "uploadAllImageAsTiffToUrl": this.dispatchCallback("onUploadAllImageAsTiffToUrlEvent", msg); break; case "uploadJpgImageByIndex": this.dispatchCallback("onUploadJpgImageByIndexEvent", msg); break; case "setUploadRequestHeaders": this.dispatchCallback("onSetUploadRequestHeadersEvent", msg); break; case "upload": this.dispatchCallback("onUploadEvent", msg); break; case "openClientLocalMultipageFile": this.dispatchCallback("onOpenClientLocalMultipageFileEvent", msg); break; case "imageEdited": this.dispatchCallback("onImageEditedEvent", msg); break; case "imageMoved": this.dispatchCallback("onImageMovedEvent", msg); this.dispatchCallback("onImageDrapEvent", msg); break; case "imageDeleted": this.dispatchCallback("onImageDeletedEvent", msg); break; case "imagesCleared": this.dispatchCallback("onImagesClearedEvent", msg); break; } } normalizeOutgoingCommand(commandData) { const cmd = Object.assign({}, commandData || {}); cmd.protocolVersion = this.protocolVersion; if (cmd.cmd_type === "startScan") { cmd.config = this.normalizeScanConfig(cmd.config || this.scaner_work_config, { includeLegacyAutoFeedEnable: this.shouldIncludeLegacyAutoFeedEnable() }); } return cmd; } sendWebSocketCommand(commandData) { try { if (this.h5socket && this.h5socket.readyState === 1) { this.h5socket.send(JSON.stringify(this.normalizeOutgoingCommand(commandData))); } else { alert("发送扫描指令失败!请刷新页面或者检查托盘扫描程序是否已经正常运行!"); } } catch (e) { alert("发送扫描指令失败!" + e); } } setLicenseKey(licenseMode, key1, key2, licenseServerUrl) { this.sendWebSocketCommand({ cmd_type: "setLicenseKey", licenseMode: licenseMode, key1: key1, key2: key2, url: licenseServerUrl }); } loadDevices() { this.sendWebSocketCommand({ cmd_type: "getDevicesList" }); } selectScanDevice(deviceIndex) { this.sendWebSocketCommand({ cmd_type: "selectScanDevice", deviceIndex: deviceIndex }); } startScan() { this.sendWebSocketCommand({ cmd_type: "startScan", config: this.scaner_work_config }); } clearAll() { this.sendWebSocketCommand({ cmd_type: "clearAll" }); } getImageCount() { this.sendWebSocketCommand({ cmd_type: "getImageCount" }); } getAllImage() { this.sendWebSocketCommand({ cmd_type: "getAllImage" }); } getImageById(indexOrImageId) { const command = { cmd_type: "getImageById" }; if (typeof indexOrImageId === "string" && indexOrImageId.trim()) { command.imageId = indexOrImageId.trim(); } else { command.index = indexOrImageId; } this.sendWebSocketCommand(command); } loadImageFromUrl(url, type) { if (!type) { const extension = url.split(".").pop().toLowerCase(); if (extension === "pdf") { type = "pdf"; } else if (extension === "tiff" || extension === "tif") { type = "tiff"; } else if (["jpg", "jpeg", "png", "bmp", "gif", "webp"].includes(extension)) { type = "image"; } else { type = "image"; } } this.sendWebSocketCommand({ cmd_type: "loadImageFromUrl", url: url, type: type }); } loadImageFromBase64(base64Data, format) { let normalizedFormat = format || "jpg"; let normalizedBase64 = base64Data; if (normalizedBase64.startsWith("data:")) { const base64Index = normalizedBase64.indexOf("base64,"); if (base64Index !== -1) { if (!format) { const mimeMatch = normalizedBase64.match(/data:image\/(\w+);/); if (mimeMatch && mimeMatch[1]) { normalizedFormat = mimeMatch[1] === "jpeg" ? "jpg" : mimeMatch[1]; } } normalizedBase64 = normalizedBase64.substring(base64Index + 7); } } this.sendWebSocketCommand({ cmd_type: "loadImageFromBase64", base64Data: normalizedBase64, format: normalizedFormat }); } rotateImage(index, angle) { this.sendWebSocketCommand({ cmd_type: "rotateImage", index: index, angle: angle }); } getImageSize(index) { this.sendWebSocketCommand({ cmd_type: "getImageSize", index: index }); } deleteImageByIndex(index, imageId) { const command = { cmd_type: "deleteImageByIndex", index: index }; if (typeof imageId === "string" && imageId.trim()) { command.imageId = imageId.trim(); } this.sendWebSocketCommand(command); } moveImage(oldIndex, newIndex) { this.sendWebSocketCommand({ cmd_type: "moveImage", oldIndex: oldIndex, newIndex: newIndex }); } setUploadRequestHeaders(headers) { this.sendWebSocketCommand({ cmd_type: "setUploadRequestHeaders", headers: this.normalizeUploadRequestHeadersInput(headers) }); } uploadAllImageAsPdfToUrl(url, id, desc) { this.sendWebSocketCommand({ cmd_type: "uploadAllImageAsPdfToUrl", url: url, id: id, desc: desc }); } uploadAllImageAsTiffToUrl(url, id, desc) { this.sendWebSocketCommand({ cmd_type: "uploadAllImageAsTiffToUrl", url: url, id: id, desc: desc }); } uploadJpgImageByIndex(url, id, desc, index) { this.sendWebSocketCommand({ cmd_type: "uploadJpgImageByIndex", index: index, url: url, id: id, desc: desc }); } saveAllImageToLocal(filename) { this.sendWebSocketCommand({ cmd_type: "saveAllImageToLocal", filename: filename }); } openClientLocalfile() { this.sendWebSocketCommand({ cmd_type: "openClientLocalfile" }); } openClientLocalMultipageFile() { this.sendWebSocketCommand({ cmd_type: "openClientLocalMultipageFile" }); } ftpUploadAllImage(serverIp, port, username, password, serverPath, filename) { this.sendWebSocketCommand({ cmd_type: "ftpUploadAllImage", serverIp: serverIp, port: port, username: username, password: password, serverPath: serverPath, filename: filename }); } setUploadButtonVisible(visible) { this.sendWebSocketCommand({ cmd_type: "setUploadButtonVisible", visible: visible }); } setFocus() { this.sendWebSocketCommand({ cmd_type: "focus" }); } hidden() { this.sendWebSocketCommand({ cmd_type: "hidden" }); } closeWebSocket() { this.h5socket.close(); } loadMultipleImagesFromBase64(base64Images) { if (!Array.isArray(base64Images) || base64Images.length === 0) { console.error("loadMultipleImagesFromBase64: 参数必须是非空数组"); return; } base64Images.forEach((image, index) => { setTimeout(() => { this.loadImageFromBase64(image.data, image.format || "jpg"); }, index * 100); }); } loadImageFromCanvas(canvas, format, quality) { const normalizedFormat = format || "jpg"; const normalizedQuality = typeof quality === "number" ? quality : 0.9; if (!canvas || !(canvas instanceof HTMLCanvasElement)) { console.error("loadImageFromCanvas: 参数必须是有效的Canvas元素"); return; } const mimeType = normalizedFormat === "jpg" ? "image/jpeg" : "image/" + normalizedFormat; const dataUrl = canvas.toDataURL(mimeType, normalizedQuality); const base64Data = dataUrl.split(",")[1]; this.loadImageFromBase64(base64Data, normalizedFormat); } loadImageFromFileInput(fileInput) { if (!fileInput || !fileInput.files || fileInput.files.length === 0) { console.error("loadImageFromFileInput: 没有选择文件"); return; } const files = Array.from(fileInput.files); files.forEach((file, index) => { if (!file.type.startsWith("image/")) { console.warn("跳过非图像文件: " + file.name); return; } const reader = new FileReader(); reader.onload = e => { const dataUrl = e.target.result; const extension = file.name.split(".").pop().toLowerCase(); const format = extension === "jpeg" ? "jpg" : extension; setTimeout(() => { this.loadImageFromBase64(dataUrl, format); }, index * 100); }; reader.readAsDataURL(file); }); } } if (typeof module !== "undefined" && module.exports) { module.exports = ScanOnWeb; } if (typeof window !== "undefined") { window.ScanOnWeb = ScanOnWeb; } return ScanOnWeb; }));