docling 1.19.1__py3-none-any.whl → 2.4.1__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.
Files changed (41) hide show
  1. docling/backend/abstract_backend.py +33 -37
  2. docling/backend/asciidoc_backend.py +431 -0
  3. docling/backend/docling_parse_backend.py +20 -16
  4. docling/backend/docling_parse_v2_backend.py +248 -0
  5. docling/backend/html_backend.py +429 -0
  6. docling/backend/md_backend.py +346 -0
  7. docling/backend/mspowerpoint_backend.py +398 -0
  8. docling/backend/msword_backend.py +496 -0
  9. docling/backend/pdf_backend.py +78 -0
  10. docling/backend/pypdfium2_backend.py +16 -11
  11. docling/cli/main.py +96 -65
  12. docling/datamodel/base_models.py +79 -193
  13. docling/datamodel/document.py +405 -320
  14. docling/datamodel/pipeline_options.py +19 -3
  15. docling/datamodel/settings.py +16 -1
  16. docling/document_converter.py +240 -251
  17. docling/models/base_model.py +28 -0
  18. docling/models/base_ocr_model.py +40 -10
  19. docling/models/ds_glm_model.py +244 -30
  20. docling/models/easyocr_model.py +57 -42
  21. docling/models/layout_model.py +158 -116
  22. docling/models/page_assemble_model.py +127 -101
  23. docling/models/page_preprocessing_model.py +79 -0
  24. docling/models/table_structure_model.py +162 -116
  25. docling/models/tesseract_ocr_cli_model.py +76 -59
  26. docling/models/tesseract_ocr_model.py +90 -58
  27. docling/pipeline/base_pipeline.py +189 -0
  28. docling/pipeline/simple_pipeline.py +56 -0
  29. docling/pipeline/standard_pdf_pipeline.py +201 -0
  30. docling/utils/export.py +4 -3
  31. docling/utils/layout_utils.py +17 -11
  32. docling/utils/profiling.py +62 -0
  33. docling-2.4.1.dist-info/METADATA +154 -0
  34. docling-2.4.1.dist-info/RECORD +45 -0
  35. docling/pipeline/base_model_pipeline.py +0 -18
  36. docling/pipeline/standard_model_pipeline.py +0 -66
  37. docling-1.19.1.dist-info/METADATA +0 -380
  38. docling-1.19.1.dist-info/RECORD +0 -34
  39. {docling-1.19.1.dist-info → docling-2.4.1.dist-info}/LICENSE +0 -0
  40. {docling-1.19.1.dist-info → docling-2.4.1.dist-info}/WHEEL +0 -0
  41. {docling-1.19.1.dist-info → docling-2.4.1.dist-info}/entry_points.txt +0 -0
@@ -1,68 +1,64 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from io import BytesIO
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Any, Iterable, Optional, Union
4
+ from typing import TYPE_CHECKING, Set, Union
5
5
 
6
- from PIL import Image
6
+ from docling_core.types.doc import DoclingDocument
7
7
 
8
8
  if TYPE_CHECKING:
9
- from docling.datamodel.base_models import BoundingBox, Cell, PageSize
9
+ from docling.datamodel.base_models import InputFormat
10
+ from docling.datamodel.document import InputDocument
10
11
 
11
12
 
12
- class PdfPageBackend(ABC):
13
-
13
+ class AbstractDocumentBackend(ABC):
14
14
  @abstractmethod
15
- def get_text_in_rect(self, bbox: "BoundingBox") -> str:
16
- pass
15
+ def __init__(self, in_doc: "InputDocument", path_or_stream: Union[BytesIO, Path]):
16
+ self.file = in_doc.file
17
+ self.path_or_stream = path_or_stream
18
+ self.document_hash = in_doc.document_hash
19
+ self.input_format = in_doc.format
17
20
 
18
21
  @abstractmethod
19
- def get_text_cells(self) -> Iterable["Cell"]:
22
+ def is_valid(self) -> bool:
20
23
  pass
21
24
 
25
+ @classmethod
22
26
  @abstractmethod
23
- def get_bitmap_rects(self, float: int = 1) -> Iterable["BoundingBox"]:
27
+ def supports_pagination(cls) -> bool:
24
28
  pass
25
29
 
26
30
  @abstractmethod
27
- def get_page_image(
28
- self, scale: float = 1, cropbox: Optional["BoundingBox"] = None
29
- ) -> Image.Image:
30
- pass
31
+ def unload(self):
32
+ if isinstance(self.path_or_stream, BytesIO):
33
+ self.path_or_stream.close()
31
34
 
32
- @abstractmethod
33
- def get_size(self) -> "PageSize":
34
- pass
35
+ self.path_or_stream = None
35
36
 
37
+ @classmethod
36
38
  @abstractmethod
37
- def is_valid(self) -> bool:
39
+ def supported_formats(cls) -> Set["InputFormat"]:
38
40
  pass
39
41
 
40
- @abstractmethod
41
- def unload(self):
42
- pass
43
42
 
43
+ class PaginatedDocumentBackend(AbstractDocumentBackend):
44
+ """DeclarativeDocumentBackend.
44
45
 
45
- class PdfDocumentBackend(ABC):
46
- @abstractmethod
47
- def __init__(self, path_or_stream: Union[BytesIO, Path], document_hash: str):
48
- self.path_or_stream = path_or_stream
49
- self.document_hash = document_hash
50
-
51
- @abstractmethod
52
- def load_page(self, page_no: int) -> PdfPageBackend:
53
- pass
46
+ A declarative document backend is a backend that can transform to DoclingDocument
47
+ straight without a recognition pipeline.
48
+ """
54
49
 
55
50
  @abstractmethod
56
51
  def page_count(self) -> int:
57
52
  pass
58
53
 
59
- @abstractmethod
60
- def is_valid(self) -> bool:
61
- pass
62
54
 
63
- @abstractmethod
64
- def unload(self):
65
- if isinstance(self.path_or_stream, BytesIO):
66
- self.path_or_stream.close()
55
+ class DeclarativeDocumentBackend(AbstractDocumentBackend):
56
+ """DeclarativeDocumentBackend.
67
57
 
68
- self.path_or_stream = None
58
+ A declarative document backend is a backend that can transform to DoclingDocument
59
+ straight without a recognition pipeline.
60
+ """
61
+
62
+ @abstractmethod
63
+ def convert(self) -> DoclingDocument:
64
+ pass
@@ -0,0 +1,431 @@
1
+ import logging
2
+ import re
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from typing import Set, Union
6
+
7
+ from docling_core.types.doc import (
8
+ DocItemLabel,
9
+ DoclingDocument,
10
+ DocumentOrigin,
11
+ GroupItem,
12
+ GroupLabel,
13
+ ImageRef,
14
+ Size,
15
+ TableCell,
16
+ TableData,
17
+ )
18
+
19
+ from docling.backend.abstract_backend import DeclarativeDocumentBackend
20
+ from docling.datamodel.base_models import InputFormat
21
+ from docling.datamodel.document import InputDocument
22
+
23
+ _log = logging.getLogger(__name__)
24
+
25
+
26
+ class AsciiDocBackend(DeclarativeDocumentBackend):
27
+
28
+ def __init__(self, in_doc: InputDocument, path_or_stream: Union[BytesIO, Path]):
29
+ super().__init__(in_doc, path_or_stream)
30
+
31
+ self.path_or_stream = path_or_stream
32
+
33
+ try:
34
+ if isinstance(self.path_or_stream, BytesIO):
35
+ text_stream = self.path_or_stream.getvalue().decode("utf-8")
36
+ self.lines = text_stream.split("\n")
37
+ if isinstance(self.path_or_stream, Path):
38
+ with open(self.path_or_stream, "r", encoding="utf-8") as f:
39
+ self.lines = f.readlines()
40
+ self.valid = True
41
+
42
+ except Exception as e:
43
+ raise RuntimeError(
44
+ f"Could not initialize AsciiDoc backend for file with hash {self.document_hash}."
45
+ ) from e
46
+ return
47
+
48
+ def is_valid(self) -> bool:
49
+ return self.valid
50
+
51
+ @classmethod
52
+ def supports_pagination(cls) -> bool:
53
+ return False
54
+
55
+ def unload(self):
56
+ return
57
+
58
+ @classmethod
59
+ def supported_formats(cls) -> Set[InputFormat]:
60
+ return {InputFormat.ASCIIDOC}
61
+
62
+ def convert(self) -> DoclingDocument:
63
+ """
64
+ Parses the ASCII into a structured document model.
65
+ """
66
+
67
+ origin = DocumentOrigin(
68
+ filename=self.file.name or "file",
69
+ mimetype="text/asciidoc",
70
+ binary_hash=self.document_hash,
71
+ )
72
+
73
+ doc = DoclingDocument(name=self.file.stem or "file", origin=origin)
74
+
75
+ doc = self._parse(doc)
76
+
77
+ return doc
78
+
79
+ def _parse(self, doc: DoclingDocument):
80
+ """
81
+ Main function that orchestrates the parsing by yielding components:
82
+ title, section headers, text, lists, and tables.
83
+ """
84
+
85
+ content = ""
86
+
87
+ in_list = False
88
+ in_table = False
89
+
90
+ text_data: list[str] = []
91
+ table_data: list[str] = []
92
+ caption_data: list[str] = []
93
+
94
+ # parents: dict[int, Union[DocItem, GroupItem, None]] = {}
95
+ parents: dict[int, Union[GroupItem, None]] = {}
96
+ # indents: dict[int, Union[DocItem, GroupItem, None]] = {}
97
+ indents: dict[int, Union[GroupItem, None]] = {}
98
+
99
+ for i in range(0, 10):
100
+ parents[i] = None
101
+ indents[i] = None
102
+
103
+ for line in self.lines:
104
+ # line = line.strip()
105
+
106
+ # Title
107
+ if self._is_title(line):
108
+ item = self._parse_title(line)
109
+ level = item["level"]
110
+
111
+ parents[level] = doc.add_text(
112
+ text=item["text"], label=DocItemLabel.TITLE
113
+ )
114
+
115
+ # Section headers
116
+ elif self._is_section_header(line):
117
+ item = self._parse_section_header(line)
118
+ level = item["level"]
119
+
120
+ parents[level] = doc.add_heading(
121
+ text=item["text"], level=item["level"], parent=parents[level - 1]
122
+ )
123
+ for k, v in parents.items():
124
+ if k > level:
125
+ parents[k] = None
126
+
127
+ # Lists
128
+ elif self._is_list_item(line):
129
+
130
+ _log.debug(f"line: {line}")
131
+ item = self._parse_list_item(line)
132
+ _log.debug(f"parsed list-item: {item}")
133
+
134
+ level = self._get_current_level(parents)
135
+
136
+ if not in_list:
137
+ in_list = True
138
+
139
+ parents[level + 1] = doc.add_group(
140
+ parent=parents[level], name="list", label=GroupLabel.LIST
141
+ )
142
+ indents[level + 1] = item["indent"]
143
+
144
+ elif in_list and item["indent"] > indents[level]:
145
+ parents[level + 1] = doc.add_group(
146
+ parent=parents[level], name="list", label=GroupLabel.LIST
147
+ )
148
+ indents[level + 1] = item["indent"]
149
+
150
+ elif in_list and item["indent"] < indents[level]:
151
+
152
+ # print(item["indent"], " => ", indents[level])
153
+ while item["indent"] < indents[level]:
154
+ # print(item["indent"], " => ", indents[level])
155
+ parents[level] = None
156
+ indents[level] = None
157
+ level -= 1
158
+
159
+ doc.add_list_item(
160
+ item["text"], parent=self._get_current_parent(parents)
161
+ )
162
+
163
+ elif in_list and not self._is_list_item(line):
164
+ in_list = False
165
+
166
+ level = self._get_current_level(parents)
167
+ parents[level] = None
168
+
169
+ # Tables
170
+ elif line.strip() == "|===" and not in_table: # start of table
171
+ in_table = True
172
+
173
+ elif self._is_table_line(line): # within a table
174
+ in_table = True
175
+ table_data.append(self._parse_table_line(line))
176
+
177
+ elif in_table and (
178
+ (not self._is_table_line(line)) or line.strip() == "|==="
179
+ ): # end of table
180
+
181
+ caption = None
182
+ if len(caption_data) > 0:
183
+ caption = doc.add_text(
184
+ text=" ".join(caption_data), label=DocItemLabel.CAPTION
185
+ )
186
+
187
+ caption_data = []
188
+
189
+ data = self._populate_table_as_grid(table_data)
190
+ doc.add_table(
191
+ data=data, parent=self._get_current_parent(parents), caption=caption
192
+ )
193
+
194
+ in_table = False
195
+ table_data = []
196
+
197
+ # Picture
198
+ elif self._is_picture(line):
199
+
200
+ caption = None
201
+ if len(caption_data) > 0:
202
+ caption = doc.add_text(
203
+ text=" ".join(caption_data), label=DocItemLabel.CAPTION
204
+ )
205
+
206
+ caption_data = []
207
+
208
+ item = self._parse_picture(line)
209
+
210
+ size = None
211
+ if "width" in item and "height" in item:
212
+ size = Size(width=int(item["width"]), height=int(item["height"]))
213
+
214
+ uri = None
215
+ if (
216
+ "uri" in item
217
+ and not item["uri"].startswith("http")
218
+ and item["uri"].startswith("//")
219
+ ):
220
+ uri = "file:" + item["uri"]
221
+ elif (
222
+ "uri" in item
223
+ and not item["uri"].startswith("http")
224
+ and item["uri"].startswith("/")
225
+ ):
226
+ uri = "file:/" + item["uri"]
227
+ elif "uri" in item and not item["uri"].startswith("http"):
228
+ uri = "file://" + item["uri"]
229
+
230
+ image = ImageRef(mimetype="image/png", size=size, dpi=70, uri=uri)
231
+ doc.add_picture(image=image, caption=caption)
232
+
233
+ # Caption
234
+ elif self._is_caption(line) and len(caption_data) == 0:
235
+ item = self._parse_caption(line)
236
+ caption_data.append(item["text"])
237
+
238
+ elif (
239
+ len(line.strip()) > 0 and len(caption_data) > 0
240
+ ): # allow multiline captions
241
+ item = self._parse_text(line)
242
+ caption_data.append(item["text"])
243
+
244
+ # Plain text
245
+ elif len(line.strip()) == 0 and len(text_data) > 0:
246
+ doc.add_text(
247
+ text=" ".join(text_data),
248
+ label=DocItemLabel.PARAGRAPH,
249
+ parent=self._get_current_parent(parents),
250
+ )
251
+ text_data = []
252
+
253
+ elif len(line.strip()) > 0: # allow multiline texts
254
+
255
+ item = self._parse_text(line)
256
+ text_data.append(item["text"])
257
+
258
+ if len(text_data) > 0:
259
+ doc.add_text(
260
+ text=" ".join(text_data),
261
+ label=DocItemLabel.PARAGRAPH,
262
+ parent=self._get_current_parent(parents),
263
+ )
264
+ text_data = []
265
+
266
+ if in_table and len(table_data) > 0:
267
+ data = self._populate_table_as_grid(table_data)
268
+ doc.add_table(data=data, parent=self._get_current_parent(parents))
269
+
270
+ in_table = False
271
+ table_data = []
272
+
273
+ return doc
274
+
275
+ def _get_current_level(self, parents):
276
+ for k, v in parents.items():
277
+ if v == None and k > 0:
278
+ return k - 1
279
+
280
+ return 0
281
+
282
+ def _get_current_parent(self, parents):
283
+ for k, v in parents.items():
284
+ if v == None and k > 0:
285
+ return parents[k - 1]
286
+
287
+ return None
288
+
289
+ # ========= Title
290
+ def _is_title(self, line):
291
+ return re.match(r"^= ", line)
292
+
293
+ def _parse_title(self, line):
294
+ return {"type": "title", "text": line[2:].strip(), "level": 0}
295
+
296
+ # ========= Section headers
297
+ def _is_section_header(self, line):
298
+ return re.match(r"^==+", line)
299
+
300
+ def _parse_section_header(self, line):
301
+ match = re.match(r"^(=+)\s+(.*)", line)
302
+
303
+ marker = match.group(1) # The list marker (e.g., "*", "-", "1.")
304
+ text = match.group(2) # The actual text of the list item
305
+
306
+ header_level = marker.count("=") # number of '=' represents level
307
+ return {
308
+ "type": "header",
309
+ "level": header_level - 1,
310
+ "text": text.strip(),
311
+ }
312
+
313
+ # ========= Lists
314
+ def _is_list_item(self, line):
315
+ return re.match(r"^(\s)*(\*|-|\d+\.|\w+\.) ", line)
316
+
317
+ def _parse_list_item(self, line):
318
+ """Extract the item marker (number or bullet symbol) and the text of the item."""
319
+
320
+ match = re.match(r"^(\s*)(\*|-|\d+\.)\s+(.*)", line)
321
+ if match:
322
+ indent = match.group(1)
323
+ marker = match.group(2) # The list marker (e.g., "*", "-", "1.")
324
+ text = match.group(3) # The actual text of the list item
325
+
326
+ if marker == "*" or marker == "-":
327
+ return {
328
+ "type": "list_item",
329
+ "marker": marker,
330
+ "text": text.strip(),
331
+ "numbered": False,
332
+ "indent": 0 if indent == None else len(indent),
333
+ }
334
+ else:
335
+ return {
336
+ "type": "list_item",
337
+ "marker": marker,
338
+ "text": text.strip(),
339
+ "numbered": True,
340
+ "indent": 0 if indent == None else len(indent),
341
+ }
342
+ else:
343
+ # Fallback if no match
344
+ return {
345
+ "type": "list_item",
346
+ "marker": "-",
347
+ "text": line,
348
+ "numbered": False,
349
+ "indent": 0,
350
+ }
351
+
352
+ # ========= Tables
353
+ def _is_table_line(self, line):
354
+ return re.match(r"^\|.*\|", line)
355
+
356
+ def _parse_table_line(self, line):
357
+ # Split table cells and trim extra spaces
358
+ return [cell.strip() for cell in line.split("|") if cell.strip()]
359
+
360
+ def _populate_table_as_grid(self, table_data):
361
+
362
+ num_rows = len(table_data)
363
+
364
+ # Adjust the table data into a grid format
365
+ num_cols = max(len(row) for row in table_data)
366
+
367
+ data = TableData(num_rows=num_rows, num_cols=num_cols, table_cells=[])
368
+ for row_idx, row in enumerate(table_data):
369
+ # Pad rows with empty strings to match column count
370
+ # grid.append(row + [''] * (max_cols - len(row)))
371
+
372
+ for col_idx, text in enumerate(row):
373
+ row_span = 1
374
+ col_span = 1
375
+
376
+ cell = TableCell(
377
+ text=text,
378
+ row_span=row_span,
379
+ col_span=col_span,
380
+ start_row_offset_idx=row_idx,
381
+ end_row_offset_idx=row_idx + row_span,
382
+ start_col_offset_idx=col_idx,
383
+ end_col_offset_idx=col_idx + col_span,
384
+ col_header=False,
385
+ row_header=False,
386
+ )
387
+ data.table_cells.append(cell)
388
+
389
+ return data
390
+
391
+ # ========= Pictures
392
+ def _is_picture(self, line):
393
+ return re.match(r"^image::", line)
394
+
395
+ def _parse_picture(self, line):
396
+ """
397
+ Parse an image macro, extracting its path and attributes.
398
+ Syntax: image::path/to/image.png[Alt Text, width=200, height=150, align=center]
399
+ """
400
+ mtch = re.match(r"^image::(.+)\[(.*)\]$", line)
401
+ if mtch:
402
+ picture_path = mtch.group(1).strip()
403
+ attributes = mtch.group(2).split(",")
404
+ picture_info = {"type": "picture", "uri": picture_path}
405
+
406
+ # Extract optional attributes (alt text, width, height, alignment)
407
+ if attributes:
408
+ picture_info["alt"] = attributes[0].strip() if attributes[0] else ""
409
+ for attr in attributes[1:]:
410
+ key, value = attr.split("=")
411
+ picture_info[key.strip()] = value.strip()
412
+
413
+ return picture_info
414
+
415
+ return {"type": "picture", "uri": line}
416
+
417
+ # ========= Captions
418
+ def _is_caption(self, line):
419
+ return re.match(r"^\.(.+)", line)
420
+
421
+ def _parse_caption(self, line):
422
+ mtch = re.match(r"^\.(.+)", line)
423
+ if mtch:
424
+ text = mtch.group(1)
425
+ return {"type": "caption", "text": text}
426
+
427
+ return {"type": "caption", "text": ""}
428
+
429
+ # ========= Plain text
430
+ def _parse_text(self, line):
431
+ return {"type": "text", "text": line.strip()}
@@ -5,19 +5,21 @@ from pathlib import Path
5
5
  from typing import Iterable, List, Optional, Union
6
6
 
7
7
  import pypdfium2 as pdfium
8
- from docling_parse.docling_parse import pdf_parser
8
+ from docling_core.types.doc import BoundingBox, CoordOrigin, Size
9
+ from docling_parse.docling_parse import pdf_parser_v1
9
10
  from PIL import Image, ImageDraw
10
11
  from pypdfium2 import PdfPage
11
12
 
12
- from docling.backend.abstract_backend import PdfDocumentBackend, PdfPageBackend
13
- from docling.datamodel.base_models import BoundingBox, Cell, CoordOrigin, PageSize
13
+ from docling.backend.pdf_backend import PdfDocumentBackend, PdfPageBackend
14
+ from docling.datamodel.base_models import Cell
15
+ from docling.datamodel.document import InputDocument
14
16
 
15
17
  _log = logging.getLogger(__name__)
16
18
 
17
19
 
18
20
  class DoclingParsePageBackend(PdfPageBackend):
19
21
  def __init__(
20
- self, parser: pdf_parser, document_hash: str, page_no: int, page_obj: PdfPage
22
+ self, parser: pdf_parser_v1, document_hash: str, page_no: int, page_obj: PdfPage
21
23
  ):
22
24
  self._ppage = page_obj
23
25
  parsed_page = parser.parse_pdf_from_key_on_page(document_hash, page_no)
@@ -27,7 +29,7 @@ class DoclingParsePageBackend(PdfPageBackend):
27
29
  self._dpage = parsed_page["pages"][0]
28
30
  else:
29
31
  _log.info(
30
- f"An error occured when loading page {page_no} of document {document_hash}."
32
+ f"An error occurred when loading page {page_no} of document {document_hash}."
31
33
  )
32
34
 
33
35
  def is_valid(self) -> bool:
@@ -177,8 +179,8 @@ class DoclingParsePageBackend(PdfPageBackend):
177
179
 
178
180
  return image
179
181
 
180
- def get_size(self) -> PageSize:
181
- return PageSize(width=self._ppage.get_width(), height=self._ppage.get_height())
182
+ def get_size(self) -> Size:
183
+ return Size(width=self._ppage.get_width(), height=self._ppage.get_height())
182
184
 
183
185
  def unload(self):
184
186
  self._ppage = None
@@ -186,23 +188,25 @@ class DoclingParsePageBackend(PdfPageBackend):
186
188
 
187
189
 
188
190
  class DoclingParseDocumentBackend(PdfDocumentBackend):
189
- def __init__(self, path_or_stream: Union[BytesIO, Path], document_hash: str):
190
- super().__init__(path_or_stream, document_hash)
191
+ def __init__(self, in_doc: "InputDocument", path_or_stream: Union[BytesIO, Path]):
192
+ super().__init__(in_doc, path_or_stream)
191
193
 
192
- self._pdoc = pdfium.PdfDocument(path_or_stream)
193
- self.parser = pdf_parser()
194
+ self._pdoc = pdfium.PdfDocument(self.path_or_stream)
195
+ self.parser = pdf_parser_v1()
194
196
 
195
197
  success = False
196
- if isinstance(path_or_stream, BytesIO):
198
+ if isinstance(self.path_or_stream, BytesIO):
197
199
  success = self.parser.load_document_from_bytesio(
198
- document_hash, path_or_stream
200
+ self.document_hash, self.path_or_stream
201
+ )
202
+ elif isinstance(self.path_or_stream, Path):
203
+ success = self.parser.load_document(
204
+ self.document_hash, str(self.path_or_stream)
199
205
  )
200
- elif isinstance(path_or_stream, Path):
201
- success = self.parser.load_document(document_hash, str(path_or_stream))
202
206
 
203
207
  if not success:
204
208
  raise RuntimeError(
205
- f"docling-parse could not load document {document_hash}."
209
+ f"docling-parse could not load document with hash {self.document_hash}."
206
210
  )
207
211
 
208
212
  def page_count(self) -> int: