我是如何使用 Git 做图床,并使用 hook 实现图片的 WebP 压缩与水印的?

本文最后更新于 2024年11月18日 中午

通常,我们的网站存储空间、网络受限,会将图片和网站分开进行存储和“解耦”,方便后续使用 CDN 加速时,图片和网站可以分开使用不同方案;也方便网站迁移时,只迁移网站本体。

不知道有没有小伙伴想过: 在自己的腾讯云轻量应用服务器上,部署 Git 服务端作为图床仓库,重新定向工作空间到网站目录,并使用 Git hook 实现图片的 WebP 压缩与水印? 这次就给大家浅浅分享一下。

Git

Git 相信大家都不陌生,它是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。Git 由 Linux 之父 Linus 于 2005 年创建,目前由 Git 软件基金会管理。

Git 的核心思想是分布式,推送内容使用 Hash 进行差异化校验,使得 Git 具有更高的可靠性和更快的速度。Git 还支持离线工作,这意味着你可以在没有网络连接的情况下进行开发,然后将其推送到远程仓库。也正是这个特性,使得 Git 成为了一个非常流行的版本控制系统。而我们,也可以使用这些特性,来创建一个图床。

舒服

想一想,本地存储图片,之后使用 Git 推送到远程仓库,是不是很方便呢?

如果是写文章,对于一些草稿图片,甚至还可以使用 Git 拉出分支,在文章完成后,合并主分支,统一上线。保持工作台干净,岂不美哉?

graph LR;
    A[开始] --> B[本地存储图片/拉取 Git 仓库]
    B --> C{是否为草稿图片}
    C -->|是| D[创建 Git 分支]
    C -->|否| E[推送到远程仓库]
    D --> G[合并主分支]
    G --> I[发布图片]
    E --> I

Bare 裸仓库

平时,我们使用 Git 时,一般会使用工作区,也就是我们平时写代码的地方。

工作区和版本控制区

进入.git 目录,就可以看到版本控制区:
Git 仓库

但是,我们在服务器上部署的 Git 仓库,不需要工作空间,只需要版本库,用于提供给客户端进行拉取和推送。所以,在服务器上,我们就可以使用裸仓库,作为图床仓库:

1
2
# 创建一个裸仓库
git init --bare

裸仓库

hook 钩子

Git 提供了钩子机制,允许我们在 Git 仓库中添加自定义脚本,以在特定事件发生时执行。这些事件包括:提交、推送、合并、拉取等。钩子脚本可以在仓库的 .git/hooks 目录中找到。

我们可以使用这些钩子脚本,来执行一些自定义操作,比如: 重新定向工作空间,以及后续的图片压缩、水印等操作。

裸仓库同样提供了 hook 钩子。比如,我们可以在 hooks/post-receive 中,编写脚本,实现重新定向工作空间:

1
2
3
#!/bin/zsh
# 重新定向工作空间,pathToWebSite 为网站目录,pathToBarePath 为裸仓库目录
git --work-tree=/pathToWebSite --git-dir=/pathToBarePath checkout -f

这样,本地推送图片到服务器上的 Git 裸仓库的流程就是:

sequenceDiagram
    participant Local as 本地计算机
    participant Server as 腾讯云轻量应用服务器
    participant GitRepo as Git 裸仓库
    participant WebSite as 网站目录(Nginx)

    Local->>Server: 推送图片到 Git 裸仓库
    Server->>GitRepo: 接收推送
    GitRepo->>Server: 触发 post-receive 钩子
    Server->>WebSite: 更新网站目录

既然 hook 是 shell 脚本,那么我们就可以使用 shell 脚本来实现图片的 WebP 压缩与水印了。

其实方法很多,比如: imagemagick,它是一个功能强大的图片处理工具,支持多种图片格式的转换,以及图片的裁剪、缩放、旋转、水印、滤镜等操作。

嘿嘿,看个人习惯啦

但是我更习惯用 python 的 Pillow 库,同样,它是一个强大的图像处理库,支持多种图片格式的转换。

支持创作

制作教程不易,如果热心的小伙伴,想支持创作,可以加入我们的电圈(还可以解锁远程协助、好友位😃):

WebChart Recognise

志同道合的小伙伴也是知音难觅。

  • 开发者爱好群: 👉 如果你对云服务器、CDN、云数据库和Linux等云计算感兴趣,亦或者喜欢编程、设计、产品、运营等领域,欢迎加入我们的开发者爱好群,一起交流学习(目前可能就我一个人?🤔,毕竟才刚刚创建~)。
QQ_Group Recognise

当然,也欢迎在B站或YouTube上关注我们:

更多:

WebP 格式

WebP 既支持有损压缩也支持无损压缩,相较于 PNG 格式,同样支持透明通道。在质量相同的情况下,WebP 也具有更小的文件体积。

WebP

所以,使用 WebP 格式,可以节省很间,进而提高网站内图片的加载速度; WebP 格式也支持渐进式加载,还可以进一步提高用户体验。

操作前提

本文的操作前提,是什么呢?首先是要有一台 Linux 服务器,比如:我就是使用腾讯云的轻量应用服务器。腾讯云的轻量应用服务器(Lighthouse),性能足够强劲。

其实也可以买 腾讯云的云服务器 (CVM) 的,选择 CVM 可以选配更强大的 CPU ,处理 WebP 压缩速度更快。

但是,我测试了一下,我使用的腾讯云轻量应用服务器,CPU 模拟型号是 Intel(R) Xeon(R) Gold 6133 CPU @ 2.50GHz,处理本文的 WebP 并行任务非常足够。

本次使用的演示服务器

轻量应用服务器长期都有活动,可以进去探索一下:

软件方面,在腾讯云轻量应用服务器上,安装好 Git 和 Python:

1
2
3
4
# 安装 Git
apt update && apt install git -y
# 安装 Python
apt install python3 python3-pip -y

与此同时,创建一个 Git 裸仓库,作为图床仓库:

1
2
# 创建一个裸仓库
git init imageHost.git --bare

创建图床仓库

图片转 WebP

如何将图片转成 WebP 格式呢? 前文已经说到,使用 Pillow 库

Pillow 库支持多种图片格式的转换,包括:JPEGPNGGIFBMPTIFFPPMWebP 等。

按道理我们在腾讯云轻量应用服务器使用的是 Linux 镜像,是可以安装 Linux 的 imagemagick 库的。感兴趣的小伙伴可以尝试一下。

当然,其实你也可以使用存储桶的 WebP 转换功能,比如:腾讯云的 COS 存储桶。

腾讯云 COS WebP 转换

其实我也有用过(大概 2021 年的时候?),改天有机会,给大家介绍一下。

Pillow 库

我们可以很方便地实现图片的 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
from PIL import Image

def save_as_webp(image_target, target_path):
"""
将图像保存为 WebP 格式,并进行优化设置.

Args:
image_target (PIL.Image.Image): 待保存的图像对象.
target_path (str): 保存图像的目标路径.
"""
# 保存图像文件为 WebP 格式
# quality: 图像质量,范围0-100,越高质量越好,但文件越大
# optimize: 启用无损优化,减小文件大小
# lossless: 启用无损压缩,保证图像质量
# method: 压缩方法,取值0-6,越大压缩比越大,但质量略有下降
# save_all: 保存动画的所有帧
# progressive: 启用渐进式显示
image_target.save(target_path, 'webp', quality=80, optimize=True, lossless=False, method=6,
save_all=True,
progressive=True)

if __name__ == "__main__":
# 假设图像对象为 image_obj
image_obj = Image.open("path/to/image.jpg")

# 定义保存路径
target_path = "path/to/save/image.webp"

# 保存为 WebP 格式
save_as_webp(image_obj, target_path)

添加水印

Pillow 库同样支持添加水印,只需要把水印图片“贴”到目标图片上即可:

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
from PIL import Image, ImageDraw, ImageFont

def add_watermark(input_path, watermark_path, output_path):
"""
在图像上添加水印.

Args:
input_path (str): 原始图像的路径.
watermark_path (str): 水印图像的路径.
output_path (str): 添加水印后的图像保存路径.
"""
# 打开原始图像
image = Image.open(input_path)

# 打开水印图像
watermark = Image.open(watermark_path)

# 获取原始图像和水印图像的尺寸
image_width, image_height = image.size
watermark_width, watermark_height = watermark.size

# 计算水印的位置,置于右下角
x = image_width - watermark_width - 20
y = image_height - watermark_height - 20

# 将水印图像叠加到原始图像上
image.paste(watermark, (x, y), watermark)

# 保存结果图像
image.save(output_path)

# 示例用法
add_watermark("path/to/image.jpg", "path/to/watermark.png", "path/to/output.jpg")

一般,水印的位置在图片的下方,并且水印需要设置透明度,在图片的尺寸过小时候,取消水印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
with Image.open(input_path) as image:
original_size = os.path.getsize(input_path)
# 计算水印的放置位置以底部居中
bg_width, bg_height = image.size
wm_width, wm_height = wm_img.size
position = ((bg_width - wm_width) // 2, bg_height - wm_height - 30) # 底部居中坐标

# 创建一个新的透明图层用于合并,以防背景颜色受影响
image_target = Image.new('RGBA', image.size, (255, 255, 255, 0)) # 完全透明图层
image_target.paste(image, (0, 0)) # 将背景图片粘贴到透明图层上

# 粘贴水印到新图层的底部中心位置,透明度已由水印图片自身定义,无需额外调整
if bg_width > 512 * 1.5 and watermark_mode:
image_target.paste(wm_img, position, mask=wm_img.split()[3]) # 使用alpha通道作为遮罩确保透明度正确

最终的流程就是:

graph LR;
    A[开始] --> B[读取原始图像];
    B --> C{图像尺寸 >= 512 * 1.5?};
    C -->|是| D[添加水印];
    C -->|否| E[跳过添加水印];
    D --> F[保存为WebP格式];
    E --> F;
    F --> G[结束];

文章内的图片,就是这样转换的。

Git 差异化文件

Git 作为版本控制工具,可以很方便地实现差异化文件检录,可能平时大家用习惯了 JetBrains、GitHub Desktop 等工具,但是,Git 的命令行强大到可怕。

我们如果要实现两个 commit 的差异化文件,只需要使用 git diff 命令即可:

1
2
# 比较两个 commit 的差异
git diff <commit-id> <commit-id>

比如:比较 Hash 值为6abbb89127c806928666b12374dfa013ef95f8b8和 Hash 值为5fe2590034f4922c427ab74bf948af46c2627d99的两个 commit 的差异:

git diff

是不是有点抽象? 这里我们可以使用类似的命令 git difftree 并追加一下参数:

  • --no-commit-id:输出的差异信息中不包含提交 ID;
  • --name-status:仅显示文件名和状态(如添加、修改、删除等),而不显示具体的差异内容;
  • -r:递归地比较两个树中的所有子树。

这样的结果就豁然开朗了:
git difftree

这个时候,如果两次 commit 存在差异,那么可能的输出结果就是:

1
2
3
4
5
6
7
8
# 修改文件
M modified_file.png
# 添加文件
A new_file.png
# 删除文件
D deleted_file.jpg
# 重命名文件、移动文件
R old_name.png new_name.pg

发现了什么?🤔 没错,我们在 shell 内,可以使用标准化输出的方式,格式化参数文件。

很丝滑

hook 实现

现在,我们看看如何使用 hook 实现自动化部署。首先是工作分区的重定向和参数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义图片文件后缀(需要转换为 WebP 格式的文件后缀)
image_extensions=(".png" ".jpg" ".jpeg" ".PNG")

# 网站目标目录
WEB_DIR="/www/webRoot/imagehost.mintimate.cn"
# 工作空间临时检录目录
WORK_SPACE_DIR="/home/git/mySpace/imagehost.mintimate.cn"
# 定义需要跳过的文件前缀
skip_prefixes=("emoticon" "emoji")
# Python Fle Path
PYTHON_MAIN="/home/git/PythonTool"
# 检录工作空间到目标目录
git --work-tree=$WORK_SPACE_DIR --git-dir=/home/git/mySource/imageHost.git checkout -f

因为,我们提交内容的时候,难免会提交一些无关紧要的文件,比如:表情包、emoji 等,所以,我们需要过滤掉这些文件,并定义需要转换的文件后缀以及需要跳过的文件前缀,配合函数完成逻辑判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 定义一个函数,用于检查文件路径是否以需要跳过的前缀开头
check_skip_prefix() {
local filepath=$1
for prefix in "${skip_prefixes[@]}"; do
if [[ "$filepath" == "$prefix"* ]]; then
return 0 # 返回 0 表示匹配到了需要跳过的前缀
fi
done
return 1 # 返回 1 表示没有匹配到需要跳过的前缀
}

# 定义一个函数,用于检查文件是否为图片
is_image_file() {
local filepath=$1
for ext in "${image_extensions[@]}"; do
if [[ "$filepath" == *"$ext" ]]; then
return 0 # 返回 0 表示是图片文件
fi
done
return 1 # 返回 1 表示不是图片文件
}

定义日志输出目录:

1
2
3
4
5
6
7
8
9
# 定义输出目录
OUTPUT_DIR="/home/git/runLog"
# 确保输出目录存在
mkdir -p $OUTPUT_DIR

# 获取当前日期和时间,格式为 YYYYMMDD-HHMMSS
NOW=$(date +"%Y%m%d-%H%M%S")
# 定义输出文件,包含时间戳
OUTPUT_FILE="${OUTPUT_DIR}/${NOW}_Change.log"

之后,标准化我们上文使用 git difftree 命令的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 读取标准输入(oldrev newrev refname)
while read oldrev newrev refname
do
# 获取变更的文件列表
echo "Changes in $refname:" >> $OUTPUT_FILE
# 使用 git diff-tree 来查看变更
git diff-tree --no-commit-id --name-status -r $oldrev $newrev | while read status_flag file1 file2
do
case $status_flag in
M|A)
echo "Modify: $file1" >> $OUTPUT_FILE
;;
D)
echo "Delete: $file1" >> $OUTPUT_FILE
;;
R)
echo "MV $file1 To $file2" >> $OUTPUT_FILE
;;
esac
done
done

最后,对文件进行二次读取,判断是否需要使用 Python 脚本进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用 case 语句处理不同的操作
while read line; do
case $line in
Modify*)
filepath=$(echo $line | awk '{print $2}')
process_file "$filepath" modify
;;
Delete*)
filepath=$(echo $line | awk '{print $2}')
rm -f "$WEB_DIR/$filepath"
rm -f "$WEB_DIR/${filepath%.*}.webp"
;;
MV*)
src=$(echo $line | awk '{print $2}')
dst=$(echo $line | awk '{print $3}')
rm -f "$WEB_DIR/$src"
rm -f "$WEB_DIR/${src%.*}.webp"
process_file "$dst" move
;;
esac
done < $OUTPUT_FILE

当然,process_file 函数的实现就比较简单了,直接调用 Python 脚本即可:

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
# 使用 Python WebP解析脚本
process_file() {
local filepath=$1
local action=$2

# 判断是否存在上级目标目录
mkdir -p "$(dirname "$WEB_DIR/$filepath")"

# 检查文件路径是否以需要跳过的前缀开头
check_skip_prefix "$filepath"
if [ $? -eq 0 ]; then
cp "$WORK_SPACE_DIR/$filepath" "$WEB_DIR/$filepath"
return
fi

# 检查文件是否为图片
is_image_file "$filepath"
if [ $? -eq 0 ]; then
# 执行 python 脚本
nohup $PYTHON_MAIN/bin/python $PYTHON_MAIN/image2WebpForGit.py -w -s "$WORK_SPACE_DIR/$filepath" -t "$WEB_DIR/${filepath%.*}.webp" >> $OUTPUT_FILE_PY 2>&1 &
else
# 如果不是图片,执行 cp 命令
cp "$WORK_SPACE_DIR/$filepath" "$WEB_DIR/$filepath"
fi
}

需要注意,这里我预留了 action 参数,但是我没有使用。

最终效果

最后,我们来看看效果,经过 commit 和 push 操作推送到我们自己的 Git 仓库之后,进而存储到腾讯云轻量应用服务器 Linux 的硬盘存储内,可以查看日志:

查看 Git Hook 操作日志

与此同时,我们也可以在 web 目录下查看转换后的图片:
查看 Git Hook 转换后的图片

看看转换后的图片和原始图片的大小对比:
查看 Git Hook 转换后的图片大小

同时,本篇文章内的图片,也是使用上述方法转换的,

对于性能的消耗,也是微乎其微的,我们可以在腾讯云轻量应用服务器的后台,看到性能监控曲线:
腾讯云轻量应用服务器性能监控曲线

END

好啦,本篇文章就到这里,感谢阅读。之后的步骤,就看每个人的想法了。比如我就是使用 Nginx 作为反向代理,将转换后的图片直接返回给用户,这样就可以减少服务器的负担了。

有时候也会套一层 CDN,这样就可以加速图片的访问了。

相关代码已经开源,可以访问 https://github.com/Mintimate/GitHookPng2WebP 查看。

最后,制作教程不易,寻找教程也不易,找到志同道合的小伙伴更是知音难觅。如果你对云服务器、CDN、云数据库和Linux等云计算感兴趣,亦或者喜欢编程、设计、产品、运营等领域,欢迎加入我们的开发者爱好群,一起交流学习: 812198734 (目前可能就我一个人?毕竟才刚刚创建 ~)。



我是如何使用 Git 做图床,并使用 hook 实现图片的 WebP 压缩与水印的?
https://www.mintimate.cn/2024/11/18/GitHookWebp/
作者
Mintimate
发布于
2024年11月18日
许可协议