MineImages — Obsidian 统一预览插件开发纪实

19 阅读4分钟

MineImages — Obsidian 统一预览插件开发纪实

一、开发背景

1.1 痛点

Obsidian 作为一款优秀的本地知识库工具,在图片和 Mermaid 图表预览方面存在明显的交互短板:

  • 图片预览:原生仅支持点击后在新标签页打开,无法在上下文中缩放、旋转或对比查看多张图片
  • Mermaid 图表:渲染后尺寸不可控,大图溢出容器,小图留白过多;编辑模式下无法直接预览
  • 功能碎片化:社区中虽有一些独立插件,但各自解决单一问题,交互风格不统一且存在功能重叠

1.2 设计目标

开发一款统一、流畅、功能完整的预览插件,满足以下设计原则:

flowchart LR
    A["统一"] --> D["图片 + Mermaid 同一浮层"]
    B["流畅"] --> E["≤500ms 打开"]
    B --> F["60fps 缩放平移"]
    C["完整"] --> G["缩放/旋转/复制/切换"]
    C --> H["标注/全屏/缩略图导航"]
    D --> I("一站式预览体验")
    E --> I
    G --> I

1.3 技术选型

秉持 "最大化利用 Obsidian 官方 API,避免重复造轮子" 的原则:

模块选用方案原因
预览容器ModalObsidian 官方推荐,自带遮罩和关闭逻辑
图表捕获(阅读模式)MarkdownPostProcessor原生渲染管道,无需额外渲染
图表捕获(实时预览)DOM 事件委托兼容 .cm-embed-block 中的动态渲染
图片捕获registerDomEvent轻量级,无侵入
变换(缩放/旋转/平移)CSS transformGPU 加速,60fps
右键菜单Menu原生 Obsidian 风格
设置界面PluginSettingTab标准配置模式
主题适配CSS 变量自动跟随深浅主题
国际化i18n.ts + as const 推导中英双语 55 个 key,设置页实时切换

二、功能介绍

2.1 核心预览能力

点击笔记中的任意图片或 Mermaid 图表,立即在统一的模态浮层中打开:

sequenceDiagram
    actor User
    participant Page as "笔记页面"
    participant Modal as "mineImages 浮层"
    participant Toolbar as "工具栏"
    
    User->>Page: "点击图片/Mermaid"
    Page->>Modal: "收集当前文件所有预览项"
    Modal->>Modal: "渲染内容(img / SVG)"
    Modal->>Toolbar: "显示工具栏"
    Toolbar-->>User: "缩放 / 旋转 / 复制 / 切换"
    
    User->>Toolbar: "点击缩放"
    Toolbar->>Modal: "CSS transform 缩放"
    Modal-->>User: "实时响应"
    
    User->>Toolbar: "点击标注"
    Toolbar->>Modal: "启用 Canvas 标注层"
    Modal-->>User: "可绘制画笔/箭头/矩形"

perview-modal.png

2.2 交互功能矩阵

功能触发方式说明
缩放Ctrl + 滚轮 / 工具栏 ± 按钮步进可配置,范围 10%–1000%
拖动平移鼠标拖拽缩放后平移查看细节
旋转工具栏按钮 / R 键90° 步进
重置双击内容 / 工具栏按钮 / 0 键恢复初始缩放和旋转
复制工具栏按钮 / 右键菜单图片/Mermaid 均复制为 PNG
切换← → 箭头键 / 工具栏 / 右键菜单遍历当前文件所有预览项
关闭ESC / 点击遮罩 / 工具栏关闭按钮
全屏工具栏按钮浏览器 Fullscreen API
标注工具栏笔/箭头/矩形/清除Canvas 叠加层,不修改原图
缩略图导航左侧面板固定大小的缩略图列表,点击跳转

2.3 页面内交互

对于 Mermaid 图表,在笔记页面中提供额外的交互能力:

flowchart TD
    subgraph "页面交互"
        A["鼠标悬停 Mermaid"] --> B["光标变为 Pointer"]
        B --> C["右下角显示红色拖拽手柄"]
        C --> D["拖拽调整图表大小"]
    end
    
    subgraph "预览交互"
        A --> E["点击打开预览浮层"]
        E --> F["全功能工具栏"]
    end
    
    D --> G["保持等比缩放"]
    D --> H["退出 auto-fit 模式"]

三、技术实现

3.1 整体架构

flowchart TB
    subgraph "Capture Layer"
        IC["image-capture.ts"] --> |"DOM 事件"| DOM[("笔记 DOM")]
        MC["mermaid-capture.ts"] --> |"MarkdownPostProcessor"| DOM
        MCL["Live Preview"] --> |"事件委托"| DOM
    end
    
    subgraph "Bridge"
        PU["preview-utils.ts"] --> |"收集预览项"| DOM
        PU --> |"打开 Modal"| PM["PreviewModal"]
    end
    
    subgraph "Preview Layer"
        PM --> TM["TransformManager<br/>缩放/旋转/平移状态"]
        PM --> NC["NavigationController<br/>项目索引管理"]
        PM --> AC["AnnotationCanvas<br/>Canvas 标注叠加层"]
        PM --> TC["TouchController<br/>触摸手势"]
        PM --> CC["clipboard.ts<br/>复制为 PNG"]
    end
    
    subgraph "UI"
        PM --> TB["工具栏<br/>按钮分组"]
        PM --> TH["缩略图栏<br/>固定大小导航"]
        PM --> CT["内容区<br/>img / SVG 渲染"]
    end

3.2 模态框生命周期

sequenceDiagram
    participant User
    participant PU as "preview-utils"
    participant PM as "PreviewModal"
    participant DOM as "DOM"
    
    User->>DOM: "点击图片/Mermaid"
    DOM->>PU: "openUnifiedModal()"
    PU->>PU: "collectAllInSourceOrder()"
    PU->>PM: "new PreviewModal(items, index)"
    
    Note over PM: "onOpen()"
    PM->>PM: "构建全屏覆盖层"
    PM->>PM: "构建内容区 + 工具栏 + 缩略图栏"
    PM->>PM: "renderCurrent() 渲染当前项"
    PM->>PM: "绑定键盘/鼠标事件"
    PM-->>User: "浮层就绪"
    
    Note over User,PM: "用户交互..."
    
    User->>PM: "ESC / 点击遮罩 / 关闭按钮"
    Note over PM: "onClose()"
    PM->>PM: "保存偏好(if enabled)"
    PM->>PM: "清理事件监听"
    PM->>PM: "销毁 Canvas"
    PM->>PM: "清空内容"

3.3 关键实现细节

3.3.1 变换管理(TransformManager)
// 核心状态
class TransformManager {
  scale: number;      // 当前缩放 (0.1 ~ 10.0)
  translateX: number; // 水平偏移
  translateY: number; // 垂直偏移
  rotation: number;   // 旋转角度 (0 / 90 / 180 / 270)
  
  // 应用到 DOM 元素
  applyTo(el: HTMLElement) {
    el.style.transform = `
      translate(${this.translateX}px, ${this.translateY}px)
      scale(${this.scale})
      rotate(${this.rotation}deg)
    `;
  }
}
3.3.2 Mermaid 自适应与拖拽缩放

在页面中通过 MutationObserver 捕获所有 .mermaid 元素,自动添加 width: 100% 适配容器宽度。由于 Mermaid 是异步渲染,bindMermaidElement 内置了 5 次 × 500ms 的重试等待 SVG 元素就绪。右下角的拖拽手柄允许用户手动调整大小:

flowchart LR
    subgraph "自动适配"
        A["Mermaid 渲染"] --> B["MutationObserver 检测"]
        B --> C["添加 mine-images-auto-fit"]
        C --> D["width: 100% !important"]
    end
    
    subgraph "手动调整"
        E["鼠标悬停"] --> F["显示拖拽手柄"]
        F --> G["拖拽"]
        G --> H["移除 auto-fit"]
        H --> I["固定宽高, 等比缩放"]
    end
3.3.3 缩略图导航

左侧缩略图栏为固定 100×80px 的导航面板,自动收集当前文件所有图片和 Mermaid 图表,点击即可跳转:

// 缩略图构建流程
private buildThumbnailBar(): void {
  this.navigation.items.forEach((item, index) => {
    const thumb = createDiv("mine-images-thumbnail");
    if (item.type === "image") {
      this.loadThumbnailImage(item.source, thumb);
    } else {
      this.renderMermaidThumbnail(item, thumb);
    }
    thumb.addEventListener("click", () => this.navigateTo(index));
  });
}

setting.png

3.3.4 标注系统

基于 HTML5 Canvas 实现轻量级标注,支持画笔、箭头、矩形三种模式:

sequenceDiagram
    participant User
    participant TB as "工具栏"
    participant AC as "AnnotationCanvas"
    
    User->>TB: "点击「画笔」/「箭头」/「矩形」"
    TB->>AC: "setMode(mode)"
    AC->>AC: "Canvas pointerEvents = auto"
    AC->>AC: "cursor = crosshair"
    
    User->>AC: "mousedown (起点)"
    AC->>AC: "记录 startX, startY"
    AC->>AC: "保存当前 canvas 快照"
    
    User->>AC: "mousemove (绘制中)"
    AC->>AC: "还原快照 → 绘制形状"
    AC-->>User: "实时预览"
    
    User->>AC: "mouseup (完成)"
    AC->>AC: "最终绘制提交"
    
    User->>TB: "点击「清除」"
    TB->>AC: "clear()"
    AC->>AC: "ctx.clearRect()"

demo.gif

3.4 图片复制流程

flowchart TD
    Start["用户点击复制"] --> Check{"图片类型?"}
    Check -->|"同源图片"| Fetch["fetch → Blob"]
    Fetch --> Clipboard["navigator.clipboard.write"]
    Clipboard --> Done["Toast 提示成功"]
    
    Check -->|"跨域图片"| Canvas["绘制到 Canvas"]
    Canvas --> Tainted{"tainted?"}
    Tainted -->|"否"| Clipboard
    Tainted -->|"是"| Fallback["复制 URL 到剪贴板"]
    Fallback --> Warn["Toast 提示CORS限制"]
    
    Check -->|"Mermaid SVG"| Serialize["序列化 SVG"]
    Serialize --> Image["new Image 加载"]
    Image --> Draw["绘制到 Canvas"]
    Draw --> PNG["转为 PNG Blob"]
    PNG --> Clipboard

四、未来规划

4.1 路线图

image.png

4.2 短期目标

  • 触摸手势优化:完善双指缩放和旋转体验
  • 标注工具增强:支持颜色选择、笔触粗细、撤销/重做
  • 性能调优:大缩略图列表按需渲染(虚拟滚动),Mermaid 渲染缓存

4.3 长期愿景

  • 移动端支持:适配 Obsidian Mobile,提供触屏友好的交互
  • 更多图表类型:支持 Excalidraw、PlantUML、Graphviz 等
  • 插件生态:开放 API,允许第三方开发者扩展预览类型

项目地址gitee.com/wmlce/mine-…

插件 IDmine-images

许可证:MIT