虚拟扫描仪File Transfer 模式
1. 需求
虚拟扫描仪需要在原有 Native Transfer 之外,再支持 TWAIN File Transfer Mode,使得扫描应用可以选择让 DS 直接把扫描结果写入磁盘文件,再回传文件路径。
主要需求:
- 支持
ICAP_XFERMECH = TWSX_FILE。 - 支持
ICAP_IMAGEFILEFORMAT,可选 PNG / JPG / BMP / TIFF。 - 支持
DAT_SETUPFILEXFER的MSG_GET / MSG_GETDEFAULT / MSG_SET / MSG_RESET。 - 支持
DAT_IMAGEFILEXFER的MSG_GET,完成实际文件写入并回传TW_SETUPFILEXFER。 - 支持两种文件路径来源:
- 应用通过
DAT_SETUPFILEXFER / MSG_SET指定的目标路径(XnView "Scan to..." 等应用使用)。 - settings UI 中用户选择的输出目录 + 文件名 + 格式。
- 当应用已自带文件路径时,settings UI 应隐藏输出相关字段,避免误导用户和覆盖应用路径。
- 文件 DPI 元数据(PNG
pHYs、JPG JFIF density、BMPbiXPelsPerMeter、TIFFXResolution等)必须与 settings UI 中选择的 DPI 一致。 - 文件名扩展名必须和所选格式匹配(PNG →
.png,JPG →.jpg,BMP →.bmp,TIFF →.tif)。 - 不支持的文件格式或路径不存在的目录,DS 不应崩溃,并通过 TWAIN 状态码报告失败。
非功能性需求:
- File Transfer 流程必须遵守 TWAIN 状态机:在 State 6 (
kXferReady) 进入文件写入,写入成功后转入 State 7 (kXferring),由应用调用DAT_PENDINGXFERS / MSG_ENDXFER结束。 - 应用可以在
MSG_XFERREADY之后才调用DAT_SETUPFILEXFER / MSG_SET(如 TWACK 这种顺序),DS 必须延迟到DAT_IMAGEFILEXFER / MSG_GET时才执行真正的文件写入。
2. 领域知识
2.1 TWAIN 的三种传输机制
TWAIN 标准定义了 ICAP_XFERMECH 的三种值:
| 取值 | 含义 |
|---|---|
TWSX_NATIVE |
DS 把整幅图作为 DIB 句柄 (TW_HANDLE) 一次性返回。 |
TWSX_FILE |
DS 把图像写入磁盘文件,回传文件路径和格式。 |
TWSX_MEMORY |
DS 按 strip 把图像数据分块返回。本项目暂不支持。 |
本项目当前实现 TWSX_NATIVE + TWSX_FILE。CAP_XFERMECH 默认是 TWSX_NATIVE,可由应用或 settings UI 切换为 TWSX_FILE。
2.2 File Transfer 涉及的 TWAIN triples
File Transfer 模式下,DSM 与 DS 之间通常按以下顺序交互:
1. DG_CONTROL / DAT_CAPABILITY / MSG_SET -> ICAP_XFERMECH = TWSX_FILE
2. DG_CONTROL / DAT_CAPABILITY / MSG_SET -> ICAP_IMAGEFILEFORMAT = TWFF_PNG
3. DG_CONTROL / DAT_SETUPFILEXFER / MSG_SET -> 指定文件路径和 Format
4. DG_CONTROL / DAT_USERINTERFACE / MSG_ENABLEDS
5. DG_CONTROL / DAT_EVENT / MSG_PROCESSEVENT -> 等待 MSG_XFERREADY
6. (state 6) 应用可再调用 DAT_SETUPFILEXFER / MSG_SET 更新路径
7. DG_IMAGE / DAT_IMAGEINFO / MSG_GET
8. DG_IMAGE / DAT_IMAGEFILEXFER / MSG_GET -> DS 写文件,回传路径
9. DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER
10. DG_CONTROL / DAT_USERINTERFACE / MSG_DISABLEDS
2.3 TW_SETUPFILEXFER
DAT_SETUPFILEXFER 与 DAT_IMAGEFILEXFER 共用 TW_SETUPFILEXFER 结构体,主要字段:FileName(最长 255 字符路径)、Format(TWFF_PNG / TWFF_JFIF / TWFF_BMP / TWFF_TIFF ...)、VRefNum(Mac 残留字段,本项目固定为 0)。
2.4 应用驱动 vs UI 驱动两种路径来源
- 应用驱动(如 XnView 的 "Scan to..."):应用先弹自家对话框选目录和文件名,再以
DAT_SETUPFILEXFER / MSG_SET把路径传给 DS。DS 不应再让用户在 settings UI 里选输出目录。 - UI 驱动(如 TWAIN 测试工具开启 File 模式但不指定路径):用户在 DS 的 settings UI 中选择输出目录、文件名和格式,DS 自己生成文件路径。
- 混合(如 TWACK 某些版本):先
MSG_ENABLEDS,settings UI 弹出,关闭后才调用DAT_SETUPFILEXFER / MSG_SET。DS 必须延迟写文件,并避免在 settings UI 中覆盖应用即将提供的路径。
2.5 DPI 元数据写入
FreeImage FreeImage_Save 在不同格式下对 DPI 字段的处理不一致:
- BMP 一般正确写入
biXPelsPerMeter/biYPelsPerMeter。 - PNG 通常写入
pHYschunk,但部分版本会忽略。 - JPG 经常不写 JFIF APP0 density 或写错单位。
- TIFF 通常正确写入
XResolution/YResolution/ResolutionUnit。
为了保证 Windows 资源管理器 "属性 → 详细信息" 始终能读到正确的水平/垂直分辨率,本项目在每次保存后都会再用自实现的 patcher 重写一遍 DPI 字段(见 patchSavedDpiMetadata 及对应的 patchPngDpiMetadata / patchJpegDpiMetadata / patchBmpDpiMetadata / patchTiffDpiMetadata)。
3. 设计目标
- 支持完整的 File Transfer 状态机,覆盖
DAT_SETUPFILEXFER全部四种消息和DAT_IMAGEFILEXFER / MSG_GET。 - 支持应用驱动和 UI 驱动两种文件路径来源,并通过 settings UI 的
app_managed_file_output标志区分。 - 与 Native Transfer 共用同一个图像生成管线(
acquireImage+preScanPrep),仅在最后一步选择 DIB 返回还是磁盘写入。 - 文件 DPI 元数据始终与 settings UI 中的 DPI 一致,覆盖 PNG / JPG / BMP / TIFF。
- 设计上为后续扩展更多文件格式(如多页 TIFF、PDF 等)保留入口。
非目标:
- 不支持
TWSX_MEMORY。 - 不支持多页文件(即使 TIFF 也只写单页)。
- 不支持加密压缩选项;JPG 质量固定为 85。
- 不支持自定义 PDF / OCR 输出。
4. 总体设计
File Transfer 在已有 Native Transfer 模块上以最小侵入的方式叠加:
TwainDataSource
├── handleDatSetupFileXfer() // 路径协商
├── handleDatImageFileXfer() // 触发写文件 + 回传路径
└── enableDs() // 在 ShowUI=TRUE 时联动 settings UI
│
├── SettingsServer (HTML UI)
│ └── app_managed_file_output 决定是否显示输出字段
│
└── VirtualScanner
├── acquireImage() + preScanPrep()
├── saveImageToFile() // UI 提供 dir + filename + format
├── saveImageToPath() // 应用直接提供完整路径
├── applyDpiMetadata() // 写入 FreeImage 内部 DPI
└── patchSavedDpiMetadata() // 二次修补 PNG/JPG/BMP/TIFF 容器
关键流程:
- 应用设置
ICAP_XFERMECH = TWSX_FILE、ICAP_IMAGEFILEFORMAT = ...。 - 应用可选调用
DAT_SETUPFILEXFER / MSG_SET,DS 记录到app_file_path_,并根据扩展名校正ICAP_IMAGEFILEFORMAT。 MSG_ENABLEDS:- 若
ShowUI=TRUE,弹出 settings UI;- 若
cur_mech == TWSX_FILE,则app_managed_file_output = true,UI 不显示输出字段; - 否则用户可以在 UI 里勾选 File 模式并选输出目录 / 文件名 / 格式。
- 若
- 调用
acquireImage()把图像准备好,但不写文件。 - 发送
MSG_XFERREADY。 - 应用收到
MSG_XFERREADY后,可再次调用DAT_SETUPFILEXFER / MSG_SET更新路径。 - 应用调用
DAT_IMAGEFILEXFER / MSG_GET: app_file_path_非空 → 调用saveImageToPath();- 否则 → 调用
saveImageToFile(),使用 UI 选择的目录 / 文件名 / 格式。 - 写入成功后回填
data->FileName / Format / VRefNum,状态转kXferring,返回TWRC_XFERDONE。 - 应用
DAT_PENDINGXFERS / MSG_ENDXFER结束,DS 清理app_file_path_。
5. 重要决策和原因
5.1 延迟到 DAT_IMAGEFILEXFER / MSG_GET 才写文件
决策:enableDs() 内只调用 acquireImage() 完成像素准备,文件写入延迟到 DAT_IMAGEFILEXFER / MSG_GET。
原因:
- TWAIN 规范允许应用在 State 6 (
MSG_XFERREADY之后) 才调用DAT_SETUPFILEXFER / MSG_SET,TWACK 等工具也确实这么做。 - 如果在
enableDs()就写文件,应用提供的新路径会被忽略,导致文件落在错的位置。 - 延迟写入只多保留一份内存 DIB,对内存影响可控。
5.2 同时支持 saveImageToFile() 与 saveImageToPath()
决策:在 VirtualScanner 中提供两条保存接口:
saveImageToFile():使用output_dir_+output_filename_+output_format_(来自 settings UI)。saveImageToPath(path):直接保存到指定绝对/相对路径,格式从扩展名推断。
原因:
- 应用驱动场景(XnView "Scan to...")必须严格落到应用指定的路径,DS 不能改名、不能改目录。
- UI 驱动场景需要 DS 自己生成时间戳文件名,并写入用户选择的目录。
- 两条路径都共享
applyDpiMetadata+patchSavedDpiMetadata,保证 DPI 元数据一致。
5.3 settings UI 引入 app_managed_file_output 标志
决策:在 enableDs() 中判断当前 ICAP_XFERMECH,若已是 TWSX_FILE,则把 ui_result.app_managed_file_output 设为 true,settings UI 隐藏输出目录、文件名、格式等控件。
原因:
- 在应用驱动场景下,输出位置完全由应用决定,UI 字段会误导用户,让用户以为自己改了目录有效。
- 隐藏字段后用户只能改颜色模式、分辨率、纸张大小等 DS 内部设置。
- 避免 settings UI 在合并
ScannerSettings时覆盖应用即将提供的app_file_path_。
5.4 扩展名推断 + ICAP_IMAGEFILEFORMAT 自动校正
决策:DAT_SETUPFILEXFER / MSG_SET 收到路径后,会根据扩展名(.png / .jpg|.jpeg / .bmp / .tif|.tiff)推断格式,并调用 caps_.setCurrentValue(ICAP_IMAGEFILEFORMAT, ff)。
原因:
- 部分应用不会在
MSG_SET时提供合法Format,只填路径;缺省值经常是0或TWFF_PNG。 - 写出的扩展名必须与文件格式匹配,否则 Explorer 和图像应用都无法识别。
- 自动校正
ICAP_IMAGEFILEFORMAT使后续MSG_GET返回的Format与实际写入的文件一致。
5.5 文件 DPI 元数据二次修补
决策:保存后再用本项目的 patchPngDpiMetadata / patchJpegDpiMetadata / patchBmpDpiMetadata / patchTiffDpiMetadata 修补一次。
原因:
- FreeImage 在不同版本/不同格式下对 DPI 元数据的写入不稳定,特别是 PNG
pHYs和 JPG JFIF density。 - 修补后能保证 Windows 资源管理器 "属性 → 详细信息" 中的水平/垂直分辨率显示正确。
- 集中放在
patchSavedDpiMetadata内调度,新增格式只需扩展这一处。
5.6 MSG_RESET 清空 app_file_path_
决策:DAT_SETUPFILEXFER / MSG_RESET 清空 app_file_path_,并 fall through 到 MSG_GET 返回当前(空)路径和当前 ICAP_IMAGEFILEFORMAT。
原因:
- 符合 TWAIN MSG_RESET 语义:把当前值复位为默认值,本项目默认为 "无应用提供路径",回退到 UI 驱动。
closeDs()也会清空app_file_path_,避免跨会话残留。
6. 架构各组件改动点
6.1 capability.cpp
ICAP_XFERMECH增加TWSX_FILE到可选值列表。- 新增
ICAP_IMAGEFILEFORMAT,默认TWFF_PNG,可选TWFF_TIFF / TWFF_BMP / TWFF_JFIF / TWFF_PNG。 - 与
ICAP_XFERMECH一并暴露给CAP_SUPPORTEDCAPS。
6.2 twain_data_source.h / .cpp
- 新增成员
std::string app_file_path_,记录应用通过DAT_SETUPFILEXFER / MSG_SET提供的路径。 - 新增 dispatch 入口:
DAT_SETUPFILEXFER → handleDatSetupFileXfer()DAT_IMAGEFILEXFER → handleDatImageFileXfer()handleDatSetupFileXfer:MSG_SET:记录路径、按扩展名校正ICAP_IMAGEFILEFORMAT。MSG_GET / MSG_GETDEFAULT:回传app_file_path_与当前ICAP_IMAGEFILEFORMAT。MSG_RESET:清空app_file_path_,然后 fall-through 到MSG_GET。handleDatImageFileXfer:- 必须在
kXferReady状态。 - 根据
app_file_path_决定saveImageToPath()还是saveImageToFile()。 - 回填
data->FileName / Format / VRefNum,状态切到kXferring,返回TWRC_XFERDONE。 enableDs():- 当
ShowUI=TRUE且ICAP_XFERMECH == TWSX_FILE时,置ui_result.app_managed_file_output = true,隐藏 UI 输出字段。 - settings UI 结果回写到
ICAP_XFERMECH / ICAP_IMAGEFILEFORMAT与VirtualScanner的output_dir_ / output_format_ / output_filename_。 - 不论 Native 还是 File 模式,都先
acquireImage(),再发MSG_XFERREADY。 closeDs():清理app_file_path_,避免跨会话残留。
6.3 virtual_scanner.h / .cpp
- 新增成员:
output_dir_ / output_format_ / output_filename_ / last_saved_file_。 - 新增方法:
setOutputDir / setOutputFormat / setOutputFilenamesaveImageToFile():组合output_dir_+output_filename_+ 格式扩展名。空文件名自动生成scan_YYYYMMDD_HHMMSS时间戳。SHCreateDirectoryExA确保目录存在。saveImageToPath(path):解析相对路径,按扩展名匹配FREE_IMAGE_FORMAT,确保父目录存在。getLastSavedFilePath():供handleDatImageFileXfer回填FileName。- 在保存路径上统一调用
applyDpiMetadata()(写 FreeImage 内部 DPI + EXIF)和patchSavedDpiMetadata()(PNG/JPG/BMP/TIFF 容器级补写)。 - 支持的
FREE_IMAGE_FORMAT:FIF_PNG / FIF_JPEG / FIF_BMP / FIF_TIFF,与ICAP_IMAGEFILEFORMAT的枚举一一对应。
6.4 settings_server.cpp (HTML UI)
SettingsUiResult新增字段:transfer_mode:0 = Native,1 = File。file_format:0/1/2/3 → PNG/JPG/BMP/TIFF。output_dir / output_filename:字符数组。app_managed_file_output:bool,应用是否已经接管输出路径。- HTML 在
app_managed_file_output == true时仅显示一行说明文字,隐藏 transfer mode / format / output 字段。 - 在 Native 默认场景下预填一个
scan_YYYYMMDD_HHMMSS文件名,方便用户切到 File 模式时直接扫描。 - 通过 JS 动态控制 Format / Output Dir / Output Filename 行的显示,并把
transfer_mode = 1时的扩展名联动显示。 - 新增
/browse端点调用SHBrowseForFolderW,让用户图形化选择输出目录。
6.5 文件 DPI 修补模块
patchPngDpiMetadata:插入或替换pHYschunk,单位 1(pixels per meter)。patchJpegDpiMetadata:在 JFIF APP0 中写入 density,单位为 dots per inch。patchBmpDpiMetadata:直接覆盖BITMAPINFOHEADER.biXPelsPerMeter / biYPelsPerMeter。patchTiffDpiMetadata:替换XResolution / YResolution / ResolutionUnit标签。
这些 patcher 既服务于 File Transfer,也服务于 Native Transfer 应用自己保存文件的链路。
7. 典型流程示例
7.1 XnView "Scan to..."(应用驱动)
1. App: ICAP_XFERMECH = TWSX_FILE
2. App: DAT_SETUPFILEXFER / MSG_SET, FileName="D:\out\page.tif", Format=TWFF_TIFF
3. App: MSG_ENABLEDS, ShowUI=TRUE
DS: app_managed_file_output=true -> UI 隐藏输出区
用户选 600 DPI / RGB -> 点 Scan
DS: acquireImage(), MSG_XFERREADY
4. App: DAT_IMAGEINFO / MSG_GET
DS: 返回 600 DPI 的 TW_IMAGEINFO
5. App: DAT_IMAGEFILEXFER / MSG_GET
DS: saveImageToPath("D:\out\page.tif") + patchTiffDpiMetadata
回传 FileName / Format=TWFF_TIFF / VRefNum=0, TWRC_XFERDONE
6. App: DAT_PENDINGXFERS / MSG_ENDXFER -> MSG_DISABLEDS
7.2 settings UI 驱动
1. App: MSG_ENABLEDS, ShowUI=TRUE
DS: ICAP_XFERMECH 当前为 TWSX_NATIVE -> UI 显示完整输出区
用户选 File / PNG / D:\scans\ / 默认时间戳文件名 -> Scan
DS: setCurrentValue(ICAP_XFERMECH, TWSX_FILE)
setCurrentValue(ICAP_IMAGEFILEFORMAT, TWFF_PNG)
scanner_.setOutputDir / setOutputFormat / setOutputFilename
acquireImage(), MSG_XFERREADY
2. App: DAT_IMAGEFILEXFER / MSG_GET
DS: app_file_path_ 为空 -> saveImageToFile()
生成 D:\scans\scan_20260526_153012.png
patchPngDpiMetadata -> 回传路径 + Format=TWFF_PNG
3. App: MSG_ENDXFER -> MSG_DISABLEDS
8. 限制
- 不支持
TWSX_MEMORYstrip 模式;某些只识别 Memory 模式的旧应用会无法工作。 - 不支持多页文件输出(多页 TIFF / PDF)。
pending_xfers_.Count固定为 1,应用每次只能拿到一张图。 - JPG 压缩质量写死为 85,没有
ICAP_IMAGEFILEXFER相关的质量协商。 ICAP_IMAGEFILEFORMAT与output_format_(0=PNG, 1=JPG, 2=BMP, 3=TIFF)的映射写在两个文件里(twain_data_source.cpp与virtual_scanner.cpp),如果未来增加格式需要同步两处。saveImageToPath仅按扩展名识别格式:扩展名缺失或拼写错误时一律回落 PNG,与应用期望可能不一致。- 没有
TW_SETUPFILEXFER2,因此最长路径仍受TW_STR255的 255 字符限制,超出会被截断。 - File Transfer 写文件是同步阻塞的,大图 + 慢盘场景下会阻塞 TWAIN 消息线程;应用层可能观察到短暂无响应。
- 写文件失败时只返回
TWCC_BUMMER,没有更细粒度的错误码(磁盘满、权限不足、目录不存在等无法区分)。 - settings UI 与
app_managed_file_output的判断依赖于enableDs()调用时刻的ICAP_XFERMECH,如果应用先ENABLEDS再SET XFERMECH会出现 UI 与最终模式不一致(目前未观察到这种顺序,但规范上不禁止)。
9. 下一步工作
- 评估并实现
TWSX_MEMORYstrip 传输,覆盖只支持 Memory 模式的老应用。 - 引入多页 TIFF / PDF 支持,使
pending_xfers_.Count可大于 1,匹配 ADF 模拟场景(如果将来支持 ADF)。 - 暴露 JPG 质量、PNG 压缩等级等设置项,可通过 settings UI 或独立 capability 协商。
- 把文件保存挪到 worker 线程,避免长时间阻塞 TWAIN 消息线程,并在 settings UI 上展示进度。
- 把格式映射(
ICAP_IMAGEFILEFORMAT<->FREE_IMAGE_FORMAT<-> extension <-> UI index)集中到一个表里,消除三处重复。 - 给
saveImageToPath增加 fallback:扩展名识别失败时改用ICAP_IMAGEFILEFORMAT当前值,而不是固定 PNG。 - 增加更细的错误码:磁盘满 ->
TWCC_LOWMEMORY/ 自定义,权限拒绝 ->TWCC_DENIED(如可用),目录不存在并创建失败 -> 单独提示。 - 增加
TW_SETUPFILEXFER2支持,突破 255 字符的路径长度限制(需要长路径感知)。 - 在 TWACK / XnView / NAPS2 / Twack2 等多个应用上做集成测试,记录每个应用对
DAT_SETUPFILEXFER/DAT_IMAGEFILEXFER的调用顺序。 - 给 settings UI 增加 "File Transfer 由应用接管" 的更明显视觉提示(图标 + 当前路径预览)。