File size: 6,577 Bytes
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""Readable previews of workspace files: spreadsheets as tables, documents and slides as text blocks.

Everything is capped (rows, columns, characters) so a 40 MB workbook previews as fast as a tiny one.
PDFs, images and HTML are served raw instead and shown by the browser itself.
"""

from __future__ import annotations

import csv
import io
import zipfile
from pathlib import Path

ROWS, COLS, CHARS = 60, 24, 60_000


def _cell(v) -> str:
    if v is None:
        return ""
    s = str(v)
    return s if len(s) <= 200 else s[:199] + "…"


def spreadsheet(p: Path) -> dict:
    if p.suffix.lower() == ".csv":
        with open(p, newline="", encoding="utf-8", errors="replace") as f:
            rows = [r[:COLS] for _, r in zip(range(ROWS + 1), csv.reader(f))]
        return {"type": "sheets", "sheets": [{"name": p.name, "rows": [[_cell(c) for c in r] for r in rows]}]}
    import openpyxl

    wb = openpyxl.load_workbook(p, read_only=True, data_only=True)
    # Formulas the file never had cached values for show as blanks in data_only mode; show the formula instead.
    wf = openpyxl.load_workbook(p, read_only=True, data_only=False)
    sheets = []
    for ws, wsf in zip(wb.worksheets[:12], wf.worksheets[:12]):
        rows = []
        for r, rf in zip(ws.iter_rows(max_row=ROWS, max_col=COLS, values_only=True),
                         wsf.iter_rows(max_row=ROWS, max_col=COLS, values_only=True)):
            rows.append([_cell(c if c is not None else (f if isinstance(f, str) and f.startswith("=") else None))
                         for c, f in zip(r, rf)])
        while rows and not any(rows[-1]):
            rows.pop()
        sheets.append({"name": ws.title, "rows": rows, "dims": ws.max_row and f"{ws.max_row} × {ws.max_column}"})
    wb.close()
    wf.close()
    return {"type": "sheets", "sheets": sheets}


def document(p: Path) -> dict:
    import docx

    d = docx.Document(p)
    blocks, n = [], 0
    body = d.element.body
    for child in body.iterchildren():
        tag = child.tag.rsplit("}", 1)[-1]
        if tag == "p":
            para = docx.text.paragraph.Paragraph(child, d)
            t = para.text.strip()
            if not t:
                continue
            style = (para.style.name or "").lower() if para.style is not None else ""
            blocks.append({"h": 1 if "heading 1" in style or style == "title" else 2 if "heading" in style else 0, "text": t})
            n += len(t)
        elif tag == "tbl":
            tbl = docx.table.Table(child, d)
            rows = [[_cell(c.text.strip()) for c in r.cells][:COLS] for r in tbl.rows[:ROWS]]
            blocks.append({"table": rows})
            n += sum(len(c) for r in rows for c in r)
        if n > CHARS:
            blocks.append({"h": 0, "text": "… (truncated)"})
            break
    return {"type": "document", "blocks": blocks}


def slides(p: Path) -> dict:
    """Each slide as a title plus its text, in reading order. The title is the title placeholder, or else the
    largest text on the slide. Text repeated on most slides (logos, confidentiality footers) is pulled out and
    shown once, and bare page labels ("01") are dropped, so each card shows what is different about that slide."""
    from collections import Counter

    from pptx import Presentation

    prs = Presentation(p)
    raw = []
    for i, s in enumerate(prs.slides, 1):
        tshape = s.shapes.title
        tid = tshape.shape_id if tshape is not None else None
        items, tables = [], []
        for sh in s.shapes:
            if getattr(sh, "has_table", False) and sh.has_table:
                tables.append([[c.text.strip() for c in r.cells] for r in list(sh.table.rows)[:12]])
                continue
            if not sh.has_text_frame:
                continue
            t = sh.text_frame.text.strip()
            if not t:
                continue
            sizes = [r.font.size.pt for para in sh.text_frame.paragraphs for r in para.runs if r.font.size]
            items.append({"t": t, "size": max(sizes) if sizes else 0, "top": sh.top or 0, "left": sh.left or 0,
                          "title": sh.shape_id == tid})
        items.sort(key=lambda x: (x["top"], x["left"]))
        raw.append((i, items, tables))
    counts = Counter(x["t"] for _, items, _ in raw for x in {x["t"]: x for x in items}.values())
    common = {t for t, c in counts.items() if len(raw) >= 3 and c >= max(3, 0.4 * len(raw))}
    label = lambda t: len(t) <= 3 and (t.isdigit() or t.isupper())   # "01", "ADC"
    out = []
    for i, items, tables in raw:
        keep = [x for x in items if x["t"] not in common and not label(x["t"])]
        pick = next((x for x in keep if x["title"]), None) or max(
            (x for x in keep if len(x["t"]) >= 4), key=lambda x: (x["size"], -x["top"]), default=None)
        title = pick["t"].split("\n")[0] if pick else ""
        texts = [x["t"] for x in keep if x is not pick]
        if pick and "\n" in pick["t"]:
            texts.insert(0, pick["t"].split("\n", 1)[1])
        out.append({"n": i, "title": title, "text": texts[:30], "tables": tables})
    return {"type": "slides", "slides": out[:60], "repeated": sorted(common, key=len)[:6]}


def pdf_text(p: Path) -> dict:
    from pypdf import PdfReader

    r = PdfReader(p)
    pages = []
    total = 0
    for i, page in enumerate(r.pages[:12], 1):
        t = (page.extract_text() or "").strip()
        pages.append({"n": i, "text": t[:6000]})
        total += len(t)
        if total > CHARS:
            break
    return {"type": "pdf", "pages": pages, "page_count": len(r.pages)}


def text(p: Path) -> dict:
    return {"type": "text", "text": p.read_text(encoding="utf-8", errors="replace")[:CHARS]}


def archive(p: Path) -> dict:
    with zipfile.ZipFile(p) as z:
        return {"type": "archive", "entries": [{"name": i.filename, "size": i.file_size} for i in z.infolist()[:300]]}


def preview(p: Path) -> dict:
    ext = p.suffix.lower().lstrip(".")
    try:
        if ext in ("xlsx", "xlsm", "csv"):
            return spreadsheet(p)
        if ext == "docx":
            return document(p)
        if ext == "pptx":
            return slides(p)
        if ext == "pdf":
            return pdf_text(p)
        if ext in ("md", "txt", "json", "xml", "yaml", "yml", "py", "sql", "log"):
            return text(p)
        if ext == "zip":
            return archive(p)
    except Exception as e:  # a malformed file should not break the page
        return {"type": "error", "error": f"{type(e).__name__}: {str(e)[:200]}"}
    return {"type": "raw"}