简介

客户端媒体的目标是为所有 WordPress 站点提供最高质量的媒体处理。如今,WordPress 依赖 Web 服务器——以及 GD 或 Imagick 等图像处理库——来处理图像上传。这种方式完全依赖于服务器环境,因此并非所有用户都能获得相同的体验。客户端媒体处理是 WordPress 7.1 中推出的一项新功能,它将图像处理转移到浏览器中,在将媒体上传到服务器之前先执行压缩、调整大小、格式转换、旋转以及“缩略图”生成等操作。借助现代 Web 能力和原生浏览器 API,现在开箱即用即可获得一流的媒体处理。

主要优势包括:

  • 一致的处理效果:无论用户使用何种主机或服务器,所有用户都能获得由通过 WebAssembly 运行的 libvips 提供的相同高质量图像处理。WordPress 可以在任意版本中发布新的或更新后的库版本,因此用户更新时总能获得最新的改进。
  • 降低服务器负载:图像处理被卸载到用户的设备上,从而释放服务器资源。
  • 不受 PHP 内存限制:服务器端图像处理通常受 PHP 内存限制的制约,导致处理大图像时出现失败。
  • 更快的下载速度:libvips 生成的 JPEG 通常比 GD/Imagick 压缩得更好,并且支持所有现代格式,因此生成的图像更小、加载更快。

当客户端处理不可用时(浏览器不受支持、设备资源不足或被显式禁用),WordPress 会透明地回退到传统的服务器端处理,无需用户干预。

上传流程

两条路径最终都汇聚到 wp_generate_attachment_metadata,因此无论采用哪条路径,服务器端插件看到的生命周期都是相同的(有关每条路径上会触发哪些钩子的详细比较,请参阅钩子触发:客户端路径与服务器端路径):

上传流程:客户端路径(WASM worker 处理加上 sideload 端点)与服务器端路径(POST /wp/v2/media 加上 PHP 缩略图)都汇聚到 wp_generate_attachment_metadata

架构概览

客户端媒体处理流水线流经多个层级:

架构概览:区块编辑器调用 @wordpress/media-utils 中的 mediaUpload(),后者移交给 @wordpress/upload-media 存储,由该存储编排 @wordpress/vips Web Worker 和 REST API 端点

三个软件包的拆分

客户端媒体处理被拆分到三个软件包中,每个软件包各自承担明确的职责:

@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 请求,以及将服务器响应传回上传存储。

处理流水线

当用户在区块编辑器中上传图像时,会执行以下流水线:

六步处理流水线:准备、转码(可选)、上传原图、生成缩略图、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 包采用惰性加载——仅当第一张图像需要处理时才加载:

  1. 加载器发现:@wordpress/vips/loader 是一个轻量模块,它会动态导入 @wordpress/vips/worker,这样 WordPress 的脚本模块系统无需将 WASM 二进制文件拉入初始编辑器包中即可发现该依赖。
  2. Worker 创建:首次使用时,会从内联 blob URL 启动一个 Web Worker。vips.wasm 和 vips-heif.wasm 在构建时以内联 base64 数据 URL 的形式嵌入,从而避免了单独下载文件以及 WASM MIME 类型问题。

  3. 单实例:getVips() 会缓存一个 promise,使并发的首次调用共享同一次初始化。该 worker 在多次上传之间保持存活,并在上传队列清空或其被回收时由 terminateVipsWorker() 终止(参见下文的内存管理)。

  4. 内存管理:WASM 线性内存只能增长、无法收缩,因此处理管线会主动限制其累积量:

    • libvips 操作缓存在启动时被禁用(Cache.max(0))。否则 libvips 会缓存先前操作的结果,这会在一批上传中无限制地增长 WASM 内存,并可能触发内存耗尽崩溃。
    • worker 在每完成 50 个 vips 操作后被回收(终止并在下次使用时重新创建),成功与失败均计入计数,因此突发的大量失败无法绕过该预算。只要有操作仍在进行,回收就会被推迟,因此活跃的 worker 绝不会在操作中途被终止。
    • Emscripten 的 setAutoDeleteLater(true) 负责自动清理,每个操作完成后还会运行一次手动清理。操作会被记录在 inProgressOperations 集合中,以便可以通过进度回调终止开关在中途取消。
  5. 批量缩略图生成: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 的浏览器:

浏览器最低版本说明
Chrome137+通过 Document-Isolation-Policy 提供完整支持。
Edge137+通过 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 GBWASM 图像处理可能需要将完整图像连同工作缓冲区一起保存在内存中;内存极低的设备可能会发生 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 文件时,客户端会依次尝试三种解码策略:

  1. createImageBitmap()——在 Safari(可通过 macOS 平台编解码器解码 HEIC)以及未来任何将 HEIC 纳入其图像管线的浏览器中有效。
  2. 通过 HTMLImageElement → OffscreenCanvas 使用操作系统许可的图像解码器——面向具备平台 HEIC 支持的浏览器的第二梯队路径。
  3. 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 脚本模块发现的轻量加载器。

转换流水线

GIF 到视频的转换流水线:检测不透明的动态 GIF,使用 ImageDecoder 解码帧,使用 WebCodecs VideoEncoder 和 mediabunny 进行编码,将视频和封面图存储为伴随文件,并在编辑器中提供区块切换

  1. 检测。isAnimatedGif()(位于 packages/upload-media/src/utils.ts)会检查 GIF89a 图形控制扩展块,以确认该文件确实是动态的。透明 GIF 会被排除在外——<video> 无法再现 GIF 的透明效果——因此它们会作为普通图像上传,且没有伴随文件。
  2. 解码。浏览器的 ImageDecoder 会解码每一帧 GIF,并遵循真实的逐帧 delay 值(当某一帧未报告延迟值时,默认采用 GIF 规范的 100ms / 10fps)。
  3. 编码。每个解码后的 VideoFrame 都会被送入 mediabunny 的 VideoSampleSource,并通过 WebCodecs VideoEncoder 进行编码——avc(H.264)编码进 Mp4OutputFormat,或 vp9 编码进 WebMOutputFormat。由于编解码器的要求,输出尺寸会被强制设为偶数。
  4. 存储为伴随文件。转换后的视频和静态首帧封面图会作为原始 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 区块。.gif URL 门控可防止非 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 处理——或该功能被禁用时——上传会经由传统的服务器端路径进行:

  1. 文件会使用默认参数上传到 POST /wp/v2/media。
  2. 服务器使用 wp_generate_attachment_metadata() 生成缩略图。
  3. 服务器端图像处理按配置使用 GD 或 Imagick。
  4. 用户体验完全相同——没有错误,也没有额外的提示。

回退是自动的,无需任何配置。

REST API 扩展

客户端媒体处理在多个方面扩展了 WordPress REST API:

新的请求参数

参数端点类型默认值描述
generate_sub_sizesPOST /wp/v2/media、POST /wp/v2/media/{id}/sideloadbooleantrue当为 false 时,服务器会跳过缩略图生成,由客户端自行生成并侧载缩略图。
convert_formatPOST /wp/v2/media、POST /wp/v2/media/{id}/sideloadbooleantrue当为 false 时,服务器会跳过通过 image_editor_output_format 过滤器进行的格式转换。
replace_filePOST /wp/v2/media/{id}/sideloadbooleanfalse当为 true 时,用侧载的文件替换附件的主文件,更新 MIME 类型和元数据,并删除旧文件。用于 HEIC → JPEG 伴随文件路径。
image_sizePOST /wp/v2/media/{id}/sideloadstring | string[]—图像尺寸名称(例如 thumbnail、medium、scaled、original)。若传入名称数组,则会在共享相同尺寸的多个尺寸下注册单个物理文件。
urlPOST /wp/v2/mediastring—当提供该参数时(代替文件主体),服务器会使用 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上传受网络限制,可以并行运行。
最大并发图片处理操作数2WASM 图片处理是内存密集型操作。同时运行过多操作有导致内存耗尽崩溃的风险。

当达到并发限制时,新项目会在队列中等待。随着操作完成,待处理项目会被自动移出队列并进行处理。

此外,针对同一 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()。当所有项目上传完毕,或任何进行中的项目被取消时,该锁即会释放。

另请参阅