pdfrum_edit/canvas.rs
1//! Drawing on an existing page without writing content-stream operators.
2//!
3//! A [`Canvas`] is a retained drawing surface over one page. A caller places
4//! fills, strokes, text and images in the page's *displayed* coordinate space
5//! — y-up, in PDF points, with the crop box and `/Rotate` already composed in
6//! — and the canvas emits one content stream that is **appended** to the
7//! page's `/Contents`. The page's own streams are never rewritten, so nothing
8//! a save would otherwise lose (`pdfrum_edit`'s regeneration losses) applies
9//! to a page that is only drawn on. The coordinate space is on [`Canvas`];
10//! the resource-merging rule is on [`EditDoc::draw_page`].
11//!
12//! # There is no layout here, deliberately
13//!
14//! [`Canvas::text`] draws one string at one point. There is no line breaking,
15//! no wrapping and no paragraph model, and the only measurement is
16//! [`Canvas::text_width`], a single string's advance. A caller who needs
17//! layout has a typesetting problem and brings their own layout to `text`.
18
19mod blend;
20mod glyphs;
21mod gradient;
22
23use std::fmt::Write as _;
24
25pub use blend::BlendMode;
26pub use glyphs::{GlyphRun, RunGlyph};
27pub use gradient::{Gradient, GradientKind, GradientStop};
28
29use crate::{ContentsShape, EmbeddedFont, EmbeddedImage, write_float, write_matrix, write_point};
30use kurbo::{Affine, BezPath, PathEl, Point, Rect, RoundedRect, Shape};
31use pdfrum_common::{Diagnostics, Limits, PageIndex};
32use pdfrum_object::{
33 Array, ByteSpan, Dict, Name, ObjRef, Object, Resolve, Stream, names as pdf_names,
34};
35
36use crate::{EditDoc, Error};
37use peniko::Color;
38
39/// A drawing either applies or names why it could not.
40type Result<T> = core::result::Result<T, Error>;
41
42/// How a shape is painted.
43///
44/// An enum rather than two `Option<Color>` fields because the three states
45/// are what the PDF paint operators actually offer, and "neither" is not one
46/// of them: a caller who wants to paint nothing does not call the method.
47#[derive(Debug, Clone, PartialEq)]
48pub enum Paint {
49 /// Filled only. Written as `f` or `f*`.
50 Fill(Color),
51 /// Stroked only, at [`Stroke::width`]. Written as `S`.
52 Stroke(Stroke),
53 /// Filled and stroked, the fill first. Written as `B` or `B*`.
54 FillStroke(Color, Stroke),
55}
56
57impl Paint {
58 /// The fill colour, if this paint fills.
59 ///
60 /// ```
61 /// use pdfrum::{Color, Paint};
62 ///
63 /// assert_eq!(Paint::Fill(Color::BLACK).fill(), Some(Color::BLACK));
64 /// ```
65 #[must_use]
66 pub fn fill(&self) -> Option<Color> {
67 match self {
68 Self::Fill(color) | Self::FillStroke(color, _) => Some(*color),
69 Self::Stroke(_) => None,
70 }
71 }
72
73 /// The stroke, if this paint strokes.
74 ///
75 /// ```
76 /// use pdfrum::{Color, Paint, Stroke};
77 ///
78 /// assert!(Paint::Fill(Color::BLACK).stroke().is_none());
79 /// assert!(Paint::Stroke(Stroke::new(Color::BLACK, 2.0)).stroke().is_some());
80 /// ```
81 #[must_use]
82 pub fn stroke(&self) -> Option<&Stroke> {
83 match self {
84 Self::Stroke(stroke) | Self::FillStroke(_, stroke) => Some(stroke),
85 Self::Fill(_) => None,
86 }
87 }
88}
89
90/// A stroke's colour, width and pen shape, in canvas units.
91///
92/// The fields stay public and every one but `color` and `width` has a
93/// default, so `Stroke { cap: LineCap::Round, ..Stroke::new(color, 1.0) }`
94/// works and the four settings ISO 32000-1 §8.4.3.3-§8.4.3.6 name are
95/// reachable without builder ceremony. [`Stroke::new`] keeps meaning what it
96/// always meant: PDF's own defaults — butt cap, miter join, miter limit 10,
97/// no dash.
98#[derive(Debug, Clone, PartialEq)]
99pub struct Stroke {
100 /// The colour. Its alpha is honoured, as an `/ExtGState` `/CA`.
101 pub color: Color,
102 /// The line width in canvas units — page points.
103 pub width: f64,
104 /// How the open ends of a subpath are drawn. `J`.
105 pub cap: LineCap,
106 /// How two segments meet at a corner. `j`.
107 pub join: LineJoin,
108 /// Where a [`LineJoin::Miter`] corner becomes a bevel instead, as the
109 /// ratio of miter length to line width. `M`.
110 pub miter_limit: MiterLimit,
111 /// The on/off pattern, or `None` for a solid line. `d`.
112 pub dash: Option<Dash>,
113}
114
115impl Stroke {
116 /// A solid stroke of `color` at `width` points, with PDF's default pen:
117 /// butt cap, miter join, miter limit 10.
118 ///
119 /// ```
120 /// let hairline = pdfrum::Stroke::new(pdfrum::Color::BLACK, 0.5);
121 /// assert_eq!(hairline.width, 0.5);
122 /// assert_eq!(hairline.cap, pdfrum::LineCap::Butt);
123 /// assert!(hairline.dash.is_none());
124 /// ```
125 #[must_use]
126 pub fn new(color: Color, width: f64) -> Self {
127 Self {
128 color,
129 width,
130 cap: LineCap::Butt,
131 join: LineJoin::Miter,
132 miter_limit: MiterLimit::default(),
133 dash: None,
134 }
135 }
136
137 /// The same stroke with `cap` at its open ends.
138 ///
139 /// ```
140 /// use pdfrum::{Color, LineCap, Stroke};
141 ///
142 /// let round = Stroke::new(Color::BLACK, 4.0).with_cap(LineCap::Round);
143 /// assert_eq!(round.cap, LineCap::Round);
144 /// ```
145 #[must_use]
146 pub fn with_cap(mut self, cap: LineCap) -> Self {
147 self.cap = cap;
148 self
149 }
150
151 /// The same stroke with `join` at its corners.
152 ///
153 /// ```
154 /// use pdfrum::{Color, LineJoin, Stroke};
155 ///
156 /// let soft = Stroke::new(Color::BLACK, 4.0).with_join(LineJoin::Round);
157 /// assert_eq!(soft.join, LineJoin::Round);
158 /// ```
159 #[must_use]
160 pub fn with_join(mut self, join: LineJoin) -> Self {
161 self.join = join;
162 self
163 }
164
165 /// The same stroke with `limit` on its miter joins.
166 ///
167 /// ```
168 /// use pdfrum::{Color, MiterLimit, Stroke};
169 ///
170 /// let blunt = Stroke::new(Color::BLACK, 4.0).with_miter_limit(MiterLimit::new(2.0));
171 /// assert_eq!(blunt.miter_limit.get(), 2.0);
172 /// ```
173 #[must_use]
174 pub fn with_miter_limit(mut self, limit: MiterLimit) -> Self {
175 self.miter_limit = limit;
176 self
177 }
178
179 /// The same stroke dashed by `dash`.
180 ///
181 /// ```
182 /// use pdfrum::{Color, Dash, Stroke};
183 ///
184 /// let dashed = Stroke::new(Color::BLACK, 1.0)
185 /// .with_dash(Dash::new(&[4.0, 2.0], 0.0).expect("a valid dash"));
186 /// assert!(dashed.dash.is_some());
187 /// ```
188 #[must_use]
189 pub fn with_dash(mut self, dash: Dash) -> Self {
190 self.dash = Some(dash);
191 self
192 }
193}
194
195/// How the open ends of a stroked subpath are drawn — ISO 32000-1 §8.4.3.3's
196/// line cap style, written as `J`.
197///
198/// An enum rather than the `0`/`1`/`2` the operator takes: the wire spelling
199/// is an encoding detail, and `LineCap::Round` says at a call
200/// site what `1` does not.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
202pub enum LineCap {
203 /// Squared off exactly at the endpoint. PDF's default.
204 #[default]
205 Butt,
206 /// A half-disc of the line's width centred on the endpoint.
207 Round,
208 /// A half-square projecting half a line width past the endpoint.
209 Square,
210}
211
212impl LineCap {
213 /// The operand `J` takes.
214 fn operand(self) -> u8 {
215 match self {
216 Self::Butt => 0,
217 Self::Round => 1,
218 Self::Square => 2,
219 }
220 }
221}
222
223/// How two segments meet at a corner — ISO 32000-1 §8.4.3.4's line join
224/// style, written as `j`.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
226pub enum LineJoin {
227 /// Extended outer edges meeting in a point, beveled past
228 /// [`Stroke::miter_limit`]. PDF's default.
229 #[default]
230 Miter,
231 /// An arc of the line's width around the corner point.
232 Round,
233 /// The notch between the two segments filled with a triangle.
234 Bevel,
235}
236
237impl LineJoin {
238 /// The operand `j` takes.
239 fn operand(self) -> u8 {
240 match self {
241 Self::Miter => 0,
242 Self::Round => 1,
243 Self::Bevel => 2,
244 }
245 }
246}
247
248/// The ratio of miter length to line width past which a [`LineJoin::Miter`]
249/// corner is drawn beveled instead — ISO 32000-1 §8.4.3.5's `M`.
250///
251/// A newtype rather than a bare `f64` because the value has a floor: the
252/// miter length is never shorter than the line width, so a ratio below 1
253/// asks for something that cannot happen. It clamps rather than refusing,
254/// unlike [`Dash`], because every out-of-range ratio has one obviously
255/// intended reading and none of them makes a reader reject the stream.
256#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
257pub struct MiterLimit(f64);
258
259impl MiterLimit {
260 /// A miter limit of `ratio`, clamped up to 1; a non-finite ratio gives
261 /// the default.
262 ///
263 /// ```
264 /// use pdfrum::MiterLimit;
265 ///
266 /// assert_eq!(MiterLimit::new(4.0).get(), 4.0);
267 /// assert_eq!(MiterLimit::new(0.5).get(), 1.0);
268 /// assert_eq!(MiterLimit::new(f64::NAN).get(), 10.0);
269 /// ```
270 #[must_use]
271 pub fn new(ratio: f64) -> Self {
272 if ratio.is_finite() {
273 Self(ratio.max(1.0))
274 } else {
275 Self::default()
276 }
277 }
278
279 /// The ratio.
280 ///
281 /// ```
282 /// assert_eq!(pdfrum::MiterLimit::default().get(), 10.0);
283 /// ```
284 #[must_use]
285 pub fn get(self) -> f64 {
286 self.0
287 }
288}
289
290impl Default for MiterLimit {
291 /// PDF's own initial value, 10 (ISO 32000-1 table 52).
292 fn default() -> Self {
293 Self(10.0)
294 }
295}
296
297/// A dash pattern — ISO 32000-1 §8.4.3.6's dash array and phase, written as
298/// `d`.
299///
300/// # Why construction is fallible
301///
302/// `d` is one of the few graphics-state operators a reader may reject
303/// outright: a negative length, or an array summing to zero, is not a
304/// degenerate dash but an *invalid* one, and a viewer that refuses it refuses
305/// the whole content stream — every later operator with it. So an invalid
306/// array is caught here, at construction, rather than normalized into a
307/// pattern the caller did not ask for and cannot see. [`Dash::new`] returns
308/// `None` and nothing reaches the stream.
309///
310/// A solid line is spelled `Stroke::dash = None`, so an empty array is
311/// refused too: it has a valid PDF spelling, but it means the thing the
312/// `Option` already says.
313#[derive(Debug, Clone, PartialEq)]
314pub struct Dash {
315 /// Alternating on and off lengths, all finite and non-negative, summing
316 /// to more than zero.
317 lengths: Vec<f64>,
318 /// How far into the pattern the line starts. Finite and non-negative.
319 phase: f64,
320}
321
322impl Dash {
323 /// A dash of alternating on/off `lengths`, starting `phase` units into
324 /// the pattern.
325 ///
326 /// `None` if `lengths` is empty, holds anything negative or not finite,
327 /// or sums to zero, or if `phase` is negative or not finite — each of
328 /// which is an invalid `d` operand rather than an unusual one.
329 ///
330 /// ```
331 /// use pdfrum::Dash;
332 ///
333 /// assert!(Dash::new(&[4.0, 2.0], 0.0).is_some());
334 /// assert!(Dash::new(&[4.0, -2.0], 0.0).is_none());
335 /// assert!(Dash::new(&[0.0, 0.0], 0.0).is_none());
336 /// assert!(Dash::new(&[], 0.0).is_none());
337 /// ```
338 #[must_use]
339 pub fn new(lengths: &[f64], phase: f64) -> Option<Self> {
340 if lengths.is_empty() || !phase.is_finite() || phase < 0.0 {
341 return None;
342 }
343 if lengths
344 .iter()
345 .any(|length| !length.is_finite() || *length < 0.0)
346 {
347 return None;
348 }
349 if lengths.iter().sum::<f64>() <= 0.0 {
350 return None;
351 }
352 Some(Self {
353 lengths: lengths.to_vec(),
354 phase,
355 })
356 }
357
358 /// The alternating on/off lengths.
359 ///
360 /// ```
361 /// let dash = pdfrum::Dash::new(&[3.0, 1.0], 0.5).expect("a valid dash");
362 /// assert_eq!(dash.lengths(), &[3.0, 1.0]);
363 /// assert_eq!(dash.phase(), 0.5);
364 /// ```
365 #[must_use]
366 pub fn lengths(&self) -> &[f64] {
367 &self.lengths
368 }
369
370 /// How far into the pattern the line starts.
371 #[must_use]
372 pub fn phase(&self) -> f64 {
373 self.phase
374 }
375}
376
377/// Which points a fill considers inside.
378///
379/// The `pdfrum-page` reader's `FillRule` carries a third `None` case for a
380/// path that is only stroked; here that case is spelled by [`Paint::Stroke`]
381/// instead, so this enum has exactly the two rules ISO 32000-1 §8.5.3.3
382/// defines.
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
384pub enum Fill {
385 /// The nonzero winding rule — `f`, `B`. The default.
386 #[default]
387 NonZero,
388 /// The even-odd rule — `f*`, `B*`.
389 EvenOdd,
390}
391
392/// One page's drawing surface.
393///
394/// Handed to the closure of [`EditDoc::draw_page`] and
395/// [`EditDoc::draw_pages`]; it cannot be constructed otherwise, because a
396/// canvas is only meaningful against the page whose space it maps and the
397/// session whose resources it merges into.
398///
399/// # The coordinate space
400///
401/// Canvas coordinates are the page **as displayed**, in points:
402///
403/// - the origin is the lower-left corner of the crop box after `/Rotate`;
404/// - x runs right and **y runs up**, as PDF page space does and unlike a
405/// raster;
406/// - the extent is [`Canvas::size`], whose sides are the crop box's *swapped*
407/// on a quarter or three-quarter turn.
408///
409/// So a caller places things where they see them: on a page with
410/// `/Rotate 90`, `Point::new(0.0, 0.0)` is the bottom-left corner on screen,
411/// and text drawn along +x reads upright there. The composition is exactly
412/// the inverse of [`pdfrum_page::Rotation::display_matrix`] over the crop
413/// box, which is the same matrix the renderer uses, so what a caller places
414/// and what a viewer shows cannot drift apart.
415///
416/// Every method takes canvas coordinates. [`Canvas::transform`] composes a
417/// further transform *inside* that space, so a rotation about a point is
418/// written in the coordinates the caller is already using.
419pub struct Canvas<'a, 'b> {
420 /// Where the operators accumulate. Not yet wrapped in `q`/`Q`.
421 out: String,
422 /// The session the resources are merged into and new objects allocated
423 /// from.
424 edit: &'a mut EditDoc<'b>,
425 /// The ceilings a font read while measuring text obeys.
426 limits: Limits,
427 /// The faces an ingested SVG's `<text>` is set in.
428 #[cfg(feature = "svg-text")]
429 fonts: crate::svg_text::SvgFonts,
430 /// The resources this drawing needs, by category, under names already
431 /// checked against the page's own.
432 added: Vec<(&'static Name, Name, Object)>,
433 /// Names already taken: the page's own, plus every name this drawing has
434 /// allocated. Fresh names are chosen against this set, per category.
435 taken: Vec<(&'static Name, Name)>,
436 /// The displayed size, in points.
437 size: kurbo::Size,
438 /// What the operators are being written into.
439 surface: Surface,
440 /// The first error a drawing method hit. Reported once, from
441 /// [`EditDoc::draw_page`], rather than at every call.
442 failed: Option<Error>,
443}
444
445impl std::fmt::Debug for Canvas<'_, '_> {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 f.debug_struct("Canvas")
448 .field("size", &self.size)
449 .field("surface", &self.surface)
450 .field("failed", &self.failed)
451 .finish_non_exhaustive()
452 }
453}
454
455/// What a canvas's operators are being written into.
456///
457/// An enum rather than an `Option<PageIndex>`, because the two destinations
458/// differ in more than whether a page index exists: a page's drawing is
459/// *appended* to `/Contents` and merges into the page's own `/Resources`,
460/// while a form's becomes a standalone `/Subtype /Form` stream with a
461/// `/Resources` of its own and no page to collide with. [`Canvas::page`]
462/// answers for the first and has nothing to answer for the second, which is
463/// why it returns an `Option`.
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465enum Surface {
466 /// One page's appended content stream.
467 Page(PageIndex),
468 /// A Form `XObject`'s own stream, placed later by [`Canvas::place_form`].
469 ///
470 /// Behind the feature that is the only thing that compiles a form: with
471 /// `svg-import` off nothing constructs it, and a variant nothing
472 /// constructs is the dead code forbids.
473 #[cfg(feature = "svg-import")]
474 Form,
475}
476
477impl Canvas<'_, '_> {
478 /// The page's displayed size in points — the crop box's, with its sides
479 /// swapped on a quarter turn.
480 ///
481 /// The canvas's own extent: `Rect::from_origin_size(Point::ZERO, size)`
482 /// is the whole visible page.
483 ///
484 /// ```
485 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
486 /// let mut edit = doc.edit();
487 /// edit.draw_page(0, |c| {
488 /// assert!(c.size().width > 0.0);
489 /// })?;
490 /// # Ok::<(), pdfrum::Error>(())
491 /// ```
492 #[must_use]
493 pub fn size(&self) -> kurbo::Size {
494 self.size
495 }
496
497 /// The whole visible page, as a rectangle in canvas coordinates.
498 ///
499 /// ```
500 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
501 /// let mut edit = doc.edit();
502 /// edit.draw_page(0, |c| {
503 /// assert_eq!(c.bounds().origin(), pdfrum::Point::ZERO);
504 /// })?;
505 /// # Ok::<(), pdfrum::Error>(())
506 /// ```
507 #[must_use]
508 pub fn bounds(&self) -> Rect {
509 Rect::from_origin_size(Point::ZERO, self.size)
510 }
511
512 /// The page this canvas draws on, or `None` when it is compiling a Form
513 /// `XObject` that no page owns yet.
514 ///
515 /// A canvas handed to [`EditDoc::draw_page`] or [`EditDoc::draw_pages`]
516 /// always answers `Some`. The `None` case is a Form `XObject` compiled
517 /// by `EditDoc::compile_svg` (feature `svg-import`), whose content
518 /// belongs to no page until `Canvas::place_svg` puts it on one.
519 ///
520 /// ```
521 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world_2_pages.pdf")?;
522 /// let mut edit = doc.edit();
523 /// edit.draw_pages(|c| assert!(c.page().is_some_and(|p| u32::from(p) < 2)))?;
524 /// # Ok::<(), pdfrum::Error>(())
525 /// ```
526 #[must_use]
527 pub fn page(&self) -> Option<PageIndex> {
528 match self.surface {
529 Surface::Page(index) => Some(index),
530 #[cfg(feature = "svg-import")]
531 Surface::Form => None,
532 }
533 }
534
535 /// Draw inside a saved graphics state, restored when `body` returns.
536 ///
537 /// This is the *only* spelling of `q`/`Q`: there is no bare `save` a
538 /// caller could leave unmatched, and no `restore` that could pop a state
539 /// the caller did not push. Nesting is the closure nesting, so an
540 /// unbalanced stream is not expressible.
541 ///
542 /// ```
543 /// use pdfrum::{Color, Paint, Rect};
544 ///
545 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
546 /// let mut edit = doc.edit();
547 /// edit.draw_page(0, |c| {
548 /// c.saved(|c| {
549 /// c.clip(Rect::new(0.0, 0.0, 100.0, 100.0), pdfrum::Fill::NonZero);
550 /// c.fill_rect(Rect::new(0.0, 0.0, 500.0, 500.0), Color::from_rgb8(200, 0, 0));
551 /// });
552 /// // The clip is gone here.
553 /// c.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::BLACK);
554 /// })?;
555 /// # Ok::<(), pdfrum::Error>(())
556 /// ```
557 pub fn saved(&mut self, body: impl FnOnce(&mut Self)) {
558 self.out.push_str("q\n");
559 body(self);
560 self.out.push_str("Q\n");
561 }
562
563 /// Compose `transform` into the canvas space, for everything drawn after
564 /// it.
565 ///
566 /// Scoped by [`Canvas::saved`], like every other graphics-state change; a
567 /// transform outside one lasts for the rest of the drawing.
568 ///
569 /// ```
570 /// use pdfrum::{Affine, Color, Rect};
571 ///
572 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
573 /// let mut edit = doc.edit();
574 /// edit.draw_page(0, |c| {
575 /// c.saved(|c| {
576 /// c.transform(Affine::rotate_about(0.5, c.bounds().center()));
577 /// c.fill_rect(Rect::new(0.0, 0.0, 100.0, 20.0), Color::BLACK);
578 /// });
579 /// })?;
580 /// # Ok::<(), pdfrum::Error>(())
581 /// ```
582 pub fn transform(&mut self, transform: Affine) {
583 write_matrix(&mut self.out, transform);
584 self.out.push_str(" cm\n");
585 }
586
587 /// Intersect the clip with `shape`, for everything drawn after it.
588 ///
589 /// Scoped by [`Canvas::saved`]: a PDF clip can only ever be narrowed, so
590 /// a `q`/`Q` is the only way back.
591 ///
592 /// ```
593 /// use pdfrum::{Fill, Rect};
594 ///
595 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
596 /// let mut edit = doc.edit();
597 /// edit.draw_page(0, |c| {
598 /// c.saved(|c| c.clip(Rect::new(10.0, 10.0, 90.0, 90.0), Fill::NonZero));
599 /// })?;
600 /// # Ok::<(), pdfrum::Error>(())
601 /// ```
602 pub fn clip(&mut self, shape: impl Shape, rule: Fill) {
603 self.write_path(&shape.into_path(0.1));
604 self.out.push_str(match rule {
605 Fill::NonZero => " W n\n",
606 Fill::EvenOdd => " W* n\n",
607 });
608 }
609
610 /// Fill `shape` with `color`, by the nonzero rule.
611 ///
612 /// ```
613 /// use pdfrum::{Color, Rect};
614 ///
615 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
616 /// let mut edit = doc.edit();
617 /// edit.draw_page(0, |c| {
618 /// c.fill(Rect::new(0.0, 0.0, 50.0, 50.0), Color::from_rgb8(0, 0, 255));
619 /// })?;
620 /// # Ok::<(), pdfrum::Error>(())
621 /// ```
622 pub fn fill(&mut self, shape: impl Shape, color: Color) {
623 self.draw(shape, Paint::Fill(color), Fill::NonZero);
624 }
625
626 /// Fill `rect` with `color` — [`Canvas::fill`] on the commonest shape.
627 ///
628 /// ```
629 /// use pdfrum::{Color, Rect};
630 ///
631 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
632 /// let mut edit = doc.edit();
633 /// edit.draw_page(0, |c| {
634 /// c.fill_rect(Rect::new(0.0, 0.0, 50.0, 50.0), Color::BLACK);
635 /// })?;
636 /// # Ok::<(), pdfrum::Error>(())
637 /// ```
638 pub fn fill_rect(&mut self, rect: Rect, color: Color) {
639 self.fill(rect, color);
640 }
641
642 /// Fill a rectangle with `radius`-point rounded corners.
643 ///
644 /// ```
645 /// use pdfrum::{Color, Rect};
646 ///
647 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
648 /// let mut edit = doc.edit();
649 /// edit.draw_page(0, |c| {
650 /// c.fill_rounded_rect(Rect::new(0.0, 0.0, 80.0, 30.0), 6.0, Color::BLACK);
651 /// })?;
652 /// # Ok::<(), pdfrum::Error>(())
653 /// ```
654 pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, color: Color) {
655 self.fill(RoundedRect::from_rect(rect, radius), color);
656 }
657
658 /// Stroke `shape`.
659 ///
660 /// ```
661 /// use pdfrum::{Color, Rect, Stroke};
662 ///
663 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
664 /// let mut edit = doc.edit();
665 /// edit.draw_page(0, |c| {
666 /// c.stroke(Rect::new(0.0, 0.0, 50.0, 50.0), Stroke::new(Color::BLACK, 1.0));
667 /// })?;
668 /// # Ok::<(), pdfrum::Error>(())
669 /// ```
670 pub fn stroke(&mut self, shape: impl Shape, stroke: Stroke) {
671 self.draw(shape, Paint::Stroke(stroke), Fill::NonZero);
672 }
673
674 /// Stroke the straight segment from `from` to `to` — a header rule, a
675 /// divider.
676 ///
677 /// ```
678 /// use pdfrum::{Color, Point, Stroke};
679 ///
680 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
681 /// let mut edit = doc.edit();
682 /// edit.draw_page(0, |c| {
683 /// let y = c.size().height - 50.0;
684 /// c.line(Point::new(50.0, y), Point::new(c.size().width - 50.0, y),
685 /// Stroke::new(Color::BLACK, 0.75));
686 /// })?;
687 /// # Ok::<(), pdfrum::Error>(())
688 /// ```
689 pub fn line(&mut self, from: Point, to: Point, stroke: Stroke) {
690 self.stroke(kurbo::Line::new(from, to), stroke);
691 }
692
693 /// Paint `shape` with `paint`, filling by `rule`.
694 ///
695 /// The general case the other shape methods narrow: [`Canvas::fill`] is
696 /// `Paint::Fill` with [`Fill::NonZero`], [`Canvas::stroke`] is
697 /// `Paint::Stroke`.
698 ///
699 /// ```
700 /// use pdfrum::{Color, Fill, Paint, Rect, Stroke};
701 ///
702 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
703 /// let mut edit = doc.edit();
704 /// edit.draw_page(0, |c| {
705 /// c.draw(
706 /// Rect::new(0.0, 0.0, 40.0, 40.0),
707 /// Paint::FillStroke(Color::from_rgb8(255, 255, 0), Stroke::new(Color::BLACK, 2.0)),
708 /// Fill::EvenOdd,
709 /// );
710 /// })?;
711 /// # Ok::<(), pdfrum::Error>(())
712 /// ```
713 pub fn draw(&mut self, shape: impl Shape, paint: Paint, rule: Fill) {
714 let path = shape.into_path(0.1);
715 if path.elements().is_empty() {
716 return;
717 }
718 let operator = paint_operator(&paint, rule);
719 self.out.push_str("q\n");
720 self.set_paint(paint);
721 self.write_path(&path);
722 self.out.push_str(operator);
723 self.out.push_str("\nQ\n");
724 }
725
726 /// Draw `text` in `font` at `size`, with its baseline starting at `at`.
727 ///
728 /// `font` is one this session loaded through [`EditDoc::embed_font`] or
729 /// [`EditDoc::standard_font`], so its glyphs are subset and embedded by
730 /// the machinery that already does that for a saved font. A base-14 face
731 /// from `standard_font` needs no embedded program.
732 ///
733 /// One string, one point, one line: see the module documentation for why
734 /// there is no wrapping.
735 ///
736 /// # Errors
737 ///
738 /// A character `font` has no glyph for is an error, not a blank — the
739 /// canvas records it and [`EditDoc::draw_page`] returns it. Nothing of
740 /// this call is written when it fails, so a refused string leaves no
741 /// half-drawn run behind.
742 ///
743 /// ```
744 /// use pdfrum::{Color, Point, StandardFont};
745 ///
746 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
747 /// let mut edit = doc.edit();
748 /// let font = edit.standard_font(StandardFont::Helvetica)?;
749 /// edit.draw_page(0, |c| {
750 /// c.text("Page 1", &font, 10.0, Point::new(72.0, 72.0), Color::BLACK);
751 /// })?;
752 /// # Ok::<(), pdfrum::Error>(())
753 /// ```
754 pub fn text(&mut self, text: &str, font: &EmbeddedFont, size: f64, at: Point, color: Color) {
755 let codes = match font.encode_checked(text) {
756 Ok(codes) => codes,
757 Err(missing) => return self.fail(Error::from(missing)),
758 };
759 if codes.is_empty() {
760 return;
761 }
762 let name = self.realize(pdf_names::FONT, Object::Ref(font.object()));
763 self.out.push_str("q\n");
764 self.set_paint(Paint::Fill(color));
765 self.out.push_str("BT\n/");
766 self.push_name(&name);
767 self.out.push(' ');
768 write_f64(&mut self.out, size);
769 self.out.push_str(" Tf 1 0 0 1 ");
770 write_point(&mut self.out, at);
771 self.out.push_str(" Tm ");
772 write_hex_string(&mut self.out, &codes);
773 self.out.push_str(" Tj\nET\nQ\n");
774 }
775
776 /// The advance of `text` in `font` at `size`, in canvas units.
777 ///
778 /// The **only** measurement this API offers, and it is what centring a
779 /// single string needs. It is not a layout engine and does not claim to
780 /// be: no line breaking, no kerning beyond the font's own advances, and
781 /// no vertical metrics.
782 ///
783 /// `0.0` for a string `font` cannot encode.
784 ///
785 /// ```
786 /// use pdfrum::StandardFont;
787 ///
788 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
789 /// let mut edit = doc.edit();
790 /// let font = edit.standard_font(StandardFont::Helvetica)?;
791 /// edit.draw_page(0, |c| {
792 /// assert!(c.text_width("Hello", &font, 12.0) > 0.0);
793 /// })?;
794 /// # Ok::<(), pdfrum::Error>(())
795 /// ```
796 #[must_use]
797 pub fn text_width(&self, text: &str, font: &EmbeddedFont, size: f64) -> f64 {
798 let Ok(codes) = font.encode_checked(text) else {
799 return 0.0;
800 };
801 crate::string_width(
802 font.object(),
803 &codes,
804 self.edit,
805 &self.limits,
806 &mut Diagnostics::default(),
807 ) * size
808 / 1000.0
809 }
810
811 /// Draw `image` stretched onto `rect`.
812 ///
813 /// `image` is one this session embedded through [`EditDoc::embed_jpeg`]
814 /// or [`EditDoc::embed_image`]. Nothing preserves the aspect ratio: a
815 /// caller who wants it kept sizes `rect` from
816 /// [`EmbeddedImage::width`](crate::EmbeddedImage::width) and
817 /// [`EmbeddedImage::height`](crate::EmbeddedImage::height).
818 ///
819 /// ```
820 /// use pdfrum::Rect;
821 ///
822 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
823 /// let mut edit = doc.edit();
824 /// let logo = edit.embed_jpeg(include_bytes!("../tests/fixtures/mona_lisa.jpg"))?;
825 /// edit.draw_page(0, |c| {
826 /// c.image(&logo, Rect::new(20.0, 20.0, 80.0, 80.0));
827 /// })?;
828 /// # Ok::<(), pdfrum::Error>(())
829 /// ```
830 pub fn image(&mut self, image: &EmbeddedImage, rect: Rect) {
831 if rect.width() == 0.0 || rect.height() == 0.0 {
832 return;
833 }
834 let name = self.realize(pdf_names::XOBJECT, Object::Ref(image.object()));
835 self.out.push_str("q\n");
836 write_matrix(
837 &mut self.out,
838 Affine::new([rect.width(), 0.0, 0.0, rect.height(), rect.x0, rect.y0]),
839 );
840 self.out.push_str(" cm /");
841 self.push_name(&name);
842 self.out.push_str(" Do\nQ\n");
843 }
844
845 /// Set the constant alpha for everything drawn after it, as an
846 /// `/ExtGState` naming `/ca` and `/CA`.
847 ///
848 /// Scoped by [`Canvas::saved`]. The alpha a [`Color`] already carries is
849 /// applied on top of this, so a translucent colour under a 0.5 opacity is
850 /// twice translucent.
851 ///
852 /// ```
853 /// use pdfrum::{Color, Rect};
854 ///
855 /// let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
856 /// let mut edit = doc.edit();
857 /// edit.draw_page(0, |c| {
858 /// c.saved(|c| {
859 /// c.opacity(0.2);
860 /// c.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgb8(255, 0, 0));
861 /// });
862 /// })?;
863 /// # Ok::<(), pdfrum::Error>(())
864 /// ```
865 pub fn opacity(&mut self, alpha: f64) {
866 let alpha = alpha.clamp(0.0, 1.0);
867 let state = Dict::from_pairs([
868 (Name::from("ca"), Object::Real(as_f32(alpha))),
869 (Name::from("CA"), Object::Real(as_f32(alpha))),
870 ]);
871 let name = self.realize(pdf_names::EXT_G_STATE, Object::Dict(state));
872 self.out.push('/');
873 self.push_name(&name);
874 self.out.push_str(" gs\n");
875 }
876
877 /// Paint `shape` with a shading dictionary, clipped to the shape.
878 ///
879 /// `sh` fills the *whole current clip*, so the shape becomes a clip and
880 /// the shading is painted through it — which is how PDF spells a
881 /// gradient-filled path. `transform` is the gradient's own coordinate
882 /// mapping, applied inside the clip so it moves the gradient rather than
883 /// the shape.
884 ///
885 /// Crate-internal because a caller-facing shading API is a design of its
886 /// own — colour spaces, function types, the extend flags — and the one
887 /// caller here is [`Canvas::draw_svg`](crate::Canvas::draw_svg), which
888 /// builds the dictionary from a `usvg` gradient.
889 #[cfg(feature = "svg-import")]
890 pub(crate) fn shade(
891 &mut self,
892 shape: &BezPath,
893 rule: Fill,
894 shading: &Dict,
895 transform: Affine,
896 opacity: f64,
897 ) {
898 let name = self.realize(pdf_names::SHADING, Object::Dict(shading.clone()));
899 self.out.push_str("q\n");
900 self.write_path(shape);
901 self.out.push_str(match rule {
902 Fill::NonZero => " W n\n",
903 Fill::EvenOdd => " W* n\n",
904 });
905 if opacity < 1.0 {
906 self.opacity(opacity);
907 }
908 write_matrix(&mut self.out, transform);
909 self.out.push_str(" cm /");
910 self.push_name(&name);
911 self.out.push_str(" sh\nQ\n");
912 }
913
914 /// Embed a PNG or JPEG an SVG `<image>` carried, as a new image
915 /// `/XObject`.
916 ///
917 /// `None` when the bytes are neither, or decode to nothing this session
918 /// can embed; the caller reports that as
919 /// [`Unsupported::ImageFormat`](crate::Unsupported::ImageFormat) rather
920 /// than failing the whole drawing, because one bad `<image>` should not
921 /// cost the rest of the document.
922 #[cfg(feature = "svg-import")]
923 pub(crate) fn embed_svg_image(&mut self, bytes: &[u8]) -> Option<EmbeddedImage> {
924 // JPEG passes through whole: `/DCTDecode` is the PDF filter for
925 // exactly these bytes, so nothing is decoded and nothing is lost.
926 if bytes.starts_with(&[0xFF, 0xD8]) {
927 return self.edit.embed_jpeg(bytes).ok();
928 }
929 let decoded = crate::svg_ingest::decode_png(bytes)?;
930 self.edit
931 .embed_image(
932 &decoded.pixels,
933 decoded.width,
934 decoded.height,
935 decoded.format,
936 )
937 .ok()
938 }
939
940 /// The session this canvas draws into.
941 ///
942 /// Ingestion reads the SVG font set off it before parsing; there is no
943 /// mutable access, because a drawing method that reached into the session
944 /// past the resource machinery could add an object nothing names.
945 #[cfg(feature = "svg-text")]
946 pub(crate) fn fonts(&self) -> &crate::svg_text::SvgFonts {
947 &self.fonts
948 }
949
950 /// Record the first failure; later ones are dropped, because the first is
951 /// the one that explains the rest.
952 fn fail(&mut self, error: Error) {
953 if self.failed.is_none() {
954 self.failed = Some(error);
955 }
956 }
957
958 /// Write the colour, width and pen operators `paint` asks for.
959 fn set_paint(&mut self, paint: Paint) {
960 let (fill, stroke) = match paint {
961 Paint::Fill(color) => (Some(color), None),
962 Paint::Stroke(stroke) => (None, Some(stroke)),
963 Paint::FillStroke(color, stroke) => (Some(color), Some(stroke)),
964 };
965
966 // Alpha rides on an `/ExtGState`, since `rg`/`RG` carry none.
967 let alpha = fill
968 .map(alpha_of)
969 .into_iter()
970 .chain(stroke.as_ref().map(|stroke| alpha_of(stroke.color)))
971 .fold(1.0_f64, f64::min);
972 if alpha < 1.0 {
973 self.opacity(alpha);
974 }
975 if let Some(color) = fill {
976 self.write_rgb(color);
977 self.out.push_str(" rg\n");
978 }
979 if let Some(stroke) = stroke {
980 self.write_rgb(stroke.color);
981 self.out.push_str(" RG\n");
982 write_f64(&mut self.out, stroke.width.max(0.0));
983 self.out.push_str(" w\n");
984 self.write_pen(&stroke);
985 }
986 }
987
988 /// Write the cap, join, miter-limit and dash operators, each only when it
989 /// differs from the graphics state's own initial value (ISO 32000-1
990 /// table 52): the drawing runs inside a fresh `q`, so an unwritten one is
991 /// already what the caller asked for and the stream stays short.
992 fn write_pen(&mut self, stroke: &Stroke) {
993 if stroke.cap != LineCap::Butt {
994 let _ = writeln!(self.out, "{} J", stroke.cap.operand());
995 }
996 if stroke.join != LineJoin::Miter {
997 let _ = writeln!(self.out, "{} j", stroke.join.operand());
998 }
999 if stroke.miter_limit != MiterLimit::default() {
1000 write_f64(&mut self.out, stroke.miter_limit.get());
1001 self.out.push_str(" M\n");
1002 }
1003 if let Some(dash) = &stroke.dash {
1004 self.out.push('[');
1005 for (i, length) in dash.lengths().iter().enumerate() {
1006 if i > 0 {
1007 self.out.push(' ');
1008 }
1009 write_f64(&mut self.out, *length);
1010 }
1011 self.out.push_str("] ");
1012 write_f64(&mut self.out, dash.phase());
1013 self.out.push_str(" d\n");
1014 }
1015 }
1016
1017 /// Append a colour's three clamped components, space separated.
1018 fn write_rgb(&mut self, color: Color) {
1019 let [r, g, b, _] = color.components;
1020 for (i, component) in [r, g, b].into_iter().enumerate() {
1021 if i > 0 {
1022 self.out.push(' ');
1023 }
1024 write_float(&mut self.out, component.clamp(0.0, 1.0));
1025 }
1026 }
1027
1028 /// Append a path's construction operators, with no trailing separator.
1029 ///
1030 /// A path that is exactly an axis-aligned rectangle is written as one
1031 /// `re`, which is both shorter and what `pdfrum-edit`'s own emitter
1032 /// writes, so the two producers spell the commonest shape the same way.
1033 ///
1034 /// Otherwise a running cursor tracks the current point, because a
1035 /// quadratic segment needs the point it starts from: PDF has no quadratic
1036 /// operator, so each one is raised to the cubic with the identical curve
1037 /// rather than flattened into lines.
1038 fn write_path(&mut self, path: &BezPath) {
1039 if let Some(rect) = axis_aligned_rect(path) {
1040 crate::write_rect(&mut self.out, rect);
1041 self.out.push_str(" re");
1042 return;
1043 }
1044 let mut at = Point::ZERO;
1045 let mut start = Point::ZERO;
1046 for (index, element) in path.elements().iter().enumerate() {
1047 if index > 0 {
1048 self.out.push(' ');
1049 }
1050 match *element {
1051 PathEl::MoveTo(p) => {
1052 write_point(&mut self.out, p);
1053 self.out.push_str(" m");
1054 at = p;
1055 start = p;
1056 }
1057 PathEl::LineTo(p) => {
1058 write_point(&mut self.out, p);
1059 self.out.push_str(" l");
1060 at = p;
1061 }
1062 PathEl::QuadTo(c, p) => {
1063 let (c1, c2) = quad_to_cubic(at, c, p);
1064 self.write_cubic(c1, c2, p);
1065 at = p;
1066 }
1067 PathEl::CurveTo(c1, c2, p) => {
1068 self.write_cubic(c1, c2, p);
1069 at = p;
1070 }
1071 PathEl::ClosePath => {
1072 self.out.push('h');
1073 at = start;
1074 }
1075 }
1076 }
1077 }
1078
1079 /// Append one `c` operator: three points, space separated.
1080 fn write_cubic(&mut self, c1: Point, c2: Point, end: Point) {
1081 write_point(&mut self.out, c1);
1082 self.out.push(' ');
1083 write_point(&mut self.out, c2);
1084 self.out.push(' ');
1085 write_point(&mut self.out, end);
1086 self.out.push_str(" c");
1087 }
1088
1089 /// Append a name's bytes, escaping what ISO 32000-1 §7.3.5 requires.
1090 ///
1091 /// Every name this canvas writes is one it minted, so nothing needs
1092 /// escaping in practice; the escape is here so that a name reaching it
1093 /// some other way still produces a stream our own lexer reads back.
1094 fn push_name(&mut self, name: &Name) {
1095 for byte in name.as_bytes() {
1096 if byte.is_ascii_alphanumeric() {
1097 self.out.push(char::from(*byte));
1098 } else {
1099 let _ = write!(self.out, "#{byte:02X}");
1100 }
1101 }
1102 }
1103
1104 /// The name `value` is known by in `category`, allocating a fresh one
1105 /// that collides with neither the page's own resources nor anything this
1106 /// drawing already added.
1107 fn realize(&mut self, category: &'static Name, value: Object) -> Name {
1108 // The same font or image drawn twice takes one name, not two.
1109 if let Some((_, name, _)) = self
1110 .added
1111 .iter()
1112 .find(|(held_category, _, held)| *held_category == category && *held == value)
1113 {
1114 return name.clone();
1115 }
1116 let name = self.free_name(category);
1117 self.taken.push((category, name.clone()));
1118 self.added.push((category, name.clone(), value));
1119 name
1120 }
1121
1122 /// The first `PdfrumC<n>` name free in `category`.
1123 ///
1124 /// The prefix is this crate's and nothing else in the workspace mints it:
1125 /// `pdfrum-edit`'s regeneration uses `FX*`, and a producer's own names are
1126 /// whatever the page already holds — which is exactly what `taken`
1127 /// carries, so a collision is checked rather than assumed away.
1128 fn free_name(&self, category: &Name) -> Name {
1129 for id in 1u32.. {
1130 let candidate = Name::from(format!("PdfrumC{id}").as_str());
1131 if !self
1132 .taken
1133 .iter()
1134 .any(|(cat, name)| *cat == category && *name == candidate)
1135 {
1136 return candidate;
1137 }
1138 }
1139 Name::from("PdfrumC1")
1140 }
1141}
1142
1143/// Drawing compiled once into a Form `XObject`, placeable on any number of
1144/// pages.
1145///
1146/// A `/Subtype /Form` stream with its own `/BBox` and `/Resources`, held as a
1147/// single object in the document. Placing it writes one `Do` — so the same
1148/// logo on twenty pages is one copy of the content and twenty references,
1149/// rather than twenty copies of the content.
1150///
1151/// Produced by [`EditDoc::compile_svg`] and placed by
1152/// [`Canvas::place_svg`](crate::Canvas::place_svg).
1153/// It carries no borrow of the session that made it, so a caller compiles
1154/// once and then places inside as many `draw_page` closures as they like.
1155#[cfg(feature = "svg-import")]
1156#[derive(Debug, Clone, PartialEq)]
1157pub struct SvgForm {
1158 /// The form's object in the session that compiled it.
1159 object: ObjRef,
1160 /// The form's own coordinate box, in its own space. A placement maps this
1161 /// onto the destination rectangle.
1162 bbox: Rect,
1163}
1164
1165#[cfg(feature = "svg-import")]
1166impl SvgForm {
1167 /// The form's `/BBox`, in the form's own coordinate space.
1168 ///
1169 /// Its aspect ratio is what [`SvgFit`](crate::SvgFit) preserves when the
1170 /// destination rectangle has a different one.
1171 #[must_use]
1172 pub fn bbox(&self) -> Rect {
1173 self.bbox
1174 }
1175}
1176
1177#[cfg(feature = "svg-import")]
1178impl Canvas<'_, '_> {
1179 /// Place `form` so its [`SvgForm::bbox`] covers `into`.
1180 ///
1181 /// One `Do` operator against the form's single object, so placing the
1182 /// same form on every page of a document costs one copy of the content
1183 /// and one reference per page. The placement is scoped in its own `q`/`Q`
1184 /// and clipped to `into`, so nothing the form draws escapes the rectangle
1185 /// and the canvas's own state survives it.
1186 ///
1187 /// The form's box is stretched onto `into`, with no fit of its own — the
1188 /// caller-facing spelling is
1189 /// [`Canvas::place_svg`](crate::Canvas::place_svg), which chooses the
1190 /// rectangle through an [`SvgFit`](crate::SvgFit) and then calls this.
1191 /// Crate-internal because a second public placement that differs only in
1192 /// taking a pre-fitted rectangle would be a way of saying the same thing
1193 /// twice.
1194 pub(crate) fn place_form(&mut self, form: &SvgForm, into: Rect) {
1195 if into.width() == 0.0 || into.height() == 0.0 || form.bbox.is_zero_area() {
1196 return;
1197 }
1198 let name = self.realize(pdf_names::XOBJECT, Object::Ref(form.object));
1199 // The form's `/BBox` is mapped onto `into`: scale by the ratio of the
1200 // two, then carry the form's own origin to the destination's. `/BBox`
1201 // is *not* assumed to start at the origin, because a compiled SVG's
1202 // need not.
1203 let scale_x = into.width() / form.bbox.width();
1204 let scale_y = into.height() / form.bbox.height();
1205 let placement = Affine::new([
1206 scale_x,
1207 0.0,
1208 0.0,
1209 scale_y,
1210 into.x0 - form.bbox.x0 * scale_x,
1211 into.y0 - form.bbox.y0 * scale_y,
1212 ]);
1213 self.out.push_str("q\n");
1214 self.write_path(&into.into_path(0.1));
1215 self.out.push_str(" W n\n");
1216 write_matrix(&mut self.out, placement);
1217 self.out.push_str(" cm /");
1218 self.push_name(&name);
1219 self.out.push_str(" Do\nQ\n");
1220 }
1221}
1222
1223#[cfg(feature = "svg-import")]
1224impl EditDoc<'_> {
1225 /// Compile `body`'s drawing into a Form `XObject` over `bbox`.
1226 ///
1227 /// The canvas `body` receives writes into the form's own stream and its
1228 /// own `/Resources`, so nothing it names can collide with a page's — a
1229 /// form is a fresh resource scope, which is why the placement is one
1230 /// object rather than a merge per page.
1231 ///
1232 /// The shared half of [`EditDoc::compile_svg`]; it is crate-internal
1233 /// because the caller-facing surface for "drawing a caller wrote once" is
1234 /// [`EditDoc::draw_page`] with the caller's own closure, and a second
1235 /// spelling of it would be an option with no reader.
1236 ///
1237 /// # Errors
1238 ///
1239 /// Whatever `body` refused to draw, as [`EditDoc::draw_page`] reports it.
1240 pub(crate) fn compile_form(
1241 &mut self,
1242 bbox: Rect,
1243 limits: &Limits,
1244 #[cfg(feature = "svg-text")] fonts: &crate::svg_text::SvgFonts,
1245 body: impl FnOnce(&mut Canvas<'_, '_>),
1246 ) -> Result<SvgForm> {
1247 let mut canvas = Canvas {
1248 out: String::new(),
1249 limits: limits.clone(),
1250 #[cfg(feature = "svg-text")]
1251 fonts: fonts.clone(),
1252 edit: self,
1253 added: Vec::new(),
1254 // A form's resource scope is its own and starts empty: there is
1255 // no page dictionary whose names it has to avoid.
1256 taken: Vec::new(),
1257 size: bbox.size(),
1258 surface: Surface::Form,
1259 failed: None,
1260 };
1261 body(&mut canvas);
1262 if let Some(error) = canvas.failed {
1263 return Err(error);
1264 }
1265 let Canvas { out, added, .. } = canvas;
1266
1267 let resources = merge_resources(&Dict::new(), &added);
1268 let bytes = out.into_bytes();
1269 let dict = Dict::from_pairs([
1270 (
1271 pdf_names::TYPE.clone(),
1272 Object::Name(pdf_names::XOBJECT.clone()),
1273 ),
1274 (pdf_names::SUBTYPE.clone(), Object::Name(Name::from("Form"))),
1275 (Name::from("FormType"), Object::Int(1)),
1276 (Name::from("BBox"), Object::Array(rect_array(bbox))),
1277 (pdf_names::RESOURCES.clone(), Object::Dict(resources)),
1278 (
1279 pdf_names::LENGTH.clone(),
1280 Object::Int(i64::try_from(bytes.len()).unwrap_or(0)),
1281 ),
1282 ]);
1283 let object = self.add(Object::Stream(Box::new(Stream::new(dict, bytes.into()))));
1284 Ok(SvgForm { object, bbox })
1285 }
1286}
1287
1288/// A rectangle as the four numbers a `/BBox` holds.
1289#[cfg(feature = "svg-import")]
1290fn rect_array(rect: Rect) -> Array {
1291 Array::of([rect.x0, rect.y0, rect.x1, rect.y1].map(|value| Object::Real(as_f32(value))))
1292}
1293
1294/// The rectangle `path` draws, when it draws exactly one.
1295///
1296/// Four corners, axis-aligned, closed — which is what `Rect::into_path`
1297/// produces and what a caller's own rectangle almost always is. Anything else
1298/// returns `None` and is written segment by segment.
1299///
1300/// The coordinate comparisons are **exact**, deliberately. This is a
1301/// recognizer for a shape the caller built, not a geometric tolerance: a path
1302/// whose corners are a rounding error apart is not the rectangle the caller
1303/// asked for, and writing it as `re` would move an edge. `pdfrum-edit`'s own
1304/// emitter recognizes its rectangles the same way.
1305#[expect(
1306 clippy::float_cmp,
1307 reason = "exact recognition of a caller-built rectangle; a tolerance here would move an edge"
1308)]
1309fn axis_aligned_rect(path: &BezPath) -> Option<Rect> {
1310 let corners: [Point; 4] = match path.elements() {
1311 // A closed four-sided path, with or without the redundant final
1312 // `LineTo` back to the start that some shapes emit before `h`.
1313 [
1314 PathEl::MoveTo(first),
1315 PathEl::LineTo(second),
1316 PathEl::LineTo(third),
1317 PathEl::LineTo(fourth),
1318 PathEl::ClosePath,
1319 ] => [*first, *second, *third, *fourth],
1320 [
1321 PathEl::MoveTo(first),
1322 PathEl::LineTo(second),
1323 PathEl::LineTo(third),
1324 PathEl::LineTo(fourth),
1325 PathEl::LineTo(back),
1326 PathEl::ClosePath,
1327 ] if back == first => [*first, *second, *third, *fourth],
1328 _ => return None,
1329 };
1330 // Axis-aligned means each side shares one coordinate with the next.
1331 for index in 0..4 {
1332 let from = *corners.get(index)?;
1333 let to = *corners.get((index + 1) % 4)?;
1334 if from.x != to.x && from.y != to.y {
1335 return None;
1336 }
1337 }
1338 // And it must be a rectangle rather than a degenerate zig-zag: opposite
1339 // corners differ in both coordinates.
1340 let (origin, opposite) = (*corners.first()?, *corners.get(2)?);
1341 if origin.x == opposite.x || origin.y == opposite.y {
1342 return None;
1343 }
1344 Some(Rect::new(origin.x, origin.y, opposite.x, opposite.y))
1345}
1346
1347/// A colour's alpha, clamped.
1348fn alpha_of(color: Color) -> f64 {
1349 f64::from(color.components[3]).clamp(0.0, 1.0)
1350}
1351
1352/// An `f64` narrowed to the `f32` a PDF number is.
1353#[expect(
1354 clippy::cast_possible_truncation,
1355 reason = "PDF numbers are f32; the geometry vocabulary is f64"
1356)]
1357fn as_f32(value: f64) -> f32 {
1358 value as f32
1359}
1360
1361/// Append an `f64` through the crate-wide number spelling.
1362fn write_f64(out: &mut String, value: f64) {
1363 write_float(out, as_f32(value));
1364}
1365
1366/// The cubic control points equal to the quadratic `previous`-`control`-`end`.
1367fn quad_to_cubic(previous: Point, control: Point, end: Point) -> (Point, Point) {
1368 let third = 2.0 / 3.0;
1369 (
1370 previous + (control - previous) * third,
1371 end + (control - end) * third,
1372 )
1373}
1374
1375/// The paint operator for a paint and a fill rule (ISO 32000-1 table 60).
1376fn paint_operator(paint: &Paint, rule: Fill) -> &'static str {
1377 match (paint, rule) {
1378 (Paint::Fill(_), Fill::NonZero) => " f",
1379 (Paint::Fill(_), Fill::EvenOdd) => " f*",
1380 (Paint::Stroke(_), _) => " S",
1381 (Paint::FillStroke(_, _), Fill::NonZero) => " B",
1382 (Paint::FillStroke(_, _), Fill::EvenOdd) => " B*",
1383 }
1384}
1385
1386/// Append `codes` as a hexadecimal string, `<...>`.
1387///
1388/// Hex rather than a literal `(...)` so that no byte ever needs escaping: a
1389/// composite font's two-byte codes are full of parentheses and backslashes,
1390/// and getting that escaping subtly wrong is how a writer produces a stream
1391/// nothing can read.
1392fn write_hex_string(out: &mut String, codes: &[u8]) {
1393 out.push('<');
1394 for byte in codes {
1395 let _ = write!(out, "{byte:02X}");
1396 }
1397 out.push('>');
1398}
1399
1400impl EditDoc<'_> {
1401 /// Draw on page `index`, appending what `body` draws as one new content
1402 /// stream.
1403 ///
1404 /// The canvas's coordinate space is the page as displayed — see
1405 /// [`Canvas`]. The stream is wrapped in `q`/`Q` and appended to the
1406 /// page's `/Contents` array, so the page's own graphics state cannot leak
1407 /// into the drawing and the drawing's cannot leak into the page. The
1408 /// page's existing streams are **not** rewritten, which is why drawing on
1409 /// a page costs none of the regeneration losses
1410 /// [`PageEdit`](pdfrum_page::PageEdit) documents.
1411 ///
1412 /// # The resource-merging rule
1413 ///
1414 /// Fonts, images and graphics states the drawing used are merged into the
1415 /// page's `/Resources` under names of this crate's own `PdfrumC<n>`
1416 /// series, each checked against the names the page already holds, so a
1417 /// merged name can collide with neither the producer's nor
1418 /// `pdfrum-edit`'s `FX*`. A `/Resources` the page shares with another
1419 /// page is copied before it is written to, so drawing on one page cannot
1420 /// change another.
1421 ///
1422 /// ```
1423 /// use pdfrum::{Color, Document, Point, SaveOptions, StandardFont};
1424 ///
1425 /// let doc = Document::open("tests/fixtures/hello_world.pdf")?;
1426 /// let mut edit = doc.edit();
1427 /// let font = edit.standard_font(StandardFont::Helvetica)?;
1428 /// edit.draw_page(0, |c| {
1429 /// c.text("drawn", &font, 12.0, Point::new(40.0, 40.0), Color::BLACK);
1430 /// })?;
1431 ///
1432 /// let mut bytes = Vec::new();
1433 /// edit.write_to(&mut bytes, &SaveOptions::default())?;
1434 /// let saved = Document::from_bytes(bytes)?;
1435 /// assert!(saved.page(0)?.text().to_string().contains("drawn"));
1436 /// # Ok::<(), pdfrum::Error>(())
1437 /// ```
1438 ///
1439 /// Glyph fonts ([`EditDoc::embed_glyph_font`]) the drawing used are
1440 /// written when it returns: subset to every glyph this session has drawn
1441 /// with them so far, with their `/W` and `/ToUnicode`.
1442 ///
1443 /// # Errors
1444 ///
1445 /// Whatever `body` refused to draw — a character the font has no glyph
1446 /// for, most often — and [`Error::InlinePage`] for a page with no
1447 /// object of its own. Nothing is written when the drawing failed.
1448 /// [`Error::Subset`] when a glyph font's face would not subset.
1449 pub fn draw_page(
1450 &mut self,
1451 index: impl Into<PageIndex>,
1452 limits: &Limits,
1453 body: impl FnOnce(&mut Canvas<'_, '_>),
1454 ) -> Result<()> {
1455 self.draw_page_with_fonts(
1456 index,
1457 limits,
1458 #[cfg(feature = "svg-text")]
1459 &crate::svg_text::SvgFonts::new(),
1460 body,
1461 )
1462 }
1463
1464 /// [`EditDoc::draw_page`] with the faces an ingested SVG's `<text>` is set
1465 /// in; without them a `<text>` draws nothing and is reported instead.
1466 ///
1467 /// # Errors
1468 ///
1469 /// As [`EditDoc::draw_page`].
1470 pub fn draw_page_with_fonts(
1471 &mut self,
1472 index: impl Into<PageIndex>,
1473 limits: &Limits,
1474 #[cfg(feature = "svg-text")] fonts: &crate::svg_text::SvgFonts,
1475 body: impl FnOnce(&mut Canvas<'_, '_>),
1476 ) -> Result<()> {
1477 self.draw_one(
1478 index,
1479 limits,
1480 #[cfg(feature = "svg-text")]
1481 fonts,
1482 body,
1483 )?;
1484 crate::font::glyph::finish(self)
1485 }
1486
1487 /// One page's drawing, without writing the glyph fonts it used: the
1488 /// callers write those once, after the last page they draw.
1489 fn draw_one(
1490 &mut self,
1491 index: impl Into<PageIndex>,
1492 limits: &Limits,
1493 #[cfg(feature = "svg-text")] fonts: &crate::svg_text::SvgFonts,
1494 body: impl FnOnce(&mut Canvas<'_, '_>),
1495 ) -> Result<()> {
1496 let index = index.into();
1497 let Some((reference, dict, resources)) = self
1498 .page_state(index)
1499 .map_err(|_| Error::PageIndexOutOfRange(index))?
1500 else {
1501 return Err(Error::InlinePage(index));
1502 };
1503 let mut diags = Diagnostics::default();
1504 let (to_page, size) = self.canvas_space(reference, &dict, &mut diags);
1505
1506 let taken = existing_names(&resources, self);
1507 let mut canvas = Canvas {
1508 out: String::new(),
1509 limits: limits.clone(),
1510 #[cfg(feature = "svg-text")]
1511 fonts: fonts.clone(),
1512 edit: self,
1513 added: Vec::new(),
1514 taken,
1515 size,
1516 surface: Surface::Page(index),
1517 failed: None,
1518 };
1519 body(&mut canvas);
1520 if let Some(error) = canvas.failed {
1521 return Err(error);
1522 }
1523 let Canvas { out, added, .. } = canvas;
1524 if out.is_empty() {
1525 return Ok(());
1526 }
1527
1528 // `q` … `Q` around the whole drawing, with the canvas-to-page
1529 // transform inside it, so neither state escapes into the other.
1530 let mut bytes = String::with_capacity(out.len() + 64);
1531 bytes.push_str("q\n");
1532 write_matrix(&mut bytes, to_page);
1533 bytes.push_str(" cm\n");
1534 bytes.push_str(&out);
1535 bytes.push_str("Q\n");
1536
1537 self.append_stream(reference, &dict, &resources, bytes.as_bytes(), &added);
1538 Ok(())
1539 }
1540
1541 /// Draw on every page, one canvas each.
1542 ///
1543 /// The closure runs once per page in order and is handed that page's own
1544 /// canvas, so [`Canvas::size`] and [`Canvas::page`] are the page's. A page
1545 /// written inline in its parent's `/Kids` is skipped rather than refused:
1546 /// a whole-document watermark should not fail because one page of a
1547 /// thousand cannot carry it.
1548 ///
1549 /// ```
1550 /// use pdfrum::{Color, Document, SaveOptions, Stroke, Point};
1551 ///
1552 /// let doc = Document::open("tests/fixtures/hello_world_2_pages.pdf")?;
1553 /// let mut edit = doc.edit();
1554 /// edit.draw_pages(|c| {
1555 /// let y = c.size().height - 40.0;
1556 /// c.line(Point::new(40.0, y), Point::new(c.size().width - 40.0, y),
1557 /// Stroke::new(Color::BLACK, 0.5));
1558 /// })?;
1559 /// let mut bytes = Vec::new();
1560 /// edit.write_to(&mut bytes, &SaveOptions::default())?;
1561 /// assert!(bytes.starts_with(b"%PDF-"));
1562 /// # Ok::<(), pdfrum::Error>(())
1563 /// ```
1564 ///
1565 /// # Errors
1566 ///
1567 /// As [`EditDoc::draw_page`], for the first page whose drawing failed.
1568 pub fn draw_pages(
1569 &mut self,
1570 limits: &Limits,
1571 body: impl FnMut(&mut Canvas<'_, '_>),
1572 ) -> Result<()> {
1573 self.draw_pages_with_fonts(
1574 limits,
1575 #[cfg(feature = "svg-text")]
1576 &crate::svg_text::SvgFonts::new(),
1577 body,
1578 )
1579 }
1580
1581 /// [`EditDoc::draw_pages`] with the faces an ingested SVG's `<text>` is
1582 /// set in.
1583 ///
1584 /// # Errors
1585 ///
1586 /// As [`EditDoc::draw_page`], for the first page whose drawing failed.
1587 pub fn draw_pages_with_fonts(
1588 &mut self,
1589 limits: &Limits,
1590 #[cfg(feature = "svg-text")] fonts: &crate::svg_text::SvgFonts,
1591 mut body: impl FnMut(&mut Canvas<'_, '_>),
1592 ) -> Result<()> {
1593 for index in 0..self.base().page_count() {
1594 let index = PageIndex::from(index);
1595 if self
1596 .page_state(index)
1597 .map_err(|_| Error::PageIndexOutOfRange(index))?
1598 .is_none()
1599 {
1600 continue;
1601 }
1602 self.draw_one(
1603 index,
1604 limits,
1605 #[cfg(feature = "svg-text")]
1606 fonts,
1607 &mut body,
1608 )?;
1609 }
1610 // Once, after every page: a subset is of everything the pages drew.
1611 crate::font::glyph::finish(self)
1612 }
1613
1614 /// The canvas-to-page transform and the displayed size for page `index`.
1615 ///
1616 /// The transform is the inverse of the renderer's own display matrix over
1617 /// the crop box, which is the whole of the coordinate-space composition:
1618 /// the crop box's offset and the `/Rotate` quarter turn fall out of it
1619 /// together, and there is nothing else to get right.
1620 fn canvas_space(
1621 &self,
1622 reference: ObjRef,
1623 dict: &Dict,
1624 diags: &mut Diagnostics,
1625 ) -> (Affine, kurbo::Size) {
1626 // The dictionary is the session's, read through the overlay, and the
1627 // inheritance walk goes through the overlay too — so a `/Rotate` or a
1628 // `/CropBox` this same session set is what the canvas is built on,
1629 // rather than the base document's stale one.
1630 let page = pdfrum_parser::PageDict {
1631 dict: dict.clone(),
1632 reference: Some(reference),
1633 };
1634 let (_, crop) =
1635 pdfrum_page::derive_boxes(&page.dict, |key| page.inherited(key, self), self, diags);
1636 let rotate_key = Name::from("Rotate");
1637 let rotate = pdfrum_page::Rotation::from_degrees(
1638 page.dict
1639 .raw(&rotate_key)
1640 .cloned()
1641 .or_else(|| page.inherited(&rotate_key, self))
1642 .and_then(|value| value.resolve(self).ok()?.get().as_int())
1643 .unwrap_or(0),
1644 );
1645 let size = if rotate.quarters().is_multiple_of(2) {
1646 kurbo::Size::new(crop.width(), crop.height())
1647 } else {
1648 kurbo::Size::new(crop.height(), crop.width())
1649 };
1650 (rotate.display_matrix(crop).inverse(), size)
1651 }
1652
1653 /// Append `bytes` as one more content stream of the page `reference`
1654 /// names, merging `added` into its `/Resources`.
1655 fn append_stream(
1656 &mut self,
1657 reference: ObjRef,
1658 dict: &Dict,
1659 resources: &Dict,
1660 bytes: &[u8],
1661 added: &[(&'static Name, Name, Object)],
1662 ) {
1663 let stream = Stream::new(
1664 Dict::from_pairs([(
1665 pdf_names::LENGTH.clone(),
1666 Object::Int(i64::try_from(bytes.len()).unwrap_or(0)),
1667 )]),
1668 ByteSpan::from(bytes.to_vec()),
1669 );
1670 let fresh = (*self).add(Object::Stream(Box::new(stream)));
1671
1672 let shape = ContentsShape::read(dict, self);
1673 let (_, next) = shape.with_added(fresh);
1674 let shared = crate::shared_objects(self);
1675
1676 let mut dict = dict.clone();
1677 // The `/Contents` array: reused when the page owns it outright,
1678 // otherwise a fresh one, exactly as `apply_rewrite` decides it.
1679 let elements = next.elements();
1680 let array = Object::Array(Array::of(elements.iter().map(|e| Object::Ref(*e))));
1681 let reusable = matches!(
1682 dict.raw(pdf_names::CONTENTS),
1683 Some(Object::Ref(r)) if !shared.contains(&r.num) && !elements.contains(r)
1684 );
1685 let contents = match dict.raw(pdf_names::CONTENTS) {
1686 Some(Object::Ref(existing)) if reusable => {
1687 let existing = *existing;
1688 (*self).replace(existing, array);
1689 Object::Ref(existing)
1690 }
1691 _ => Object::Ref((*self).add(array)),
1692 };
1693 dict = with_key(&dict, pdf_names::CONTENTS, contents);
1694
1695 let merged = merge_resources(resources, added);
1696 match dict.raw(pdf_names::RESOURCES) {
1697 // The page reaches its resources through an object it does not
1698 // share: write through it and leave the page's key alone.
1699 Some(Object::Ref(existing)) if !shared.contains(&existing.num) => {
1700 let existing = *existing;
1701 (*self).replace(existing, Object::Dict(merged));
1702 }
1703 // Shared, inline or absent: the page gets its own copy, so
1704 // drawing on one page cannot change another.
1705 _ => dict = with_key(&dict, pdf_names::RESOURCES, Object::Dict(merged)),
1706 }
1707
1708 (*self).replace(reference, Object::Dict(dict));
1709 }
1710}
1711
1712/// Every name the page's `/Resources` already uses, per category, so a fresh
1713/// one is chosen against them rather than merely hoped to differ.
1714fn existing_names(resources: &Dict, r: &impl Resolve) -> Vec<(&'static Name, Name)> {
1715 let mut taken = Vec::new();
1716 for category in [pdf_names::FONT, pdf_names::XOBJECT, pdf_names::EXT_G_STATE] {
1717 let Some(sub) = resources.dict(category, r) else {
1718 continue;
1719 };
1720 for (name, _) in sub.iter() {
1721 taken.push((category, name.clone()));
1722 }
1723 }
1724 taken
1725}
1726
1727/// `resources` with `added` merged in, each under the category it belongs to.
1728///
1729/// Every other key — colour spaces, patterns, `/ProcSet` — is carried through
1730/// untouched: the canvas emits no operator that would name one.
1731fn merge_resources(resources: &Dict, added: &[(&'static Name, Name, Object)]) -> Dict {
1732 let mut out = Dict::new();
1733 for (key, value) in resources.iter() {
1734 let extra: Vec<_> = added
1735 .iter()
1736 .filter(|(category, _, _)| *category == key)
1737 .collect();
1738 if extra.is_empty() {
1739 out.push(key.clone(), value.clone());
1740 continue;
1741 }
1742 // A category the page already has: keep every entry and add ours.
1743 // The sub-dictionary may be indirect; it is inlined here rather than
1744 // written through, because the object could be shared with a page
1745 // this drawing is not touching.
1746 let mut sub = match value {
1747 Object::Dict(dict) => dict.clone(),
1748 _ => Dict::new(),
1749 };
1750 for (_, name, held) in extra {
1751 sub.push(name.clone(), held.clone());
1752 }
1753 out.push(key.clone(), Object::Dict(sub));
1754 }
1755 // The categories the drawing used that the page had none of. Taken from
1756 // `added` rather than from a fixed list of the categories a canvas
1757 // happens to mint today: a drawing that reaches for a new one — `sh`
1758 // brought `/Shading` — must not silently lose its resources, which is a
1759 // resource named in the stream and absent from `/Resources`, and so a
1760 // draw that does nothing at all.
1761 for (category, _, _) in added {
1762 if out.contains_key(category) {
1763 continue;
1764 }
1765 let mut sub = Dict::new();
1766 for (_, name, held) in added.iter().filter(|(cat, _, _)| cat == category) {
1767 sub.push(name.clone(), held.clone());
1768 }
1769 if !sub.is_empty() {
1770 out.push((*category).clone(), Object::Dict(sub));
1771 }
1772 }
1773 out
1774}
1775
1776/// A copy of `dict` with `key` set, keeping every other entry in its place.
1777fn with_key(dict: &Dict, key: &Name, value: Object) -> Dict {
1778 let mut out = Dict::new();
1779 let mut written = false;
1780 for (existing, held) in dict.iter() {
1781 if existing == key {
1782 if !written {
1783 out.push(existing.clone(), value.clone());
1784 written = true;
1785 }
1786 } else {
1787 out.push(existing.clone(), held.clone());
1788 }
1789 }
1790 if !written {
1791 out.push(key.clone(), value);
1792 }
1793 out
1794}