docling-core 2.85.0__tar.gz → 2.86.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.
Files changed (118) hide show
  1. {docling_core-2.85.0 → docling_core-2.86.0}/PKG-INFO +1 -1
  2. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/cli/view.py +1 -2
  3. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/document.py +111 -95
  4. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/page.py +6 -12
  5. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/utils.py +39 -7
  6. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/generate_docs.py +7 -6
  7. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core.egg-info/PKG-INFO +1 -1
  8. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core.egg-info/SOURCES.txt +1 -0
  9. {docling_core-2.85.0 → docling_core-2.86.0}/pyproject.toml +2 -1
  10. docling_core-2.86.0/test/test_upath_support.py +182 -0
  11. {docling_core-2.85.0 → docling_core-2.86.0}/LICENSE +0 -0
  12. {docling_core-2.85.0 → docling_core-2.86.0}/README.md +0 -0
  13. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/__init__.py +0 -0
  14. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/cli/__init__.py +0 -0
  15. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/cli/serialize.py +0 -0
  16. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/experimental/__init__.py +0 -0
  17. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/experimental/serializer/__init__.py +0 -0
  18. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/experimental/serializer/outline.py +0 -0
  19. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/py.typed +0 -0
  20. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/search/__init__.py +0 -0
  21. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/search/package.py +0 -0
  22. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/__init__.py +0 -0
  23. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/__init__.py +0 -0
  24. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/base.py +0 -0
  25. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/chunk_expander.py +0 -0
  26. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/code_chunking/__init__.py +0 -0
  27. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/code_chunking/_language_code_chunkers.py +0 -0
  28. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/code_chunking/_utils.py +0 -0
  29. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/code_chunking/base_code_chunking_strategy.py +0 -0
  30. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/code_chunking/code_chunk.py +0 -0
  31. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/code_chunking/standard_code_chunking_strategy.py +0 -0
  32. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/doc_chunk.py +0 -0
  33. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/hierarchical_chunker.py +0 -0
  34. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/hybrid_chunker.py +0 -0
  35. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/line_chunker.py +0 -0
  36. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/page_chunker.py +0 -0
  37. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/tokenizer/__init__.py +0 -0
  38. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/tokenizer/base.py +0 -0
  39. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/tokenizer/huggingface.py +0 -0
  40. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/chunker/tokenizer/openai.py +0 -0
  41. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/deserializer/__init__.py +0 -0
  42. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/deserializer/base.py +0 -0
  43. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/deserializer/doclang.py +0 -0
  44. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/profiler/__init__.py +0 -0
  45. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/profiler/doc_profiler.py +0 -0
  46. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/__init__.py +0 -0
  47. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/_doclang_utils.py +0 -0
  48. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/azure.py +0 -0
  49. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/base.py +0 -0
  50. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/common.py +0 -0
  51. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/doclang.py +0 -0
  52. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/doctags.py +0 -0
  53. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/html.py +0 -0
  54. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/html_styles.py +0 -0
  55. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/latex.py +0 -0
  56. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/markdown.py +0 -0
  57. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/markdown_excel.py +0 -0
  58. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/plain_text.py +0 -0
  59. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/serializer/webvtt.py +0 -0
  60. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/visualizer/__init__.py +0 -0
  61. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/visualizer/base.py +0 -0
  62. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/visualizer/key_value_visualizer.py +0 -0
  63. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/visualizer/layout_visualizer.py +0 -0
  64. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/visualizer/reading_order_visualizer.py +0 -0
  65. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/transforms/visualizer/table_visualizer.py +0 -0
  66. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/__init__.py +0 -0
  67. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/base.py +0 -0
  68. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/__init__.py +0 -0
  69. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/base.py +0 -0
  70. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/labels.py +0 -0
  71. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/tokens.py +0 -0
  72. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/doc/webvtt.py +0 -0
  73. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/io/__init__.py +0 -0
  74. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/legacy_doc/__init__.py +0 -0
  75. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/legacy_doc/base.py +0 -0
  76. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/types/legacy_doc/document.py +0 -0
  77. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/__init__.py +0 -0
  78. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/alias.py +0 -0
  79. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/file.py +0 -0
  80. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/generate_jsonschema.py +0 -0
  81. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/legacy.py +0 -0
  82. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/settings.py +0 -0
  83. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core/utils/validators.py +0 -0
  84. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core.egg-info/dependency_links.txt +0 -0
  85. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core.egg-info/entry_points.txt +0 -0
  86. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core.egg-info/requires.txt +0 -0
  87. {docling_core-2.85.0 → docling_core-2.86.0}/docling_core.egg-info/top_level.txt +0 -0
  88. {docling_core-2.85.0 → docling_core-2.86.0}/setup.cfg +0 -0
  89. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_azure_serializer.py +0 -0
  90. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_base.py +0 -0
  91. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_chunk_expander.py +0 -0
  92. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_code_chunker.py +0 -0
  93. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_code_chunking_strategy.py +0 -0
  94. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_data_gen_flag.py +0 -0
  95. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_deserializer_doclang.py +0 -0
  96. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_doc_base.py +0 -0
  97. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_doclang_archive.py +0 -0
  98. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_docling_doc.py +0 -0
  99. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_doctags_load.py +0 -0
  100. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_hierarchical_chunker.py +0 -0
  101. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_hierarchy.py +0 -0
  102. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_hybrid_chunker.py +0 -0
  103. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_latex_serialization.py +0 -0
  104. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_line_chunker.py +0 -0
  105. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_metadata.py +0 -0
  106. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_otsl_table_export.py +0 -0
  107. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_page.py +0 -0
  108. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_page_chunker.py +0 -0
  109. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_plain_text_serialization.py +0 -0
  110. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_profiler.py +0 -0
  111. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_regions_to_table.py +0 -0
  112. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_serialization.py +0 -0
  113. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_serialization_doclang.py +0 -0
  114. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_serialization_doctag.py +0 -0
  115. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_serialization_outline.py +0 -0
  116. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_utils.py +0 -0
  117. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_visualization.py +0 -0
  118. {docling_core-2.85.0 → docling_core-2.86.0}/test/test_webvtt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docling-core
3
- Version: 2.85.0
3
+ Version: 2.86.0
4
4
  Summary: A python library to define and validate data types in Docling.
5
5
  Author-email: Cesar Berrospi Ramis <ceb@zurich.ibm.com>, Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>
6
6
  Maintainer-email: Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>, Cesar Berrospi Ramis <ceb@zurich.ibm.com>
@@ -69,8 +69,7 @@ def view(
69
69
  image_mode=ImageRefMode.EMBEDDED,
70
70
  split_page_view=split_view,
71
71
  )
72
- with open(target_path, "w", encoding="utf-8") as f:
73
- f.write(html_output)
72
+ target_path.write_text(html_output, encoding="utf-8")
74
73
  webbrowser.open(url=f"file://{target_path.absolute().resolve()}")
75
74
 
76
75
 
@@ -6,7 +6,6 @@ import hashlib
6
6
  import json
7
7
  import logging
8
8
  import mimetypes
9
- import os
10
9
  import re
11
10
  import sys
12
11
  import tempfile
@@ -15,7 +14,7 @@ import warnings
15
14
  from collections.abc import Iterable, Sequence
16
15
  from dataclasses import dataclass
17
16
  from enum import Enum
18
- from io import BytesIO
17
+ from io import BytesIO, StringIO
19
18
  from pathlib import Path
20
19
  from typing import (
21
20
  Annotated,
@@ -69,6 +68,7 @@ from docling_core.types.doc.labels import (
69
68
  )
70
69
  from docling_core.types.doc.tokens import DocumentToken, TableToken
71
70
  from docling_core.types.doc.utils import (
71
+ is_remote_path,
72
72
  parse_otsl_table_content,
73
73
  relative_path,
74
74
  resolve_archive_path,
@@ -1074,22 +1074,23 @@ class DocumentOrigin(BaseModel):
1074
1074
  )
1075
1075
 
1076
1076
  _extra_mimetypes: typing.ClassVar[list[str]] = [
1077
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1078
- "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
1079
- "application/vnd.openxmlformats-officedocument.presentationml.template",
1080
- "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
1077
+ "application/vnd.box.boxnote",
1078
+ "application/vnd.oasis.opendocument.presentation",
1079
+ "application/vnd.oasis.opendocument.spreadsheet",
1080
+ "application/vnd.oasis.opendocument.text",
1081
1081
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1082
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
1083
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
1082
1084
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1083
- "application/vnd.oasis.opendocument.text",
1084
- "application/vnd.oasis.opendocument.spreadsheet",
1085
- "application/vnd.oasis.opendocument.presentation",
1085
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1086
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
1087
+ "audio/mp3",
1088
+ "audio/wav",
1089
+ "audio/x-wav",
1086
1090
  "text/asciidoc",
1087
- "text/markdown",
1088
1091
  "text/csv",
1092
+ "text/markdown",
1089
1093
  "text/vtt",
1090
- "audio/x-wav",
1091
- "audio/wav",
1092
- "audio/mp3",
1093
1094
  ]
1094
1095
 
1095
1096
  @field_validator("binary_hash", mode="before")
@@ -6070,6 +6071,39 @@ class DoclingDocument(BaseModel):
6070
6071
 
6071
6072
  return result
6072
6073
 
6074
+ @staticmethod
6075
+ def _save_image_and_resolve_uri(
6076
+ img: PILImage.Image,
6077
+ loc_path: Path,
6078
+ reference_path: Optional[Path],
6079
+ ) -> Union[AnyUrl, Path]:
6080
+ """Save *img* to *loc_path* and return the URI to store on the ImageRef.
6081
+
6082
+ Uses a BytesIO intermediate buffer so that the write is compatible with
6083
+ UPath remote backends (PIL cannot write directly to non-local paths).
6084
+ For remote paths the URI is returned as an absolute AnyUrl string; for
6085
+ local paths it is returned relative to *reference_path* when available,
6086
+ or as the absolute *loc_path* when *reference_path* is None.
6087
+
6088
+ Args:
6089
+ img: The PIL image to save.
6090
+ loc_path: Destination path (local or remote UPath).
6091
+ reference_path: Base path used to compute a relative URI for local
6092
+ storage. Pass ``None`` to store the absolute path instead.
6093
+
6094
+ Returns:
6095
+ An AnyUrl for remote paths, or a Path (relative or absolute) for local paths.
6096
+ """
6097
+ buf = BytesIO()
6098
+ img.save(buf, format="PNG")
6099
+ loc_path.write_bytes(buf.getvalue())
6100
+
6101
+ if is_remote_path(loc_path) or is_remote_path(reference_path):
6102
+ return AnyUrl(str(loc_path))
6103
+ if reference_path is not None:
6104
+ return relative_path(reference_path.resolve(), loc_path.resolve())
6105
+ return loc_path
6106
+
6073
6107
  def _with_pictures_refs(
6074
6108
  self,
6075
6109
  image_dir: Path,
@@ -6088,65 +6122,37 @@ class DoclingDocument(BaseModel):
6088
6122
  """
6089
6123
  result: DoclingDocument = copy.deepcopy(self)
6090
6124
 
6091
- img_count = 0
6092
6125
  image_dir.mkdir(parents=True, exist_ok=True)
6093
6126
 
6094
- if image_dir.is_dir():
6095
- for item, _ in result.iterate_items(page_no=page_no, with_groups=False):
6096
- if isinstance(item, PictureItem):
6097
- img = item.get_image(doc=self)
6098
- if img is not None:
6099
- hexhash = PictureItem._image_to_hexhash(img)
6100
-
6101
- # loc_path = image_dir / f"image_{img_count:06}.png"
6102
- if hexhash is not None:
6103
- loc_path = image_dir / f"image_{img_count:06}_{hexhash}.png"
6104
-
6105
- img.save(loc_path)
6106
- if reference_path is not None:
6107
- obj_path = relative_path(
6108
- reference_path.resolve(),
6109
- loc_path.resolve(),
6110
- )
6111
- else:
6112
- obj_path = loc_path
6113
-
6114
- if item.image is None:
6115
- scale = img.size[0] / item.prov[0].bbox.width
6116
- item.image = ImageRef.from_pil(image=img, dpi=round(72 * scale))
6117
- item.image.uri = Path(obj_path)
6118
- elif item.image is not None:
6119
- item.image.uri = Path(obj_path)
6120
-
6121
- # if item.image._pil is not None:
6122
- # item.image._pil.close()
6123
-
6124
- img_count += 1
6125
-
6126
- if include_page_images:
6127
- for p_no, page in result.pages.items():
6128
- if page_no is not None and p_no != page_no:
6129
- continue
6130
- if page.image is None:
6131
- continue
6132
- img = page.image.pil_image
6133
- if img is None:
6134
- continue
6127
+ img_count = 0
6128
+ for item, _ in result.iterate_items(page_no=page_no, with_groups=False):
6129
+ if isinstance(item, PictureItem):
6130
+ img = item.get_image(doc=self)
6131
+ if img is not None:
6135
6132
  hexhash = PictureItem._image_to_hexhash(img)
6136
- if hexhash is None:
6137
- continue
6138
-
6139
- loc_path = image_dir / f"page_{p_no:06}_{hexhash}.png"
6140
- img.save(loc_path)
6141
- if reference_path is not None:
6142
- obj_path = relative_path(
6143
- reference_path.resolve(),
6144
- loc_path.resolve(),
6145
- )
6146
- else:
6147
- obj_path = loc_path
6148
-
6149
- page.image.uri = Path(obj_path)
6133
+ if hexhash is not None:
6134
+ loc_path = image_dir / f"image_{img_count:06}_{hexhash}.png"
6135
+ obj_path = self._save_image_and_resolve_uri(img, loc_path, reference_path)
6136
+ if item.image is None:
6137
+ scale = img.size[0] / item.prov[0].bbox.width
6138
+ item.image = ImageRef.from_pil(image=img, dpi=round(72 * scale))
6139
+ item.image.uri = obj_path # type: ignore[assignment]
6140
+ img_count += 1
6141
+
6142
+ if include_page_images:
6143
+ for p_no, page in result.pages.items():
6144
+ if page_no is not None and p_no != page_no:
6145
+ continue
6146
+ if page.image is None:
6147
+ continue
6148
+ img = page.image.pil_image
6149
+ if img is None:
6150
+ continue
6151
+ hexhash = PictureItem._image_to_hexhash(img)
6152
+ if hexhash is None:
6153
+ continue
6154
+ loc_path = image_dir / f"page_{p_no:06}_{hexhash}.png"
6155
+ page.image.uri = self._save_image_and_resolve_uri(img, loc_path, reference_path) # type: ignore[assignment]
6150
6156
 
6151
6157
  return result
6152
6158
 
@@ -6207,7 +6213,7 @@ class DoclingDocument(BaseModel):
6207
6213
  artifacts_dir, reference_path = self._get_output_paths(filename, artifacts_dir)
6208
6214
 
6209
6215
  if image_mode == ImageRefMode.REFERENCED:
6210
- os.makedirs(artifacts_dir, exist_ok=True)
6216
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
6211
6217
 
6212
6218
  new_doc = self._make_copy_with_refmode(
6213
6219
  artifacts_dir,
@@ -6218,8 +6224,7 @@ class DoclingDocument(BaseModel):
6218
6224
  )
6219
6225
 
6220
6226
  out = new_doc.export_to_dict(coord_precision=coord_precision, confid_precision=confid_precision)
6221
- with open(filename, "w", encoding="utf-8") as fw:
6222
- json.dump(out, fw, indent=indent)
6227
+ filename.write_text(json.dumps(out, indent=indent), encoding="utf-8")
6223
6228
 
6224
6229
  @classmethod
6225
6230
  def load_from_json(cls, filename: Union[str, Path]) -> "DoclingDocument":
@@ -6234,8 +6239,7 @@ class DoclingDocument(BaseModel):
6234
6239
  """
6235
6240
  if isinstance(filename, str):
6236
6241
  filename = Path(filename)
6237
- with open(filename, encoding="utf-8") as f:
6238
- return cls.model_validate_json(f.read())
6242
+ return cls.model_validate_json(filename.read_text(encoding="utf-8"))
6239
6243
 
6240
6244
  def save_as_yaml(
6241
6245
  self,
@@ -6252,7 +6256,7 @@ class DoclingDocument(BaseModel):
6252
6256
  artifacts_dir, reference_path = self._get_output_paths(filename, artifacts_dir)
6253
6257
 
6254
6258
  if image_mode == ImageRefMode.REFERENCED:
6255
- os.makedirs(artifacts_dir, exist_ok=True)
6259
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
6256
6260
 
6257
6261
  new_doc = self._make_copy_with_refmode(
6258
6262
  artifacts_dir,
@@ -6263,8 +6267,9 @@ class DoclingDocument(BaseModel):
6263
6267
  )
6264
6268
 
6265
6269
  out = new_doc.export_to_dict(coord_precision=coord_precision, confid_precision=confid_precision)
6266
- with open(filename, "w", encoding="utf-8") as fw:
6267
- yaml.dump(out, fw, default_flow_style=default_flow_style)
6270
+ stream = StringIO()
6271
+ yaml.dump(out, stream, default_flow_style=default_flow_style)
6272
+ filename.write_text(stream.getvalue(), encoding="utf-8")
6268
6273
 
6269
6274
  @classmethod
6270
6275
  def load_from_yaml(cls, filename: Union[str, Path]) -> "DoclingDocument":
@@ -6278,8 +6283,7 @@ class DoclingDocument(BaseModel):
6278
6283
  """
6279
6284
  if isinstance(filename, str):
6280
6285
  filename = Path(filename)
6281
- with open(filename, encoding="utf-8") as f:
6282
- data = yaml.load(f, Loader=yaml.SafeLoader)
6286
+ data = yaml.load(filename.read_text(encoding="utf-8"), Loader=yaml.SafeLoader)
6283
6287
  return DoclingDocument.model_validate(data)
6284
6288
 
6285
6289
  def export_to_dict(
@@ -6330,7 +6334,7 @@ class DoclingDocument(BaseModel):
6330
6334
  artifacts_dir, reference_path = self._get_output_paths(filename, artifacts_dir)
6331
6335
 
6332
6336
  if image_mode == ImageRefMode.REFERENCED:
6333
- os.makedirs(artifacts_dir, exist_ok=True)
6337
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
6334
6338
 
6335
6339
  new_doc = self._make_copy_with_refmode(artifacts_dir, image_mode, page_no, reference_path=reference_path)
6336
6340
 
@@ -6355,8 +6359,7 @@ class DoclingDocument(BaseModel):
6355
6359
  mark_meta=mark_meta,
6356
6360
  )
6357
6361
 
6358
- with open(filename, "w", encoding="utf-8") as fw:
6359
- fw.write(md_out)
6362
+ filename.write_text(md_out, encoding="utf-8")
6360
6363
 
6361
6364
  def export_to_markdown(
6362
6365
  self,
@@ -6592,7 +6595,7 @@ class DoclingDocument(BaseModel):
6592
6595
  artifacts_dir, reference_path = self._get_output_paths(filename, artifacts_dir)
6593
6596
 
6594
6597
  if image_mode == ImageRefMode.REFERENCED:
6595
- os.makedirs(artifacts_dir, exist_ok=True)
6598
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
6596
6599
 
6597
6600
  new_doc = self._make_copy_with_refmode(artifacts_dir, image_mode, page_no, reference_path=reference_path)
6598
6601
 
@@ -6610,16 +6613,30 @@ class DoclingDocument(BaseModel):
6610
6613
  include_annotations=include_annotations,
6611
6614
  )
6612
6615
 
6613
- with open(filename, "w", encoding="utf-8") as fw:
6614
- fw.write(html_out)
6616
+ filename.write_text(html_out, encoding="utf-8")
6615
6617
 
6616
6618
  def _get_output_paths(
6617
- self, filename: Union[str, Path], artifacts_dir: Optional[Path] = None
6619
+ self,
6620
+ filename: Path,
6621
+ artifacts_dir: Optional[Path] = None,
6618
6622
  ) -> tuple[Path, Optional[Path]]:
6619
- if isinstance(filename, str):
6620
- filename = Path(filename)
6623
+ """Resolve output and artifacts directory paths from the given filename.
6624
+
6625
+ Both ``Path`` and ``UPath`` objects are accepted since ``UPath`` is ``Path``-compatible for local paths, and remote ``UPath`` objects implement the same interface used here (``with_suffix``, ``with_name``,
6626
+ ``is_absolute``, ``parent``, ``/``).
6627
+
6628
+ Args:
6629
+ filename: Destination file path.
6630
+ artifacts_dir: Optional explicit artifacts directory. When ``None``, a sibling
6631
+ directory named ``<stem>_artifacts`` is derived from ``filename``.
6632
+
6633
+ Returns:
6634
+ A tuple of ``(artifacts_dir, reference_path)`` where ``reference_path`` is
6635
+ the parent of ``filename`` when ``artifacts_dir`` is relative, or ``None``
6636
+ when it is absolute.
6637
+ """
6621
6638
  if artifacts_dir is None:
6622
- # Remove the extension and add '_pictures'
6639
+ # Remove the extension and add '_artifacts'
6623
6640
  artifacts_dir = filename.with_suffix("")
6624
6641
  artifacts_dir = artifacts_dir.with_name(artifacts_dir.name + "_artifacts")
6625
6642
  if artifacts_dir.is_absolute():
@@ -6767,8 +6784,7 @@ class DoclingDocument(BaseModel):
6767
6784
  omit_voice_end=omit_voice_end,
6768
6785
  )
6769
6786
 
6770
- with open(filename, "w", encoding="utf-8") as fw:
6771
- fw.write(vtt_out)
6787
+ filename.write_text(vtt_out, encoding="utf-8")
6772
6788
 
6773
6789
  @staticmethod
6774
6790
  def load_from_doctags( # noqa: C901
@@ -7379,8 +7395,7 @@ class DoclingDocument(BaseModel):
7379
7395
  minified=minified,
7380
7396
  )
7381
7397
 
7382
- with open(filename, "w", encoding="utf-8") as fw:
7383
- fw.write(out)
7398
+ filename.write_text(out, encoding="utf-8")
7384
7399
 
7385
7400
  @deprecated("Use export_to_doctags() instead.")
7386
7401
  def export_to_document_tokens(self, *args, **kwargs):
@@ -7470,9 +7485,10 @@ class DoclingDocument(BaseModel):
7470
7485
  filename: Union[str, Path],
7471
7486
  ) -> None:
7472
7487
  """Save the document as DocLang."""
7488
+ if isinstance(filename, str):
7489
+ filename = Path(filename)
7473
7490
  out = self.export_to_doclang()
7474
- with open(filename, "w", encoding="utf-8") as fw:
7475
- fw.write(f"{out}\n")
7491
+ filename.write_text(f"{out}\n", encoding="utf-8")
7476
7492
 
7477
7493
  def save_as_doclang_archive(
7478
7494
  self,
@@ -688,8 +688,7 @@ class SegmentedPdfPage(SegmentedPage):
688
688
  if isinstance(filename, str):
689
689
  filename = Path(filename)
690
690
  out = self.export_to_dict()
691
- with open(filename, "w", encoding="utf-8") as fw:
692
- json.dump(out, fw, indent=indent)
691
+ filename.write_text(json.dumps(out, indent=indent), encoding="utf-8")
693
692
 
694
693
  @classmethod
695
694
  def load_from_json(cls, filename: Union[str, Path]) -> "SegmentedPdfPage":
@@ -703,8 +702,7 @@ class SegmentedPdfPage(SegmentedPage):
703
702
  """
704
703
  if isinstance(filename, str):
705
704
  filename = Path(filename)
706
- with open(filename, encoding="utf-8") as f:
707
- return cls.model_validate_json(f.read())
705
+ return cls.model_validate_json(filename.read_text(encoding="utf-8"))
708
706
 
709
707
  def crop_text(self, cell_unit: TextCellUnit, bbox: BoundingBox, eps: float = 1.0) -> str:
710
708
  """Extract text from cells within the specified bounding box.
@@ -1390,8 +1388,7 @@ class PdfTableOfContents(BaseModel):
1390
1388
  if isinstance(filename, str):
1391
1389
  filename = Path(filename)
1392
1390
  out = self.export_to_dict()
1393
- with open(filename, "w", encoding="utf-8") as fw:
1394
- json.dump(out, fw, indent=indent)
1391
+ filename.write_text(json.dumps(out, indent=indent), encoding="utf-8")
1395
1392
 
1396
1393
  @classmethod
1397
1394
  def load_from_json(cls, filename: Union[str, Path]) -> "PdfTableOfContents":
@@ -1405,8 +1402,7 @@ class PdfTableOfContents(BaseModel):
1405
1402
  """
1406
1403
  if isinstance(filename, str):
1407
1404
  filename = Path(filename)
1408
- with open(filename, encoding="utf-8") as f:
1409
- return cls.model_validate_json(f.read())
1405
+ return cls.model_validate_json(filename.read_text(encoding="utf-8"))
1410
1406
 
1411
1407
 
1412
1408
  class ParsedPdfDocument(BaseModel):
@@ -1451,8 +1447,7 @@ class ParsedPdfDocument(BaseModel):
1451
1447
  if isinstance(filename, str):
1452
1448
  filename = Path(filename)
1453
1449
  out = self.export_to_dict()
1454
- with open(filename, "w", encoding="utf-8") as fw:
1455
- json.dump(out, fw, indent=indent)
1450
+ filename.write_text(json.dumps(out, indent=indent), encoding="utf-8")
1456
1451
 
1457
1452
  @classmethod
1458
1453
  def load_from_json(cls, filename: Union[str, Path]) -> "ParsedPdfDocument":
@@ -1466,5 +1461,4 @@ class ParsedPdfDocument(BaseModel):
1466
1461
  """
1467
1462
  if isinstance(filename, str):
1468
1463
  filename = Path(filename)
1469
- with open(filename, encoding="utf-8") as f:
1470
- return cls.model_validate_json(f.read())
1464
+ return cls.model_validate_json(filename.read_text(encoding="utf-8"))
@@ -7,7 +7,7 @@ import shutil
7
7
  import unicodedata
8
8
  import zipfile
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Optional
10
+ from typing import TYPE_CHECKING, Any
11
11
 
12
12
  from docling_core.types.doc.tokens import _LOC_PREFIX, DocumentToken, TableToken
13
13
 
@@ -15,21 +15,53 @@ if TYPE_CHECKING:
15
15
  from docling_core.types.doc.document import TableCell, TableData
16
16
 
17
17
 
18
- def relative_path(src: Path, target: Path) -> Path:
18
+ def is_remote_path(p: Any) -> bool:
19
+ """Check if a path is a remote/cloud path (e.g., S3, GCS, Azure).
20
+
21
+ Works with UPath objects from universal-pathlib. Local paths return False.
22
+
23
+ Args:
24
+ p: A path object (Path, UPath, or similar)
25
+
26
+ Returns:
27
+ bool: True if the path is a remote/cloud path, False otherwise.
28
+ """
29
+ # UPath objects have a 'protocol' attribute
30
+ protocol = getattr(p, "protocol", None)
31
+ return protocol is not None and protocol not in ("file", "")
32
+
33
+
34
+ def relative_path(src: str | Path, target: str | Path) -> Path:
19
35
  """Compute the relative path from `src` to `target`.
20
36
 
21
37
  Args:
22
- src (str | Path): The source directory or file path (must be absolute).
23
- target (str | Path): The target directory or file path (must be absolute).
38
+ src: The source directory or file path (must be absolute).
39
+ target: The target directory or file path (must be absolute).
24
40
 
25
41
  Returns:
26
42
  Path: The relative path from `src` to `target`.
27
43
 
28
44
  Raises:
29
45
  ValueError: If either `src` or `target` is not an absolute path.
46
+
47
+ Note:
48
+ For remote paths (UPath with non-file protocols), this function cannot
49
+ compute relative paths. Use is_remote_path() to check before calling.
30
50
  """
31
- src = Path(src).resolve()
32
- target = Path(target).resolve()
51
+ # Convert to Path only if string, otherwise keep original type
52
+ if isinstance(src, str):
53
+ src = Path(src)
54
+ if isinstance(target, str):
55
+ target = Path(target)
56
+
57
+ try:
58
+ src = src.resolve()
59
+ target = target.resolve()
60
+ except (AttributeError, NotImplementedError, OSError) as e:
61
+ raise ValueError(
62
+ f"Cannot resolve paths. This function only supports local filesystem paths. "
63
+ f"Remote paths should use absolute URIs. Error: {e}"
64
+ ) from e
33
65
 
34
66
  # Ensure both paths are absolute
35
67
  if not src.is_absolute():
@@ -93,7 +125,7 @@ def safe_extract_zip_archive(archive: Path, destination: Path) -> None:
93
125
  shutil.copyfileobj(src, dst)
94
126
 
95
127
 
96
- def get_html_tag_with_text_direction(html_tag: str, text: str, attrs: Optional[dict] = None) -> str:
128
+ def get_html_tag_with_text_direction(html_tag: str, text: str, attrs: dict | None = None) -> str:
97
129
  """Form the HTML element with tag, text, and optional dir attribute."""
98
130
  my_attrs = attrs or {}
99
131
  if (dir := my_attrs.get("dir")) is not None and dir != "ltr":
@@ -6,7 +6,6 @@ Example:
6
6
 
7
7
  import argparse
8
8
  import json
9
- import os
10
9
  from argparse import BooleanOptionalAction
11
10
  from pathlib import Path
12
11
  from shutil import rmtree
@@ -24,15 +23,16 @@ def _prepare_directory(folder: str, clean: bool = False) -> None:
24
23
  folder: The name of the directory.
25
24
  clean: Whether any existing content in the directory should be removed.
26
25
  """
27
- if os.path.isdir(folder):
26
+ folder_path = Path(folder)
27
+ if folder_path.is_dir():
28
28
  if clean:
29
- for path in Path(folder).glob("**/*"):
29
+ for path in folder_path.glob("**/*"):
30
30
  if path.is_file():
31
31
  path.unlink()
32
32
  elif path.is_dir():
33
33
  rmtree(path)
34
34
  else:
35
- os.makedirs(folder, exist_ok=True)
35
+ folder_path.mkdir(parents=True, exist_ok=True)
36
36
 
37
37
 
38
38
  def generate_collection_jsonschema(folder: str):
@@ -41,10 +41,11 @@ def generate_collection_jsonschema(folder: str):
41
41
  Args:
42
42
  folder: The name of the directory.
43
43
  """
44
+ folder_path = Path(folder)
44
45
  for item in MODELS:
45
46
  json_schema = generate_json_schema(item)
46
- with open(os.path.join(folder, f"{item}.json"), mode="w", encoding="utf8") as json_file:
47
- json.dump(json_schema, json_file, ensure_ascii=False, indent=2)
47
+ output_file = folder_path / f"{item}.json"
48
+ output_file.write_text(json.dumps(json_schema, ensure_ascii=False, indent=2), encoding="utf-8")
48
49
 
49
50
 
50
51
  def main() -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docling-core
3
- Version: 2.85.0
3
+ Version: 2.86.0
4
4
  Summary: A python library to define and validate data types in Docling.
5
5
  Author-email: Cesar Berrospi Ramis <ceb@zurich.ibm.com>, Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>
6
6
  Maintainer-email: Panos Vagenas <pva@zurich.ibm.com>, Michele Dolfi <dol@zurich.ibm.com>, Christoph Auer <cau@zurich.ibm.com>, Peter Staar <taa@zurich.ibm.com>, Cesar Berrospi Ramis <ceb@zurich.ibm.com>
@@ -110,6 +110,7 @@ test/test_serialization.py
110
110
  test/test_serialization_doclang.py
111
111
  test/test_serialization_doctag.py
112
112
  test/test_serialization_outline.py
113
+ test/test_upath_support.py
113
114
  test/test_utils.py
114
115
  test/test_visualization.py
115
116
  test/test_webvtt.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docling-core"
3
- version = "2.85.0" # DO NOT EDIT, updated automatically
3
+ version = "2.86.0" # DO NOT EDIT, updated automatically
4
4
  description = "A python library to define and validate data types in Docling."
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -116,6 +116,7 @@ dev = [
116
116
  "ruff>=0.14.11",
117
117
  "types-defusedxml (>=0.7.0.20250822, <0.8.0)",
118
118
  "opencv-python-headless>=4.6.0.66,<5.0.0.0",
119
+ "universal-pathlib>=0.3.10",
119
120
  ]
120
121
  constraints = [
121
122
  'pandas (>=2.1.4,<3.0.0); python_version < "3.11"',
@@ -0,0 +1,182 @@
1
+ """Tests for UPath/fsspec support for cloud storage compatibility."""
2
+
3
+ import uuid
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ from PIL import Image as PILImage
8
+ from pydantic import AnyUrl
9
+ from upath import UPath
10
+
11
+ from docling_core.types.doc import DoclingDocument, ImageRefMode
12
+ from docling_core.types.doc.base import Size
13
+ from docling_core.types.doc.document import ImageRef
14
+ from docling_core.types.doc.utils import is_remote_path, relative_path
15
+
16
+
17
+ @pytest.fixture
18
+ def memory_base():
19
+ """Provide a unique memory:// base path for each test."""
20
+ return UPath(f"memory://test-{uuid.uuid4()}")
21
+
22
+
23
+ # =============================================================================
24
+ # Tests for is_remote_path()
25
+ # =============================================================================
26
+
27
+
28
+ def test_is_remote_path():
29
+ """Test is_remote_path() function with various path types."""
30
+ # Local paths should return False
31
+ assert is_remote_path(Path("/local/path")) is False
32
+ assert is_remote_path(Path(".")) is False
33
+ assert is_remote_path(None) is False
34
+ assert is_remote_path("/some/path") is False
35
+ assert is_remote_path(object()) is False
36
+
37
+ # File protocol should return False
38
+ class MockFilePath:
39
+ protocol = "file"
40
+
41
+ class MockEmptyProtocol:
42
+ protocol = ""
43
+
44
+ assert is_remote_path(MockFilePath()) is False
45
+ assert is_remote_path(MockEmptyProtocol()) is False
46
+
47
+ # Cloud protocols should return True
48
+ for protocol in ["s3", "gcs", "az", "http", "https"]:
49
+
50
+ class MockCloudPath:
51
+ pass
52
+
53
+ MockCloudPath.protocol = protocol
54
+ assert is_remote_path(MockCloudPath()) is True
55
+
56
+ # Real UPath objects
57
+ assert is_remote_path(UPath("/tmp/test")) is False
58
+ assert is_remote_path(UPath("memory://test/path")) is True
59
+
60
+
61
+ # =============================================================================
62
+ # Tests for relative_path()
63
+ # =============================================================================
64
+
65
+
66
+ def test_relative_path():
67
+ """Test relative_path() function with various scenarios."""
68
+ # Basic relative path computation
69
+ assert relative_path(Path("/a/b"), Path("/a/b/c/d.txt")) == Path("c/d.txt")
70
+ assert relative_path("/a/b", "/a/b/c.txt") == Path("c.txt")
71
+
72
+ # Navigation with parent directories
73
+ # Sibling directory
74
+ assert relative_path(Path("/a/b/c"), Path("/a/b/d/e.txt")) == Path("../d/e.txt")
75
+ # Parent directory
76
+ assert relative_path(Path("/a/b/c/d"), Path("/a/b/e.txt")) == Path("../../e.txt")
77
+
78
+ # UPath with local file protocol
79
+ src = UPath("/home/user/docs")
80
+ target = UPath("/home/user/docs/images/img.png")
81
+ assert relative_path(src, target) == Path("images/img.png")
82
+
83
+
84
+ # =============================================================================
85
+ # UPath integration tests (memory filesystem)
86
+ # =============================================================================
87
+
88
+
89
+ def test_upath_integration(sample_doc, memory_base):
90
+ """Test UPath integration with various document operations."""
91
+ # JSON roundtrip
92
+ json_path = memory_base / "doc.json"
93
+ sample_doc.save_as_json(json_path)
94
+ assert json_path.exists()
95
+ loaded = DoclingDocument.load_from_json(json_path)
96
+ assert sample_doc.export_to_dict() == loaded.export_to_dict()
97
+
98
+ # YAML roundtrip
99
+ yaml_path = memory_base / "doc.yaml"
100
+ sample_doc.save_as_yaml(yaml_path)
101
+ assert yaml_path.exists()
102
+ loaded = DoclingDocument.load_from_yaml(yaml_path)
103
+ assert sample_doc.export_to_dict() == loaded.export_to_dict()
104
+
105
+ # Markdown export
106
+ md_path = memory_base / "doc.md"
107
+ sample_doc.save_as_markdown(md_path)
108
+ assert md_path.exists()
109
+ assert len(md_path.read_text()) > 0
110
+
111
+ # HTML export
112
+ html_path = memory_base / "doc.html"
113
+ sample_doc.save_as_html(html_path)
114
+ assert html_path.exists()
115
+ assert "<html" in html_path.read_text().lower()
116
+
117
+ # Doctags export
118
+ dt_path = memory_base / "doc.dt"
119
+ sample_doc.save_as_doctags(dt_path)
120
+ assert dt_path.exists()
121
+
122
+ # Referenced mode
123
+ json_ref_path = memory_base / "doc_ref.json"
124
+ artifacts_dir = memory_base / "artifacts"
125
+ sample_doc.save_as_json(json_ref_path, artifacts_dir=artifacts_dir, image_mode=ImageRefMode.REFERENCED)
126
+ assert json_ref_path.exists()
127
+
128
+ # Referenced images use string URI
129
+ image_dir = memory_base / "images"
130
+ doc_with_refs = sample_doc._with_pictures_refs(image_dir=image_dir, page_no=None)
131
+ for pic in doc_with_refs.pictures:
132
+ if pic.image is not None and pic.image.uri is not None:
133
+ # For remote storage, URI should not be a UPath (can't serialize)
134
+ assert not is_remote_path(pic.image.uri)
135
+
136
+ # Nested path support
137
+ nested_path = memory_base / "a" / "b" / "c" / "doc.json"
138
+ sample_doc.save_as_json(nested_path)
139
+ assert nested_path.exists()
140
+
141
+ # Empty document
142
+ empty_doc = DoclingDocument(name="Empty")
143
+ empty_path = memory_base / "empty.json"
144
+ empty_doc.save_as_json(empty_path)
145
+ loaded_empty = DoclingDocument.load_from_json(empty_path)
146
+ assert loaded_empty.name == "Empty"
147
+ assert len(loaded_empty.texts) == 0
148
+
149
+
150
+ def test_upath_include_page_images(memory_base):
151
+ """Test _with_pictures_refs() with include_page_images=True on a remote path.
152
+
153
+ Ensures that page images are saved via BytesIO (UPath-compatible) and that
154
+ page.image.uri is stored as an AnyUrl, not a Path, for remote storage.
155
+ """
156
+ page_img = PILImage.new("RGB", (64, 64), "blue")
157
+ image_ref = ImageRef.from_pil(image=page_img, dpi=72)
158
+
159
+ doc = DoclingDocument(name="PageImageDoc")
160
+ doc.add_page(page_no=1, size=Size(width=64, height=64), image=image_ref)
161
+
162
+ image_dir = memory_base / "page_images"
163
+
164
+ result = doc._with_pictures_refs(
165
+ image_dir=image_dir,
166
+ page_no=None,
167
+ include_page_images=True,
168
+ )
169
+
170
+ # The image directory must have been created and contain the page image file
171
+ assert image_dir.exists()
172
+ saved_files = list(image_dir.iterdir())
173
+ assert len(saved_files) == 1, "Expected exactly one page image to be saved"
174
+ assert saved_files[0].name.endswith(".png")
175
+
176
+ # For a remote path the URI must be an AnyUrl, not a plain Path or UPath,
177
+ # so that the document remains JSON-serialisable
178
+ page = result.pages[1]
179
+ assert page.image is not None
180
+ assert page.image.uri is not None
181
+ assert isinstance(page.image.uri, AnyUrl), f"Expected AnyUrl for remote page image URI, got {type(page.image.uri)}"
182
+ assert not is_remote_path(page.image.uri)
File without changes
File without changes
File without changes