feat: affichage PDF avec TOC + frame-src CSP
CI / lint (push) Successful in 23s
CI / security (push) Successful in 14s
CI / test (push) Successful in 27s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 1m29s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Successful in 23s
CI / security (push) Successful in 14s
CI / test (push) Successful in 27s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 1m29s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
- CSP: ajout frame-src 'self' http://127.0.0.1:* (permet iframe PDF) - pdf_reader.py: extract_pdf_toc() — extrait les bookmarks/outline (pypdf + pymupdf) - main.py: inclut pdf_toc dans la réponse /api/file pour les PDF - viewer.js: sidebar TOC avec liens vers les pages (si bookmarks présents) - style.css: layout .pdf-body flex + .pdf-toc sidebar 240px
This commit is contained in:
+3
-1
@@ -2339,9 +2339,10 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
# === PDF: special handling before read_text (binary file) ===
|
# === PDF: special handling before read_text (binary file) ===
|
||||||
if ext == ".pdf":
|
if ext == ".pdf":
|
||||||
try:
|
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_text = extract_pdf_text(file_path, max_chars=100000)
|
||||||
pdf_meta = extract_pdf_metadata(file_path)
|
pdf_meta = extract_pdf_metadata(file_path)
|
||||||
|
pdf_toc = extract_pdf_toc(file_path)
|
||||||
size = file_path.stat().st_size
|
size = file_path.stat().st_size
|
||||||
return {
|
return {
|
||||||
"vault": vault_name,
|
"vault": vault_name,
|
||||||
@@ -2356,6 +2357,7 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
"is_pdf": True,
|
"is_pdf": True,
|
||||||
"unsupported": False,
|
"unsupported": False,
|
||||||
"pdf_metadata": pdf_meta,
|
"pdf_metadata": pdf_meta,
|
||||||
|
"pdf_toc": pdf_toc,
|
||||||
"size_bytes": size,
|
"size_bytes": size,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -57,6 +57,44 @@ def extract_pdf_metadata(file_path: Path) -> dict:
|
|||||||
return info
|
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:
|
def _extract_pymupdf(file_path: Path, max_chars: int) -> str:
|
||||||
doc = fitz.open(str(file_path))
|
doc = fitz.open(str(file_path))
|
||||||
parts = []
|
parts = []
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"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": {
|
"bundle": {
|
||||||
|
|||||||
+17
-2
@@ -402,10 +402,22 @@ async function renderBacklinksPanel(vault, path, container) {
|
|||||||
export function renderFile(data) {
|
export function renderFile(data) {
|
||||||
const area = document.getElementById("content-area");
|
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) {
|
if (data.is_pdf) {
|
||||||
const pdfUrl = `/api/file/${encodeURIComponent(data.vault)}/pdf/stream?path=${encodeURIComponent(data.path)}`;
|
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 pages = data.pdf_metadata && data.pdf_metadata.pages ? data.pdf_metadata.pages : '?';
|
||||||
|
const toc = data.pdf_toc || [];
|
||||||
|
|
||||||
|
let tocHtml = '';
|
||||||
|
if (toc.length > 0) {
|
||||||
|
tocHtml = '<div class="pdf-toc"><h3>Table des matières</h3><ul>';
|
||||||
|
for (const item of toc) {
|
||||||
|
const indent = (item.level - 1) * 16;
|
||||||
|
tocHtml += `<li style="padding-left:${indent}px"><a href="#" onclick="document.querySelector('.pdf-iframe').contentWindow.location.hash='page=${item.page}';return false">${escapeHtml(item.title)}</a> <span class="toc-page">p.${item.page}</span></li>`;
|
||||||
|
}
|
||||||
|
tocHtml += '</ul></div>';
|
||||||
|
}
|
||||||
|
|
||||||
area.innerHTML = `
|
area.innerHTML = `
|
||||||
<div class="pdf-viewer-container">
|
<div class="pdf-viewer-container">
|
||||||
<div class="pdf-toolbar">
|
<div class="pdf-toolbar">
|
||||||
@@ -417,7 +429,10 @@ export function renderFile(data) {
|
|||||||
<i data-lucide="download" style="width:14px;height:14px"></i> Télécharger
|
<i data-lucide="download" style="width:14px;height:14px"></i> Télécharger
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<iframe src="${pdfUrl}" class="pdf-iframe" title="${escapeHtml(data.title)}"></iframe>
|
<div class="pdf-body">
|
||||||
|
${tocHtml}
|
||||||
|
<iframe src="${pdfUrl}" class="pdf-iframe" title="${escapeHtml(data.title)}"></iframe>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8498,6 +8498,46 @@ body.popup-mode .content-area {
|
|||||||
border: none;
|
border: none;
|
||||||
min-height: 70vh;
|
min-height: 70vh;
|
||||||
}
|
}
|
||||||
|
.pdf-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.pdf-toc {
|
||||||
|
width: 240px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: var(--surface2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.pdf-toc h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.pdf-toc ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pdf-toc li {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.pdf-toc a {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.pdf-toc a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.toc-page {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── PaneManager — Split View ── */
|
/* ── PaneManager — Split View ── */
|
||||||
.pane-grid {
|
.pane-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user