philipyaz/p215-scan

GitHub: philipyaz/p215-scan

一款原生 macOS SwiftUI 应用,旨在替代 Canon P-215II 扫描仪老旧的官方驱动,在单一窗口内完成扫描、图像编辑与可搜索 PDF 导出。

Stars: 0 | Forks: 0

P215 Scan icon

P215 Scan

一款适用于 Canon imageFORMULA P-215II 文档扫描仪的原生 macOS 应用。

build release MIT license macOS 14+, Apple Silicon

![P215 Scan 主窗口](https://static.pigsec.cn/wp-content/uploads/repos/cas/c8/c8c6555d91c9dd4d39ceff141028184a98d47cf40e40c523b1861735b5797ed5.png) Canon 的 CaptureOnTouch Lite 是一款发布于 2019 年的 x86_64 应用,只能在 Rosetta 下运行。本项目用一个原生的 arm64 SwiftUI 应用替代它:扫描、整理、导出带搜索功能的 PDF —— 所有操作都在一个窗口内完成,而不是繁琐的三步向导。 ## 安装 使用 [Homebrew](https://brew.sh)(同时也会安装 SANE backend): ``` HOMEBREW_CASK_OPTS=--no-quarantine brew install --cask philipyaz/tap/p215-scan ``` (Homebrew 6 移除了 `--no-quarantine` 命令行参数;使用环境变量的形式在所有版本的 Homebrew 上都有效。) 或者手动安装:从 [Releases](https://github.com/philipyaz/p215-scan/releases) 下载 zip 压缩包,将 `P215 Scan.app` 拖入 `/Applications`,然后执行 `brew install sane-backends`。 发布的版本未经公证(本项目背后没有付费的 Apple Developer 账号);如果不使用 no-quarantine 选项,macOS 会拒绝首次启动,直到你在 **系统设置 → 隐私与安全性 → 仍要打开** 中手动允许。 或者从源码构建 —— 只需执行一次 `swiftc` 命令,不需要 Xcode 工程: ``` git clone https://github.com/philipyaz/p215-scan.git cd p215-scan/app && ./build.sh && open "build/P215 Scan.app" ``` ## 设置 将扫描仪背面的 **Auto Start** 开关拨至 **OFF**。随后它将作为一个真正的扫描仪(USB `1083:165b`)被枚举,并由 SANE 的 `canon_dr` backend 驱动,该 backend 会对传感器进行校准并提供纯净的图像。 该应用在 Auto Start **开启** 时也可以工作,通过逆向工程的传输协议进行通信(见下文),但此模式下的图像质量要差得多 —— 保留该路径只是因为它不需要拨动开关,而并不意味着它效果更好。 ## 功能 实现 CaptureOnTouch Lite 的功能对等,在单个窗口内完成,而非三步向导。 | | | |---|---| | 色彩模式 | 自动检测、24 位彩色、彩色(照片)、灰度、灰度(照片)、黑白、误差扩散、文本增强 | | 分辨率 | 自动、150、200、300、400、600 dpi | | 页面大小 | 匹配原件、A4、A5、A5R、A6、A6R、B5、B6、B6R、Legal、Letter、扫描仪最大尺寸 | | 扫描面 | 单面、双面 | | 处理 | 跳过空白页、纠偏、裁剪至内容、旋转以匹配文本方向 | | 页面编辑器 | 双击页面:缩放/适应、旋转、纠偏、裁剪、亮度、对比度、灰度、黑白、还原 | | 选择 | 点击、⌘-点击、Shift 范围选择、全选 / 仅选奇数 / 仅选偶数、带撤销删除 | | 输出 | PDF、TIFF、JPEG、PNG;单文件或每页一个文件;压缩支持标准/高质量,并带有 1–5 质量滑块 | | OCR | 支持 14 种语言,包括日文与英文混合,带有隐形文本层,通过 Apple 的 Vision 实现 | | 文件命名 | 基础名 + 日期(YYYYMMDD / MMDDYYYY / DDMMYYYY)+ 时间 + 带可设定位数和起始值的计数器 | | 预设 | 文本、照片、全自动、黑白、彩色,以及自定义预设 —— 支持重命名、复制、删除 | | 一键任务 | 将预设保存为任务:扫描**并**直接保存到对应文件夹,无弹窗 | | 页面管理 | 拖拽排序、带撤销删除、空白页会被标记并隐藏,而非直接丢弃 | 快捷键:**⌘R** 扫描,**⇧⌘R** 继续扫描,**⌘S** 保存,**⌘I** 导入,**⌘A** 全选,**⌥⌘←** / **⌥⌘→** 旋转,**⌘Z** 撤销删除,**⌘,** 偏好设置。(旋转操作刻意避开了 `⌘[` / `⌘]` —— 因为这些组合在瑞士、法国和德国的键盘布局下需要额外的修饰键,导致快捷键会悄无声息地失效。) **导入** 可将现有图片添加为页面,因此即使未连接扫描仪,也可以使用其编辑和导出功能。 ![页面编辑器](https://static.pigsec.cn/wp-content/uploads/repos/cas/e2/e2f59a95d4b474910fbc7557ef443e749a04371206b4023f0e01a30bf724a181.png) ## 端到端验证 针对物理扫描仪测试,使用一张双面打印的纸张(一面是浅色手写内容,另一面是密集的法语文本): ``` raw capture 2544x3300 px both sides (8.48 x 11.00 in @ 300 dpi) ink coverage 0.04% handwriting | 10.58% dense text both sides kept, full size, correct colour export 2-page searchable PDF, 610x792 pt per page OCR 1092 characters extracted — including the handwriting ``` 并通过应用自身的 UI 驱动:导入两页,全选,旋转 180°,保存 —— 最终生成 `~/Documents/Scan_20260727_001.pdf`(2 页,610×792 pt,已通过 PDFKit 验证),并在完成提示横幅上显示 **打开** 和 **在访达中显示**。 ## 踩过的坑及原因 以下每个问题都是经过实际测量的,而非凭空猜测。记录它们是因为每一个坑都是后来者必定会遇到的陷阱。 **`--swcrop` 在这台扫描仪上具有破坏性。** `canon_dr` 的软件裁剪在此无法找到纸张边缘 —— 因为其校准效果太差 —— 导致它直接按*墨迹*进行裁剪。一张完整的 2544×3300 页面被缩成了 494×440,除了手写笔记外的所有内容全部丢失。应用绝不会发送该指令;“匹配原件大小”依赖于扫描仪自身的页面长度检测功能(该功能非常准确),再辅以编辑器中的手动裁剪。 **分通道自动色阶会毁掉彩色纸张。** 将每个色彩通道拉伸至各自的黑/白点能轻易消除色偏,但如果页面中某个通道的色阶分布过窄,这些点就会挤在一起,导致整个页面色彩过饱和。它曾把一张带有紫色墨迹的白纸变成了纯黄色。现在色阶是基于亮度(luminance)进行测量,并均匀应用到全部三个通道上,从而固定黑点并保持色相不受影响。 **扫描仪的黑点严重偏高。** 原始扫描结果中没有任何比 luma ~161 更暗的颜色 —— “黑色”文本实际上变成了中灰色。`canon_dr` 将该型号的校准评价为*差*,这便是其在实际使用中的具体表现。正因如此,应用默认开启了自动色阶;若不开启,每次扫描的图像看起来都会像褪了色一样。 **空白页检测绝不能执行删除。** 过去程序会在传递页面之前执行 `continue`,因此一旦阈值判定失误,就会在没有计数、没有警告且无法撤销的情况下销毁真实的扫描件。现在页面仅会被*标记*,并隐藏在“已隐藏 N 个空白页 — 显示”的标签后。此外,该阈值必须比看起来要低得多:一旦排除纸张边缘,一张带有简短手写笔记的纸张测得的墨水覆盖率仅为 **0.04%**,因此高于此值的任何设定都会隐藏掉真实的页面。 **在合理的分辨率下测量墨迹,并忽略纸张边缘。** 在 200px 的代理图像上,一整页普通文本会平均化呈灰色,从而被判定为空白 —— 一张密集的文本页面测得仅为 0.12% 并被丢弃。而在 1000px 下测量结果为 4.17%。此外,外围 3.5% 的区域必须被排除在外:ADF(自动进纸器)会在边缘留下暗带,仅此暗带就会被读取为约 1.5% 的墨水覆盖率。 **取消扫描必须升级至 SIGKILL。** `Process.terminate()` 会发送 SIGTERM,但在 USB 读取受阻时,`scanimage` 会对此信号置之不理。随后产生的孤儿进程会一直占用 USB 设备,导致后续每次检测都失败 —— 此时扫描仪看起来像是连接不稳定,但实际上其硬件并无任何问题。 **绝对不要使用 `scanimage -L` 轮询扫描仪。** 该命令会*直接打开 USB 设备*。将其设置为定时运行会导致健康的扫描仪在“已找到”和“未找到”之间不停闪烁,甚至可能使设备完全卡死。现在设备状态是从 IOKit registry 中读取的,这种方式完全不会触及总线;只有在设备状态发生改变时才会进行真正探测,且失败后会采用 30 秒 → 5 分钟的退避策略。 **两个 Apple API 陷阱。** `CGPDFContext` 会悄无声息地忽略以 `NSValue` 形式传递的 media box —— 它必须是包装了 `CGRect` 的 `CFData`,否则导出的每一页都会变成 US Letter 尺寸,且图像会发生溢出。此外,Vision 对于带有 alpha 通道的图像会返回*零*文本识别结果,因此页面在进行 OCR 之前必须先被展平并置于白色背景上。 ## 如果扫描仪停止响应 请拔下 USB 线并重新插上。卡纸或传输中断可能导致设备已被枚举,但不再响应控制传输(`sane-find-scanner` 会报告“could not fetch string descriptor”),且没有任何软件重置能够清除该状态。 ## 另一种传输方式 在 **开启** Auto Start 的情况下,扫描仪会呈现为 USB 闪存驱动器,此时任何扫描软件都无法访问它。Canon 的解决此问题的方法非常与众不同,并在此被完整逆向工程:虚拟的 `ONTOUCHLITE` 卷中的两个 2 MiB 大小的文件 `INDATA.dat` 和 `transfer.dat`,实际上是映射到扫描仪命令缓冲区的视窗,而固件会监控这些文件背后的磁盘块。整个驱动完全采用基础的 POSIX `read`/`write` 实现 —— Canon 的启动程序完全没有引入任何 IOKit。 ``` transfer.dat 0x00 24-byte command block (12-byte header + 12-byte SCSI CDB) 0x18 4-byte status doorbell 0x1C data-out block, payload at 0x28 INDATA.dat 0x00 data-in payload ``` 其数据帧结构与 `canon_dr` 的 USB 批量传输帧在字节级别上完全一致,因此完整的 Canon DR 命令集均适用。这种方式是有效的 —— 曾通过此方式实际扫描过一页文档 —— 但模拟前端必须由主机进行编程控制,且三遍粗略校准会导致本设备卡死,因此生成的图像往往偏白且带有条纹。详情见 [`reference/protocol/PROTOCOL.md`](reference/protocol/PROTOCOL.md) 和 [`reference/protocol/SCANSEQ.md`](reference/protocol/SCANSEQ.md)。 ## 布局 ``` app/ Sources/SaneBackend.swift scanimage transport (the good path) Sources/Tunnel.swift file-tunnel SCSI transport Sources/ScanEngine.swift SET WINDOW / SCAN / READ, de-interlacing, calibration Sources/ImagePipeline.swift Core Image rendering, thumbnails, levels, deskew, ink Sources/Orientation.swift Vision-based page orientation Sources/Export.swift PDF / TIFF / JPEG / PNG, Vision OCR text layer Sources/USBPresence.swift IOKit presence, no bus traffic Sources/Snapshot.swift headless UI screenshots, no permissions needed Sources/*View*.swift SwiftUI interface Sources/CLI/main.swift headless harness build.sh / build-cli.sh / make-icon.sh p215 SANE-based Python CLI reference/ protocol/ PROTOCOL.md (file tunnel), SCANSEQ.md (scan sequence) — published, our own write-up … everything else (Canon's originals, disassembly, captures, measurements) is local-only and gitignored: not redistributable ``` CLI 测试脚手架是测试硬件变更的最快方式: ``` cd app && ./build-cli.sh ./build/p215cli sane # what SANE sees ./build/p215cli sane-scan o.pdf # full scan + export ./build/p215cli diag ~/diag # raw scan, no processing, reports every measurement ./build/p215cli hist page.png # full-res luminance histogram ./build/p215cli orient page.png # detected rotation ./build/p215cli probe # tunnel-mode device identity ``` ## 资助我 开发这款应用,是因为一台完好的扫描仪理应配有更好的软件。如果它让你的扫描仪免于被丢进抽屉吃灰,你可以选择 [在 GitHub 上赞助本项目](https://github.com/sponsors/philipyaz) —— 权当是请我喝杯啤酒,而非商业交易。 ## 许可证与商标 [MIT](LICENSE)。Canon、imageFORMULA 和 CaptureOnTouch 是 Canon Inc. 的商标。本项目与 Canon 没有任何关联、未受其认可,也未被其支持。本项目不包含任何 Canon 的代码;硬件协议的逆向工程是为了实现互操作性,并且本代码仓库刻意排除了 Canon 自家的软件。
标签:macOS原生应用, SwiftUI, 文档扫描, 硬件外设, 逆向工具, 驱动程序