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