前言 在上一篇文章中,我们实现了基本的 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 ();const height = ~~(metadata.height * 0.1 ); const width = ~~(metadata.width * 0.2 );const resizeOps = {};if (height > width || height < 40 ) { resizeOps.height = height > 70 ? 70 : height; } else { resizeOps.width = width; }const watermarkImage = await sharp (resolve (process.argv [1 ], '../logo.png' )) .resize (resizeOps) .toBuffer ();
然后我们将水印处理好后,提供给原图合并水印图片并压缩转为 webp 后输出到指定目录下。
1 2 3 4 5 6 7 8 await sharp (sourceUrl) .composite ([{ input : watermarkImage, gravity : 'southeast' , blend : 'over' }]) .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 ; const targetUrl = '/img/posts' + resolve (fileDir, url.replace (extname (url), '.webp' )); parsedContent.body = parsedContent.body .replaceAll (item, `} )` ); } } return `---\n${yamlMetadata} \n---\n\n${parsedContent.body} ` ; }
4. 优化图片处理流程 至此,图片迁移功能已完成了。然而,随着图片数量的不断增加,压缩和添加水印的速度会逐渐变慢。由于我们并不需要在每次同步时都完全重新处理所有图片,所以可以将处理过的文件写入缓存文件。这样,在每次处理文件之前,先判断图片是否发生了变动,再决定是否进行处理。
关于如何判断图片是否发生变更,我想到了在撰写《子资源篡改攻击》时浏览器的解决方式。浏览器通过文件的哈希值来判断文件是否发生过变更,通常会使用sha256
、sha384
等哈希算法。但这些算法虽然安全,效率却相对较低。我们并不需要如此高安全性的算法,直接使用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) : {};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 不支持的语法还没有处理,下一篇我们将会来处理这些问题。