☰
PhotoCraft 贡献指南:从 Never crash 铁律到新增一条引擎命令的完整路径
2026/10/27 1:49:49 网站建设 项目流程

PhotoCraft 贡献指南:从 Never crash 铁律到新增一条引擎命令的完整路径

【免费下载链接】photocraftPhotoCraft: An open-source, photo editor written by scratch in pure Rust项目地址: https://gitcode.com/gh_mirrors/pho/photocraft

PhotoCraft 是纯 Rust 编写、以 1:1 对齐 Photoshop 为目标的图像编辑器,本文讲清 Rust 开源项目如何贡献到它:资产与许可证边界、Never crash 铁律、命令注册主流程与合入门禁。读完即可独立提交一条可运行、可撤销、Agent 可调用的引擎命令。

语言/运行时:仓库只允许 Rust,禁止 JavaScript 与 TypeScript。Web 端是同一套 Rust 代码经trunk+wasm-bindgen编译到 WebAssembly 的产物,唯一的 JS 是 wasm-bindgen 自动生成的胶水代码,绝不应手写。桌面端是原生 egui/eframe 加 wgpu,AGENTS.md 明确禁止引入 Tauri、Electron 或任何 webview 壳,也禁止把这类依赖加进构建步骤或打包目标。

许可证与资产:贡献代码采用 MIT OR Apache-2.0 双许可证,根目录提供 LICENSE-MIT、LICENSE-APACHE 与 NOTICE。资产规则分四类,全部登记与变更必须在同一个提交中完成:

资产类别规则
第三方资产只用宽松许可证,许可证文件放资产旁边(如 assets/fonts/OFL-Inter.txt、assets/icons/LICENSE-lucide.txt)
所有资产(含原创)登记进 ATTRIBUTION.md:路径、标题、作者、来源、许可证
字体文件永远不进入本仓库,放在外部 craft-fonts 仓库,经CRAFT_FONTS_DIR构建输入引用
docs/brand/的 ArtCraft Logo不是开源资产,受 docs/brand/LICENSE-brand.txt 约束

字体的接入方式是可选构建输入:crates/text/build.rs 读取$CRAFT_FONTS_DIR/fonts/manifest.txt,把字体嵌入为photocraft_text::CRAFT_FONTS;未设置时该常量为空,构建行为不变,一切功能仍须正常工作。该变量必须是绝对路径,因为 build.rs 在crates/text下运行,相对路径会解析错位置;wasm32 构建即使设置了它也不嵌入字体(UI 字体单方面就把 release wasm 推过 24 MiB 门禁,见 docs/development.md › Fonts):

# clone craft-fonts 到本仓库旁边后(docs/development.md:106) CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p photocraft

对齐边界(clean-room):PhotoCraft 是 clean-room 重实现——可以研究和观察 Photoshop 等专有软件的行为与外观,但禁止复制其代码、着色器、图标、ICC 配置文件或任何资产。实现依据仅限公开规范(Adobe PSD 规范、ICC 标准、ISO 32000 混合模式)与观察。这条边界同时约束文档措辞:产品名在用户可见文本中恒写作PhotoCraft,机器名保持小写(crate、二进制、id 如ai.storyteller.photocraft)。

Never crash 铁律:用户把真实工作托付给这个编辑器,崩溃意味着工作丢失,所以一个畸形文件、一条坏命令参数、一个损坏的偏好文件都必须产生用户或 Agent 可以处理的错误,而不是 panic。纪律要点:非测试代码禁用unwrap()、expect()、panic!、unreachable!、todo!、unimplemented!;错误经Result与?传播;文件、参数、选区派生的数字一律视为敌对,用get()而非[i],长度与偏移用checked_*/saturating_*。整仓在根 Cargo.toml 第 81 行设置unsafe_code = "forbid",唯一例外是隔离的crates/tablet(macOS 读取数位笔输入需要 AppKit 互操作)。这条纪律由工具链强制执行:clippy.toml 全文只有四行 allow-in-tests 配置,即 unwrap/expect/panic/下标索引只允许出现在测试中;干净 crate 携带 crate 级属性(crates/engine/src/lib.rs 第 8 行#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]);应用壳在命令分发与文件导入导出周围捕获逃逸 panic,保住文档;每次崩溃修复必须附带一个修复前会 panic 的合成回归测试。

把一个新功能从想法变成合入:完整路径

PhotoCraft 的核心工程约定是「命令,而非处理器」:一切用户可见行为都是引擎命令,UI、CLI、控制通道与 MCP 只是同一注册表的分发端。新增功能的主路径因此固定为七个子步骤:锁定 id、落算法、注册、适配上下文、测试、接对话框、刷新报告。

锁定命令 id

每个命令的 id 与 Photoshop 菜单结构一一对应,登记在 crates/ui-egui/src/menu_catalog.rs 的CATALOG表中——一条四元组:菜单路径、标签、默认快捷键、命令 id。使用与目录完全一致的 id,对应菜单项即刻「活」过来,不需要写任何 UI 代码:

// crates/ui-egui/src/menu_catalog.rs:6 /// (menu path, label or "---", default shortcut, command id) pub static CATALOG: &[(&[&str], &str, Option<&str>, &str)] = &[ (&["File"], "New…", Some("Cmd+N"), "file.new"), (&["File"], "Open…", Some("Cmd+O"), "file.open"), (&["File"], "Save", Some("Cmd+S"), "file.save"), // ……共 777 行,每个 Photoshop 菜单项一行 ];

⚠️ 没有 Photoshop 菜单入口的内部命令(如智能滤镜删除)沿用同风格的描述性 id(例如layer.smartFilter.delete),菜单路径留空(menu: &[])。只有纯视图/窗口状态(缩放、面板、屏幕模式)才属于 shell 层的ui-egui/src/menus.rsUI_COMMANDS,不进引擎注册表。

在最低层 crate 落算法

算法实现放进放得下的最低层 crate:成像算法进algo,笔刷进paint,路径/形状进vector,文字进text,色彩进cms,并带单元测试。算法函数有三条约定:接收位深无关的Surface(photocraft-raster的 COW 瓦片表面)而不是裸&[u8];按瓦片工作以利于并行与缓存;随机性必须带种子,保证导出、测试与回放可复现。以调整类为例,数学住在 crates/algo/src/tone.rs,引擎模块只负责参数解析与调用——这就是「算法在 L2、命令在 L5」的分层落地:

// crates/algo/src/tone.rs:22 /// Image › Adjustments › Shadows/Highlights 参数(Photoshop 对话框,百分比 0..=100)。 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ShadowsHighlights { pub shadow_amount: f32, pub shadow_tone: f32, pub shadow_radius: f32, // ……其余字段同理,Default 实现即 Photoshop 对话框默认值 }

⚠️ 位深(8/16/32f)与色彩模型(RGB/Gray/CMYK/Lab)是运行时数据:公共 API 中禁止引入仅u8的像素路径,色彩转换一律走photocraft-cms(不得假设 sRGB),测试覆盖 8、16、32 三个位深。

注册进引擎命令表

命令本体放在引擎模块crates/engine/src/<area>_cmds.rs,模块暴露一个specs()函数,返回CommandSpec列表;crates/engine/src/commands.rs 定义了这个结构体(第 14 行起),注册列表从第 1497 行开始按功能域逐行扩展,例如v.extend(crate::filters::specs());——新模块只需在这个列表加一行。CommandSpec的每个字段都有明确职责:

// crates/engine/src/commands.rs:14 /// Metadata + implementation for one command. pub struct CommandSpec { pub id: &'static str, pub label: &'static str, pub menu: &'static [&'static str], // 如 ["Layer", "New"];空 = 不进菜单 pub shortcut: Option<&'static str>, // egui 记法(Cmd+Shift+N),UI 按平台映射 pub params: &'static str, // 面向人/Agent 的参数文档(JSON 风格) pub enabled: Enabled, // fn(&Session) -> Result<(), String>,置灰菜单项 pub run: Run, // fn(&mut Session, &Value) -> Result<Value> pub journal: bool, // 是否记入会话日志;查询类命令为 false }

填enabled时优先复用现成谓词——commands.rs第 33 行起定义了has_doc、has_layer、has_pixel_layer、has_paintable等,返回Err(说明)时对应菜单项置灰并显示该说明。

⚠️params文档串不是装饰:Agent 经commands/command_list方法读取的就是它,CLI 里photocraft-cli commands --filter blur展示的也是它,类型、单位、取值范围与默认值都要写清楚(参照 crates/engine/src/adjust_cmds.rs 第 541 行的{"shadowAmount":0..100=35,...}风格)。run闭包返回 JSON 结果并恰好记录一步历史——拖拽与打字这类连续会话用coalesce合并,否则每次鼠标移动都会产生一条撤销记录。

尊重文档上下文

命令必须正确处理当前文档的全部上下文维度:活动选区(含羽化)、目标是图层还是蒙版、图层锁、色彩模型与位深。commands.rs的paint_surface(第 69 行)演示了标准做法:参数"target":"mask"时写入蒙版表面,并分别检查蒙版锁与像素锁;锁定图层返回 Photoshop 措辞的可读错误,而不是崩溃:

// crates/engine/src/commands.rs:93 /// Refuse a layer whose pixels are locked (directly or through a group), with Photoshop's message. pub(crate) fn check_pixels_unlocked(doc: &Document, id: LayerId) -> Result<()> { let locks = doc.effective_locks(id); let l = doc.layer(id).ok_or(EngineError::NoLayer(id))?; if locks.pixels || locks.all { return Err(EngineError::Other(format!( "Could not complete your request because the layer \"{}\" is locked", l.name))); } Ok(()) }

⚠️ 上下文检查与 panic_hunt 是同一件事的两面:任何上下文组合(空文档、无选区、目标层无蒙版、锁定层)下命令都应返回Err,不存在「先 panic 再处理」的捷径。

跑通模块测试与对抗模糊

模块内测试覆盖五类:正常行为、undo/redo、禁用态(enabled为假)、坏参数(越界/错类型必须得Err)、多种位深。模块测试通过后跑对抗参数模糊测试——crates/engine/tests/panic_hunt.rs 对每一条命令注入敌对参数,每条命令在独立线程上执行、带 4 秒超时,panic 由全局 hook 计数,任何一条命令在任何参数与文档状态下 panic 都会让测试变红:

# 模块测试(adjust_cmds 为例,可换成你的模块名) cargo test -p photocraft-engine --lib adjust_cmds # 对抗参数模糊:opt-in(--ignored),增改命令后必须跑并保持绿色 cargo test -p photocraft-engine --test panic_hunt -- --ignored

⚠️ AGENTS.md 要求每条新命令自带「优雅失败」测试:空/越界/错类型参数得到Err而不是崩溃。这条测试与 panic_hunt 互补——前者是文档化的契约,后者是穷尽的护栏。

接上 schema 驱动的对话框

带参数的滤镜与调整使用 schema 驱动的对话框加实时预览,代码在 crates/ui-egui/src/filter_dialog.rs 与 crates/ui-egui/src/dialogs.rs:参数 schema 必须与注册时的params文档串保持一致,滑杆移动即触发预览渲染,大文档走ui-egui/src/proxy.rs的降采样代理保证流畅。提交前必须视觉验证,离屏快照示例不弹窗口、不抢焦点,--script是有序的控制协议调用,输入类调用会等应用处理完事件再应答:

# 离屏渲染完整 UI(docs/development.md:551) cargo run --release -p photocraft-ui-egui --example snapshot -- \ --out ui.png --size 1440x900 --scale 2 --open photo.jpg \ --script '[["ui.set", {"tool": "type"}], ["ui.menu.invoke", {"id": "image.canvasSize"}]]'

⚠️ PR 必须附上修改前后的截图对比;演示图片必须使用公有领域艺术图,不能是个人照片。UI 颜色与圆角一律来自theme::Tokens(ui-egui/src/theme.rs)与widgets::*组件库,不得硬编码。

刷新 parity 报告

菜单覆盖率由cargo xtask parity度量:它把menu_catalog.rs的菜单树与实时命令注册表对比,重写 docs/parity-checklist.md。新命令使存活命令数上升后,把重新生成的文件一并提交——报告是生成物,提交它而不是手改它:

# 新增命令后运行;重写 docs/parity-checklist.md,并把再生文件提交进 PR cargo xtask parity

护栏是 crates/ui-egui/src/parity.rs:第 10 行的FLOOR(当前值 631)是存活命令数的地板,测试parity_does_not_regress断言live >= FLOOR。地板只升不降——parity 上升时把FLOOR抬高,绝不下调。

合入前的强制检查

测试矩阵

测试要求按涉及面分项,语料测试依赖固定 pin 且经 sha256 校验的语料集(项目自有的 Photoshop 制作 oracle、psd-tools、ag-psd、PngSuite 等),pin 集中在 xtask/src/corpus_pins.rs;语料全部落在corpus/(gitignored),永远不提交语料文件。corpuscargo feature 默认关闭,普通cargo test不编译语料测试;feature 打开后缺失语料直接失败而不是静默跳过;CI 的corpusjob 每次 PR 与 main 都执行。

涉及面必须跑的测试触发条件
任何变更cargo test -p <crates you touched>(含模块测试)恒触发
格式 crateround-trip 与畸形输入测试,外加真实文件语料cargo xtask test-corpus触碰 psd、io、codecs、compose、gpu、text、format 任一 crate
像素代码8、16、32 位深度下的行为测试引入或改动像素路径
命令模块内坏参数「优雅失败」测试 +cargo test -p photocraft-engine --test panic_hunt -- --ignored新增或改动命令

静态检查与分层

风格门禁零容忍:cargo fmt之后cargo clippy -- -D warnings,风格跟随周边代码,注释解释「为什么」而非「是什么」。依赖分层由 xtask/src/layers.rs 的表驱动规则引擎检查——crate 只能依赖更低的层;psd、codecs等 Standalone crate 工作区零依赖(唯一例外是codecs → heif,见第 95 行STANDALONE_EXCEPTIONS);同层内还有顺序约束(geom → cms → color → raster与compose → gpu,第 82 行);L6 以下任何 crate 禁止使用UI_CRATES列表中的 egui、eframe、winit、rfd 等库(第 104 行,UI_MIN_LAYER = 6)。cargo xtask 分层检查cargo xtask layers是 CI 必过项,新增 crate 必须先注册进第 35 行的TABLE否则直接失败:

cargo fmt cargo clippy -- -D warnings cargo xtask layers # 分层门禁:规则源在 xtask/src/layers.rs cargo xtask wasm # 动了 L0–L6 时:wasm32-unknown-unknown 目标必须通过 check

cargo xtask wasm的约束来自「永不破坏 wasm」:文件系统代码要么cfg(not(target_arch = "wasm32")),要么走平台服务。

可观测数字

仓库把可测量的数字做成只升不降的地板:parity 地板(FLOOR)、语料地板、性能预算(perf/budgets.toml,预算达标后把enforce = true打开)。cargo xtask scorecard从 scorecard/*.toml、perf/budgets.toml、perf/baseline.json与语料地板重新生成 docs/scorecard.md,CI 用--check模式失败于任何过期报告——移动了数字却不提交再生报告,流水线会红:

cargo xtask scorecard # 重新生成 docs/scorecard.md(移动数字后必须提交它) cargo xtask scorecard --check # CI 视角:报告过期即失败 cargo xtask perf --quick # 碰了热路径时;perf --update-baseline 发布完整基线

✅ 提交前最后扫一遍:

  • cargo test -p <你触碰的 crate>
  • cargo clippy -p <crate> --all-targets -- -D warnings
  • cargo xtask layers
  • cargo xtask wasm(动了 L0–L6 时)
  • cargo xtask parity并提交重新生成的docs/parity-checklist.md(新增命令时)
  • cargo test -p photocraft-engine --test panic_hunt -- --ignored(增改命令时)
  • 在log/devlog.md追加一条简短 devlog:落了什么、数字、还欠什么

速查:关键路径与命令

项目路径或命令
获取仓库git clone https://gitcode.com/gh_mirrors/pho/photocraft
贡献文档docs/contributing.md
黄金规则与 agent 指南AGENTS.md
菜单 id 目录crates/ui-egui/src/menu_catalog.rs
命令注册入口crates/engine/src/commands.rs(v.extend列表,第 1497 行起)
分层规则表xtask/src/layers.rs
对抗参数模糊测试crates/engine/tests/panic_hunt.rs
parity 报告docs/parity-checklist.md(cargo xtask parity生成)
语料 pin 与校验xtask/src/corpus_pins.rs(cargo xtask test-corpus)
资产登记ATTRIBUTION.md
devloglog/devlog.md(本地、gitignored;规则见 AGENTS.md 第 5 节)
离屏 UI 快照cargo run --release -p photocraft-ui-egui --example snapshot

改动通过全部门禁后,往log/devlog.md追加一条简短记录(落了什么、数字、还欠什么),并保持每一步树上代码可构建——会话可能随时中断,devlog 加绿色的树是下一个接手者(人或 Agent)的起点。随后推送分支并开 PR,附上前后截图对比;若改动移动了数字,把重新生成的docs/parity-checklist.md与docs/scorecard.md一并放进同一个 PR。

【免费下载链接】photocraftPhotoCraft: An open-source, photo editor written by scratch in pure Rust项目地址: https://gitcode.com/gh_mirrors/pho/photocraft

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询