scanonweb
Version:
ScanOnWeb - 扫描控件 JavaScript SDK,支持 v2 协议、scanSource 和增量图像事件
655 lines (652 loc) • 21.2 kB
JavaScript
/*!
* scanonweb v2.0.1
* ScanOnWeb - 扫描控件 JavaScript SDK,支持 v2 协议、scanSource 和增量图像事件
* https://www.brainysoft.cn
*
* Copyright (c) 2026 BrainySoft
* Licensed under the MIT license
*/
'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;
}
module.exports = ScanOnWeb;