Skip to content

Commit 6adfbda

Browse files
feat: add PdfShape to SegmentedPdfPage (#507)
* chore: moved PdfLine to PdfShape Signed-off-by: Peter Staar <[email protected]> * updated the PdfShape Signed-off-by: Peter Staar <[email protected]> * update the _render_shapes Signed-off-by: Peter Staar <[email protected]> * moved rendering shapes before the text Signed-off-by: Peter Staar <[email protected]> * keep with deprecation warnings Signed-off-by: Peter Staar <[email protected]> --------- Signed-off-by: Peter Staar <[email protected]>
1 parent 193c25f commit 6adfbda

File tree

1 file changed

+101
-38
lines changed

1 file changed

+101
-38
lines changed

docling_core/types/doc/page.py

Lines changed: 101 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import math
77
import re
88
import typing
9+
import warnings
910
from collections.abc import Iterator
1011
from enum import Enum
1112
from pathlib import Path
@@ -375,14 +376,63 @@ def to_top_left_origin(self, page_height: float):
375376

376377

377378
class PdfLine(ColorMixin, OrderedElement):
378-
"""Model representing a line in a PDF document."""
379+
"""Model representing a line in a PDF document.
380+
381+
.. deprecated::
382+
Use :class:`PdfShape` instead.
383+
"""
379384

380385
parent_id: int
381386
points: list[Coord2D]
382387
width: float = 1.0
383388

384389
coord_origin: CoordOrigin = CoordOrigin.BOTTOMLEFT
385390

391+
def __init__(self, **data):
392+
"""Initialize PdfLine with a deprecation warning."""
393+
warnings.warn(
394+
"PdfLine is deprecated, use PdfShape instead.",
395+
DeprecationWarning,
396+
stacklevel=2,
397+
)
398+
super().__init__(**data)
399+
400+
401+
class PdfShape(OrderedElement):
402+
"""Model representing a vector shape in a PDF document."""
403+
404+
parent_id: int
405+
points: list[Coord2D]
406+
407+
coord_origin: CoordOrigin = CoordOrigin.BOTTOMLEFT
408+
409+
# graphics state
410+
has_graphics_state: bool = False
411+
412+
line_width: float = -1.0
413+
miter_limit: float = -1.0
414+
415+
line_cap: int = -1 # 0=butt, 1=round, 2=projecting square
416+
line_join: int = -1 # 0=miter, 1=round, 2=bevel
417+
418+
dash_phase: float = 0.0
419+
dash_array: list[float] = []
420+
421+
flatness: float = -1.0
422+
423+
rgb_stroking: ColorRGBA = ColorRGBA(r=0, g=0, b=0, a=255)
424+
rgb_filling: ColorRGBA = ColorRGBA(r=0, g=0, b=0, a=255)
425+
426+
# deprecated — use rgb_stroking / rgb_filling instead
427+
rgba: Optional[ColorRGBA] = Field(
428+
default=None,
429+
deprecated="Use `rgb_stroking` and `rgb_filling` instead.",
430+
)
431+
width: Optional[float] = Field(
432+
default=None,
433+
deprecated="Use `line_width` instead.",
434+
)
435+
386436
def __len__(self) -> int:
387437
"""Return the number of points in the line."""
388438
return len(self.points)
@@ -541,7 +591,11 @@ class SegmentedPdfPage(SegmentedPage):
541591
# Redefine typing to use PdfPageDimensions
542592
dimension: PdfPageGeometry
543593

544-
lines: list[PdfLine] = []
594+
lines: list[PdfLine] = Field(
595+
default=[],
596+
deprecated="Use `shapes` instead.",
597+
)
598+
shapes: list[PdfShape] = []
545599

546600
# Redefine typing of elements to include PdfTextCell
547601
char_cells: list[Union[PdfTextCell, TextCell]]
@@ -713,10 +767,10 @@ def render_as_image(
713767
bitmap_resources_outline: str = "black",
714768
bitmap_resources_fill: str = "yellow",
715769
bitmap_resources_alpha: float = 1.0,
716-
draw_lines: bool = True,
717-
line_color: str = "black",
718-
line_width: int = 1,
719-
line_alpha: float = 1.0,
770+
draw_shapes: bool = True,
771+
shape_color: str = "black",
772+
shape_width: int = 1,
773+
shape_alpha: float = 1.0,
720774
draw_annotations: bool = True,
721775
annotations_outline: str = "white",
722776
annotations_color: str = "green",
@@ -750,10 +804,10 @@ def render_as_image(
750804
bitmap_resources_outline: Outline color for bitmap resources
751805
bitmap_resources_fill: Fill color for bitmap resources
752806
bitmap_resources_alpha: Alpha value for bitmap resources
753-
draw_lines: Whether to draw lines
754-
line_color: Color for lines
755-
line_width: Width for lines
756-
line_alpha: Alpha value for lines
807+
draw_shapes: Whether to draw shapes
808+
shape_color: Color for shapes
809+
shape_width: Width for shapes
810+
shape_alpha: Alpha value for shapes
757811
draw_annotations: Whether to draw annotations
758812
annotations_outline: Outline color for annotations
759813
annotations_color: Fill color for annotations
@@ -771,7 +825,7 @@ def render_as_image(
771825
cell_bl_alpha,
772826
cell_tr_alpha,
773827
bitmap_resources_alpha,
774-
line_alpha,
828+
shape_alpha,
775829
annotations_alpha,
776830
cropbox_alpha,
777831
]:
@@ -799,6 +853,15 @@ def render_as_image(
799853
bitmap_resources_alpha=bitmap_resources_alpha,
800854
)
801855

856+
if draw_shapes:
857+
draw = self._render_shapes(
858+
draw=draw,
859+
page_height=page_height,
860+
shape_color=shape_color,
861+
shape_alpha=shape_alpha,
862+
shape_width=shape_width,
863+
)
864+
802865
if draw_cells_text:
803866
result = self._render_cells_text(cell_unit=cell_unit, img=result, page_height=page_height)
804867

@@ -834,15 +897,6 @@ def render_as_image(
834897
cell_tr_radius=cell_tr_radius,
835898
)
836899

837-
if draw_lines:
838-
draw = self._render_lines(
839-
draw=draw,
840-
page_height=page_height,
841-
line_color=line_color,
842-
line_alpha=line_alpha,
843-
line_width=line_width,
844-
)
845-
846900
return result
847901

848902
def _get_rgba(self, name: str, alpha: float):
@@ -1107,37 +1161,46 @@ def _draw_cells_tr(
11071161

11081162
return draw
11091163

1110-
def _render_lines(
1164+
def _render_shapes(
11111165
self,
11121166
draw: ImageDraw.ImageDraw,
11131167
page_height: float,
1114-
line_color: str,
1115-
line_alpha: float,
1116-
line_width: float,
1168+
shape_color: str,
1169+
shape_alpha: float,
1170+
shape_width: float,
11171171
) -> ImageDraw.ImageDraw:
1118-
"""Render lines on the page.
1172+
"""Render shapes on the page.
11191173
11201174
Args:
11211175
draw: PIL ImageDraw object
11221176
page_height: Height of the page
1123-
line_color: Color for lines
1124-
line_alpha: Alpha value for lines
1125-
line_width: Width for lines
1177+
shape_color: Default color for shapes (used as fallback)
1178+
shape_alpha: Alpha value for shapes
1179+
shape_width: Default width for shapes (used as fallback)
11261180
11271181
Returns:
11281182
Updated ImageDraw object
11291183
"""
1130-
fill = self._get_rgba(name=line_color, alpha=line_alpha)
1184+
# Draw each shape using its own stroking/filling colors
1185+
for shape in self.shapes:
1186+
shape.to_top_left_origin(page_height=page_height)
11311187

1132-
# Draw each rectangle by connecting its four points
1133-
for line in self.lines:
1134-
line.to_top_left_origin(page_height=page_height)
1135-
for segment in line.iterate_segments():
1136-
draw.line(
1137-
(segment[0][0], segment[0][1], segment[1][0], segment[1][1]),
1138-
fill=fill,
1139-
width=max(1, round(line.width)),
1140-
)
1188+
stroking_rgba = shape.rgb_stroking.as_tuple()
1189+
filling_rgba = shape.rgb_filling.as_tuple()
1190+
1191+
width = max(1, round(shape.line_width)) if shape.line_width > 0 else max(1, round(shape_width))
1192+
1193+
# If the shape is closed (first and last points coincide), fill it
1194+
if len(shape.points) >= 3 and shape.points[0] == shape.points[-1]:
1195+
poly = [(p.x, p.y) for p in shape.points]
1196+
draw.polygon(poly, outline=stroking_rgba, fill=filling_rgba)
1197+
else:
1198+
for segment in shape.iterate_segments():
1199+
draw.line(
1200+
(segment[0][0], segment[0][1], segment[1][0], segment[1][1]),
1201+
fill=stroking_rgba,
1202+
width=width,
1203+
)
11411204

11421205
return draw
11431206

0 commit comments

Comments
 (0)