FilmFrame 是一间运行在浏览器里的数字暗房。它把本地照片排成接触印样,为单张照片或整卷长条生成 35mm(135)胶片边框,并在当前设备完成裁切、渲染与导出。
照片不会上传到 FilmFrame 服务。刷新或关闭页面后,照片和处理结果不会保留;胶片设置与本地配方会保存在浏览器中。生产部署可以启用服务端邀请码门禁,但鉴权服务只处理邀请码哈希与会话状态,不接触照片。
- 16 款胶片均配有独立的真实 135 PNG 模板和齿孔蒙版。
- 真实 135 支持单张成片与连续胶片长条。
- 经典片边使用 Canvas 程序化绘制,可调整文字、日期、边框和齿孔。
- 帧号颜色与真实 135 齿孔颜色可以全局自定义,也可以恢复为原片效果。
- 扫描输出可保留自定义背景色,也可以只保留底片主体。
- 支持 JPG、PNG、WebP,以及静态 HEIC/HEIF;既可选择文件,也可拖入工作区。
- HEIC/HEIF 会在当前浏览器会话中转换为 JPEG 后进入同一套预览与冲洗流程,不会上传;当前只取单个主静态画面,不处理 Live Photo、视频或多帧导出。
- 接触印样支持排序、逐张入选、全部入选、清空入选和一键删除整卷。
- 只冲洗待处理或设置已变化的照片,失败项可单独重试。
- 冲洗期间可以停止后续任务,已完成的结果会保留。
- 大尺寸或高内存压力批次会在渲染前给出提示或阻止危险操作。
- 管理员可以把单个 Canvas 的 RGBA 准入预算设为
128–2048 MiB,默认值为700 MiB;这不是上传文件大小限制,也不保证浏览器一定能完成分配。
- 在固定胶片窗口内拖动照片,使用滚轮或滑杆进行 100% 至 300% 缩放。
- 支持 90 度旋转、重置、取消与应用构图。
- 原图与成片可快速切换,设置变化后会生成本地即时预览。
- 构图只在确认后写入当前照片,取消不会改变正式结果。
- 单张输出支持 JPEG 与 PNG。
- 多张单幅成片可按当前顺序打包为 ZIP。
- 连底长条按入选顺序生成,可直接预览和下载。
- 只有与当前设置和构图匹配的结果可以下载,旧结果会标记为待更新。
- 空工作区使用连续滚动的 135 胶片作为背景,画幅保持 36:24(3:2)。
- 胶片动画在悬停、键盘聚焦和拖入文件时暂停,并遵守
prefers-reduced-motion。 - 摄影名言来自人工审核的本地快照,每 24 小时自动更换;浏览器不会为轮换实时请求第三方接口。
| 品牌 / 系列 | 型号 |
|---|---|
| Kodak Portra | 160、400、800 |
| Kodak 彩色负片 | Gold 200、Ultramax 400、ColorPlus 200、Pro Image 100、Ektar 100 |
| Kodak 反转片 | Ektachrome E100 |
| Kodak 黑白片 | Tri-X 400、T-Max 100、T-Max 400、T-Max P3200 |
| Fujifilm | Superia 400 |
| CineStill | 800T |
| Ilford | HP5 Plus |
环境要求:Node.js 20 或更高版本。
git clone https://github.com/Zeno-cc/FilmFrame.git
cd FilmFrame
npm ci
npm run devVite 默认输出本地访问地址。若需要固定到项目常用端口:
npm run dev -- --host 127.0.0.1 --port 5174- 添加 JPG、PNG、WebP、HEIC 或 HEIF 照片。
- 在接触印样中排序并选择需要冲洗的照片。
- 在暗房配方中选择胶片、真实 135 或经典片边,以及输出设置。
- 需要时打开单张预览调整构图。
- 生成单张成片或连续胶片长条。
- 下载单图、长条,或把当前有效成片打包为 ZIP。
桌面端使用右侧暗房配方面板;平板使用侧边抽屉;手机使用底部设置面板和固定操作栏。
npm run dev # 启动 Vite 开发服务器
npm run typecheck # TypeScript 类型检查
npm test # 运行 Vitest 单元测试
npm run test:e2e # 运行 Playwright 浏览器测试
npm run check # 单元测试 + 类型检查 + 生产构建
npm run check:access # 鉴权服务测试 + 类型检查 + 构建
npm run check:all # 前端与鉴权服务完整检查
npm run check:release # 完整 pre-tag 发布门禁
npm run build # 构建 dist 静态站点
npm run preview # 本地预览生产构建
npm run sync:quotes # 从 Wikiquote 生成待人工审核的名言候选
npm run verify:deployment # 校验 Compose、OpenResty 边界与可选线上探针sync:quotes 只生成 generated/ 下的候选文件,不会自动覆盖应用使用的审核快照。
Cloudflare / OpenResty
-> 服务端邀请码与会话检查
-> 通过后分发 React 静态应用
-> App 工作流与页面状态
-> Canvas / Worker 本地图像处理
-> Blob URL 预览、下载与 ZIP
独立管理域名
-> Cloudflare Access:白名单 Google + Independent MFA Passkey
-> 鉴权服务验证 Access JWT
-> 生成、查看和撤销邀请码
- React 19 + TypeScript + Vite 5
- Tailwind CSS 4 + 项目语义化 CSS token
- Canvas / OffscreenCanvas 图像合成
- Web Worker 可选渲染路径
exif-js本地读取拍摄日期heic-to/csp1.5.2 按首次 HEIC/HEIF 导入延迟加载并在本地转换静态画面;该依赖体积较大(npm 解包约 24.4 MB),许可证为 LGPL-3.0- Express 5 + SQLite 邀请码与服务端会话
- Cloudflare Access JWT 源站校验
- Vitest 单元测试与 Playwright 浏览器测试
关键文档:
应用没有普通用户账户、图片上传接口、云同步或遥测。用户照片只通过浏览器 File、Canvas、Worker 和 Blob URL 在当前页面会话中流转。HEIC/HEIF 转换同样发生在页面内;首次使用只加载同源发布的转换代码,不会把照片发送给转换服务。
可选的生产门禁使用 SQLite 保存邀请码与会话 token 的 SHA-256 哈希、有效期和撤销状态。数据库不保存邀请码明文、照片、EXIF、胶片设置或渲染结果。
运行时网络请求仅用于同源静态素材,例如真实 135 模板和齿孔蒙版,以及受邀请码会话保护的 /api/runtime-config。该配置接口只返回当前 Canvas 准入预算,不包含照片、管理员身份或邀请码信息。摄影名言来自随应用发布的审核快照;只有用户主动点击出处链接时才会打开 Wikiquote。
提交前建议运行:
npm ci
npm --prefix server/access ci
npm run check:all
npm run test:e2e
git diff --check当前测试覆盖上传校验、设置与配方存储、构图几何、批次准入、运行时 Canvas 预算、结果失效、Worker 生命周期、真实 135 素材、齿孔蒙版、空暗房响应式布局,以及从上传到冲洗、预览和导出的关键浏览器流程。
不启用访问控制时,dist/ 仍可部署到任意静态文件服务器。需要不可由前端状态绕过的邀请码门禁时,必须使用仓库中的 Compose 与 OpenResty 反代方案;纯静态托管不能形成权限边界。
| 配置项 | 值 |
|---|---|
| 前端 Node.js | >=20 |
| 鉴权服务 Node.js | 22 LTS |
| 静态容器 | 127.0.0.1:18082 |
| 鉴权容器 | 127.0.0.1:18083 |
| 持久化 | SQLite named volume |
| 配置模板 | .env.example |
| OpenResty 模板 | ops/openresty/ |
cp .env.example .env
# 填写 Cloudflare Access team domain、管理应用 audience 和管理员邮箱
docker compose up -d --build
npm run verify:deployment -- --live生产切换前还必须配置 Cloudflare Access 的精确管理员邮箱、Google 登录方式与 Independent MFA WebAuthn,清理旧缓存,并把两个 OpenResty 示例合并到对应的 1Panel 站点。任何 Google Client Secret 都只能保存在 Google/Cloudflare 配置中,不能写入 .env 或仓库。
| 浏览器路径 | 自动化范围 | 发布证据 |
|---|---|---|
| Chromium / Desktop Chrome | 完整 Playwright 回归 | CI 必须通过 |
| Firefox | 聚焦兼容流程 | CI 必须通过 |
| WebKit / Desktop Safari | 聚焦兼容流程 | CI 必须通过;不能替代真机 iPhone |
| iPhone Safari | 不以桌面模拟替代 | v1.3.0 标签前必须完成脱敏真机烟测 |
| Android Chrome | 不以桌面模拟替代 | v1.3.0 标签前必须完成脱敏真机烟测 |
Worker 或 OffscreenCanvas 不可用时,应用会回退到主线程 Canvas。真机流程和内存压力记录方法见 浏览器与移动端烟测。
仓库目前没有许可证文件。在添加明确许可证之前,请不要假定代码或胶片素材可以自由复制、修改或商用。第三方 heic-to 1.5.2 使用 LGPL-3.0;其许可证与项目自身代码/素材的授权状态应分别审阅。