McNeilAlex commited on
Commit
968fcad
Β·
verified Β·
1 Parent(s): 0f24ed4

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. Dockerfile +10 -0
  2. README.md +14 -4
  3. __pycache__/detect.cpython-313.pyc +0 -0
  4. app.py +29 -0
  5. detect.py +133 -0
  6. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+
7
+ COPY . .
8
+
9
+ EXPOSE 7860
10
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,20 @@
1
  ---
2
- title: Pdf Detect Api
3
- emoji: πŸ’»
4
  colorFrom: blue
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PDF Field Detector
3
+ emoji: πŸ“‹
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # PDF Field Detector
11
+
12
+ FastAPI service that detects form fields in PDFs.
13
+
14
+ - **AcroForm PDFs**: extracts native widget positions
15
+ - **Flat vector PDFs**: mines drawing layer for checkboxes, underlines, rectangles
16
+ - **Scanned/image PDFs**: returns `needs_ml` signal for FFDNet fallback
17
+
18
+ ## API
19
+
20
+ `POST /detect` β€” multipart form upload, returns JSON with field boxes as fractions 0..1.
__pycache__/detect.cpython-313.pyc ADDED
Binary file (5.91 kB). View file
 
app.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from detect import detect_pdf
4
+
5
+ app = FastAPI(title="PDF Field Detector")
6
+
7
+ app.add_middleware(
8
+ CORSMiddleware,
9
+ allow_origins=["*"],
10
+ allow_methods=["POST", "GET"],
11
+ allow_headers=["*"],
12
+ )
13
+
14
+ @app.get("/health")
15
+ def health():
16
+ return {"ok": True}
17
+
18
+ @app.post("/detect")
19
+ async def detect(file: UploadFile = File(...)):
20
+ if not file.filename.lower().endswith(".pdf"):
21
+ raise HTTPException(400, "Only PDF files accepted")
22
+ pdf_bytes = await file.read()
23
+ if len(pdf_bytes) > 50 * 1024 * 1024:
24
+ raise HTTPException(413, "PDF too large (max 50MB)")
25
+ try:
26
+ pages = detect_pdf(pdf_bytes)
27
+ return {"pages": pages}
28
+ except Exception as e:
29
+ raise HTTPException(500, str(e))
detect.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core PDF field detection logic.
3
+ Handles three cases:
4
+ 1. AcroForm PDF β†’ extract native widgets
5
+ 2. Flat vector PDF β†’ extract from drawings layer (rectangles, lines)
6
+ 3. Image/scanned PDF β†’ return 'needs_ml' signal for FFDNet fallback
7
+ """
8
+ import fitz
9
+ from typing import Literal
10
+
11
+
12
+ FieldType = Literal['checkbox', 'text', 'signature']
13
+
14
+
15
+ def _label_near(x0, y0, x1, y1, text_spans):
16
+ """Find the nearest text label above or left of a rect."""
17
+ best, best_dist = '', 1e9
18
+ for span in text_spans:
19
+ sx0, sy0, sx1, sy1 = span['bbox']
20
+ # Candidate: text ends to the left, or is above (within 20pt)
21
+ if sx1 <= x0 + 5 and abs((sy0 + sy1) / 2 - (y0 + y1) / 2) < 20:
22
+ dist = x0 - sx1
23
+ if 0 <= dist < best_dist:
24
+ best, best_dist = span['text'], dist
25
+ elif sy1 <= y0 + 2 and sx0 >= x0 - 5 and sx1 <= x1 + 5:
26
+ dist = y0 - sy1
27
+ if 0 <= dist < best_dist:
28
+ best, best_dist = span['text'], dist
29
+ return best.strip()
30
+
31
+
32
+ def detect_page(page) -> dict:
33
+ pw, ph = page.rect.width, page.rect.height
34
+ widgets = list(page.widgets())
35
+ drawings = page.get_drawings()
36
+ images = page.get_images(full=False)
37
+
38
+ # ── Case 1: AcroForm ────────────────────────────────────────────────────
39
+ if widgets:
40
+ boxes = []
41
+ for w in widgets:
42
+ r = w.rect
43
+ ftype: FieldType
44
+ if w.field_type in (fitz.PDF_WIDGET_TYPE_CHECKBOX, fitz.PDF_WIDGET_TYPE_RADIOBUTTON):
45
+ ftype = 'checkbox'
46
+ elif w.field_type == fitz.PDF_WIDGET_TYPE_SIGNATURE:
47
+ ftype = 'signature'
48
+ else:
49
+ ftype = 'text'
50
+ boxes.append({
51
+ 'type': ftype,
52
+ 'x': r.x0 / pw, 'y': (ph - r.y1) / ph,
53
+ 'w': r.width / pw, 'h': r.height / ph,
54
+ 'label': w.field_name or '',
55
+ 'source': 'acroform',
56
+ })
57
+ return {'source': 'acroform', 'boxes': boxes}
58
+
59
+ # ── Case 3: Image/scanned β€” no vector data to mine ────────────────────
60
+ if not drawings:
61
+ return {'source': 'needs_ml', 'boxes': []}
62
+
63
+ # ── Case 2: Flat vector PDF ────────────────────────────────────────────
64
+ text_spans = []
65
+ for block in page.get_text('dict', flags=fitz.TEXT_INHIBIT_SPACES).get('blocks', []):
66
+ for line in block.get('lines', []):
67
+ for span in line.get('spans', []):
68
+ t = span.get('text', '').strip()
69
+ if t:
70
+ text_spans.append({'text': t, 'bbox': span['bbox']})
71
+
72
+ boxes = []
73
+ seen = set()
74
+
75
+ for d in drawings:
76
+ r = d['rect']
77
+ w, h = r.width, r.height
78
+ if w < 1 or h < 1:
79
+ continue
80
+
81
+ key = (round(r.x0), round(r.y0))
82
+ if key in seen:
83
+ continue
84
+ seen.add(key)
85
+
86
+ x_frac = r.x0 / pw
87
+ y_frac = r.y0 / ph
88
+ w_frac = w / pw
89
+ h_frac = h / ph
90
+
91
+ # Small square β†’ checkbox / radio
92
+ if abs(w - h) < w * 0.35 and 3 < w < 22:
93
+ label = _label_near(r.x0, r.y0, r.x1, r.y1, text_spans)
94
+ boxes.append({
95
+ 'type': 'checkbox', 'source': 'vector',
96
+ 'x': x_frac, 'y': y_frac, 'w': w_frac, 'h': h_frac,
97
+ 'label': label,
98
+ })
99
+
100
+ # Thin horizontal line β†’ text underline input
101
+ elif h < 2.5 and w > 20:
102
+ label = _label_near(r.x0, r.y0, r.x1, r.y1, text_spans)
103
+ pad = min(14 / ph, 0.02)
104
+ boxes.append({
105
+ 'type': 'text', 'source': 'vector_line',
106
+ 'x': x_frac, 'y': max(0, y_frac - pad),
107
+ 'w': w_frac, 'h': pad + h_frac + 1 / ph,
108
+ 'label': label,
109
+ })
110
+
111
+ # Rectangular box wider than tall β†’ text input field
112
+ elif w > h * 1.2 and h > 6 and w < pw * 0.95:
113
+ label = _label_near(r.x0, r.y0, r.x1, r.y1, text_spans)
114
+ boxes.append({
115
+ 'type': 'text', 'source': 'vector_rect',
116
+ 'x': x_frac, 'y': y_frac, 'w': w_frac, 'h': h_frac,
117
+ 'label': label,
118
+ })
119
+
120
+ return {'source': 'vector', 'boxes': boxes}
121
+
122
+
123
+ def detect_pdf(pdf_bytes: bytes) -> list[dict]:
124
+ doc = fitz.open(stream=pdf_bytes, filetype='pdf')
125
+ pages = []
126
+ for page_num, page in enumerate(doc):
127
+ result = detect_page(page)
128
+ result['page'] = page_num
129
+ result['width'] = page.rect.width
130
+ result['height'] = page.rect.height
131
+ pages.append(result)
132
+ doc.close()
133
+ return pages
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
3
+ python-multipart==0.0.9
4
+ pymupdf==1.24.10