使用 Cloudinary 在 WordPress 中实现响应式图片,第二部分

❥ 赞助商

在本系列的第 1 部分中,我介绍了响应式图片的一些背景信息,描述了如何向 img 元素添加 srcsetsizes 属性,以便根据用户浏览器和设备的大小和功能向网站用户提供适当大小的图片文件。我还分享了 WordPress 如何使用其原生图片调整大小功能自动实现 srcsetsizes,以及如何使用像 Cloudinary 这样的外部图片服务来扩展 WordPress 提供的原生实现。

文章系列

  1. 响应式图片和 WordPress 简介
  2. 整合 Cloudinary 和响应式图片的 WordPress 插件(您当前所在位置!)

在本期中,我将更详细地介绍 WordPress 中图片调整大小的工作原理。我将解释如何使用 WordPress 内置的钩子以及 Cloudinary 的应用程序编程接口 (API) 以及其 PHP 集成库 来创建一个 WordPress 插件,将响应式图片调整大小和优化卸载到 Cloudinary。

入门

本文假设您了解如何创建 WordPress 插件。如果您不了解,请在继续之前阅读 WordPress Codex 文章 编写插件。在我的示例中,我使用 WP-CLI 从命令行创建插件的脚手架。

接下来,按照 WordPress PHP 入门指南 中的说明下载并包含 Cloudinary 的 PHP 库 到您的插件中。将库文件保存到插件内的 `/lib/cloudinary_php/` 目录中,并使用以下代码将它们包含到您的主要插件文件中

// Load dependencies.
require 'lib/cloudinary_php/src/Cloudinary.php';
require 'lib/cloudinary_php/src/Uploader.php';
require 'lib/cloudinary_php/src/Api.php';

最后,使用在您的 Cloudinary 管理控制台 中找到的配置参数设置 Cloudinary,这些参数您定义为 `wp-config.php` 文件中的常量,因为将帐户信息直接保存到您的插件中是一个不好的做法。以下是我的插件中配置设置的示例

Cloudinary::config( array(
 "cloud_name" => CLD_CLOUD_NAME,
 "api_key"    => CLD_API_KEY,
 "api_secret" => CLD_API_SECRET
) );

现在您已经将插件配置为与 Cloudinary API 通信,您可以开始构建插件的功能。

WordPress 中的图片调整大小

在规划我的插件时,我希望它能与 WordPress 中管理图片的默认用户体验无缝集成。我还希望保留本地文件的副本,以便即使我决定停用插件,所有内容都能继续正常工作。为了实现这些目标,了解 WordPress 在您上传图片时会做什么非常有帮助。

默认情况下,wp_generate_attachment_metadata() 会调整图片大小并将有关图片的元数据存储到数据库中。

当您上传图片时,WordPress 会执行 media_handle_upload() 函数,该函数将上传的文件保存到服务器并在数据库中创建一个新的帖子,使用 wp_insert_attachment() 函数来表示图片。帖子创建后,media_handle_upload() 会调用 wp_generate_attachment_metadata() 来创建有关图片的更多元数据。就是在这个步骤中,WordPress 创建了图片的额外尺寸,并将有关这些尺寸的信息包含到附件元数据中。以下是一个 WordPress 中附件元数据默认情况下看起来像什么的示例

array(
  'width' => 1500,
  'height' => 1500,
  'file' => '2016/11/image.jpg',
  'sizes' => array(
    'thumbnail' => array(
      'file' => 'image-150x150.jpg',
      'width' => 150,
      'height' => 150,
      'mime-type' => 'image/jpeg',
    ),
    'medium' => array(
      'file' => 'image-300x300.jpg',
      'width' => 300,
      'height' => 300,
      'mime-type' => 'image/jpeg',
    ),
    'large' => array(
      'file' => 'image-1024x1024.jpg',
      'width' => 1024,
      'height' => 1024,
      'mime-type' => 'image/jpeg',
    ),
  ),
)

如您所见,WordPress 现在知道原始图片文件的名称和尺寸,以及图片上传后由 WordPress 创建的任何额外尺寸。WordPress 会引用这些元数据来创建显示在网页上的图片所需的 HTML 标记。这是我们将通过包含由 Cloudinary API 生成的信息来扩展的信息。

将 WordPress 与 Cloudinary 整合

在本文的剩余部分,我将引用插件中的 Cloudinary_WP_Integration 类。以下是 该类的完整源代码。在这个类中,我添加了一个名为 register_hooks() 的方法,它通过利用 WordPress 内置的过滤器钩子来添加我所有的自定义功能。要更好地了解 WordPress 钩子,请阅读 WordPress Codex 中的插件 API

generate_cloudinary_data() 将图片镜像到 Cloudinary 并将额外的、特定于 Cloudinary 的数据保存到数据库中。

由于我想将上传的文件镜像到 Cloudinary 并使用其 API 来生成一组用于 srcset 属性的图片尺寸,因此我首先要做的就是钩入 wp_generate_attachment_metadata 过滤器,以扩展 WordPress 正在创建的元数据。为了注册此功能,我在 register_hooks() 类中添加了以下代码

add_filter( 'wp_generate_attachment_metadata', array( $this, 'generate_cloudinary_data' ) );

这告诉 WordPress 在 wp_generate_attachment_metadata 过滤器被触发时调用我类中的 generate_cloudinary_data() 方法。以下是该方法的示例

public function generate_cloudinary_data( $metadata ) {
 // Bail early if we don't have a file path to work with.
 if ( ! isset( $metadata['file'] ) ) {
  return $metadata;
 }

 $uploads = wp_get_upload_dir();
 $filepath = trailingslashit( $uploads['basedir'] ) . $metadata['file'];

 // Mirror the image on Cloudinary and build custom metadata from the response.
 if ( $data = $this->handle_upload( $filepath ) ) {
  $metadata['cloudinary_data'] = array(
   'public_id'  => $data['public_id'],
   'width'      => $data['width'],
   'height'     => $data['height'],
   'bytes'      => $data['bytes'],
   'url'        => $data['url'],
   'secure_url' => $data['secure_url'],
  );

  foreach ( $data['responsive_breakpoints'][0]['breakpoints'] as $size ) {
   $metadata['cloudinary_data']['sizes'][$size['width'] ] = $size;
  }
 };

 return $metadata;
}

这段代码使用 wp_get_upload_dir() 来构建上传图片的路径,并将其传递给第二个方法 handle_upload(),该方法将图片上传到 Cloudinary 并返回 API 中的数据。当 handle_upload() 完成时,我将返回的数据添加到元数据中的 cloudinary_data 数组中,然后循环遍历 Cloudinary 返回的每个断点尺寸(我将在稍后解释),并将它们保存到 cloudinary_data['sizes'] 键中。让我们来看看 handle_upload() 方法中发生了什么

public function handle_upload( $file ) {
  $data = false;
  if ( is_callable( array( '\Cloudinary\Uploader', 'upload' ) ) ) {
    $api_args = array(
      'responsive_breakpoints' => array(
        array(
          'create_derived' => false,
          'bytes_step'  => 20000,
          'min_width' => 200,
          'max_width' => 1000,
          'max_images' => 20,
        ),
     ),
     'use_filename' => true,
    );
    $response = \Cloudinary\Uploader::upload( $file, $api_args );
    // Check for a valid response before returning Cloudinary data.
    $data = isset( $response['public_id'] ) ? $response : false;
  }
  return $data;
}

此方法使用 is_callable() 来确定它是否可以调用我之前导入的 \Cloudinary\Uploader 类中的 upload() 方法。如果可以,它将构建我计划传递给 \Cloudinary\Uploader::upload() 的参数。首先,我使用 Cloudinary 的 响应式图片断点功能 来根据图片本身的内容自动生成最佳的图片尺寸集。我在这里向 responsive_breakpoints 参数传递了一些选项,因此让我解释一下每个选项

  • create_derived 告诉 Cloudinary 是否应该在原始图片上传后立即创建额外的图片文件。传递 false 会生成有关图片的数据,而不会实际创建文件,直到它们被请求为止。
  • bytes_step 定义了在创建新尺寸之前,两张图片之间允许的最大字节数。我将使用 20,000(或 20 KB),但您可以根据需要调整此数字。
  • min_widthmax_width 参数告诉 Cloudinary 最小和最大图片的尺寸分别是多少,这样您就不会创建不必要的图片尺寸。
  • max_images 设置 Cloudinary 应该创建的图片总数的最大值。

有了这些信息,Cloudinary 会自动确定要创建的最佳图片数量和尺寸,以用于 srcset 属性。最后,我将 use_filename 设置为 true,这告诉 Cloudinary 使用与我上传的图片名称(定义为 $file 变量)相匹配的文件名,而不是生成随机的图片文件名。这有助于我在我的 Cloudinary 库中识别图片,但除此之外没有实际差别。

现在我已经有了将图片自动上传到 Cloudinary 并将返回的数据保存到图片的附件元数据中的方法,我可以使用这些数据从 Cloudinary 内容交付网络 (CDN) 而不是我的本地服务器提供图片。为此,我首先要过滤所有附件 URL,以便使用 Cloudinary URL 而不是本地 URL。为此,我在 register_hooks() 方法中向 wp_get_attachment_url 钩子添加了一个名为 get_attachment_url() 的过滤器,如下所示

add_filter( 'wp_get_attachment_url', array( $this, 'get_attachment_url' ), 10, 2 );

这一行会返回要传递给我的 get_attachment_url() 方法的图片的 URL 和附件 ID,该方法如下所示

public function get_attachment_url( $url, $attachment_id ) {
  $metadata = wp_get_attachment_metadata( $attachment_id );

  if ( isset( $metadata['cloudinary_data']['secure_url'] ) ) {
    $url = $metadata['cloudinary_data']['secure_url'];
  }

  return $url;
}

此方法会查找与我的图片关联的元数据,并确定是否存在我在上一步中保存的 cloudinary_data 中的 URL。如果存在,它会返回 Cloudinary 中的 URL。否则,它会返回本地 URL。

这会处理完整尺寸图片的 URL,但是替换 WordPress 创建的任何尺寸(即中间尺寸)的 URL 可能有点棘手。为了实现这一点,我需要钩入 image_downsize(),它是 WordPress 用于获取与图片关联的中间尺寸的信息的函数。在这里,我使用 Cloudinary 而不是本地文件。

以下代码注册了我的过滤器,然后是使用 Cloudinary 数据替换 WordPress 数据的方法

add_filter( 'image_downsize', array( $this, 'image_downsize' ), 10, 3 );
public function image_downsize( $downsize, $attachment_id, $size ) {
  $metadata = wp_get_attachment_metadata( $attachment_id );

  if ( isset( $metadata['cloudinary_data']['secure_url'] ) ) {
    $sizes = $this->get_wordpress_image_size_data( $size );

    // If we found size data, let's figure out our own downsize attributes.
    if ( is_string( $size ) && isset( $sizes[ $size ] ) &&
       ( $sizes[ $size ]['width'] <= $metadata['cloudinary_data']['width'] ) &&
       ( $sizes[ $size ]['height'] <= $metadata['cloudinary_data']['height'] ) ) {

      $width = $sizes[ $size ]['width'];
      $height = $sizes[ $size ]['height'];

      $dims = image_resize_dimensions( $metadata['width'], $metadata['height'], $sizes[ $size ]['width'], $sizes[ $size ]['height'], $sizes[ $size ]['crop'] );

      if ( $dims ) {
        $width = $dims[4];
        $height = $dims[5];
      }

      $crop = ( $sizes[ $size ]['crop'] ) ? 'c_lfill' : 'c_limit';

      $url_params = "w_$width,h_$height,$crop";

      $downsize = array(
        str_replace( '/image/upload', '/image/upload/' . $url_params, $metadata['cloudinary_data']['secure_url'] ),
        $width,
        $height,
        true,
      );

    } elseif ( is_array( $size ) ) {
      $downsize = array(
        str_replace( '/image/upload', "/image/upload/w_$size[0],h_$size[1],c_limit", $metadata['cloudinary_data']['secure_url'] ),
        $size[0],
        $size[1],
        true,
      );
    }
  }

  return $downsize;
}

这是一段很长的代码,所以让我逐步解释一下。同样地,我首先获取附件元数据,并检查 $metadata['cloudinary_data'] 信息。然后我使用名为 get_wordpress_image_size_data() 的辅助函数来获取在 WordPress 中注册的图片尺寸,然后将其传递给 image_resize_dimensions() 来计算如果我使用的是命名尺寸(例如,缩略图、中等)的预期尺寸。如果 $size 参数已经是尺寸数组,这偶尔会发生,我会将这些尺寸直接传递给 Cloudinary 进行处理。

我应该在这里指出,我本可以使用 Cloudinary API 来复制 WordPress 创建的所有备用尺寸。相反,我选择利用 Cloudinary 的动态 URL 图片生成功能 来生成我需要的额外尺寸,方法是将完整尺寸图片的 URL 替换为这样的动态参数

str_replace( 
  '/image/upload',
  "/image/upload/w_$size[0],h_$size[1],c_limit",
  $metadata['cloudinary_data']['secure_url'] );

如果图片尺寸应该是精确的裁剪,我将使用 Cloudinary 的 c_lfill 裁剪算法。否则,c_limit 会使图片适应我的目标尺寸,同时保持原始文件的纵横比。

完成这些步骤后,Cloudinary 应该会为上传到 WordPress 的所有新图像提供服务。最后一步是使用之前从 Cloudinary 的响应式图像断点功能获取的元数据生成 `srcset` 和 `sizes` 属性。

自动生成 srcset 和 sizes

最终结果。

要了解 WordPress 响应式图像实现的细节,你可能需要阅读 WordPress 4.4 中的响应式图像。简单来说,我将回顾 WordPress 动态添加 `srcset` 和 `sizes` 属性到图像的两种情况。

动态生成的图像的响应式标记

首先,WordPress 会自动尝试将这些属性添加到使用 `wp_get_attachment_image()` 或类似函数在模板中动态生成的任何图像。你可以通过使用 `wp_get_attachment_image_attributes` 过滤器在标记组合之前过滤图像属性,从而为这些图像添加 `srcset` 和 `sizes` 属性。

add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 10, 3 );
public function wp_get_attachment_image_attributes( $attr, $attachment, $size ) {
  $metadata = wp_get_attachment_metadata( $attachment->ID );

  if ( is_string( $size ) ) {
    if ( 'full' === $size ) {
      $width = $attachment['width'];
      $height = $attachment['height'];
    } elseif ( $data = $this->get_wordpress_image_size_data( $size ) ) {
      // Bail early if this is a cropped image size.
      if ( $data[$size]['crop'] ) {
        return $attr;
      }

      $width = $data[$size]['width'];
      $height = $data[$size]['height'];
    }
  } elseif ( is_array( $size ) ) {
    list( $width, $height ) = $size;
  }

  if ( isset( $metadata['cloudinary_data']['sizes'] ) ) {
    $srcset = '';

    foreach( $metadata['cloudinary_data']['sizes'] as $s ) {
      $srcset .= $s['secure_url'] . ' ' . $s['width'] . 'w, ';
    }

    if ( ! empty( $srcset ) ) {
      $attr['srcset'] = rtrim( $srcset, ', ' );
      $sizes = sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $width );

      // Convert named size to dimension array for the filter.
      $size = array($width, $height);
      $attr['sizes'] = apply_filters( 'wp_calculate_image_sizes', $sizes, $size, $attr['src'], $metadata, $attachment->ID );
    }
  }

  return $attr;
}

在 `wp_get_attachment_image_attributes()` 方法中,你根据 `$size` 参数计算图像的尺寸。目前,我只为那些纵横比与原始文件匹配的图像添加 `srcset` 和 `sizes`,以便我可以利用我在上传图像时 Cloudinary 提供的断点尺寸。如果我确定 `$size` 是不同的纵横比(例如硬裁剪),我将返回 `$attr` 值不变。

获得图像尺寸后,你遍历 `$metadata['cloudinary_data']['sizes']` 数组中的所有断点尺寸来构建 `srcset` 属性。之后,你根据图像宽度创建 `sizes` 属性。最后,将 `sizes` 属性值传递给 `wp_calculate_image_sizes()` 过滤器,以便主题和插件可以根据其特定的布局需求修改 `sizes` 属性。

文章内容中图像的响应式标记

WordPress 还自动将 `srcset` 和 `sizes` 属性添加到嵌入到文章内容中的图像。WordPress 不会将这些属性保存在数据库中的文章内容中,而是会在生成页面时动态生成它们。这样,随着 提供响应式图像的新方法变得可用,WordPress 可以轻松地采用它们。

你希望你的 Cloudinary 集成与原生实现一样具有未来友好性。因此,用你自己的过滤器 `make_content_images_responsive()` 替换 WordPress 使用的内容过滤器 `wp_make_content_images_responsive()`。以下是实现这两个任务的代码。

// Replace the default WordPress content filter with our own.
remove_filter( 'the_content', 'wp_make_content_images_responsive' );
add_filter( 'the_content', array( $this, 'make_content_images_responsive',  ) );
public function make_content_images_responsive( $content ) {
  if ( ! preg_match_all( '/<img [^>]+>/', $content, $matches ) ) {
    return $content;
  }

  $selected_images = $attachment_ids = array();

  foreach( $matches[0] as $image ) {
    if ( false === strpos( $image, ' srcset=' ) && preg_match( '/wp-image-([0-9]+)/i', $image, $class_id ) &&
      ( $attachment_id = absint( $class_id[1] ) ) ) {

      /*
       * If exactly the same image tag is used more than once, overwrite it.
       * All identical tags will be replaced later with 'str_replace()'.
       */
      $selected_images[ $image ] = $attachment_id;
      // Overwrite the ID when the same image is included more than once.
      $attachment_ids[ $attachment_id ] = true;
    }
  }

  if ( count( $attachment_ids ) > 1 ) {
    /*
     * Warm object cache for use with 'get_post_meta()'.
     *
     * To avoid making a database call for each image, a single query
     * warms the object cache with the meta information for all images.
     */
    update_meta_cache( 'post', array_keys( $attachment_ids ) );
  }

  foreach ( $selected_images as $image => $attachment_id ) {
    $image_meta = wp_get_attachment_metadata( $attachment_id );
    $content = str_replace( $image, $this->add_srcset_and_sizes( $image, $image_meta, $attachment_id ), $content );
  }

  return $content;
}

`make_content_images_responsive()` 方法本质上是 WordPress 中的 `wp_make_content_images_responsive()` 函数的副本,它搜索所有 `<img>` 元素的内容(处理一些边缘情况并包含一些性能优化)并将它们传递给处理添加 `srcset` 和 `sizes` 属性的第二个函数。我为此目的在我的类中创建了一个自定义回调方法,名为 `add_srcset_and_sizes()`。

public function add_srcset_and_sizes( $image, $image_meta, $attachment_id ) {
  if ( isset( $image_meta['cloudinary_data']['sizes'] ) ) {
    // See if our filename is in the URL string.
    if ( false !== strpos( $image, wp_basename( $image_meta['cloudinary_data']['url'] ) ) && false === strpos( $image, 'c_lfill') ) {
      $src = preg_match( '/src="([^"]+)"/', $image, $match_src ) ? $match_src[1] : '';
      $width  = preg_match( '/ width="([0-9]+)"/',  $image, $match_width  ) ? (int) $match_width[1]  : 0;
      $height = preg_match( '/ height="([0-9]+)"/', $image, $match_height ) ? (int) $match_height[1] : 0;

      $srcset = '';

      foreach( $image_meta['cloudinary_data']['sizes'] as $s ) {
        $srcset .= $s['secure_url'] . ' ' . $s['width'] .  'w, ';
      }

      if ( ! empty( $srcset ) ) {
        $srcset = rtrim( $srcset, ', ' );
        $sizes = sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $width );

        // Convert named size to dimension array.
        $size = array($width, $height);
        $sizes = apply_filters( 'wp_calculate_image_sizes', $sizes, $size, $src, $image_meta, $attachment_id );
      }

      $image = preg_replace( '/src="([^"]+)"/', 'src="$1" srcset="' . $srcset . '" sizes="' . $sizes .'"', $image );
    }
  }

  return $image;
}

在这里,我再次确保我的附件元数据包含来自 Cloudinary 的尺寸数据。然后,我确保图像标记包含与我上传到 Cloudinary 的图像相同的文件名,以防图像标记在插入内容后没有被编辑。最后,我包含 `false === strpos( $image, 'c_lfill')` 来确定 URL 是否指示 Cloudinary 正在对图像进行硬裁剪,类似于我在 `wp_get_attachment_image_attributes()` 中检查硬裁剪的方式。如果所有检查都通过,我就可以遍历在我最初将图像上传到 Cloudinary 时创建的断点尺寸,并使用这些数据来构建我的 `srcset` 和 `sizes` 属性。

有了这个功能,你现在可以成功地将所有响应式图像处理外包到 Cloudinary,并从 Cloudinary CDN 而不是你的本地 Web 服务器提供优化后的图像。

总结

希望这能让你更好地了解 WordPress 如何处理图像大小调整,并展示如何扩展 WordPress 以利用 Cloudinary 动态生成和提供针对不同设备类型和尺寸优化的图像。要在你的网站上试用此代码,请 从 GitHub 下载插件,并确保提供有关你认为可以改进的任何内容的反馈。


这篇文章(以及插件!)由 Joe McGill 撰写。

文章系列

  1. 响应式图片和 WordPress 简介
  2. 整合 Cloudinary 和响应式图片的 WordPress 插件(您当前所在位置!)

Cloudinary 可以极大地帮助你在网络上实现响应式图像! 他们不仅可以创建发送最佳尺寸所需的图像的不同尺寸,还可以以正确的格式发送,甚至可以完全自动化任何应用程序的过程。