Skip to main content

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}