use kurbo::{Affine, BezPath, Rect};
use pdfrum_common::PageIndex;
use pdfrum_page::GraphicsState;
use pdfrum_page::{
ColorSpace, Content, FillRule, IndexOutOfRange, PageObject, PathObject, TextObject, TextSegment,
};
use crate::Page;
#[derive(Debug, Clone)]
pub struct PageEdit {
pub(crate) index: PageIndex,
pub(crate) page: pdfrum_page::Page,
}
impl PageEdit {
#[must_use]
pub fn len(&self) -> usize {
self.page.objects().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.page.objects().is_empty()
}
#[must_use]
pub fn index(&self) -> PageIndex {
self.index
}
#[must_use]
pub fn objects(&self) -> &[PageObject] {
self.page.objects()
}
pub fn object_mut(&mut self, index: usize) -> Option<&mut PageObject> {
self.page.object_mut(index)
}
pub fn push(&mut self, object: PageObject) {
self.page.push_object(object);
}
pub fn insert(&mut self, index: usize, object: PageObject) -> Result<(), IndexOutOfRange> {
self.page.insert_object(index, object)
}
pub fn remove(&mut self, index: usize) -> Option<PageObject> {
self.page.remove_object(index)
}
pub fn show(&mut self, index: usize) -> Result<(), IndexOutOfRange> {
self.set_active(index, true)
}
pub fn hide(&mut self, index: usize) -> Result<(), IndexOutOfRange> {
self.set_active(index, false)
}
fn set_active(&mut self, index: usize, active: bool) -> Result<(), IndexOutOfRange> {
let len = self.page.objects.len();
let Some(object) = self.page.objects.get_mut(index) else {
return Err(IndexOutOfRange { index, len });
};
object.set_active(active);
Ok(())
}
#[must_use]
pub fn is_visible(&self, index: usize) -> Option<bool> {
self.page.objects().get(index).map(PageObject::is_active)
}
pub fn transform(&mut self, index: usize, transform: Affine) -> Result<(), IndexOutOfRange> {
let len = self.page.objects.len();
let Some(object) = self.page.object_mut(index) else {
return Err(IndexOutOfRange { index, len });
};
transform_object(object, transform);
Ok(())
}
#[must_use]
pub fn is_modified(&self) -> bool {
self.page.is_dirty()
}
#[must_use]
pub fn graph(&self) -> &pdfrum_page::Page {
&self.page
}
pub fn graph_mut(&mut self) -> &mut pdfrum_page::Page {
&mut self.page
}
}
pub(crate) fn transform_object(object: &mut PageObject, transform: Affine) {
match object {
PageObject::Path(p) => p.object.matrix = transform * p.object.matrix,
PageObject::Text(t) => {
t.object.matrix = transform * t.object.matrix;
t.object.position = transform * t.object.position;
}
PageObject::Image(i) => i.object.matrix = transform * i.object.matrix,
PageObject::Shading(s) => s.object.matrix = transform * s.object.matrix,
PageObject::Form(f) => f.object.matrix = transform * f.object.matrix,
}
}
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: pdfrum_page::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: pdfrum_page::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(),
)))
}
}
impl PageEdit {
#[must_use]
pub fn font_of(&self, index: usize) -> Option<pdfrum_object::ObjRef> {
match self.page.objects().get(index)? {
PageObject::Text(text) => text.object.font_source,
_ => None,
}
}
#[must_use]
pub fn image_of(&self, index: usize) -> Option<pdfrum_object::ObjRef> {
match self.page.objects().get(index)? {
PageObject::Image(image) => image.object.source,
_ => None,
}
}
}
impl Page<'_> {
#[must_use]
pub fn edit(&self) -> PageEdit {
PageEdit {
index: self.index,
page: self.build_for_edit(&mut pdfrum_page::BuildContext::new()),
}
}
}