Skip to content

Commit 11e4510

Browse files
committed
Render PDF pages as images
1 parent f241190 commit 11e4510

8 files changed

Lines changed: 156 additions & 39 deletions

File tree

Computations/Code/__init__.py

Whitespace-only changes.

Computations/Code/app/app.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@
2424
import pandas as pd
2525
import streamlit as st
2626

27+
import streamlit.components.v1 as components
28+
from streamlit_tree_select import tree_select
29+
2730

2831
CURRENT_FILE = Path(__file__).resolve()
2932
CODE_DIR = CURRENT_FILE.parents[1]
3033
sys.path.append(str(CODE_DIR))
3134

3235
from core.indexer import scan_project # noqa: E402
3336
from core.validator import validate_project_structure, calculate_structure_score # noqa: E402
34-
from core.previewer import get_file_type, read_text_file, read_table_file # noqa: E402
37+
from core.previewer import get_file_type, read_text_file, read_table_file, render_notebook_html, render_pdf_pages # noqa: E402
3538
from core.exporter import file_index_to_csv, file_index_to_json, file_index_to_html # noqa: E402
36-
from core.tree_builder import build_tree, tree_to_markdown # noqa: E402
39+
from core.tree_builder import build_tree, tree_to_markdown, tree_to_select_nodes # noqa: E402
3740
from core.reporter import generate_markdown_report # noqa: E402
3841
from core.output_writer import create_output_directory, save_text_outputs # noqa: E402
3942

@@ -116,6 +119,7 @@
116119

117120
project_tree = build_tree(str(path), include_hidden=include_hidden)
118121
tree_markdown = tree_to_markdown(project_tree)
122+
tree_nodes = tree_to_select_nodes(project_tree)
119123

120124
except Exception as error:
121125
st.error(f"Could not scan project: {error}")
@@ -152,18 +156,37 @@
152156
st.write("### Files by extension")
153157
st.bar_chart(df["extension"].replace("", "[no extension]").value_counts())
154158

159+
155160
elif page == "Directory tree":
156161

157-
st.subheader("Directory tree")
162+
st.subheader("Interactive directory tree")
158163

159164
st.write(
160165
"""
161-
This view shows the selected project as a nested directory tree.
162-
Hidden files and hidden folders are excluded from this view.
166+
Select files or folders from the project tree.
167+
Hidden files and ignored system folders follow the scan settings from the sidebar.
163168
"""
164169
)
165170

166-
st.code(tree_markdown, language="markdown")
171+
selected_nodes = tree_select(
172+
tree_nodes,
173+
checked=[],
174+
expanded=[str(path)],
175+
)
176+
177+
st.write("### Selected paths")
178+
179+
checked_paths = selected_nodes.get("checked", [])
180+
181+
if checked_paths:
182+
for selected_path in checked_paths:
183+
st.code(selected_path)
184+
else:
185+
st.info("No file or folder selected.")
186+
187+
with st.expander("Show markdown tree"):
188+
st.code(tree_markdown, language="markdown")
189+
167190

168191
elif page == "Structure validation":
169192

@@ -275,7 +298,29 @@
275298
)
276299

277300
elif preview_type == "pdf":
278-
st.info("PDF preview will be added in a later step.")
301+
try:
302+
max_pages = st.slider(
303+
"Number of PDF pages to preview",
304+
min_value=1,
305+
max_value=50,
306+
value=5,
307+
)
308+
309+
pdf_pages = render_pdf_pages(selected_path, max_pages=max_pages)
310+
311+
for page_number, image in pdf_pages:
312+
st.write(f"Page {page_number}")
313+
st.image(image, use_container_width=True)
314+
315+
except Exception as error:
316+
st.error(f"Could not render PDF: {error}")
317+
318+
elif preview_type == "notebook":
319+
try:
320+
notebook_html = render_notebook_html(selected_path)
321+
components.html(notebook_html, height=900, scrolling=True)
322+
except Exception as error:
323+
st.error(f"Could not render notebook: {error}")
279324

280325
else:
281326
st.warning("Preview is not supported for this file type yet.")

Computations/Code/core/__init__.py

Whitespace-only changes.

Computations/Code/core/previewer.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@
88
- Text/code files.
99
- CSV/TSV tables.
1010
- Images.
11-
12-
Unsupported or future previews
13-
------------------------------
14-
- PDF rendering.
15-
- Binary scientific objects such as H5AD/RDS.
11+
- PDFs.
12+
- Jupyter notebooks.
1613
"""
1714

1815
from pathlib import Path
1916
from typing import Optional
2017

18+
import nbformat
2119
import pandas as pd
20+
from nbconvert import HTMLExporter
2221

22+
import fitz
23+
from PIL import Image
24+
import io
2325

2426
TEXT_EXTENSIONS = {
2527
".txt", ".md", ".py", ".r", ".rmd", ".sh",
@@ -37,10 +39,6 @@
3739

3840

3941
def get_file_type(file_path: str) -> str:
40-
"""
41-
Return broad preview type for a file.
42-
"""
43-
4442
suffix = Path(file_path).suffix.lower()
4543

4644
if suffix in TABLE_EXTENSIONS:
@@ -55,14 +53,13 @@ def get_file_type(file_path: str) -> str:
5553
if suffix == ".pdf":
5654
return "pdf"
5755

56+
if suffix == ".ipynb":
57+
return "notebook"
58+
5859
return "unsupported"
5960

6061

6162
def read_text_file(file_path: str, max_chars: int = 10000) -> str:
62-
"""
63-
Read a text-like file safely.
64-
"""
65-
6663
path = Path(file_path)
6764

6865
try:
@@ -77,10 +74,6 @@ def read_text_file(file_path: str, max_chars: int = 10000) -> str:
7774

7875

7976
def read_table_file(file_path: str, max_rows: Optional[int] = 500) -> pd.DataFrame:
80-
"""
81-
Read CSV/TSV file for preview.
82-
"""
83-
8477
path = Path(file_path)
8578
suffix = path.suffix.lower()
8679

@@ -90,4 +83,45 @@ def read_table_file(file_path: str, max_rows: Optional[int] = 500) -> pd.DataFra
9083
if suffix == ".tsv":
9184
return pd.read_csv(path, sep="\t", nrows=max_rows)
9285

93-
raise ValueError(f"Unsupported table format: {suffix}")
86+
raise ValueError(f"Unsupported table format: {suffix}")
87+
88+
89+
def render_notebook_html(file_path: str) -> str:
90+
"""
91+
Convert a Jupyter notebook to HTML for preview.
92+
"""
93+
94+
path = Path(file_path)
95+
96+
notebook = nbformat.read(path, as_version=4)
97+
98+
exporter = HTMLExporter()
99+
exporter.exclude_input_prompt = True
100+
exporter.exclude_output_prompt = True
101+
102+
body, _ = exporter.from_notebook_node(notebook)
103+
104+
return body
105+
106+
107+
def render_pdf_pages(file_path: str, max_pages: int = 5, zoom: float = 2.0):
108+
"""
109+
Render PDF pages as PIL images for Streamlit preview.
110+
"""
111+
112+
path = Path(file_path)
113+
document = fitz.open(path)
114+
115+
images = []
116+
117+
for page_number in range(min(len(document), max_pages)):
118+
page = document.load_page(page_number)
119+
matrix = fitz.Matrix(zoom, zoom)
120+
pixmap = page.get_pixmap(matrix=matrix)
121+
122+
image = Image.open(io.BytesIO(pixmap.tobytes("png")))
123+
images.append((page_number + 1, image))
124+
125+
document.close()
126+
127+
return images

Computations/Code/core/tree_builder.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,22 @@
22
Directory Tree Builder
33
44
Builds a nested representation of a local project directory and converts it
5-
to a markdown-style tree view.
5+
to Streamlit tree-select compatible nodes.
66
"""
77

88
from pathlib import Path
9-
from typing import Dict, Any
9+
from typing import Dict, Any, List
1010

1111
from core.indexer import should_ignore
1212

1313

1414
def build_tree(project_path: str, include_hidden: bool = False) -> Dict[str, Any]:
15-
"""
16-
Build a nested dictionary representation of a project directory.
17-
"""
18-
1915
root = Path(project_path).expanduser().resolve()
2016

2117
def walk(path: Path) -> Dict[str, Any]:
2218
children = []
2319

2420
for item in sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name.lower())):
25-
2621
if should_ignore(item, include_hidden=include_hidden):
2722
continue
2823

@@ -49,16 +44,32 @@ def walk(path: Path) -> Dict[str, Any]:
4944

5045

5146
def tree_to_markdown(tree: Dict[str, Any], indent: int = 0) -> str:
52-
"""
53-
Convert nested tree dictionary to markdown text.
54-
"""
55-
5647
prefix = " " * indent
57-
5848
icon = "📁" if tree["type"] == "directory" else "📄"
5949
line = f"{prefix}- {icon} {tree['name']}\n"
6050

6151
for child in tree.get("children", []):
6252
line += tree_to_markdown(child, indent + 1)
6353

64-
return line
54+
return line
55+
56+
57+
def tree_to_select_nodes(tree: Dict[str, Any]) -> List[Dict[str, Any]]:
58+
"""
59+
Convert tree dictionary to streamlit-tree-select node format.
60+
"""
61+
62+
def convert_node(node: Dict[str, Any]) -> Dict[str, Any]:
63+
icon = "📁" if node["type"] == "directory" else "📄"
64+
65+
converted = {
66+
"label": f"{icon} {node['name']}",
67+
"value": node["path"],
68+
}
69+
70+
if node.get("children"):
71+
converted["children"] = [convert_node(child) for child in node["children"]]
72+
73+
return converted
74+
75+
return [convert_node(tree)]

Computations/Code/tests/__init__.py

Whitespace-only changes.

environment.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ dependencies:
2828
- annotated-types==0.7.0
2929
- anyio==4.13.0
3030
- attrs==26.1.0
31+
- beautifulsoup4==4.14.3
32+
- bleach==6.3.0
3133
- blinker==1.9.0
3234
- cachetools==7.1.1
3335
- certifi==2026.4.22
3436
- charset-normalizer==3.4.7
3537
- click==8.3.3
38+
- defusedxml==0.7.1
39+
- fastjsonschema==2.21.2
3640
- gitdb==4.0.12
3741
- gitpython==3.1.50
3842
- h11==0.16.0
@@ -43,39 +47,57 @@ dependencies:
4347
- jinja2==3.1.6
4448
- jsonschema==4.26.0
4549
- jsonschema-specifications==2025.9.1
50+
- jupyter-client==8.8.0
51+
- jupyter-core==5.9.1
52+
- jupyterlab-pygments==0.3.0
4653
- markdown==3.10.2
4754
- markdown-it-py==4.1.0
4855
- markupsafe==3.0.3
4956
- mdurl==0.1.2
57+
- mistune==3.2.1
5058
- narwhals==2.20.0
59+
- nbclient==0.10.4
60+
- nbconvert==7.17.1
61+
- nbformat==5.10.4
5162
- numpy==2.4.4
5263
- pandas==3.0.2
64+
- pandocfilters==1.5.1
5365
- pillow==12.2.0
66+
- platformdirs==4.9.6
5467
- pluggy==1.6.0
5568
- protobuf==7.34.1
5669
- pyarrow==24.0.0
5770
- pydantic==2.13.4
5871
- pydantic-core==2.46.4
5972
- pydeck==0.9.2
6073
- pygments==2.20.0
74+
- pymupdf==1.27.2.3
6175
- pytest==9.0.3
6276
- python-dateutil==2.9.0.post0
6377
- python-multipart==0.0.27
6478
- pyyaml==6.0.3
79+
- pyzmq==27.1.0
6580
- referencing==0.37.0
6681
- requests==2.33.1
6782
- rich==15.0.0
6883
- rpds-py==0.30.0
6984
- six==1.17.0
7085
- smmap==5.0.3
86+
- soupsieve==2.8.3
7187
- starlette==1.0.0
7288
- streamlit==1.57.0
89+
- streamlit-pdf==2.0.1
90+
- streamlit-tree-select==0.0.5
7391
- tenacity==9.1.4
92+
- tinycss2==1.4.0
7493
- toml==0.10.2
94+
- tornado==6.5.5
95+
- traitlets==5.15.0
7596
- typing-extensions==4.15.0
7697
- typing-inspection==0.4.2
7798
- urllib3==2.6.3
7899
- uvicorn==0.46.0
79100
- watchdog==6.0.0
101+
- webencodings==0.5.1
80102
- websockets==16.0
81103
prefix: /Users/utkarsh/anaconda3/envs/fssnav

requirements.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ pygments
77
rich
88
watchdog
99
pytest
10-
pyyaml
10+
pyyaml
11+
streamlit-tree-select
12+
nbconvert
13+
nbformat
14+
pymupdf
15+
pillow

0 commit comments

Comments
 (0)