pub struct PathContent {
pub bbox: Rect,
pub operations: Vec<PathOperation>,
pub stroke_color: Option<Color>,
pub fill_color: Option<Color>,
pub stroke_width: f32,
pub line_cap: LineCap,
pub line_join: LineJoin,
pub dash_pattern: Option<(Vec<f32>, f32)>,
pub matrix: Option<[f32; 6]>,
pub reading_order: Option<usize>,
pub artifact_type: Option<ArtifactType>,
pub layer: Option<String>,
}Expand description
Vector path content that can be extracted from or written to a PDF.
This represents vector graphics such as lines, curves, and shapes.
Fields§
§bbox: RectBounding box of the path
operations: Vec<PathOperation>Path operations
stroke_color: Option<Color>Stroke color (None for no stroke)
fill_color: Option<Color>Fill color (None for no fill)
stroke_width: f32Stroke width in points
line_cap: LineCapLine cap style
line_join: LineJoinLine join style
dash_pattern: Option<(Vec<f32>, f32)>Optional dash pattern: Some((dashes, phase)) emits a
[dashes...] phase d operator before stroking. dashes is on/
off lengths in points (e.g. [3.0, 2.0] = dash 3 pt, gap 2 pt,
repeating); phase is the starting offset. None leaves the
line solid.
matrix: Option<[f32; 6]>Optional 2D affine transform in PDF row order [a b c d e f].
When set, the path is wrapped in q ... cm ... Q on emission
so graphics-state stays scoped. Populated by
FluentPageBuilder::{rotated, scaled, translated, with_transform}
closures — v0.3.39 (text-only) extended here to cover paths.
#393 Bundle A-2 follow-up.
reading_order: Option<usize>Reading order index
artifact_type: Option<ArtifactType>When set, this path is wrapped in /Artifact <</Type /T>> BDC … EMC
markers so accessibility tools can skip it. Useful for decorative
separator lines (footnote rules, header/footer rules).
layer: Option<String>Optional Content Group (PDF “layer”) name resolved from the
surrounding BDC /OC … EMC markers in the content stream. Set
during path extraction by PathExtractor when an OCG is active;
None for paths emitted outside any /OC-tagged marked-content
region or in PDFs that do not declare optional content.
The string is the human-readable /Name entry of the referenced
OptionalContentGroup, e.g. "A-GRID", "S-COLS", "A-WALL-DIM"
for PDFs exported from Revit/AutoCAD with layer metadata intact.
Reference: ISO 32000-1:2008 §8.11 (Optional Content) + §14.6
(Marked Content).
Implementations§
Source§impl PathContent
impl PathContent
Sourcepub const PAGE_FRAME_COVERAGE_THRESHOLD: f32 = 0.9
pub const PAGE_FRAME_COVERAGE_THRESHOLD: f32 = 0.9
True when this path is a rectangle covering at least this fraction of the MediaBox in both dimensions. 0.9 leaves generous margin for a page background that stops just short of the trim box while still rejecting anything smaller than page furniture — a genuine table spanning most of a page’s text column is still well under 90% of the full MediaBox once margins are accounted for. GH#1656. ~keep
Sourcepub fn from_operations(operations: Vec<PathOperation>) -> Self
pub fn from_operations(operations: Vec<PathOperation>) -> Self
Create a path from operations.
Sourcepub fn with_stroke(self, color: Color) -> Self
pub fn with_stroke(self, color: Color) -> Self
Set stroke color.
Sourcepub fn with_stroke_width(self, width: f32) -> Self
pub fn with_stroke_width(self, width: f32) -> Self
Set stroke width.
Sourcepub fn with_reading_order(self, order: usize) -> Self
pub fn with_reading_order(self, order: usize) -> Self
Set reading order.
Sourcepub fn with_layer(self, layer: impl Into<String>) -> Self
pub fn with_layer(self, layer: impl Into<String>) -> Self
Set the Optional Content Group (PDF “layer”) name. Used by
PathExtractor while walking the content stream to attach the
active OCG name to each extracted path.
Sourcepub fn push(&mut self, op: PathOperation)
pub fn push(&mut self, op: PathOperation)
Add a path operation.
Sourcepub fn has_stroke(&self) -> bool
pub fn has_stroke(&self) -> bool
Check if this path has a stroke.
Sourcepub fn is_straight_line(&self) -> bool
pub fn is_straight_line(&self) -> bool
Check if this path represents a single straight line (v0.3.14).
A path is a straight line if it has exactly 2 operations: MoveTo followed by LineTo.
Sourcepub fn rendered_bbox(&self) -> Rect
pub fn rendered_bbox(&self) -> Rect
Rendered extents of this path: the geometric bbox
inflated by the stroke (ISO 32000-1:2008 §8.4.3.2 — half the line
width straddles each side of the path).
Print-era producers draw a table’s vertical rule as a ~1 pt
horizontal segment stroked as wide as the table is tall, so the
geometric bbox (1×0 pt) bears no resemblance to the bar the reader
sees. Classifiers that reason about what is rendered
(line orientation, table primitives, region hit-testing) should use
this; consumers that need the raw path geometry (corner tests, SVG
path data, editing) keep bbox.
For a single straight segment the inflation is exact: half the
stroke width perpendicular to the segment, plus the cap extension
along it (Butt caps add nothing axially; Round/Square add half
the width per end, §8.4.3.3). For any other shape the bbox is
inflated by half the width on all four sides — a conservative
superset that is exact for axis-aligned stroked outlines.
Unstroked paths return bbox unchanged.
Sourcepub fn is_horizontal_line(&self, tolerance: f32) -> bool
pub fn is_horizontal_line(&self, tolerance: f32) -> bool
Check if this path is a horizontal line within a tolerance (v0.3.16).
tolerance bounds the geometric minor extent as before (a nearly
axis-aligned segment or sliver rectangle). Orientation is then
confirmed on the rendered_bbox: a 1 pt
segment stroked 430 pt wide renders as a vertical bar and must not
classify as a horizontal line just because its raw geometry is a
horizontal speck.
Sourcepub fn is_vertical_line(&self, tolerance: f32) -> bool
pub fn is_vertical_line(&self, tolerance: f32) -> bool
Check if this path is a vertical line within a tolerance (v0.3.16).
Same contract as is_horizontal_line:
geometric thinness within tolerance, orientation confirmed on the
rendered extents.
Sourcepub fn is_nearly_touching(&self, other: &Rect, tolerance: f32) -> bool
pub fn is_nearly_touching(&self, other: &Rect, tolerance: f32) -> bool
Check if this path’s bounding box is nearly touching another (v0.3.16).
Sourcepub fn is_rectangle(&self) -> bool
pub fn is_rectangle(&self) -> bool
Check if this path represents a single rectangle (v0.3.14).
A path is a rectangle if it has exactly 1 operation: Rectangle, or if it has 5 operations: MoveTo, 3x LineTo, ClosePath that form a rectangle.
Sourcepub fn is_table_primitive(&self) -> bool
pub fn is_table_primitive(&self) -> bool
Check if this path is “box-like” or “line-like” based on its dimensions (v0.3.16). This is a fuzzy heuristic for table detection.
Line length is judged on the rendered_bbox
while thinness stays geometric, so a table rule encoded as a short
segment with a table-height stroke width — a 1×0 pt geometric speck
rendering as a 1×430 pt bar — is recognized as the ruling it draws
, and an ordinary rule with a thick stroke keeps
classifying as a line exactly as before.
Thresholds (inherited from the v0.3.16 heuristic): a ruling must run more than 5 pt — half an em at 10 pt body text — so tick marks and list bullets don’t qualify; it must be under 2 pt across, twice the heaviest common ruling weight (typical PDF rules are 0.5–1 pt) while safely below any filled cell; and a box must stay under 1000 pt per side so absurdly large shapes (chart backgrounds, full artboards) don’t seed a cluster on their own.
GH#1656: 1000 pt does not exclude an ordinary page — A4
(595×842) and US-Letter (612×792) both pass this bound, so a
full-page background rectangle (common Office-to-PDF output) is
accepted here as a table primitive and can cluster with every
other primitive on the page. This function has no access to the
page’s MediaBox (PathContent carries no page dimensions), so it
cannot make that call itself. Page-relative filtering of such
furniture happens one level up, in PdfDocument::extract_tables_with_config
and PdfDocument::extract_page_tables (document/tables.rs), which use
PathContent::is_page_frame_rectangle
against the real MediaBox before primitives reach the clusterer in
spatial_table_detector.rs. ~keep
Sourcepub fn is_page_frame_rectangle(&self, media_box: (f32, f32, f32, f32)) -> bool
pub fn is_page_frame_rectangle(&self, media_box: (f32, f32, f32, f32)) -> bool
True when this path is a rectangle spanning at least
PAGE_FRAME_COVERAGE_THRESHOLD
of media_box in both width and height — page furniture (a
full-bleed background fill, a decorative frame) rather than a table
primitive. GH#1656: is_table_primitive’s < 1000 pt bound admits
ordinary page-sized rectangles (A4, Letter), which then seed a
whole-page false-positive cluster; this predicate is how callers with
access to the page’s MediaBox (PathContent itself carries none) can
exclude that furniture before it reaches the clusterer.
media_box is (llx, lly, urx, ury) as returned by
PdfDocument::get_page_media_box. Requiring both dimensions to
clear the threshold is deliberate: a full-page-width footer rule
(wide, but a fraction of a point tall) must not be classified as page
furniture and dropped — it is a legitimate table/ruling primitive.
A degenerate media_box (zero or negative area, non-finite) can’t
establish “most of the page”, so this returns false — callers
should treat a failed MediaBox lookup as “leave every primitive
alone”, not as license to filter on garbage. ~keep
Sourcepub fn rounded_rect(
x: f32,
y: f32,
width: f32,
height: f32,
radius: f32,
) -> Self
pub fn rounded_rect( x: f32, y: f32, width: f32, height: f32, radius: f32, ) -> Self
Create a rounded rectangle path.
§Arguments
x- X coordinate of the bottom-left cornery- Y coordinate of the bottom-left cornerwidth- Width of the rectangleheight- Height of the rectangleradius- Corner radius (clamped to min(width, height) / 2)
§Example
let rounded = PathContent::rounded_rect(10.0, 10.0, 100.0, 50.0, 5.0);Sourcepub fn to_points(&self, tolerance: f32) -> Vec<Vec<(f32, f32)>>
pub fn to_points(&self, tolerance: f32) -> Vec<Vec<(f32, f32)>>
Flatten this path into polylines — one Vec<(x, y)> per subpath.
Straight segments (MoveTo/LineTo) pass through unchanged; cubic
Béziers (CurveTo) are adaptively subdivided so the resulting polyline
stays within tolerance of the true curve. A Rectangle becomes its
own closed 5-point subpath, and ClosePath appends the subpath’s start
point. A new subpath begins at every MoveTo (and every Rectangle).
tolerance is expressed in the same coordinate space as the path’s own
points, and the returned points are in that space verbatim. For paths
from crate::document::PdfDocument::extract_paths that is the page’s
default user space (PDF points, y-up), with any content-stream cm and
Form-XObject /Matrix transforms already folded in — so a path drawn
under a scaling transform is returned (and flattened) at its final
on-page size, and no tolerance rescaling is needed. It is not device or
pixel space: page /Rotate and rendering DPI are not applied. Smaller
values yield more, finer points. A non-positive or non-finite tolerance
is floored to a small epsilon so subdivision always terminates.
This is intended for consumers that need sampled coordinates rather than drawing operators — e.g. digitising chart/ECG/CAD traces from vector PDFs.
§Examples
use xberg_native_pdf::elements::{PathContent, PathOperation};
let path = PathContent::from_operations(vec![
PathOperation::MoveTo(0.0, 0.0),
PathOperation::CurveTo(0.0, 10.0, 10.0, 10.0, 10.0, 0.0),
]);
let subpaths = path.to_points(0.25);
assert_eq!(subpaths.len(), 1);
assert_eq!(subpaths[0][0], (0.0, 0.0)); // starts at P0