pub struct Canvas<'a, 'b> { /* private fields */ }Expand description
One page’s drawing surface.
Handed to the closure of EditDoc::draw_page and
EditDoc::draw_pages; it cannot be constructed otherwise, because a
canvas is only meaningful against the page whose space it maps and the
session whose resources it merges into.
§The coordinate space
Canvas coordinates are the page as displayed, in points:
- the origin is the lower-left corner of the crop box after
/Rotate; - x runs right and y runs up, as PDF page space does and unlike a raster;
- the extent is
Canvas::size, whose sides are the crop box’s swapped on a quarter or three-quarter turn.
So a caller places things where they see them: on a page with
/Rotate 90, Point::new(0.0, 0.0) is the bottom-left corner on screen,
and text drawn along +x reads upright there. The composition is exactly
the inverse of pdfrum_page::Rotation::display_matrix over the crop
box, which is the same matrix the renderer uses, so what a caller places
and what a viewer shows cannot drift apart.
Every method takes canvas coordinates. Canvas::transform composes a
further transform inside that space, so a rotation about a point is
written in the coordinates the caller is already using.
Implementations§
Source§impl Canvas<'_, '_>
impl Canvas<'_, '_>
Sourcepub fn size(&self) -> Size
pub fn size(&self) -> Size
The page’s displayed size in points — the crop box’s, with its sides swapped on a quarter turn.
The canvas’s own extent: Rect::from_origin_size(Point::ZERO, size)
is the whole visible page.
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
assert!(c.size().width > 0.0);
})?;Sourcepub fn bounds(&self) -> Rect
pub fn bounds(&self) -> Rect
The whole visible page, as a rectangle in canvas coordinates.
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
assert_eq!(c.bounds().origin(), pdfrum::Point::ZERO);
})?;Sourcepub fn page(&self) -> Option<PageIndex>
pub fn page(&self) -> Option<PageIndex>
The page this canvas draws on, or None when it is compiling a Form
XObject that no page owns yet.
A canvas handed to EditDoc::draw_page or EditDoc::draw_pages
always answers Some. The None case is a Form XObject compiled
by EditDoc::compile_svg (feature svg-import), whose content
belongs to no page until Canvas::place_svg puts it on one.
let doc = pdfrum::Document::open("tests/fixtures/hello_world_2_pages.pdf")?;
let mut edit = doc.edit();
edit.draw_pages(|c| assert!(c.page().is_some_and(|p| u32::from(p) < 2)))?;Sourcepub fn saved(&mut self, body: impl FnOnce(&mut Self))
pub fn saved(&mut self, body: impl FnOnce(&mut Self))
Draw inside a saved graphics state, restored when body returns.
This is the only spelling of q/Q: there is no bare save a
caller could leave unmatched, and no restore that could pop a state
the caller did not push. Nesting is the closure nesting, so an
unbalanced stream is not expressible.
use pdfrum::{Color, Paint, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.saved(|c| {
c.clip(Rect::new(0.0, 0.0, 100.0, 100.0), pdfrum::Fill::NonZero);
c.fill_rect(Rect::new(0.0, 0.0, 500.0, 500.0), Color::from_rgb8(200, 0, 0));
});
// The clip is gone here.
c.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::BLACK);
})?;Sourcepub fn transform(&mut self, transform: Affine)
pub fn transform(&mut self, transform: Affine)
Compose transform into the canvas space, for everything drawn after
it.
Scoped by Canvas::saved, like every other graphics-state change; a
transform outside one lasts for the rest of the drawing.
use pdfrum::{Affine, Color, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.saved(|c| {
c.transform(Affine::rotate_about(0.5, c.bounds().center()));
c.fill_rect(Rect::new(0.0, 0.0, 100.0, 20.0), Color::BLACK);
});
})?;Sourcepub fn clip(&mut self, shape: impl Shape, rule: Fill)
pub fn clip(&mut self, shape: impl Shape, rule: Fill)
Intersect the clip with shape, for everything drawn after it.
Scoped by Canvas::saved: a PDF clip can only ever be narrowed, so
a q/Q is the only way back.
use pdfrum::{Fill, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.saved(|c| c.clip(Rect::new(10.0, 10.0, 90.0, 90.0), Fill::NonZero));
})?;Sourcepub fn fill(&mut self, shape: impl Shape, color: Color)
pub fn fill(&mut self, shape: impl Shape, color: Color)
Fill shape with color, by the nonzero rule.
use pdfrum::{Color, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.fill(Rect::new(0.0, 0.0, 50.0, 50.0), Color::from_rgb8(0, 0, 255));
})?;Sourcepub fn fill_rect(&mut self, rect: Rect, color: Color)
pub fn fill_rect(&mut self, rect: Rect, color: Color)
Fill rect with color — Canvas::fill on the commonest shape.
use pdfrum::{Color, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.fill_rect(Rect::new(0.0, 0.0, 50.0, 50.0), Color::BLACK);
})?;Sourcepub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, color: Color)
pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, color: Color)
Fill a rectangle with radius-point rounded corners.
use pdfrum::{Color, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.fill_rounded_rect(Rect::new(0.0, 0.0, 80.0, 30.0), 6.0, Color::BLACK);
})?;Sourcepub fn stroke(&mut self, shape: impl Shape, stroke: Stroke)
pub fn stroke(&mut self, shape: impl Shape, stroke: Stroke)
Stroke shape.
use pdfrum::{Color, Rect, Stroke};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.stroke(Rect::new(0.0, 0.0, 50.0, 50.0), Stroke::new(Color::BLACK, 1.0));
})?;Sourcepub fn line(&mut self, from: Point, to: Point, stroke: Stroke)
pub fn line(&mut self, from: Point, to: Point, stroke: Stroke)
Stroke the straight segment from from to to — a header rule, a
divider.
use pdfrum::{Color, Point, Stroke};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
let y = c.size().height - 50.0;
c.line(Point::new(50.0, y), Point::new(c.size().width - 50.0, y),
Stroke::new(Color::BLACK, 0.75));
})?;Sourcepub fn draw(&mut self, shape: impl Shape, paint: Paint, rule: Fill)
pub fn draw(&mut self, shape: impl Shape, paint: Paint, rule: Fill)
Paint shape with paint, filling by rule.
The general case the other shape methods narrow: Canvas::fill is
Paint::Fill with Fill::NonZero, Canvas::stroke is
Paint::Stroke.
use pdfrum::{Color, Fill, Paint, Rect, Stroke};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.draw(
Rect::new(0.0, 0.0, 40.0, 40.0),
Paint::FillStroke(Color::from_rgb8(255, 255, 0), Stroke::new(Color::BLACK, 2.0)),
Fill::EvenOdd,
);
})?;Sourcepub fn text(
&mut self,
text: &str,
font: &EmbeddedFont,
size: f64,
at: Point,
color: Color,
)
pub fn text( &mut self, text: &str, font: &EmbeddedFont, size: f64, at: Point, color: Color, )
Draw text in font at size, with its baseline starting at at.
font is one this session loaded through EditDoc::embed_font or
EditDoc::standard_font, so its glyphs are subset and embedded by
the machinery that already does that for a saved font. A base-14 face
from standard_font needs no embedded program.
One string, one point, one line: see the module documentation for why there is no wrapping.
§Errors
A character font has no glyph for is an error, not a blank — the
canvas records it and EditDoc::draw_page returns it. Nothing of
this call is written when it fails, so a refused string leaves no
half-drawn run behind.
use pdfrum::{Color, Point, StandardFont};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
let font = edit.standard_font(StandardFont::Helvetica)?;
edit.draw_page(0, |c| {
c.text("Page 1", &font, 10.0, Point::new(72.0, 72.0), Color::BLACK);
})?;Sourcepub fn text_width(&self, text: &str, font: &EmbeddedFont, size: f64) -> f64
pub fn text_width(&self, text: &str, font: &EmbeddedFont, size: f64) -> f64
The advance of text in font at size, in canvas units.
The only measurement this API offers, and it is what centring a single string needs. It is not a layout engine and does not claim to be: no line breaking, no kerning beyond the font’s own advances, and no vertical metrics.
0.0 for a string font cannot encode.
use pdfrum::StandardFont;
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
let font = edit.standard_font(StandardFont::Helvetica)?;
edit.draw_page(0, |c| {
assert!(c.text_width("Hello", &font, 12.0) > 0.0);
})?;Sourcepub fn image(&mut self, image: &EmbeddedImage, rect: Rect)
pub fn image(&mut self, image: &EmbeddedImage, rect: Rect)
Draw image stretched onto rect.
image is one this session embedded through EditDoc::embed_jpeg
or EditDoc::embed_image. Nothing preserves the aspect ratio: a
caller who wants it kept sizes rect from
EmbeddedImage::width and
EmbeddedImage::height.
use pdfrum::Rect;
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
let logo = edit.embed_jpeg(include_bytes!("../tests/fixtures/mona_lisa.jpg"))?;
edit.draw_page(0, |c| {
c.image(&logo, Rect::new(20.0, 20.0, 80.0, 80.0));
})?;Sourcepub fn opacity(&mut self, alpha: f64)
pub fn opacity(&mut self, alpha: f64)
Set the constant alpha for everything drawn after it, as an
/ExtGState naming /ca and /CA.
Scoped by Canvas::saved. The alpha a Color already carries is
applied on top of this, so a translucent colour under a 0.5 opacity is
twice translucent.
use pdfrum::{Color, Rect};
let doc = pdfrum::Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
c.saved(|c| {
c.opacity(0.2);
c.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::from_rgb8(255, 0, 0));
});
})?;Source§impl Canvas<'_, '_>
impl Canvas<'_, '_>
Sourcepub fn draw_svg(
&mut self,
svg: &str,
into: Rect,
fit: SvgFit,
) -> Result<SvgIngestReport, Error>
Available on crate feature svg-import only.
pub fn draw_svg( &mut self, svg: &str, into: Rect, fit: SvgFit, ) -> Result<SvgIngestReport, Error>
svg-import only.Draw an SVG document into into, as vectors.
svg is the document’s source, and fit says how its own coordinate
box is placed in the rectangle — see SvgFit. The whole drawing is
scoped: it is wrapped in one q/Q and clipped to into, so nothing
the SVG does escapes the rectangle the caller named and the canvas’s
own graphics state is untouched afterwards.
The returned SvgIngestReport lists every construct that could not
be carried, and an empty report means the whole document went in.
§Errors
Error::Svg when usvg cannot resolve the
document at all — malformed XML, or an <svg> with no usable size.
A construct that resolves but does not map is a report item, not an
error.
use pdfrum::{Document, Rect, SvgFit};
// A plain string with escaped quotes rather than a raw one: a `#`
// inside a doc comment ends the raw-string hash count.
const LOGO: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" \
viewBox=\"0 0 10 10\">\
<circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"#c00\"/></svg>";
let doc = Document::open("tests/fixtures/hello_world.pdf")?;
let mut edit = doc.edit();
edit.draw_page(0, |c| {
let report = c.draw_svg(LOGO, Rect::new(40.0, 40.0, 140.0, 140.0), SvgFit::Contain);
assert!(report.is_ok_and(|r| r.is_empty()));
})?;Sourcepub fn draw_svg_from(
&mut self,
svg: &str,
into: Rect,
fit: SvgFit,
resources_dir: Option<&Path>,
) -> Result<SvgIngestReport, Error>
Available on crate feature svg-import only.
pub fn draw_svg_from( &mut self, svg: &str, into: Rect, fit: SvgFit, resources_dir: Option<&Path>, ) -> Result<SvgIngestReport, Error>
svg-import only.Draw an SVG document whose relative <image href> links resolve
against resources_dir.
Canvas::draw_svg is this with no directory, which is right for a
document held in memory; a document read from a file wants the file’s
own directory here, or its linked images do not load.
§Errors
As Canvas::draw_svg.
Sourcepub fn place_svg(&mut self, form: &SvgForm, into: Rect, fit: SvgFit)
Available on crate feature svg-import only.
pub fn place_svg(&mut self, form: &SvgForm, into: Rect, fit: SvgFit)
svg-import only.Place an SvgForm compiled by
DocEdit::compile_svg, fitting its
box into into the way Canvas::draw_svg fits a document.
The deduplicating spelling of draw_svg: the SVG is compiled once and
this writes one Do per placement, so the same logo on twenty pages
is one content stream rather than twenty. The un-fitted placement is
this without the fit, stretching the form’s box onto the rectangle.
use pdfrum::{Document, Rect, SvgFit};
const LOGO: &str = "<svg xmlns=\"http://www.w3.org/2000/svg\" \
viewBox=\"0 0 10 10\">\
<circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"#c00\"/></svg>";
let doc = Document::open("tests/fixtures/hello_world_2_pages.pdf")?;
let mut edit = doc.edit();
let (logo, report) = edit.compile_svg(LOGO)?;
assert!(report.is_empty());
edit.draw_pages(|c| c.place_svg(&logo, Rect::new(10.0, 10.0, 60.0, 60.0), SvgFit::Contain))?;