Skip to content

About

Windows 手写批注软件,把 Word / Excel / PowerPoint / 图片 / 文本 / PDF 等格式统一转换成 PDF,在窗口内渲染,用手写笔直接批注,最后导出成带批注的 PDF。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

InkAnnotator — Windows 手写批注软件

把 Word / Excel / PowerPoint / 图片 / 文本 / PDF 等格式统一转换成 PDF,在窗口内渲染,用手写笔直接批注,最后导出成带批注的 PDF。


一、技术选型(为什么这么选)

环节 方案 理由
UI 框架 .NET 8 + WPF InkCanvas 是 Windows 上唯一原生接入 Windows Ink 的托管控件:压感、倾斜、笔尾擦除、低延迟湿墨渲染全部开箱即用。Electron / Qt 都要自己模拟压感,手感差一大截
文档 → PDF LibreOffice 无头模式(主)+ Office COM(备)+ 内建图片/文本转换 LibreOffice 免费、格式覆盖最广、可随程序分发;装了 Office 的机器自动走 COM 保真度更高
PDF 渲染 pdfium(Docnet.Core) Chrome 同款渲染内核,速度和保真度最好,NuGet 自带原生库免安装
PDF 写回 PdfSharp 6 MIT 协议,可以把墨迹当矢量路径写进原页面内容流,导出后放大不糊
墨迹存储 ISF(StrokeCollection.Save) 微软原生墨迹格式,完整保留压感与笔属性,体积小

核心设计决策:墨迹坐标与缩放解耦。 每页的 InkCanvas 尺寸永远固定为「PDF 点尺寸 × 96/72」的逻辑尺寸,缩放通过外层 ScaleTransform 实现。这样无论用户怎么缩放、怎么换分辨率重渲底图,已有笔迹的坐标都不会漂移——这是很多同类软件做错的地方。


二、目录结构

InkAnnotator/
├── InkAnnotator.sln
├── .vscode/                        VSCode 调试配置
│   ├── launch.json
│   ├── tasks.json
│   └── settings.json
└── src/
    ├── InkAnnotator.Core/          与界面无关的引擎层
    │   ├── Conversion/
    │   │   ├── IDocumentConverter.cs          转换器接口
    │   │   ├── DocumentConversionService.cs   调度:按扩展名挑转换器、失败自动降级
    │   │   ├── LibreOfficeConverter.cs        soffice --headless --convert-to pdf
    │   │   ├── OfficeInteropConverter.cs      Word/Excel/PPT COM 后期绑定
    │   │   └── BuiltInConverters.cs           PDF 直通 / 图片 / 纯文本
    │   ├── Rendering/
    │   │   ├── PdfRenderService.cs            pdfium 光栅化(串行化 + DocReader 缓存)
    │   │   └── PageComposer.cs                底图+墨迹+图章+文字框的统一绘制(打印复用)
    │   ├── Documents/
    │   │   ├── AnnotationPackage.cs           .ianno 工程包读写(含 notes/ 分区)
    │   │   ├── Annotations.cs                 文字批注框 / 图章的数据模型
    │   │   └── StampLibrary.cs                图章矢量几何(汉字也转成路径)
    │   ├── Export/
    │   │   ├── PdfInkExporter.cs              批注矢量烧录进 PDF(三种墨迹模式)
    │   │   └── InkOutlineBuilder.cs           压感 → 变宽多边形轮廓(自算模式用)
    │   └── Fonts/
    │       └── WindowsFontResolver.cs         给 PdfSharp 提供中文字体
    └── InkAnnotator.App/           WPF 界面层
        ├── App.xaml(.cs)           全局异常兜底、命令行参数
        ├── MainWindow.xaml(.cs)    三段式布局 / 缩略图 / 页面视图 / 滚动虚拟化
        ├── AboutWindow.xaml(.cs)   关于:版本 / 快捷键 / 组件与运行环境
        ├── Assets/                 AppIcon.ico(exe 图标,8 个尺寸)/ AppLogo.png
        ├── ViewModels/
        │   ├── ViewModelBase.cs    INotifyPropertyChanged + RelayCommand
        │   ├── MainViewModel.cs    应用状态与全部命令
        │   └── PageViewModel.cs    单页:底图 + 墨迹 + 文字框/图章 + 按需渲染
        ├── Controls/
        │   ├── PenInkCanvas.cs     仅笔书写 / 防手掌误触 / 笔尾擦除
        │   └── AnnotationLayer.xaml(.cs)  文字批注框 / 图章的放置、拖动、缩放、删除
        ├── Services/
        │   ├── UndoRedoManager.cs  通用撤销栈(墨迹 + 文字框 + 图章)
        │   ├── PrintService.cs     自定义 DocumentPaginator,按打印分辨率重渲
        │   └── AutoSaveService.cs  自动保存与崩溃恢复
        ├── Converters/
        └── Themes/Styles.xaml       设计系统:色板 / 图标几何 / 扁平控件样式

三、环境准备

  1. .NET 8 SDK(必须)

    winget install Microsoft.DotNet.SDK.8
  2. LibreOffice(可选,只影响 Office 格式)

    winget install TheDocumentFoundation.LibreOffice

    装在默认路径即可,程序会自动从注册表和常见路径探测 soffice.exe。 没装 LibreOffice 但装了 Microsoft Office 也能跑,走 COM 通道。

    不装也能用:PDF、图片、纯文本走的是内建转换器,零外部依赖。 只有 Word / Excel / PowerPoint 需要这两者之一,程序会按探测结果 动态调整文件对话框的筛选器,不会让你挑到一个注定打不开的 docx。

  3. VSCode 扩展

    • ms-dotnettools.csdevkit(C# Dev Kit,含调试器)
    • ms-dotnettools.csharp

⚠️ 必须是 x64。Docnet.Core 带的是 64 位 pdfium,两个 csproj 都已写死 <PlatformTarget>x64</PlatformTarget>。


四、在 VSCode 里跑起来

cd InkAnnotator
dotnet restore
dotnet build

然后在 VSCode 中按 F5,选「调试 InkAnnotator (WPF)」。

第二个配置「调试并直接打开某个文件」会弹框让你输入文档路径,直接进入已加载状态——调转换和渲染逻辑时很省事。

断点建议放这几处:

想调什么 断点位置
格式转换 DocumentConversionService.ConvertToPdfAsync 的 foreach (var converter in available)
LibreOffice 命令行 LibreOfficeConverter.ConvertAsync 的 proc.Start()
页面渲染 PdfRenderService.RenderCore
可视区调度 MainWindow.UpdateVisiblePagesAsync
墨迹落笔 MainViewModel.OnPageStrokesEdited
导出坐标换算 PdfInkExporter.MapPoint
压感导出(默认路径) PdfInkExporter.TryDrawExactGeometry
压感轮廓展开(自算路径) InkOutlineBuilder.Build
打印每页合成 AnnotatedDocumentPaginator.GetPage
批注放置/拖动 AnnotationLayer.Root_MouseLeftButtonDown / OnDragDelta

五、功能一览

  • 打开:Ctrl+O,或直接把文件拖进窗口,或把文件拖到 exe 上
    • 始终支持:PDF、图片(png/jpg/bmp/tif/gif/webp)、纯文本、.ianno 批注工程
    • 装了 LibreOffice 或 Office 才支持:Word / Excel / PowerPoint / ODF / RTF / HTML
  • 书写:手写笔 / 荧光笔,8 色调色板,粗细 0.6–14
  • 擦除:整笔擦、局部擦,笔尾倒置自动切擦除
  • 选择:套索选中墨迹后可拖动、缩放、删除(InkCanvas 原生能力)
  • 撤销/重做:Ctrl+Z / Ctrl+Y,深度 500 步
  • 缩放:Ctrl+滚轮(以鼠标为锚点)、Ctrl +/−、Ctrl+0、适应宽度
  • 翻页:PageUp / PageDown、左侧缩略图点击
  • 仅笔书写:勾选后手指只平移页面,避免手掌误触
  • 文字批注框:选「文字」工具后在页面上点一下即插入并直接进入编辑;标题条拖动、右下角缩放、Esc 结束编辑;字号 / 文字色 / 底色 / 加粗可调
  • 图章:对勾、叉、问号、圈、三角、星、叹号、优、良、阅、订正,点哪盖哪,大小与颜色可调
  • 打印:Ctrl+P,可选打印范围(全部 / 当前页 / 自定义),可选是否包含批注;横竖不一致时自动转 90°
  • 找批注:缩略图右上角显示批注数量徽标、左侧有竖条;Ctrl+PageUp / Ctrl+PageDown 在「有批注的页」之间跳;选项浮层里可勾选「缩略图只显示有批注的页」,把缩略图栏筛成一份批注索引

界面结构

主栏只放「写」相关的东西:左侧文件菜单与文档名,中间七个工具,右侧撤销重做与选项。工具的参数放在下面的上下文栏里,只显示当前工具的那一组——选到「选择」或「浏览」时整条收起,画布多出 44px。页码、缩放、批注跳转这类「看」的控制全部下沉到底部状态栏。一次性开关(仅笔书写、打印含批注、只看批注页)收进右上角的选项浮层。

缩略图栏与正文双向联动:点缩略图跳到该页,滚动正文时缩略图选中项跟着走并自动滚进视野。

  • 文件(带底色的那一个)打开后台菜单:打开 / 保存 / 另存为 / 导出 PDF / 打印,以及「打印时包含批注」。
  • 绘图(默认选项卡)分三组 —— 工具(笔、荧光笔、文字框、图章、橡皮、选择、浏览)、样式、编辑(撤销 / 重做 / 清除本页)。 「样式」组的内容随当前工具切换:笔和荧光笔给色板 + 粗细滑块 + 实时预览,文字框给字号 / 字色 / 底色 / 加粗,图章给两行图形库 + 大小 + 颜色,橡皮给整笔 / 局部两种方式。
  • 视图分四组:显示(导航窗格、仅笔书写、只显示有批注的页)、缩放、页面、批注。
  • 选项卡条右端可以折叠整个功能区,画布多出 88px。
  • 状态栏承担高频的「看」的操作:页码、批注跳转、缩放滑块与适应宽度 —— 和 Office 一样。

文档区背景是浅灰(#E6E6E6),页面靠 1px 边框加淡阴影浮起来,不用深色画布。


六、已知限制与后续可做的事

  1. 旋转页面:PdfInkExporter.HandlePageRotation 默认按 /Rotate 做坐标映射。极少数 PdfSharp 版本会自行处理旋转导致重复变换,若导出后旋转页墨迹错位,把这个静态属性设为 false 再试。

  2. MediaBox 原点偏移 已修:PageMapping 现在按 pdfium 的规则取 MediaBox ∩ CropBox 作为可见区,并补偿 PdfSharp 不处理的页面框原点(OffsetX = box.X1、OffsetY = page.Height − box.Y2)。扫描件的非零 MediaBox 原点、印刷排版件的偏移 CropBox 都不再错位;常规页面两个偏移都算出 0,行为与旧版逐点一致。若某个 PdfSharp 版本将来自己处理了原点,把 PdfInkExporter.HandlePageBoxOffset 置 false。

    同一条坐标链上还有一处小误差也一起修了:PdfRenderService 原先用 PageDimensions(1.0) 量页面尺寸,而 Docnet 的 GetPageWidth() 是 (int)(FPDF_GetPageWidth * scaling)——截断,A4 的 595.276×841.890 会被报成 595×841。现在改成放大 MeasureScale = 100 再除回来,截断误差 ≤0.01pt。Docnet 的 PageReader 构造函数只做 FPDF_LoadPage,位图是 GetImage 时才分配,所以放大量尺寸不占内存。

    需要在 Windows 上验证的两个用例(本轮改动只做了代码审查,没有编译运行):

    • 非零 MediaBox 原点的扫描件 + 带偏移 CropBox 的印刷排版件,导出后墨迹是否与屏幕重合;
    • /Rotate = 90/270 的页面。OffsetY 用的是 page.Height,若某个 PdfSharp 版本对旋转页自行交换了 Width/Height,这里会连带偏移;出现错位就先关 HandlePageRotation 定位是哪一层重复变换了。
  3. 压感导出的三种模式:由 PdfInkExporter.Mode 控制。

    模式 做法 取舍
    ExactGeometry(默认) 直接取 Stroke.GetGeometry(),即 WPF 渲染用的那份实心轮廓,展平成多边形 与屏幕完全一致;密集手写时顶点多、PDF 偏大。可用 FlattenTolerance / MinVertexSpacing 抽稀
    PressureOutline InkOutlineBuilder 自己按相邻采样点压力算半宽、沿法线偏移、左正右反闭合 顶点少、文件小、参数可调(MinPressureFactor / MaxPressureFactor / SmoothIterations);FitToCurve 的曲线拟合靠两轮 Chaikin 逼近,锐角处与屏幕有细微差异
    FixedWidth 定宽折线 旧行为,丢笔锋,仅用于对比排查

    注意:采集压力、屏幕渲染笔锋、ISF 存盘三件事 Ink 组件本来就内置,这里补的只是「WPF 墨迹 → PDF 矢量路径」这一段桥接,PdfSharp 不认识 WPF 墨迹。

  4. 无 LibreOffice 也无 Office 时:只能打开 PDF、图片、纯文本。这是设计如此,不是降级 —— DocumentConversionService 把这两个通道标成 RequiresExternalSoftware,筛选器里不出现相应格式,误开时抛 MissingConverterException,UI 弹的是一条安装指引而不是错误。「关于」窗口的"可选组件"一栏如实显示探测结果。

  5. 超大文档:页面容器目前不做 UI 虚拟化(只虚拟化位图)。上千页时可以改用 VirtualizingStackPanel + 像素滚动。

  6. 打印仍占 UI 线程:DocumentPaginator.GetPage 是同步接口,WPF 打印栈全程占着 UI 线程,这一点改不了。已经做的是把过程可见化:PrintProgressWindow 显示「第 N / M 页」进度条并提供取消,靠在每页回调里手动排一次消息队列(DispatcherFrame + PushFrame)来刷新;排队期间主窗口被置为 IsEnabled = false,不会命令重入。取消后剩余页面产出空白页快速排空任务,而不是抛异常打断 XpsDocumentWriter(有些驱动会留下卡住的作业),并如实告知"前 N 页可能已进入打印队列"。真正的异步化要把分页器搬到独立 STA 线程,前提是先把墨迹和批注快照成非线程亲和的数据。

  7. PDF 里的文字批注字重:WindowsFontResolver 只提供一种字重,加粗用微偏移模拟,不是真正的 Bold 字形。

  8. 旋转页上的文字批注:靠 RotateAtTransform(-rotate, 锚点) 把坐标系转回直立方向。/Rotate = 0(绝大多数情况)完全不走这条分支。

  9. 可以加的功能:朗读、OCR 文字层(Tesseract)、多设备墨迹同步、按页导出 PNG、批注清单侧栏。


七、发布

dotnet publish src/InkAnnotator.App/InkAnnotator.App.csproj -c Release -r win-x64 --self-contained false -o publish

VSCode 里也可以直接跑 tasks 里的「publish (单文件 x64)」。 分发时记得连同 runtimes/win-x64/native/pdfium.dll 一起打包(publish 会自动带上)。


八、许可证

本项目以 MIT 协议 发布:可自由使用、修改、商用与再分发,保留版权声明即可。

依赖的第三方组件(Docnet.Core / PDFium / PDFsharp)均为 MIT 或 BSD-3-Clause, 详见 THIRD-PARTY-NOTICES.md。 LibreOffice 与 Microsoft Office 只是被当作外部程序调用,不随本项目分发。

九、参与与反馈

  • 发现问题或想要新功能:提 Issue,附上版本号(设置 → 关于 → 复制版本信息)、 系统版本、复现步骤;如果和某个文档相关,尽量给一个可以公开的最小样例。
  • 提交代码:从 main 拉分支 → 改动 → PR。改到墨迹坐标、导出映射、打印分页这三块时, 请在 PR 里说明用什么文档验证过(尤其是非零 MediaBox 的扫描件、/Rotate 非 0 的页面)。
  • 「六、已知限制」里的条目欢迎认领。

About

Windows 手写批注软件,把 Word / Excel / PowerPoint / 图片 / 文本 / PDF 等格式统一转换成 PDF,在窗口内渲染,用手写笔直接批注,最后导出成带批注的 PDF。

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages