pdftext 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pdftext-0.3.0 → pdftext-0.3.2}/PKG-INFO +12 -6
- {pdftext-0.3.0 → pdftext-0.3.2}/README.md +11 -5
- {pdftext-0.3.0 → pdftext-0.3.2}/extract_text.py +12 -6
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/extraction.py +8 -8
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/inference.py +8 -6
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/pdf/chars.py +4 -3
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/postprocessing.py +2 -2
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/settings.py +5 -2
- {pdftext-0.3.0 → pdftext-0.3.2}/pyproject.toml +1 -1
- {pdftext-0.3.0 → pdftext-0.3.2}/LICENSE +0 -0
- {pdftext-0.3.0 → pdftext-0.3.2}/models/dt.joblib +0 -0
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/model.py +0 -0
- {pdftext-0.3.0 → pdftext-0.3.2}/pdftext/pdf/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pdftext
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Extract structured text from pdfs quickly
|
|
5
5
|
Home-page: https://github.com/VikParuchuri/pdftext
|
|
6
6
|
License: Apache-2.0
|
|
@@ -49,25 +49,27 @@ pdftext PDF_PATH --out_path output.txt
|
|
|
49
49
|
- `--out_path` path to the output txt file. If not specified, will write to stdout.
|
|
50
50
|
- `--sort` will attempt to sort in reading order if specified.
|
|
51
51
|
- `--keep_hyphens` will keep hyphens in the output (they will be stripped and words joined otherwise)
|
|
52
|
+
- `--pages` will specify pages (comma separated) to extract
|
|
52
53
|
|
|
53
54
|
## JSON
|
|
54
55
|
|
|
55
56
|
This command outputs structured blocks and lines with font and other information.
|
|
56
57
|
|
|
57
58
|
```shell
|
|
58
|
-
pdftext PDF_PATH --out_path output.txt --
|
|
59
|
+
pdftext PDF_PATH --out_path output.txt --json
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
- `PDF_PATH` must be a single pdf file.
|
|
62
63
|
- `--out_path` path to the output txt file. If not specified, will write to stdout.
|
|
63
|
-
- `--
|
|
64
|
+
- `--json` specifies json output
|
|
64
65
|
- `--sort` will attempt to sort in reading order if specified.
|
|
66
|
+
- `--pages` will specify pages (comma separated) to extract
|
|
65
67
|
|
|
66
68
|
The output will be a json list, with each item in the list corresponding to a single page in the input pdf (in order). Each page will include the following keys:
|
|
67
69
|
|
|
68
70
|
- `bbox` - the page bbox, in `[x1, y1, x2, y2]` format
|
|
69
71
|
- `rotation` - how much the page is rotated, in degrees (`0`, `90`, `180`, or `270`)
|
|
70
|
-
- `
|
|
72
|
+
- `page` - the index of the page
|
|
71
73
|
- `blocks` - the blocks that make up the text in the pdf. Approximately equal to a paragraph.
|
|
72
74
|
- `bbox` - the block bbox, in `[x1, y1, x2, y2]` format
|
|
73
75
|
- `lines` - the lines inside the block
|
|
@@ -91,17 +93,21 @@ If the pdf is rotated, the bboxes will be relative to the rotated page (they're
|
|
|
91
93
|
Extract plain text:
|
|
92
94
|
|
|
93
95
|
```python
|
|
96
|
+
import pypdfium2 as pdfium
|
|
94
97
|
from pdftext.extraction import plain_text_output
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
pdf = pdfium.PdfDocument(PDF_PATH)
|
|
100
|
+
text = plain_text_output(pdf, sort=False, hyphens=False, page_range=[1,2,3]) # Optional arguments explained above
|
|
97
101
|
```
|
|
98
102
|
|
|
99
103
|
Extract structured blocks and lines:
|
|
100
104
|
|
|
101
105
|
```python
|
|
106
|
+
import pypdfium2 as pdfium
|
|
102
107
|
from pdftext.extraction import dictionary_output
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
pdf = pdfium.PdfDocument(PDF_PATH)
|
|
110
|
+
text = dictionary_output(pdf, sort=False, page_range=[1,2,3]) # Optional arguments explained above
|
|
105
111
|
```
|
|
106
112
|
|
|
107
113
|
If you want more customization, check out the `pdftext.extraction._get_pages` function for a starting point to dig deeper. pdftext is a pretty thin wrapper around [pypdfium2](https://pypdfium2.readthedocs.io/en/stable/), so you might want to look at the documentation for that as well.
|
|
@@ -26,25 +26,27 @@ pdftext PDF_PATH --out_path output.txt
|
|
|
26
26
|
- `--out_path` path to the output txt file. If not specified, will write to stdout.
|
|
27
27
|
- `--sort` will attempt to sort in reading order if specified.
|
|
28
28
|
- `--keep_hyphens` will keep hyphens in the output (they will be stripped and words joined otherwise)
|
|
29
|
+
- `--pages` will specify pages (comma separated) to extract
|
|
29
30
|
|
|
30
31
|
## JSON
|
|
31
32
|
|
|
32
33
|
This command outputs structured blocks and lines with font and other information.
|
|
33
34
|
|
|
34
35
|
```shell
|
|
35
|
-
pdftext PDF_PATH --out_path output.txt --
|
|
36
|
+
pdftext PDF_PATH --out_path output.txt --json
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
- `PDF_PATH` must be a single pdf file.
|
|
39
40
|
- `--out_path` path to the output txt file. If not specified, will write to stdout.
|
|
40
|
-
- `--
|
|
41
|
+
- `--json` specifies json output
|
|
41
42
|
- `--sort` will attempt to sort in reading order if specified.
|
|
43
|
+
- `--pages` will specify pages (comma separated) to extract
|
|
42
44
|
|
|
43
45
|
The output will be a json list, with each item in the list corresponding to a single page in the input pdf (in order). Each page will include the following keys:
|
|
44
46
|
|
|
45
47
|
- `bbox` - the page bbox, in `[x1, y1, x2, y2]` format
|
|
46
48
|
- `rotation` - how much the page is rotated, in degrees (`0`, `90`, `180`, or `270`)
|
|
47
|
-
- `
|
|
49
|
+
- `page` - the index of the page
|
|
48
50
|
- `blocks` - the blocks that make up the text in the pdf. Approximately equal to a paragraph.
|
|
49
51
|
- `bbox` - the block bbox, in `[x1, y1, x2, y2]` format
|
|
50
52
|
- `lines` - the lines inside the block
|
|
@@ -68,17 +70,21 @@ If the pdf is rotated, the bboxes will be relative to the rotated page (they're
|
|
|
68
70
|
Extract plain text:
|
|
69
71
|
|
|
70
72
|
```python
|
|
73
|
+
import pypdfium2 as pdfium
|
|
71
74
|
from pdftext.extraction import plain_text_output
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
pdf = pdfium.PdfDocument(PDF_PATH)
|
|
77
|
+
text = plain_text_output(pdf, sort=False, hyphens=False, page_range=[1,2,3]) # Optional arguments explained above
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
Extract structured blocks and lines:
|
|
77
81
|
|
|
78
82
|
```python
|
|
83
|
+
import pypdfium2 as pdfium
|
|
79
84
|
from pdftext.extraction import dictionary_output
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
pdf = pdfium.PdfDocument(PDF_PATH)
|
|
87
|
+
text = dictionary_output(pdf, sort=False, page_range=[1,2,3]) # Optional arguments explained above
|
|
82
88
|
```
|
|
83
89
|
|
|
84
90
|
If you want more customization, check out the `pdftext.extraction._get_pages` function for a starting point to dig deeper. pdftext is a pretty thin wrapper around [pypdfium2](https://pypdfium2.readthedocs.io/en/stable/), so you might want to look at the documentation for that as well.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
+
import pypdfium2 as pdfium
|
|
3
4
|
|
|
4
5
|
from pdftext.extraction import plain_text_output, dictionary_output
|
|
5
6
|
|
|
@@ -8,18 +9,23 @@ def main():
|
|
|
8
9
|
parser = argparse.ArgumentParser(description="Extract plain text from PDF. Not guaranteed to be in order.")
|
|
9
10
|
parser.add_argument("pdf_path", type=str, help="Path to the PDF file")
|
|
10
11
|
parser.add_argument("--out_path", type=str, help="Path to the output text file, defaults to stdout", default=None)
|
|
11
|
-
parser.add_argument("--
|
|
12
|
+
parser.add_argument("--json", action="store_true", help="Output json instead of plain text", default=False)
|
|
12
13
|
parser.add_argument("--sort", action="store_true", help="Attempt to sort the text by reading order", default=False)
|
|
13
14
|
parser.add_argument("--keep_hyphens", action="store_true", help="Keep hyphens in words", default=False)
|
|
15
|
+
parser.add_argument("--pages", type=str, help="Comma separated pages to extract, like 1,2,3", default=None)
|
|
14
16
|
args = parser.parse_args()
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
pdf_doc = pdfium.PdfDocument(args.pdf_path)
|
|
19
|
+
pages = None
|
|
20
|
+
if args.pages is not None:
|
|
21
|
+
pages = [int(p) for p in args.pages.split(",")]
|
|
22
|
+
assert all(p <= len(pdf_doc) for p in pages), "Invalid page number(s) provided"
|
|
17
23
|
|
|
18
|
-
if args.
|
|
19
|
-
text =
|
|
20
|
-
elif args.output_type == "json":
|
|
21
|
-
text = dictionary_output(args.pdf_path, sort=args.sort)
|
|
24
|
+
if args.json:
|
|
25
|
+
text = dictionary_output(pdf_doc, sort=args.sort, page_range=pages)
|
|
22
26
|
text = json.dumps(text)
|
|
27
|
+
else:
|
|
28
|
+
text = plain_text_output(pdf_doc, sort=args.sort, hyphens=args.keep_hyphens, page_range=pages)
|
|
23
29
|
|
|
24
30
|
if args.out_path is None:
|
|
25
31
|
print(text)
|
|
@@ -7,29 +7,29 @@ from pdftext.pdf.utils import unnormalize_bbox
|
|
|
7
7
|
from pdftext.postprocessing import merge_text, sort_blocks, postprocess_text, handle_hyphens
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def _get_pages(
|
|
10
|
+
def _get_pages(pdf_doc, model=None, page_range=None):
|
|
11
11
|
if model is None:
|
|
12
12
|
model = get_model()
|
|
13
|
-
text_chars = get_pdfium_chars(
|
|
13
|
+
text_chars = get_pdfium_chars(pdf_doc, page_range=page_range)
|
|
14
14
|
pages = inference(text_chars, model)
|
|
15
15
|
return pages
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def plain_text_output(
|
|
19
|
-
text = paginated_plain_text_output(
|
|
18
|
+
def plain_text_output(pdf_doc, sort=False, model=None, hyphens=False, page_range=None) -> str:
|
|
19
|
+
text = paginated_plain_text_output(pdf_doc, sort=sort, model=model, hyphens=hyphens, page_range=page_range)
|
|
20
20
|
return "\n".join(text)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def paginated_plain_text_output(
|
|
24
|
-
pages = _get_pages(
|
|
23
|
+
def paginated_plain_text_output(pdf_doc, sort=False, model=None, hyphens=False, page_range=None) -> List[str]:
|
|
24
|
+
pages = _get_pages(pdf_doc, model, page_range)
|
|
25
25
|
text = []
|
|
26
26
|
for page in pages:
|
|
27
27
|
text.append(merge_text(page, sort=sort, hyphens=hyphens).strip())
|
|
28
28
|
return text
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def dictionary_output(
|
|
32
|
-
pages = _get_pages(
|
|
31
|
+
def dictionary_output(pdf_doc, sort=False, model=None, page_range=None):
|
|
32
|
+
pages = _get_pages(pdf_doc, model, page_range)
|
|
33
33
|
for page in pages:
|
|
34
34
|
for block in page["blocks"]:
|
|
35
35
|
bad_keys = [key for key in block.keys() if key not in ["lines", "bbox"]]
|
|
@@ -3,6 +3,7 @@ from itertools import chain
|
|
|
3
3
|
import sklearn
|
|
4
4
|
|
|
5
5
|
from pdftext.pdf.utils import LINE_BREAKS, TABS, SPACES
|
|
6
|
+
from pdftext.settings import settings
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def update_current(current, new_char):
|
|
@@ -98,7 +99,7 @@ def update_block(blocks, block):
|
|
|
98
99
|
return block
|
|
99
100
|
|
|
100
101
|
|
|
101
|
-
def infer_single_page(text_chars):
|
|
102
|
+
def infer_single_page(text_chars, block_threshold=settings.BLOCK_THRESHOLD):
|
|
102
103
|
prev_char = None
|
|
103
104
|
prev_font_info = None
|
|
104
105
|
|
|
@@ -120,14 +121,15 @@ def infer_single_page(text_chars):
|
|
|
120
121
|
sorted_keys = sorted(training_row.keys())
|
|
121
122
|
training_row = [training_row[key] for key in sorted_keys]
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
prediction_probs = yield training_row
|
|
125
|
+
# First item is probability of same line/block, second is probability of new line, third is probability of new block
|
|
126
|
+
if prediction_probs[0] >= .5:
|
|
125
127
|
pass
|
|
126
|
-
elif
|
|
128
|
+
elif prediction_probs[2] > block_threshold:
|
|
127
129
|
span = update_span(line, span)
|
|
128
130
|
line = update_line(block, line)
|
|
129
131
|
block = update_block(blocks, block)
|
|
130
|
-
elif prev_char["char"] in LINE_BREAKS and
|
|
132
|
+
elif prev_char["char"] in LINE_BREAKS and prediction_probs[1] >= .5: # Look for newline character as a forcing signal for a new line
|
|
131
133
|
span = update_span(line, span)
|
|
132
134
|
line = update_line(block, line)
|
|
133
135
|
elif prev_font_info != font_info:
|
|
@@ -180,7 +182,7 @@ def inference(text_chars, model):
|
|
|
180
182
|
|
|
181
183
|
# Disable nan, etc, validation for a small speedup
|
|
182
184
|
with sklearn.config_context(assume_finite=True):
|
|
183
|
-
predictions = model.
|
|
185
|
+
predictions = model.predict_proba(training_rows)
|
|
184
186
|
for pred, page_idx in zip(predictions, training_idxs):
|
|
185
187
|
next_prediction[page_idx] = pred
|
|
186
188
|
sorted_keys = sorted(page_blocks.keys())
|
|
@@ -23,11 +23,12 @@ def update_previous_fonts(text_chars: Dict, i: int, fontname: str, fontflags: in
|
|
|
23
23
|
text_chars["chars"][j]["font"]["flags"] = fontflags
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def get_pdfium_chars(
|
|
27
|
-
pdf = pdfium.PdfDocument(pdf_path)
|
|
26
|
+
def get_pdfium_chars(pdf, fontname_sample_freq=settings.FONTNAME_SAMPLE_FREQ, page_range=None):
|
|
28
27
|
blocks = []
|
|
28
|
+
if page_range is None:
|
|
29
|
+
page_range = range(len(pdf))
|
|
29
30
|
|
|
30
|
-
for page_idx in
|
|
31
|
+
for page_idx in page_range:
|
|
31
32
|
page = pdf.get_page(page_idx)
|
|
32
33
|
text_page = page.get_textpage()
|
|
33
34
|
mediabox = page.get_mediabox()
|
|
@@ -29,14 +29,14 @@ def postprocess_text(text: str) -> str:
|
|
|
29
29
|
|
|
30
30
|
def handle_hyphens(text: str, keep_hyphens=False) -> str:
|
|
31
31
|
if keep_hyphens:
|
|
32
|
-
text = text.replace(HYPHEN_CHAR, "
|
|
32
|
+
text = text.replace(HYPHEN_CHAR, "-\n")
|
|
33
33
|
elif len(text) == 0:
|
|
34
34
|
pass
|
|
35
35
|
else:
|
|
36
36
|
new_text = ""
|
|
37
37
|
found_hyphen = False
|
|
38
38
|
for i in range(len(text) - 1):
|
|
39
|
-
if text[i] == HYPHEN_CHAR
|
|
39
|
+
if text[i] == HYPHEN_CHAR:
|
|
40
40
|
found_hyphen = True
|
|
41
41
|
elif found_hyphen:
|
|
42
42
|
if text[i] in LINE_BREAKS:
|
|
@@ -7,10 +7,13 @@ class Settings(BaseSettings):
|
|
|
7
7
|
BASE_PATH: str = os.path.dirname(os.path.dirname(__file__))
|
|
8
8
|
MODEL_PATH: str = os.path.join(BASE_PATH, "models", "dt.joblib")
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
FONT_BUFFER_SIZE: int = 1024
|
|
10
|
+
# Fonts
|
|
11
|
+
FONT_BUFFER_SIZE: int = 1024 # How many characters to buffer when reading a font name
|
|
12
12
|
FONTNAME_SAMPLE_FREQ: int = 10
|
|
13
13
|
|
|
14
|
+
# Inference
|
|
15
|
+
BLOCK_THRESHOLD: float = 0.8 # Confidence threshold for block detection
|
|
16
|
+
|
|
14
17
|
# Benchmark
|
|
15
18
|
RESULTS_FOLDER: str = "results"
|
|
16
19
|
BENCH_DATASET_NAME: str = "vikp/pdf_bench"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|