docling-core 2.97.2__tar.gz → 2.98.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 (152) hide show
  1. {docling_core-2.97.2 → docling_core-2.98.0}/PKG-INFO +1 -1
  2. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/markdown.py +82 -3
  3. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/__init__.py +1 -0
  4. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/base.py +7 -0
  5. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/document.py +11 -0
  6. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core.egg-info/PKG-INFO +1 -1
  7. {docling_core-2.97.2 → docling_core-2.98.0}/pyproject.toml +1 -1
  8. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_serialization.py +123 -0
  9. {docling_core-2.97.2 → docling_core-2.98.0}/LICENSE +0 -0
  10. {docling_core-2.97.2 → docling_core-2.98.0}/README.md +0 -0
  11. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/__init__.py +0 -0
  12. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/cli/__init__.py +0 -0
  13. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/cli/serialize.py +0 -0
  14. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/cli/view.py +0 -0
  15. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/compat.py +0 -0
  16. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/experimental/__init__.py +0 -0
  17. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/experimental/serializer/__init__.py +0 -0
  18. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/experimental/serializer/outline.py +0 -0
  19. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/py.typed +0 -0
  20. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/search/__init__.py +0 -0
  21. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/search/package.py +0 -0
  22. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/__init__.py +0 -0
  23. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/__init__.py +0 -0
  24. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/base.py +0 -0
  25. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/chunk_expander.py +0 -0
  26. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/code_chunking/__init__.py +0 -0
  27. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/code_chunking/_language_code_chunkers.py +0 -0
  28. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/code_chunking/_utils.py +0 -0
  29. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/code_chunking/base_code_chunking_strategy.py +0 -0
  30. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/code_chunking/code_chunk.py +0 -0
  31. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/code_chunking/standard_code_chunking_strategy.py +0 -0
  32. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/doc_chunk.py +0 -0
  33. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/hierarchical_chunker.py +0 -0
  34. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/hybrid_chunker.py +0 -0
  35. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/line_chunker.py +0 -0
  36. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/page_chunker.py +0 -0
  37. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/tokenizer/__init__.py +0 -0
  38. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/tokenizer/base.py +0 -0
  39. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/tokenizer/huggingface.py +0 -0
  40. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/chunker/tokenizer/openai.py +0 -0
  41. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/deserializer/__init__.py +0 -0
  42. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/deserializer/base.py +0 -0
  43. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/deserializer/doclang.py +0 -0
  44. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/deserializer/doclang_source_mapping.py +0 -0
  45. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/profiler/__init__.py +0 -0
  46. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/profiler/doc_profiler.py +0 -0
  47. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/__init__.py +0 -0
  48. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/_doclang_utils.py +0 -0
  49. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/azure.py +0 -0
  50. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/base.py +0 -0
  51. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/common.py +0 -0
  52. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/doclang.py +0 -0
  53. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/doctags.py +0 -0
  54. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/html.py +0 -0
  55. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/html_styles.py +0 -0
  56. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/latex.py +0 -0
  57. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/markdown_excel.py +0 -0
  58. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/plain_text.py +0 -0
  59. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/serializer/webvtt.py +0 -0
  60. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/visualizer/__init__.py +0 -0
  61. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/visualizer/base.py +0 -0
  62. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/visualizer/key_value_visualizer.py +0 -0
  63. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/visualizer/layout_visualizer.py +0 -0
  64. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/visualizer/reading_order_visualizer.py +0 -0
  65. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/transforms/visualizer/table_visualizer.py +0 -0
  66. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/__init__.py +0 -0
  67. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/base.py +0 -0
  68. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/__init__.py +0 -0
  69. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/annotations.py +0 -0
  70. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/constants.py +0 -0
  71. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/content_layer.py +0 -0
  72. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/formatting.py +0 -0
  73. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/meta.py +0 -0
  74. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/origin.py +0 -0
  75. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/reference.py +0 -0
  76. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/scalars.py +0 -0
  77. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/common/source.py +0 -0
  78. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/doctags.py +0 -0
  79. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/__init__.py +0 -0
  80. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/code.py +0 -0
  81. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/content.py +0 -0
  82. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/form.py +0 -0
  83. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/group.py +0 -0
  84. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/key_value.py +0 -0
  85. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/node.py +0 -0
  86. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/picture/__init__.py +0 -0
  87. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/picture/charts.py +0 -0
  88. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/picture/classification.py +0 -0
  89. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/picture/meta.py +0 -0
  90. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/picture/molecule.py +0 -0
  91. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/picture/picture.py +0 -0
  92. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/table/__init__.py +0 -0
  93. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/table/table.py +0 -0
  94. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/table/table_data.py +0 -0
  95. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/items/text.py +0 -0
  96. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/labels.py +0 -0
  97. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/page.py +0 -0
  98. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/tokens.py +0 -0
  99. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/utils.py +0 -0
  100. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/doc/webvtt.py +0 -0
  101. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/io/__init__.py +0 -0
  102. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/legacy_doc/__init__.py +0 -0
  103. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/legacy_doc/base.py +0 -0
  104. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/types/legacy_doc/document.py +0 -0
  105. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/__init__.py +0 -0
  106. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/alias.py +0 -0
  107. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/file.py +0 -0
  108. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/generate_docs.py +0 -0
  109. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/generate_jsonschema.py +0 -0
  110. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/legacy.py +0 -0
  111. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/settings.py +0 -0
  112. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core/utils/validators.py +0 -0
  113. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core.egg-info/SOURCES.txt +0 -0
  114. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core.egg-info/dependency_links.txt +0 -0
  115. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core.egg-info/entry_points.txt +0 -0
  116. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core.egg-info/requires.txt +0 -0
  117. {docling_core-2.97.2 → docling_core-2.98.0}/docling_core.egg-info/top_level.txt +0 -0
  118. {docling_core-2.97.2 → docling_core-2.98.0}/setup.cfg +0 -0
  119. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_azure_serializer.py +0 -0
  120. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_base.py +0 -0
  121. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_chunk_expander.py +0 -0
  122. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_cli_serialize.py +0 -0
  123. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_code_chunker.py +0 -0
  124. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_code_chunking_strategy.py +0 -0
  125. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_compat.py +0 -0
  126. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_data_gen_flag.py +0 -0
  127. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_deserializer_doclang.py +0 -0
  128. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_deserializer_doclang_source_mapping.py +0 -0
  129. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_doc_base.py +0 -0
  130. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_doclang_archive.py +0 -0
  131. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_docling_doc.py +0 -0
  132. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_doctags_load.py +0 -0
  133. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_hierarchical_chunker.py +0 -0
  134. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_hierarchy.py +0 -0
  135. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_hybrid_chunker.py +0 -0
  136. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_latex_serialization.py +0 -0
  137. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_line_chunker.py +0 -0
  138. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_metadata.py +0 -0
  139. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_otsl_table_export.py +0 -0
  140. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_page.py +0 -0
  141. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_page_chunker.py +0 -0
  142. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_plain_text_serialization.py +0 -0
  143. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_profiler.py +0 -0
  144. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_regions_to_table.py +0 -0
  145. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_serialization_doclang.py +0 -0
  146. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_serialization_doctag.py +0 -0
  147. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_serialization_outline.py +0 -0
  148. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_tokens.py +0 -0
  149. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_upath_support.py +0 -0
  150. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_utils.py +0 -0
  151. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_visualization.py +0 -0
  152. {docling_core-2.97.2 → docling_core-2.98.0}/tests/test_webvtt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docling-core
3
- Version: 2.97.2
3
+ Version: 2.98.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>
@@ -37,8 +37,10 @@ from docling_core.transforms.serializer.common import (
37
37
  )
38
38
  from docling_core.types.doc import (
39
39
  BaseMeta,
40
+ CaptionPlacement,
40
41
  CodeItem,
41
42
  ContentLayer,
43
+ CoordOrigin,
42
44
  DescriptionAnnotation,
43
45
  DescriptionMetaField,
44
46
  DocItem,
@@ -65,6 +67,7 @@ from docling_core.types.doc import (
65
67
  PictureItem,
66
68
  PictureMoleculeData,
67
69
  PictureTabularChartData,
70
+ ProvenanceItem,
68
71
  RichTableCell,
69
72
  SectionHeaderItem,
70
73
  SummaryMetaField,
@@ -89,6 +92,61 @@ def _cell_content_has_table(item: NodeItem, doc: DoclingDocument) -> bool:
89
92
  return False
90
93
 
91
94
 
95
+ def _captions_below_item(item: FloatingItem, doc: DoclingDocument) -> bool | None:
96
+ """Tell whether the item's captions sit below the item, judging by bbox centers.
97
+
98
+ Returns ``True`` if every caption is centered lower than the item on the page,
99
+ ``False`` if the item has captions located at or above it, and ``None`` if the
100
+ position cannot be determined (no captions, missing provenance, different pages
101
+ or unresolvable page height), in which case the caller keeps its default order.
102
+ """
103
+
104
+ def center_y(prov: ProvenanceItem) -> float | None:
105
+ bbox = prov.bbox
106
+ if bbox.coord_origin != CoordOrigin.TOPLEFT:
107
+ page = doc.pages.get(prov.page_no)
108
+ if page is None:
109
+ return None
110
+ bbox = bbox.to_top_left_origin(page_height=page.size.height)
111
+ return (bbox.t + bbox.b) / 2
112
+
113
+ if not item.prov or not item.captions:
114
+ return None
115
+ item_prov = item.prov[0]
116
+ item_y = center_y(item_prov)
117
+ if item_y is None:
118
+ return None
119
+
120
+ below: list[bool] = []
121
+ for cap_ref in item.captions:
122
+ cap = cap_ref.resolve(doc)
123
+ if not isinstance(cap, DocItem):
124
+ continue
125
+ cap_prov = next((p for p in cap.prov if p.page_no == item_prov.page_no), None)
126
+ if cap_prov is None or (cap_y := center_y(cap_prov)) is None:
127
+ return None
128
+ below.append(cap_y > item_y)
129
+ return all(below) if below else None
130
+
131
+
132
+ def _caption_goes_after(
133
+ item: FloatingItem,
134
+ doc: DoclingDocument,
135
+ params: "MarkdownParams",
136
+ standard_after: bool,
137
+ ) -> bool:
138
+ """Decide whether the item's captions are serialized after the item.
139
+
140
+ ``standard_after`` is the item type's default order, used for ``STANDARD``
141
+ placement and whenever ``LAYOUT`` placement cannot determine the position.
142
+ """
143
+ if params.caption_placement == CaptionPlacement.LAYOUT:
144
+ below = _captions_below_item(item, doc)
145
+ if below is not None:
146
+ return below
147
+ return standard_after
148
+
149
+
92
150
  def _mark_subtree_visited(
93
151
  item: NodeItem,
94
152
  doc: DoclingDocument,
@@ -197,6 +255,15 @@ class MarkdownParams(CommonParams):
197
255
  default=True,
198
256
  description="Include the picture classification prediction (the image's predicted class).",
199
257
  )
258
+ caption_placement: CaptionPlacement = Field(
259
+ default=CaptionPlacement.STANDARD,
260
+ description=(
261
+ "Where captions go relative to their item. 'standard' keeps the per-type order "
262
+ "(before tables and pictures, after code and formulas); 'layout' places a caption "
263
+ "after its item if the caption's bbox center is lower on the page, else before, "
264
+ "falling back to 'standard' when positions are unavailable."
265
+ ),
266
+ )
200
267
 
201
268
 
202
269
  class MarkdownTextSerializer(BaseModel, BaseTextSerializer):
@@ -373,7 +440,10 @@ class MarkdownTextSerializer(BaseModel, BaseTextSerializer):
373
440
  if isinstance(item, FloatingItem):
374
441
  cap_res = doc_serializer.serialize_captions(item=item, **kwargs)
375
442
  if cap_res.text:
376
- res_parts.append(cap_res)
443
+ if _caption_goes_after(item, doc, params, standard_after=True):
444
+ res_parts.append(cap_res)
445
+ else:
446
+ res_parts.insert(0, cap_res)
377
447
 
378
448
  text = (" " if is_inline_scope else "\n\n").join([r.text for r in res_parts])
379
449
  if processing_pending:
@@ -732,7 +802,8 @@ class MarkdownTableSerializer(BaseTableSerializer):
732
802
  item=item,
733
803
  **kwargs,
734
804
  )
735
- if cap_res.text:
805
+ cap_after = _caption_goes_after(item, doc, params, standard_after=False)
806
+ if cap_res.text and not cap_after:
736
807
  res_parts.append(cap_res)
737
808
 
738
809
  if item.self_ref not in doc_serializer.get_excluded_refs(**kwargs):
@@ -790,6 +861,9 @@ class MarkdownTableSerializer(BaseTableSerializer):
790
861
  if table_text:
791
862
  res_parts.append(create_ser_result(text=table_text, span_source=item))
792
863
 
864
+ if cap_res.text and cap_after:
865
+ res_parts.append(cap_res)
866
+
793
867
  text_res = "\n\n".join([r.text for r in res_parts])
794
868
 
795
869
  return create_ser_result(text=text_res, span_source=res_parts)
@@ -828,7 +902,8 @@ class MarkdownPictureSerializer(BasePictureSerializer):
828
902
  item=item,
829
903
  **kwargs,
830
904
  )
831
- if cap_res.text:
905
+ cap_after = _caption_goes_after(item, doc, params, standard_after=False)
906
+ if cap_res.text and not cap_after:
832
907
  res_parts.append(cap_res)
833
908
 
834
909
  if item.self_ref not in doc_serializer.get_excluded_refs(**kwargs):
@@ -862,6 +937,10 @@ class MarkdownPictureSerializer(BasePictureSerializer):
862
937
  md_table_content = temp_table.export_to_markdown(temp_doc)
863
938
  if len(md_table_content) > 0:
864
939
  res_parts.append(create_ser_result(text=md_table_content, span_source=item))
940
+
941
+ if cap_res.text and cap_after:
942
+ res_parts.append(cap_res)
943
+
865
944
  text_res = "\n\n".join([r.text for r in res_parts if r.text])
866
945
 
867
946
  return create_ser_result(text=text_res, span_source=res_parts)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from docling_core.types.doc.base import (
4
4
  BoundingBox,
5
+ CaptionPlacement,
5
6
  CoordOrigin,
6
7
  ImageRefMode,
7
8
  PydanticSerCtxKey,
@@ -14,6 +14,13 @@ class ImageRefMode(str, Enum):
14
14
  REFERENCED = "referenced" # reference the image via uri
15
15
 
16
16
 
17
+ class CaptionPlacement(str, Enum):
18
+ """Placement of captions relative to the item they caption."""
19
+
20
+ STANDARD = "standard" # per item type: before tables and pictures, after code and formulas
21
+ LAYOUT = "layout" # by bbox: after the item if the caption is centered lower on the page, else before
22
+
23
+
17
24
  class CoordOrigin(str, Enum):
18
25
  """CoordOrigin."""
19
26
 
@@ -52,6 +52,7 @@ from typing_extensions import Self, deprecated, override
52
52
  from docling_core.types.base import _JSON_POINTER_REGEX, VERSION_PATTERN, UniqueList
53
53
  from docling_core.types.doc import BoundingBox, Size
54
54
  from docling_core.types.doc.base import (
55
+ CaptionPlacement,
55
56
  CoordOrigin,
56
57
  ImageRefMode,
57
58
  PydanticSerCtxKey,
@@ -3722,6 +3723,7 @@ class DoclingDocument(BaseModel):
3722
3723
  mark_meta: bool = False,
3723
3724
  use_legacy_annotations: bool | None = None, # deprecated
3724
3725
  include_picture_classification: bool = True,
3726
+ caption_placement: CaptionPlacement = CaptionPlacement.STANDARD,
3725
3727
  ):
3726
3728
  """Save to markdown."""
3727
3729
  if isinstance(filename, str):
@@ -3755,6 +3757,7 @@ class DoclingDocument(BaseModel):
3755
3757
  use_legacy_annotations=use_legacy_annotations,
3756
3758
  mark_meta=mark_meta,
3757
3759
  include_picture_classification=include_picture_classification,
3760
+ caption_placement=caption_placement,
3758
3761
  )
3759
3762
 
3760
3763
  filename.write_text(md_out, encoding="utf-8")
@@ -3788,6 +3791,7 @@ class DoclingDocument(BaseModel):
3788
3791
  blocked_meta_names: set[str] | None = None,
3789
3792
  mark_meta: bool = False,
3790
3793
  include_picture_classification: bool = True,
3794
+ caption_placement: CaptionPlacement = CaptionPlacement.STANDARD,
3791
3795
  ) -> str:
3792
3796
  r"""Serialize to Markdown.
3793
3797
 
@@ -3863,6 +3867,12 @@ class DoclingDocument(BaseModel):
3863
3867
  classification prediction (the image's predicted class) in the export.
3864
3868
  (Default value = True).
3865
3869
  :type include_picture_classification: bool = True
3870
+ :param caption_placement: Where captions go relative to their item: "standard" keeps
3871
+ the per-type order (before tables and pictures, after code and formulas); "layout"
3872
+ places a caption after its item if the caption's bbox center is lower on the page,
3873
+ else before, falling back to "standard" when positions are unavailable.
3874
+ (Default value = CaptionPlacement.STANDARD).
3875
+ :type caption_placement: CaptionPlacement = CaptionPlacement.STANDARD
3866
3876
  """
3867
3877
  from docling_core.transforms.serializer.markdown import (
3868
3878
  MarkdownDocSerializer,
@@ -3915,6 +3925,7 @@ class DoclingDocument(BaseModel):
3915
3925
  compact_tables=compact_tables,
3916
3926
  traverse_pictures=traverse_pictures,
3917
3927
  include_picture_classification=include_picture_classification,
3928
+ caption_placement=caption_placement,
3918
3929
  ),
3919
3930
  )
3920
3931
  ser_res = serializer.serialize()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docling-core
3
- Version: 2.97.2
3
+ Version: 2.98.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docling-core"
3
- version = "2.97.2" # DO NOT EDIT, updated automatically
3
+ version = "2.98.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"]
@@ -18,6 +18,7 @@ from docling_core.transforms.serializer.html import (
18
18
  HTMLTableSerializer,
19
19
  )
20
20
  from docling_core.transforms.serializer.markdown import (
21
+ CaptionPlacement,
21
22
  MarkdownDocSerializer,
22
23
  MarkdownParams,
23
24
  MarkdownPictureSerializer,
@@ -1747,3 +1748,125 @@ def test_export_to_html_image_dir_without_referenced_raises(sample_doc, tmp_path
1747
1748
  """Passing image_dir without REFERENCED mode raises ValueError."""
1748
1749
  with pytest.raises(ValueError, match=r"ImageRefMode\.REFERENCED"):
1749
1750
  sample_doc.export_to_html(image_dir=tmp_path / "images")
1751
+
1752
+
1753
+ def _make_captioned_doc(
1754
+ *,
1755
+ kind: str,
1756
+ cap_bbox: BoundingBox | None,
1757
+ obj_bbox: BoundingBox | None,
1758
+ page_no: int = 1,
1759
+ ) -> DoclingDocument:
1760
+ doc = DoclingDocument(name="cap_order")
1761
+ doc.add_page(page_no=1, size=Size(width=100, height=200))
1762
+
1763
+ def prov(bbox: BoundingBox | None) -> ProvenanceItem | None:
1764
+ if bbox is None:
1765
+ return None
1766
+ return ProvenanceItem(page_no=page_no, bbox=bbox, charspan=(0, 0))
1767
+
1768
+ caption = doc.add_text(label=DocItemLabel.CAPTION, text="THE CAPTION", prov=prov(cap_bbox))
1769
+ if kind == "table":
1770
+ table_data = TableData(num_rows=1, num_cols=1)
1771
+ table_data.add_row(["cell"])
1772
+ table_data.num_rows = 1
1773
+ doc.add_table(data=table_data, caption=caption, prov=prov(obj_bbox))
1774
+ else:
1775
+ doc.add_picture(caption=caption, prov=prov(obj_bbox))
1776
+ return doc
1777
+
1778
+
1779
+ def _tl(t: float, b: float) -> BoundingBox:
1780
+ return BoundingBox(l=0, r=10, t=t, b=b, coord_origin=CoordOrigin.TOPLEFT)
1781
+
1782
+
1783
+ def _bl(t: float, b: float) -> BoundingBox:
1784
+ return BoundingBox(l=0, r=10, t=t, b=b, coord_origin=CoordOrigin.BOTTOMLEFT)
1785
+
1786
+
1787
+ def _serialize_md(doc: DoclingDocument, placement: CaptionPlacement) -> str:
1788
+ return MarkdownDocSerializer(doc=doc, params=MarkdownParams(caption_placement=placement)).serialize().text
1789
+
1790
+
1791
+ @pytest.mark.parametrize("kind, marker", [("table", "cell"), ("picture", "<!-- image -->")])
1792
+ @pytest.mark.parametrize(
1793
+ "cap_bbox, obj_bbox, layout_caption_first",
1794
+ [
1795
+ (None, None, True), # no bboxes: falls back to standard order (caption first)
1796
+ (_tl(150, 170), None, True), # object without bbox: falls back to standard order
1797
+ (_tl(10, 30), _tl(50, 100), True), # caption above
1798
+ (_tl(150, 170), _tl(50, 100), False), # caption below
1799
+ (_bl(190, 170), _bl(150, 100), True), # caption above, BOTTOMLEFT origin
1800
+ (_bl(50, 30), _bl(150, 100), False), # caption below, BOTTOMLEFT origin
1801
+ (_tl(150, 170), _bl(150, 100), False), # mixed origins: caption (y=160) below object (y=75)
1802
+ (_tl(10, 30), _bl(150, 100), True), # mixed origins: caption (y=20) above object (y=75)
1803
+ ],
1804
+ )
1805
+ def test_markdown_caption_placement(kind, marker, cap_bbox, obj_bbox, layout_caption_first):
1806
+ doc = _make_captioned_doc(kind=kind, cap_bbox=cap_bbox, obj_bbox=obj_bbox)
1807
+
1808
+ # standard ignores bboxes: tables and pictures always have the caption first
1809
+ md = _serialize_md(doc, CaptionPlacement.STANDARD)
1810
+ assert md.index("THE CAPTION") < md.index(marker)
1811
+ assert md == doc.export_to_markdown()
1812
+
1813
+ md = _serialize_md(doc, CaptionPlacement.LAYOUT)
1814
+ if layout_caption_first:
1815
+ assert md.index("THE CAPTION") < md.index(marker)
1816
+ else:
1817
+ assert md.index("THE CAPTION") > md.index(marker)
1818
+
1819
+
1820
+ def test_markdown_caption_placement_layout_other_page_falls_back_to_standard():
1821
+ doc = DoclingDocument(name="cap_pages")
1822
+ doc.add_page(page_no=1, size=Size(width=100, height=200))
1823
+ doc.add_page(page_no=2, size=Size(width=100, height=200))
1824
+ caption = doc.add_text(
1825
+ label=DocItemLabel.CAPTION,
1826
+ text="THE CAPTION",
1827
+ prov=ProvenanceItem(page_no=2, bbox=_tl(150, 170), charspan=(0, 0)),
1828
+ )
1829
+ doc.add_picture(caption=caption, prov=ProvenanceItem(page_no=1, bbox=_tl(50, 100), charspan=(0, 0)))
1830
+ md = _serialize_md(doc, CaptionPlacement.LAYOUT)
1831
+ assert md.index("THE CAPTION") < md.index("<!-- image -->")
1832
+
1833
+
1834
+ @pytest.mark.parametrize(
1835
+ "cap_bbox, placement, caption_first",
1836
+ [
1837
+ (None, CaptionPlacement.STANDARD, False), # standard: code captions come after
1838
+ (_tl(10, 30), CaptionPlacement.STANDARD, False), # standard ignores bboxes
1839
+ (None, CaptionPlacement.LAYOUT, False), # no bboxes: falls back to standard
1840
+ (_tl(10, 30), CaptionPlacement.LAYOUT, True), # caption above the code
1841
+ (_tl(150, 170), CaptionPlacement.LAYOUT, False), # caption below the code
1842
+ ],
1843
+ )
1844
+ def test_markdown_caption_placement_code(cap_bbox, placement, caption_first):
1845
+ doc = DoclingDocument(name="cap_code")
1846
+ doc.add_page(page_no=1, size=Size(width=100, height=200))
1847
+
1848
+ def prov(bbox):
1849
+ return None if bbox is None else ProvenanceItem(page_no=1, bbox=bbox, charspan=(0, 0))
1850
+
1851
+ caption = doc.add_text(label=DocItemLabel.CAPTION, text="THE CAPTION", prov=prov(cap_bbox))
1852
+ code = doc.add_code(text="print(1)", prov=prov(_tl(50, 100) if cap_bbox else None))
1853
+ code.captions.append(caption.get_ref())
1854
+
1855
+ md = _serialize_md(doc, placement)
1856
+ assert (md.index("THE CAPTION") < md.index("print(1)")) == caption_first
1857
+
1858
+
1859
+ @pytest.mark.parametrize("placement", [CaptionPlacement.LAYOUT, "layout"])
1860
+ def test_export_and_save_markdown_caption_placement(tmp_path, placement):
1861
+ doc = _make_captioned_doc(kind="picture", cap_bbox=_tl(150, 170), obj_bbox=_tl(50, 100))
1862
+
1863
+ md = doc.export_to_markdown(caption_placement=placement)
1864
+ assert md.index("THE CAPTION") > md.index("<!-- image -->")
1865
+
1866
+ out = tmp_path / "out.md"
1867
+ doc.save_as_markdown(out, caption_placement=placement)
1868
+ assert out.read_text(encoding="utf-8") == md
1869
+
1870
+ # default is unchanged
1871
+ assert doc.export_to_markdown().index("THE CAPTION") < doc.export_to_markdown().index("<!-- image -->")
1872
+ assert doc.export_to_markdown() == doc.export_to_markdown(caption_placement="standard")
File without changes
File without changes
File without changes