简介
客户端媒体的目标是为所有 WordPress 站点提供最高质量的媒体处理。如今,WordPress 依赖 Web 服务器——以及 GD 或 Imagick 等图像处理库——来处理图像上传。这种方式完全依赖于服务器环境,因此并非所有用户都能获得相同的体验。客户端媒体处理是 WordPress 7.1 中推出的一项新功能,它将图像处理转移到浏览器中,在将媒体上传到服务器之前先执行压缩、调整大小、格式转换、旋转以及“缩略图”生成等操作。借助现代 Web 能力和原生浏览器 API,现在开箱即用即可获得一流的媒体处理。
主要优势包括:
- 一致的处理效果:无论用户使用何种主机或服务器,所有用户都能获得由通过 WebAssembly 运行的 libvips 提供的相同高质量图像处理。WordPress 可以在任意版本中发布新的或更新后的库版本,因此用户更新时总能获得最新的改进。
- 降低服务器负载:图像处理被卸载到用户的设备上,从而释放服务器资源。
- 不受 PHP 内存限制:服务器端图像处理通常受 PHP 内存限制的制约,导致处理大图像时出现失败。
- 更快的下载速度:libvips 生成的 JPEG 通常比 GD/Imagick 压缩得更好,并且支持所有现代格式,因此生成的图像更小、加载更快。
当客户端处理不可用时(浏览器不受支持、设备资源不足或被显式禁用),WordPress 会透明地回退到传统的服务器端处理,无需用户干预。
上传流程
两条路径最终都汇聚到 wp_generate_attachment_metadata,因此无论采用哪条路径,服务器端插件看到的生命周期都是相同的(有关每条路径上会触发哪些钩子的详细比较,请参阅钩子触发:客户端路径与服务器端路径):
架构概览
客户端媒体处理流水线流经多个层级:
三个软件包的拆分
客户端媒体处理被拆分到三个软件包中,每个软件包各自承担明确的职责:
@wordpress/upload-media —— 队列与编排
core/upload-media 数据存储管理上传队列并协调处理流水线。它负责:
- 维护一个带有状态(queued、processing、paused、uploaded、error)的项目队列。
- 实施并发限制(最多 5 个并发上传,最多 2 个并发图像处理操作)。
- 为每个项目按顺序串联各个操作:准备 → 转码 → 上传 → 生成缩略图 → sideload → 完成。
- 处理批量分组、进度跟踪、重试逻辑和错误处理。失败的网络请求会自动采用指数退避和抖动策略进行重试(总共最多尝试 4 次),之后才会显示错误。
- 管理用于图像预览的 blob URL,并在使用后将其撤销。
关键源文件:
packages/upload-media/src/store/—— 包含 actions、selectors 和 reducer 的类 Redux 存储。packages/upload-media/src/feature-detection.ts—— 浏览器能力检测。
@wordpress/vips —— WASM 图像处理
对 wasm-vips(libvips 的 WebAssembly 编译版本)的 JavaScript 封装。它在 Web Worker 中运行,以避免阻塞主线程,并提供以下功能:
- 格式转换(JPEG、PNG、WebP、AVIF、GIF)。
- 调整大小和裁剪(支持智能裁剪)。
- 基于 EXIF 方向的旋转。
- 透明度检测(避免将带透明度的 PNG 转换为 JPEG)。
- 压缩,质量可配置(取值 0–1,默认值为
0.82)。
关键源文件:
packages/vips/src/index.ts—— 核心处理函数。packages/vips/src/worker.ts—— 由加载器动态导入的 Web Worker 入口。packages/vips/src/vips-worker.ts—— 带延迟初始化的 Worker 封装。packages/vips/src/loader.ts—— 用于 WordPress 脚本模块发现的最小加载器。
@wordpress/media-utils —— HTTP 传输
提供 uploadMedia() 和 sideloadMedia() 函数,用于处理发往 WordPress REST API 的实际 HTTP 请求。该软件包负责文件上传、sideload 请求,以及将服务器响应传回上传存储。
处理流水线
当用户在区块编辑器中上传图像时,会执行以下流水线:
1. 准备
prepareItem() 操作会检查文件类型并确定要运行哪些操作。对于 WASM 处理器支持的图像,将使用完整的流水线。对于其他文件类型(视频、音频、文档),文件将直接上传到服务器,并采用标准的服务器端处理。
2. 转码(可选)
如果站点配置了图像格式转换(例如,通过 image_editor_output_format 过滤器将 JPEG 转换为 WebP),则 transcodeImageItem() 操作会使用 @wordpress/vips 转换图像。对于带透明度的 PNG 有特殊处理——它们不会被转换为 JPEG,以保留 alpha 通道。
3. 上传原图
uploadItem() 操作会通过 REST API 将(可选转码后的)图像上传到服务器。请求中包含 generate_sub_sizes: false,以告知服务器不要生成缩略图,因为客户端会自行处理。
4. 生成缩略图
上传完成后,服务器会返回 missing_image_sizes——仍需生成的缩略图尺寸列表。generateThumbnails() 操作会为每个缺失的尺寸创建 sideload 项目。
在处理之前,会根据有效输出尺寸对各个尺寸进行去重。当主题注册的图像尺寸与某个内置尺寸具有相同的宽度/高度/裁剪设置时(例如,Twenty Eleven 的 large 与 WordPress 核心的 medium_large 在 768×1024 下完全一致),客户端会生成一个物理文件,并通过向 sideload 路由的 image_size 参数传递数组,告知服务器以两个名称注册该文件。这与服务器端路径处理重复尺寸的方式一致,避免了生成多余的文件。
对于超过 big_image_size_threshold(默认值:2560px)的图像,还会生成一个缩放版本并进行 sideload。首先上传的是未缩放的原图(步骤 3);子尺寸的文件名派生自该原图的基本名称(basename),只有缩放后的全尺寸副本才带有 -scaled 后缀。这与 WordPress 核心的 wp_create_image_subsizes() 命名方式一致。
如果原图需要 EXIF 旋转(orientation ≠ 1),则还会生成一个旋转版本并进行 sideload。
5. Sideload 子尺寸
每个缩略图都会在客户端进行处理(调整大小/裁剪 → 可选的格式转换),然后通过 sideload 端点(POST /wp/v2/media/{id}/sideload)上传到服务器。服务器会将每个子尺寸存储到附件元数据中,并按照 image_size 中提供的每个尺寸名称进行注册。
为防止出现竞态条件,针对同一篇文章的 sideload 上传会被串行化——如果某个项目正在进行 sideload,则针对同一文章的其他项目会暂停,直到该 sideload 完成为止。
6. 完成
当某个项目的所有 sideload 完成后,finalizeItem() 操作会调用 POST /wp/v2/media/{id}/finalize。该端点会应用上下文为 'update' 的 wp_generate_attachment_metadata 过滤器,以便服务器端插件(水印、CDN 同步、自定义元数据处理等)能够在写入所有子尺寸之后对附件进行后处理。
该过滤器在初次上传期间已经以 'create' 上下文触发过一次,因此每次客户端上传时插件都会经历两轮处理。这种双重触发模式与 WordPress 在服务器上处理大图像上传的方式一致:子尺寸的生成会被延迟,并触发第二轮 'update' 处理。
finalize 步骤采用了门控机制:如果仍有任何子 sideload 处于待处理状态,该操作会等待。一旦最后一个 sideload 完成,就会触发父项目的待处理 Finalize 操作。
如果 finalize 请求失败,会记录该错误,但上传仍会被视为成功——finalize 采用尽力而为的策略,这样插件故障就不会阻塞用户的上传。
图像质量解析
客户端编码遵循与服务端图像质量管控相同的 PHP 过滤器:wp_editor_set_quality,以及针对 JPEG 输出仍受支持的 jpeg_quality。
上传完成后,附件的 REST 响应会携带一个按尺寸区分的 image_quality 字段。服务器以与 WP_Image_Editor::set_quality() 相同的方式解析输出 MIME 类型(在 image_editor_output_format 之后),然后针对完整图像尺寸应用过滤器得到 default,并针对每个已注册子尺寸的尺寸应用过滤器,仅当某个尺寸的过滤值与 default 不同时,才在 sizes 下报告该尺寸:
"image_quality": {
"default": 82,
"sizes": { "thumbnail": 60 }
}
客户端将 WordPress 的 1–100 标度转换为 vips worker 所期望的 0–1 标度,并在子尺寸缩放和转码期间应用各尺寸对应的值。当该字段缺失时(较旧的服务器),客户端会回退到硬编码的默认值 0.82。
钩子触发:客户端路径与服务端路径
将图像处理移入浏览器会改变某些服务端钩子的触发位置,甚至是否会触发。不过,整体契约得以保留:客户端路径会以相同的相对顺序,触发服务端上传所触发的全部核心上传生命周期钩子。以下结论来自对上传期间每个被触发钩子的完整审计,该审计在两条路径上针对所有受支持的图像类型运行(#80210)。
客户端上传以一系列 REST 请求的形式到达服务器,每个请求都会触发自己的钩子:
- 创建(
POST /wp/v2/media,原始文件):标准上传流程照常触发,包括wp_handle_upload_prefilter、wp_check_filetype_and_ext、wp_handle_upload、add_attachment、rest_insert_attachment/rest_after_insert_attachment、wp_read_image_metadata,以及上下文为'create'的wp_generate_attachment_metadata(在任何客户端生成的子尺寸存在之前)。 - 旁加载(
POST /wp/v2/media/{id}/sideload,每个生成的文件一次):每个客户端生成的子尺寸都会经过标准上传处理器,因此wp_handle_upload_prefilter和wp_handle_upload会针对每个文件触发一次,并使用最终的文件路径。伴随文件(HEIC 原图、动画 GIF 的视频和海报)经过相同的端点并触发相同的逐文件钩子。REST 上传路径始终使用upload动作,因此wp_handle_sideload_prefilter在两条路径上均不会触发。 - 完成(
POST /wp/v2/media/{id}/finalize):wp_generate_attachment_metadata会以上下文'update'和完整的sizes数组再次触发——参见完成。
以新角色触发的过滤器
由于编码发生在浏览器中,在写入图像文件时,若干图像处理过滤器不再被查询。这些过滤器仍会触发,其值也仍控制着处理管线,但它们是在服务器计算发送给客户端的设置(REST 索引和上传响应)时被消费,而非在编码期间:
| 过滤器 | 值的使用位置 |
|---|---|
image_editor_output_format | 构建由客户端应用的按 MIME 的输出格式映射。 |
wp_editor_set_quality / jpeg_quality | 按已注册尺寸解析为上传响应的 image_quality 字段。 |
big_image_size_threshold | 通过 REST 索引发送给客户端;缩放发生在浏览器中。 |
image_save_progressive | 在上传时读取;客户端据此应用渐进式/交错编码。 |
image_strip_meta | 在 REST 索引上导出;当其返回 false 时,客户端会在生成的图像上保留所有元数据。 |
image_max_bit_depth | 在 REST 索引上导出;客户端据此限制生成图像的输出位深。 |
intermediate_image_sizes / intermediate_image_sizes_advanced | 在计算已注册尺寸和 missing_image_sizes 时被消费。 |
这些过滤器上的回调仍然有效,但它们在计算设置时运行,而不是每次编码运行一次,因此它们看到的参数值或调用次数可能与服务端路径不同。
在客户端路径上不会触发的钩子
以下三个钩子是服务端图像编辑所固有的,在客户端路径上永远不会触发:
| 钩子 | 无法触发的原因 | 替代方案 |
|---|---|---|
wp_image_editors | 不会实例化服务端的 WP_Image_Editor。 | 依赖自定义图像编辑器实现的站点可以通过 wp_client_side_media_processing_enabled 禁用该功能。 |
image_make_intermediate_size | 子尺寸并非由服务端编辑器生成。 | wp_handle_upload 会针对每个旁加载的子尺寸文件触发一次,而 wp_generate_attachment_metadata 的完成阶段会交付完整的元数据。 |
image_memory_limit | 图像处理不涉及 PHP 内存。 | 不需要——处理发生在浏览器中。 |
这些钩子被有意保持静默:人为触发它们会误导插件,使其以为可以修改已在浏览器中完成编码的文件。
EXIF 旋转
对于携带 EXIF 方向信息的图像,wp_image_maybe_exif_rotate 仍会在创建请求时触发,但媒体端点在处理客户端上传时会将其值强制为 false:客户端从 exif_orientation 响应字段读取方向并自行应用旋转(参见缩略图生成)。
WASM 模块加载
WASM worker 包采用惰性加载——仅当第一张图像需要处理时才加载:
- 加载器发现:
@wordpress/vips/loader是一个轻量模块,它会动态导入@wordpress/vips/worker,这样 WordPress 的脚本模块系统无需将 WASM 二进制文件拉入初始编辑器包中即可发现该依赖。 Worker 创建:首次使用时,会从内联 blob URL 启动一个 Web Worker。
vips.wasm和vips-heif.wasm在构建时以内联 base64 数据 URL 的形式嵌入,从而避免了单独下载文件以及 WASM MIME 类型问题。单实例:
getVips()会缓存一个 promise,使并发的首次调用共享同一次初始化。该 worker 在多次上传之间保持存活,并在上传队列清空或其被回收时由terminateVipsWorker()终止(参见下文的内存管理)。内存管理:WASM 线性内存只能增长、无法收缩,因此处理管线会主动限制其累积量:
- libvips 操作缓存在启动时被禁用(
Cache.max(0))。否则 libvips 会缓存先前操作的结果,这会在一批上传中无限制地增长 WASM 内存,并可能触发内存耗尽崩溃。 - worker 在每完成 50 个 vips 操作后被回收(终止并在下次使用时重新创建),成功与失败均计入计数,因此突发的大量失败无法绕过该预算。只要有操作仍在进行,回收就会被推迟,因此活跃的 worker 绝不会在操作中途被终止。
- Emscripten 的
setAutoDeleteLater(true)负责自动清理,每个操作完成后还会运行一次手动清理。操作会被记录在inProgressOperations集合中,以便可以通过进度回调终止开关在中途取消。
- libvips 操作缓存在启动时被禁用(
- 批量缩略图生成:
batchResizeImage()使用newFromBuffer()对源图像进行一次解码,并调用image.copyMemory()将像素物化到 WASM 内存中。随后,每个子尺寸都通过thumbnailImage()基于这份内存中的副本生成,并直接写入输出格式。
跨源隔离
客户端媒体处理需要 SharedArrayBuffer 来支持 WASM 线程,而浏览器仅在跨源隔离的上下文中提供它。
PHP 头信息
对于 Chromium 137 及以上版本,WordPress 会在区块编辑器屏幕上发送 Document-Isolation-Policy(DIP)头:
Document-Isolation-Policy: isolate-and-credentialless
该头提供了针对单个文档的跨源隔离,这是使用 SharedArrayBuffer 能力所必需的。客户端媒体处理利用该能力在单独的 worker 线程中运行处理,从而提升性能,并避免在图像处理期间阻塞主线程。
该标头由 gutenberg_send_document_isolation_policy_header()(位于 lib/media/load.php 中)在 load-post.php、load-post-new.php、load-site-editor.php 和 load-widgets.php 界面上发送。在带有除 edit 以外的 action 参数的管理页面上会跳过 DIP,以避免与依赖同源 iframe 访问的页面构建器发生冲突。
gutenberg_use_document_isolation_policy 过滤器可用于控制是否应用 DIP。禁用 DIP 也会同时禁用客户端媒体处理,因为没有 SharedArrayBuffer,WASM worker 便无法运行。
不注入 crossorigin 属性
isolate-and-credentialless 会以无凭证方式加载跨源子资源,而不是阻止它们,因此来自其他源的脚本、样式、图像、音频和视频无需 crossorigin 属性即可正常工作。因此 WordPress 不会添加该属性。强制使用 crossorigin="anonymous" 会将每次加载都变成 CORS 请求,而对于任何未随附 Access-Control-Allow-Origin 标头的资源(例如转存到 CDN 的媒体或指向第三方 URL 的图像区块),这样的请求都会失败。
只有在脚本从画布中读回跨源图像或视频帧的像素、且仅当该源提供 CORS 标头时,才需要该属性。有此需求的代码会在其创建的元素上设置 crossOrigin(可选择通过 media.crossOrigin 过滤器),而不是依赖全页面范围的注入。
外部图像
将外部图像导入媒体库(图像区块的“上传到媒体库”操作和发布前的“外部媒体”面板)并不会在浏览器中获取图像字节。浏览器端的跨源获取受 CORS 限制——并且在 credentialless 隔离文档中会彻底失败——因此编辑器改为将图像 URL POST 到 POST /wp/v2/media(url 参数),由服务器下载并侧载该文件,这与核心的 media_sideload_image() 背后所用的底层原语相同。编辑器通过 mediaSideloadFromUrl 区块编辑器设置公开此路径。
浏览器支持
客户端媒体处理仅限于支持 Document-Isolation-Policy 的基于 Chromium 的浏览器:
| 浏览器 | 最低版本 | 说明 |
|---|---|---|
| Chrome | 137+ | 通过 Document-Isolation-Policy 提供完整支持。 |
| Edge | 137+ | 通过 Document-Isolation-Policy 提供完整支持。 |
| Firefox | — | 不支持。 |
| Safari | — | 不支持 WASM 管线;HEIC 画布回退仍然可用。 |
不支持 DIP 的浏览器会自动回退到服务器端处理。
功能检测
在启用客户端处理之前,会在 packages/upload-media/src/feature-detection.ts 中检查浏览器的能力。所有检查都必须通过;任何一项检查失败都会导致透明回退到服务器端处理。
| 检查项 | 阈值 | 原因 |
|---|---|---|
| WebAssembly 可用 | — | wasm-vips 所必需 |
| SharedArrayBuffer 可用 | — | WASM 线程所必需(意味着 Document-Isolation-Policy 已启用) |
| Web Worker 可用 | — | 运行于主线程之外的 vips worker 所必需 |
| 设备内存 | > 2 GB | WASM 图像处理可能需要将完整图像连同工作缓冲区一起保存在内存中;内存极低的设备可能会发生 OOM |
| 硬件并发 | ≥ 2 个 CPU 核心 | 在编码期间,WASM worker 可能会独占一个 CPU 核心长达数十秒 |
| 网络连接 | 非 2g/slow-2g,无 Save-Data | 避免在无法承受的网络连接上下载约 13 MB 的 worker |
CSP 允许 blob: worker | — | 创建内联 worker 所必需;通过尝试从 blob URL 构造 worker 来验证 |
在任何 JavaScript 功能检测运行之前,还会检查 PHP 端的功能标志(wp_client_side_media_processing_enabled 过滤器)。Document-Isolation-Policy 标头仅在 Chromium 137+ 上发送,从而在不支持通过 DIP 实现跨源隔离的浏览器上直接跳过 JavaScript 路径。
另一个单独的 isHeicCanvasSupported() 检查(createImageBitmap + OffscreenCanvas)决定着 HEIC 回退路径是否启用。它独立于完整的 VIPS 支持运行,因此 Safari(无法运行 WASM 管线但可以原生解码 HEIC)仍能在浏览器中处理 iPhone 照片。
支持的格式
以下格式在 WASM/vips 管线中处理(CLIENT_SIDE_SUPPORTED_MIME_TYPES):
| 格式 | 读取 | 写入 | 透明度 | 动画 | 渐进式/交错 |
|---|---|---|---|---|---|
| JPEG | 是 | 是 | 否 | 否 | 是 |
| PNG | 是 | 是 | 是 | 否 | 是 |
| WebP | 是 | 是 | 是 | 是 | 否 |
| AVIF | 是 | 是 | 是 | 否 | 否 |
| GIF | 是 | 是 | 否 | 是 | 是 |
注意:
– AVIF 编码使用 effort: 2 来平衡编码速度与质量。
– 高位深 AVIF 源(10 位或 12 位,常见于 HDR 照片)在生成的子尺寸中保留其位深,而不是被静默截断为 8 位。
– 动画 GIF 和 WebP 图像在 vips 管线中处理时会保留所有帧。
– 不透明的动画 GIF 还会在 vips 管线之外被转换为配套视频(MP4/WebM)——请参阅下文的动画 GIF 转视频转换。
– 当 PNG 含有透明度时,会跳过从 PNG 到 JPEG 的转换。
– 当 generate_sub_sizes=false 时,AVIF 上传会绕过服务器的 wp_prevent_unsupported_mime_type_uploads 检查,因此不具备服务器端 AVIF 支持的主机仍然可以接受客户端处理过的 AVIF 文件。
– UltraHDR JPEG 在子尺寸生成过程中会保留其 HDR 增益图——请参阅下文的UltraHDR JPEG。
UltraHDR JPEG
UltraHDR JPEG 会在标准 SDR 基础图像之外嵌入一张 ISO 21496-1 增益图。增益图是一种为 SDR 图像添加 HDR 数据的全新且向后兼容的方式,得到了 Google(UltraHDR)、Apple(Adaptive HDR)和 Adobe(Camera Raw、Lightroom 和 Photoshop)的支持。SDR 显示器仅显示基础图像,而 HDR 显示器则会应用增益图来恢复完整的动态范围。无论 JPEG 文件出自哪款相机或应用,该管线都会检测其中的增益图。
当上传 JPEG 时,客户端会探测其中是否含有增益图(@wordpress/vips 中的 getUltraHdrInfo(),由 libvips 的 uhdrload 加载器提供支持)。当发现增益图时:
- 原图不经修改直接上传。没有任何转码或重新编码会触及主文件,因此完整的 HDR 数据会完好无损地进入媒体库。
- 子尺寸保留其增益图。缩放操作经由 libvips 的
uhdrload/uhdrsave管线处理,该管线会在解码基础图像的同时解码增益图。增益图与基础图像同步进行缩放和裁剪,并以keep: 'icc|gainmap'保存,因此每个生成的缩略图本身都是有效的 UltraHDR JPEG。 - 跳过格式转换。
image_editor_output_format映射不会应用于 UltraHDR 源——转换为其他编解码器会剥离增益图。
对于因大图像门控而被路由到服务器的图像,会跳过此探测,因为只有客户端缩放路径会保留增益图。
HEIC/HEIF
HEIC/HEIF 通过基于画布的回退路径来处理,而不是使用 wasm-vips,因为 HEIC 所使用的 HEVC 编解码器存在专利/许可限制,导致无法在浏览器包中附带解码器。
当上传 HEIC 文件时,客户端会依次尝试三种解码策略:
createImageBitmap()——在 Safari(可通过 macOS 平台编解码器解码 HEIC)以及未来任何将 HEIC 纳入其图像管线的浏览器中有效。- 通过
HTMLImageElement→OffscreenCanvas使用操作系统许可的图像解码器——面向具备平台 HEIC 支持的浏览器的第二梯队路径。 - HEIC 容器解析 + WebCodecs
VideoDecoder——Chromium 107+ 即使在没有 HEIC 图像支持的情况下,也可以通过平台编解码器(macOS 上的 VideoToolbox、Windows 上的 Microsoft HEVC 视频扩展)解码 HEVC 比特流。客户端会解析 ISOBMFF 容器,提取 HEVC 图块,并在画布上将它们重新组装起来。
解码后的图像会被导出为 JPEG(.jpg,MIME 类型为 image/jpeg)并上传,原始 HEIC 则作为配套文件保留在 $metadata['original'] 中。在此路径中,服务器负责生成子尺寸(generate_sub_sizes: true、convert_format: true),因此 image_editor_output_format 映射会为 JPEG 子尺寸保留。
动画 GIF 转视频转换
与现代视频相比,动态 GIF 体积庞大且效率低下。当上传不透明的动态 GIF 时,客户端会将其转换为 MP4(或 WebM)视频,使其播放效果与原始 GIF 一致,但下载的数据量要少得多。这是一条与上文所述的 wasm-vips 图像路径截然不同的流水线——它使用浏览器原生的WebCodecs API 以及mediabunny库,而非 libvips。
@wordpress/video-conversion 包
转换逻辑位于一个专用包中,该包沿用了 @wordpress/vips 的 worker 模式:Web Worker 与 mediabunny 打包在一起,并通过 Comlink 风格的代理暴露给主线程,因此内存密集型的编码在主线程之外运行。
关键源文件:
packages/video-conversion/src/index.ts——convertGifToVideo()帧处理流水线。packages/video-conversion/src/worker.ts—— Worker API 接口(Comlink 端点)。packages/video-conversion/src/video-conversion-worker.ts—— Worker 宿主封装。packages/video-conversion/src/loader.ts—— 用于 WordPress 脚本模块发现的轻量加载器。
转换流水线
- 检测。
isAnimatedGif()(位于packages/upload-media/src/utils.ts)会检查 GIF89a 图形控制扩展块,以确认该文件确实是动态的。透明 GIF 会被排除在外——<video>无法再现 GIF 的透明效果——因此它们会作为普通图像上传,且没有伴随文件。 - 解码。浏览器的
ImageDecoder会解码每一帧 GIF,并遵循真实的逐帧delay值(当某一帧未报告延迟值时,默认采用 GIF 规范的 100ms / 10fps)。 - 编码。每个解码后的
VideoFrame都会被送入 mediabunny 的VideoSampleSource,并通过 WebCodecsVideoEncoder进行编码——avc(H.264)编码进Mp4OutputFormat,或vp9编码进WebMOutputFormat。由于编解码器的要求,输出尺寸会被强制设为偶数。 - 存储为伴随文件。转换后的视频和静态首帧封面图会作为原始 GIF 附件的伴随文件被侧载(与 HEIC 原始文件采用相同的模式),并记录在
media_details.animated_video和media_details.animated_video_poster中。GIF 仍然是单个image/gif附件——视频和封面图绝不会成为单独的附件。
该操作以 OperationType.TranscodeGif 的形式接入上传 store,拥有自己的 transcodeGifItem() 操作,并由 prepareItem() 链式调用。它在运行时采用视频处理并发数限制为 1(由 getActiveVideoProcessingCount() 跟踪),独立于图像处理限制,因为编码过程非常消耗内存。
编辑器区块切换
与 HEIC(仅替换存储的文件)不同,GIF→视频转换提供了在编辑器中更改区块的选项——不存在渲染时的 PHP 过滤器。
- 视频区块的“GIF”变体。
core/video声明了两个变体:“Video”和“GIF”,二者仅通过属性组合加以区分——GIF 变体为! controls && loop && autoplay && muted && playsInline(packages/block-library/src/video/variations.js中的isGifVariation())。没有引入新的区块属性。该变体的作用域限定为block+transform(不包括插入器),因为它表示的是转换后的 GIF,而非直接插入的内容。其编辑器预览会自动播放、循环并静音,因此表现与原始 GIF 相同。 - 选择性切换。区块永远不会被自动替换。一旦伴随视频可用,独立的
core/image区块就会显示一个“以视频显示”工具栏控件(packages/block-library/src/image/animated-gif-convert-control.js中的AnimatedGifConvertControl),用它将区块替换为播放该伴随文件的视频区块 GIF 变体。该控件不会在图库(Gallery)中提供(图库只接受图像区块);媒体和文本(Media & Text)与封面(Cover)区块不受影响,因为它们中的媒体并非core/image区块。.gifURL 门控可防止非 GIF 图像仅为查找伴随文件而触发附件数据获取。 - 完全可逆。GIF 视频区块上的“以 GIF 显示”工具栏控件可将其切回原始的
core/image,因此双向往返均可实现。 - 原生前端渲染。由于转换后的区块是真正的
core/video,它会序列化为原生的<video autoplay loop muted playsinline poster>,并在前端渲染时无需任何过滤。
浏览器支持与回退
转换路径需要 WebCodecs 编码,该功能在 prepareItem 时通过 typeof ImageDecoder !== 'undefined' && typeof VideoEncoder !== 'undefined' 进行门控,此外还有针对各编解码器的 canEncodeVideo() 检查。当 WebCodecs 不可用时(例如缺少 VideoEncoder 的 Firefox),worker 会返回 Unsupported 错误,而原始 GIF 会留在队列中按原样上传。与客户端媒体处理的其他部分一样,回退过程是透明的。
PHP
PHP 方面的足迹极小(lib/media/animated-gif-to-video.php,外加一处脚本排队和一项侧载许可):
- 在编辑器中将
@wordpress/video-conversion/loader脚本模块排队(lib/client-assets.php)。 - 允许将转换后的视频和封面图作为附件的有效侧载尺寸(
class-gutenberg-rest-attachments-controller.php)。 - 当附件被删除时,清理侧载的伴随视频和封面图(
delete_attachment钩子上的gutenberg_delete_animated_gif_video()),因为 WordPress 核心的wp_delete_attachment_files()并不知道这些文件。删除操作会委托给wp_delete_file_from_directory(),并将范围限定在 uploads 目录内。
回退行为
客户端媒体处理在设计上具备透明的回退机制。当浏览器不支持 WASM 处理——或该功能被禁用时——上传会经由传统的服务器端路径进行:
- 文件会使用默认参数上传到
POST /wp/v2/media。 - 服务器使用
wp_generate_attachment_metadata()生成缩略图。 - 服务器端图像处理按配置使用 GD 或 Imagick。
- 用户体验完全相同——没有错误,也没有额外的提示。
回退是自动的,无需任何配置。
REST API 扩展
客户端媒体处理在多个方面扩展了 WordPress REST API:
新的请求参数
| 参数 | 端点 | 类型 | 默认值 | 描述 |
|---|---|---|---|---|
generate_sub_sizes | POST /wp/v2/media、POST /wp/v2/media/{id}/sideload | boolean | true | 当为 false 时,服务器会跳过缩略图生成,由客户端自行生成并侧载缩略图。 |
convert_format | POST /wp/v2/media、POST /wp/v2/media/{id}/sideload | boolean | true | 当为 false 时,服务器会跳过通过 image_editor_output_format 过滤器进行的格式转换。 |
replace_file | POST /wp/v2/media/{id}/sideload | boolean | false | 当为 true 时,用侧载的文件替换附件的主文件,更新 MIME 类型和元数据,并删除旧文件。用于 HEIC → JPEG 伴随文件路径。 |
image_size | POST /wp/v2/media/{id}/sideload | string | string[] | — | 图像尺寸名称(例如 thumbnail、medium、scaled、original)。若传入名称数组,则会在共享相同尺寸的多个尺寸下注册单个物理文件。 |
url | POST /wp/v2/media | string | — | 当提供该参数时(代替文件主体),服务器会使用 download_url() 下载远程图像,并通过 media_handle_sideload() 将其侧载。用于在无需浏览器跨域获取的情况下导入外部图像——参见下文的外部图像。 |
当 generate_sub_sizes 为 false 时,以下服务端过滤器也会被暂时禁用:
– intermediate_image_sizes_advanced —— 阻止子尺寸的生成。
– fallback_intermediate_image_sizes —— 阻止回退尺寸的生成。
– wp_image_maybe_exif_rotate —— 阻止服务端的 EXIF 旋转(由客户端处理)。
– big_image_size_threshold —— 阻止服务端的大图片缩放(由客户端处理)。
新增响应字段
| 字段 | 类型 | 描述 |
|---|---|---|
exif_orientation | 整数 | EXIF 方向值(1–8)。不为 1 的值表示该图片需要旋转。 |
missing_image_sizes | 数组 | 尚未针对此附件生成的已注册图片尺寸名称的列表。 |
filename | 字符串 | 原始附件文件名。 |
filesize | 整数 | 附件文件大小(以字节为单位)。 |
image_quality | 对象 | 来自 wp_editor_set_quality 过滤器的按尺寸区分的编码质量(1–100):{ default, sizes },其中 sizes 仅列出过滤值与 default 不同的已注册尺寸。参见 图片质量解析。 |
Sideload 端点
POST /wp/v2/media/{id}/sideload
将处理后的图片变体(缩略图、缩放版本或旋转后的原图)上传到现有附件。image_size 参数指定要上传的是哪个变体,接受任何已注册的图片尺寸名称,以及 original 和 scaled。
在将文件写入附件元数据之前,该端点会验证上传图片的尺寸是否与所声明的 image_size 相符。每种尺寸的宽度和高度都必须为正值;original 上传必须与附件存储的原始尺寸完全一致;而常规的已注册尺寸不得超过该尺寸注册的宽度或高度(允许 1px 的舍入容差)。scaled、full 和 original-heic 尺寸不受最大尺寸检查的限制。当验证失败时,上传的文件将被删除,请求会返回 400 错误:rest_upload_dimension_mismatch、rest_upload_invalid_dimensions 或 rest_upload_unknown_size。这可以防止尺寸不符的文件(例如以 thumbnail 尺寸名称旁加载的全分辨率图片)破坏附件的响应式图片集。
Finalize 端点
POST /wp/v2/media/{id}/finalize
在所有客户端操作(上传、缩略图生成、旁加载)完成后,以 'update' 上下文应用 wp_generate_attachment_metadata 过滤器。该过滤器在初次上传期间已经以 'create' 上下文触发过一次 —— 正是这第二次执行让服务端插件(水印、CDN 同步、自定义图片尺寸)能够看到完整的子尺寸元数据。
该端点需要 edit_post 和 upload_files 权限。它会读取现有的附件元数据,应用该过滤器,保存结果,并返回刷新后的附件记录。编辑器会使用此响应,将区块中存储的媒体 URL 更新为最终的服务端文件,例如为超过 big_image_size_threshold 的图片生成的 -scaled 副本。如果没有这次刷新,区块将保留未缩放原图的 URL,这将导致 wp_calculate_image_srcset() 无法匹配子尺寸文件,从而在前端不输出 srcset。
REST 索引中的媒体设置
启用客户端处理后,REST API 根索引(GET /)会(针对具有 upload_files 权限的用户)增加以下内容:
image_sizes—— 所有已注册的图片尺寸及其宽高和裁剪设置,派生自wp_get_registered_image_subsizes()。image_size_threshold—— 当前的big_image_size_threshold过滤器值。image_strip_meta—— 当前的image_strip_meta过滤器值。当为false时,客户端会在生成的图片上保留所有元数据(EXIF、XMP、IPTC),而不是去除色彩配置文件(以及 HDR 增益图)之外的所有内容。image_max_bit_depth—— 当前的image_max_bit_depth过滤器值。限制客户端生成图片的位深;与高位深的 AVIF 源相关。
其他服务端过滤器(image_editor_output_format、image_save_progressive)会在上传时于服务端读取,而不是通过索引公开。符合客户端处理条件的 MIME 类型集合固定为 CLIENT_SIDE_SUPPORTED_MIME_TYPES(JPEG、PNG、GIF、WebP、AVIF)—— 该列表没有公开的过滤器。
并发
上传存储强制执行两个独立的并发限制,以平衡性能与资源使用:
| 限制 | 默认值 | 原因 |
|---|---|---|
| 最大并发上传数 | 5 | 上传受网络限制,可以并行运行。 |
| 最大并发图片处理操作数 | 2 | WASM 图片处理是内存密集型操作。同时运行过多操作有导致内存耗尽崩溃的风险。 |
当达到并发限制时,新项目会在队列中等待。随着操作完成,待处理项目会被自动移出队列并进行处理。
此外,针对同一 WordPress 文章的旁加载上传会被串行化处理,以防止附件元数据更新中出现竞态条件。如果某篇文章已有一个旁加载正在进行,针对同一文章的其他旁加载将被暂停,直至第一个完成。
上传进度
在上传进行期间,编辑器会显示一个展示批量进度的 snackbar(@wordpress/editor 中的 UploadProgressSnackbar)。该组件是一个控制器,通过 notices 存储管理一条通知,因此它会与其他所有编辑器 snackbar 一样进行定位和堆叠。它从两个数据源读取信息,以便在这两条路径上都能报告进度:
- 用于客户端处理的
core/upload-media队列,仅统计用户上传的原始文件(生成的子尺寸和缩略图会被忽略)。 - 由编辑器的
mediaUpload包装器填充的编辑器本地跟踪器,用于传统服务端路径。
上传运行期间,文本旁会显示一个加载旋转图标(spinner);当最后一次上传完成时,它会短暂变为一个对勾,随后 snackbar 关闭。出于无障碍访问的考虑,wp.a11y.speak() 会在上传开始时播报一次、在完成时再播报一次,避免逐次进度更新带来的频繁播报。
文章保存锁定
在上传进行期间,编辑器会设置文章保存锁,以防止用户发布或保存引用了尚未完成旁加载的附件的草稿。传统上传路径和客户端路径都共享这一行为:
useUploadSaveLock监视core/upload-media存储,并在项目进出队列时向编辑器存储分发lockPostSaving/unlockPostSaving。- 传统(仅
uploadMedia)路径也通过UploadSaveLockWrapper进行锁定,后者统计活跃的上传数量,而不是跟踪队列。
“保存草稿”按钮、“发布”按钮和 Ctrl/Cmd+S 快捷键都会检查 isPostSavingLocked()。当所有项目上传完毕,或任何进行中的项目被取消时,该锁即会释放。
另请参阅
- 客户端媒体处理操作指南 —— 为插件和主题开发者提供的实用指导(禁用、自定义和调试该功能)。
- 编辑器过滤器参考:客户端媒体处理 —— 完整的过滤器与 REST API 参数参考。