Skip to content

Pin And Compositing

51hhh edited this page Sep 16, 2026 · 1 revision

贴图与图像合成

PinManager

pin/ 持有贴图内容、窗口尺寸、缩放/透明度/锁定状态和清理逻辑。图片数据由内存所有权管理,不生成临时文件。窗口销毁时通过窗口事件自动从注册表移除条目。

生命周期:

clip / 图片 / 截图
  → PinManager
  → 隐藏创建窗口
  → 首帧就绪
  → show() → set_focus() → 摆位置顶
  → 缩放 / 透明度 / 锁定 / 复制 / 保存
  → 销毁清理

贴回原处

origins.rs 的 PinOriginRegistry 记住「我们自己截下来复制走的图」原本在屏幕上的矩形,之后从历史里贴同一张图时靠它贴回原处。

键是解码后像素的 sha256(含宽高),不是 PNG 字节。 图片经剪贴板走一圈是原始 RGBA,watcher 会重新编码,PNG 字节不稳定。

登记方交出的是 PinFingerprint(已算好的摘要 + 宽高),这样调用方能和剪贴板写入共用同一份解码像素,不必为登记再解一次或复制一份十几 MB。lookup 先只读 PNG 头比宽高,尺寸不匹配就直接返回,省掉整张解码。

内容尺寸的单位

这是一个容易出错的地方:

  • 有原始矩形时直接用它(选区本来就是逻辑像素)
  • 没有时入参是图片像素,必须先按合成器报告的真实缩放折成 CSS 像素

用整数缓冲区缩放或干脆不折算,都会让贴图被放大再重采样。详见显示缩放。

窗口高度下限

窗口外框有 252 px 高度下限(竖排工具条需要 249 px)。随手框一个小按钮的贴图按内容算只有一百多像素高,工具条会被切掉。

下限只加高窗口、不改内容尺寸,多出来的高度由前端留成左上角对齐的透明留白——否则「贴回原处」当场就偏。

缩放时的摆位

只有位置真的可信才重新摆位。 Wayland 协议不把窗口位置告诉客户端,outer_position() 与 Moved 都是 GTK 自己那份假值。拿它算「保持中心」等于每格滚轮都把窗口传送到工作区原点,所以 known_pin_position 在 Wayland 上返回 None。

摆位与置顶的平台差异

GNOME Wayland 上 set_position 与 set_always_on_top 都是静默空操作。摆位和置顶只能借道 Shell 扩展,因此顺序必须是:

show() → set_focus() → PlaceWindow

因为窗口对象要映射后才存在。缩放之后还要再摆一次(改尺寸会把窗口带回普通层)。尺寸始终留在客户端。

两条路都失败只是位置/层级不理想,绝不让贴图本身失败。

PlaceWindow 是缩放的每帧热路径,它的前置检查(会话类型、扩展装没装、协议版本、令牌文件内容)结果被缓存:成功的结果一直有效,失败的结果 30 秒后重试,安装/卸载立刻作废缓存。

每帧应答不带图片

缩放与不透明度是每帧都会走的路径,所以 update_pin 的应答是 PinState(label、内容尺寸、scale、opacity、locked、position),不带图片字节或文本。

每帧重编一张全图 base64 纯属浪费,而且会让前端重建 object URL、造成闪烁。前端 mergePinState 把应答合并进手里那份 payload。

同一时刻只允许一个 update_pin 请求在飞,在飞就攒着——避免每帧向 GTK 主线程堆同步 IPC。

可编辑 PNG 容器(v3)

pin/project.rs 是可编辑 PNG 的信任边界。

标准 PNG 的 IDAT       = 最新合成图(任何看图软件都能打开)
clippy-project iTXt   = 压缩的自包含工程数据
  ├── canonical 原图
  ├── 尺寸 / sha256
  ├── annotations
  ├── adjustments
  ├── rendererVersion
  └── preview.rgbaSha256

preview.rgbaSha256 以「宽高 + 解码 RGBA」绑定当前 IDAT。这样做的效果:

  • 拒绝把合法工程块移植到另一张图上
  • 允许同像素 PNG 改变压缩级别或 chunk 切分

容器 v2 仍可读,并在下一次可编辑保存时升级为 v3。renderer v1/v2 都能恢复,未知版本安全降级成普通 PNG。

预算

writer 与 reader 共用:64 MiB 合成图、64 MiB 原图、96 MiB 解压 JSON、160 MiB 容器。

损坏的元数据或摘要不匹配只丢可编辑性,不让整个文件不可用。

两条安全规则

  • editable 保存失败不得静默伪装成 flat。 用户以为存的是可继续编辑的工程,实际存成扁平图,下次打开发现标注全没了。
  • flat 导出会重编码并移除工程块。 否则模糊、马赛克处理过的文件里仍然内嵌着原图,等于泄露。

拖放入口

主窗口常态不提供普通图片打开按钮。把单个 PNG 拖入时,open_pin_project_file 只为通过全部校验的工程创建贴图;普通、损坏或未来版本的工程返回 None,不绕过剪贴板队列。

权威合成器

pin/render_v2.rs 是贴图工程与截图选区的唯一最终合成器。

Copy、扁平保存、可编辑保存、截图的 Copy/Save/Pin 复用同一语义。调整、四种效果、九种矢量/文字工具全部走锁定版本的纯 Rust CPU 路径和内嵌 Noto 字体,不查询 WebView、GPU 或系统字体。

因此相同输入可以用 RGBA SHA-256 金图锁定。

预算 值
渲染上限 32 Mi 像素
模糊缓存 32 Mi 像素
效果工作量 512 Mi 像素

任务在 blocking worker 执行。v1 未修改时复用 IDAT,第一次真实编辑才升级 v2。

前端只提交操作文档

画布文档的唯一坐标系是 canonical source pixels。前端提交 renderer v2 操作层,不上传一份互相竞争的 WebView PNG。

projectSchema.ts 在 IPC 之后再次防御性校验持久化文档;usePinCanvas.ts 恢复 annotations/adjustments/history baseline,并用 revision/savedRevision 判断 dirty。

关闭工具只停止交互,已有 composition 仍覆盖显示。editable save、flat export 与最新合成复制是三个显式动作,但共用后端的权威合成结果。

标注工具

工具核心在 react/annotation/,与窗口无关;贴图和截图覆盖层都只提交操作文档。共 16 个工具分三组:

分组 工具 说明
选择 select、object、eraser select 就是框选/移动/缩放选区(选区自己是裁剪框,因此没有独立 crop 工具);橡皮一次点击删一个标注,保持撤销粒度
绘制 pen、marker、rect、ellipse、line、arrow、measure、text 四种拖拽形态(折线/矩形/线段/文本)复用同一套包围盒、命中与移动逻辑;ellipse 只在轮廓附近命中;measure 标注原图像素长度
效果 highlight、blur、mosaic、spotlight、magnifier blur/mosaic/spotlight/magnifier 需读取或压暗底图,始终先于矢量标注绘制;magnifier 从原图重采样使预览与导出清晰度一致

highlight 只是半透明矢量色块,按用途归在效果组,但绘制顺序仍跟随矢量标注。

马赛克必须以原图像素采样,不能采样已经绘制过的画布——否则重复涂抹会逐次变糊,且预览与导出不一致。

图片查看器

viewer/ 是独立窗口,按不可变图片快照隔离操作:原历史条目被删除不影响已打开的图片。

  • 无限平移画布,「适应图片」可找回移出视口的图
  • 自绘顶栏(拖动、最小化、全屏切换、关闭),不用系统标题栏
  • 同一张图聚焦已有窗口,不同图独立开窗;最小化后再次点击同图恢复原窗口及编辑内容
  • 绘制沿用同一套画布文档;复制、保存和贴图使用后端原图合成

窗口控制命令需要核验 caller label 与句柄;重复打开按 show → unminimize → focus 顺序恢复。

全屏切换要等权威状态,不能乐观假设:外部窗口管理器切换后,Esc 必须等到确认状态才动作,查询失败或等待期间出现保存模态时不能晚一步关掉窗口。

Clone this wiki locally