Hệ thống hành chính điện tử có một yêu cầu không thể tránh khỏi: điều khiển máy quét tài liệu trực tiếp từ trình duyệt web và tải ảnh扫描 lên hệ thống. Năm 2010, tôi đã viết một điều khiển ActiveX bằng VC++ sử dụng giao thức TWAIN để giao tiếp với máy quét, xử lý ảnh xám và nén trước khi tải lên qua HTTP POST. Hiện nay IE đã ngừng hỗ trợ, ActiveX không còn khả dụng. Giải pháp thay thế là cài đặt một dịch vụ HTTP cục bộ trên máy khách, frontend gọi tới qua WebSocket.
Bối cảnh thực tế
Tại quầy dịch vụ hành chính, người dân đưa tài liệu, nhân viên đặt vào máy quét, nhấn nút "Quét" trên trang web, tài liệu tự động nhập vào hệ thống.
Nghe đơn giản nhưng rất phức tạp. Trình duyệt vì lý do bảo mật không thể truy cập trực tiếp phần cứng cục bộ. Hạn chế này đã tồn tại từ thời Netscape và vẫn không thay đổi.
Giải pháp năm 2010: ActiveX kết hợp TWAIN
Khi đó hệ thống hành chính chạy hoàn toàn trên IE, IE hỗ trợ điều khiển ActiveX - một thành phần COM nhúng vào trang web với quyền truy cập cục bộ, có thể điều khiển trực tiếp phần cứng.
Tôi dùng VC++ 6.0 viết điều khiển DocScanner với các chức năng chính:
1. Giao tiếp máy quét qua TWAIN
TWAIN là giao thức truyền thông tiêu chuẩn cho máy quét, hầu hết các loại đều hỗ trợ. Điều khiển kế thừa lớp CTwain, đóng gói các lời gọi DSM:
// DocScannerCtrl.h
class CDocScannerCtrl : public COleControl, public CTwain
{
afx_msg long BeginCapture(); // JS gọi để bắt đầu quét
afx_msg void TerminateCapture(); // JS gọi để hủy quét
void OnImageReady(HANDLE hDib, TW_IMAGEINFO& imgInfo);
void FireScanComplete(); // Callback khi quét xong
};
JavaScript gọi BeginCapture(), điều khiển mở máy quét qua TWAIN và lấy ảnh từng trang:
long CDocScannerCtrl::BeginCapture()
{
m_scanIndex = 0;
m_totalPages = 0;
m_savedCount = 0;
if (!HasValidSource()) {
PromptSourceSelection();
}
StartAcquisition(TWCPP_ANYCOUNT, m_showUI);
return 0;
}
2. Xử lý ảnh
Ảnh quét ra định dạng DIB, dung lượng rất lớn nếu lưu trực tiếp. Điều khiển dùng GDI+ xử lý:
- Thay đổi kích thước: ảnh rộng hơn 1191px sẽ được thu nhỏ giữ tỉ lệ (1191px tương ứng khổ A4 300DPI)
- Chuyển ảnh xám: chuyển RGB 24-bit sang ảnh xám 8-bit theo công thức
gray = (R*76 + G*150 + B*30) >> 8 - Phân ngưỡng nhị phân: pixel có giá trị xám lớn hơn 220 thành trắng, nhỏ hơn thành đen - tài liệu hành chính thường là giấy trắng chữ đen, nhị phân hóa giúp file nhỏ hơn và rõ hơn
- Nén JPEG: chất lượng 80, kiểm soát dung lượng file
// Thu nhỏ về chiều rộng 1191px
if (origW > MAX_WIDTH) {
newW = MAX_WIDTH;
newH = origH * MAX_WIDTH / origW;
}
// Nén JPEG chất lượng 80
encodeParams.Parameter[0].Guid = EncoderQuality;
long jpegQuality = 80;
encodeParams.Parameter[0].Value = &jpegQuality;
bitmap.Save(outputPath, &jpegClsid, &encodeParams);
3. Tải lên qua HTTP POST
Sau khi quét xong tự động tải lên server, dùng lớp CUploadHelper đóng gói HTTP POST dạng multipart/form-data:
// Tự động tải lên sau khi quét
m_uploader.UploadFile(m_targetUrl, m_uploadPath, m_port, outputPath);
Tải lên thành công thì xóa file tạm cục bộ. Nếu thất bại (server trả mã lỗi 101-500) thì ghi vào m_failedList, JS có thể lấy danh sách qua GetFailedFiles().
4. Tương tác với JavaScript
// Cấu hình tham số
scanner.SetOutputDir("C:\\scandata\\");
scanner.SetServerUrl("http://192.168.1.1");
scanner.SetUploadPath("/upload");
scanner.SetServerPort("8080");
scanner.SetDisplayUI("1");
scanner.SetColorMode("1");
scanner.SetImageFormat("jpg");
// Bắt đầu quét
scanner.BeginCapture();
// Xử lý sự kiện hoàn thành
function scanner::OnScanDone(pageCount) {
alert("Quét hoàn tất, tổng cộng " + pageCount + " trang");
var successFiles = scanner.GetSavedFiles();
var failedFiles = scanner.GetFailedFiles();
}
Toàn bộ luồng: JS gọi BeginCapture → TWAIN điều khiển máy quét → OnImageReady xử lý ảnh → UploadFile tải lên → FireScanComplete thông báo JS.
5. Cài đặt
Điều khiển ActiveX biên dịch thành gói .cab, khi người dùng truy cập trang lần đầu IE tự động nhắc cài đặt. Không cần khởi động dịch vụ riêng, không cần cấu hình cổng, không lo tiến trình bị sập.
Giải pháp hiện tại: Dịch vụ HTTP cục bộ
Chrome và Edge không hỗ trợ plugin trình duyệt truy cập phần cứng. Cách làm hiện tại: cài đặt một dịch vụ HTTP cục bộ (hoặc ứng dụng Electron) trên máy khách, frontend gọi qua WebSocket/HTTP, dịch vụ này giao tiếp TWAIN với máy quét.
Trước đây: Trình duyệt → ActiveX → TWAIN → Máy quét
Hiện nay: Trình duyệt → WebSocket → Dịch vụ cục bộ → TWAIN → Máy quét
Thêm một lớp trung gian, kéo theo các vấn đề mới:
| Khía cạnh | ActiveX | Dịch vụ cục bộ |
|---|---|---|
| Cài đặt | Trình duyệt tự nhắc, cài một lần | Cần cài đặt chương trình dịch vụ riêng |
| Khởi động | Tự động khi mở trang | Phải đảm bảo dịch vụ đang chạy |
| Cổng kết nối | Không cần | Phải cấp phát cổng, có thể xung đột |
| Sự cố tiến trình | Chạy cùng tiến trình trình duyệt | Dịch vụ sập cần khởi động lại |
| Tường lửa | Không liên quan | Phải mở cổng cục bộ |
| Bảo trì | Hầu như không cần | Xử lý ngoại lệ, ghi log, tự khởi động lại |
Bản chất không thay đổi
Dù ActiveX hay dịch vụ cục bộ, công việc thực hiện hoàn toàn giống nhau:
- Vượt qua sandbox của trình duyệt — lấy quyền truy cập phần cứng
- Điều khiển máy quét — qua giao thức TWAIN
- Xử lý ảnh — thay đổi kích thước, chuyển xám, nén
- Tải lên server — HTTP POST
- Thông báo frontend — báo cho JS biết quét xong
2010: ActiveX → 5 bước trên
2020: Dịch vụ cục bộ → vẫn 5 bước đó
2026: Dịch vụ cục bộ → không thay đổi
Công nghệ thay đổi ba vòng, nhưng vấn đề cốt lõi vẫn một: trình duyệt không thể truy cập trực tiếp phần cứng, phải đi đường vòng. Trước đây đường vòng là plugin trình duyệt, nay là dịch vụ cục bộ. Đường dài hơn nhưng đích đến giống nhau.
Mô hình bảo mật của trình duyệt quyết định nó không thể truy cập phần cứng, hạn chế này có từ ngày đầu tiên và không thay đổi. Dù frontend framework thay bao nhiêu lần (jQuery → Angular → React → Vue), máy quét, máy in, đầu đọc thẻ tại các quầy dịch vụ hành chính luôn cần một thành phần nằm ngoài trình duyệt để làm cầu nối.
Trước đây thành phần đó là ActiveX, hiện nay là dịch vụ HTTP cục bộ. Tương lai có thể là WebUSB? Có thể, nhưng khi nào các nhà sản xuất máy quét hỗ trợ WebUSB thì chưa ai biết.