docling-core 2.27.0__py3-none-any.whl → 2.28.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.

Potentially problematic release.


This version of docling-core might be problematic. Click here for more details.

@@ -8,27 +8,21 @@ import warnings
8
8
  from functools import cached_property
9
9
  from typing import Any, Iterable, Iterator, Optional, Union
10
10
 
11
- from pydantic import (
12
- BaseModel,
13
- ConfigDict,
14
- PositiveInt,
15
- TypeAdapter,
16
- computed_field,
17
- model_validator,
18
- )
19
- from typing_extensions import Self
11
+ from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator
20
12
 
21
13
  from docling_core.transforms.chunker.hierarchical_chunker import (
22
14
  ChunkingSerializerProvider,
23
15
  )
16
+ from docling_core.transforms.chunker.tokenizer.base import BaseTokenizer
24
17
 
25
18
  try:
26
19
  import semchunk
27
- from transformers import AutoTokenizer, PreTrainedTokenizerBase
28
20
  except ImportError:
29
21
  raise RuntimeError(
30
- "Module requires 'chunking' extra; to install, run: "
31
- "`pip install 'docling-core[chunking]'`"
22
+ "Extra required by module: 'chunking' by default (or 'chunking-openai' if "
23
+ "specifically using OpenAI tokenization); to install, run: "
24
+ "`pip install 'docling-core[chunking]'` or "
25
+ "`pip install 'docling-core[chunking-openai]'`"
32
26
  )
33
27
 
34
28
  from docling_core.experimental.serializer.base import (
@@ -45,6 +39,16 @@ from docling_core.transforms.chunker import (
45
39
  from docling_core.types import DoclingDocument
46
40
 
47
41
 
42
+ def _get_default_tokenizer():
43
+ from docling_core.transforms.chunker.tokenizer.huggingface import (
44
+ HuggingFaceTokenizer,
45
+ )
46
+
47
+ return HuggingFaceTokenizer.from_pretrained(
48
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
49
+ )
50
+
51
+
48
52
  class HybridChunker(BaseChunker):
49
53
  r"""Chunker doing tokenization-aware refinements on top of document layout chunking.
50
54
 
@@ -58,26 +62,40 @@ class HybridChunker(BaseChunker):
58
62
 
59
63
  model_config = ConfigDict(arbitrary_types_allowed=True)
60
64
 
61
- tokenizer: Union[PreTrainedTokenizerBase, str] = (
62
- "sentence-transformers/all-MiniLM-L6-v2"
63
- )
64
- max_tokens: int = None # type: ignore[assignment]
65
+ tokenizer: BaseTokenizer = Field(default_factory=_get_default_tokenizer)
65
66
  merge_peers: bool = True
66
67
 
67
68
  serializer_provider: BaseSerializerProvider = ChunkingSerializerProvider()
68
69
 
69
- @model_validator(mode="after")
70
- def _patch_tokenizer_and_max_tokens(self) -> Self:
71
- self._tokenizer = (
72
- self.tokenizer
73
- if isinstance(self.tokenizer, PreTrainedTokenizerBase)
74
- else AutoTokenizer.from_pretrained(self.tokenizer)
75
- )
76
- if self.max_tokens is None:
77
- self.max_tokens = TypeAdapter(PositiveInt).validate_python(
78
- self._tokenizer.model_max_length
79
- )
80
- return self
70
+ @model_validator(mode="before")
71
+ @classmethod
72
+ def _patch(cls, data: Any) -> Any:
73
+ if isinstance(data, dict) and (tokenizer := data.get("tokenizer")):
74
+ max_tokens = data.get("max_tokens")
75
+ if isinstance(tokenizer, BaseTokenizer):
76
+ pass
77
+ else:
78
+ from docling_core.transforms.chunker.tokenizer.huggingface import (
79
+ HuggingFaceTokenizer,
80
+ )
81
+
82
+ if isinstance(tokenizer, str):
83
+ data["tokenizer"] = HuggingFaceTokenizer.from_pretrained(
84
+ model_name=tokenizer,
85
+ max_tokens=max_tokens,
86
+ )
87
+ else:
88
+ # migrate previous HF-based tokenizers
89
+ kwargs = {"tokenizer": tokenizer}
90
+ if max_tokens is not None:
91
+ kwargs["max_tokens"] = max_tokens
92
+ data["tokenizer"] = HuggingFaceTokenizer(**kwargs)
93
+ return data
94
+
95
+ @property
96
+ def max_tokens(self) -> int:
97
+ """Get maximum number of tokens allowed."""
98
+ return self.tokenizer.get_max_tokens()
81
99
 
82
100
  @computed_field # type: ignore[misc]
83
101
  @cached_property
@@ -92,7 +110,7 @@ class HybridChunker(BaseChunker):
92
110
  for t in text:
93
111
  total += self._count_text_tokens(t)
94
112
  return total
95
- return len(self._tokenizer.tokenize(text))
113
+ return self.tokenizer.count_tokens(text=text)
96
114
 
97
115
  class _ChunkLengthInfo(BaseModel):
98
116
  total_len: int
@@ -101,7 +119,7 @@ class HybridChunker(BaseChunker):
101
119
 
102
120
  def _count_chunk_tokens(self, doc_chunk: DocChunk):
103
121
  ser_txt = self.contextualize(chunk=doc_chunk)
104
- return len(self._tokenizer.tokenize(text=ser_txt))
122
+ return self.tokenizer.count_tokens(text=ser_txt)
105
123
 
106
124
  def _doc_chunk_length(self, doc_chunk: DocChunk):
107
125
  text_length = self._count_text_tokens(doc_chunk.text)
@@ -198,7 +216,7 @@ class HybridChunker(BaseChunker):
198
216
  # captions:
199
217
  available_length = self.max_tokens - lengths.other_len
200
218
  sem_chunker = semchunk.chunkerify(
201
- self._tokenizer, chunk_size=available_length
219
+ self.tokenizer.get_tokenizer(), chunk_size=available_length
202
220
  )
203
221
  if available_length <= 0:
204
222
  warnings.warn(
@@ -0,0 +1 @@
1
+ """Define the tokenizer types."""
@@ -0,0 +1,25 @@
1
+ """Define base classes for tokenization."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class BaseTokenizer(BaseModel, ABC):
10
+ """Base tokenizer class."""
11
+
12
+ @abstractmethod
13
+ def count_tokens(self, text: str) -> int:
14
+ """Get number of tokens for given text."""
15
+ ...
16
+
17
+ @abstractmethod
18
+ def get_max_tokens(self) -> int:
19
+ """Get maximum number of tokens allowed."""
20
+ ...
21
+
22
+ @abstractmethod
23
+ def get_tokenizer(self) -> Any:
24
+ """Get underlying tokenizer object."""
25
+ ...
@@ -0,0 +1,70 @@
1
+ """HuggingFace tokenization."""
2
+
3
+ import sys
4
+ from os import PathLike
5
+ from typing import Optional, Union
6
+
7
+ from pydantic import ConfigDict, PositiveInt, TypeAdapter, model_validator
8
+ from typing_extensions import Self
9
+
10
+ from docling_core.transforms.chunker.tokenizer.base import BaseTokenizer
11
+
12
+ try:
13
+ from transformers import AutoTokenizer, PreTrainedTokenizerBase
14
+ except ImportError:
15
+ raise RuntimeError(
16
+ "Module requires 'chunking' extra; to install, run: "
17
+ "`pip install 'docling-core[chunking]'`"
18
+ )
19
+
20
+
21
+ class HuggingFaceTokenizer(BaseTokenizer):
22
+ """HuggingFace tokenizer."""
23
+
24
+ model_config = ConfigDict(arbitrary_types_allowed=True)
25
+
26
+ tokenizer: PreTrainedTokenizerBase
27
+ max_tokens: int = None # type: ignore[assignment]
28
+
29
+ @model_validator(mode="after")
30
+ def _patch(self) -> Self:
31
+ if hasattr(self.tokenizer, "model_max_length"):
32
+ model_max_tokens: PositiveInt = TypeAdapter(PositiveInt).validate_python(
33
+ self.tokenizer.model_max_length
34
+ )
35
+ user_max_tokens = self.max_tokens or sys.maxsize
36
+ self.max_tokens = min(model_max_tokens, user_max_tokens)
37
+ elif self.max_tokens is None:
38
+ raise ValueError(
39
+ "max_tokens must be defined as model does not define model_max_length"
40
+ )
41
+ return self
42
+
43
+ def count_tokens(self, text: str):
44
+ """Get number of tokens for given text."""
45
+ return len(self.tokenizer.tokenize(text=text))
46
+
47
+ def get_max_tokens(self):
48
+ """Get maximum number of tokens allowed."""
49
+ return self.max_tokens
50
+
51
+ @classmethod
52
+ def from_pretrained(
53
+ cls,
54
+ model_name: Union[str, PathLike],
55
+ max_tokens: Optional[int] = None,
56
+ **kwargs,
57
+ ) -> Self:
58
+ """Create tokenizer from model name."""
59
+ my_kwargs = {
60
+ "tokenizer": AutoTokenizer.from_pretrained(
61
+ pretrained_model_name_or_path=model_name, **kwargs
62
+ ),
63
+ }
64
+ if max_tokens is not None:
65
+ my_kwargs["max_tokens"] = max_tokens
66
+ return cls(**my_kwargs)
67
+
68
+ def get_tokenizer(self):
69
+ """Get underlying tokenizer object."""
70
+ return self.tokenizer
@@ -0,0 +1,34 @@
1
+ """OpenAI tokenization."""
2
+
3
+ from pydantic import ConfigDict
4
+
5
+ from docling_core.transforms.chunker.hybrid_chunker import BaseTokenizer
6
+
7
+ try:
8
+ import tiktoken
9
+ except ImportError:
10
+ raise RuntimeError(
11
+ "Module requires 'chunking-openai' extra; to install, run: "
12
+ "`pip install 'docling-core[chunking-openai]'`"
13
+ )
14
+
15
+
16
+ class OpenAITokenizer(BaseTokenizer):
17
+ """OpenAI tokenizer."""
18
+
19
+ model_config = ConfigDict(arbitrary_types_allowed=True)
20
+
21
+ tokenizer: tiktoken.Encoding
22
+ max_tokens: int
23
+
24
+ def count_tokens(self, text: str) -> int:
25
+ """Get number of tokens for given text."""
26
+ return len(self.tokenizer.encode(text=text))
27
+
28
+ def get_max_tokens(self) -> int:
29
+ """Get maximum number of tokens allowed."""
30
+ return self.max_tokens
31
+
32
+ def get_tokenizer(self) -> tiktoken.Encoding:
33
+ """Get underlying tokenizer object."""
34
+ return self.tokenizer
@@ -0,0 +1 @@
1
+ """Define the visualizer types."""
@@ -0,0 +1,23 @@
1
+ """Define base classes for visualization."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from PIL.Image import Image
7
+ from pydantic import BaseModel
8
+
9
+ from docling_core.types.doc import DoclingDocument
10
+
11
+
12
+ class BaseVisualizer(BaseModel, ABC):
13
+ """Visualize base class."""
14
+
15
+ @abstractmethod
16
+ def get_visualization(
17
+ self,
18
+ *,
19
+ doc: DoclingDocument,
20
+ **kwargs,
21
+ ) -> dict[Optional[int], Image]:
22
+ """Get visualization of the document as images by page."""
23
+ raise NotImplementedError()
@@ -0,0 +1,212 @@
1
+ """Define classes for layout visualization."""
2
+
3
+ from copy import deepcopy
4
+ from typing import Literal, Optional, Union
5
+
6
+ from PIL import ImageDraw, ImageFont
7
+ from PIL.Image import Image
8
+ from PIL.ImageFont import FreeTypeFont
9
+ from pydantic import BaseModel
10
+ from typing_extensions import override
11
+
12
+ from docling_core.transforms.visualizer.base import BaseVisualizer
13
+ from docling_core.types.doc import DocItemLabel
14
+ from docling_core.types.doc.base import CoordOrigin
15
+ from docling_core.types.doc.document import ContentLayer, DocItem, DoclingDocument
16
+ from docling_core.types.doc.page import BoundingRectangle, TextCell
17
+
18
+
19
+ class _TLBoundingRectangle(BoundingRectangle):
20
+ coord_origin: Literal[CoordOrigin.TOPLEFT] = CoordOrigin.TOPLEFT
21
+
22
+
23
+ class _TLTextCell(TextCell):
24
+ rect: _TLBoundingRectangle
25
+
26
+
27
+ class _TLCluster(BaseModel):
28
+ id: int
29
+ label: DocItemLabel
30
+ brec: _TLBoundingRectangle
31
+ confidence: float = 1.0
32
+ cells: list[_TLTextCell] = []
33
+ children: list["_TLCluster"] = [] # Add child cluster support
34
+
35
+
36
+ class LayoutVisualizer(BaseVisualizer):
37
+ """Layout visualizer."""
38
+
39
+ class Params(BaseModel):
40
+ """Layout visualization parameters."""
41
+
42
+ show_label: bool = True
43
+
44
+ base_visualizer: Optional[BaseVisualizer] = None
45
+ params: Params = Params()
46
+
47
+ def _draw_clusters(
48
+ self, image: Image, clusters: list[_TLCluster], scale_x: float, scale_y: float
49
+ ) -> None:
50
+ """Draw clusters on an image."""
51
+ draw = ImageDraw.Draw(image, "RGBA")
52
+ # Create a smaller font for the labels
53
+ font: Union[ImageFont.ImageFont, FreeTypeFont]
54
+ try:
55
+ font = ImageFont.truetype("arial.ttf", 12)
56
+ except OSError:
57
+ # Fallback to default font if arial is not available
58
+ font = ImageFont.load_default()
59
+ for c_tl in clusters:
60
+ all_clusters = [c_tl, *c_tl.children]
61
+ for c in all_clusters:
62
+ # Draw cells first (underneath)
63
+ cell_color = (0, 0, 0, 40) # Transparent black for cells
64
+ for tc in c.cells:
65
+ cx0, cy0, cx1, cy1 = tc.rect.to_bounding_box().as_tuple()
66
+ cx0 *= scale_x
67
+ cx1 *= scale_x
68
+ cy0 *= scale_y
69
+ cy1 *= scale_y
70
+
71
+ draw.rectangle(
72
+ [(cx0, cy0), (cx1, cy1)],
73
+ outline=None,
74
+ fill=cell_color,
75
+ )
76
+ # Draw cluster rectangle
77
+ x0, y0, x1, y1 = c.brec.to_bounding_box().as_tuple()
78
+ x0 *= scale_x
79
+ x1 *= scale_x
80
+ y0 *= scale_y
81
+ y1 *= scale_y
82
+
83
+ cluster_fill_color = (*list(DocItemLabel.get_color(c.label)), 70)
84
+ cluster_outline_color = (
85
+ *list(DocItemLabel.get_color(c.label)),
86
+ 255,
87
+ )
88
+ draw.rectangle(
89
+ [(x0, y0), (x1, y1)],
90
+ outline=cluster_outline_color,
91
+ fill=cluster_fill_color,
92
+ )
93
+
94
+ if self.params.show_label:
95
+ # Add label name and confidence
96
+ label_text = f"{c.label.name} ({c.confidence:.2f})"
97
+ # Create semi-transparent background for text
98
+ text_bbox = draw.textbbox((x0, y0), label_text, font=font)
99
+ text_bg_padding = 2
100
+ draw.rectangle(
101
+ [
102
+ (
103
+ text_bbox[0] - text_bg_padding,
104
+ text_bbox[1] - text_bg_padding,
105
+ ),
106
+ (
107
+ text_bbox[2] + text_bg_padding,
108
+ text_bbox[3] + text_bg_padding,
109
+ ),
110
+ ],
111
+ fill=(255, 255, 255, 180), # Semi-transparent white
112
+ )
113
+ # Draw text
114
+ draw.text(
115
+ (x0, y0),
116
+ label_text,
117
+ fill=(0, 0, 0, 255), # Solid black
118
+ font=font,
119
+ )
120
+
121
+ def _draw_doc_layout(
122
+ self, doc: DoclingDocument, images: Optional[dict[Optional[int], Image]] = None
123
+ ):
124
+ """Draw the document clusters and optionaly the reading order."""
125
+ clusters = []
126
+ my_images: dict[Optional[int], Image] = {}
127
+
128
+ if images is not None:
129
+ my_images = images
130
+
131
+ # Initialise `my_images` beforehand: sometimes, you have the
132
+ # page-images but no DocItems!
133
+ for page_nr, page in doc.pages.items():
134
+ page_image = doc.pages[page_nr].image
135
+ if page_image is None or (pil_img := page_image.pil_image) is None:
136
+ raise RuntimeError("Cannot visualize document without images")
137
+ elif page_nr not in my_images:
138
+ image = deepcopy(pil_img)
139
+ my_images[page_nr] = image
140
+
141
+ prev_image = None
142
+ prev_page_nr = None
143
+ for idx, (elem, _) in enumerate(
144
+ doc.iterate_items(
145
+ included_content_layers={ContentLayer.BODY, ContentLayer.FURNITURE}
146
+ )
147
+ ):
148
+ if not isinstance(elem, DocItem):
149
+ continue
150
+ if len(elem.prov) == 0:
151
+ continue # Skip elements without provenances
152
+ prov = elem.prov[0]
153
+ page_nr = prov.page_no
154
+
155
+ if page_nr in my_images:
156
+ image = my_images[page_nr]
157
+ else:
158
+ raise RuntimeError(f"Cannot visualize page-image for {page_nr}")
159
+
160
+ if prev_page_nr is None or page_nr > prev_page_nr: # new page begins
161
+ # complete previous drawing
162
+ if prev_page_nr is not None and prev_image and clusters:
163
+ self._draw_clusters(
164
+ image=prev_image,
165
+ clusters=clusters,
166
+ scale_x=prev_image.width / doc.pages[prev_page_nr].size.width,
167
+ scale_y=prev_image.height / doc.pages[prev_page_nr].size.height,
168
+ )
169
+ clusters = []
170
+
171
+ tlo_bbox = prov.bbox.to_top_left_origin(
172
+ page_height=doc.pages[prov.page_no].size.height
173
+ )
174
+ cluster = _TLCluster(
175
+ id=idx,
176
+ label=elem.label,
177
+ brec=_TLBoundingRectangle.from_bounding_box(bbox=tlo_bbox),
178
+ cells=[],
179
+ )
180
+ clusters.append(cluster)
181
+
182
+ prev_page_nr = page_nr
183
+ prev_image = image
184
+
185
+ # complete last drawing
186
+ if prev_page_nr is not None and prev_image and clusters:
187
+ self._draw_clusters(
188
+ image=prev_image,
189
+ clusters=clusters,
190
+ scale_x=prev_image.width / doc.pages[prev_page_nr].size.width,
191
+ scale_y=prev_image.height / doc.pages[prev_page_nr].size.height,
192
+ )
193
+
194
+ return my_images
195
+
196
+ @override
197
+ def get_visualization(
198
+ self,
199
+ *,
200
+ doc: DoclingDocument,
201
+ **kwargs,
202
+ ) -> dict[Optional[int], Image]:
203
+ """Get visualization of the document as images by page."""
204
+ base_images = (
205
+ self.base_visualizer.get_visualization(doc=doc, **kwargs)
206
+ if self.base_visualizer
207
+ else None
208
+ )
209
+ return self._draw_doc_layout(
210
+ doc=doc,
211
+ images=base_images,
212
+ )
@@ -0,0 +1,149 @@
1
+ """Define classes for reading order visualization."""
2
+
3
+ from copy import deepcopy
4
+ from typing import Optional
5
+
6
+ from PIL import ImageDraw
7
+ from PIL.Image import Image
8
+ from typing_extensions import override
9
+
10
+ from docling_core.transforms.visualizer.base import BaseVisualizer
11
+ from docling_core.types.doc.document import ContentLayer, DocItem, DoclingDocument
12
+
13
+
14
+ class ReadingOrderVisualizer(BaseVisualizer):
15
+ """Reading order visualizer."""
16
+
17
+ base_visualizer: Optional[BaseVisualizer] = None
18
+
19
+ def _draw_arrow(
20
+ self,
21
+ draw: ImageDraw.ImageDraw,
22
+ arrow_coords: tuple[float, float, float, float],
23
+ line_width: int = 2,
24
+ color: str = "red",
25
+ ):
26
+ """Draw an arrow inside the given draw object."""
27
+ x0, y0, x1, y1 = arrow_coords
28
+
29
+ # Arrow parameters
30
+ start_point = (x0, y0) # Starting point of the arrow
31
+ end_point = (x1, y1) # Ending point of the arrow
32
+ arrowhead_length = 20 # Length of the arrowhead
33
+ arrowhead_width = 10 # Width of the arrowhead
34
+
35
+ # Draw the arrow shaft (line)
36
+ draw.line([start_point, end_point], fill=color, width=line_width)
37
+
38
+ # Calculate the arrowhead points
39
+ dx = end_point[0] - start_point[0]
40
+ dy = end_point[1] - start_point[1]
41
+ angle = (dx**2 + dy**2) ** 0.5 + 0.01 # Length of the arrow shaft
42
+
43
+ # Normalized direction vector for the arrow shaft
44
+ ux, uy = dx / angle, dy / angle
45
+
46
+ # Base of the arrowhead
47
+ base_x = end_point[0] - ux * arrowhead_length
48
+ base_y = end_point[1] - uy * arrowhead_length
49
+
50
+ # Left and right points of the arrowhead
51
+ left_x = base_x - uy * arrowhead_width
52
+ left_y = base_y + ux * arrowhead_width
53
+ right_x = base_x + uy * arrowhead_width
54
+ right_y = base_y - ux * arrowhead_width
55
+
56
+ # Draw the arrowhead (triangle)
57
+ draw.polygon(
58
+ [end_point, (left_x, left_y), (right_x, right_y)],
59
+ fill=color,
60
+ )
61
+ return draw
62
+
63
+ def _draw_doc_reading_order(
64
+ self,
65
+ doc: DoclingDocument,
66
+ images: Optional[dict[Optional[int], Image]] = None,
67
+ ):
68
+ """Draw the reading order."""
69
+ # draw = ImageDraw.Draw(image)
70
+ x0, y0 = None, None
71
+ my_images: dict[Optional[int], Image] = images or {}
72
+ prev_page = None
73
+ for elem, _ in doc.iterate_items(
74
+ included_content_layers={ContentLayer.BODY, ContentLayer.FURNITURE},
75
+ ):
76
+ if not isinstance(elem, DocItem):
77
+ continue
78
+ if len(elem.prov) == 0:
79
+ continue # Skip elements without provenances
80
+ prov = elem.prov[0]
81
+ page_no = prov.page_no
82
+ image = my_images.get(page_no)
83
+
84
+ if image is None or prev_page is None or page_no > prev_page:
85
+ # new page begins
86
+ prev_page = page_no
87
+ x0 = y0 = None
88
+
89
+ if image is None:
90
+ page_image = doc.pages[page_no].image
91
+ if page_image is None or (pil_img := page_image.pil_image) is None:
92
+ raise RuntimeError("Cannot visualize document without images")
93
+ else:
94
+ image = deepcopy(pil_img)
95
+ my_images[page_no] = image
96
+ draw = ImageDraw.Draw(image)
97
+
98
+ # if prov.page_no not in true_doc.pages or prov.page_no != 1:
99
+ # logging.error(f"{prov.page_no} not in true_doc.pages -> skipping! ")
100
+ # continue
101
+
102
+ tlo_bbox = prov.bbox.to_top_left_origin(
103
+ page_height=doc.pages[prov.page_no].size.height
104
+ )
105
+ ro_bbox = tlo_bbox.normalized(doc.pages[prov.page_no].size)
106
+ ro_bbox.l = round(ro_bbox.l * image.width) # noqa: E741
107
+ ro_bbox.r = round(ro_bbox.r * image.width)
108
+ ro_bbox.t = round(ro_bbox.t * image.height)
109
+ ro_bbox.b = round(ro_bbox.b * image.height)
110
+
111
+ if ro_bbox.b > ro_bbox.t:
112
+ ro_bbox.b, ro_bbox.t = ro_bbox.t, ro_bbox.b
113
+
114
+ if x0 is None and y0 is None:
115
+ x0 = (ro_bbox.l + ro_bbox.r) / 2.0
116
+ y0 = (ro_bbox.b + ro_bbox.t) / 2.0
117
+ else:
118
+ assert x0 is not None
119
+ assert y0 is not None
120
+
121
+ x1 = (ro_bbox.l + ro_bbox.r) / 2.0
122
+ y1 = (ro_bbox.b + ro_bbox.t) / 2.0
123
+
124
+ draw = self._draw_arrow(
125
+ draw=draw,
126
+ arrow_coords=(x0, y0, x1, y1),
127
+ line_width=2,
128
+ color="red",
129
+ )
130
+ x0, y0 = x1, y1
131
+ return my_images
132
+
133
+ @override
134
+ def get_visualization(
135
+ self,
136
+ *,
137
+ doc: DoclingDocument,
138
+ **kwargs,
139
+ ) -> dict[Optional[int], Image]:
140
+ """Get visualization of the document as images by page."""
141
+ base_images = (
142
+ self.base_visualizer.get_visualization(doc=doc, **kwargs)
143
+ if self.base_visualizer
144
+ else None
145
+ )
146
+ return self._draw_doc_reading_order(
147
+ doc=doc,
148
+ images=base_images,
149
+ )