【免费下载链接】flux-markdown
Markdown Preview Enhanced for macOS QuickLook
如果你在 macOS QuickLook 中打开 Markdown 文件时发现本地图片加载失败、只看到破碎图标,那这篇文章正是为你写的。FluxMarkdown 作为面向 macOS QuickLook 的 Markdown 预览工具,核心难题之一就是让相对路径引用的本地图片(如./images/logo.png)在沙箱 WebView 中可靠显示。它的答案是一条双通道图片管线:Swift 端把小图读出来转成Base64 数据 URI 直接注入渲染器,超预算的大图则走自定义的local-md://协议由 Swift 代理读取。下面带你完整走一遍这条管线的设计与原理。
为什么本地图片在 QuickLook 里总加载失败?
问题根源不在 Markdown 语法,而在 macOS 的安全机制:
| 障碍 | 说明 |
|---|---|
| WKWebView 沙箱 | QuickLook 扩展运行在沙箱中,网页内的<img src="./a.png">无法直接读磁盘文件 |
file://协议受限 | WebKit 对本地文件协议有严格的同源与权限限制 |
| 路径解析错位 | 预览页面加载的是 HTML 字符串,<base>指向不是你的文档目录 |
换句话说,浏览器引擎"看得见"图片标签,却"摸不着"图片文件。常见的绕法(比如把整个目录交给 WebView)又会破坏沙箱安全模型。FluxMarkdown 的做法是:图片字节流由 Swift 亲自读取,再"喂"给网页,WebView 全程不直接接触文件系统。
双通道图片管线:一张图看懂
Markdown 内容 │ ▼ ① Swift 解析所有本地图片引用(Markdown 语法 + HTML <img>) │ ├─ 命中预算 ──► ② 读取文件 → Base64 编码 → data: URI │ │ │ ▼ │ ③ 注入 options.imageData 传给 JS 渲染器 │ │ │ ▼ │ ④ JS 渲染时把 <img src> 替换为 data: URI ✅ │ └─ 超预算大图 ──► ⑤ 改写成 local-md:// URL │ ▼ ⑥ LocalSchemeHandler 拦截请求 校验 allowlist → 读取字节 → 返回响应 ✅两条通道共用同一套"预算"和"白名单",只是投递方式不同:小图随渲染参数一次性注入,大图按需惰性加载。
通道一:Swift 扫描文档,Base64 注入
第一步:把文档里所有本地图片"找出来"
MarkdownImageDataCollector用三套正则扫全文,覆盖三种写法:
- 行内语法:
alt(含<path>尖括号与标题) - 引用式语法:
![alt][ref]+[ref]: path - 裸 HTML:
<img src="...">
关键过滤逻辑在 shouldInline:只处理相对路径且不含http://、https://、data:、file://、绝对路径前缀的引用——这些统统交给其他通道处理。
路径解析时还会做一件"防逃逸"的事:resolveImageURL 逐段处理..和.,随后在 localImageReferences 中拒绝任何"解析前后路径不一致"的引用——即符号链接不会进入白名单,防止通过 symlink 跳出文档目录。
第二步:预算内读取并 Base64 编码
默认预算定义在 Limits.default:
| 限制项 | 默认值 | 含义 |
|---|---|---|
maxImageCount | 32 张 | 单文档最多内联图片数 |
maxImageBytes | 2 MB | 单张图大小上限 |
maxTotalBytes | 8 MB | 全部内联图总大小上限 |
预算内的图片被 BoundedFileReader 有界读取(即使stat显示小、实际文件更大也能兜住),然后一行核心代码完成注入准备:
"data:\(mimeType);base64,\(data.base64EncodedString())"—— 见 collectImageData 第 35 行
同时会写入带./前缀与不带前缀的两份 key,保证 JS 端用images/logo.png或./images/logo.png都能命中。
第三步:交给 JS 渲染器替换
渲染调用发生在 PreviewViewController.renderPendingMarkdown:Swift 把imageData映射塞进 options,随window.renderMarkdown(content, options)传入 WebView。
JS 端的替换入口是 resolveImageSource:
- 先查
imageData映射(兼容./前缀归一化),命中则直接用 Base64 数据 URI——图片不产生任何网络/文件请求; - 未命中且是相对路径,则改写成
local-md://地址进入回退通道,并附带?v=renderVersion缓存破坏参数。
对应行为在 prepareRenderedImages 中统一处理:DOM 挂载后再遍历所有<img>改写 src,并监听error事件打上image-load-failed样式类,加载失败时展示占位 UI 而不是裸破碎图标。
还有一个容易被忽略的细节:markdown-it 默认会拒绝部分data:链接(典型如data:image/svg+xml;base64,...,因为 MIME 里含+)。渲染器在 buildMd 第 387-393 行 重写了validateLink,对data:与local-md://直接放行,这是 Base64 图能存活到 DOM 的前提。
通道二:local-md:// 自定义 Scheme 回退
超预算的大图、以及内联被跳过的引用,会走第二条通道。Swift 端为 WebView 注册了一个WKURLSchemeHandler(见 MarkdownWebView 第 68 行 与 PreviewViewController 第 367 行):
webConfiguration.setURLSchemeHandler(handler, forURLScheme: "local-md")每次渲染前,Swift 会把文档基目录和显式白名单同步给 handler(MarkdownWebView 第 402-420 行):
handler.baseDirectory = baseDir handler.allowedFileURLs = MarkdownImageDataCollector.referencedLocalImageURLs(from: url, content: content)请求进来后的校验链条在 LocalSchemeHandler 中依次执行:
- 扩展名白名单:仅
png / jpg / jpeg / gif / svg / webp可加载,其他一律拒绝(resolveContainedFileURL); - 显式授权:请求路径必须精确匹配"从当前文档解析出来的图片引用",目录整体不可访问;
- Security-Scoped 访问:在独立队列上
startAccessingSecurityScopedResource()后读取,读取同样受 2MB 上限约束; - 预算复核:已加载图片数量与总字节数再次对账,超限返回
fileReadTooLarge错误; - 正确 MIME 响应:按扩展名构造
Content-Type与no-cache头,保证 GIF 动图等按正确类型解码。
这套设计的效果是:WebView 能加载大图,但只能加载"这篇文档明确写到的那几张图",白名单之外的一切路径——包括父目录、兄弟目录、symlink——都拿不到字节。
导出 HTML:把管线成果固化进文件
Base64 注入不只是预览的便利,还是离线导出的基础。exportHTML 在拿到渲染结果后,会用正则找出所有file://与local-md://的src,经 handler 重新校验后读取文件、Base64 编码并原地替换为data:URI。最终导出的单文件 HTML 里图片全部自包含,拷到任何设备、断网打开都不缺图。JS 侧的 exportHTML 也提前把所有相对src规范化为绝对地址,方便 Swift 精确匹配替换。
实战体验:3 分钟验证图片管线
体验步骤(macOS 环境):
- 安装应用,系统设置 → 隐私与安全性 → 扩展 → QuickLook 中启用 FluxMarkdown;
- 新建一个文件夹,放入图片与一份 Markdown,用相对路径引用;
- 在 Finder 中选中文件按空格预览,图片应当立即可见。
仓库自带一份覆盖各场景的测试文档,可直接用来对照:Tests/fixtures/images-test.md 中依次验证了同目录、上级目录、子目录、file://协议、主目录绝对路径、越权路径(预期失败)与网络图片。若预览异常,官方推荐的排查方式是实时过滤日志(见 docs/features/IMAGES.md 第 98-99 行):
log stream --predicate 'subsystem == "com.markdownquicklook.app"' --level debug日志中🔵 Start loading resource表示回退通道收到请求,🔴 Rejected resource outside base directory则说明请求被白名单拦截——后者往往意味着图片路径引用有误或指向了沙箱之外。
常见问题速查
| 现象 | 原因 | 解决 |
|---|---|---|
相对路径图正常,/Users/...绝对路径失败 | 沙箱 entitlements 仅放开用户主目录等范围 | 把图片移入主目录内,或改用相对路径 |
| 文档里图很多,最后一两张不显示 | 触及 32 张 / 8MB 内联预算,且回退也被限流 | 减小图片体积或拆分文档 |
| 单张图超过 2MB 不显示 | 单图上限 | 压缩图片(建议 <1MB) |
symlink指向的图不显示 | 防逃逸策略,symlink 不进白名单 | 直接引用真实文件路径 |
data:image/svg+xml;base64被 markdown-it 拒绝 | 链接校验器不认含+的 MIME | 已内置 validateLink 重写 修复,无需处理 |
总结
FluxMarkdown 处理本地图片的思路可以浓缩为一句话:让 Swift 当"图片搬运工",让 WebView 只当"显示器"。
- ✅ 小图:Swift 读文件 → Base64 编码 → 随渲染参数注入 → JS 直接改写
<img src>,零请求、零沙箱冲突; - ✅ 大图:
local-md://自定义协议 + 逐文件白名单 + Security-Scoped 读取,按需加载且不扩大权限面; - ✅ 预算与 MIME 双重限流,防止恶意或失当文档耗尽资源;
- ✅ 导出链路复用同一套 Base64 能力,产出真正离线可用的 HTML。
想深入更多细节,推荐阅读 docs/features/IMAGES.md 与历史排障记录 docs/history/images/BASE64_IMAGE_FIX.md,它们完整梳理了支持矩阵、失败行为与安全授权模型。
【免费下载链接】flux-markdown
Markdown Preview Enhanced for macOS QuickLook
相关推荐
Jspreadsheet 图片上传与嵌入:base64 本地图片与远程图片的渲染实战
Jspreadsheet 图片上传与嵌入:base64 本地图片与远程图片的渲染实战 在 Jspreadsheet 数据表格中嵌入图片是构建富媒体数据应用的常见
前端UI组件深度解析HsMod:基于BepInEx的炉石传说插件开发完全指南
深度解析HsMod:基于BepInEx的炉石传说插件开发完全指南 HsMod是一个基于BepInEx框架的《炉石传说》修改插件项目,为游戏开发者提供了超过55项
游戏开发Zettlr图片管理:本地与云端图片插入最佳实践
Zettlr图片管理:本地与云端图片插入最佳实践 在Markdown写作中,图片管理往往是影响创作流畅度的关键瓶颈。你是否还在为截图后繁琐的保存 上传 引用流程
桌面应用前端知识管理科研
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考