Obsidian to Hugo工作流 #
Hugo允许我们使用Markdown格式进行写作,并自动转换为静态网站,Markdown编辑器可以自由地选择。我使用的是Obsidian编辑器,将.md
文件同步至HomeServer后,在Linux环境中构建并预览网站,最终Push至Github并由Cloudflare解析。这样的工作流并无可说道之处,但是其中的附件(图片、视频)的处理是比较麻烦的,常规的处理办法有:
- 使用Markdown风格链接附件,并随项目上传至Github仓库;
- 写作时使用图床(图床服务、OSS等)托管图片并在
.md
文件中以Markdown风格插入链接。
不管哪种方法,在写作时都是不够流畅的,在前期、后期还可能要处理图像的压缩问题,当然我们可以引入第三方的工具比如WebP Cloud Services,但有悖于我用Cloudflare一把梭哈的理念1,且无法解决CF的R2免费存储空间(10GB)可能不足的问题。
为了让Obsidian - Hugo
的写作、发布流更加顺畅,我之前使用脚本在本地(HomeServer)转换Obsidian的WiKi链接(双方括号)并压缩图像自动上传至CloudFlare R2。有24小时在线的HomeServer辅助,这套流程允许我在全平台同步写作、插入附件,足够流畅。但使用过程中发现,固定的压缩设置使得一些照片质量有明显的下降,而对于一些画面简单的图片,体积似乎还有压缩的空间。
因此,Obsidian - Hugo
自动化发布的核心在于解决多媒体附件(主要是图片)问题,即自动转换附件链接、自动优化图片体积与质量、自动上传OSS。
图像优化既是一门艺术,也是一门科学:说它是一门艺术,是因为单个图像的压缩并不存在明确的最佳方案,说它是一门科学,则是因为有许多发展成熟的方法和算法都能够显著缩减图像的大小。找到图像的最佳设置需要在许多方面进行认真分析:格式能力、编码数据的内容、质量、像素尺寸等。2
图像尺寸的确定 #
首先要确定目标尺寸宽高,高清原图超大的尺寸对于Web浏览来说是无意义的3,浏览器会根据css样式和设备屏幕自动缩放图像,与其让浏览器对图像降采样,不如在服务端完成,如此可以有效减小存储体积并提高网站加载速度。
应对多设备的最佳画质图片问题,可以通过优化单张图片,或者准备数张不同尺寸的图片并根据设备自动选择。为了在有限容量的R2存储桶中尽可能多地存储文件,我们还是采用优化单张图片的路线。因此首先要确定图片的宽高尺寸应该设置为多大。
图像元素 #
根据hugo-book的样式源码:
// /themes/hugo-book/assets/_defaults.scss
$font-size-base: 16px !default;
$font-size-16: 1rem !default;
$body-min-width: 20rem !default;
$container-max-width: 80rem !default;
$menu-width: 16rem !default;
$toc-width: 16rem !default;
1rem
对应16px
,因此有效画布的最大尺寸为80*16px=1280px
,其中菜单menu
和标签toc
的组件宽度固定为16rem
,对于横向分辨率大于1280px
的浏览器,两边自动留空。在网站的正文
区域,最大横向显示尺寸为(80-16-16)*16px=768px
,由此我们初步确定正文
插图的最大的横向尺寸为768px
,其它容器中的图像尺寸也可以在css样式中查到,或者直接通过浏览器开发者模式
(ctrl+shift+c
)并用样式拾取器查看。

对于移动端,我们在浏览器开发者模式
中模拟移动设备(Device Toolbar
),可以看到一些预设的设备尺寸,比如Samsung Galxy S20 Ultra
的尺寸为412*915px
。然而,如果我们将图像降采样为412px
像素,会感到模糊不清,412px
也远低于真实的屏幕分辨率。这和浏览器渲染网页使用的是“CSS 像素(viewport)”而非物理像素有关,例如Galaxy S20 Ultra的CSS 像素为412px*915px
,物理像素为1080px*2400px
(FHD+模式),比值Device Pixel Ratio (DPR)为2.62。
设备DPR #
在像素密度较高的设备(如 Retina 显示器)上,DPR 大于 1(例如 2、3 甚至更高)。 这意味着单个 CSS 像素将由多个物理像素表示,从而产生更清晰的图像和文本。从本质上讲,DPR允许 Web 开发人员使内容适应不同的屏幕分辨率,确保图像和文本在各种设备上显示清晰且大小合适。例如在hugo-book主题中,通过布局断点$mobile-breakpoint
来进行移动端适配,$mobile-breakpoint
的默认值为56rem
即896px
。
$mobile-breakpoint: $menu-width + $body-min-width * 1.2 + $toc-width !default;
@media screen and (max-width: $mobile-breakpoint) {
.book-menu {
visibility: hidden;
margin-inline-start: -$menu-width;
z-index: 1;
}
...
但是,我们仍应准备等于或略高于CSS像素 * DPR
的图像,这样才能在高清屏(高 DPR)下显示清晰。在移动端,我们也可以简单地直接取物理像素为参考值(即不考虑边距)。
目前,移动端的高分屏已达1440px
,我们就取这个作为移动端的尺寸上限。在桌面端,由于高分屏的普及,DPR也随移动端一样水涨船高,比如4k屏和iMac Retina屏幕,这类屏幕的DPR一般可达 2。因此,综合现有硬件和hugo-book模板的设置,我们应该准备的插图横向尺寸不应小于1440px
和768*2=1536px
。我们将博客中不同种类的图的最大尺寸计算出来,以最终的综合尺寸
为目标像素值。
图像类型 | CSS像素/px | 移动端最大尺寸/px | DPR(desktop) | 综合尺寸/px |
---|---|---|---|---|
banner | 768 | 1440 | 2.0 | 1536 |
插图 | 614 | 1440 | 2.0 | 1440 |
双栏插图 | 332 | 720 | 2.0 | 720 |
卡片插图 | 120 | 360 | 2.0 | 360 |
正确地截图 #
相机/手机拍摄的图像分辨远远超过参考像素值,我们对其进行降采样处理。但如果网站中有大量的屏幕截图,其原始尺寸可能就比参考像素低了,当然我们可以对其进行超采样,但这件事放在前期做效果会更好。
在浏览器中,直接通过ctrl+鼠标滚轮
对窗口放大(chrome最大可以至500%),再对目标进行截图就可以得到分辨率更高的图了。桌面环境(win11),可以通过系统
- 屏幕
- 缩放和布局
增加缩放比例,让小元素放大显示。
以截本站侧边栏状态图为例,该组件的CSS像素宽度为224px
,2k/25寸屏的缩放率一般为150%,若直接屏幕截图得到的图片宽度为336px
,放在正文中(768px宽度)显然是不够的,我们直接将浏览器放大500%,就可以截到宽度近1700px
的图,对比如下。
重采样、格式、质量 #
python的Pillow
库可以快速实现图像缩放、压缩和保存。其重采样默认算法为BICUBIC
,我们对时间不敏感,因此直接显式指定质量最高的LANCZOS
插值4。
img = img.resize((w, h), resample=Image.Resampling.LANCZOS)
在保存时,可以进一步选择格式、压缩率和质量。
img.save(output_path, format="WEBP", method=6, quality=quality)
显式指定速度最慢但压缩率最高的method=6
,尽可能地降低文件大小。
显式指定格式format="WEBP"
。WebP支持有损压缩与无损压缩,且支持动图和Alpha透明通道。平均而言,WebP 的文件大小比同类 JPEG 图像减少了 30%。时至今日,Webp
格式已经得到绝大多数浏览器支持(仅IE不支持)5。博客网站完全可以统一使用Webp格式图像。
quality
极大地影响文件体积和质量,一般来说取值60-80
就能平衡好质量-体积
这对矛盾。大多文章也就介绍到这了,以一个较为普适但固定的参数来应对所有场景。但我在实践中发现,固定的quality
取值经常造成图像压缩过度或不足,而用肉眼去评估图像质量显然是低效的。

图像压缩与质量评价 #
评价指标 #
为了在保证质量的同时,根据每张图片的具体情况最大化地压缩图像体积,要引入评价标准,主观地来说,可以通过以下几点,但无法量化差异:
- 是否模糊、失真(如文字边缘锯齿、细节丢失);
- 色彩是否偏差、对比度是否异常;
- 压缩伪影(如马赛克、块效应、色带);
尝试使用NeRF常用的评价指标来协助量化差异。
指标 | 全称 | 原理 | 含义 | 优点 | 缺点 |
---|---|---|---|---|---|
PSNR | Peak Signal-to-Noise Ratio 峰值信噪比 | 基于 MSE(均方误差)计算信号强度与噪声强度的比值 | 误差越小,PSNR 越高,代表压缩图像越接近原图 | 简单快速,历史最悠久 | 无法反映结构、感知差异;对视觉不敏感 |
SSIM | Structural Similarity Index 结构相似性 | 模拟人眼对亮度、对比度、结构的感知 | 更符合人眼感知的结构对比 | 考虑结构相似性,视觉友好 | 对多尺度、大范围失真不敏感 |
MS-SSIM | Multi-Scale SSIM 多尺度结构相似性 | 多分辨率下计算 SSIM,增强全局感知 | 比 SSIM 更稳定地反映感知质量 | 比 SSIM 更接近人类主观评分 | 计算复杂度比 SSIM 稍高 |
LPIPS | Learned Perceptual Image Patch Similarity 感知图像差异 | 基于深度学习,使用特征空间距离计算感知相似度 | 越小越接近,越大越不相似 | 最贴近人眼感知,处理模糊、结构变形效果好 | 依赖模型,速度慢 |
实际操作,同时使用MS-SSIM和LPIPS来评价图像质量,分别设置阈值为0.98
和0.02
,通过搜索quality
的值来获取最佳设置。我测试了博客中的20张图片,前6张为手机拍摄的照片,后14张则包含了电子书插图、屏幕截图、线稿图、PPT合成图等。以quality=80
作为参考值,由结果可以看到,自适应的quality
选择范围很大,10-86都有;20张图的平均结果,自适应方法节省空间84.81%,固定质量(80)则为81.69%。虽然总的来看空间节省差距不大,但可以初步看出,“照片”所需的quality
要比截图、线稿要大一些,且分辨率越大的图,quality
也会偏大;而画面干净的图quality
可以很小,比如7.png
,9.png
,10.png
,19.jpg
。
文件名 | 原始 | 自适应 | Q=80 | 质量Q | MS-A | LPIPS-A | MS-Q80 | LPIPS-Q80 |
---|---|---|---|---|---|---|---|---|
1.jpg | 425.7KB | 150.5KB | 185.7KB | 71 | 0.9924 | 0.0191 | 0.9944 | 0.0104 |
2.jpg | 2175.4KB | 163.7KB | 235.5KB | 63 | 0.9898 | 0.0199 | 0.9926 | 0.0120 |
3.jpg | 559.1KB | 206.1KB | 159.9KB | 85 | 0.9913 | 0.0191 | 0.9885 | 0.0325 |
4.jpg | 537.9KB | 177.9KB | 146.7KB | 84 | 0.9907 | 0.0193 | 0.9889 | 0.0248 |
5.jpg | 660.3KB | 336.2KB | 291.1KB | 83 | 0.9912 | 0.0177 | 0.9884 | 0.0276 |
6.png | 1183.5KB | 67.5KB | 50.9KB | 86 | 0.9915 | 0.0197 | 0.9882 | 0.0309 |
7.png | 288.1KB | 61.5KB | 146.5KB | 12 | 0.9859 | 0.0186 | 0.9962 | 0.0041 |
8.jpg | 232.1KB | 37.9KB | 55.2KB | 56 | 0.9892 | 0.0200 | 0.9945 | 0.0074 |
9.png | 101.8KB | 12.0KB | 26.0KB | 13 | 0.9888 | 0.0185 | 0.9976 | 0.0030 |
10.png | 200.6KB | 16.7KB | 41.8KB | 12 | 0.9893 | 0.0189 | 0.9987 | 0.0020 |
11.png | 159.8KB | 14.8KB | 26.1KB | 33 | 0.9881 | 0.0197 | 0.9958 | 0.0053 |
12.png | 402.3KB | 163.4KB | 147.2KB | 83 | 0.9803 | 0.0072 | 0.9792 | 0.0097 |
13.png | 128.2KB | 8.9KB | 16.4KB | 25 | 0.9852 | 0.0179 | 0.9901 | 0.0072 |
14.png | 925.1KB | 83.1KB | 155.3KB | 31 | 0.9844 | 0.0197 | 0.9926 | 0.0086 |
15.png | 642.8KB | 61.8KB | 114.4KB | 40 | 0.9854 | 0.0198 | 0.9924 | 0.0085 |
16.png | 749.8KB | 37.4KB | 38.9KB | 79 | 0.9955 | 0.0188 | 0.9957 | 0.0166 |
17.png | 880.0KB | 25.1KB | 38.9KB | 52 | 0.9905 | 0.0193 | 0.9940 | 0.0076 |
18.png | 523.0KB | 35.1KB | 41.9KB | 76 | 0.9916 | 0.0182 | 0.9933 | 0.0145 |
19.jpg | 444.8KB | 53.8KB | 126.7KB | 10 | 0.9911 | 0.0157 | 0.9995 | 0.0009 |
20.jpg | 331.1KB | 41.4KB | 69.6KB | 16 | 0.9933 | 0.0189 | 0.9983 | 0.0037 |
总和 | 11551.3KB | 1754.8KB | 2114.6KB |
与在线工具的对比 #
有很多在线图像压缩工具或CDN提供了图像优化服务,来试试我们的压缩效果与之相比如何6。WebP Cloud Services和 TinyPNG的压缩率选择有较大的差别,WebP Cloud Services的质量较好,TinyPNG的压缩率较大。我们无法精细地调试这些在线工具的压缩参数,本地的灵活性则较高,通过参数的调试可以达到同样的效果。最终,根据我们设定的MS-SSIM和LPIPS阈值,使用quality=85
获得了42.5KB大小的最终文件,而原文件大小599.0KB。
quality=93 | WebP Cloud | quality=75 | TinyPNG | quality=85 | |
---|---|---|---|---|---|
SIZE /KB | 75.8 | 75.7 | 28.4 | 28.6 | 42.5 |
PSNR /dB | 42.82 | 41.83 | 37.89 | 35.27 | 40.10 |
SSIM | 0.9846 | 0.9782 | 0.9598 | 0.9595 | 0.9716 |
MS-SSIM | 0.9969 | 0.9954 | 0.9903 | 0.9890 | 0.9937 |
VIF | 0.8806 | 0.8513 | 0.7720 | 0.7661 | 0.8279 |
LPIPS | 0.0055 | 0.0074 | 0.0263 | 0.0260 | 0.0198 |
链接转换与文件上传 #
把图片的压缩问题解决后,剩下的就很简单了:
- 使用正则表达式匹配markdown文件中的链接(WiKI和Markdown风格);
- 分类,判断类型:静态/动态图片、视频、本地/外链/站内链接;
- 根据分类进行降采样、压缩,移除元数据;
- 生成唯一文件名,保存至本地,将markdown文件中的链接替换为标准图像引用格式;
- 将压缩参数、文件名映射保存至数据库。
以下是几种正则匹配的Markdown语法和转换后的样式:

[](image_filename | url)
![[image_filename | url]]
[[image_filename | url]]
>>>>>Transfer to>>>>>

banner: [[image_filename | url]]
>>>>>Transfer to>>>>>
banner: https://img.osnsyc.top/XXXXXX.webp
{{< dbcard
...
cover=[[Pasted image 2025.png]]
...
>}}
>>>>>Transfer to>>>>>
{{< dbcard
...
cover="https://img.osnsyc.top/XXXXXX.webp"
...
>}}

[](video_filename | url)
![[video_filename | url]]
[[video_filename | url]]
>>>>>Transfer to>>>>>
{{< video src="https://img.osnsyc.top/XXXXXX.mp4" >}}
链接替换完成后生成一个新的markdown文件,可以由hugo正确地编译。生成的附件也可以用脚本一键上传至Cloudfalre R2存储桶。最终,一个完整的Obsidian至Hugo的工作流只需要两行命令即可完成。相关代码可以移步GitHub - osnsyc/obsidian-to-hugo-toolbox查看。
# 格式化Markdown文件
python md_publisher.py -f test_post.md
# 上传至R2
python r2_uploader.py -f <path_to_file_or_folder>

我在去年建立博客之初Hugo博客快速搭建小记写道:
我写了9篇博文,平均篇幅4570字,附件不算少,R2空间用了160MB,算下来,免费空间够写562篇长文了。
现在看来,博客共有文章24篇,各类图像都有7,优化后的附件总大小为113.9MB,所以现在算下来,CloudFlare R2免费空间够写2107篇长文了。
数量 | 体积/MB | 平均体积 | |
---|---|---|---|
静态图片 | 258 | 23.1 | 0.09MB |
动态图片 | 29 | 87.9 | 3.03MB |
视频 | 1 | 3.9 | 3.9MB |
总和 | 288 | 113.9 | 0.40MB |
相关阅读 #
- How to optimize images for website performance: best image sizes, compression, tools & testing
- web.dev, Responsive Images
- Hugo博客快速搭建小记
-
Google Web Fundamentals – Choose the correct level of compression ↩︎
-
除非是摄影网站,且需要放大展示原图的。 ↩︎
-
大多情况下是成立的,但在实践中,在某些情况下Resampling.BOX可能更好,比如纯线条图,边缘呈阶跃特征,Lanczos容易产生振铃。 ↩︎
-
WebP image format | Can I use… Support tables for HTML5, CSS3, etc ↩︎
-
目前动图没有特别优化,依赖原始编码设置,从文件体积方面看,还是推荐优先使用视频展现动画。 ↩︎