QuadTree / src /app.py
daemon03's picture
UI update
fd1723f verified
import streamlit as st
import numpy as np
from PIL import Image
import io
import sys
import os
# Bug 3 & 4 fixes - Top level imports, recursion limit
sys.setrecursionlimit(10000)
sys.path.insert(0, os.path.dirname(__file__))
from quadtree_engine import (
read_ppm_bytes, process_image, arr_to_pil, compress_image, decompress_image, pad_to_square_pow2
)
st.set_page_config(
page_title="Image Manipulation Engine",
page_icon="✨",
layout="wide",
initial_sidebar_state="collapsed"
)
# ── CSS ──────────────────────────────────────────────────────────────────────
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
html, body, [data-testid="stAppViewContainer"], .stApp {
background: #FAF6F0 !important; /* Claude's warm light orange/beige background */
color: #2D2D2D !important;
font-family: 'Inter', sans-serif !important;
}
header[data-testid="stHeader"] {
background: transparent !important;
}
/* Hide deploy buttons and toolbar */
header[data-testid="stHeader"] .stAppDeployButton,
header[data-testid="stHeader"] [data-testid="stToolbar"],
header[data-testid="stHeader"] [data-testid="stHeaderActionElements"] {
display: none !important;
}
h1, h2, h3, h4, h5, h6, [data-testid="stMarkdownContainer"] h1 {
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
color: #1A1A1A !important;
}
/* Claude UI Buttons */
.stButton > button {
background-color: #1A1A1A !important;
color: #FFFFFF !important;
border: none !important;
border-radius: 8px !important;
font-family: 'Inter', sans-serif !important;
font-weight: 500 !important;
padding: 0.5rem 1rem !important;
transition: all 0.2s ease;
width: 100%;
}
.stButton > button:hover {
background-color: #333333 !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
/* Claude UI Secondary Button (Download) */
.stDownloadButton > button {
background-color: #FFFFFF !important;
color: #1A1A1A !important;
border: 1px solid #D1D1D1 !important;
border-radius: 8px !important;
font-family: 'Inter', sans-serif !important;
font-weight: 500 !important;
width: 100%;
padding: 0.5rem 1rem !important;
transition: all 0.2s ease;
}
.stDownloadButton > button:hover {
background-color: #F8F8F8 !important;
border-color: #1A1A1A !important;
}
/* Inputs */
.stSelectbox > div > div {
background-color: #FFFFFF !important;
border-radius: 8px !important;
border: 1px solid #E5E0D8 !important;
color: #1A1A1A !important;
}
.stTextInput > div > div > input {
background-color: #FFFFFF !important;
border-radius: 8px !important;
border: 1px solid #E5E0D8 !important;
color: #1A1A1A !important;
}
/* Slider Customization */
.stSlider [data-testid="stTickBar"] {
background-color: #E5E0D8 !important;
}
.stSlider [data-testid="stSliderThumb"] {
background-color: #D97757 !important; /* Claude's Peach/Orange accent */
}
/* Tabs Styling */
.stTabs [data-baseweb="tab-list"] {
gap: 2rem;
}
.stTabs [data-baseweb="tab"] {
font-family: 'Inter', sans-serif !important;
font-weight: 500 !important;
padding-bottom: 0.5rem !important;
color: #777777 !important;
}
.stTabs [aria-selected="true"] {
color: #1A1A1A !important;
border-bottom-color: #D97757 !important;
}
/* Hide Sidebar */
[data-testid="stSidebar"] {
display: none !important;
}
[data-testid="collapsedControl"] {
display: none !important;
}
/* Custom CSS classes for components */
.claude-stats-container {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.claude-stat-card {
flex: 1;
background-color: #FFFFFF;
border: 1px solid #E5E0D8;
border-radius: 12px;
padding: 1.25rem;
text-align: center;
min-width: 140px;
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
}
.claude-stat-label {
font-size: 0.75rem;
color: #888888;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
font-weight: 600;
}
.claude-stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #1A1A1A;
}
.claude-stat-sub {
font-size: 0.75rem;
color: #D97757;
margin-top: 0.25rem;
}
.step-row {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
background-color: #FFFFFF;
border: 1px solid #E5E0D8;
padding: 0.75rem;
border-radius: 8px;
}
.step-num {
color: #D97757;
font-weight: 700;
margin-right: 1rem;
min-width: 60px;
}
.code-block-custom {
background-color: #F8F9FA;
border: 1px solid #E5E0D8;
padding: 1rem;
border-radius: 8px;
color: #2D2D2D;
}
hr {
border-color: #E5E0D8 !important;
}
/* Make radio button labels dark */
[data-testid="stRadio"] label {
color: #1A1A1A !important;
}
</style>
""", unsafe_allow_html=True)
# ── Helpers ───────────────────────────────────────────────────────────────────
PPM_DIR = os.path.join(os.path.dirname(__file__), "c_project_files")
def list_ppms():
result = []
if not os.path.isdir(PPM_DIR):
return result
for f in sorted(os.listdir(PPM_DIR)):
if not f.endswith(".ppm"):
continue
fpath = os.path.join(PPM_DIR, f)
try:
with open(fpath, "rb") as fh:
header = fh.read(3)
if header.startswith(b'P6'):
result.append(f)
except Exception:
pass
return result
def load_array(uploaded=None, ppm_name=None):
if uploaded is not None:
uploaded.seek(0)
raw = uploaded.read()
elif ppm_name:
path = os.path.join(PPM_DIR, ppm_name)
with open(path, "rb") as f:
raw = f.read()
else:
return None, None
try:
arr = np.array(Image.open(io.BytesIO(raw)).convert("RGB"), dtype=np.uint8)
return arr, raw
except Exception:
pass
try:
arr = read_ppm_bytes(raw)
return arr, raw
except Exception as e:
fname = uploaded.name if uploaded else ppm_name
raise ValueError(f"Cannot read '{fname}' as an image.\nDetail: {e}")
def count_nodes_and_leaves(node, depth=0):
if node is None:
return 0, 0, 0 # total, leaves, max_d
if node.is_leaf():
return 1, 1, depth
counts = [count_nodes_and_leaves(c, depth+1) for c in [node.topLeft, node.topRight, node.bottomLeft, node.bottomRight]]
total = 1 + sum(c[0] for c in counts)
leaves = sum(c[1] for c in counts)
max_d = max(c[2] for c in counts)
return total, leaves, max_d
# ── App Header ───────────────────────────────────────────────────────────────
st.markdown("<h1 style='text-align: center; margin-bottom: 2rem; font-size: 2.5rem; letter-spacing: -0.02em;'>Image Manipulation Engine</h1>", unsafe_allow_html=True)
# ── Main Area ────────────────────────────────────────────────────────────────
tab1, tab2, tab3 = st.tabs(["IMAGE VIEW", "ALGORITHM EXPLORER", "C SOURCE"])
OP_MAP = {
"Compress Only": "compress_only",
"Grayscale": "grayscale",
"Negative": "negative",
"Sepia": "sepia",
"Brighten": "brighten",
"Mirror (Horizontal)": "mirror",
"Flip (Vertical)": "water",
"Rotate Left 90Β°": "rotate_left",
"Rotate Right 90Β°": "rotate_right",
"Blend / Union": "union",
}
with tab1:
# --- CONTROL PANEL ---
st.markdown("<div style='background-color: #FFFFFF; padding: 1.5rem; border-radius: 12px; border: 1px solid #E5E0D8; margin-bottom: 2rem; box-shadow: 0 4px 12px rgba(0,0,0,0.03);'>", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 1, 1])
arr1 = None
file_id = None
arr2 = None
with col1:
st.markdown("<div style='color: #1A1A1A; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.95rem;'>1. Input Source</div>", unsafe_allow_html=True)
input_options = ["Upload file"]
has_ppms = os.path.isdir(PPM_DIR) and len(list_ppms()) > 0
if has_ppms:
input_options.append("Use Sample Images")
input_mode = st.radio("Source", input_options, label_visibility="collapsed", horizontal=True)
if input_mode == "Upload file":
up1 = st.file_uploader("Primary image", type=["ppm","png","jpg","jpeg"], key="u1", label_visibility="collapsed")
if up1:
file_id = f"{up1.name}_{up1.size}"
try:
arr1, _ = load_array(uploaded=up1)
except ValueError as e:
st.error(str(e))
else:
sel = st.selectbox("Select PPM", list_ppms(), label_visibility="collapsed")
if sel:
file_id = f"repo_{sel}"
try:
arr1, _ = load_array(ppm_name=sel)
except ValueError as e:
st.error(str(e))
with col2:
st.markdown("<div style='color: #1A1A1A; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.95rem;'>2. Operation</div>", unsafe_allow_html=True)
# Spacer to align with the radio button in col1
st.markdown("<div style='height: 38px; margin-bottom: 0px;'></div>", unsafe_allow_html=True)
operation = st.selectbox("Operation", list(OP_MAP.keys()), label_visibility="collapsed")
op_key = OP_MAP[operation]
if op_key == "union":
st.markdown("<div style='font-size: 0.85rem; color: #777; margin-top: 0.5rem; margin-bottom: 0.2rem;'>Second image for blending:</div>", unsafe_allow_html=True)
if input_mode == "Upload file":
up2 = st.file_uploader("Second image", type=["ppm","png","jpg","jpeg"], key="u2", label_visibility="collapsed")
if up2:
try:
arr2, _ = load_array(uploaded=up2)
except ValueError as e:
st.error(str(e))
else:
sel2 = st.selectbox("Second PPM", list_ppms(), key="sel2", label_visibility="collapsed")
if sel2:
try:
arr2, _ = load_array(ppm_name=sel2)
except ValueError as e:
st.error(str(e))
with col3:
st.markdown("<div style='color: #1A1A1A; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.95rem;'>3. Configuration</div>", unsafe_allow_html=True)
# Spacer to align with the radio button in col1
st.markdown("<div style='height: 38px; margin-bottom: 0px;'></div>", unsafe_allow_html=True)
threshold = st.slider("Quality vs Compression", 1, 500, 30, label_visibility="collapsed")
if threshold <= 30:
st.markdown("<div style='color: #777; font-size: 0.8rem; margin-top: -10px;'>Mode: <span style='color: #10B981; font-weight: 600;'>Lossless</span></div>", unsafe_allow_html=True)
elif threshold <= 100:
st.markdown("<div style='color: #777; font-size: 0.8rem; margin-top: -10px;'>Mode: <span style='color: #F59E0B; font-weight: 600;'>Balanced</span></div>", unsafe_allow_html=True)
else:
st.markdown("<div style='color: #777; font-size: 0.8rem; margin-top: -10px;'>Mode: <span style='color: #EF4444; font-weight: 600;'>Lossy</span></div>", unsafe_allow_html=True)
st.markdown("<hr style='margin-top: 1.5rem; margin-bottom: 1.5rem;'>", unsafe_allow_html=True)
col_empty, col_btn = st.columns([4, 2])
with col_btn:
run = st.button("✨ Generate Result", disabled=(arr1 is None))
st.markdown("</div>", unsafe_allow_html=True)
# State management
if file_id != st.session_state.get("last_file_id"):
st.session_state.pop("result", None)
st.session_state.pop("tree", None)
st.session_state.pop("op_done", None)
st.session_state["last_file_id"] = file_id
# --- IMAGE PROCESSING ---
if run and arr1 is not None:
st.session_state.pop("result", None)
st.session_state.pop("tree", None)
with st.spinner("Processing image..."):
try:
if op_key == "compress_only":
padded, oh, ow = pad_to_square_pow2(arr1)
size = padded.shape[0]
tree = compress_image(padded, 0, 0, size, threshold)
out = np.zeros((size, size, 3), dtype=np.uint8)
decompress_image(tree, out, 0, 0, size)
computed = out[:oh, :ow]
elif op_key == "union":
if arr2 is None:
st.error("Please provide a second image for union.")
computed, tree = None, None
else:
computed, tree = process_image(arr1, "union", threshold, arr2, return_tree=True)
else:
computed, tree = process_image(arr1, op_key, threshold, return_tree=True)
if computed is not None:
st.session_state["result"] = computed
st.session_state["tree"] = tree
st.session_state["op_done"] = operation
st.session_state["thresh_done"] = threshold
except RecursionError:
st.error("RecursionError: image too large for this threshold. Try a higher threshold.")
except Exception as e:
st.error(f"Error: {e}")
result_arr = st.session_state.get("result")
tree = st.session_state.get("tree")
# --- RESULTS DISPLAY ---
if arr1 is not None:
MAX_DIM = 2048
h, w = arr1.shape[:2]
if h > MAX_DIM or w > MAX_DIM:
st.warning(f"Image is {w}Γ—{h}. Images over {MAX_DIM}px may be slow or crash. Consider resizing first.")
rc1, rc2 = st.columns(2)
with rc1:
st.markdown("<div style='text-align: center; color: #888; margin-bottom: 1rem; font-weight: 600; letter-spacing: 0.05em; font-size: 0.85rem;'>ORIGINAL</div>", unsafe_allow_html=True)
st.image(arr_to_pil(arr1), use_container_width=True)
st.markdown(f"<div style='text-align: center; color: #777; font-size: 0.85rem; margin-top: 0.5rem;'>{arr1.shape[1]} Γ— {arr1.shape[0]} px</div>", unsafe_allow_html=True)
with rc2:
st.markdown("<div style='text-align: center; color: #888; margin-bottom: 1rem; font-weight: 600; letter-spacing: 0.05em; font-size: 0.85rem;'>PROCESSED</div>", unsafe_allow_html=True)
if result_arr is not None:
st.image(arr_to_pil(result_arr), use_container_width=True)
op_str = st.session_state.get("op_done", "")
th_str = st.session_state.get("thresh_done", "")
st.markdown(f"<div style='text-align: center; color: #777; font-size: 0.85rem; margin-top: 0.5rem;'>{op_str} (Threshold: {th_str})</div>", unsafe_allow_html=True)
else:
st.markdown("""
<div style='background-color: #FFFFFF; border: 1px dashed #D1D1D1; border-radius: 12px; height: 300px; display: flex; align-items: center; justify-content: center; flex-direction: column;'>
<span style='font-size: 2rem; margin-bottom: 1rem;'>✨</span>
<span style='color: #888; font-family: "Inter", sans-serif;'>Ready to process</span>
</div>
""", unsafe_allow_html=True)
# QuadTree Stats
if result_arr is not None and tree is not None:
total_nodes, leaves, max_d = count_nodes_and_leaves(tree)
total_pixels = arr1.shape[0] * arr1.shape[1]
ratio = total_pixels / total_nodes if total_nodes > 0 else 0
st.markdown("""
<div class='claude-stats-container'>
<div class='claude-stat-card'>
<div class='claude-stat-label'>Total Nodes</div>
<div class='claude-stat-value'>{nodes}</div>
<div class='claude-stat-sub'>{ratio:.1f}x fewer nodes</div>
</div>
<div class='claude-stat-card'>
<div class='claude-stat-label'>Leaf Nodes</div>
<div class='claude-stat-value'>{leaves}</div>
</div>
<div class='claude-stat-card'>
<div class='claude-stat-label'>Max Depth</div>
<div class='claude-stat-value'>{max_d}</div>
</div>
<div class='claude-stat-card'>
<div class='claude-stat-label'>Total Pixels</div>
<div class='claude-stat-value'>{pixels}</div>
</div>
</div>
""".replace('{nodes}', f"{total_nodes:,}").replace('{ratio}', f"{ratio}").replace('{leaves}', f"{leaves:,}").replace('{max_d}', str(max_d)).replace('{pixels}', f"{total_pixels:,}"), unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
col_empty, col_dl = st.columns([4, 1])
with col_dl:
img_pil = arr_to_pil(result_arr)
buf = io.BytesIO()
img_pil.save(buf, format="PNG")
st.download_button("Download Result", buf.getvalue(), "result.png", "image/png")
else:
st.markdown("""
<div style='background-color: #FFFFFF; border: 1px dashed #D1D1D1; border-radius: 12px; padding: 4rem; text-align: center; margin-top: 2rem;'>
<h3 style='color: #1A1A1A; font-weight: 500; margin-bottom: 0.5rem;'>No Image Selected</h3>
<p style='color: #666;'>Upload an image or select one from the repository to get started.</p>
</div>
""", unsafe_allow_html=True)
with tab2:
st.markdown("### 1. What is a Quadtree?")
c1, c2 = st.columns(2)
with c1:
st.code("""
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ β”‚
β”‚ TL β”‚ TR β”‚
β”‚ β”‚ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚ β”‚
β”‚ BL β”‚ BR β”‚
β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜
""", language="text")
with c2:
st.markdown("""
- **`red`, `green`, `blue`**: The average color of this region.
- **`area`**: The total pixels covered by this node.
- **Children**: If variance > threshold, the region splits into 4 sub-regions (`topLeft`, `topRight`, `bottomLeft`, `bottomRight`).
- **Leaf Node**: If variance <= threshold, children are null. The region is colored uniformly.
""")
st.markdown("---")
st.markdown("### 2. How Compression Works")
st.markdown("<div style='color: #777; margin-bottom: 1rem;'>See how threshold controls quality vs. compression</div>", unsafe_allow_html=True)
@st.cache_data
def get_synthetic_image():
y, x = np.mgrid[0:64, 0:64]
r = (x * 4) % 255
g = (y * 4) % 255
b = ((x + y) * 2) % 255
noise = np.random.randint(0, 50, (64, 64, 3))
img = np.stack([r, g, b], axis=-1) + noise
return np.clip(img, 0, 255).astype(np.uint8)
synth_img = get_synthetic_image()
sim_thresh = st.slider("Simulator Threshold", 1, 200, 50, key="sim_t")
sc1, sc2 = st.columns(2)
with sc1:
st.image(arr_to_pil(synth_img), caption="Original (64x64)", use_container_width=True)
with sc2:
sim_tree = compress_image(synth_img, 0, 0, 64, sim_thresh)
sim_out = np.zeros((64, 64, 3), dtype=np.uint8)
decompress_image(sim_tree, sim_out, 0, 0, 64)
total_sim, leaves_sim, _ = count_nodes_and_leaves(sim_tree)
st.image(arr_to_pil(sim_out), caption=f"Compressed (Nodes: {total_sim})", use_container_width=True)
st.markdown("---")
st.markdown("### 3. Compression Algorithm Step-by-Step")
st.markdown("""
<div class="step-row"><span class="step-num">STEP 1</span><span>Divide image into 4 quadrants</span></div>
<div class="step-row"><span class="step-num">STEP 2</span><span>Compute mean RGB + variance for each quadrant</span></div>
<div class="step-row"><span class="step-num">STEP 3</span><span>If variance ≀ threshold: <b>LEAF</b> β†’ store avg color, stop subdividing</span></div>
<div class="step-row"><span class="step-num">STEP 4</span><span>If variance > threshold: <b>RECURSE</b> into each quadrant with size/2</span></div>
<div class="step-row"><span class="step-num">STEP 5</span><span>Continue until size=1 pixel or variance ≀ threshold</span></div>
""", unsafe_allow_html=True)
st.markdown("---")
st.markdown("### 4. Color Filters β€” How They Work on the Tree")
fc1, fc2, fc3, fc4 = st.columns(4)
with fc1:
with st.container():
st.markdown("**Grayscale**")
st.code("L = 0.299R + 0.587G + 0.114B\nR'=L, G'=L, B'=L")
st.caption("Weights green channel most heavily")
with fc2:
with st.container():
st.markdown("**Negative**")
st.code("R' = 255 - R\nG' = 255 - G\nB' = 255 - B")
st.caption("Inverts each channel β€” dark becomes light")
with fc3:
with st.container():
st.markdown("**Sepia**")
st.code("R' = 0.393R + ...\nG' = ...\nB' = ...")
st.caption("Warm brownish tones by mixing channels")
with fc4:
with st.container():
st.markdown("**Brighten**")
st.code("R' = min(255, R*1.3)\nG' = ...\nB' = ...")
st.caption("Scales all channels up")
st.markdown("---")
st.markdown("### 5. Spatial Transforms β€” Pointer Swaps")
st.markdown("<div style='color: #D97757; margin-bottom: 1rem;'><strong>No pixel data is ever copied. Only 4 pointer assignments per node.</strong></div>", unsafe_allow_html=True)
tc1, tc2 = st.columns(2)
with tc1:
st.code("""MIRROR (horizontal):
Before: TL | TR
-------
BL | BR
After: TR | TL
-------
BR | BL""", language="text")
with tc2:
st.code("""ROTATE LEFT 90Β°:
Before: TL | TR
-------
BL | BR
After: TR | BR
-------
TL | BL""", language="text")
tc3, tc4 = st.columns(2)
with tc3:
st.code("""FLIP (vertical):
Before: TL | TR
-------
BL | BR
After: BL | BR
-------
TL | TR""", language="text")
with tc4:
st.code("""ROTATE RIGHT 90Β°:
Before: TL | TR
-------
BL | BR
After: BL | TL
-------
BR | TR""", language="text")
st.markdown("---")
st.markdown("### 6. Union / Blend β€” Three Cases")
st.markdown("""
- **Case 1:** Both nodes are internal β†’ recurse into all 4 child pairs.
- **Case 2:** t1 is a leaf, t2 has children β†’ blend t1's solid color with each of t2's children.
- **Case 3:** t2 is a leaf, t1 has children β†’ blend t2's solid color with each of t1's children.
Averaging formula: `result.R = (t1.R + t2.R) / 2`
*Note: This produces a pixel-perfect 50/50 blend without ever decompressing either image to a pixel buffer.*
""")
st.markdown("---")
st.markdown("### 7. Complexity Analysis")
st.markdown("""
| Operation | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Compress | O(n log n) | O(n) | n = total pixels |
| Decompress | O(n) | O(n) | Linear tree traversal |
| Filter | O(k) | O(k) | k = tree nodes, k β‰ͺ n |
| Rotate/Mirror | O(k) | O(1) | Only pointer swaps |
| Union | O(min(k1,k2)) | O(min(k1,k2)) | Bounded by smaller tree |
<div style="border-left: 4px solid #D97757; padding-left: 1rem; margin-top: 1rem; color: #2D2D2D;">
<strong>Key Takeaway:</strong> Filters and transforms run on the compressed tree β€” they're O(nodes) not O(pixels). At threshold=100, a 512Γ—512 image (262K pixels) may have fewer than 5,000 nodes.
</div>
""", unsafe_allow_html=True)
with tab3:
st.markdown("### C Source vs Python")
st.markdown("""
<div style="background-color: #FFFFFF; border: 1px solid #E5E0D8; padding: 1rem; margin-bottom: 1rem; border-radius: 8px;">
πŸ”— View the full C implementation on GitHub:<br>
<a href="https://github.com/Harshwardhan-Deshmukh03/Image-Manipulation-QuadTree.git" target="_blank"
style="color:#D97757;font-weight:600;text-decoration:none;">
github.com/Harshwardhan-Deshmukh03/Image-Manipulation-QuadTree
</a>
</div>
""", unsafe_allow_html=True)
st.markdown("""
| C File | Python Equivalent |
|---|---|
| `compress.c` | `compress_image()` |
| `filters.c` | `apply_grayscale()`, `apply_negative()`, etc. |
| `rotate.c` | `rotate_left()`, `get_mirror_image()`, etc. |
| `union.c` | `union_of_images()` |
| `decompress.c` | `decompress_image()` |
""")
c_files = {
"main.c": "CLI entry point β€” parses flags, orchestrates the full pipeline",
"compress.c": "Quadtree construction from pixel matrix using variance threshold",
"decompress.c": "Quadtree β†’ pixel matrix reconstruction",
"filters.c": "Color filters: grayscale, negative, sepia, brighten",
"rotate.c": "Spatial transforms: mirror, flip, rotate L/R/180",
"union.c": "Pixel-level blending of two Quadtrees",
"suppl.c": "PPM I/O, getMean(), tree serialization helpers",
"suppl.h": "All struct definitions: pixels, qtNode, qtInfo",
}
st.markdown("<br>", unsafe_allow_html=True)
for fname, desc in c_files.items():
fpath = os.path.join(PPM_DIR, fname)
with st.expander(f"`{fname}` β€” {desc}"):
if os.path.isfile(fpath):
with open(fpath) as f:
st.code(f.read(), language="c")
else:
st.info(f"File not found at: {fpath}")