通过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 ZA50mm · 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 完整性
© 转载需附带本文链接,依据 CC BY-NC-SA 4.0 发布。
猜你喜欢
评论