CoolFace
Apppublic

sankhyan/st_table_extractor

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
st.py280 linesDownload Raw Back to root
1# File: streamlit_app.py
2
3import os
4import re
5import csv
6import tempfile
7import base64
8
9import pandas as pd
10import PyPDF2
11import pytesseract
12from pdf2image import convert_from_path
13import streamlit as st
14
15# ─── Configuration ────────────────────────────────────────────────────────────
16
17POPPLER_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "poppler", "bin")
18TESSERACT_CMD = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
19pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD
20
21
22# ─── Helper Functions (from your original Flask code) ─────────────────────────
23
24def get_poppler_path():
25    return POPPLER_PATH
26
27def convert_hindi_digits(text: str) -> str:
28    hindi_digits = {
29        "०": "0", "१": "1", "२": "2", "३": "3", "४": "4",
30        "५": "5", "६": "6", "७": "7", "८": "8", "९": "9",
31    }
32    return "".join(hindi_digits.get(ch, ch) for ch in text)
33
34def extract_text_from_pdf(pdf_path: str, lang: str = "eng") -> str:
35    text = ""
36    poppler_path = get_poppler_path()
37
38    with open(pdf_path, "rb") as f:
39        reader = PyPDF2.PdfReader(f)
40        num_pages = len(reader.pages)
41
42        for page_num in range(num_pages):
43            page = reader.pages[page_num]
44            page_text = page.extract_text() or ""
45
46            if not page_text.strip():
47                try:
48                    images = convert_from_path(
49                        pdf_path,
50                        first_page=page_num + 1,
51                        last_page=page_num + 1,
52                        poppler_path=(poppler_path if os.path.exists(os.path.join(poppler_path, "pdftoppm")) else None),
53                        dpi=400,
54                        grayscale=True,
55                    )
56                    if images:
57                        img = images[0]
58                        page_text = pytesseract.image_to_string(img, lang=lang, config="--psm 6")
59                except Exception as e:
60                    st.error(f"OCR failed on page {page_num+1}: {e}")
61                    page_text = ""
62
63            text += page_text + "\n"
64
65    return text
66
67def is_valid_page_number(page_str: str) -> bool:
68    if not page_str:
69        return False
70    return all(ch in "0123456789०१२३४५६७८९" for ch in page_str)
71
72def parse_toc(text: str, is_hindi: bool = False):
73    entries = []
74
75    common_patterns = [
76        r"^(.*?)[\s\.\-]+(\d+)\s*$",
77        r"^(.*?)[\s\-\_]+(\d+)\s*$",
78        r"^\s*(\d+\..*?)[\s\.\-]+(\d+)\s*$",
79    ]
80
81    hindi_patterns = [
82        r"^\s*([०१२३४५६७८९]+\.\s+.*?)[\s\.\-]*([०१२३४५६७८९\d]+)\s*$",
83        r"^(.*?)[\s\-\—]+([०१२३४५६७८९\d]+)\s*$",
84        r"^(.*?)[\s\.]+([०१२३४५६७८९\d]+)\s*$",
85        r"^(.*?(?:अध्याय|खंड|परिशिष्ट|प्रस्तावना|भाग|अनुभाग|प्रकरण)\s*[०१२३४५६७८९]*[\.\:\-]?\s*.*?)[\s\.\-]*([०१२३४५६७८९\d]+)\s*$",
86        r"^(.*?)\s+([०१२३४५६७८९\d]+)$",
87    ]
88
89    patterns = hindi_patterns if is_hindi else common_patterns
90    skip_terms_eng = ["table of contents", "contents", "page", "chap"]
91    skip_terms_hindi = ["विषय सूची", "अनुक्रमणिका", "सामग्री", "पृष्ठ", "अध्याय"]
92    skip_terms = skip_terms_hindi if is_hindi else skip_terms_eng
93
94    for line in text.split("\n"):
95        line = line.strip()
96        if not line or len(line) < 5:
97            continue
98        if any(term in line.lower() for term in skip_terms):
99            continue
100
101        for pattern in patterns:
102            match = re.match(pattern, line, re.IGNORECASE | re.UNICODE)
103            if not match:
104                continue
105
106            groups = match.groups()
107            if len(groups) == 2:
108                chapter = groups[0].strip()
109                page = groups[1].strip()
110            elif len(groups) == 3:
111                chapter = f"{groups[0]} {groups[1]}".strip()
112                page = groups[2].strip()
113            else:
114                continue
115
116            if is_valid_page_number(page):
117                page = convert_hindi_digits(page)
118                entries.append({"chapter": chapter, "page": page})
119                break
120            else:
121                fallback = re.search(r"(\d+|[०१२३४५६७८९]+)$", line)
122                if fallback:
123                    page = fallback.group(1)
124                    if is_valid_page_number(page):
125                        chapter = line[: fallback.start()].strip()
126                        page = convert_hindi_digits(page)
127                        entries.append({"chapter": chapter, "page": page})
128                        break
129
130    return entries
131
132
133# ─── Streamlit UI ──────────────────────────────────────────────────────────────
134
135st.set_page_config(page_title="PDF TOC Extractor", layout="centered")
136st.title("📄 PDF TOC Extractor")
137
138st.write(
139    """
140    1. Upload a PDF.  
141    2. Use the Zoom slider to preview it.  
142    3. Click “Extract TOC” → the app will parse and display it in an editable table.  
143    4. Optionally add rows/columns at any index.  
144    5. Finally, download a CSV named after your PDF.
145    """
146)
147
148# — Step 1: File Uploader & Zoom Slider —────────────────────────────────────────
149
150uploaded_file = st.file_uploader("Choose a PDF file", type=["pdf"])
151zoom_pct = st.slider("Preview Zoom (%)", min_value=50, max_value=200, value=100, step=10)
152
153if uploaded_file:
154    # Display PDF preview inside an <iframe> with zoom scaled by zoom_pct
155    pdf_bytes = uploaded_file.read()
156    b64_pdf = base64.b64encode(pdf_bytes).decode("utf-8")
157    iframe_width = int(700 * (zoom_pct / 100))
158    iframe_height = 800
159
160    st.markdown(
161        f"""
162        <iframe
163            src="data:application/pdf;base64,{b64_pdf}"
164            width="{iframe_width}px"
165            height="{iframe_height}px"
166            style="border: none;"
167        ></iframe>
168        """,
169        unsafe_allow_html=True,
170    )
171
172    if "raw_pdf_bytes" not in st.session_state:
173        st.session_state["raw_pdf_bytes"] = pdf_bytes
174
175    # — Step 2: Extract TOC Button —───────────────────────────────────────────
176
177    language = st.selectbox("OCR Language (if needed)", ("eng", "hin", "both"))
178    if st.button("Extract TOC"):
179        with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
180            tmp.write(st.session_state["raw_pdf_bytes"])
181            tmp_path = tmp.name
182
183        is_hindi = (language == "hin" or language == "both")
184        ocr_lang = "eng+hin" if language == "both" else language
185
186        try:
187            raw_text = extract_text_from_pdf(tmp_path, lang=ocr_lang)
188            toc_list = parse_toc(raw_text, is_hindi=is_hindi)
189
190            if not toc_list:
191                st.warning("No TOC entries detected.")
192            else:
193                df = pd.DataFrame(toc_list)
194                st.session_state["df"] = df
195
196        except Exception as e:
197            st.error(f"Extraction error: {e}")
198            if os.path.exists(tmp_path):
199                os.remove(tmp_path)
200            st.stop()
201        finally:
202            if os.path.exists(tmp_path):
203                os.remove(tmp_path)
204
205# — Step 3: Editable Table & Add Row/Column —─────────────────────────────────
206
207if "df" in st.session_state:
208    st.subheader("🔧 Editable Table of Contents")
209    df = st.session_state["df"]
210
211    # Show the editable DataFrame (using st.data_editor instead of st.experimental_data_editor)
212    edited_df = st.data_editor(df, num_rows="dynamic", use_container_width=True)
213    st.session_state["df"] = edited_df
214
215    st.markdown("---")
216
217    # — Add a blank row at chosen index ───────────────────────────────────────
218    st.write("### ➕ Add a Blank Row")
219    max_row_idx = len(st.session_state["df"])
220    # Give the number_input its own key so we can read it on button click
221    st.number_input(
222        "Insert new row at index (0-based)", 
223        min_value=0, max_value=max_row_idx, value=max_row_idx, step=1, key="new_row_idx"
224    )
225    if st.button("Add Row", key="add_row_button"):
226        df_current = st.session_state["df"]
227        new_row_idx = st.session_state["new_row_idx"]
228
229        # Create a one-row DataFrame of empty strings matching columns
230        blank_row = pd.DataFrame({col: [""] for col in df_current.columns})
231
232        # Split and concatenate at new_row_idx
233        top = df_current.iloc[: new_row_idx].reset_index(drop=True)
234        bottom = df_current.iloc[new_row_idx :].reset_index(drop=True)
235        new_df = pd.concat([top, blank_row, bottom], ignore_index=True)
236
237        st.session_state["df"] = new_df
238        st.experimental_rerun()
239
240    st.markdown("----")
241
242    # — Add a blank column at chosen index ──────────────────────────────────
243    st.write("### ➕ Add a Blank Column")
244    new_col_name = st.text_input("New column name", value="", key="new_col_name")
245    max_col_idx = len(st.session_state["df"].columns)
246    st.number_input(
247        "Insert new column at index (0-based)", 
248        min_value=0, max_value=max_col_idx, value=max_col_idx, step=1, key="new_col_idx"
249    )
250    if st.button("Add Column", key="add_col_button"):
251        if st.session_state["new_col_name"].strip() == "":
252            st.error("Column name cannot be empty.")
253        else:
254            df_current = st.session_state["df"]
255            new_col_idx = st.session_state["new_col_idx"]
256            col_name = st.session_state["new_col_name"]
257
258            df_current.insert(new_col_idx, col_name, "")
259            st.session_state["df"] = df_current
260            st.experimental_rerun()
261
262    st.markdown("---")
263
264    # — Step 4: Download as CSV with PDF name ────────────────────────────────
265    st.write("### 💾 Download CSV")
266
267    original_pdf_name = uploaded_file.name
268    base_name = os.path.splitext(original_pdf_name)[0]
269    csv_filename = f"{base_name}.csv"
270
271    final_df = st.session_state["df"]
272    csv_data = final_df.to_csv(index=False).encode("utf-8")
273
274    st.download_button(
275        label="Download TOC as CSV",
276        data=csv_data,
277        file_name=csv_filename,
278        mime="text/csv",
279    )
280