加载中...
不想等待可以点我关掉

前言

前几天不经意间看了实现全站图片使用avif格式,替代臃肿的webp教程 | 张洪Heo (zhheo.com),才发现现在BiliBili全部是avif格式的图片了

我:

哇,这压缩率真的炸裂。

如今,各大浏览器基本都支持avif图像格式了,可以考虑一下跟上时代的步伐了

在开始之前,先看看目前的图片上传&用户访问路径

可见有三个问题

  1. lsky pro 不支持avif格式,使用avif无法与之前的框架完美契合
  2. 当用户的浏览器不支持webp格式时,无法查看图片(包劝退的
  3. 对于已上传的图片无法平滑过渡

因此,不如将转换格式的任务交由节点处理,保存图片源文件,随用随转

思路

我站图片存储的架构可参考基于Onedrive的高可用性图床,不过因为我后来装依赖装吐了遂用go重写了整个程序。因此,我要面临的问题是,我要怎么在go中完成图片格式的转换?

为了偷懒,我直接用Imagick了,反正它发行了apt软件包,安装也不费事

  • 为与外部Imagick交互,我使用了gographics/imagick库来处理
  • 为与之前的程序兼容,我打算在「返回本地文件」处做文章

完善程序

图片转换函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func TransformImage(inputPath string, outputFormat string, quality uint) (string, error) {
// logrus.Info(inputPath, outputFormat)
if outputFormat == "" {
return inputPath, nil
}

// 构造新的文件路径
// 文件命名:原始文件名_质量.格式
newPath := filepath.Join("transformed", strings.Replace(inputPath, filepath.Ext(inputPath), "_"+strconv.Itoa(int(quality))+"."+outputFormat, 1))
dir := filepath.Dir(newPath)
err := os.MkdirAll(dir, 0755)
checkErr(err)
// 如果文件已经存在,返回现有的路径
if _, err := os.Stat(newPath); err == nil {
return newPath, nil
}

// 初始化
imagick.Initialize()
defer imagick.Terminate()
mw := imagick.NewMagickWand()
defer mw.Destroy()

// 读取原始图像
err = mw.ReadImage(inputPath)
checkErr(err)

// 获取原始图像的色彩空间和色彩深度
//originalColorspace := mw.GetImageColorspace()
//originalDepth := mw.GetImageDepth()
//logrus.Info("originalColorspace:", originalColorspace, "originalDepth:", originalDepth)

// 设置新的格式
err = mw.SetImageFormat(outputFormat)
checkErr(err)

// 在转换格式后,恢复原始图像的色彩空间和色彩深度
// err = mw.SetImageColorspace(originalColorspace)
// checkErr(err)
// err = mw.SetImageDepth(originalDepth)
// checkErr(err)

// 设置图片质量
err = mw.SetImageCompressionQuality(quality)
checkErr(err)

// 写入新图像
err = mw.WriteImage(newPath)
checkErr(err)
// logrus.Info(newPath)

return newPath, nil
}
注意

需开启CGO

清除缓存函数
1
2
3
4
5
6
7
8
9
10
11
12
func delCache() {
tmp_size := getDirectorySize("tmp")
transformedtmp_size := getDirectorySize("transformed")
if tmp_size > 5368709120 {
clearOldFiles("tmp", 60)
logrus.Info("tmp folder cleaned")
}
if transformedtmp_size > 5368709120 {
clearOldFiles("transformed", 30)
logrus.Info("transformed folder cleaned")
}
}
主路由函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func nodeReturnFile(c *gin.Context) {
...
fmt := c.Query("fmt")
quality, _ := strconv.ParseUint(c.Query("q"), 10, 32)

if quality == 0 {
quality = 95
}
...
transformedpath, err := TransformImage(cachePath, fmt, uint(quality))
checkErr(err)
extstatus, extype := getContentType(filepath.Ext(transformedpath))
if extstatus {
c.Header("Content-Type", extype)
}
c.File(transformedpath)
return
}

效果对比

右下查看源文件

原图(jpg,3.32MB)

压缩比:100%(~ ̄▽ ̄)~
压缩比:100%(~ ̄▽ ̄)~

AVIF(185KB,avif时质量参数无效)

压缩比:5.52%
压缩比:5.52%

WEBP(925KB,q=95%)

压缩比:27.26%
压缩比:27.26%

WEBP(925KB,q=50%)

压缩比:7.28%
压缩比:7.28%

HEIF(1.43MB,q=95%)

压缩比:43.24%
压缩比:43.24%

HEIF(402KB,q=50%)

压缩比:11.86%
压缩比:11.86%

原图(WEBP,334KB,q=100%)

压缩比:100%(~ ̄▽ ̄)~
压缩比:100%(~ ̄▽ ̄)~

AVIF(120KB,avif时质量参数无效)

压缩比:36.90%
压缩比:36.90%

WEBP(130KB,q=50%)

压缩比:39.28%
压缩比:39.28%

前端处理

为了照顾不支持avif的浏览器,我打算直接在前端处理,替换为.webp或原始文件

不太漂亮,大佬轻喷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
document.addEventListener('DOMContentLoaded', function() {
// 从页面中提取第一个AVIF图片链接
// function getFirstPictureUrl(type) {
// const images = document.querySelectorAll('img');
// for (let img of images) {
// if (img.getAttribute("data-src") && img.getAttribute("data-src").includes('fmt=',type)) {
// return img.getAttribute("data-src");
// }
// }
// return null;
// }

// 检测浏览器是否支持AVIF格式
function supportCheck(type, url) {
return new Promise(resolve => {
// 先从localStorage中获取结果
const result = localStorage.getItem("support_" + type);
if (result !== null) {
// 如果结果存在,就直接返回
console.log(type, "support status loaded from localStorage:", result === "true");
resolve(result === "true");
} else {
// 如果结果不存在,就进行检测
const image = new Image();
image.src = url;
image.onload = () => {
console.log(type, "supported");
// 将结果保存到localStorage
localStorage.setItem("support_" + type, "true");
resolve(true);
};
image.onerror = () => {
console.log(type, "not supported");
// 将结果保存到localStorage
localStorage.setItem("support_" + type, "false");
// 显示提示消息
hud.toast(`当前浏览器不支持使用${type},已降级为使用其他格式`, 2500);
resolve(false);
};
}
});
}


// 替换图片URL中的avif为webp
function replacepicture(from, to) {
const images = document.querySelectorAll('img');
images.forEach(img => {
let attr = img.src.startsWith('data') ? 'data-src' : 'src';
if (img.getAttribute(attr) && img.getAttribute(attr).includes('fmt=' + from)) {
if (to == "") {
console.log("Replacing ", from, " with origin ext for image:", img.getAttribute(attr));
img.setAttribute(attr, img.getAttribute(attr).replace('fmt=' + from, ''));
} else {
console.log("Replacing ", from, " with ", to, " for image:", img.getAttribute(attr));
img.setAttribute(attr, img.getAttribute(attr).replace('fmt=' + from, 'fmt=' + to));
}
}
});
}


const firstAvifUrl = "/img/check/status.avif"; // 第一个AVIF图片链接
if (firstAvifUrl) {
// 使用第一个AVIF图片链接进行检测
supportCheck("AVIF",firstAvifUrl).then(supported => {
if (!supported) {
replacepicture("avif","webp");
const firstWebpUrl = "/img/check/status.webp"; // 第一个WEBP图片链接
supportCheck("WEBP",firstWebpUrl).then(supported => {
if (!supported) {
// hud.toast("当前浏览器不支持使用webp,已降级为使用原始图片", 2500);
// replacepicture("webp","");
replacepicture("webp","png");
}else{
console.log("Webp images will be used.");
}
});
} else {
console.log("AVIF images will be used.");
}
});
} else {
console.log("No AVIF images found on the page.");
}
});

基于@Heo修改,支持了懒加载及stellar的toast,facybox还有点问题

迁移工作

使用vscode进行正则替换:(https://onep\.hzchu\.top/.*\.webp)$1?fmt=avif

优缺

优点

  1. 契合先前架构,对以往图片可以直接迁移
  2. 提升了使用旧设备访客的体验
  3. 配合stellar支持直接查看原图
  4. by the way,因为缤纷云也使用`fmt=*`的格式,故该前端代码也可以作用于缤纷云上的图片大意了,它自己支持自动降级导致程序出错

缺点

  1. 增加存储成本。主要是节点端(OD可以忽略不计),按目前的来流程一张图片可能会在本地产生多个副本,使存储占用翻几番

  2. 需要外部安装ImageMagick组件,程序不再轻量

  3. 在转换为avif过程中,部分图片可观测到部分色彩丢失。不过也可以使用webp替代

  4. 缓存命名不具备可复用性。但目前不计划添加任何参数,加上数据量小倒也没影响

实际测试

Safari15.5(thx appetize.io)
Safari15.5(thx appetize.io)
Chrome69
Chrome69

在旧版本中,程序均能正常运行,但在chrome的旧版本中因stellar的原因无法加载