diff --git a/backend/main.py b/backend/main.py index d810b38..fffb41d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2339,9 +2339,10 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative # === PDF: special handling before read_text (binary file) === if ext == ".pdf": try: - from backend.pdf_reader import extract_pdf_metadata, extract_pdf_text + from backend.pdf_reader import extract_pdf_metadata, extract_pdf_text, extract_pdf_toc pdf_text = extract_pdf_text(file_path, max_chars=100000) pdf_meta = extract_pdf_metadata(file_path) + pdf_toc = extract_pdf_toc(file_path) size = file_path.stat().st_size return { "vault": vault_name, @@ -2356,6 +2357,7 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative "is_pdf": True, "unsupported": False, "pdf_metadata": pdf_meta, + "pdf_toc": pdf_toc, "size_bytes": size, } except Exception as e: diff --git a/backend/pdf_reader.py b/backend/pdf_reader.py index bd431ee..c018ec2 100644 --- a/backend/pdf_reader.py +++ b/backend/pdf_reader.py @@ -57,6 +57,44 @@ def extract_pdf_metadata(file_path: Path) -> dict: return info +def extract_pdf_toc(file_path: Path) -> list[dict]: + """Extract table of contents (bookmarks/outline) from a PDF file. + + Returns a list of {title, page, level} dicts, or empty list on failure. + """ + toc: list[dict] = [] + try: + if PDF_READER == "pymupdf": + doc = fitz.open(str(file_path)) + raw = doc.get_toc(simple=False) + for item in raw: + toc.append({ + "title": str(item[1]), + "page": item[2], + "level": item[0], + }) + doc.close() + elif PdfReader is not None: + reader = PdfReader(str(file_path)) + outline = reader.outline + if outline: + def _flatten(items, level=1): + for item in items: + if isinstance(item, list): + _flatten(item, level + 1) + elif hasattr(item, 'title') and hasattr(item, 'page'): + page_num = reader.get_page_number(item.page) + 1 if hasattr(item, 'page') and item.page else 1 + toc.append({ + "title": str(item.title), + "page": page_num, + "level": level, + }) + _flatten(outline) + except Exception as e: + logger.warning("Failed to extract PDF TOC from %s: %s", file_path, e) + return toc + + def _extract_pymupdf(file_path: Path, max_chars: int) -> str: doc = fitz.open(str(file_path)) parts = [] diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index d80a6ef..4f58b3a 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -27,7 +27,7 @@ } ], "security": { - "csp": "default-src 'self' http://127.0.0.1:*; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://127.0.0.1:* https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh; style-src 'self' 'unsafe-inline' http://127.0.0.1:* https://cdnjs.cloudflare.com https://fonts.googleapis.com; font-src 'self' http://127.0.0.1:* https://fonts.gstatic.com; img-src 'self' data: blob: http://127.0.0.1:* https:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*;" + "csp": "default-src 'self' http://127.0.0.1:*; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://127.0.0.1:* https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh; style-src 'self' 'unsafe-inline' http://127.0.0.1:* https://cdnjs.cloudflare.com https://fonts.googleapis.com; font-src 'self' http://127.0.0.1:* https://fonts.gstatic.com; img-src 'self' data: blob: http://127.0.0.1:* https:; frame-src 'self' http://127.0.0.1:*; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*;" } }, "bundle": { diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index 735f04f..cf8eaf8 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -402,10 +402,22 @@ async function renderBacklinksPanel(vault, path, container) { export function renderFile(data) { const area = document.getElementById("content-area"); - // Handle PDF files — render in iframe + // Handle PDF files — render in iframe with TOC sidebar if (data.is_pdf) { const pdfUrl = `/api/file/${encodeURIComponent(data.vault)}/pdf/stream?path=${encodeURIComponent(data.path)}`; const pages = data.pdf_metadata && data.pdf_metadata.pages ? data.pdf_metadata.pages : '?'; + const toc = data.pdf_toc || []; + + let tocHtml = ''; + if (toc.length > 0) { + tocHtml = '