PascalEugster/payloadcms-plugin-image-optimizer

GitHub: PascalEugster/payloadcms-plugin-image-optimizer

这是一个用于 Payload CMS 3.x 的自动图片优化插件,支持将上传的图片转换为 WebP/AVIF 格式并进行压缩与元数据剥离。

Stars: 12 | Forks: 0

# @inoo-ch/payload-image-optimizer [![npm version](https://img.shields.io/npm/v/@inoo-ch/payload-image-optimizer)](https://www.npmjs.com/package/@inoo-ch/payload-image-optimizer) [![npm downloads](https://img.shields.io/npm/dm/@inoo-ch/payload-image-optimizer)](https://www.npmjs.com/package/@inoo-ch/payload-image-optimizer) [![GitHub](https://img.shields.io/github/license/PascalEugster/payloadcms-plugin-image-optimizer)](https://github.com/PascalEugster/payloadcms-plugin-image-optimizer) 一个用于自动优化图片的 [Payload CMS](https://payloadcms.com) 插件。将上传的图片转换为 WebP/AVIF,调整至可配置的尺寸限制,去除 EXIF 元数据,生成 [ThumbHash](https://evanw.github.io/thumbhash/) 模糊占位符,并支持从管理面板批量重新生成。 由 [inoo.ch](https://inoo.ch) 构建和维护 —— 一家致力于打造现代网络体验的瑞士数字代理商。 ## 功能 - **格式转换** — 自动生成可配置质量的 WebP 和 AVIF 变体 - **智能调整大小** — 将图片限制在最大尺寸内,同时保持宽高比 - **EXIF 剥离** — 移除元数据以减小文件体积并提升隐私保护 - **ThumbHash 占位符** — 生成微小的模糊哈希,实现即时图片预览 - **批量重新生成** — 通过管理 UI 重新处理现有图片,并附带进度跟踪 - **按集合配置** — 可针对每个集合覆盖格式、质量和尺寸 - **管理 UI** — 侧边栏显示状态徽章、节省的文件大小以及模糊预览 - **ImageBox 组件** — 即插即用的 Next.js `` 封装,支持 ThumbHash 模糊、淡入效果、响应式变体加载和智能 `sizes` 默认值 - **响应式变体加载器** — 直接提供预生成的 Payload 尺寸变体,绕过 `/_next/image` 的重复优化 - **模板友好** — `getOptimizedImageProps()` 仅需 3 行代码即可集成到 Payload 网站模板中 - **FadeImage 组件** — 独立的淡入图片组件,适用于使用 `getImageOptimizerProps()` 的自定义设置 ## 系统要求 - Payload CMS `^3.37.0` - Next.js `^14.0.0` 或 `^15.0.0` - React `^18.0.0` 或 `^19.0.0` - Node.js `^18.20.2` 或 `>=20.9.0` ## 安装 ``` pnpm add @inoo-ch/payload-image-optimizer # 或 npm install @inoo-ch/payload-image-optimizer # 或 yarn add @inoo-ch/payload-image-optimizer ``` ## 快速开始 将插件添加到您的 `payload.config.ts`: ``` import { buildConfig } from 'payload' import { imageOptimizer } from '@inoo-ch/payload-image-optimizer' export default buildConfig({ // ... plugins: [ imageOptimizer({ collections: { media: true, }, }), ], }) ``` 就这样。上传到 `media` 集合的每张图片都将使用合理的默认值自动优化。 ## 配置 ### 完整示例 ``` imageOptimizer({ collections: { media: { formats: [ { format: 'webp', quality: 90 }, { format: 'avif', quality: 75 }, ], maxDimensions: { width: 4096, height: 4096 }, }, avatars: true, // uses global defaults }, // Global defaults (overridden by per-collection config) formats: [ { format: 'webp', quality: 80 }, // { format: 'avif', quality: 65 }, // opt-in — AVIF is ~5-10x slower to encode than WebP ], maxDimensions: { width: 2560, height: 2560 }, generateThumbHash: true, stripMetadata: true, clientOptimization: true, disabled: false, }) ``` ### 选项 | 选项 | 类型 | 默认值 | 描述 | |---|---|---|---| | `collections` | `Record` | *必需* | 要优化的集合。使用 `true` 应用默认设置,或使用对象进行覆盖。 | | `formats` | `FormatQuality[]` | `[{ format: 'webp', quality: 80 }]` | 输出格式和质量 (1-100)。 | | `maxDimensions` | `{ width: number, height: number }` | `{ width: 2560, height: 2560 }` | 图片的最大尺寸。图片将被调整大小以适应这些边界。 | | `generateThumbHash` | `boolean` | `true` | 生成 ThumbHash 模糊占位符以实现即时图片预览。 | | `stripMetadata` | `boolean` | `true` | 从图片中移除 EXIF 和其他元数据。 | | `uniqueFileNames` | `boolean` | `false` | 使用 UUID 替换文件名(例如,`photo.jpg` → `a1b2c3d4.webp`)。防止在上传和重新生成时出现 Vercel Blob "已存在" 错误。 | | `clientOptimization` | `boolean` | `true` | 在上传前使用 Canvas API 在浏览器中预先调整图片大小。对于大图片,最多可减少 90% 的上传体积。 | | `disabled` | `boolean` | `false` | 禁用优化,同时保持架构字段完整。 | ### 按集合覆盖 每个集合都可以覆盖 `formats` 和 `maxDimensions`: ``` collections: { // Hero images: higher quality, larger dimensions heroes: { formats: [{ format: 'webp', quality: 95 }], maxDimensions: { width: 3840, height: 2160 }, }, // Thumbnails: smaller, more aggressive compression thumbnails: { formats: [ { format: 'webp', quality: 60 }, { format: 'avif', quality: 45 }, ], maxDimensions: { width: 800, height: 800 }, }, } ``` ### 客户端优化 当设置 `clientOptimization: true` 时,图片会在上传前在浏览器中预先调整大小。这使用 Canvas API(零额外依赖)将大图片缩小以适应 `maxDimensions`,然后再进入上传流程。 ``` imageOptimizer({ clientOptimization: true, collections: { media: true }, }) ``` **它的作用:** - 一张 12MB 的 DSLR 照片在*上传前*被调整为约 100-500KB —— 传输的数据减少 90% 以上 - 对于使用云存储 + `clientUploads: true` 的情况尤为重要,因为文件会通过 blob 存储进行往返传输 - 减少无服务器函数的处理时间(输入更小 = sharp 转换更快) - EXIF 元数据被自动剥离(Canvas 输出不包含元数据) **保留在服务器端的内容:** 格式转换(WebP/AVIF)、ThumbHash 生成和变体创建仍然在服务器上通过 sharp 进行,以确保质量一致。客户端仅处理调整大小 —— 这是零质量折衷的最高效优化方式。 **限制:** 仅适用于管理面板中的单文件上传。批量上传和 API/程序化上传照常在服务器端处理。 ## 工作原理 1. **上传** — 图片被上传到配置的集合 2. **预处理** — 单次 sharp 流水线剥离元数据、调整大小,并可选择转换格式 —— 所有操作一次性完成 3. **保存** — Payload 将优化后的图片写入磁盘 4. **转换** — 后台作业将图片转换为额外的格式变体(例如 AVIF)并异步生成 ThumbHash 5. **完成** — 文档更新为包含变体 URL、文件大小、ThumbHash 和优化状态 格式转换和 ThumbHash 生成作为异步后台作业运行,因此上传会立即返回。 ### Vercel / 无服务器部署 图片处理(尤其是 AVIF 编码和元数据剥离)可能会超过默认的无服务器函数超时时间。插件导出了一个推荐的 `maxDuration`,您可以从 Payload API 路由中重新导出: ``` // src/app/(payload)/api/[...slug]/route.ts export { maxDuration } from '@inoo-ch/payload-image-optimizer' ``` 这将超时时间设置为 60 秒,这对于大多数配置来说已经足够。如果没有此设置,繁重的处理配置可能会导致 Vercel 上的上传超时。 #### 使用 Vercel Blob 上传大文件 即使配置了 `maxDuration` 和 `bodySizeLimit`,通过 Payload 管理面板上传的大文件仍然会经过 Next.js API 路由,这会触及 Vercel 的请求体大小限制(无服务器函数上为 4.5MB)。如果您正在使用 `@payloadcms/storage-vercel-blob`,请启用 `clientUploads` 以完全绕过此限制: ``` vercelBlobStorage({ collections: { media: true }, token: process.env.BLOB_READ_WRITE_TOKEN, clientUploads: true, // uploads go directly from browser to Vercel Blob }) ``` 启用 `clientUploads: true` 后,文件直接从浏览器上传到 Vercel Blob(高达 5TB),服务器仅处理小的 JSON 元数据负载。无论文件大小如何,这都消除了请求体大小限制错误。 #### "此 blob 已存在" 错误 当 `replaceOriginal: true`(默认)时,插件会在上传期间更改文件名(例如,`photo.jpg` → `photo.webp`)。如果该名称的 blob 已存在,Vercel Blob 会抛出错误,因为 `@payloadcms/storage-vercel-blob` 不会将 [`allowOverwrite`](https://vercel.com/docs/vercel-blob#overwriting-blobs) 传递给 Vercel Blob SDK。 **修复方法:** 在插件配置中启用 `uniqueFileNames` —— 在存储适配器看到它们之前用 UUID 替换原始文件名: ``` imageOptimizer({ collections: { media: true }, uniqueFileNames: true, // photo.jpg → a1b2c3d4-5e6f-7890-abcd-ef1234567890.webp }) ``` 这可以防止初始上传和批量重新生成时发生冲突(重新生成任务也会为云存储重新上传生成新的 UUID)。Payload 将完整的 URL 存储在数据库中,因此 UUID 文件名对您的应用程序是透明的。 **替代方案:** 如果您希望保留原始文件名,请在存储适配器上设置 `addRandomSuffix: true`: ``` vercelBlobStorage({ collections: { media: true }, token: process.env.BLOB_READ_WRITE_TOKEN, clientUploads: true, addRandomSuffix: true, }) ``` ## 与 Payload 默认图片处理的区别 Payload CMS 内置了 [sharp](https://sharp.pixelplumbing.com/),可以在上传时调整图片大小并生成尺寸。此插件在 `beforeChange` 钩子中优化上传的图片,并将结果写回 `req.file.data`。Payload 的 `generateFileData` 在钩子之前运行,并使用 `Promise.all` 处理 `imageSizes` 生成,因此该插件专注于 Payload 原生不支持的功能:格式转换(WebP/AVIF)、元数据剥离和 ThumbHash 生成。使用 `clientOptimization: true`(默认)是加快包含许多 `imageSizes` 的上传的最有效方法,因为它在 Payload 处理之前减小了源图片。 ### 对比 | 功能 | Payload 默认 | 使用此插件 | |---|---|---| | 调整至最大尺寸 | 通过 `imageSizes` 配置手动操作 | 自动 —— 全局或按集合配置一次 | | WebP/AVIF 转换 | 需要自定义钩子 | 内置,零配置 | | EXIF 元数据剥离 | 未内置 | 自动(可配置) | | 模糊哈希占位符 | 需要自定义钩子 | ThumbHash 自动生成 | | 优化状态和节省空间 | 不可用 | 每张图片的管理侧边栏面板 | | 批量重新处理现有图片 | 不可用 | 一键重新生成,附带进度跟踪 | | 带模糊占位符的 Next.js `` | 手动连接 | 即插即用的 `` / `` 组件 | | 按集合覆盖格式/质量 | 不适用 | 支持 | ### CPU 和资源影响 - **单次流水线** — 元数据剥离、调整大小和格式转换在单个 sharp 流水线中运行(一次解码/编码周期),最大限度地减少了处理开销。 - **延迟 ThumbHash** — ThumbHash 生成在后台运行(通过格式转换作业或 `waitUntil`),而不是阻塞上传响应。 - **单格式模式**(例如,仅 WebP 且 `replaceOriginal: true`)与 Payload 的默认 sharp 处理相比几乎零开销 —— 插件替换了 sharp 处理过程,而不是添加第二个过程。 - **额外的格式变体**(例如 WebP 和 AVIF)在上传后作为后台作业运行 —— 这是您会看到比原生 Payload 额外 CPU 使用的唯一领域。请注意,AVIF 编码比 WebP 慢约 5-10 倍。 - **批量重新生成** 顺序处理图片,而不是一次性处理,因此不会导致服务器激增。 如果您使用的是资源受限的服务器,请使用单格式模式,您的 CPU 成本将与原生 Payload 大致相同。 ## 管理 UI 插件向文档侧边栏添加了一个 **优化状态** 面板,显示: - 状态徽章(待处理 / 处理中 / 完成 / 错误) - 原始与优化后的文件大小及节省百分比 - ThumbHash 模糊预览缩略图 - 生成的格式变体列表,包含尺寸和文件大小 **重新生成图片** 按钮出现在集合列表视图中,允许您通过实时进度条批量重新处理现有图片。 ## 显示图片 ### 选项 1:`ImageBox`(新项目) 即插即用的 Next.js `` 封装 —— 以最佳实践显示图片的最简单方法: ``` import { ImageBox } from '@inoo-ch/payload-image-optimizer/client' // Hero image — fill mode with priority // Card grid — explicit sizes hint // Fixed dimensions ``` **它自动执行的操作:** - 每张图片的 ThumbHash 模糊占位符 - 平滑的模糊到清晰淡入过渡 - 来自 `focalX`/`focalY` 的焦点定位 - 响应式变体加载器 —— 直接提供预生成的 Payload 尺寸变体,而不是 `/_next/image` 重新优化(当在集合上配置了 `imageSizes` 时) - 填充模式的智能 `sizes` 默认值 —— `(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw`,而不是浏览器的 `100vw` 假设 - 通过 `updatedAt` 进行缓存清除 ### 选项 2:`getOptimizedImageProps()`(现有项目 / Payload 网站模板) 如果您正在使用 [Payload 网站模板](https://github.com/payloadcms/payload/tree/main/templates/website) 或拥有现有的 `` 组件,请添加 3 行代码: ``` import { getOptimizedImageProps } from '@inoo-ch/payload-image-optimizer/client' const optimizedProps = getOptimizedImageProps(resource) ``` 这将用每张图片的 ThumbHash 替换模板的硬编码模糊占位符,添加焦点支持,并启用响应式变体加载。 ### 响应式变体加载 当您的集合配置了 `imageSizes`(例如,`thumbnail: 300`、`medium: 900`、`large: 1400`)时,`ImageBox` 和 `getOptimizedImageProps()` 都会自动创建一个混合的next/image` 加载器,它将: 1. 选择大于或等于请求宽度的最小预生成变体 2. 直接从您的存储提供它(绕过 `/_next/image` —— 无双重优化) 3. 当不存在接近的变体匹配时,回退到 `/_next/image` 这意味着上传到配置了 `imageSizes` 的集合的图片可以免费获得响应式加载 —— 无需额外配置。 ## 文档架构 插件向每个配置的集合添加一个 `imageOptimizer` 字段组: ``` { imageOptimizer: { status: 'pending' | 'processing' | 'complete' | 'error', originalSize: number, // bytes optimizedSize: number, // bytes thumbHash: string, // base64-encoded ThumbHash error: string, // error message (if failed) variants: [ { format: string, // 'webp' | 'avif' filename: string, // e.g. 'photo-optimized.webp' filesize: number, width: number, height: number, mimeType: string, url: string, }, ], }, } ``` ## REST API 端点 ### 开始批量重新生成 ``` POST /api/image-optimizer/regenerate Content-Type: application/json { "collectionSlug": "media", "force": false } ``` - `force: false` — 仅重新生成尚未完成的图片 - `force: true` — 从头开始重新处理所有图片 **响应:** `{ "queued": 42, "collectionSlug": "media" }` ### 检查重新生成进度 ``` GET /api/image-optimizer/regenerate?collection=media ``` **响应:** `{ "collectionSlug": "media", "total": 42, "complete": 30, "errored": 1, "pending": 11 }` 两个端点都需要经过身份验证的用户。 ## AI Agent 集成 面向 AI 编码代理的完整技术文档可在 [`AGENT_DOCS.md`](./AGENT_DOCS.md) 中获取。它在一个参考文件中涵盖了所有配置选项、字段架构、端点、客户端工具、后台作业和上下文标志。 ### AI Agent 提示词 将此指令复制粘贴到您的 AI 编码代理,以使其自主集成插件: ## 许可证 MIT - [inoo.ch](https://inoo.ch)
标签:AVIF, CMS, EXIF, GNU通用公共许可证, MITM代理, Node.js, PayloadCMS, React, SEO, Syscalls, ThumbHash, TypeScript, WebP, 云存储, 元数据, 内容管理系统, 前端, 前端组件, 占位符, 响应式图片, 图像处理, 图片优化, 图片压缩, 图片缩放, 安全插件, 尺寸限制, 性能优化, 批量处理, 插件, 格式转换, 检测绕过, 模糊预览, 网络调试, 自动化, 自动化攻击, 隐私