Go中使用ImageMagick转换图片格式,实现全站图片“Avif”
前言
前几天不经意间看了实现全站图片使用avif格式,替代臃肿的webp教程 | 张洪Heo (zhheo.com),才发现现在BiliBili全部是avif格式的图片了
我:
哇,这压缩率真的炸裂。
如今,各大浏览器基本都支持avif图像格式了,可以考虑一下跟上时代的步伐了
在开始之前,先看看目前的图片上传&用户访问路径
可见有三个问题:
lsky pro
不支持avif格式,使用avif无法与之前的框架完美契合- 当用户的浏览器不支持webp格式时,无法查看图片
(包劝退的 - 对于已上传的图片无法平滑过渡
因此,不如将转换格式的任务交由节点处理,保存图片源文件,随用随转
思路
我站图片存储的架构可参考基于Onedrive的高可用性图床,不过因为我后来装依赖装吐了遂用go重写了整个程序。因此,我要面临的问题是,我要怎么在go中完成图片格式的转换?
为了偷懒,我直接用Imagick了,反正它发行了apt软件包,安装也不费事
完善程序
图片转换函数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) { 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)
err = mw.SetImageFormat(outputFormat) checkErr(err)
err = mw.SetImageCompressionQuality(quality) checkErr(err)
err = mw.WriteImage(newPath) checkErr(err)
return newPath, nil }
|
清除缓存函数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 }
|
效果对比
右下查看源文件
前端处理
为了照顾不支持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() { function supportCheck(type, url) { return new Promise(resolve => { 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.setItem("support_" + type, "true"); resolve(true); }; image.onerror = () => { console.log(type, "not supported"); localStorage.setItem("support_" + type, "false"); hud.toast(`当前浏览器不支持使用${type},已降级为使用其他格式`, 2500); resolve(false); }; } }); } 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"; if (firstAvifUrl) { supportCheck("AVIF",firstAvifUrl).then(supported => { if (!supported) { replacepicture("avif","webp"); const firstWebpUrl = "/img/check/status.webp"; supportCheck("WEBP",firstWebpUrl).then(supported => { if (!supported) { 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
优缺
优点
- 契合先前架构,对以往图片可以直接迁移
- 提升了使用旧设备访客的体验
- 配合stellar支持直接查看原图
by the way,因为缤纷云也使用`fmt=*`的格式,故该前端代码也可以作用于缤纷云上的图片大意了,它自己支持自动降级导致程序出错
缺点
增加存储成本。主要是节点端(OD可以忽略不计),按目前的来流程一张图片可能会在本地产生多个副本,使存储占用翻几番
需要外部安装ImageMagick组件,程序不再轻量
在转换为avif过程中,部分图片可观测到部分色彩丢失。不过也可以使用webp替代
缓存命名不具备可复用性。但目前不计划添加任何参数,加上数据量小倒也没影响
实际测试
Safari15.5(thx appetize.io)
在旧版本中,程序均能正常运行,但在chrome的旧版本中因stellar的原因无法加载