commit ef411fbe232ba3cc2d8626404e06c17d39e517b3 Author: Florian Egger Date: Tue May 26 14:27:01 2026 +0200 initial commit diff --git a/importFileToObsidian/README.md b/importFileToObsidian/README.md new file mode 100644 index 0000000..0613031 --- /dev/null +++ b/importFileToObsidian/README.md @@ -0,0 +1,165 @@ +# Obsidian Import Utility + +Import **Markdown** and **PDF** files into an [Obsidian](https://obsidian.md) vault with a single click. + +## Features + +- **.md files** + - Copies contents into a new Obsidian note. + - Uses the filename as the note title (prepended as `# Title`). + +- **.pdf files** + - Converts PDF text into a markdown note. + - Extracts embedded images and embeds them in the note. + - Creates a **hidden folder** per file (e.g. `.myfile_assets/`) containing: + - The original PDF + - All extracted images + - Links back to the original PDF from the note. + +## Installation + +1. Install Python 3 (usually already present on Linux). +2. Install the required dependency: + + ```bash + pip install -r requirements.txt + ``` + + Or directly: + ```bash + pip install pymupdf + ``` + +3. Tell the script where your Obsidian vault lives. Choose **one** method: + + - **Environment variable** (recommended for Thunar): + ```bash + export OBSIDIAN_VAULT="$HOME/Obsidian/MyVault" + ``` + - **Command-line flag**: + ```bash + python main.py --vault ~/Obsidian/MyVault /path/to/file.pdf + ``` + - **Auto-detect**: if you have exactly one folder inside `~/Obsidian`, it will be picked automatically. + +## Usage + +### From the terminal + +```bash +# Markdown +python main.py /path/to/notes.md + +# PDF +python main.py /path/to/document.pdf + +# With explicit vault +python main.py --vault ~/Obsidian/Work /path/to/slides.pdf +``` + +### With the wrapper script + +```bash +./import-to-obsidian.sh /path/to/file.pdf +``` + +Edit `VAULT_PATH=""` inside `import-to-obsidian.sh` if you prefer a hard-coded default. + +--- + +## Thunar Custom Action + +To add a right-click menu entry in **Thunar** (Xfce file manager): + +### Option A – Using the Thunar GUI + +1. Open Thunar. +2. Go to **Edit → Configure custom actions…** +3. Click **+** (Add a new custom action). +4. Fill in the fields: + + | Field | Value | + |-------|-------| + | **Name** | Import to Obsidian | + | **Description** | Import selected file into Obsidian vault | + | **Command** | `/usr/bin/python3 /full/path/to/main.py --vault /home/USER/Obsidian/MyVault %F` | + | **Icon** | `folder-documents` (or any icon you like) | + +5. Switch to the **Appearance Conditions** tab. +6. Check **Text files** and **Other files** (or just `*.md;*.pdf` if you want to restrict it). +7. Click **OK**. + +> **Tip:** `%F` passes all selected files. If you want the action to appear only for single selections you can use `%f` instead. + +### Option B – Editing `uca.xml` directly + +Paste the following snippet into `~/.config/Thunar/uca.xml` inside the `` block (adjust paths): + +```xml + + folder-documents + Import to Obsidian + /usr/bin/python3 /home/USER/Code/projects/obsidianUtils/importFileToObsidian/main.py --vault /home/USER/Obsidian/MyVault %F + Import selected file(s) into Obsidian vault + *.md;*.pdf + + + +``` + +Restart Thunar or press `Ctrl+Shift+R` inside a Thunar window to reload custom actions. + +--- + +## Environment Variable in `.bashrc` + +If you want the vault path to be available everywhere (including Thunar when launched from your shell), add this to `~/.bashrc` or `~/.profile`: + +```bash +export OBSIDIAN_VAULT="$HOME/Obsidian/MyVault" +``` + +> Note: Thunar started from a graphical session (not from a terminal) may not see variables defined in `.bashrc`. In that case, hard-code the `--vault` flag in the custom action command. + +--- + +## File Structure After Import + +### Markdown import + +``` +MyVault/ +└── notes.md # imported note with '# notes' as title +``` + +### PDF import + +``` +MyVault/ +├── document.md +└── .document_assets/ + ├── document.pdf + ├── page_1_img_1.png + ├── page_1_img_2.jpg + └── page_2_img_1.png +``` + +The generated `document.md` contains: +- The extracted text per page. +- Embedded images under an "Extracted Images" section. +- A link back to the original PDF inside the hidden assets folder. + +--- + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| `Vault path does not exist` | Double-check `--vault` or `OBSIDIAN_VAULT`. | +| `PDF handling requires 'pymupdf'` | Run `pip install pymupdf`. | +| Thunar action does not appear | Make sure the file pattern matches (`*.md;*.pdf`) and restart Thunar (`Ctrl+Shift+R`). | +| Images are missing in the note | `pymupdf` extracts raw embedded images; vector graphics or scanned pages may not yield separate image files. | + +## License + +MIT (or choose your own). diff --git a/importFileToObsidian/__pycache__/main.cpython-314.pyc b/importFileToObsidian/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..7f87382 Binary files /dev/null and b/importFileToObsidian/__pycache__/main.cpython-314.pyc differ diff --git a/importFileToObsidian/handlers/__pycache__/md_handler.cpython-314.pyc b/importFileToObsidian/handlers/__pycache__/md_handler.cpython-314.pyc new file mode 100644 index 0000000..cb6afcb Binary files /dev/null and b/importFileToObsidian/handlers/__pycache__/md_handler.cpython-314.pyc differ diff --git a/importFileToObsidian/handlers/__pycache__/pdf_handler.cpython-314.pyc b/importFileToObsidian/handlers/__pycache__/pdf_handler.cpython-314.pyc new file mode 100644 index 0000000..b08c5ba Binary files /dev/null and b/importFileToObsidian/handlers/__pycache__/pdf_handler.cpython-314.pyc differ diff --git a/importFileToObsidian/handlers/md_handler.py b/importFileToObsidian/handlers/md_handler.py new file mode 100644 index 0000000..15f2b6b --- /dev/null +++ b/importFileToObsidian/handlers/md_handler.py @@ -0,0 +1,29 @@ +"""Handler for importing Markdown files into Obsidian.""" + +import shutil +from pathlib import Path + +from utils.config import ensure_unique_path, sanitize_filename + + +def handle_markdown(file_path: Path, vault_path: Path) -> Path: + """Import a Markdown file into the Obsidian vault. + + - Copies contents to a new note. + - Uses the filename (without extension) as the note title. + - Prepends a title heading if the file does not already start with it. + """ + title = sanitize_filename(file_path.stem) + target_name = f"{title}.md" + target_path = ensure_unique_path(vault_path / target_name) + + content = file_path.read_text(encoding="utf-8") + + # Prepend title if the file doesn't already start with it as an H1 + first_line = content.splitlines()[0] if content.strip() else "" + if not first_line.strip().startswith(f"# {title}"): + content = f"# {title}\n\n{content}" + + target_path.write_text(content, encoding="utf-8") + print(f"[MD] Created note: {target_path}") + return target_path diff --git a/importFileToObsidian/handlers/pdf_handler.py b/importFileToObsidian/handlers/pdf_handler.py new file mode 100644 index 0000000..c9fe6f9 --- /dev/null +++ b/importFileToObsidian/handlers/pdf_handler.py @@ -0,0 +1,98 @@ +"""Handler for importing PDF files into Obsidian.""" + +import shutil +from pathlib import Path + +from utils.config import ensure_unique_path, sanitize_filename + + +def handle_pdf(file_path: Path, vault_path: Path) -> Path: + """Import a PDF file into the Obsidian vault. + + - Converts PDF content to a new Obsidian markdown note. + - Creates a hidden folder for extracted images and the original PDF. + - Embeds extracted images and links to the original PDF. + """ + try: + import fitz # pymupdf + except ImportError: + raise ImportError( + "PDF handling requires 'pymupdf'. Install it with:\n pip install pymupdf" + ) + + title = sanitize_filename(file_path.stem) + note_name = f"{title}.md" + note_path = ensure_unique_path(vault_path / note_name) + + # Hidden assets folder: .{title}_assets + assets_folder_name = f".{title}_assets" + assets_folder = vault_path / assets_folder_name + counter = 1 + while assets_folder.exists(): + assets_folder_name = f".{title}_assets_{counter}" + assets_folder = vault_path / assets_folder_name + counter += 1 + assets_folder.mkdir(parents=True, exist_ok=True) + + # Copy original PDF into assets folder + pdf_copy_name = f"{title}.pdf" + pdf_copy_path = ensure_unique_path(assets_folder / pdf_copy_name) + shutil.copy2(file_path, pdf_copy_path) + + # Open PDF and extract content + images + doc = fitz.open(file_path) + markdown_lines = [f"# {title}", ""] + + # Link to original PDF + rel_pdf_path = f"{assets_folder_name}/{pdf_copy_path.name}" + markdown_lines.append(f"**Original PDF:** [{pdf_copy_path.name}]({rel_pdf_path})") + markdown_lines.append("") + + # Extract images and text per page + image_entries = [] # list of (page_num, img_path, rel_path) + for page_num in range(len(doc)): + page = doc.load_page(page_num) + + # Extract images + img_list = page.get_images(full=True) + for img_index, img in enumerate(img_list): + xref = img[0] + try: + base_image = doc.extract_image(xref) + image_bytes = base_image["image"] + ext = base_image["ext"] + img_filename = f"page_{page_num + 1}_img_{img_index + 1}.{ext}" + img_path = assets_folder / img_filename + # Handle duplicates + img_path = ensure_unique_path(img_path) + img_path.write_bytes(image_bytes) + rel_img = f"{assets_folder_name}/{img_path.name}" + image_entries.append((page_num + 1, img_path, rel_img)) + except Exception as e: + print( + f"[PDF] Warning: could not extract image {img_index} on page {page_num + 1}: {e}" + ) + + # Extract text + text = page.get_text() + if text.strip(): + markdown_lines.append(f"## Page {page_num + 1}") + markdown_lines.append("") + markdown_lines.append(text.strip()) + markdown_lines.append("") + + doc.close() + + # Add image section if any images were extracted + if image_entries: + markdown_lines.append("## Extracted Images") + markdown_lines.append("") + for page_num, img_path, rel_img in image_entries: + markdown_lines.append(f"### Page {page_num}") + markdown_lines.append(f"![{img_path.name}]({rel_img})") + markdown_lines.append("") + + note_path.write_text("\n".join(markdown_lines), encoding="utf-8") + print(f"[PDF] Created note: {note_path}") + print(f"[PDF] Assets folder: {assets_folder}") + return note_path diff --git a/importFileToObsidian/import-to-obsidian.sh b/importFileToObsidian/import-to-obsidian.sh new file mode 100755 index 0000000..4f7f6ac --- /dev/null +++ b/importFileToObsidian/import-to-obsidian.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Wrapper script for importing files into Obsidian. +# Adjust VAULT_PATH below or leave empty to rely on ~/.bashrc / env / auto-detection. + +VAULT_PATH="" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +if [ -n "$VAULT_PATH" ]; then + python3 "$SCRIPT_DIR/main.py" --vault "$VAULT_PATH" "$@" +else + python3 "$SCRIPT_DIR/main.py" "$@" +fi diff --git a/importFileToObsidian/main.py b/importFileToObsidian/main.py new file mode 100755 index 0000000..036b520 --- /dev/null +++ b/importFileToObsidian/main.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Obsidian Import Utility + +Import files into an Obsidian vault: + - .md -> copy contents to a new note (filename as title) + - .pdf -> convert to markdown, extract images, link original PDF + +Usage: + python main.py /path/to/file.pdf + python main.py --vault ~/Obsidian/MyVault /path/to/file.md +""" + +import argparse +import sys +from pathlib import Path + +from handlers.md_handler import handle_markdown +from handlers.pdf_handler import handle_pdf +from utils.config import get_vault_path + + +def main(): + parser = argparse.ArgumentParser(description="Import files into an Obsidian vault.") + parser.add_argument("file", help="Path to the file to import.") + parser.add_argument( + "--vault", + dest="vault", + default=None, + help="Path to the Obsidian vault. Overrides OBSIDIAN_VAULT env var.", + ) + args = parser.parse_args() + + file_path = Path(args.file).expanduser().resolve() + if not file_path.exists(): + print(f"Error: file not found: {file_path}", file=sys.stderr) + sys.exit(1) + + try: + vault_path = get_vault_path(args.vault) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + suffix = file_path.suffix.lower() + + if suffix == ".md": + handle_markdown(file_path, vault_path) + elif suffix == ".pdf": + handle_pdf(file_path, vault_path) + else: + print( + f"Error: unsupported file type '{suffix}'. Supported: .md, .pdf", + file=sys.stderr, + ) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/importFileToObsidian/requirements.txt b/importFileToObsidian/requirements.txt new file mode 100644 index 0000000..eee9252 --- /dev/null +++ b/importFileToObsidian/requirements.txt @@ -0,0 +1 @@ +pymupdf diff --git a/importFileToObsidian/utils/__pycache__/config.cpython-314.pyc b/importFileToObsidian/utils/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..324ef82 Binary files /dev/null and b/importFileToObsidian/utils/__pycache__/config.cpython-314.pyc differ diff --git a/importFileToObsidian/utils/config.py b/importFileToObsidian/utils/config.py new file mode 100644 index 0000000..ad28941 --- /dev/null +++ b/importFileToObsidian/utils/config.py @@ -0,0 +1,63 @@ +"""Configuration and common utilities for Obsidian import.""" + +import os +from pathlib import Path +from typing import Optional + +DEFAULT_CONFIG_PATH = Path.home() / ".config" / "obsidian-import" / "config.ini" + + +def get_vault_path(cli_vault: Optional[str] = None) -> Path: + """Resolve the Obsidian vault path from CLI arg, env var, or default locations.""" + if cli_vault: + vault = Path(cli_vault).expanduser().resolve() + if not vault.exists(): + raise FileNotFoundError(f"Vault path does not exist: {vault}") + return vault + + env_vault = os.environ.get("OBSIDIAN_VAULT") + if env_vault: + vault = Path(env_vault).expanduser().resolve() + if vault.exists(): + return vault + + # Try common default locations + obsidian_dir = Path.home() / "Obsidian" + if obsidian_dir.exists(): + vaults = [d for d in obsidian_dir.iterdir() if d.is_dir()] + if len(vaults) == 1: + return vaults[0] + elif len(vaults) > 1: + raise RuntimeError( + f"Multiple vaults found in {obsidian_dir}. " + "Set OBSIDIAN_VAULT or use --vault." + ) + + raise RuntimeError( + "Could not determine Obsidian vault path. " + "Use --vault or set OBSIDIAN_VAULT environment variable." + ) + + +def sanitize_filename(name: str) -> str: + """Remove or replace characters unsafe for filenames.""" + invalid = '<>:"/\\|?*' + for ch in invalid: + name = name.replace(ch, '_') + return name.strip('. ') + + +def ensure_unique_path(target: Path) -> Path: + """If target exists, append a counter to make it unique.""" + if not target.exists(): + return target + stem = target.stem + suffix = target.suffix + parent = target.parent + counter = 1 + while True: + new_name = f"{stem}_{counter}{suffix}" + candidate = parent / new_name + if not candidate.exists(): + return candidate + counter += 1