use kurbo::{Affine, BezPath, Rect};
use pdfrum_page::{
ColorSpace, Content, FillRule, GraphicsState, PageObject, PathObject, TextObject,
TextRenderMode, TextSegment,
};
fn rgba_of(color: peniko::Color) -> ([f32; 3], f32) {
let [r, g, b, alpha] = color.components;
(
[r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)],
alpha.clamp(0.0, 1.0),
)
}
#[derive(Debug, Clone, PartialEq)]
pub struct PathBuilder {
pub path: BezPath,
pub fill: Option<peniko::Color>,
pub stroke: Option<peniko::Color>,
pub line_width: f32,
pub even_odd: bool,
pub matrix: Affine,
}
impl Default for PathBuilder {
fn default() -> Self {
Self {
path: BezPath::new(),
fill: Some(peniko::Color::BLACK),
stroke: None,
line_width: 1.0,
even_odd: false,
matrix: Affine::IDENTITY,
}
}
}
impl PathBuilder {
#[must_use]
pub fn rect(rect: Rect) -> Self {
let mut path = BezPath::new();
path.move_to((rect.x0, rect.y0));
path.line_to((rect.x1, rect.y0));
path.line_to((rect.x1, rect.y1));
path.line_to((rect.x0, rect.y1));
path.close_path();
Self {
path,
..Self::default()
}
}
#[must_use]
pub fn build(self) -> PageObject {
let mut state = GraphicsState::default();
if let Some(fill) = self.fill {
let (rgb, alpha) = rgba_of(fill);
state.fill.set_stock(ColorSpace::DeviceRgb, &rgb);
state.general.fill_alpha = alpha;
}
if let Some(stroke) = self.stroke {
let (rgb, alpha) = rgba_of(stroke);
state.stroke.set_stock(ColorSpace::DeviceRgb, &rgb);
state.general.stroke_alpha = alpha;
}
state.stroke_params.width = self.line_width;
let fill_rule = match (self.fill.is_some(), self.even_odd) {
(false, _) => FillRule::None,
(true, false) => FillRule::Winding,
(true, true) => FillRule::EvenOdd,
};
PageObject::Path(Box::new(Content::new(
PathObject {
path: self.path,
matrix: self.matrix,
fill_rule,
stroke: self.stroke.is_some(),
},
state,
)))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextBuilder {
pub codes: Vec<u8>,
pub font: pdfrum_object::ObjRef,
pub size: f32,
pub position: kurbo::Point,
pub render_mode: TextRenderMode,
pub fill: peniko::Color,
}
impl TextBuilder {
#[must_use]
pub fn new(codes: impl Into<Vec<u8>>, font: pdfrum_object::ObjRef, size: f32) -> Self {
Self {
codes: codes.into(),
font,
size,
position: kurbo::Point::ZERO,
render_mode: TextRenderMode::Fill,
fill: peniko::Color::BLACK,
}
}
#[must_use]
pub fn build(self) -> PageObject {
let mut state = GraphicsState::default();
let (rgb, alpha) = rgba_of(self.fill);
state.fill.set_stock(ColorSpace::DeviceRgb, &rgb);
state.general.fill_alpha = alpha;
PageObject::Text(Box::new(Content::new(
TextObject {
segments: Box::new([TextSegment {
codes: self.codes.into_boxed_slice(),
kerning: 0.0,
}]),
position: self.position,
matrix: Affine::scale(f64::from(self.size)),
font: None,
font_source: Some(self.font),
render_mode: self.render_mode,
type3_metrics: std::collections::BTreeMap::new(),
},
state,
)))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageBuilder {
pub source: pdfrum_object::ObjRef,
pub matrix: Affine,
}
impl ImageBuilder {
#[must_use]
pub fn at(source: pdfrum_object::ObjRef, rect: Rect) -> Self {
Self {
source,
matrix: Affine::new([rect.width(), 0.0, 0.0, rect.height(), rect.x0, rect.y0]),
}
}
#[must_use]
pub fn build(self) -> PageObject {
let placeholder = pdfrum_page::ImageData {
width: 1,
height: 1,
samples: pdfrum_page::Samples::Whole(pdfrum_page::Pixels::Gray8(Box::new([0]))),
mask: None,
matte: None,
interpolate: false,
};
PageObject::Image(Box::new(Content::new(
pdfrum_page::ImageObject {
image: std::sync::Arc::new(placeholder),
matrix: self.matrix,
is_mask: false,
oc: None,
source: Some(self.source),
},
GraphicsState::default(),
)))
}
}