docling 2.16.0__py3-none-any.whl → 2.18.0__py3-none-any.whl
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.
- docling/backend/html_backend.py +21 -20
- docling/backend/md_backend.py +92 -43
- docling/backend/mspowerpoint_backend.py +39 -27
- docling/backend/msword_backend.py +172 -130
- docling/backend/xml/uspto_backend.py +25 -25
- docling/cli/main.py +18 -3
- docling/datamodel/document.py +4 -0
- docling/datamodel/pipeline_options.py +1 -0
- docling/datamodel/settings.py +16 -1
- docling/document_converter.py +12 -2
- docling/models/rapid_ocr_model.py +1 -0
- docling/models/table_structure_model.py +9 -5
- docling/models/tesseract_ocr_cli_model.py +72 -4
- docling/models/tesseract_ocr_model.py +37 -37
- docling/pipeline/base_pipeline.py +3 -1
- docling/utils/glm_utils.py +4 -0
- docling/utils/ocr_utils.py +9 -0
- {docling-2.16.0.dist-info → docling-2.18.0.dist-info}/METADATA +20 -12
- {docling-2.16.0.dist-info → docling-2.18.0.dist-info}/RECORD +22 -21
- {docling-2.16.0.dist-info → docling-2.18.0.dist-info}/WHEEL +1 -1
- {docling-2.16.0.dist-info → docling-2.18.0.dist-info}/LICENSE +0 -0
- {docling-2.16.0.dist-info → docling-2.18.0.dist-info}/entry_points.txt +0 -0
@@ -389,7 +389,7 @@ class PatentUsptoIce(PatentUspto):
|
|
389
389
|
if name == self.Element.TITLE.value:
|
390
390
|
if text:
|
391
391
|
self.parents[self.level + 1] = self.doc.add_title(
|
392
|
-
parent=self.parents[self.level],
|
392
|
+
parent=self.parents[self.level],
|
393
393
|
text=text,
|
394
394
|
)
|
395
395
|
self.level += 1
|
@@ -406,7 +406,7 @@ class PatentUsptoIce(PatentUspto):
|
|
406
406
|
abstract_item = self.doc.add_heading(
|
407
407
|
heading_text,
|
408
408
|
level=heading_level,
|
409
|
-
parent=self.parents[heading_level],
|
409
|
+
parent=self.parents[heading_level],
|
410
410
|
)
|
411
411
|
self.doc.add_text(
|
412
412
|
label=DocItemLabel.PARAGRAPH,
|
@@ -434,7 +434,7 @@ class PatentUsptoIce(PatentUspto):
|
|
434
434
|
claims_item = self.doc.add_heading(
|
435
435
|
heading_text,
|
436
436
|
level=heading_level,
|
437
|
-
parent=self.parents[heading_level],
|
437
|
+
parent=self.parents[heading_level],
|
438
438
|
)
|
439
439
|
for text in self.claims:
|
440
440
|
self.doc.add_text(
|
@@ -452,7 +452,7 @@ class PatentUsptoIce(PatentUspto):
|
|
452
452
|
self.doc.add_text(
|
453
453
|
label=DocItemLabel.PARAGRAPH,
|
454
454
|
text=text,
|
455
|
-
parent=self.parents[self.level],
|
455
|
+
parent=self.parents[self.level],
|
456
456
|
)
|
457
457
|
self.text = ""
|
458
458
|
|
@@ -460,7 +460,7 @@ class PatentUsptoIce(PatentUspto):
|
|
460
460
|
self.parents[self.level + 1] = self.doc.add_heading(
|
461
461
|
text=text,
|
462
462
|
level=self.level,
|
463
|
-
parent=self.parents[self.level],
|
463
|
+
parent=self.parents[self.level],
|
464
464
|
)
|
465
465
|
self.level += 1
|
466
466
|
self.text = ""
|
@@ -470,7 +470,7 @@ class PatentUsptoIce(PatentUspto):
|
|
470
470
|
empty_table = TableData(num_rows=0, num_cols=0, table_cells=[])
|
471
471
|
self.doc.add_table(
|
472
472
|
data=empty_table,
|
473
|
-
parent=self.parents[self.level],
|
473
|
+
parent=self.parents[self.level],
|
474
474
|
)
|
475
475
|
|
476
476
|
def _apply_style(self, text: str, style_tag: str) -> str:
|
@@ -721,7 +721,7 @@ class PatentUsptoGrantV2(PatentUspto):
|
|
721
721
|
if self.Element.TITLE.value in self.property and text.strip():
|
722
722
|
title = text.strip()
|
723
723
|
self.parents[self.level + 1] = self.doc.add_title(
|
724
|
-
parent=self.parents[self.level],
|
724
|
+
parent=self.parents[self.level],
|
725
725
|
text=title,
|
726
726
|
)
|
727
727
|
self.level += 1
|
@@ -749,7 +749,7 @@ class PatentUsptoGrantV2(PatentUspto):
|
|
749
749
|
self.parents[self.level + 1] = self.doc.add_heading(
|
750
750
|
text=text.strip(),
|
751
751
|
level=self.level,
|
752
|
-
parent=self.parents[self.level],
|
752
|
+
parent=self.parents[self.level],
|
753
753
|
)
|
754
754
|
self.level += 1
|
755
755
|
|
@@ -769,7 +769,7 @@ class PatentUsptoGrantV2(PatentUspto):
|
|
769
769
|
claims_item = self.doc.add_heading(
|
770
770
|
heading_text,
|
771
771
|
level=heading_level,
|
772
|
-
parent=self.parents[heading_level],
|
772
|
+
parent=self.parents[heading_level],
|
773
773
|
)
|
774
774
|
for text in self.claims:
|
775
775
|
self.doc.add_text(
|
@@ -787,7 +787,7 @@ class PatentUsptoGrantV2(PatentUspto):
|
|
787
787
|
abstract_item = self.doc.add_heading(
|
788
788
|
heading_text,
|
789
789
|
level=heading_level,
|
790
|
-
parent=self.parents[heading_level],
|
790
|
+
parent=self.parents[heading_level],
|
791
791
|
)
|
792
792
|
self.doc.add_text(
|
793
793
|
label=DocItemLabel.PARAGRAPH, text=abstract, parent=abstract_item
|
@@ -799,7 +799,7 @@ class PatentUsptoGrantV2(PatentUspto):
|
|
799
799
|
self.doc.add_text(
|
800
800
|
label=DocItemLabel.PARAGRAPH,
|
801
801
|
text=paragraph,
|
802
|
-
parent=self.parents[self.level],
|
802
|
+
parent=self.parents[self.level],
|
803
803
|
)
|
804
804
|
elif self.Element.CLAIM.value in self.property:
|
805
805
|
# we may need a space after a paragraph in claim text
|
@@ -811,7 +811,7 @@ class PatentUsptoGrantV2(PatentUspto):
|
|
811
811
|
empty_table = TableData(num_rows=0, num_cols=0, table_cells=[])
|
812
812
|
self.doc.add_table(
|
813
813
|
data=empty_table,
|
814
|
-
parent=self.parents[self.level],
|
814
|
+
parent=self.parents[self.level],
|
815
815
|
)
|
816
816
|
|
817
817
|
def _apply_style(self, text: str, style_tag: str) -> str:
|
@@ -938,7 +938,7 @@ class PatentUsptoGrantAps(PatentUspto):
|
|
938
938
|
self.parents[self.level + 1] = self.doc.add_heading(
|
939
939
|
heading.value,
|
940
940
|
level=self.level,
|
941
|
-
parent=self.parents[self.level],
|
941
|
+
parent=self.parents[self.level],
|
942
942
|
)
|
943
943
|
self.level += 1
|
944
944
|
|
@@ -959,7 +959,7 @@ class PatentUsptoGrantAps(PatentUspto):
|
|
959
959
|
|
960
960
|
if field == self.Field.TITLE.value:
|
961
961
|
self.parents[self.level + 1] = self.doc.add_title(
|
962
|
-
parent=self.parents[self.level], text=value
|
962
|
+
parent=self.parents[self.level], text=value
|
963
963
|
)
|
964
964
|
self.level += 1
|
965
965
|
|
@@ -971,14 +971,14 @@ class PatentUsptoGrantAps(PatentUspto):
|
|
971
971
|
self.doc.add_text(
|
972
972
|
label=DocItemLabel.PARAGRAPH,
|
973
973
|
text=value,
|
974
|
-
parent=self.parents[self.level],
|
974
|
+
parent=self.parents[self.level],
|
975
975
|
)
|
976
976
|
|
977
977
|
elif field == self.Field.NUMBER.value and section == self.Section.CLAIMS.value:
|
978
978
|
self.doc.add_text(
|
979
979
|
label=DocItemLabel.PARAGRAPH,
|
980
980
|
text="",
|
981
|
-
parent=self.parents[self.level],
|
981
|
+
parent=self.parents[self.level],
|
982
982
|
)
|
983
983
|
|
984
984
|
elif (
|
@@ -996,7 +996,7 @@ class PatentUsptoGrantAps(PatentUspto):
|
|
996
996
|
last_claim = self.doc.add_text(
|
997
997
|
label=DocItemLabel.PARAGRAPH,
|
998
998
|
text="",
|
999
|
-
parent=self.parents[self.level],
|
999
|
+
parent=self.parents[self.level],
|
1000
1000
|
)
|
1001
1001
|
|
1002
1002
|
last_claim.text += f" {value}" if last_claim.text else value
|
@@ -1012,7 +1012,7 @@ class PatentUsptoGrantAps(PatentUspto):
|
|
1012
1012
|
self.parents[self.level + 1] = self.doc.add_heading(
|
1013
1013
|
value,
|
1014
1014
|
level=self.level,
|
1015
|
-
parent=self.parents[self.level],
|
1015
|
+
parent=self.parents[self.level],
|
1016
1016
|
)
|
1017
1017
|
self.level += 1
|
1018
1018
|
|
@@ -1029,7 +1029,7 @@ class PatentUsptoGrantAps(PatentUspto):
|
|
1029
1029
|
self.doc.add_text(
|
1030
1030
|
label=DocItemLabel.PARAGRAPH,
|
1031
1031
|
text=value,
|
1032
|
-
parent=self.parents[self.level],
|
1032
|
+
parent=self.parents[self.level],
|
1033
1033
|
)
|
1034
1034
|
|
1035
1035
|
def parse(self, patent_content: str) -> Optional[DoclingDocument]:
|
@@ -1283,7 +1283,7 @@ class PatentUsptoAppV1(PatentUspto):
|
|
1283
1283
|
title = text.strip()
|
1284
1284
|
if title:
|
1285
1285
|
self.parents[self.level + 1] = self.doc.add_text(
|
1286
|
-
parent=self.parents[self.level],
|
1286
|
+
parent=self.parents[self.level],
|
1287
1287
|
label=DocItemLabel.TITLE,
|
1288
1288
|
text=title,
|
1289
1289
|
)
|
@@ -1301,7 +1301,7 @@ class PatentUsptoAppV1(PatentUspto):
|
|
1301
1301
|
abstract_item = self.doc.add_heading(
|
1302
1302
|
heading_text,
|
1303
1303
|
level=heading_level,
|
1304
|
-
parent=self.parents[heading_level],
|
1304
|
+
parent=self.parents[heading_level],
|
1305
1305
|
)
|
1306
1306
|
self.doc.add_text(
|
1307
1307
|
label=DocItemLabel.PARAGRAPH,
|
@@ -1331,7 +1331,7 @@ class PatentUsptoAppV1(PatentUspto):
|
|
1331
1331
|
claims_item = self.doc.add_heading(
|
1332
1332
|
heading_text,
|
1333
1333
|
level=heading_level,
|
1334
|
-
parent=self.parents[heading_level],
|
1334
|
+
parent=self.parents[heading_level],
|
1335
1335
|
)
|
1336
1336
|
for text in self.claims:
|
1337
1337
|
self.doc.add_text(
|
@@ -1350,14 +1350,14 @@ class PatentUsptoAppV1(PatentUspto):
|
|
1350
1350
|
self.parents[self.level + 1] = self.doc.add_heading(
|
1351
1351
|
text=text,
|
1352
1352
|
level=self.level,
|
1353
|
-
parent=self.parents[self.level],
|
1353
|
+
parent=self.parents[self.level],
|
1354
1354
|
)
|
1355
1355
|
self.level += 1
|
1356
1356
|
else:
|
1357
1357
|
self.doc.add_text(
|
1358
1358
|
label=DocItemLabel.PARAGRAPH,
|
1359
1359
|
text=text,
|
1360
|
-
parent=self.parents[self.level],
|
1360
|
+
parent=self.parents[self.level],
|
1361
1361
|
)
|
1362
1362
|
self.text = ""
|
1363
1363
|
|
@@ -1366,7 +1366,7 @@ class PatentUsptoAppV1(PatentUspto):
|
|
1366
1366
|
empty_table = TableData(num_rows=0, num_cols=0, table_cells=[])
|
1367
1367
|
self.doc.add_table(
|
1368
1368
|
data=empty_table,
|
1369
|
-
parent=self.parents[self.level],
|
1369
|
+
parent=self.parents[self.level],
|
1370
1370
|
)
|
1371
1371
|
|
1372
1372
|
def _apply_style(self, text: str, style_tag: str) -> str:
|
docling/cli/main.py
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
import importlib
|
2
|
-
import json
|
3
2
|
import logging
|
3
|
+
import platform
|
4
4
|
import re
|
5
|
+
import sys
|
5
6
|
import tempfile
|
6
7
|
import time
|
7
8
|
import warnings
|
8
|
-
from enum import Enum
|
9
9
|
from pathlib import Path
|
10
10
|
from typing import Annotated, Dict, Iterable, List, Optional, Type
|
11
11
|
|
12
12
|
import typer
|
13
13
|
from docling_core.types.doc import ImageRefMode
|
14
14
|
from docling_core.utils.file import resolve_source_to_path
|
15
|
-
from pydantic import TypeAdapter
|
15
|
+
from pydantic import TypeAdapter
|
16
16
|
|
17
17
|
from docling.backend.docling_parse_backend import DoclingParseDocumentBackend
|
18
18
|
from docling.backend.docling_parse_v2_backend import DoclingParseV2DocumentBackend
|
@@ -65,10 +65,15 @@ def version_callback(value: bool):
|
|
65
65
|
docling_core_version = importlib.metadata.version("docling-core")
|
66
66
|
docling_ibm_models_version = importlib.metadata.version("docling-ibm-models")
|
67
67
|
docling_parse_version = importlib.metadata.version("docling-parse")
|
68
|
+
platform_str = platform.platform()
|
69
|
+
py_impl_version = sys.implementation.cache_tag
|
70
|
+
py_lang_version = platform.python_version()
|
68
71
|
print(f"Docling version: {docling_version}")
|
69
72
|
print(f"Docling Core version: {docling_core_version}")
|
70
73
|
print(f"Docling IBM Models version: {docling_ibm_models_version}")
|
71
74
|
print(f"Docling Parse version: {docling_parse_version}")
|
75
|
+
print(f"Python: {py_impl_version} ({py_lang_version})")
|
76
|
+
print(f"Platform: {platform_str}")
|
72
77
|
raise typer.Exit()
|
73
78
|
|
74
79
|
|
@@ -206,6 +211,14 @@ def convert(
|
|
206
211
|
TableFormerMode,
|
207
212
|
typer.Option(..., help="The mode to use in the table structure model."),
|
208
213
|
] = TableFormerMode.FAST,
|
214
|
+
enrich_code: Annotated[
|
215
|
+
bool,
|
216
|
+
typer.Option(..., help="Enable the code enrichment model in the pipeline."),
|
217
|
+
] = False,
|
218
|
+
enrich_formula: Annotated[
|
219
|
+
bool,
|
220
|
+
typer.Option(..., help="Enable the formula enrichment model in the pipeline."),
|
221
|
+
] = False,
|
209
222
|
artifacts_path: Annotated[
|
210
223
|
Optional[Path],
|
211
224
|
typer.Option(..., help="If provided, the location of the model artifacts."),
|
@@ -360,6 +373,8 @@ def convert(
|
|
360
373
|
do_ocr=ocr,
|
361
374
|
ocr_options=ocr_options,
|
362
375
|
do_table_structure=True,
|
376
|
+
do_code_enrichment=enrich_code,
|
377
|
+
do_formula_enrichment=enrich_formula,
|
363
378
|
document_timeout=document_timeout,
|
364
379
|
)
|
365
380
|
pipeline_options.table_structure_options.do_cell_matching = (
|
docling/datamodel/document.py
CHANGED
@@ -157,6 +157,8 @@ class InputDocument(BaseModel):
|
|
157
157
|
self.page_count = self._backend.page_count()
|
158
158
|
if not self.page_count <= self.limits.max_num_pages:
|
159
159
|
self.valid = False
|
160
|
+
elif self.page_count < self.limits.page_range[0]:
|
161
|
+
self.valid = False
|
160
162
|
|
161
163
|
except (FileNotFoundError, OSError) as e:
|
162
164
|
self.valid = False
|
@@ -352,6 +354,8 @@ class _DocumentConversionInput(BaseModel):
|
|
352
354
|
mime = FormatToMimeType[InputFormat.MD][0]
|
353
355
|
elif ext in FormatToExtensions[InputFormat.JSON_DOCLING]:
|
354
356
|
mime = FormatToMimeType[InputFormat.JSON_DOCLING][0]
|
357
|
+
elif ext in FormatToExtensions[InputFormat.PDF]:
|
358
|
+
mime = FormatToMimeType[InputFormat.PDF][0]
|
355
359
|
return mime
|
356
360
|
|
357
361
|
@staticmethod
|
@@ -119,6 +119,7 @@ class RapidOcrOptions(OcrOptions):
|
|
119
119
|
det_model_path: Optional[str] = None # same default as rapidocr
|
120
120
|
cls_model_path: Optional[str] = None # same default as rapidocr
|
121
121
|
rec_model_path: Optional[str] = None # same default as rapidocr
|
122
|
+
rec_keys_path: Optional[str] = None # same default as rapidocr
|
122
123
|
|
123
124
|
model_config = ConfigDict(
|
124
125
|
extra="forbid",
|
docling/datamodel/settings.py
CHANGED
@@ -1,13 +1,28 @@
|
|
1
1
|
import sys
|
2
2
|
from pathlib import Path
|
3
|
+
from typing import Annotated, Tuple
|
3
4
|
|
4
|
-
from pydantic import BaseModel
|
5
|
+
from pydantic import BaseModel, PlainValidator
|
5
6
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
6
7
|
|
7
8
|
|
9
|
+
def _validate_page_range(v: Tuple[int, int]) -> Tuple[int, int]:
|
10
|
+
if v[0] < 1 or v[1] < v[0]:
|
11
|
+
raise ValueError(
|
12
|
+
"Invalid page range: start must be ≥ 1 and end must be ≥ start."
|
13
|
+
)
|
14
|
+
return v
|
15
|
+
|
16
|
+
|
17
|
+
PageRange = Annotated[Tuple[int, int], PlainValidator(_validate_page_range)]
|
18
|
+
|
19
|
+
DEFAULT_PAGE_RANGE: PageRange = (1, sys.maxsize)
|
20
|
+
|
21
|
+
|
8
22
|
class DocumentLimits(BaseModel):
|
9
23
|
max_num_pages: int = sys.maxsize
|
10
24
|
max_file_size: int = sys.maxsize
|
25
|
+
page_range: PageRange = DEFAULT_PAGE_RANGE
|
11
26
|
|
12
27
|
|
13
28
|
class BatchConcurrencySettings(BaseModel):
|
docling/document_converter.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
import logging
|
2
|
+
import math
|
2
3
|
import sys
|
3
4
|
import time
|
4
5
|
from functools import partial
|
5
6
|
from pathlib import Path
|
6
|
-
from typing import Dict, Iterable, Iterator, List, Optional, Type, Union
|
7
|
+
from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union
|
7
8
|
|
8
9
|
from pydantic import BaseModel, ConfigDict, model_validator, validate_call
|
9
10
|
|
@@ -31,7 +32,12 @@ from docling.datamodel.document import (
|
|
31
32
|
_DocumentConversionInput,
|
32
33
|
)
|
33
34
|
from docling.datamodel.pipeline_options import PipelineOptions
|
34
|
-
from docling.datamodel.settings import
|
35
|
+
from docling.datamodel.settings import (
|
36
|
+
DEFAULT_PAGE_RANGE,
|
37
|
+
DocumentLimits,
|
38
|
+
PageRange,
|
39
|
+
settings,
|
40
|
+
)
|
35
41
|
from docling.exceptions import ConversionError
|
36
42
|
from docling.pipeline.base_pipeline import BasePipeline
|
37
43
|
from docling.pipeline.simple_pipeline import SimplePipeline
|
@@ -184,6 +190,7 @@ class DocumentConverter:
|
|
184
190
|
raises_on_error: bool = True,
|
185
191
|
max_num_pages: int = sys.maxsize,
|
186
192
|
max_file_size: int = sys.maxsize,
|
193
|
+
page_range: PageRange = DEFAULT_PAGE_RANGE,
|
187
194
|
) -> ConversionResult:
|
188
195
|
all_res = self.convert_all(
|
189
196
|
source=[source],
|
@@ -191,6 +198,7 @@ class DocumentConverter:
|
|
191
198
|
max_num_pages=max_num_pages,
|
192
199
|
max_file_size=max_file_size,
|
193
200
|
headers=headers,
|
201
|
+
page_range=page_range,
|
194
202
|
)
|
195
203
|
return next(all_res)
|
196
204
|
|
@@ -202,10 +210,12 @@ class DocumentConverter:
|
|
202
210
|
raises_on_error: bool = True, # True: raises on first conversion error; False: does not raise on conv error
|
203
211
|
max_num_pages: int = sys.maxsize,
|
204
212
|
max_file_size: int = sys.maxsize,
|
213
|
+
page_range: PageRange = DEFAULT_PAGE_RANGE,
|
205
214
|
) -> Iterator[ConversionResult]:
|
206
215
|
limits = DocumentLimits(
|
207
216
|
max_num_pages=max_num_pages,
|
208
217
|
max_file_size=max_file_size,
|
218
|
+
page_range=page_range,
|
209
219
|
)
|
210
220
|
conv_input = _DocumentConversionInput(
|
211
221
|
path_or_stream_iterator=source, limits=limits, headers=headers
|
@@ -209,12 +209,16 @@ class TableStructureModel(BasePageModel):
|
|
209
209
|
tc.bbox = tc.bbox.scaled(1 / self.scale)
|
210
210
|
table_cells.append(tc)
|
211
211
|
|
212
|
+
assert "predict_details" in table_out
|
213
|
+
|
212
214
|
# Retrieving cols/rows, after post processing:
|
213
|
-
num_rows = table_out["predict_details"]
|
214
|
-
num_cols = table_out["predict_details"]
|
215
|
-
otsl_seq =
|
216
|
-
"
|
217
|
-
|
215
|
+
num_rows = table_out["predict_details"].get("num_rows", 0)
|
216
|
+
num_cols = table_out["predict_details"].get("num_cols", 0)
|
217
|
+
otsl_seq = (
|
218
|
+
table_out["predict_details"]
|
219
|
+
.get("prediction", {})
|
220
|
+
.get("rs_seq", [])
|
221
|
+
)
|
218
222
|
|
219
223
|
tbl = Table(
|
220
224
|
otsl_seq=otsl_seq,
|
@@ -4,7 +4,7 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import tempfile
|
6
6
|
from subprocess import DEVNULL, PIPE, Popen
|
7
|
-
from typing import Iterable, Optional, Tuple
|
7
|
+
from typing import Iterable, List, Optional, Tuple
|
8
8
|
|
9
9
|
import pandas as pd
|
10
10
|
from docling_core.types.doc import BoundingBox, CoordOrigin
|
@@ -14,6 +14,7 @@ from docling.datamodel.document import ConversionResult
|
|
14
14
|
from docling.datamodel.pipeline_options import TesseractCliOcrOptions
|
15
15
|
from docling.datamodel.settings import settings
|
16
16
|
from docling.models.base_ocr_model import BaseOcrModel
|
17
|
+
from docling.utils.ocr_utils import map_tesseract_script
|
17
18
|
from docling.utils.profiling import TimeRecorder
|
18
19
|
|
19
20
|
_log = logging.getLogger(__name__)
|
@@ -28,10 +29,13 @@ class TesseractOcrCliModel(BaseOcrModel):
|
|
28
29
|
|
29
30
|
self._name: Optional[str] = None
|
30
31
|
self._version: Optional[str] = None
|
32
|
+
self._tesseract_languages: Optional[List[str]] = None
|
33
|
+
self._script_prefix: Optional[str] = None
|
31
34
|
|
32
35
|
if self.enabled:
|
33
36
|
try:
|
34
37
|
self._get_name_and_version()
|
38
|
+
self._set_languages_and_prefix()
|
35
39
|
|
36
40
|
except Exception as exc:
|
37
41
|
raise RuntimeError(
|
@@ -73,12 +77,20 @@ class TesseractOcrCliModel(BaseOcrModel):
|
|
73
77
|
return name, version
|
74
78
|
|
75
79
|
def _run_tesseract(self, ifilename: str):
|
76
|
-
|
80
|
+
r"""
|
81
|
+
Run tesseract CLI
|
82
|
+
"""
|
77
83
|
cmd = [self.options.tesseract_cmd]
|
78
84
|
|
79
|
-
if
|
85
|
+
if "auto" in self.options.lang:
|
86
|
+
lang = self._detect_language(ifilename)
|
87
|
+
if lang is not None:
|
88
|
+
cmd.append("-l")
|
89
|
+
cmd.append(lang)
|
90
|
+
elif self.options.lang is not None and len(self.options.lang) > 0:
|
80
91
|
cmd.append("-l")
|
81
92
|
cmd.append("+".join(self.options.lang))
|
93
|
+
|
82
94
|
if self.options.path is not None:
|
83
95
|
cmd.append("--tessdata-dir")
|
84
96
|
cmd.append(self.options.path)
|
@@ -106,6 +118,63 @@ class TesseractOcrCliModel(BaseOcrModel):
|
|
106
118
|
|
107
119
|
return df_filtered
|
108
120
|
|
121
|
+
def _detect_language(self, ifilename: str):
|
122
|
+
r"""
|
123
|
+
Run tesseract in PSM 0 mode to detect the language
|
124
|
+
"""
|
125
|
+
assert self._tesseract_languages is not None
|
126
|
+
|
127
|
+
cmd = [self.options.tesseract_cmd]
|
128
|
+
cmd.extend(["--psm", "0", "-l", "osd", ifilename, "stdout"])
|
129
|
+
_log.info("command: {}".format(" ".join(cmd)))
|
130
|
+
proc = Popen(cmd, stdout=PIPE, stderr=DEVNULL)
|
131
|
+
output, _ = proc.communicate()
|
132
|
+
decoded_data = output.decode("utf-8")
|
133
|
+
df = pd.read_csv(
|
134
|
+
io.StringIO(decoded_data), sep=":", header=None, names=["key", "value"]
|
135
|
+
)
|
136
|
+
scripts = df.loc[df["key"] == "Script"].value.tolist()
|
137
|
+
if len(scripts) == 0:
|
138
|
+
_log.warning("Tesseract cannot detect the script of the page")
|
139
|
+
return None
|
140
|
+
|
141
|
+
script = map_tesseract_script(scripts[0].strip())
|
142
|
+
lang = f"{self._script_prefix}{script}"
|
143
|
+
|
144
|
+
# Check if the detected language has been installed
|
145
|
+
if lang not in self._tesseract_languages:
|
146
|
+
msg = f"Tesseract detected the script '{script}' and language '{lang}'."
|
147
|
+
msg += " However this language is not installed in your system and will be ignored."
|
148
|
+
_log.warning(msg)
|
149
|
+
return None
|
150
|
+
|
151
|
+
_log.debug(
|
152
|
+
f"Using tesseract model for the detected script '{script}' and language '{lang}'"
|
153
|
+
)
|
154
|
+
return lang
|
155
|
+
|
156
|
+
def _set_languages_and_prefix(self):
|
157
|
+
r"""
|
158
|
+
Read and set the languages installed in tesseract and decide the script prefix
|
159
|
+
"""
|
160
|
+
# Get all languages
|
161
|
+
cmd = [self.options.tesseract_cmd]
|
162
|
+
cmd.append("--list-langs")
|
163
|
+
_log.info("command: {}".format(" ".join(cmd)))
|
164
|
+
proc = Popen(cmd, stdout=PIPE, stderr=DEVNULL)
|
165
|
+
output, _ = proc.communicate()
|
166
|
+
decoded_data = output.decode("utf-8")
|
167
|
+
df = pd.read_csv(io.StringIO(decoded_data), header=None)
|
168
|
+
self._tesseract_languages = df[0].tolist()[1:]
|
169
|
+
|
170
|
+
# Decide the script prefix
|
171
|
+
if any([l.startswith("script/") for l in self._tesseract_languages]):
|
172
|
+
script_prefix = "script/"
|
173
|
+
else:
|
174
|
+
script_prefix = ""
|
175
|
+
|
176
|
+
self._script_prefix = script_prefix
|
177
|
+
|
109
178
|
def __call__(
|
110
179
|
self, conv_res: ConversionResult, page_batch: Iterable[Page]
|
111
180
|
) -> Iterable[Page]:
|
@@ -120,7 +189,6 @@ class TesseractOcrCliModel(BaseOcrModel):
|
|
120
189
|
yield page
|
121
190
|
else:
|
122
191
|
with TimeRecorder(conv_res, "ocr"):
|
123
|
-
|
124
192
|
ocr_rects = self.get_ocr_rects(page)
|
125
193
|
|
126
194
|
all_ocr_cells = []
|