danielhjerresen commited on
Commit
4f46bdf
·
verified ·
1 Parent(s): b5a01dd

Delete src/pdf_counter.py

Browse files
Files changed (1) hide show
  1. src/pdf_counter.py +0 -326
src/pdf_counter.py DELETED
@@ -1,326 +0,0 @@
1
- # pdf_counter.py
2
- import re
3
- from collections import Counter
4
- import fitz
5
-
6
-
7
- # ============================================================
8
- # REGEX PATTERNS
9
- # ============================================================
10
- # These patterns are used to identify page numbers and
11
- # running headers that should not be counted as content.
12
-
13
- PAGE_NUMBER_RE = re.compile(
14
- r"^\s*(side\s*)?\d+\s*(/|af|-)?\s*\d*\s*$",
15
- re.IGNORECASE,
16
- )
17
-
18
- RUNNING_HEADER_RE = re.compile(
19
- r"^\d+(\.\d+)+\.?\s+.+\s+([ivxlcdm]+|\d+)$",
20
- re.IGNORECASE,
21
- )
22
-
23
-
24
- # ============================================================
25
- # TEXT NORMALIZATION
26
- # ============================================================
27
- # Cleans extracted text by replacing multiple whitespace
28
- # characters (spaces, tabs, line breaks) with a single space.
29
- # This ensures consistent comparison and character counting.
30
-
31
- def normalize(text: str) -> str:
32
- return re.sub(r"\s+", " ", text).strip()
33
-
34
-
35
- # ============================================================
36
- # PDF EXTRACTION
37
- # ============================================================
38
- # Reads the PDF and extracts all text blocks from each page.
39
- #
40
- # For every block we store:
41
- # - Page number
42
- # - Original text
43
- # - Lowercase version for comparisons
44
- # - Vertical coordinates on the page
45
- # - Page height
46
- #
47
- # The position data is later used to detect headers/footers.
48
-
49
- def extract_pages(pdf_bytes: bytes):
50
- doc = fitz.open(stream=pdf_bytes, filetype="pdf")
51
- pages = []
52
-
53
- for page_no, page in enumerate(doc, start=1):
54
- blocks = []
55
-
56
- for block in page.get_text("blocks", sort=True):
57
- x0, y0, x1, y1, text, *_ = block
58
-
59
- text = normalize(text)
60
-
61
- if text:
62
- blocks.append({
63
- "page": page_no,
64
- "text": text,
65
- "text_key": text.lower(),
66
- "y0": y0,
67
- "y1": y1,
68
- "height": page.rect.height,
69
- })
70
-
71
- pages.append(blocks)
72
-
73
- return pages
74
-
75
-
76
- # ============================================================
77
- # PAGE NUMBER DETECTION
78
- # ============================================================
79
- # Checks whether a text block looks like a page number.
80
-
81
- def is_page_number(text: str) -> bool:
82
- return PAGE_NUMBER_RE.match(text) is not None
83
-
84
-
85
- # ============================================================
86
- # PAGE POSITION HELPERS
87
- # ============================================================
88
- # Determines whether a text block is located near the top
89
- # or bottom of the page.
90
- #
91
- # Top area = top 15%
92
- # Bottom area = bottom 15%
93
- #
94
- # These areas are where headers and footers are expected.
95
-
96
- def is_top_area(block: dict) -> bool:
97
- return block["y1"] <= block["height"] * 0.15
98
-
99
-
100
- def is_bottom_area(block: dict) -> bool:
101
- return block["y0"] >= block["height"] * 0.85
102
-
103
-
104
- # ============================================================
105
- # RUNNING HEADER DETECTION
106
- # ============================================================
107
- # Identifies chapter-style running headers such as:
108
- #
109
- # 2.1 Methods 12
110
- # 4.3 Results iv
111
- #
112
- # They typically appear near the top of each page and
113
- # follow a numbering pattern.
114
- #
115
- # "Chapter X" headings are excluded because they are often
116
- # actual content rather than page headers.
117
-
118
- def is_running_header(block: dict) -> bool:
119
- text = block["text"]
120
-
121
- if text.lower().startswith("chapter "):
122
- return False
123
-
124
- return is_top_area(block) and RUNNING_HEADER_RE.match(text) is not None
125
-
126
-
127
- # ============================================================
128
- # HEADER / FOOTER DETECTION
129
- # ============================================================
130
- # Finds text that appears repeatedly in the top or bottom
131
- # regions of many pages.
132
- #
133
- # Repeated top text -> header candidate
134
- # Repeated bottom text -> footer candidate
135
- #
136
- # A text must appear on at least min_ratio of pages before
137
- # it is classified as a header/footer.
138
- #
139
- # Default: 50% of pages.
140
-
141
- def detect_headers_and_footers(pages, min_ratio=0.5):
142
- header_counter = Counter()
143
- footer_counter = Counter()
144
-
145
- running_headers = set()
146
- page_numbers = set()
147
-
148
- for blocks in pages:
149
- headers_seen = set()
150
- footers_seen = set()
151
-
152
- for block in blocks:
153
- text = block["text"]
154
- text_key = block["text_key"]
155
-
156
- # Collect page numbers separately
157
- if is_page_number(text):
158
- page_numbers.add(text)
159
- continue
160
-
161
- # Collect running headers separately
162
- if is_running_header(block):
163
- running_headers.add(text)
164
- continue
165
-
166
- # Potential header candidate
167
- if is_top_area(block):
168
- headers_seen.add(text_key)
169
-
170
- # Potential footer candidate
171
- if is_bottom_area(block):
172
- footers_seen.add(text_key)
173
-
174
- # Count once per page
175
- header_counter.update(headers_seen)
176
- footer_counter.update(footers_seen)
177
-
178
- min_count = max(2, int(len(pages) * min_ratio))
179
-
180
- detected_headers = {
181
- text for text, count in header_counter.items()
182
- if count >= min_count
183
- }
184
-
185
- detected_footers = {
186
- text for text, count in footer_counter.items()
187
- if count >= min_count
188
- }
189
-
190
- return (
191
- detected_headers,
192
- detected_footers,
193
- running_headers,
194
- page_numbers,
195
- )
196
-
197
-
198
- # ============================================================
199
- # CHARACTER COUNTING ENGINE
200
- # ============================================================
201
- # Main workflow:
202
- #
203
- # 1. Extract all text blocks from the PDF.
204
- # 2. Detect repeated headers and footers.
205
- # 3. Detect page numbers.
206
- # 4. Remove unwanted elements.
207
- # 5. Count characters in remaining content.
208
- # 6. Return detailed results and diagnostics.
209
-
210
- def count_characters(
211
- pdf_bytes: bytes,
212
- excluded_pages: set[int] | None = None,
213
- remove_headers: bool = True,
214
- remove_footers: bool = True,
215
- remove_page_numbers: bool = True,
216
- ):
217
- excluded_pages = excluded_pages or set()
218
-
219
- # Extract all page data
220
- pages = extract_pages(pdf_bytes)
221
-
222
- # Detect recurring elements
223
- (
224
- detected_headers,
225
- detected_footers,
226
- running_headers,
227
- detected_page_numbers,
228
- ) = detect_headers_and_footers(pages)
229
-
230
- included_text_parts = []
231
- page_results = []
232
- removed_items = []
233
-
234
- # Process each page individually
235
- for page_no, blocks in enumerate(pages, start=1):
236
-
237
- # Skip pages excluded by the user
238
- if page_no in excluded_pages:
239
- page_results.append({
240
- "Side": page_no,
241
- "Tegn": 0,
242
- "Status": "Fravalgt",
243
- })
244
- continue
245
-
246
- kept_text = []
247
-
248
- # Evaluate every text block
249
- for block in blocks:
250
- text = block["text"]
251
- text_key = block["text_key"]
252
-
253
- # Remove page numbers
254
- if remove_page_numbers and is_page_number(text):
255
- removed_items.append({
256
- "Side": page_no,
257
- "Type": "Sidetal",
258
- "Tekst": text,
259
- })
260
- continue
261
-
262
- # Remove repeated headers
263
- if remove_headers and text_key in detected_headers:
264
- removed_items.append({
265
- "Side": page_no,
266
- "Type": "Sidehoved",
267
- "Tekst": text,
268
- })
269
- continue
270
-
271
- # Remove running chapter headers
272
- if remove_headers and is_running_header(block):
273
- removed_items.append({
274
- "Side": page_no,
275
- "Type": "Løbende sidehoved",
276
- "Tekst": text,
277
- })
278
- continue
279
-
280
- # Remove repeated footers
281
- if remove_footers and text_key in detected_footers:
282
- removed_items.append({
283
- "Side": page_no,
284
- "Type": "Sidefod",
285
- "Tekst": text,
286
- })
287
- continue
288
-
289
- # Keep everything else
290
- kept_text.append(text)
291
-
292
- # Combine all remaining text on the page
293
- page_text = " ".join(kept_text)
294
-
295
- included_text_parts.append(page_text)
296
-
297
- # Store page statistics
298
- page_results.append({
299
- "Side": page_no,
300
- "Tegn": len(page_text),
301
- "Status": "Talt med",
302
- })
303
-
304
- # Combine text from all included pages
305
- full_text = " ".join(
306
- t for t in included_text_parts if t
307
- )
308
-
309
- # Return complete result package
310
- return {
311
- "total_characters": len(full_text),
312
- "page_results": page_results,
313
- "included_text": full_text,
314
-
315
- # Diagnostic information
316
- "detected_headers": sorted(detected_headers),
317
- "detected_footers": sorted(detected_footers),
318
- "detected_running_headers": sorted(running_headers),
319
- "detected_page_numbers": sorted(detected_page_numbers),
320
-
321
- # Log of removed items
322
- "removed_items": removed_items,
323
-
324
- # Total pages in document
325
- "page_count": len(pages),
326
- }