- 图像处理
- 桌面应用
【免费下载链接】photocraft
An open-source, clean-room reimplementation of Adobe Photoshop in pure Rust
PhotoCraft(一个纯 Rust 编写的 Photoshop 重写项目)通过 WebAssembly 将完整的图像编辑器搬进浏览器,而 packaging/web/README.md 是这份 Web 版的"部署说明书"。本篇以该文档为骨架,结合 packaging/web/package.sh、apps/photocraft-web 的构建配置与工作区 Cargo.toml 中的wasm-releaseprofile,完整讲清:如何构建出自包含的静态站点、wasm 体积是如何被压进 25 MiB 托管上限的、MIME/压缩/缓存/HTTPS 四项必备服务器设置,以及 iframe 嵌入与渲染后端回退的实操细节。读完后你应能独立地把 PhotoCraft Web 版部署到域名根、任意子路径前缀或 CDN 桶,并正确嵌入到既有页面中。
发布包内容:一个完全自包含的静态站点
photocraft-web-<version>.zip(来自 GitHub release,或由 packaging/web/package.sh 生成)在photocraft-web-<version>/目录下持有一个纯静态站点:
| 文件 | 作用 |
|---|---|
index.html | 页面本体,全部资源通过相对 URL 加载 |
photocraft-web-<hash>.js | wasm-bindgen 胶水代码(生成物,ES module) |
photocraft-web-<hash>_bg.wasm | 应用本体:原始约 19 MiB,gzip 后 8 MiB,Brotli 后 5.6 MiB(见下文体积一节) |
_headers、.htaccess | Netlify/Cloudflare Pages 与 Apache 的示例头规则 |
没有任何服务端代码,文件夹内容可以上传到任何能托管静态文件的地方。从 package.sh 的打包逻辑看,zip 里还会额外带上有用的元信息:
HOSTING.md—— 即 packaging/web/README.md 本身,随包分发;- 仓库根部的
README.md、LICENSE等文档(copy_docs函数,定义在 packaging/env.sh); - 示例服务器配置 _headers 与 .htaccess。
版本名来自[workspace.package] version(当前工作区为0.3.0),由 packaging/env.sh 用awk直接从根 Cargo.toml 读取,可用PHOTOCRAFT_VERSION覆盖。
构建与打包流程
脚本用法(见 packaging/web/package.sh 头部注释):
packaging/web/package.sh # 先构建再打包 packaging/web/package.sh --skip-build # 只打包已有的 dist/web前置依赖是trunk(brew install trunk或cargo install trunk --locked)与wasm32-unknown-unknown目标。脚本的实际执行链是:
- 构建:在
apps/photocraft-web下执行trunk build --release,产物写入dist/web(由 apps/photocraft-web/Trunk.toml 的dist = "../../dist/web"指定)。Trunk 会自行下载匹配的wasm-bindgen与wasm-opt。 - 完整性校验:确认
dist/web/index.html存在;再用正则(src|href)="/[^/]检查页面是否引入了根绝对路径 URL,一旦发现立即报错退出——因为站点必须能在任意子路径下运行(下文第四节的相对路径约束)。 - 体积闸门(size gate):对
dist/web里每个.wasm检查大小,超过MAX_WASM_BYTES(默认25165824,即 24 MiB)即失败。注释里写明这是针对 Cloudflare Pages/Workers 25 MiB 单文件上限(issue #198)的"提前失败"设计,只建议在排查时用PHOTOCRAFT_WASM_MAX_BYTES覆盖。 - 打包:把站点复制到
target/web-package/photocraft-web-<version>/,放入_headers与.htaccess(注释注明"未用到的主机上无害"),附上HOSTING.md与许可证文档,最后zip -qr9输出到$DIST/photocraft-web-<version>.zip(DIST默认为dist/release)。
本地开发时也可以不打包:trunk serve(Trunk.toml 配置监听127.0.0.1:8765)或直接对dist/web起一个静态服务器即可。
wasm 体积实测与 25 MiB 托管上限
实测数据
以下数字来自 packaging/web/README.md,测量于 0.2.x 构建(packaging/web/package.sh,gzip-9、Brotli quality 11):
| 文件 | Raw | gzip | Brotli |
|---|---|---|---|
photocraft-web-<hash>_bg.wasm | 19,680,726 字节(18.8 MiB) | 8,177,260(7.8 MiB) | 5,868,731(5.6 MiB) |
photocraft-web-<hash>.js | 约 160 KB | 约 23 KB | 约 20 KB |
为什么必须压体积
Cloudflare Pages 与 Workers 静态资源会拒绝任何超过25 MiB(26,214,400 字节)的单文件。当前的 wasm 能放进去,而package.sh在超过 24 MiB 时就让构建失败,确保发布物不会因为体积回归而被常见托管方拒收——文档还提到 0.2.0 及之前的 release 曾发布过 25.8 MiB 的 wasm,被 Cloudflare 拒绝(issue #198)。
体积控制由两层实现共同完成:
第一层:wasm-releaseCargo profile。工作区根 Cargo.toml 中专门定义了该 profile,并附了完整的动机注释:
# 主机对单文件大小有限制(Cloudflare Pages/Workers: 25 MiB),且 wasm 每次访问都要下载, # 所以这个 profile 面向体积优化:fat LTO,除像素处理 crate 外全部 opt-level "s"。 [profile.wasm-release] inherits = "release" opt-level = "s" lto = "fat" codegen-units = 1 strip = "debuginfo" [profile.wasm-release.package."*"] opt-level = "s" # 像素处理类 crate 保持 opt-level 3,让滤镜与合成保持速度: [profile.wasm-release.package.photocraft-raster] opt-level = 3 # …(photocraft-color / cms / compose / algo / paint 同为 opt-level = 3)注释中给出的实际效果是:该 profile 把 wasm 从 release 配置的 26.3 MiB 降到 18.8 MiB。opt-level = "s"+ fat LTO 负责整体瘦身,而对真正吃 CPU 的像素类 crate(raster、color、cms、compose、algo、paint)单独保留opt-level = 3,是"体积优先、热点不牺牲速度"的折中。该 profile 通过 apps/photocraft-web/index.html 中 trunk 指令的data-cargo-profile="wasm-release"生效。
第二层:wasm-opt -Oz。index.html 的<link>location /photocraft/ { types { application/wasm wasm; text/javascript js; text/html html; } gzip on; gzip_types application/wasm text/javascript text/html; location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } location ~* index\.html$ { add_header Cache-Control "no-cache"; } }
Netlify / Cloudflare Pages 与 Apache
发布包自带的 _headers(Netlify/Cloudflare Pages 语法):
/* X-Content-Type-Options: nosniff /*.wasm Content-Type: application/wasm Cache-Control: public, max-age=31536000, immutable /*.js Cache-Control: public, max-age=31536000, immutable /index.html Cache-Control: no-cache.htaccess(Apache,需要mod_mime,mod_deflate与mod_headers可选):
AddType application/wasm .wasm AddType text/javascript .js <IfModule mod_deflate.c> AddOutputFilterByType DEFLATE application/wasm text/javascript text/html </IfModule> <IfModule mod_headers.c> <FilesMatch "\.(wasm|js)$"> Header set Cache-Control "public, max-age=31536000, immutable" </FilesMatch> <FilesMatch "^index\.html$"> Header set Cache-Control "no-cache" </FilesMatch> </IfModule>本地快速验证
在站点文件夹内运行python3 -m http.server 8765,然后打开http://localhost:8765/。因为localhost属于安全上下文,这个方式就能验证包括 WebGPU 在内的完整路径。
在既有页面中嵌入(iframe)
packaging/web/README.md 给出的嵌入模板:
<iframe src="https://example.com/photocraft/" title="PhotoCraft image editor" style="width: 100%; height: 720px; border: 0;" allow="fullscreen; clipboard-read; clipboard-write" allowfullscreen> </iframe>配套要点:
- 尺寸由 iframe 决定:应用会填满 iframe 并跟随其大小,所以调整的是 iframe 而不是应用。
- 键盘:与任何嵌入式应用一样,用户点进 iframe 后快捷键才归它。
- 跨域嵌入可用:偏好设置存放在 iframe 自身的
localStorage中。对照源码,这个键是photocraft.preferences(apps/photocraft-web/src/web.rs 的PREFS_KEY,通过Services::load_prefs/save_prefs读写)。注意:对第三方存储做分区或拦截的浏览器可能让用户每次访问都丢失偏好,应用此时以默认值启动。 - 沙箱 iframe 的最低权限:
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"。缺allow-same-origin就没有存储;缺allow-downloads会挡住 Save 与 Export——因为 Web 版的保存就是触发浏览器下载:web.rs 的download()用 Blob + 对象 URL + 隐藏<a download>点击实现,且没有"另存为"对话框,建议文件名直接成为下载名(pick_save实现)。 - 不要发送
X-Frame-Options: DENY,也不要发送把嵌入方排除在外的frame-ancestorsCSP。
顺带一提,浏览器端与桌面版的其余差异(apps/photocraft-web/src/main.rs 头部注释):没有 TCP 控制服务器(浏览器不能监听 socket);File › Open 用浏览器文件选择器,文件字节经异步 inbox 投递;拖放文件由WebShell异步读取。可打开的扩展名清单OPEN_EXTS(web.rs)覆盖 pcraft/psd/psb/psdt、常见位图格式、HEIC/HEIF 以及主流 RAW 格式,外加 Photoshop 笔刷.abr与渐变.grd。
渲染器选择与回退
PhotoCraft 用 wgpu 渲染:浏览器支持 WebGPU 时优先 WebGPU,否则自动回退 WebGL2。URL 查询参数可强制选择,且对 iframe 的src同样生效:
| Flag | 效果 |
|---|---|
| (不传) | 有 WebGPU 用 WebGPU,否则 WebGL2 |
?webgl | 强制 WebGL2 后端(WebGPU 驱动行为异常时很有用) |
?cpu | 强制 CPU 画布路径(最慢,但兼容性最好) |
例如:<iframe src="https://example.com/photocraft/?webgl" ...>。
从 apps/photocraft-web/src/web.rs 可以看到这两个 flag 的落地方式:query()读取window.location.search,contains("webgl")时把 wgpu 的instance_descriptor.backends设为Backends::GL;contains("cpu")时(force_cpu)跳过app.set_wgpu(rs),即完全不创建 GPU 渲染状态。apps/photocraft-web/src/main.rs 的模块文档也把?cpu与桌面端的PHOTOCRAFT_CPU_CANVAS=1环境变量对应起来。
两个值得注意的实现细节:
- 大文档仍走 GPU 合成:web.rs 启动时调用
photocraft_ui_egui::gpu_canvas::use_adapter_limits,其实现(crates/ui-egui/src/gpu_canvas.rs)把纹理与缓冲尺寸上限从 egui 默认的 8192 px 换成适配器自身上限——否则宽高超过 8192 px 的文档会被迫退回 CPU 合成器。 - 启动失败的兜底:WebGPU 与 WebGL2 都没有的浏览器会在应用位置显示一条提示;若 wasm 始终下载不到(或页面版本与文件不匹配),index.html 内置的 CSS 动画会在 20 秒后显示"加载异常 + 重新加载"提示(页面自写,无需手写脚本),并有单元测试锁定这一行为(apps/photocraft-web/src/main.rs 的
loading_page_offers_a_reload_when_startup_stalls)。启动真正失败时,web.rs 会把加载区替换为错误信息,并注明"A browser with WebGPU or WebGL2 is required"。
部署核对清单
综合全文,上线前可逐项核对:
- zip 内
.wasm大小 < 24 MiB(package.sh已强制;ls -lh复查); - 服务器对
.wasm返回Content-Type: application/wasm(curl -I验证); - gzip/Brotli 已对 wasm/js/html 生效,压缩后体积接近 8 MiB / 5.6 MiB;
- 带哈希资源
immutable、index.htmlno-cache; - 站点在
https://或localhost下访问(否则只剩 WebGL2 回退); - 无 COOP/COEP 要求;若站点已有 COEP
require-corp,已补 CORP 头; - iframe 嵌入场景:
sandbox至少含allow-scripts allow-same-origin allow-downloads allow-popups,且未设置排斥嵌入方的frame-ancestors/X-Frame-Options; - WebGPU 驱动异常时可用
?webgl强制回退,最坏情况?cpu保底。
以上流程与限制均以当前仓库(工作区版本 0.3.0)的 packaging/web/README.md、packaging/web/package.sh 及apps/photocraft-web源码为准;其中体积实测数据出自 0.2.x 构建,用于说明量级时请参考对应前提。
- 图像处理
- 桌面应用
【免费下载链接】photocraft
An open-source, clean-room reimplementation of Adobe Photoshop in pure Rust
相关推荐
Dashy 云上部署全指南:静态托管、容器运行时与任意 CDN 的选型与实操
Dashy 云上部署全指南:静态托管、容器运行时与任意 CDN 的选型与实操 Dashy 本身被设计为在本地局域网 / 家庭服务器(homelab)中运行的个人
前端后端认证鉴权OpenChamber Web 版部署与使用指南:在浏览器中运行 OpenCode AI Agent
OpenChamber Web 版部署与使用指南:在浏览器中运行 OpenCode AI Agent OpenChamber 的 @openchamber/we
AI Agent人工智能代码智能体交互助手FilmCraft Web 版部署全解:把一个 13 MB 的 wasm 视频编辑器打包为可托管到任意路径的静态站点
FilmCraft Web 版部署全解:把一个 13 MB 的 wasm 视频编辑器打包为可托管到任意路径的静态站点 FilmCraft(一个用纯 Rust 重
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考