WordPress 利用 PHP Exif 扩展实现图片元信息展示

文章目录
文章目录
  1. 1. 实现思路
  2. 2. 读取 EXIF 数据
  3. 3. 前端展示
  4. 4. 与主题兼容
  5. 5. 写在最后

作为一个摄影爱好者(伪),我一直希望能在博客上方便、快速地展示照片的 EXIF 信息。EXIF(Exchangeable Image File Format)是嵌入在数码照片文件中的一组元数据,它记录了拍摄时的关键信息,例如快门速度、光圈值、ISO、镜头型号、拍摄时间、相机机型等。这些信息不仅能让人回顾拍摄时的状态,也能帮助摄影爱好者更好地分析和改进拍摄技巧。对我而言,它更像是一种“照片的指纹”,让作品多了一层可回味的技术味道。

早些年,我的图片和其他静态文件都托管在七牛云上,当时利用它提供的 EXIF 接口实现信息展示,简单、方便,而且完全没有性能压力(代码附后)。但随着网站访问量逐渐上升,外链流量的费用也水涨船高。考虑到成本和可控性,我最终还是决定停用七牛云,改回自托管的方式,也因此萌生了直接在主题中实现 EXIF 展示的想法。

其实早在这之前,我写过一篇《用 WP Simple EXIF 让博客图片展示更多细节信息》,介绍过一个博友开发的开源插件。那款插件功能相当完善,配置项也非常丰富,本质上同样是基于 PHP 的 EXIF 扩展实现的。不过,它的样式相对花哨,不太符合我博客简洁的视觉风格。于是,这次我借助 AI 的帮助,从零开始按照自己的需求重新调整了一套方案,在保持简洁的同时加入了缓存机制,进一步优化性能。本文就是对这次改造的记录与分享,如果你也想在博客中展示照片的 EXIF 信息,希望这篇内容能给你一些启发。

2023.11.14-十八梯-渝丹凤银楼
相机:SONY ILCE-7RM3A   镜头:FE 35mm F1.4 GM   焦距:35MM   光圈:f/8   快门:1/80   ISO:800   时间:2023:11:14 20:49:26
2023.11.14 十八梯-渝丹凤银楼

1. 实现思路

这次实现的核心思路,是在不依赖插件的前提下,让 WordPress 自动为文章中的图片叠加 EXIF 信息。具体做法是通过 the_content 钩子对文章内容进行过滤,利用正则表达式匹配所有 <img> 标签,然后将图片的地址(src)传入自定义的 simple_exif_process_image() 函数中进行处理。

在该函数中,会先尝试将图片的 URL 转换成本地路径,再通过 EXIF 读取函数提取图片的元数据。若数据存在,就拼接成新的 HTML 结构,在图片上方生成一层包含 EXIF 信息的透明条,实现信息展示的自动化与无侵入效果。

2. 读取 EXIF 数据

读取图片信息主要依赖 PHP 内置的 exif_read_data() 函数。为保证兼容与安全,我只针对 JPG、JPEG 和 TIF 格式进行解析。代码中对关键字段进行了提取与格式化,包括相机品牌、型号、镜头、焦距、光圈、快门、ISO 值和拍摄时间。针对部分相机数据不规范的情况,还做了一些兼容性处理:

  • 焦距自动计算等效 35mm 数值
  • 光圈与快门的分数形式转换为直观可读的格式
  • 镜头信息同时支持 UndefinedTag:0xA434LensModel 字段
<?php
// 利用 PHP EXIF 扩展在图像上叠加图片元数据
if (!defined('ABSPATH')) exit;

// 过滤文章内容,自动处理图片
add_filter('the_content', function ($content) {
    if (!is_singular() && !in_the_loop()) return $content;

    return preg_replace_callback(
        '/<img(.*?)src=[\'"](.*?)[\'"](.*?)>/i',
        'simple_exif_process_image',
        $content
    );
}, 20);

/**
 * 处理单张图片
 */
function simple_exif_process_image($m)
{
    $img_tag = $m[0];
    $src     = $m[2];

    $path = simple_exif_local_path($src);
    if (!$path || !file_exists($path)) return $img_tag;

    $exif = simple_exif_data($path);
    if (empty($exif)) return $img_tag;

    return simple_exif_output($img_tag, $exif);
}

/**
 * 将 URL 转换为本地路径
 */
function simple_exif_local_path($src)
{
    $upload = wp_upload_dir();
    if (strpos($src, $upload['baseurl']) === 0) {
        $path = str_replace($upload['baseurl'], $upload['basedir'], $src);
        return file_exists($path) ? $path : '';
    }
    return '';
}

/**
 * 输出包含 EXIF 信息的 HTML 结构
 */
function simple_exif_output($img, $exif)
{
    $brand    = isset($exif['make']) ? trim($exif['make']) : '';
    $model    = isset($exif['model']) ? trim($exif['model']) : '';
    $lens     = isset($exif['lens']) ? trim($exif['lens']) : '';
    $focal    = isset($exif['focal_length']) ? strtoupper($exif['focal_length']) : '';
    $aperture = isset($exif['aperture']) ? $exif['aperture'] : '';
    $shutter  = isset($exif['shutter_speed']) ? $exif['shutter_speed'] : '';
    $iso      = isset($exif['iso']) ? $exif['iso'] : '';
    $time     = isset($exif['date_taken']) ? $exif['date_taken'] : '';

    $sep  = '&nbsp;&nbsp;&nbsp;';
    $text = '相机:' . esc_html($brand . ' ' . $model) . $sep
          . '镜头:' . esc_html($lens) . $sep
          . '焦距:' . esc_html($focal) . $sep
          . '光圈:f/' . esc_html($aperture) . $sep
          . '快门:' . esc_html($shutter) . $sep
          . 'ISO:' . esc_html($iso) . $sep
          . '时间:' . esc_html($time);

    return '<div class="wp-simple-exif-container">'
         . $img
         . '<div class="wp-simple-exif-data">' . $text . '</div>'
         . '</div>';
}

/**
 * 读取并解析 EXIF 信息
 */
function simple_exif_data($file)
{
    if (!function_exists('exif_read_data')) return [];

    $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
    if (!in_array($ext, ['jpg', 'jpeg', 'tif', 'tiff'])) return [];

    $exif = @exif_read_data($file);
    if (!$exif) return [];

    $out = [];

    if (!empty($exif['Make']))               $out['make']          = $exif['Make'];
    if (!empty($exif['Model']))              $out['model']         = $exif['Model'];
    if (!empty($exif['ISOSpeedRatings']))    $out['iso']           = $exif['ISOSpeedRatings'];
    if (!empty($exif['DateTimeOriginal']))   $out['date_taken']    = $exif['DateTimeOriginal'];

    if (!empty($exif['UndefinedTag:0xA434']))
        $out['lens'] = $exif['UndefinedTag:0xA434'];
    elseif (!empty($exif['LensModel']))
        $out['lens'] = $exif['LensModel'];

    // 焦距:优先使用等效 35mm
    if (!empty($exif['FocalLengthIn35mmFilm'])) {
        $out['focal_length'] = $exif['FocalLengthIn35mmFilm'] . 'MM';
    } elseif (!empty($exif['FocalLength'])) {
        $f = explode('/', $exif['FocalLength']);
        $focal = ($f[1] ?? 1) ? round($f[0] / $f[1]) : $f[0];
        $out['focal_length'] = $focal . 'MM';
    }

    // 光圈
    if (!empty($exif['FNumber'])) {
        $a = explode('/', $exif['FNumber']);
        $out['aperture'] = ($a[1] ?? 1) ? round($a[0] / $a[1], 1) : $a[0];
    }

    // 快门
    if (!empty($exif['ExposureTime'])) {
        $s = explode('/', $exif['ExposureTime']);
        if (count($s) == 2 && intval($s[1]) > 0)
            $out['shutter_speed'] = '1/' . intval($s[1]);
        else
            $out['shutter_speed'] = $exif['ExposureTime'];
    }

    return $out;
}

3. 前端展示

前端部分主要通过 .wp-simple-exif-container 容器实现,将图片与 EXIF 信息放在一起,层叠展示。图片上方会出现一条半透明的黑色信息条,显示相机型号、镜头、光圈、快门、ISO、拍摄时间等字段,整体简洁不喧宾夺主。

同时加入了基础的交互动画:鼠标悬停时信息条平滑滑出,图片可轻微放大,既保持低调,又有一定动感。针对移动端,还调整了字体与间距,保证在小屏幕上也能清晰显示。

/* EXIF 信息容器样式 */
.wp-simple-exif-container {
    position: relative;
    display: inline-block;
    max-width: 100%;
    margin: 0;
    padding: 0;
    line-height: 0;
    overflow: hidden;
    box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
}

.wp-simple-exif-container img {
    display: block;
    max-width: 100%;
    height: auto;
    transition: transform .3s ease;
}

/* 信息条样式 */
.wp-simple-exif-data {
    position: absolute;
    top: -0.2px;
    left: 0;
    right: 0;
    padding: 8px 12px;
    background-color: rgba(0, 0, 0, .5);
    color: #fff;
    font-size: 13px;
    line-height: 1.4;
    transform: translateY(-100%);
    transition: transform .3s ease;
    z-index: 2;
    text-align: center;
    max-height: 60%;
    overflow-y: auto;
    white-space: pre-wrap;
}

/* 悬停显示效果 */
.wp-simple-exif-container:hover .wp-simple-exif-data {
    transform: translateY(0);
}

/* 移动端适配 */
@media screen and (max-width: 600px) {
    .wp-simple-exif-data {
        font-size: 10px;
        padding: 6px;
    }
}

/* 可选:悬停放大图片效果 */
/* 
.wp-simple-exif-container:hover img {
    transform: scale(1.01);
}
*/

4. 与主题兼容

为了让这套 EXIF 展示方案与主题原有的灯箱(lightGallery)功能兼容,我在 main.js 中对灯箱的初始化代码进行了小幅调整,主要是扩展了图片选择器的范围。原本的选择器仅能识别普通的 <a><img></a> 结构,而现在增加了对 .wp-simple-exif-container 包裹图片的识别,从而保证图片在显示 EXIF 信息的同时,依旧可以正常触发灯箱的点击放大、预览与切换功能。

除此之外,我还针对 WordPress 自带的相册模块(Gallery Block)以及部分文章中 img 标签附加 <a> 链接的情况进行了兼容性测试。由于不同主题在渲染图片时的 DOM 层级、class 命名略有差异,适当调整选择器范围与优先级是必要的。目前这套方案在 Impeka 主题中表现稳定,既能保持主题的灯箱交互,也能在各类图片展示场景下顺利加载 EXIF 信息。如果你使用的是其他主题,只需根据其图片容器的 class 名称微调选择器,即可实现同样的兼容效果。

20241006 汉源湖的风景-4
相机:DJI FC3411   镜头:22.4 mm f/2.8   焦距:22MM   光圈:f/2.8   快门:1/1600   ISO:120   时间:2024:10:06 13:49:12
20241006 汉源湖的风景

5. 写在最后

整个方案思路清晰、实现轻量,不依赖额外插件,仅用少量 PHP 与 CSS 即可完成。它既能保持主题的简洁风格,又为照片增添了实用的细节展示。后续若要进一步优化,可以加入缓存机制(例如 transient 缓存),或将样式、字体等配置项外部化,方便按需调整。

如果你也希望在博客中为照片添加一点“摄影味”,不妨试试这种方式——简单、优雅、够极客。最后,我把之前用启牛云接口的方案一并附上,你如果恰巧也用的是七牛、百度或阿里 OSS 这类对象存储,可以参考下。

// 以下 JavaScript 代码原用于从七牛 CDN 获取图片 EXIF 信息并插入到页面中
// —— 2025.10.06 起停用七牛,改为使用本地 PHP 插件方案

$(document).ready(function () {
    $('#grve-post-content, #grve-single-content')
        .find('.wp-block-image img[src*="staticfile.shephe.com"], .wp-block-gallery img[src*="staticfile.shephe.com"]')
        .each(function () {
            var img = this;
            var $el = $(this);
            var src = img.src.toLowerCase();

            var index = -1;
            var ext = '';

            if (src.lastIndexOf('.jpg') > -1) {
                index = src.lastIndexOf('.jpg');
                ext = '.jpg';
            } else if (src.lastIndexOf('.jpeg') > -1) {
                index = src.lastIndexOf('.jpeg');
                ext = '.jpeg';
            } else if (src.lastIndexOf('.png') > -1) {
                index = src.lastIndexOf('.png');
                ext = '.png';
            } else {
                return; // 非支持格式跳过
            }

            // 构建七牛 EXIF 请求地址
            var exifUrl = src.slice(0, index) + ext + '?exif';

            // 请求 EXIF 数据
            $.ajax({
                url: exifUrl,
                success: function (res) {
                    var exif = res;

                    var parse = function (attr, label) {
                        return !attr ? '' : (label ? label + ' ' : '') + attr.value + ' ';
                    };

                    var datetime = exif.DateTimeOriginal.val.split(/\:|\s/);
                    var date = datetime[0] + '-' + datetime[1] + '-' + datetime[2];

                    var model = exif.Model ? exif.Model.val : '无';
                    var fnum = exif.FNumber ? exif.FNumber.val.split(/\//)[1] : '无';
                    var extime = exif.ExposureTime ? exif.ExposureTime.val : '无';
                    var iso = exif.ISOSpeedRatings ? exif.ISOSpeedRatings.val.split(/,\s/)[0] : '无';
                    var flength = exif.FocalLength ? exif.FocalLength.val : '无';

                    var exifData =
                        '日期:' + date +
                        ' 器材:' + model +
                        ' 光圈:' + fnum +
                        ' 快门:' + extime +
                        ' 感光度:' + iso +
                        ' 焦距:' + flength;

                    var content =
                        '<div id="exif-text-margin" class="exif-caption" exif-data="' + exifData + '"></div>';

                    $el.after(content);
                }
            });
        });
});

/* 显示七牛 EXIF 信息的样式 */
.wp-block-image figure {
    position: relative !important;
}

.wp-block-image {
    position: relative;
}

.exif-caption {
    width: 100%;
}

/* 信息条样式 */
.exif-caption:before {
    position: absolute !important;
    top: 0;
    right: 0;
    left: 0;
    display: block;
    overflow: hidden;
    margin: 0 auto;
    height: 26px;
    max-width: 100%;
    background-color: #333;
    color: #fff;
    content: attr(exif-data);
    text-align: center;
    font-size: 13px;
    line-height: 26px;
    opacity: 0;
    transition: opacity .5s;
}

/* 悬停效果 */
figure.wp-block-gallery:hover .exif-caption[exif-data]:before {
    opacity: 0;
}

.wp-block-image figure:hover .exif-caption[exif-data]:before,
figure.wp-block-image:hover .exif-caption[exif-data]:before {
    opacity: .7;
}

/* exif end */
「WordPress 利用 PHP Exif 扩展实现图片元信息展示」有 9 条评论
  • 大致
    10/28/2025 at 12:36

    给the_content加钩子没问题。取exif有现成的wp_read_image_metadata()了解一下。

    • 的头像
      Kevin
      10/28/2025 at 21:39

      好嘞,感谢分享,回头试试~~
      若是能提高性能,那就换上~

  • 夜未央
    10/27/2025 at 08:37

    这样操作,是不是等于缓存后,网页加载只多了一个js文件?我问的是多图时会不会对加载速度造成影响。

    • 的头像
      Kevin
      10/27/2025 at 09:16

      本页第 5. 里面的内容是我以前用的,已经作废。所以:
      1. 根本就没有多加载任何 js 文件
      2. 会加载数行 css ,对页面实际渲染没有任何影响
      3. 会增加一定 php 负担,主要是读取 jpeg/jpg 的exif,更改 html 结构,做了限定,且本身运算量有限,因此几乎没有任何影响

  • 的头像
    acevs
    10/26/2025 at 16:05

    照片的快捷键或者操作记录。

  • Lvtu
    10/26/2025 at 12:00

    越来越专业,但是对我这种摄影小白来说,这些参数没啥用 😊

  • 的头像
    obaby
    10/26/2025 at 10:55

    这个对于图片博主还是蛮有用的,我的基本都没啥exif信息。哈哈哈

    • 的头像
      Kevin
      10/26/2025 at 11:06

      那倒不会哦,手机拍图只要不是通过 WordPress 自带的压缩,一般都带 exif 信息的,
      现在很多压缩软件都可以保留图片元信息~

发表评论

请输入关键词…