Obsidian 自动同步(2)- 图片迁移

前言

在上一篇文章中,我们实现了基本的 Obsidian 笔记自动迁移到 Hexo 的功能。然而,在迁移过程中,对于本地图片的处理尚未实现,这导致文章中嵌入的图片无法在 Hexo 上展示。因此,本篇我们将解决图片迁移的问题,确保图片可以在博客中正确的现实。

实现思路

1. 分析文章中的图片链接并迁移

为了准确迁移 Obsidian 笔记中的图片,我们首先需要提取文章中的图片链接。可以通过正则表达式从 Markdown 文件中获取图片路径,然后按照 hexo 的结构存放这些图片。

2. 优化图片并添加水印

单纯复制图片虽然能够实现展示,但由于图片尺寸过大,可能会影响页面加载速度。此外,很多博客站点都会对图片添加水印,不仅能标明图片的归属,还可以提升博客的识别度。因此,我们将引入 sharp 库,对图片进行压缩并添加水印,以优化展示效果。

3. 修改文章中的图片路径

图片迁移后,需要同步更新文章中的图片路径以确保展示正确。Hexo 要求图片放在 source 目录下并通过绝对路径引用,所以我们要在处理过程中修改 Markdown 文件中图片路径。

源码实现

1. 分析并复制图片到博客仓库

首先,我们使用正则表达式提取 Markdown 文件中的图片链接,通过匹配链接获取图片的文件路径。如果图片是本地图片(非网络链接且文件类型符合要求),则读取图片所在目录,并按照预设的目标目录结构进行存储。

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
// 解析并复制图片到博客仓库
async resolveAndCopyImage(source, content) {
// 使用正则匹配图片链接
const matches = content.match(REGEX.IMAGE_URL);
if (!matches) return;

for (const item of matches) {
// 获取图片实际路径
const url = decodeURIComponent(item.replace(REGEX.IMAGE_URL, '$1'));
if (/^https?:\/\/.*/.test(url) || !IMAGE_TYPES.includes(extname(url).replace('.', ''))) continue;

// 获取图片源目录和目标存储目录
const sourceDir = source.replace(basename(source), '');
const sourceUrl = resolve(sourceDir, url);
const targetUrl = resolve(this.targetDir, '../img/posts' + sourceDir.replace(this.sourceDir, '') + url);

try {
// 检查图片是否已处理
if (this.checkFileHash(sourceUrl)) return;
// 确保目标目录存在
this.ensureDirectoryExists(targetUrl.replace(basename(targetUrl), ''));
copyFileSync(sourceUrl, targetUrl); // 复制图片
} catch (error) {
logger.error('图片迁移失败:' + error.toString());
}
}
}

注意:
我这里只匹配了常规 markdown 语法:![图片描述](图片路径), 没有对 wiki 链接作解析:![[图片路径]], 这是因为常规 markdown 语法普适度更广,而 wiki 链接语法不是所有 markdown 编辑器都会识别。

文件与链接 中,关闭 Wiki 链接,即可在链接图片与笔记时自动使用 markdown 语法。

2. 处理图片:压缩与水印

为避免影响加载速度和页面美观,我们使用 sharp 库对图片进行压缩,并在角落添加水印。

sharp 介绍

sharp 是一个高性能的 Node.js 图像处理库,基于 libvips 库构建。sharp 支持多种图像格式(如 JPEG、PNG、WebP、TIFF、GIF 等)的快速、高效的图像处理。它可以处理各种图像操作,如缩放、裁剪、旋转、调整透明度等。由于 libvips 的高效性,sharp 通常比使用 GraphicsMagick 或 ImageMagick 的图像处理库更快,且内存占用更低。

具体实现

首先,我们通过 sharp 来获取图片的元数据。由于 sharp 在添加水印时规定水印图片的宽高不能大于原图,所以在获取到原图大小后,需计算出一个相对合适的水印大小。

需注意的是,在对水印图片进行resize 操作时,传入的宽高必须为整数,若为小数则会报错,因此可使用~~快速取整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取图片元数据并计算水印大小
const metadata = await sharp(sourceUrl).metadata();
// 设置水印高度占原图的 10%
const height = ~~(metadata.height * 0.1);
// 设置水印宽度占原图的 20%
const width = ~~(metadata.width * 0.2);
const resizeOps = {};

// 动态调整水印尺寸
if (height > width || height < 40) {
resizeOps.height = height > 70 ? 70 : height;
} else {
resizeOps.width = width;
}

// 获取调整后水印的 buffer
const watermarkImage = await sharp(resolve(process.argv[1], '../logo.png'))
// 重置大小
.resize(resizeOps)
// 输出为 buffer
.toBuffer();

然后我们将水印处理好后,提供给原图合并水印图片并压缩转为 webp 后输出到指定目录下。

1
2
3
4
5
6
7
8
// 添加水印并压缩为 webp 格式
await sharp(sourceUrl)
// 将水印放置在右下角
.composite([{ input: watermarkImage, gravity: 'southeast', blend: 'over' }])
// 转换为 webp 格式,压缩质量值为 0-100, 默认 80
.webp({ quality: 80 })
// 输出图片到指定路径
.toFile(targetUrl.replace(extname(targetUrl), '.webp'));

3. 修改文章中的图片路径

图片迁移至 Hexo 后,还需修改 Markdown 文件中图片的引用路径,以确保前端展示的正确性。通过正则匹配文章中的图片链接,针对 Hexo 的要求调整图片存储路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
processFileMetadata(parsedContent, filePath) {
const fileDir = filePath.replace(this.sourceDir, '').replace(basename(filePath), '');
const matches = parsedContent.body.match(REGEX.IMAGE_URL);

if (matches) {
for (const item of matches) {
const url = decodeURIComponent(item.replace(REGEX.IMAGE_URL, '$1'));
if (/^https?:\/\/.*/.test(url) || !IMAGE_TYPES.includes(extname(url).replace('.', ''))) continue;

// 更新 Markdown 文件中的图片路径为绝对路径
const targetUrl = '/img/posts' + resolve(fileDir, url.replace(extname(url), '.webp'));
parsedContent.body = parsedContent.body.replaceAll(item, `![](${encodeURI(targetUrl)})`);
}
}

return `---\n${yamlMetadata}\n---\n\n${parsedContent.body}`;
}

4. 优化图片处理流程

至此,图片迁移功能已完成了。然而,随着图片数量的不断增加,压缩和添加水印的速度会逐渐变慢。由于我们并不需要在每次同步时都完全重新处理所有图片,所以可以将处理过的文件写入缓存文件。这样,在每次处理文件之前,先判断图片是否发生了变动,再决定是否进行处理。

关于如何判断图片是否发生变更,我想到了在撰写《子资源篡改攻击》时浏览器的解决方式。浏览器通过文件的哈希值来判断文件是否发生过变更,通常会使用sha256sha384等哈希算法。但这些算法虽然安全,效率却相对较低。我们并不需要如此高安全性的算法,直接使用md5即可。它既能保证生成唯一的哈希值,又能确保执行高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const cacheContent = readFileSync(resolve(this.rootPath, '.git-hooks/.cache/fileHash.json'), 'utf-8');
this.fileHashCache = cacheContent ? JSON.parse(cacheContent) : {};

// 检查文件 hash 值
function checkFileHash(filePath) {
const fileBuffer = readFileSync(filePath);
const hashSum = crypto.createHash('md5');
const fileHash = hashSum.update(fileBuffer).digest('hex');

if (!fileHashCache[filePath]) {
fileHashCache[filePath] = fileHash;
return false;
}
return fileHashCache[filePath] === fileHash;
}

结语

本篇我们实现图片迁移之后,博客基本就差不多可以正常浏览了,但对于一些 Hexo 不支持的语法还没有处理,下一篇我们将会来处理这些问题。


Obsidian 自动同步(2)- 图片迁移
https://blog.pangcy.cn/2024/10/27/前端编程相关/前端框架与库/hexo/Obsidian 自动同步(2)- 图片迁移/
作者
子洋
发布于
2024年10月27日
许可协议