Skip to content

Commit e9714a0

Browse files
committed
feat: add FillMode and stroke/fill color attributes to Line and Rect
1 parent 33d71be commit e9714a0

File tree

7 files changed

+136
-23
lines changed

7 files changed

+136
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add `is_stroked` attribute to `Rect` and `Line`: indicates whether the PDF path is stroked (from `path.is_stroked()`)
13+
- Add `fill_mode` attribute to `Rect` and `Line`: fill rule from pdfium-render `PdfPathFillMode` (NONE, WINDING, or EVEN_ODD); expose `FillMode` enum to Python with same three values
14+
- Replace `Line.color` with `Line.stroke_color` and `Line.fill_color` (aligned with `Rect`)
1215
- Add `Table.to_list()` returning `list[list[TableCellValue]]`: each cell has `text` (or `None` when merged), `merged_left`, and `merged_top` so merge direction (left vs above) is explicit
1316
- Add `TableCellValue` class with attributes `text`, `merged_left`, and `merged_top` for use with `to_list()`
1417
- Add `get_intersections_from_edges(h_edges, v_edges, ...)` function: given horizontal and vertical edges (as returned by `get_edges`), returns a mapping from every `(x, y)` intersection point to the edges that pass through it; accepts the same tolerance kwargs as `get_edges`
@@ -21,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2124

2225
### Changed
2326

27+
- **Breaking:** `Line.color` has been removed. Use `Line.stroke_color` and `Line.fill_color` instead (aligned with `Rect`).
2428
- `Page` is now a Python-level wrapper that holds a `doc` back-reference; Rust-side type is `Pyo3Page`
2529

2630
### Deprecated

docs/reference/api.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,16 @@ Represents a rectangle extracted from a PDF page.
469469
| `fill_color` | `tuple[int, int, int, int]` | Fill color (RGBA) |
470470
| `stroke_color` | `tuple[int, int, int, int]` | Stroke color (RGBA) |
471471
| `stroke_width` | `float` | Stroke width |
472+
| `is_stroked` | `bool` | Whether the path is stroked |
473+
| `fill_mode` | `FillMode` | Fill rule (NONE, WINDING, or EVEN_ODD) |
474+
475+
---
476+
477+
### FillMode
478+
479+
PDF path fill rule: winding (nonzero) or even-odd.
480+
481+
**Values:** `FillMode.NONE`, `FillMode.WINDING`, `FillMode.EVEN_ODD` (mirrors pdfium-render `PdfPathFillMode`)
472482

473483
---
474484

@@ -482,8 +492,11 @@ Represents a line segment extracted from a PDF page.
482492
|-----------|------|-------------|
483493
| `line_type` | `Literal["straight", "polyline", "curve"]` | Type of line |
484494
| `points` | `list[tuple[float, float]]` | Points defining the line path |
485-
| `color` | `tuple[int, int, int, int]` | Color (RGBA) |
495+
| `stroke_color` | `tuple[int, int, int, int]` | Stroke color (RGBA) |
496+
| `fill_color` | `tuple[int, int, int, int]` | Fill color (RGBA) |
486497
| `width` | `float` | Line width |
498+
| `is_stroked` | `bool` | Whether the line is stroked |
499+
| `fill_mode` | `FillMode` | Fill rule (NONE, WINDING, or EVEN_ODD) |
487500

488501
---
489502

python/tablers/tablers.pyi

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,25 @@ class Rect:
351351
The stroke (border) color as an RGBA tuple.
352352
stroke_width : float
353353
The stroke width of the rectangle border.
354+
is_stroked : bool
355+
Whether the path is stroked.
356+
fill_mode : FillMode
357+
Fill rule for the path (NONE, WINDING, or EVEN_ODD; mirrors pdfium-render PdfPathFillMode).
354358
"""
355359

356360
bbox: BBox
357361
fill_color: Color
358362
stroke_color: Color
359363
stroke_width: float
364+
is_stroked: bool
365+
fill_mode: FillMode
366+
367+
class FillMode:
368+
"""PDF path fill rule: mirrors pdfium-render PdfPathFillMode (NONE, WINDING, EVEN_ODD)."""
369+
370+
NONE: FillMode # Path not filled
371+
WINDING: FillMode
372+
EVEN_ODD: FillMode
360373

361374
class Line:
362375
"""
@@ -370,16 +383,25 @@ class Line:
370383
The type of line segment.
371384
points : list of Point
372385
The points that define the line path.
373-
color : Color
374-
The color of the line as an RGBA tuple.
386+
stroke_color : Color
387+
The stroke color of the line as an RGBA tuple.
388+
fill_color : Color
389+
The fill color of the line as an RGBA tuple.
375390
width : float
376391
The width of the line stroke.
392+
is_stroked : bool
393+
Whether the line is stroked.
394+
fill_mode : FillMode
395+
Fill rule for the path (NONE, WINDING, or EVEN_ODD; mirrors pdfium-render PdfPathFillMode).
377396
"""
378397

379398
line_type: Literal["straight", "polyline", "curve"]
380399
points: list[Point]
381-
color: Color
400+
stroke_color: Color
401+
fill_color: Color
382402
width: float
403+
is_stroked: bool
404+
fill_mode: FillMode
383405

384406
class Char:
385407
"""

src/edges.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ pub(crate) fn make_edges(
601601
x2: p1.0,
602602
y2: cmp::max(p1.1, p2.1),
603603
width: line.width,
604-
color: line.color,
604+
color: line.stroke_color,
605605
});
606606
} else if ((h_strat & 0b11u8) != 0)
607607
&& ((p1.1 - p2.1).abs() < snap_y_tol.into_inner())
@@ -613,7 +613,7 @@ pub(crate) fn make_edges(
613613
x2: cmp::max(p1.0, p2.0),
614614
y2: p1.1,
615615
width: line.width,
616-
color: line.color,
616+
color: line.stroke_color,
617617
})
618618
}
619619
} else if line.line_type == LineType::Polyline
@@ -633,7 +633,7 @@ pub(crate) fn make_edges(
633633
x2: x_min,
634634
y2: y_max,
635635
width: x_max - x_min,
636-
color: line.color,
636+
color: line.fill_color,
637637
});
638638
} else if ((h_strat & 0b11u8) != 0) && ((y_max - y_min) < *snap_y_tol) {
639639
edges.get_mut(&Orientation::Horizontal).unwrap().push(Edge {
@@ -643,7 +643,7 @@ pub(crate) fn make_edges(
643643
x2: x_max,
644644
y2: y_min,
645645
width: y_max - y_min,
646-
color: line.color,
646+
color: line.fill_color,
647647
});
648648
}
649649
}
@@ -743,7 +743,7 @@ mod tests {
743743
use crate::test_utils::load_pdfium;
744744
use crate::words::Word;
745745
use ordered_float::OrderedFloat;
746-
use pdfium_render::prelude::PdfColor;
746+
use pdfium_render::prelude::{PdfColor, PdfPathFillMode};
747747

748748
fn of(v: f32) -> OrderedFloat<f32> {
749749
OrderedFloat(v)
@@ -1140,8 +1140,11 @@ mod tests {
11401140
.into_iter()
11411141
.map(|(x, y)| (OrderedFloat(x), OrderedFloat(y)))
11421142
.collect(),
1143-
color: PdfColor::new(0, 0, 0, 255),
1143+
stroke_color: PdfColor::new(0, 0, 0, 255),
1144+
fill_color: PdfColor::new(0, 0, 0, 255),
11441145
width: OrderedFloat(1.0),
1146+
is_stroked: false,
1147+
fill_mode: PdfPathFillMode::Winding,
11451148
}
11461149
}
11471150

@@ -1153,8 +1156,11 @@ mod tests {
11531156
.into_iter()
11541157
.map(|(x, y)| (OrderedFloat(x), OrderedFloat(y)))
11551158
.collect(),
1156-
color: PdfColor::new(0, 0, 0, 255),
1159+
stroke_color: PdfColor::new(0, 0, 0, 255),
1160+
fill_color: PdfColor::new(0, 0, 0, 255),
11571161
width: OrderedFloat(1.0),
1162+
is_stroked: true,
1163+
fill_mode: PdfPathFillMode::None,
11581164
}
11591165
}
11601166

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,7 @@ fn tablers(_py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
961961
m.add_class::<Table>()?;
962962
m.add_class::<PyCellGroup>()?;
963963
m.add_class::<PyTableCellValue>()?;
964+
m.add_class::<crate::objects::FillMode>()?;
964965
m.add_class::<TfSettings>()?;
965966
m.add_class::<WordsExtractSettings>()?;
966967
m.add_function(pyo3::wrap_pyfunction!(py_find_all_cells_bboxes, m)?)?;

src/objects.rs

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
use ordered_float::OrderedFloat;
2-
use pdfium_render::prelude::{PdfColor, PdfPathSegmentType};
2+
use pdfium_render::prelude::{PdfColor, PdfPathFillMode, PdfPathSegmentType};
33
use pyo3::prelude::*;
44

5+
/// PDF path fill rule: mirrors pdfium-render's PdfPathFillMode for Python exposure.
6+
#[pyclass]
7+
#[derive(Clone, Copy, PartialEq, Eq)]
8+
#[allow(non_camel_case_types)]
9+
#[allow(clippy::upper_case_acronyms)]
10+
pub enum FillMode {
11+
/// Path is not filled.
12+
NONE,
13+
/// Nonzero winding rule.
14+
WINDING,
15+
/// Even-odd rule.
16+
EVEN_ODD,
17+
}
18+
19+
impl From<PdfPathFillMode> for FillMode {
20+
fn from(m: PdfPathFillMode) -> Self {
21+
match m {
22+
PdfPathFillMode::None => FillMode::NONE,
23+
PdfPathFillMode::Winding => FillMode::WINDING,
24+
PdfPathFillMode::EvenOdd => FillMode::EVEN_ODD,
25+
}
26+
}
27+
}
28+
529
/// Container for all extracted objects from a PDF page.
630
///
731
/// This struct holds all rectangles, lines, and characters found in a page.
@@ -49,6 +73,11 @@ pub struct Rect {
4973
/// The stroke width of the rectangle border.
5074
#[pyo3(get)]
5175
pub stroke_width: f32,
76+
/// Whether the path is stroked.
77+
#[pyo3(get)]
78+
pub is_stroked: bool,
79+
/// Fill rule for the path (from pdfium-render PdfPathFillMode).
80+
pub fill_mode: PdfPathFillMode,
5281
}
5382

5483
#[pymethods]
@@ -85,6 +114,12 @@ impl Rect {
85114
self.stroke_color.alpha(),
86115
)
87116
}
117+
118+
/// Returns the fill mode (NONE, WINDING, or EVEN_ODD).
119+
#[getter]
120+
fn fill_mode(&self) -> FillMode {
121+
self.fill_mode.into()
122+
}
88123
}
89124

90125
/// Represents a line segment extracted from a PDF page.
@@ -97,10 +132,17 @@ pub struct Line {
97132
pub line_type: LineType,
98133
/// The points that define the line path.
99134
pub points: Vec<Point>,
100-
/// The color of the line.
101-
pub color: PdfColor,
135+
/// The stroke color of the line.
136+
pub stroke_color: PdfColor,
137+
/// The fill color of the line.
138+
pub fill_color: PdfColor,
102139
/// The width of the line stroke.
103140
pub width: OrderedFloat<f32>,
141+
/// Whether the line is stroked.
142+
#[pyo3(get)]
143+
pub is_stroked: bool,
144+
/// Fill rule for the path (from pdfium-render PdfPathFillMode).
145+
pub fill_mode: PdfPathFillMode,
104146
}
105147

106148
#[pymethods]
@@ -124,14 +166,25 @@ impl Line {
124166
.collect()
125167
}
126168

127-
/// Returns the line color as an RGBA tuple.
169+
/// Returns the stroke color as an RGBA tuple.
170+
#[getter]
171+
fn stroke_color(&self) -> (u8, u8, u8, u8) {
172+
(
173+
self.stroke_color.red(),
174+
self.stroke_color.green(),
175+
self.stroke_color.blue(),
176+
self.stroke_color.alpha(),
177+
)
178+
}
179+
180+
/// Returns the fill color as an RGBA tuple.
128181
#[getter]
129-
fn color(&self) -> (u8, u8, u8, u8) {
182+
fn fill_color(&self) -> (u8, u8, u8, u8) {
130183
(
131-
self.color.red(),
132-
self.color.green(),
133-
self.color.blue(),
134-
self.color.alpha(),
184+
self.fill_color.red(),
185+
self.fill_color.green(),
186+
self.fill_color.blue(),
187+
self.fill_color.alpha(),
135188
)
136189
}
137190

@@ -140,6 +193,12 @@ impl Line {
140193
fn width(&self) -> f32 {
141194
self.width.into_inner()
142195
}
196+
197+
/// Returns the fill mode (NONE, WINDING, or EVEN_ODD).
198+
#[getter]
199+
fn fill_mode(&self) -> FillMode {
200+
self.fill_mode.into()
201+
}
143202
}
144203

145204
/// Represents a text character extracted from a PDF page.

src/pages.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::objects::*;
22
use ordered_float::OrderedFloat;
33
use pdfium_render::prelude::PdfPage as PdfiumPage;
4-
use pdfium_render::prelude::*;
4+
use pdfium_render::prelude::{PdfPathFillMode, *};
55
use std::cell::RefCell;
66
use std::cmp;
77

@@ -248,13 +248,18 @@ impl Page {
248248
fill_color: obj.fill_color().unwrap(),
249249
stroke_color: obj.stroke_color().unwrap(),
250250
stroke_width: obj.stroke_width().unwrap().value,
251+
is_stroked: obj.is_stroked().unwrap(),
252+
fill_mode: obj.fill_mode().unwrap_or(PdfPathFillMode::None),
251253
});
252254
} else if points.len() == 2 && points[1].1 == PdfPathSegmentType::LineTo {
253255
objects.lines.push(Line {
254256
line_type: LineType::Straight,
255257
points: points.iter().map(|p| p.0).collect(),
256-
color: obj.stroke_color().unwrap(),
258+
stroke_color: obj.stroke_color().unwrap(),
259+
fill_color: obj.fill_color().unwrap(),
257260
width: OrderedFloat(obj.stroke_width().unwrap().value * 2.0),
261+
is_stroked: obj.is_stroked().unwrap(),
262+
fill_mode: obj.fill_mode().unwrap_or(PdfPathFillMode::None),
258263
});
259264
} else {
260265
// Check if all points except points[0] have LineTo as their second element
@@ -269,8 +274,11 @@ impl Page {
269274
objects.lines.push(Line {
270275
line_type,
271276
points: points.iter().map(|p| p.0).collect(),
272-
color: obj.stroke_color().unwrap(),
277+
stroke_color: obj.stroke_color().unwrap(),
278+
fill_color: obj.fill_color().unwrap(),
273279
width: OrderedFloat(obj.stroke_width().unwrap().value * 2.0),
280+
is_stroked: obj.is_stroked().unwrap(),
281+
fill_mode: obj.fill_mode().unwrap_or(PdfPathFillMode::None),
274282
});
275283
}
276284
}

0 commit comments

Comments
 (0)