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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pdftext
3
- Version: 0.2.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
- - `chars` - the individual characters in the line
75
- - `char` - the actual character, encoded in utf-8
76
- - `rotation` - how much the character is rotated, in degrees
77
- - `bbox` - the character bbox, in `[x1, y1, x2, y2]` format
78
- - `char_idx` - the index of the character on the page (from `0` to number of characters, in original pdf order)
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 character
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
- - `chars` - the individual characters in the line
52
- - `char` - the actual character, encoded in utf-8
53
- - `rotation` - how much the character is rotated, in degrees
54
- - `bbox` - the character bbox, in `[x1, y1, x2, y2]` format
55
- - `char_idx` - the index of the character on the page (from `0` to number of characters, in original pdf order)
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 character
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 ["chars", "bbox"]]
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 char in line["chars"]:
43
- char["bbox"] = unnormalize_bbox(char["bbox"], page["width"], page["height"])
44
- char["char"] = postprocess_text(char["char"])
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 == 1:
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
- else:
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
- block = update_block(blocks, block)
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=False)
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", "\u000C"]
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, LIGATURES
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 char in line["chars"]:
63
- line_text += char["char"]
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pdftext"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Extract structured text from pdfs quickly"
5
5
  authors = ["Vik Paruchuri <vik.paruchuri@gmail.com>"]
6
6
  license = "Apache-2.0"
Binary file
File without changes
File without changes
File without changes