pdftext 0.2.0__tar.gz → 0.3.0__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.2.0 → pdftext-0.3.0}/PKG-INFO +9 -7
- {pdftext-0.2.0 → pdftext-0.3.0}/README.md +8 -6
- {pdftext-0.2.0 → pdftext-0.3.0}/extract_text.py +2 -1
- pdftext-0.3.0/models/dt.joblib +0 -0
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/extraction.py +11 -9
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/inference.py +21 -7
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/pdf/chars.py +1 -1
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/pdf/utils.py +2 -12
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/postprocessing.py +47 -5
- {pdftext-0.2.0 → pdftext-0.3.0}/pyproject.toml +1 -1
- pdftext-0.2.0/models/dt.joblib +0 -0
- {pdftext-0.2.0 → pdftext-0.3.0}/LICENSE +0 -0
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/model.py +0 -0
- {pdftext-0.2.0 → pdftext-0.3.0}/pdftext/settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pdftext
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Extract structured text from pdfs quickly
|
|
5
5
|
Home-page: https://github.com/VikParuchuri/pdftext
|
|
6
6
|
License: Apache-2.0
|
|
@@ -48,6 +48,7 @@ pdftext PDF_PATH --out_path output.txt
|
|
|
48
48
|
- `PDF_PATH` must be a single pdf file.
|
|
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
|
+
- `--keep_hyphens` will keep hyphens in the output (they will be stripped and words joined otherwise)
|
|
51
52
|
|
|
52
53
|
## JSON
|
|
53
54
|
|
|
@@ -71,13 +72,14 @@ The output will be a json list, with each item in the list corresponding to a si
|
|
|
71
72
|
- `bbox` - the block bbox, in `[x1, y1, x2, y2]` format
|
|
72
73
|
- `lines` - the lines inside the block
|
|
73
74
|
- `bbox` - the line bbox, in `[x1, y1, x2, y2]` format
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `rotation` - how much the
|
|
77
|
-
- `bbox` - the
|
|
78
|
-
- `
|
|
75
|
+
- `spans` - the individual text spans in the line (text spans have the same font/weight/etc)
|
|
76
|
+
- `text` - the text in the span, encoded in utf-8
|
|
77
|
+
- `rotation` - how much the span is rotated, in degrees
|
|
78
|
+
- `bbox` - the span bbox, in `[x1, y1, x2, y2]` format
|
|
79
|
+
- `char_start_idx` - the start index of the first span character in the pdf
|
|
80
|
+
- `char_end_idx` - the end index of the last span character in the pdf
|
|
79
81
|
- `font` this is font info straight from the pdf, see [this pdfium code](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/public/fpdf_text.h)
|
|
80
|
-
- `size` - the size of the font used for the
|
|
82
|
+
- `size` - the size of the font used for the text
|
|
81
83
|
- `weight` - font weight
|
|
82
84
|
- `name` - font name, may be None
|
|
83
85
|
- `flags` - font flags, in the format of the `PDF spec 1.7 Section 5.7.1 Font Descriptor Flags`
|
|
@@ -25,6 +25,7 @@ pdftext PDF_PATH --out_path output.txt
|
|
|
25
25
|
- `PDF_PATH` must be a single pdf file.
|
|
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
|
+
- `--keep_hyphens` will keep hyphens in the output (they will be stripped and words joined otherwise)
|
|
28
29
|
|
|
29
30
|
## JSON
|
|
30
31
|
|
|
@@ -48,13 +49,14 @@ The output will be a json list, with each item in the list corresponding to a si
|
|
|
48
49
|
- `bbox` - the block bbox, in `[x1, y1, x2, y2]` format
|
|
49
50
|
- `lines` - the lines inside the block
|
|
50
51
|
- `bbox` - the line bbox, in `[x1, y1, x2, y2]` format
|
|
51
|
-
- `
|
|
52
|
-
- `
|
|
53
|
-
- `rotation` - how much the
|
|
54
|
-
- `bbox` - the
|
|
55
|
-
- `
|
|
52
|
+
- `spans` - the individual text spans in the line (text spans have the same font/weight/etc)
|
|
53
|
+
- `text` - the text in the span, encoded in utf-8
|
|
54
|
+
- `rotation` - how much the span is rotated, in degrees
|
|
55
|
+
- `bbox` - the span bbox, in `[x1, y1, x2, y2]` format
|
|
56
|
+
- `char_start_idx` - the start index of the first span character in the pdf
|
|
57
|
+
- `char_end_idx` - the end index of the last span character in the pdf
|
|
56
58
|
- `font` this is font info straight from the pdf, see [this pdfium code](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/public/fpdf_text.h)
|
|
57
|
-
- `size` - the size of the font used for the
|
|
59
|
+
- `size` - the size of the font used for the text
|
|
58
60
|
- `weight` - font weight
|
|
59
61
|
- `name` - font name, may be None
|
|
60
62
|
- `flags` - font flags, in the format of the `PDF spec 1.7 Section 5.7.1 Font Descriptor Flags`
|
|
@@ -10,12 +10,13 @@ def main():
|
|
|
10
10
|
parser.add_argument("--out_path", type=str, help="Path to the output text file, defaults to stdout", default=None)
|
|
11
11
|
parser.add_argument("--output_type", type=str, help="Type of output to generate", default="plain_text")
|
|
12
12
|
parser.add_argument("--sort", action="store_true", help="Attempt to sort the text by reading order", default=False)
|
|
13
|
+
parser.add_argument("--keep_hyphens", action="store_true", help="Keep hyphens in words", default=False)
|
|
13
14
|
args = parser.parse_args()
|
|
14
15
|
|
|
15
16
|
assert args.output_type in ["plain_text", "json"], "Invalid output type, must be 'plain_text' or 'json'"
|
|
16
17
|
|
|
17
18
|
if args.output_type == "plain_text":
|
|
18
|
-
text = plain_text_output(args.pdf_path, sort=args.sort)
|
|
19
|
+
text = plain_text_output(args.pdf_path, sort=args.sort, hyphens=args.keep_hyphens)
|
|
19
20
|
elif args.output_type == "json":
|
|
20
21
|
text = dictionary_output(args.pdf_path, sort=args.sort)
|
|
21
22
|
text = json.dumps(text)
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ from pdftext.inference import inference
|
|
|
4
4
|
from pdftext.model import get_model
|
|
5
5
|
from pdftext.pdf.chars import get_pdfium_chars
|
|
6
6
|
from pdftext.pdf.utils import unnormalize_bbox
|
|
7
|
-
from pdftext.postprocessing import merge_text, sort_blocks, postprocess_text
|
|
7
|
+
from pdftext.postprocessing import merge_text, sort_blocks, postprocess_text, handle_hyphens
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _get_pages(pdf_path, model=None):
|
|
@@ -15,16 +15,16 @@ def _get_pages(pdf_path, model=None):
|
|
|
15
15
|
return pages
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def plain_text_output(pdf_path, sort=False, model=None) -> str:
|
|
19
|
-
text = paginated_plain_text_output(pdf_path, sort=sort, model=model)
|
|
18
|
+
def plain_text_output(pdf_path, sort=False, model=None, hyphens=False) -> str:
|
|
19
|
+
text = paginated_plain_text_output(pdf_path, sort=sort, model=model, hyphens=hyphens)
|
|
20
20
|
return "\n".join(text)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def paginated_plain_text_output(pdf_path, sort=False, model=None) -> List[str]:
|
|
23
|
+
def paginated_plain_text_output(pdf_path, sort=False, model=None, hyphens=False) -> List[str]:
|
|
24
24
|
pages = _get_pages(pdf_path, model)
|
|
25
25
|
text = []
|
|
26
26
|
for page in pages:
|
|
27
|
-
text.append(merge_text(page, sort=sort).strip())
|
|
27
|
+
text.append(merge_text(page, sort=sort, hyphens=hyphens).strip())
|
|
28
28
|
return text
|
|
29
29
|
|
|
30
30
|
|
|
@@ -36,12 +36,14 @@ def dictionary_output(pdf_path, sort=False, model=None):
|
|
|
36
36
|
for key in bad_keys:
|
|
37
37
|
del block[key]
|
|
38
38
|
for line in block["lines"]:
|
|
39
|
-
bad_keys = [key for key in line.keys() if key not in ["
|
|
39
|
+
bad_keys = [key for key in line.keys() if key not in ["bbox", "spans"]]
|
|
40
40
|
for key in bad_keys:
|
|
41
41
|
del line[key]
|
|
42
|
-
for
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
for span in line["spans"]:
|
|
43
|
+
span["bbox"] = unnormalize_bbox(span["bbox"], page["width"], page["height"])
|
|
44
|
+
span["text"] = postprocess_text(span["text"])
|
|
45
|
+
span["text"] = handle_hyphens(span["text"], keep_hyphens=True)
|
|
46
|
+
|
|
45
47
|
line["bbox"] = unnormalize_bbox(line["bbox"], page["width"], page["height"])
|
|
46
48
|
block["bbox"] = unnormalize_bbox(block["bbox"], page["width"], page["height"])
|
|
47
49
|
if sort:
|
|
@@ -68,14 +68,25 @@ def create_training_row(char_info, prev_char, currblock, currline):
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def update_span(line, span):
|
|
71
|
+
if len(span["chars"]) > 0:
|
|
72
|
+
span["font"] = span["chars"][0]["font"]
|
|
73
|
+
span["rotation"] = span["chars"][0]["rotation"]
|
|
74
|
+
char_bboxes = [char["bbox"] for char in span["chars"]]
|
|
75
|
+
span["bbox"] = [min([bbox[0] for bbox in char_bboxes]),
|
|
76
|
+
min([bbox[1] for bbox in char_bboxes]),
|
|
77
|
+
max([bbox[2] for bbox in char_bboxes]),
|
|
78
|
+
max([bbox[3] for bbox in char_bboxes])]
|
|
79
|
+
span["text"] = "".join([char["char"] for char in span["chars"]])
|
|
80
|
+
span["char_start_idx"] = span["chars"][0]["char_idx"]
|
|
81
|
+
span["char_end_idx"] = span["chars"][-1]["char_idx"]
|
|
82
|
+
|
|
83
|
+
del span["chars"]
|
|
71
84
|
line["spans"].append(span)
|
|
72
85
|
span = {"chars": []}
|
|
73
86
|
return span
|
|
74
87
|
|
|
75
88
|
|
|
76
89
|
def update_line(block, line):
|
|
77
|
-
line["chars"] = list(chain.from_iterable(s["chars"] for s in line["spans"]))
|
|
78
|
-
del line["spans"]
|
|
79
90
|
block["lines"].append(line)
|
|
80
91
|
line = {"spans": []}
|
|
81
92
|
return line
|
|
@@ -89,6 +100,7 @@ def update_block(blocks, block):
|
|
|
89
100
|
|
|
90
101
|
def infer_single_page(text_chars):
|
|
91
102
|
prev_char = None
|
|
103
|
+
prev_font_info = None
|
|
92
104
|
|
|
93
105
|
blocks = {
|
|
94
106
|
"blocks": [],
|
|
@@ -102,6 +114,7 @@ def infer_single_page(text_chars):
|
|
|
102
114
|
line = {"spans": []}
|
|
103
115
|
span = {"chars": []}
|
|
104
116
|
for i, char_info in enumerate(text_chars["chars"]):
|
|
117
|
+
font_info = f"{char_info['font']['name']}_{char_info['font']['size']}_{char_info['font']['weight']}_{char_info['font']['flags']}_{char_info['rotation']}"
|
|
105
118
|
if prev_char:
|
|
106
119
|
training_row = create_training_row(char_info, prev_char, block, line)
|
|
107
120
|
sorted_keys = sorted(training_row.keys())
|
|
@@ -110,21 +123,22 @@ def infer_single_page(text_chars):
|
|
|
110
123
|
prediction = yield training_row
|
|
111
124
|
if prediction == 0:
|
|
112
125
|
pass
|
|
113
|
-
elif prediction ==
|
|
114
|
-
span = update_span(line, span)
|
|
115
|
-
elif prediction == 2:
|
|
126
|
+
elif prediction == 2 and prev_char["char"] in LINE_BREAKS:
|
|
116
127
|
span = update_span(line, span)
|
|
117
128
|
line = update_line(block, line)
|
|
118
|
-
|
|
129
|
+
block = update_block(blocks, block)
|
|
130
|
+
elif prev_char["char"] in LINE_BREAKS and prediction == 1: # Look for newline character as a forcing signal for a new line
|
|
119
131
|
span = update_span(line, span)
|
|
120
132
|
line = update_line(block, line)
|
|
121
|
-
|
|
133
|
+
elif prev_font_info != font_info:
|
|
134
|
+
span = update_span(line, span)
|
|
122
135
|
|
|
123
136
|
span["chars"].append(char_info)
|
|
124
137
|
update_current(line, char_info)
|
|
125
138
|
update_current(block, char_info)
|
|
126
139
|
|
|
127
140
|
prev_char = char_info
|
|
141
|
+
prev_font_info = font_info
|
|
128
142
|
if len(span["chars"]) > 0:
|
|
129
143
|
update_span(line, span)
|
|
130
144
|
if len(line["spans"]) > 0:
|
|
@@ -74,7 +74,7 @@ def get_pdfium_chars(pdf_path, fontname_sample_freq=settings.FONTNAME_SAMPLE_FRE
|
|
|
74
74
|
|
|
75
75
|
rotation = pdfium_c.FPDFText_GetCharAngle(text_page, i)
|
|
76
76
|
rotation = rotation * 180 / math.pi # convert from radians to degrees
|
|
77
|
-
coords = text_page.get_charbox(i, loose=
|
|
77
|
+
coords = text_page.get_charbox(i, loose=True)
|
|
78
78
|
device_coords = page_bbox_to_device_bbox(page, coords, page_width, page_height, bl_origin, page_rotation, normalize=True)
|
|
79
79
|
|
|
80
80
|
char_info = {
|
|
@@ -4,20 +4,10 @@ import math
|
|
|
4
4
|
|
|
5
5
|
from pdftext.settings import settings
|
|
6
6
|
|
|
7
|
-
LINE_BREAKS = ["\n", "\u000D", "\u000A"
|
|
8
|
-
TABS = ["\t", "\u0009"]
|
|
7
|
+
LINE_BREAKS = ["\n", "\u000D", "\u000A"]
|
|
8
|
+
TABS = ["\t", "\u0009", "\x09"]
|
|
9
9
|
SPACES = [" ", "\ufffe", "\uFEFF", "\xa0"]
|
|
10
|
-
HYPHEN = "-"
|
|
11
10
|
WHITESPACE_CHARS = ["\n", "\r", "\f", "\t", " "]
|
|
12
|
-
LIGATURES = {
|
|
13
|
-
"ff": "ff",
|
|
14
|
-
"ffi": "ffi",
|
|
15
|
-
"ffl": "ffl",
|
|
16
|
-
"fi": "fi",
|
|
17
|
-
"fl": "fl",
|
|
18
|
-
"st": "st",
|
|
19
|
-
"ſt": "st",
|
|
20
|
-
}
|
|
21
11
|
|
|
22
12
|
|
|
23
13
|
def char_count(textpage, *rect):
|
|
@@ -1,16 +1,57 @@
|
|
|
1
1
|
from typing import List, Dict
|
|
2
2
|
import unicodedata
|
|
3
3
|
|
|
4
|
-
from pdftext.pdf.utils import SPACES, LINE_BREAKS, TABS, WHITESPACE_CHARS
|
|
4
|
+
from pdftext.pdf.utils import SPACES, LINE_BREAKS, TABS, WHITESPACE_CHARS
|
|
5
|
+
|
|
6
|
+
LIGATURES = {
|
|
7
|
+
"ff": "ff",
|
|
8
|
+
"ffi": "ffi",
|
|
9
|
+
"ffl": "ffl",
|
|
10
|
+
"fi": "fi",
|
|
11
|
+
"fl": "fl",
|
|
12
|
+
"st": "st",
|
|
13
|
+
"ſt": "st",
|
|
14
|
+
}
|
|
15
|
+
HYPHEN_CHAR = "\x02"
|
|
16
|
+
REPLACEMENTS = {
|
|
17
|
+
"\r\n": "\n",
|
|
18
|
+
}
|
|
5
19
|
|
|
6
20
|
|
|
7
21
|
def postprocess_text(text: str) -> str:
|
|
22
|
+
for old, new in REPLACEMENTS.items():
|
|
23
|
+
text = text.replace(old, new)
|
|
8
24
|
text = replace_special_chars(text)
|
|
9
25
|
text = replace_control_chars(text)
|
|
10
26
|
text = replace_ligatures(text)
|
|
11
27
|
return text
|
|
12
28
|
|
|
13
29
|
|
|
30
|
+
def handle_hyphens(text: str, keep_hyphens=False) -> str:
|
|
31
|
+
if keep_hyphens:
|
|
32
|
+
text = text.replace(HYPHEN_CHAR, "-")
|
|
33
|
+
elif len(text) == 0:
|
|
34
|
+
pass
|
|
35
|
+
else:
|
|
36
|
+
new_text = ""
|
|
37
|
+
found_hyphen = False
|
|
38
|
+
for i in range(len(text) - 1):
|
|
39
|
+
if text[i] == HYPHEN_CHAR and text[i+1] in LINE_BREAKS:
|
|
40
|
+
found_hyphen = True
|
|
41
|
+
elif found_hyphen:
|
|
42
|
+
if text[i] in LINE_BREAKS:
|
|
43
|
+
pass
|
|
44
|
+
elif text[i] in SPACES:
|
|
45
|
+
new_text = new_text.rstrip() + "\n"
|
|
46
|
+
found_hyphen = False
|
|
47
|
+
else:
|
|
48
|
+
new_text += text[i]
|
|
49
|
+
else:
|
|
50
|
+
new_text += text[i]
|
|
51
|
+
text = new_text
|
|
52
|
+
return text
|
|
53
|
+
|
|
54
|
+
|
|
14
55
|
def replace_special_chars(text: str) -> str:
|
|
15
56
|
for item in SPACES:
|
|
16
57
|
text = text.replace(item, " ")
|
|
@@ -22,7 +63,7 @@ def replace_special_chars(text: str) -> str:
|
|
|
22
63
|
|
|
23
64
|
|
|
24
65
|
def replace_control_chars(text: str) -> str:
|
|
25
|
-
return "".join(char for char in text if unicodedata.category(char)[0] != "C" or char in WHITESPACE_CHARS)
|
|
66
|
+
return "".join(char for char in text if (unicodedata.category(char)[0] != "C" or char == HYPHEN_CHAR or char in WHITESPACE_CHARS))
|
|
26
67
|
|
|
27
68
|
|
|
28
69
|
def replace_ligatures(text: str) -> str:
|
|
@@ -50,7 +91,7 @@ def sort_blocks(blocks: List, tolerance=1.25) -> List:
|
|
|
50
91
|
return sorted_page_blocks
|
|
51
92
|
|
|
52
93
|
|
|
53
|
-
def merge_text(page: Dict, sort=False) -> str:
|
|
94
|
+
def merge_text(page: Dict, sort=False, hyphens=False) -> str:
|
|
54
95
|
text = ""
|
|
55
96
|
if sort:
|
|
56
97
|
page["blocks"] = sort_blocks(page["blocks"])
|
|
@@ -59,12 +100,13 @@ def merge_text(page: Dict, sort=False) -> str:
|
|
|
59
100
|
block_text = ""
|
|
60
101
|
for line in block["lines"]:
|
|
61
102
|
line_text = ""
|
|
62
|
-
for
|
|
63
|
-
line_text +=
|
|
103
|
+
for span in line["spans"]:
|
|
104
|
+
line_text += span["text"]
|
|
64
105
|
line_text = postprocess_text(line_text)
|
|
65
106
|
line_text = line_text.rstrip() + "\n"
|
|
66
107
|
|
|
67
108
|
block_text += line_text
|
|
68
109
|
block_text = block_text.rstrip() + "\n\n"
|
|
69
110
|
text += block_text
|
|
111
|
+
text = handle_hyphens(text, keep_hyphens=hyphens)
|
|
70
112
|
return text
|
pdftext-0.2.0/models/dt.joblib
DELETED
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|