Skip to content

Select and Save SVGs #97

@farhanadam

Description

@farhanadam

@artisticat1 Thank you for your plugin. I am using both Tikz and the plugin for the first time in Obsidian. I wanted to save these renders and could not find a good method. Asked ChatGPT 4o and got this after 2 hours of back and forth. I am attaching my note on the working solution below. I hope it can help you add this feature to your plugin.

Tikz Smart Export Obsidian Plugin

Objectives

Save rendered Tikz plots to the assets folder.

Methods

Used ChatGPT 4o and created a full plugin. This is my first plugin build. Looks like it needs only a manifest.json and main.js to become visible in the plugins folder. Below is the full code. This was completely generated by ChatGPT 4o; only prompts were used, no code modification was done by me.

Results

Version 1 (09-April-2025)

Works well. Needs three commands to achieve goal. In the command palette:

  1. Tikz Smart Export: Enter Tikz Selection Mode
  2. Tikz Smart Export: Export Tikz Selected Diagram
  3. Tikz Smart Export: Exit Tikz Selection Mode

These three steps achieve selection and export of svgs from the current note. Note that it does not scan the whole note but just the section visible. So scroll and select again if there is more to select.

svgs are saved to the assets folder, mine being 'Images/svgs'.
File name is generated with the title of the note, headings (if any) and an index.
If the file exists, it will just make another copy with an appended index.

Functional, no bugs reported yet.

Files of version 1 (09-April-2025)

The following files were put in Obsidian's plugins folder in a folder of choice
Relative path in: .obsidian\plugins\tikz-smart-export with tikz-smart-export as our choice.

readme.md

# TikZ Smart Export Plugin

This Obsidian plugin allows you to select rendered TikZ diagrams (via TikzJax) and export them as SVGs into your vault.

## Features
- Select diagrams visually using an overlay in preview mode
- Export only what you want
- Automatically detects diagrams in math blocks and across shadow DOM
- Saves to a vault-relative folder (default: `assets/tikz-exports`)
- Prevents overwriting: appends suffix if a filename already exists

## Commands
- `Enter TikZ Selection Mode` – show overlay on each diagram
- `Export Selected TikZ Diagrams` – save selected SVGs
- `Exit TikZ Selection Mode` – clear selections and overlays

## Settings
- **Export folder**: where SVGs are saved (relative to your vault)

## Installation
1. Download this repo as a ZIP
2. Extract to `.obsidian/plugins/tikz-smart-export/`
3. Enable in Obsidian’s Community Plugins settings

## Roadmap Ideas
- Add heading support for filenames
- Add "Export All in Note"
- PDF export batching
- Drag-to-desktop

manifest.json

{
  "id": "tikz-smart-export",
  "name": "TikZ Smart Export",
  "version": "1.0.0",
  "minAppVersion": "0.12.0",
  "description": "Select, export, and save TikzJax-rendered diagrams as SVGs in your vault, with filename conflict handling.",
  "author": "ChatGPT",
  "authorUrl": "",
  "main": "main.js"
}

main.js

const { Plugin, PluginSettingTab, Setting, Notice, normalizePath } = require("obsidian");

module.exports = class TikzSmartExportPlugin extends Plugin {
  async onload() {
    await this.loadSettings();
    this.selectedSVGs = new Set();

    this.addCommand({
      id: "enter-tikz-selection-mode",
      name: "Enter TikZ Selection Mode",
      callback: () => this.enterSelectionMode()
    });

    this.addCommand({
      id: "export-selected-tikz",
      name: "Export Selected TikZ Diagrams",
      callback: () => this.exportSelected()
    });

    this.addCommand({
      id: "exit-tikz-selection-mode",
      name: "Exit TikZ Selection Mode",
      callback: () => this.exitSelectionMode()
    });

    this.addSettingTab(new TikzExportSettingTab(this.app, this));
  }

  async loadSettings() {
    this.settings = Object.assign({
      exportFolder: "assets/tikz-exports"
    }, await this.loadData());
  }

  async saveSettings() {
    await this.saveData(this.settings);
  }

  getAllRelevantSVGs() {
    const svgs = new Set();

    document.querySelectorAll("svg").forEach(svg => {
      if (!svg.closest(".svg-icon") && !svg.closest(".nav-folder-title")) {
        svgs.add(svg);
      }
    });

    document.querySelectorAll("*").forEach(el => {
      if (el.shadowRoot) {
        el.shadowRoot.querySelectorAll("svg").forEach(svg => {
          if (!svg.closest(".svg-icon")) {
            svgs.add(svg);
          }
        });
      }
    });

    return Array.from(svgs).filter(svg => {
      const box = svg.getBoundingClientRect();
      const inMathBlock = svg.closest(".math") || svg.closest("mjx-container");
      return (box.width > 50 && box.height > 30) || inMathBlock;
    });
  }

  enterSelectionMode() {
    this.selectedSVGs.clear();
    const svgs = this.getAllRelevantSVGs();

    svgs.forEach(svg => {
      if (svg.parentElement.querySelector(".tikz-export-overlay")) return;

      const overlay = document.createElement("div");
      overlay.className = "tikz-export-overlay";
      overlay.innerText = "Click to Select";
      overlay.style.position = "absolute";
      overlay.style.top = "0";
      overlay.style.left = "0";
      overlay.style.width = "100%";
      overlay.style.height = "100%";
      overlay.style.background = "rgba(255,255,255,0.6)";
      overlay.style.color = "#333";
      overlay.style.display = "flex";
      overlay.style.alignItems = "center";
      overlay.style.justifyContent = "center";
      overlay.style.fontSize = "14px";
      overlay.style.cursor = "pointer";
      overlay.style.zIndex = "999";

      const wrapper = svg.parentElement;
      wrapper.style.position = "relative";
      wrapper.appendChild(overlay);

      overlay.addEventListener("click", (e) => {
        e.stopPropagation();
        if (this.selectedSVGs.has(svg)) {
          this.selectedSVGs.delete(svg);
          overlay.innerText = "Click to Select";
          overlay.style.background = "rgba(255,255,255,0.6)";
        } else {
          this.selectedSVGs.add(svg);
          overlay.innerText = "Selected ✔";
          overlay.style.background = "rgba(200,255,200,0.7)";
        }
      });
    });

    new Notice("Selection mode activated. Click on diagrams to select.");
  }

  exitSelectionMode() {
    this.selectedSVGs.clear();
    document.querySelectorAll(".tikz-export-overlay").forEach(el => el.remove());
    new Notice("Selection mode exited.");
  }

  async exportSelected() {
    if (this.selectedSVGs.size === 0) {
      new Notice("⚠️ No diagrams selected.");
      return;
    }

    const folderPath = normalizePath(this.settings.exportFolder);
    const folder = this.app.vault.getAbstractFileByPath(folderPath);
    if (!folder) {
      await this.app.vault.createFolder(folderPath).catch(() => {
        new Notice("❌ Could not create export folder.");
      });
    }

    const noteTitle = this.app.workspace.getActiveFile()?.basename || "untitled";
    let index = 1;

    for (const svg of this.selectedSVGs) {
      const baseName = `${noteTitle}_tikz_${index++}`;
      let filename = baseName + ".svg";
      let fullPath = normalizePath(`${this.settings.exportFolder}/${filename}`);
      let suffix = 1;

      while (this.app.vault.getAbstractFileByPath(fullPath)) {
        filename = `${baseName}_${suffix++}.svg`;
        fullPath = normalizePath(`${this.settings.exportFolder}/${filename}`);
      }

      const svgText = svg.outerHTML;
      const encoder = new TextEncoder();
      const uint8array = encoder.encode(svgText);

      try {
        await this.app.vault.createBinary(fullPath, uint8array);
        new Notice(`✅ Saved: ${filename}`);
      } catch (err) {
        new Notice(`❌ Failed to save ${filename}: ${err.message}`);
      }
    }

    this.exitSelectionMode();
    new Notice("🎉 Export complete.");
  }
};

class TikzExportSettingTab extends PluginSettingTab {
  constructor(app, plugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display() {
    const { containerEl } = this;
    containerEl.empty();
    containerEl.createEl("h2", { text: "TikZ Smart Export Settings" });

    new Setting(containerEl)
      .setName("Export folder")
      .setDesc("Vault-relative path where exported SVGs will be saved.")
      .addText(text => text
        .setPlaceholder("assets/tikz-exports")
        .setValue(this.plugin.settings.exportFolder)
        .onChange(async (value) => {
          this.plugin.settings.exportFolder = value.trim();
          await this.plugin.saveSettings();
        }));
  }
}

Only 183 lines of code to get this done.

A small data.json was made on changing the folder location.

data.json

{
  "exportFolder": "Images/svgs"
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions