docling 2.0.0__py3-none-any.whl → 2.2.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.
@@ -8,8 +8,10 @@ from typing import Dict, Iterable, Iterator, List, Optional, Type
8
8
  from pydantic import BaseModel, ConfigDict, model_validator, validate_call
9
9
 
10
10
  from docling.backend.abstract_backend import AbstractDocumentBackend
11
+ from docling.backend.asciidoc_backend import AsciiDocBackend
11
12
  from docling.backend.docling_parse_backend import DoclingParseDocumentBackend
12
13
  from docling.backend.html_backend import HTMLDocumentBackend
14
+ from docling.backend.md_backend import MarkdownDocumentBackend
13
15
  from docling.backend.mspowerpoint_backend import MsPowerpointDocumentBackend
14
16
  from docling.backend.msword_backend import MsWordDocumentBackend
15
17
  from docling.datamodel.base_models import ConversionStatus, DocumentStream, InputFormat
@@ -52,6 +54,16 @@ class PowerpointFormatOption(FormatOption):
52
54
  backend: Type[AbstractDocumentBackend] = MsPowerpointDocumentBackend
53
55
 
54
56
 
57
+ class MarkdownFormatOption(FormatOption):
58
+ pipeline_cls: Type = SimplePipeline
59
+ backend: Type[AbstractDocumentBackend] = MarkdownDocumentBackend
60
+
61
+
62
+ class AsciiDocFormatOption(FormatOption):
63
+ pipeline_cls: Type = SimplePipeline
64
+ backend: Type[AbstractDocumentBackend] = AsciiDocBackend
65
+
66
+
55
67
  class HTMLFormatOption(FormatOption):
56
68
  pipeline_cls: Type = SimplePipeline
57
69
  backend: Type[AbstractDocumentBackend] = HTMLDocumentBackend
@@ -74,6 +86,12 @@ _format_to_default_options = {
74
86
  InputFormat.PPTX: FormatOption(
75
87
  pipeline_cls=SimplePipeline, backend=MsPowerpointDocumentBackend
76
88
  ),
89
+ InputFormat.MD: FormatOption(
90
+ pipeline_cls=SimplePipeline, backend=MarkdownDocumentBackend
91
+ ),
92
+ InputFormat.ASCIIDOC: FormatOption(
93
+ pipeline_cls=SimplePipeline, backend=AsciiDocBackend
94
+ ),
77
95
  InputFormat.HTML: FormatOption(
78
96
  pipeline_cls=SimplePipeline, backend=HTMLDocumentBackend
79
97
  ),
@@ -69,7 +69,7 @@ class BaseOcrModel:
69
69
  coverage, ocr_rects = find_ocr_rects(page.size, bitmap_rects)
70
70
 
71
71
  # return full-page rectangle if sufficiently covered with bitmaps
72
- if coverage > BITMAP_COVERAGE_TRESHOLD:
72
+ if coverage > max(BITMAP_COVERAGE_TRESHOLD, self.options.bitmap_area_threshold):
73
73
  return [
74
74
  BoundingBox(
75
75
  l=0,
@@ -81,6 +81,14 @@ class BaseOcrModel:
81
81
  ]
82
82
  # return individual rectangles if the bitmap coverage is smaller
83
83
  else: # coverage <= BITMAP_COVERAGE_TRESHOLD:
84
+
85
+ # skip OCR if the bitmap area on the page is smaller than the options threshold
86
+ ocr_rects = [
87
+ rect
88
+ for rect in ocr_rects
89
+ if rect.area() / (page.size.width * page.size.height)
90
+ > self.options.bitmap_area_threshold
91
+ ]
84
92
  return ocr_rects
85
93
 
86
94
  # Filters OCR cells by dropping any OCR cell that intersects with an existing programmatic cell.
@@ -5,15 +5,23 @@ from typing import List, Union
5
5
  from deepsearch_glm.nlp_utils import init_nlp_model
6
6
  from deepsearch_glm.utils.doc_utils import to_docling_document
7
7
  from deepsearch_glm.utils.load_pretrained_models import load_pretrained_nlp_models
8
- from docling_core.types import BaseText
9
- from docling_core.types import Document as DsDocument
10
- from docling_core.types import DocumentDescription as DsDocumentDescription
11
- from docling_core.types import FileInfoObject as DsFileInfoObject
12
- from docling_core.types import PageDimensions, PageReference, Prov, Ref
13
- from docling_core.types import Table as DsSchemaTable
14
8
  from docling_core.types.doc import BoundingBox, CoordOrigin, DoclingDocument
15
9
  from docling_core.types.legacy_doc.base import BoundingBox as DsBoundingBox
16
- from docling_core.types.legacy_doc.base import Figure, TableCell
10
+ from docling_core.types.legacy_doc.base import (
11
+ Figure,
12
+ PageDimensions,
13
+ PageReference,
14
+ Prov,
15
+ Ref,
16
+ )
17
+ from docling_core.types.legacy_doc.base import Table as DsSchemaTable
18
+ from docling_core.types.legacy_doc.base import TableCell
19
+ from docling_core.types.legacy_doc.document import BaseText
20
+ from docling_core.types.legacy_doc.document import (
21
+ CCSDocumentDescription as DsDocumentDescription,
22
+ )
23
+ from docling_core.types.legacy_doc.document import CCSFileInfoObject as DsFileInfoObject
24
+ from docling_core.types.legacy_doc.document import ExportedCCSDocument as DsDocument
17
25
  from PIL import ImageDraw
18
26
  from pydantic import BaseModel, ConfigDict
19
27
 
@@ -202,6 +210,7 @@ class GlmModel:
202
210
  page_dimensions = [
203
211
  PageDimensions(page=p.page_no + 1, height=p.size.height, width=p.size.width)
204
212
  for p in conv_res.pages
213
+ if p.size is not None
205
214
  ]
206
215
 
207
216
  ds_doc: DsDocument = DsDocument(
@@ -41,48 +41,50 @@ class EasyOcrModel(BaseOcrModel):
41
41
 
42
42
  for page in page_batch:
43
43
  assert page._backend is not None
44
-
45
- ocr_rects = self.get_ocr_rects(page)
46
-
47
- all_ocr_cells = []
48
- for ocr_rect in ocr_rects:
49
- # Skip zero area boxes
50
- if ocr_rect.area() == 0:
51
- continue
52
- high_res_image = page._backend.get_page_image(
53
- scale=self.scale, cropbox=ocr_rect
54
- )
55
- im = numpy.array(high_res_image)
56
- result = self.reader.readtext(im)
57
-
58
- del high_res_image
59
- del im
60
-
61
- cells = [
62
- OcrCell(
63
- id=ix,
64
- text=line[1],
65
- confidence=line[2],
66
- bbox=BoundingBox.from_tuple(
67
- coord=(
68
- (line[0][0][0] / self.scale) + ocr_rect.l,
69
- (line[0][0][1] / self.scale) + ocr_rect.t,
70
- (line[0][2][0] / self.scale) + ocr_rect.l,
71
- (line[0][2][1] / self.scale) + ocr_rect.t,
72
- ),
73
- origin=CoordOrigin.TOPLEFT,
74
- ),
44
+ if not page._backend.is_valid():
45
+ yield page
46
+ else:
47
+ ocr_rects = self.get_ocr_rects(page)
48
+
49
+ all_ocr_cells = []
50
+ for ocr_rect in ocr_rects:
51
+ # Skip zero area boxes
52
+ if ocr_rect.area() == 0:
53
+ continue
54
+ high_res_image = page._backend.get_page_image(
55
+ scale=self.scale, cropbox=ocr_rect
75
56
  )
76
- for ix, line in enumerate(result)
77
- ]
78
- all_ocr_cells.extend(cells)
57
+ im = numpy.array(high_res_image)
58
+ result = self.reader.readtext(im)
59
+
60
+ del high_res_image
61
+ del im
62
+
63
+ cells = [
64
+ OcrCell(
65
+ id=ix,
66
+ text=line[1],
67
+ confidence=line[2],
68
+ bbox=BoundingBox.from_tuple(
69
+ coord=(
70
+ (line[0][0][0] / self.scale) + ocr_rect.l,
71
+ (line[0][0][1] / self.scale) + ocr_rect.t,
72
+ (line[0][2][0] / self.scale) + ocr_rect.l,
73
+ (line[0][2][1] / self.scale) + ocr_rect.t,
74
+ ),
75
+ origin=CoordOrigin.TOPLEFT,
76
+ ),
77
+ )
78
+ for ix, line in enumerate(result)
79
+ ]
80
+ all_ocr_cells.extend(cells)
79
81
 
80
- ## Remove OCR cells which overlap with programmatic cells.
81
- filtered_ocr_cells = self.filter_ocr_cells(all_ocr_cells, page.cells)
82
+ ## Remove OCR cells which overlap with programmatic cells.
83
+ filtered_ocr_cells = self.filter_ocr_cells(all_ocr_cells, page.cells)
82
84
 
83
- page.cells.extend(filtered_ocr_cells)
85
+ page.cells.extend(filtered_ocr_cells)
84
86
 
85
- # DEBUG code:
86
- # self.draw_ocr_rects_and_cells(page, ocr_rects)
87
+ # DEBUG code:
88
+ # self.draw_ocr_rects_and_cells(page, ocr_rects)
87
89
 
88
- yield page
90
+ yield page
@@ -273,68 +273,72 @@ class LayoutModel(BasePageModel):
273
273
 
274
274
  def __call__(self, page_batch: Iterable[Page]) -> Iterable[Page]:
275
275
  for page in page_batch:
276
- assert page.size is not None
277
-
278
- clusters = []
279
- for ix, pred_item in enumerate(
280
- self.layout_predictor.predict(page.get_image(scale=1.0))
281
- ):
282
- label = DocItemLabel(
283
- pred_item["label"].lower().replace(" ", "_").replace("-", "_")
284
- ) # Temporary, until docling-ibm-model uses docling-core types
285
- cluster = Cluster(
286
- id=ix,
287
- label=label,
288
- confidence=pred_item["confidence"],
289
- bbox=BoundingBox.model_validate(pred_item),
290
- cells=[],
291
- )
292
- clusters.append(cluster)
293
-
294
- # Map cells to clusters
295
- # TODO: Remove, postprocess should take care of it anyway.
296
- for cell in page.cells:
297
- for cluster in clusters:
298
- if not cell.bbox.area() > 0:
299
- overlap_frac = 0.0
300
- else:
301
- overlap_frac = (
302
- cell.bbox.intersection_area_with(cluster.bbox)
303
- / cell.bbox.area()
304
- )
305
-
306
- if overlap_frac > 0.5:
307
- cluster.cells.append(cell)
308
-
309
- # Pre-sort clusters
310
- # clusters = self.sort_clusters_by_cell_order(clusters)
311
-
312
- # DEBUG code:
313
- def draw_clusters_and_cells():
314
- image = copy.deepcopy(page.image)
315
- draw = ImageDraw.Draw(image)
316
- for c in clusters:
317
- x0, y0, x1, y1 = c.bbox.as_tuple()
318
- draw.rectangle([(x0, y0), (x1, y1)], outline="green")
319
-
320
- cell_color = (
321
- random.randint(30, 140),
322
- random.randint(30, 140),
323
- random.randint(30, 140),
276
+ assert page._backend is not None
277
+ if not page._backend.is_valid():
278
+ yield page
279
+ else:
280
+ assert page.size is not None
281
+
282
+ clusters = []
283
+ for ix, pred_item in enumerate(
284
+ self.layout_predictor.predict(page.get_image(scale=1.0))
285
+ ):
286
+ label = DocItemLabel(
287
+ pred_item["label"].lower().replace(" ", "_").replace("-", "_")
288
+ ) # Temporary, until docling-ibm-model uses docling-core types
289
+ cluster = Cluster(
290
+ id=ix,
291
+ label=label,
292
+ confidence=pred_item["confidence"],
293
+ bbox=BoundingBox.model_validate(pred_item),
294
+ cells=[],
324
295
  )
325
- for tc in c.cells: # [:1]:
326
- x0, y0, x1, y1 = tc.bbox.as_tuple()
327
- draw.rectangle([(x0, y0), (x1, y1)], outline=cell_color)
328
- image.show()
296
+ clusters.append(cluster)
297
+
298
+ # Map cells to clusters
299
+ # TODO: Remove, postprocess should take care of it anyway.
300
+ for cell in page.cells:
301
+ for cluster in clusters:
302
+ if not cell.bbox.area() > 0:
303
+ overlap_frac = 0.0
304
+ else:
305
+ overlap_frac = (
306
+ cell.bbox.intersection_area_with(cluster.bbox)
307
+ / cell.bbox.area()
308
+ )
309
+
310
+ if overlap_frac > 0.5:
311
+ cluster.cells.append(cell)
312
+
313
+ # Pre-sort clusters
314
+ # clusters = self.sort_clusters_by_cell_order(clusters)
315
+
316
+ # DEBUG code:
317
+ def draw_clusters_and_cells():
318
+ image = copy.deepcopy(page.image)
319
+ draw = ImageDraw.Draw(image)
320
+ for c in clusters:
321
+ x0, y0, x1, y1 = c.bbox.as_tuple()
322
+ draw.rectangle([(x0, y0), (x1, y1)], outline="green")
323
+
324
+ cell_color = (
325
+ random.randint(30, 140),
326
+ random.randint(30, 140),
327
+ random.randint(30, 140),
328
+ )
329
+ for tc in c.cells: # [:1]:
330
+ x0, y0, x1, y1 = tc.bbox.as_tuple()
331
+ draw.rectangle([(x0, y0), (x1, y1)], outline=cell_color)
332
+ image.show()
329
333
 
330
- # draw_clusters_and_cells()
334
+ # draw_clusters_and_cells()
331
335
 
332
- clusters, page.cells = self.postprocess(
333
- clusters, page.cells, page.size.height
334
- )
336
+ clusters, page.cells = self.postprocess(
337
+ clusters, page.cells, page.size.height
338
+ )
335
339
 
336
- # draw_clusters_and_cells()
340
+ # draw_clusters_and_cells()
337
341
 
338
- page.predictions.layout = LayoutPrediction(clusters=clusters)
342
+ page.predictions.layout = LayoutPrediction(clusters=clusters)
339
343
 
340
- yield page
344
+ yield page
@@ -54,111 +54,119 @@ class PageAssembleModel(BasePageModel):
54
54
  def __call__(self, page_batch: Iterable[Page]) -> Iterable[Page]:
55
55
  for page in page_batch:
56
56
  assert page._backend is not None
57
- assert page.predictions.layout is not None
58
- # assembles some JSON output page by page.
59
-
60
- elements: List[PageElement] = []
61
- headers: List[PageElement] = []
62
- body: List[PageElement] = []
63
-
64
- for cluster in page.predictions.layout.clusters:
65
- # _log.info("Cluster label seen:", cluster.label)
66
- if cluster.label in LayoutModel.TEXT_ELEM_LABELS:
67
-
68
- textlines = [
69
- cell.text.replace("\x02", "-").strip()
70
- for cell in cluster.cells
71
- if len(cell.text.strip()) > 0
72
- ]
73
- text = self.sanitize_text(textlines)
74
- text_el = TextElement(
75
- label=cluster.label,
76
- id=cluster.id,
77
- text=text,
78
- page_no=page.page_no,
79
- cluster=cluster,
80
- )
81
- elements.append(text_el)
82
-
83
- if cluster.label in LayoutModel.PAGE_HEADER_LABELS:
84
- headers.append(text_el)
85
- else:
86
- body.append(text_el)
87
- elif cluster.label == LayoutModel.TABLE_LABEL:
88
- tbl = None
89
- if page.predictions.tablestructure:
90
- tbl = page.predictions.tablestructure.table_map.get(
91
- cluster.id, None
92
- )
93
- if (
94
- not tbl
95
- ): # fallback: add table without structure, if it isn't present
96
- tbl = Table(
97
- label=cluster.label,
98
- id=cluster.id,
99
- text="",
100
- otsl_seq=[],
101
- table_cells=[],
102
- cluster=cluster,
103
- page_no=page.page_no,
104
- )
57
+ if not page._backend.is_valid():
58
+ yield page
59
+ else:
60
+ assert page.predictions.layout is not None
105
61
 
106
- elements.append(tbl)
107
- body.append(tbl)
108
- elif cluster.label == LayoutModel.FIGURE_LABEL:
109
- fig = None
110
- if page.predictions.figures_classification:
111
- fig = page.predictions.figures_classification.figure_map.get(
112
- cluster.id, None
113
- )
114
- if (
115
- not fig
116
- ): # fallback: add figure without classification, if it isn't present
117
- fig = FigureElement(
62
+ # assembles some JSON output page by page.
63
+
64
+ elements: List[PageElement] = []
65
+ headers: List[PageElement] = []
66
+ body: List[PageElement] = []
67
+
68
+ for cluster in page.predictions.layout.clusters:
69
+ # _log.info("Cluster label seen:", cluster.label)
70
+ if cluster.label in LayoutModel.TEXT_ELEM_LABELS:
71
+
72
+ textlines = [
73
+ cell.text.replace("\x02", "-").strip()
74
+ for cell in cluster.cells
75
+ if len(cell.text.strip()) > 0
76
+ ]
77
+ text = self.sanitize_text(textlines)
78
+ text_el = TextElement(
118
79
  label=cluster.label,
119
80
  id=cluster.id,
120
- text="",
121
- data=None,
122
- cluster=cluster,
81
+ text=text,
123
82
  page_no=page.page_no,
83
+ cluster=cluster,
124
84
  )
125
- elements.append(fig)
126
- body.append(fig)
127
- elif cluster.label == LayoutModel.FORMULA_LABEL:
128
- equation = None
129
- if page.predictions.equations_prediction:
130
- equation = (
131
- page.predictions.equations_prediction.equation_map.get(
85
+ elements.append(text_el)
86
+
87
+ if cluster.label in LayoutModel.PAGE_HEADER_LABELS:
88
+ headers.append(text_el)
89
+ else:
90
+ body.append(text_el)
91
+ elif cluster.label == LayoutModel.TABLE_LABEL:
92
+ tbl = None
93
+ if page.predictions.tablestructure:
94
+ tbl = page.predictions.tablestructure.table_map.get(
132
95
  cluster.id, None
133
96
  )
134
- )
135
- if not equation: # fallback: add empty formula, if it isn't present
136
- text = self.sanitize_text(
137
- [
138
- cell.text.replace("\x02", "-").strip()
139
- for cell in cluster.cells
140
- if len(cell.text.strip()) > 0
141
- ]
142
- )
143
- equation = TextElement(
144
- label=cluster.label,
145
- id=cluster.id,
146
- cluster=cluster,
147
- page_no=page.page_no,
148
- text=text,
149
- )
150
- elements.append(equation)
151
- body.append(equation)
97
+ if (
98
+ not tbl
99
+ ): # fallback: add table without structure, if it isn't present
100
+ tbl = Table(
101
+ label=cluster.label,
102
+ id=cluster.id,
103
+ text="",
104
+ otsl_seq=[],
105
+ table_cells=[],
106
+ cluster=cluster,
107
+ page_no=page.page_no,
108
+ )
109
+
110
+ elements.append(tbl)
111
+ body.append(tbl)
112
+ elif cluster.label == LayoutModel.FIGURE_LABEL:
113
+ fig = None
114
+ if page.predictions.figures_classification:
115
+ fig = (
116
+ page.predictions.figures_classification.figure_map.get(
117
+ cluster.id, None
118
+ )
119
+ )
120
+ if (
121
+ not fig
122
+ ): # fallback: add figure without classification, if it isn't present
123
+ fig = FigureElement(
124
+ label=cluster.label,
125
+ id=cluster.id,
126
+ text="",
127
+ data=None,
128
+ cluster=cluster,
129
+ page_no=page.page_no,
130
+ )
131
+ elements.append(fig)
132
+ body.append(fig)
133
+ elif cluster.label == LayoutModel.FORMULA_LABEL:
134
+ equation = None
135
+ if page.predictions.equations_prediction:
136
+ equation = (
137
+ page.predictions.equations_prediction.equation_map.get(
138
+ cluster.id, None
139
+ )
140
+ )
141
+ if (
142
+ not equation
143
+ ): # fallback: add empty formula, if it isn't present
144
+ text = self.sanitize_text(
145
+ [
146
+ cell.text.replace("\x02", "-").strip()
147
+ for cell in cluster.cells
148
+ if len(cell.text.strip()) > 0
149
+ ]
150
+ )
151
+ equation = TextElement(
152
+ label=cluster.label,
153
+ id=cluster.id,
154
+ cluster=cluster,
155
+ page_no=page.page_no,
156
+ text=text,
157
+ )
158
+ elements.append(equation)
159
+ body.append(equation)
152
160
 
153
- page.assembled = AssembledUnit(
154
- elements=elements, headers=headers, body=body
155
- )
161
+ page.assembled = AssembledUnit(
162
+ elements=elements, headers=headers, body=body
163
+ )
156
164
 
157
- # Remove page images (can be disabled)
158
- if not self.options.keep_images:
159
- page._image_cache = {}
165
+ # Remove page images (can be disabled)
166
+ if not self.options.keep_images:
167
+ page._image_cache = {}
160
168
 
161
- # Unload backend
162
- page._backend.unload()
169
+ # Unload backend
170
+ page._backend.unload()
163
171
 
164
- yield page
172
+ yield page
@@ -17,9 +17,13 @@ class PagePreprocessingModel(BasePageModel):
17
17
 
18
18
  def __call__(self, page_batch: Iterable[Page]) -> Iterable[Page]:
19
19
  for page in page_batch:
20
- page = self._populate_page_images(page)
21
- page = self._parse_page_cells(page)
22
- yield page
20
+ assert page._backend is not None
21
+ if not page._backend.is_valid():
22
+ yield page
23
+ else:
24
+ page = self._populate_page_images(page)
25
+ page = self._parse_page_cells(page)
26
+ yield page
23
27
 
24
28
  # Generate the page image and store it in the page object
25
29
  def _populate_page_images(self, page: Page) -> Page: