通过Hugo短代码功能实现图片及其EXIF信息展示

目录

我有一个习惯就是在网络上分享图片时不管是调整尺寸还是压缩,都习惯保留元数据,当然也希望在分析图片时能够展示元数据。本文介绍如何在 Hugo 静态博客中借助短代码实现图片展示功能,包括单张图片和图片组的短代码实现,以及自动获取并格式化显示图片 EXIF 信息的方法。

具体效果如下图所示:

样例

也可见 万圣节和亳都·新象的一些照片 天津两日纪行 等文章。

需实现的功能

  • 支持单张图片和图片组两种展示方式
  • 自动从图片 EXIF 数据中提取拍摄信息
  • 格式化显示焦距、光圈、快门、ISO、相机品牌和镜头型号
  • 支持自定义标题和多种参数传递方式

短代码实现

1. 单张图片短代码 (figure.html)

创建 layouts/shortcodes/figure.html 文件:

<!-- layouts/shortcodes/figure.html -->
{{ $page := .Page }}

<!-- 支持两种语法:位置参数和命名参数 -->
{{ $imageParam := .Get 0 }}
{{ $src := .Get "src" }}
{{ $title := .Get "title" }}
{{ $alt := .Get "alt" }}
{{ $class := .Get "class" }}

<!-- 如果使用了位置参数(新语法) -->
{{ if $imageParam }}
  {{ $imagePath := $imageParam }}
  {{ $customTitle := "" }}
  
  <!-- 检查参数是否包含自定义标题(使用冒号分隔) -->
  {{ if in $imageParam ":" }}
    {{ $parts := split $imageParam ":" }}
    {{ $imagePath = index $parts 0 }}
    {{ $customTitle = index $parts 1 }}
  {{ end }}
  
  {{ $src = $imagePath }}
  {{ $title = $customTitle }}
{{ end }}

{{ $imageResource := $page.Resources.GetMatch $src }}

{{ if $imageResource }}
  {{ $exif := $imageResource.Exif }}
  {{ $autoTitle := $title | default $exif.Tags.ImageDescription | default (humanize (path.Base $src | replaceRE "\\..*$" "" | replaceRE "-|_" " ")) }}
  {{ $autoAlt := $alt | default $autoTitle }}
  
  <figure class="{{ $class }}">
    <a href="{{ $imageResource.RelPermalink }}" class="no-a-style" data-fancybox="global-gallery" data-caption="{{ $autoTitle }}">
      <img 
        title="{{ $autoTitle }}" 
        alt="{{ $autoAlt }}" 
        src="{{ $imageResource.RelPermalink }}" 
        loading="lazy"
      >
    </a>
    {{ if or $autoTitle $exif }}
      <figcaption>
        {{ if $autoTitle }}<span class="title">{{ $autoTitle }}</span>{{ end }}
        {{ with $exif }}
          {{ $exifParts := slice }}
          
          <!-- 1. 焦距 -->
          {{ with .Tags.FocalLength }}
            {{ $focalLength := . }}
            {{ if findRE "^[0-9]+/[0-9]+$" $focalLength }}
              {{ $parts := split $focalLength "/" }}
              {{ $numerator := float (index $parts 0) }}
              {{ $denominator := float (index $parts 1) }}
              {{ if gt $denominator 0 }}
                {{ $value := div $numerator $denominator }}
                {{ if eq (mod $value 1) 0 }}
                  {{ $focalLength = printf "%.0fmm" $value }}
                {{ else }}
                  {{ $focalLength = printf "%.1fmm" $value }}
                {{ end }}
              {{ end }}
            {{ else }}
              {{ $focalLength = printf "%smm" $focalLength }}
            {{ end }}
            {{ $exifParts = $exifParts | append $focalLength }}
          {{ end }}
          
          <!-- 2. 光圈 -->
          {{ with .Tags.FNumber }}
            {{ $fnumber := . }}
            {{ if findRE "^[0-9]+/[0-9]+$" $fnumber }}
              {{ $parts := split $fnumber "/" }}
              {{ $numerator := float (index $parts 0) }}
              {{ $denominator := float (index $parts 1) }}
              {{ if gt $denominator 0 }}
                {{ $fnumber = printf "f/%.1f" (div $numerator $denominator) }}
              {{ end }}
            {{ else }}
              {{ $fnumber = printf "f/%s" $fnumber }}
            {{ end }}
            {{ $exifParts = $exifParts | append $fnumber }}
          {{ end }}
          
          <!-- 3. 快门速度 -->
          {{ with .Tags.ExposureTime }}
            {{ $exposureTime := . }}
            {{ if findRE "^[0-9]+/[0-9]+$" $exposureTime }}
              {{ $parts := split $exposureTime "/" }}
              {{ $numerator := float (index $parts 0) }}
              {{ $denominator := float (index $parts 1) }}
              {{ if gt $denominator 0 }}
                {{ $value := div $numerator $denominator }}
                {{ if ge $value 1 }}
                  {{ $exposureTime = printf "%.0fs" $value }}
                {{ else }}
                  {{ $exposureTime = printf "1/%.0fs" (div 1 $value) }}
                {{ end }}
              {{ end }}
            {{ else }}
              {{ $exposureTime = printf "%ss" $exposureTime }}
            {{ end }}
            {{ $exifParts = $exifParts | append $exposureTime }}
          {{ end }}
          
          <!-- 4. ISO - 尝试多个可能的标签 -->
          {{ $isoValue := "" }}
          {{ with .Tags.ISOSpeedRatings }}{{ $isoValue = . }}{{ end }}
          {{ if not $isoValue }}{{ with .Tags.ISOSpeed }}{{ $isoValue = . }}{{ end }}{{ end }}
          {{ if not $isoValue }}{{ with .Tags.ISO }}{{ $isoValue = . }}{{ end }}{{ end }}
          {{ if not $isoValue }}{{ with .Tags.PhotographicSensitivity }}{{ $isoValue = . }}{{ end }}{{ end }}
          
          {{ with $isoValue }}
            {{ $isoInt := int . }}
            {{ if gt $isoInt 0 }}
              {{ $exifParts = $exifParts | append (printf "ISO%d" $isoInt) }}
            {{ end }}
          {{ end }}
          
          <!-- 5. 相机品牌 -->
          {{ with .Tags.Model }}
            {{ $cameraModel := . }}
            {{ if findRE "iPhone" $cameraModel }}
              {{ $cameraModel = "iPhone" }}
            {{ else if findRE "iPad" $cameraModel }}
              {{ $cameraModel = "iPad" }}
            {{ else if findRE "Canon" $cameraModel }}
              {{ $cameraModel = "Canon" }}
            {{ else if findRE "Nikon" $cameraModel }}
              {{ $cameraModel = "Nikon" }}
            {{ else if findRE "Sony" $cameraModel }}
              {{ $cameraModel = "Sony" }}
            {{ else if findRE "ILCE-" $cameraModel }}
              {{ $cameraModel = "Sony" }}
            {{ else if findRE "FUJIFILM" $cameraModel }}
              {{ $cameraModel = "Fujifilm" }}
            {{ else if findRE "X-T" $cameraModel }}
              {{ $cameraModel = "Fujifilm" }}
            {{ end }}
            {{ $exifParts = $exifParts | append $cameraModel }}
          {{ end }}
          
          <!-- 6. 镜头型号(完整显示) -->
          {{ with .Tags.LensModel }}
            {{ $lensModel := . }}
            {{ $lensModel = trim $lensModel " " }}
            {{ if and $lensModel (ne $lensModel "") }}
              {{ $exifParts = $exifParts | append $lensModel }}
            {{ end }}
          {{ end }}
          
          {{ if gt (len $exifParts) 0 }}
            <span class="exif">{{ delimit $exifParts " · " }}</span>
          {{ end }}
        {{ end }}
      </figcaption>
    {{ end }}
  </figure>
{{ else }}
  {{ errorf "图片未找到: %s" $src }}
{{ end }}

使用方法:

方式一:位置参数

{{< figure "image.jpg" >}}
{{< figure "image.jpg:自定义标题" >}}

方式二:命名参数

{{< figure src="image.jpg" title="自定义标题" alt="替代文本" class="custom-class" >}}

2. 图片组短代码 (figure-group.html)

创建 layouts/shortcodes/figure-group.html 文件:

<!-- layouts/shortcodes/figure-group.html -->
{{ $page := .Page }}
{{ $columns := .Get "columns" | default 2 }}
{{ $images := .Get "images" }}

<figure-group style="grid-template-columns: repeat({{ $columns }}, 1fr);">
{{ range split $images "," }}
  {{ $imageParam := trim . " " }}
  {{ if $imageParam }}
    {{ $imagePath := $imageParam }}
    {{ $customTitle := "" }}
    
    <!-- 检查参数是否包含自定义标题(使用冒号分隔) -->
    {{ if in $imageParam ":" }}
      {{ $parts := split $imageParam ":" }}
      {{ $imagePath = index $parts 0 }}
      {{ $customTitle = index $parts 1 }}
    {{ end }}
    
    {{ $imageResource := $page.Resources.GetMatch (printf "%s" $imagePath) }}
    
    {{ if $imageResource }}
      {{ $exif := $imageResource.Exif }}
      
      <!-- 确定标题:优先使用自定义标题,其次使用 EXIF 标题,最后使用文件名 -->
      {{ $title := $customTitle }}
      {{ if not $title }}
        {{ $title = $exif.Tags.ImageDescription | default (humanize (path.Base $imagePath | replaceRE "\\..*$" "" | replaceRE "-|_" " ")) }}
      {{ end }}
      
      <figure>
        <a href="{{ $imageResource.RelPermalink }}" class="no-a-style" data-fancybox="global-gallery" data-caption="{{ $title }}">
          <img 
            title="{{ $title }}" 
            alt="{{ $title }}" 
            src="{{ $imageResource.RelPermalink }}" 
            loading="lazy"
          >
        </a>
        <figcaption>
          <span class="title">{{ $title }}</span>
          {{ with $exif }}
            {{ $exifParts := slice }}
            
            <!-- 1. 焦距 -->
            {{ with .Tags.FocalLength }}
              {{ $focalLength := . }}
              {{ if findRE "^[0-9]+/[0-9]+$" $focalLength }}
                {{ $parts := split $focalLength "/" }}
                {{ $numerator := float (index $parts 0) }}
                {{ $denominator := float (index $parts 1) }}
                {{ if gt $denominator 0 }}
                  {{ $value := div $numerator $denominator }}
                  {{ if eq (mod $value 1) 0 }}
                    {{ $focalLength = printf "%.0fmm" $value }}
                  {{ else }}
                    {{ $focalLength = printf "%.1fmm" $value }}
                  {{ end }}
                {{ end }}
              {{ else }}
                {{ $focalLength = printf "%smm" $focalLength }}
              {{ end }}
              {{ $exifParts = $exifParts | append $focalLength }}
            {{ end }}
            
            <!-- 2. 光圈 -->
            {{ with .Tags.FNumber }}
              {{ $fnumber := . }}
              {{ if findRE "^[0-9]+/[0-9]+$" $fnumber }}
                {{ $parts := split $fnumber "/" }}
                {{ $numerator := float (index $parts 0) }}
                {{ $denominator := float (index $parts 1) }}
                {{ if gt $denominator 0 }}
                  {{ $fnumber = printf "f/%.1f" (div $numerator $denominator) }}
                {{ end }}
              {{ else }}
                {{ $fnumber = printf "f/%s" $fnumber }}
              {{ end }}
              {{ $exifParts = $exifParts | append $fnumber }}
            {{ end }}
            
            <!-- 3. 快门速度 -->
            {{ with .Tags.ExposureTime }}
              {{ $exposureTime := . }}
              {{ if findRE "^[0-9]+/[0-9]+$" $exposureTime }}
                {{ $parts := split $exposureTime "/" }}
                {{ $numerator := float (index $parts 0) }}
                {{ $denominator := float (index $parts 1) }}
                {{ if gt $denominator 0 }}
                  {{ $value := div $numerator $denominator }}
                  {{ if ge $value 1 }}
                    {{ $exposureTime = printf "%.0fs" $value }}
                  {{ else }}
                    {{ $exposureTime = printf "1/%.0fs" (div 1 $value) }}
                  {{ end }}
                {{ end }}
              {{ else }}
                {{ $exposureTime = printf "%ss" $exposureTime }}
              {{ end }}
              {{ $exifParts = $exifParts | append $exposureTime }}
            {{ end }}
            
            <!-- 4. ISO - 尝试多个可能的标签 -->
            {{ $isoValue := "" }}
            {{ with .Tags.ISOSpeedRatings }}{{ $isoValue = . }}{{ end }}
            {{ if not $isoValue }}{{ with .Tags.ISOSpeed }}{{ $isoValue = . }}{{ end }}{{ end }}
            {{ if not $isoValue }}{{ with .Tags.ISO }}{{ $isoValue = . }}{{ end }}{{ end }}
            {{ if not $isoValue }}{{ with .Tags.PhotographicSensitivity }}{{ $isoValue = . }}{{ end }}{{ end }}
            
            {{ with $isoValue }}
              {{ $isoInt := int . }}
              {{ if gt $isoInt 0 }}
                {{ $exifParts = $exifParts | append (printf "ISO%d" $isoInt) }}
              {{ end }}
            {{ end }}
            
            <!-- 5. 相机品牌 -->
            {{ with .Tags.Model }}
              {{ $cameraModel := . }}
              {{ if findRE "iPhone" $cameraModel }}
                {{ $cameraModel = "iPhone" }}
              {{ else if findRE "iPad" $cameraModel }}
                {{ $cameraModel = "iPad" }}
              {{ else if findRE "Canon" $cameraModel }}
                {{ $cameraModel = "Canon" }}
              {{ else if findRE "Nikon" $cameraModel }}
                {{ $cameraModel = "Nikon" }}
              {{ else if findRE "Sony" $cameraModel }}
                {{ $cameraModel = "Sony" }}
              {{ else if findRE "ILCE-" $cameraModel }}
                {{ $cameraModel = "Sony" }}
              {{ else if findRE "FUJIFILM" $cameraModel }}
                {{ $cameraModel = "Fujifilm" }}
              {{ else if findRE "X-T" $cameraModel }}
                {{ $cameraModel = "Fujifilm" }}
              {{ end }}
              {{ $exifParts = $exifParts | append $cameraModel }}
            {{ end }}
            
            <!-- 6. 镜头型号(完整显示) -->
            {{ with .Tags.LensModel }}
              {{ $lensModel := . }}
              {{ $lensModel = trim $lensModel " " }}
              {{ if and $lensModel (ne $lensModel "") }}
                {{ $exifParts = $exifParts | append $lensModel }}
              {{ end }}
            {{ end }}
            
            {{ if gt (len $exifParts) 0 }}
              <span class="exif">{{ delimit $exifParts " · " }}</span>
            {{ end }}
          {{ end }}
        </figcaption>
      </figure>
    {{ else }}
      {{ errorf "图片未找到: %s" (printf "%s" $imagePath) }}
    {{ end }}
  {{ end }}
{{ end }}
</figure-group>

使用方法:

{{< figure-group images="image1.jpg, image2.jpg:带标题的图片, image3.jpg" columns="3" >}}

EXIF 信息处理

数据提取

Hugo 通过 .Exif 方法自动获取图片的 EXIF 数据,主要提取以下信息:

  • 焦距 (FocalLength)
  • 光圈值 (FNumber)
  • 曝光时间 (ExposureTime)
  • ISO 感光度 (ISOSpeedRatings/ISO/PhotographicSensitivity)
  • 相机型号 (Model)
  • 镜头型号 (LensModel)

数据格式化

焦距处理:

{{ with .Tags.FocalLength }}
  {{ if findRE "^[0-9]+/[0-9]+$" . }}
    <!-- 转换分数格式为小数 -->
  {{ else }}
    {{ printf "%smm" . }}
  {{ end }}
{{ end }}

光圈值处理:

{{ with .Tags.FNumber }}
  {{ if findRE "^[0-9]+/[0-9]+$" . }}
    <!-- 计算实际光圈值 -->
    {{ printf "f/%.1f" (div $numerator $denominator) }}
  {{ else }}
    {{ printf "f/%s" . }}
  {{ end }}
{{ end }}

ISO 值处理(多标签尝试):

{{ $isoValue := "" }}
{{ with .Tags.ISOSpeedRatings }}{{ $isoValue = . }}{{ end }}
{{ if not $isoValue }}{{ with .Tags.ISO }}{{ $isoValue = . }}{{ end }}{{ end }}
{{ if not $isoValue }}{{ with .Tags.PhotographicSensitivity }}{{ $isoValue = . }}{{ end }}{{ end }}

{{ with $isoValue }}
  <!-- 处理不同类型 -->
  {{ if eq (printf "%T" .) "uint16" }}
    {{ printf "ISO%d" . }}
  {{ else }}
    {{ printf "ISO%s" . }}
  {{ end }}
{{ end }}

显示格式

最终 EXIF 信息以统一格式显示:

焦距 · 光圈 · 快门 · ISO · 相机品牌 · 镜头型号

示例:

  • 85mm · f/1.4 · 1/1250s · ISO800 · Sony · FE 55mm F1.8 ZA
  • 50mm · f/1.8 · 1/341s · iPhone · iPhone 15 Pro back camera 6.765mm f/1.78

其他说明

1. 资源获取

使用 $page.Resources.GetMatch 获取图片资源,确保正确处理 Hugo 页面包内的图片。

2. 标题优先级

标题显示优先级:自定义标题 > EXIF ImageDescription > 文件名转换。

3. 错误处理

  • 图片未找到时显示错误信息
  • 空值检查避免模板错误
  • 数据类型安全转换

4. 用户体验

  • 支持 Lightbox 效果(通过 Fancybox)
  • 懒加载提升页面性能
  • 响应式图片显示

常见问题

ISO 信息不显示

  • 检查图片是否包含 EXIF 数据
  • 尝试多个 ISO 标签名称
  • 处理不同数据类型(uint16、int、string)

数值格式化异常

  • 分数值转换为小数显示
  • 处理特殊字符和空格
  • 类型安全转换避免模板错误

移动端图片 EXIF 缺失

  • iPhone 拍摄的 HEIC 格式可能丢失部分 EXIF
  • 建议使用 JPEG 格式保证 EXIF 完整性
评论