Files
craft-libs/vendor/krilla/src/surface.rs
T
aiongg 7895f5ed79 krilla: optional content groups (layers), patch 12 (#258)
Document::add_optional_content_group writes an OCG; Surface::start_optional_content and
end_optional_content mark content with /OC BDC/EMC, registering the group under the stream's
/Properties resources. Marks nest independently of tagging, and misuse returns or records a
KrillaError instead of panicking. The catalog gets /OCProperties (hidden groups in /OFF) and,
opt-in, /PageMode /UseOC. PDF/A-1 reports ValidationError::OptionalContent; below PDF 1.5 the
version requirement is registered too.
2026-10-24 02:13:19 -04:00

707 lines
26 KiB
Rust

//! Drawing onto a surface.
//!
//! This module contains most core part of krilla: The [`Surface`] struct. A surface
//! represents a drawing area on which you can define the contents of your page. This includes
//! operations such as applying linear transformations, showing text or images and drawing paths.
use std::num::NonZeroU64;
use crate::chunk_container::ChunkContainer;
use crate::color::rgb;
use crate::configure::validate::VersionedFeature;
use crate::configure::ValidationError;
use crate::content::ContentBuilder;
use crate::geom::Path;
#[cfg(any(feature = "raster-images", feature = "pdf"))]
use crate::geom::Size;
use crate::geom::{Point, Transform};
use crate::graphic::Graphic;
use crate::graphics::blend::{BlendMode, GroupOptions, Overprint};
use crate::graphics::graphics_state::ExtGState;
#[cfg(feature = "raster-images")]
use crate::graphics::image::Image;
use crate::graphics::mask::Mask;
use crate::graphics::paint::{Fill, FillRule, Stroke};
use crate::graphics::shading_function::ShadingFunction;
use crate::interchange::tagging::{ContentTag, Identifier, PageTagIdentifier};
use crate::num::NormalizedF32;
use crate::paint::{InnerPaint, Paint};
#[cfg(feature = "pdf")]
use crate::pdf::PdfDocument;
use crate::serialize::SerializeContext;
use crate::stream::{Stream, StreamBuilder};
use crate::tagging::ArtifactType;
use crate::text::Font;
use crate::text::{draw_glyph, Glyph};
#[cfg(feature = "simple-text")]
use crate::text::{shape::naive_shape, TextDirection};
/// Can be used to associate render operations with a unique identifier.
/// This is useful if you want to backtrack a validation error to a specific
/// identifier chosen by you.
pub type Location = NonZeroU64;
/// A surface.
///
/// Represents a drawing area for defining graphical content. The origin of the
/// coordinate axis is in the top-left corner.
///
/// You cannot directly create an instance of a [`Surface`] yourself.
/// Instead, there are two ways of getting access to a surface, which you can then use to draw on:
///
/// - The first way, and also the most common one you will use, is by creating a new document,
/// adding a page to it and then invoking the [`Page::surface`] method. The surface returned as part of
/// that represents the drawing area of the page.
/// - The second way is by calling the [`Surface::stream_builder`] method on the surface, to create a sub-drawing
/// context. See the documentation of the [`stream`] module for more information.
///
/// The surface uses a `push` and `pop` based mechanism for applying certain actions. For example,
/// there is a [`Surface::push_transform`] method which allows you to concatenate a new transform to the
/// current transform matrix. There is also a [`Surface::push_clip_path`] method, which allows you to
/// intersect the current drawing area with a clip path. Once you call such a `push` method,
/// the action that it invokes will be in place until you call the `pop` method, which will then
/// revert the last `push` operation. This allows you to, for example, define a clipping path area
/// or a mask to use only for certain objects.
///
/// It is important that, for each `push` method you invoke, there must be a corresponding `pop`
/// invocation that reverts it. If, at the end of the using the surface, the number of pushes/pops is
/// uneven, the program will panic.
///
/// [`stream`]: crate::stream
/// [`Page::surface`]: crate::page::Page::surface
pub struct Surface<'a> {
pub(crate) sc: &'a mut SerializeContext,
pub(crate) chunk_container: &'a mut ChunkContainer,
fill: Option<Fill>,
stroke: Option<Stroke>,
bd: Builders,
push_instructions: Vec<PushInstruction>,
page_identifier: Option<PageTagIdentifier>,
finish_fn: Box<dyn FnMut(Stream, i32) + 'a>,
}
impl<'a> Surface<'a> {
pub(crate) fn new(
sc: &'a mut SerializeContext,
chunk_container: &'a mut ChunkContainer,
root_builder: ContentBuilder,
page_identifier: Option<PageTagIdentifier>,
finish_fn: Box<dyn FnMut(Stream, i32) + 'a>,
) -> Surface<'a> {
Self {
sc,
chunk_container,
bd: Builders::new(root_builder),
page_identifier,
fill: None,
stroke: None,
push_instructions: vec![],
finish_fn,
}
}
/// The content stream being drawn and the serialization context, borrowed together.
pub(crate) fn content_and_context(&mut self) -> (&mut ContentBuilder, &mut SerializeContext) {
(self.bd.get_mut(), self.sc)
}
/// Return a `StreamBuilder` to allow drawing on a sub-context.
pub fn stream_builder(&mut self) -> StreamBuilder<'_> {
StreamBuilder::new(self.sc, self.chunk_container)
}
/// Set the fill that should be used for the next drawing operation.
///
/// You can set it to `None` if you want to disable filling (though
/// if there is no active stroke, than krilla will choose to fill
/// with black by default).
pub fn set_fill(&mut self, fill: Option<Fill>) {
self.fill = fill;
}
/// Get the currently active fill.
pub fn get_fill(&mut self) -> Option<&Fill> {
self.fill.as_ref()
}
/// Set the stroke that should be used for drawing operations.
///
/// You can set it to `None` if you want to disable stroking.
pub fn set_stroke(&mut self, stroke: Option<Stroke>) {
self.stroke = stroke;
}
/// Get the currently active stroke.
pub fn get_stroke(&mut self) -> Option<&Stroke> {
self.stroke.as_ref()
}
/// Draw a path using the currently active fill and/or stroke.
pub fn draw_path(&mut self, path: &Path) {
if self.fill.is_some() || self.stroke.is_some() {
if self.has_complex_fill_or_stroke() {
self.bd.get_mut().draw_path(
&path.0,
self.fill.as_ref(),
None,
self.sc,
self.chunk_container,
);
self.bd.get_mut().draw_path(
&path.0,
None,
self.stroke.as_ref(),
self.sc,
self.chunk_container,
);
} else {
self.bd.get_mut().draw_path(
&path.0,
self.fill.as_ref(),
self.stroke.as_ref(),
self.sc,
self.chunk_container,
);
}
} else {
// Draw with black by default.
self.bd.get_mut().draw_path(
&path.0,
Some(&Fill::default()),
None,
self.sc,
self.chunk_container,
);
}
}
/// Start a new tagged content section.
///
/// # Panics
/// Panics if a tagged section has already been started.
/// Panics if an artifact with a required bbox has none.
pub fn start_tagged(&mut self, tag: ContentTag) -> Identifier {
if let Some(id) = &mut self.page_identifier {
match tag {
// An artifact is actually not really part of tagged PDF and doesn't have
// a marked content identifier, so we need to return a dummy one here. It's just
// the API of krilla that conflates artifacts with tagged content,
// for the sake of simplicity. But the user of the library does not need to know
// about this.
ContentTag::Artifact(artifact) => {
if matches!(artifact.kind, ArtifactType::Header | ArtifactType::Footer)
&& self.sc.serialize_settings().pdf_version()
< VersionedFeature::HeaderFooterArtifactSubtypes.minimum_pdf_version()
{
self.sc.register_validation_error(
ValidationError::RequiresNewerPdfVersion(
VersionedFeature::HeaderFooterArtifactSubtypes,
self.sc.location,
),
);
}
if artifact.requires_properties(self.sc.serialize_settings().pdf_version()) {
self.bd
.get_mut()
.start_marked_content_with_properties(self.sc, None, tag);
} else {
self.bd.get_mut().start_marked_content(tag.name());
}
if artifact.requires_bbox(self.sc.serialize_settings().pdf_version())
&& artifact.bbox.is_none()
{
panic!("Background artifact must have a bounding box in PDF 1.7");
}
Identifier::dummy()
}
ContentTag::Span(_) | ContentTag::Other => {
self.bd.get_mut().start_marked_content_with_properties(
self.sc,
Some(id.mcid),
tag,
);
id.bump().into()
}
}
} else {
Identifier::dummy()
}
}
/// End the current tagged section.
///
/// # Panics
/// Panics if no tagged section has been started.
pub fn end_tagged(&mut self) {
if self.page_identifier.is_some() {
self.bd.get_mut().end_marked_content();
}
}
fn outline_glyphs(
&mut self,
glyphs: &[impl Glyph],
context_color: rgb::Color,
start: Point,
font: Font,
font_size: f32,
) {
let (mut cur_x, y) = (start.x, start.y);
for glyph in glyphs {
let mut base_transform = tiny_skia_path::Transform::from_translate(
cur_x + glyph.x_offset(font_size),
y - glyph.y_offset(font_size),
);
base_transform = base_transform.pre_concat(tiny_skia_path::Transform::from_scale(
font_size / font.units_per_em(),
-font_size / font.units_per_em(),
));
draw_glyph(
font.clone(),
context_color,
glyph.glyph_id(),
Transform::from_tsp(base_transform),
self,
);
cur_x += glyph.x_advance(font_size);
}
}
/// Draw a sequence of glyphs using the currently active fill and/or stroke.
///
/// This is a very low-level method, which gives you full control over how to place
/// the glyphs that make up the text. This means that you must have your own text processing
/// logic for dealing with bidirectional text, font fallback, text layouting, etc.
pub fn draw_glyphs(
&mut self,
start: Point,
glyphs: &[impl Glyph],
font: Font,
text: &str,
font_size: f32,
outlined: bool,
) {
let context_color = self.context_color();
if outlined {
self.outline_glyphs(glyphs, context_color, start, font, font_size);
} else {
match (self.fill.as_ref(), self.stroke.as_ref()) {
(Some(f), Some(s)) => {
if self.has_complex_fill_or_stroke() {
// We need to draw fill and stroke separately. In order to avoid embedding
// text twice, we fill the text as normal text and then stroke it as a path.
self.bd.get_mut().draw_glyphs(
start,
self.sc,
self.chunk_container,
Some(f),
None,
context_color,
glyphs,
font.clone(),
text,
font_size,
);
self.outline_glyphs(glyphs, context_color, start, font, font_size);
} else {
self.bd.get_mut().draw_glyphs(
start,
self.sc,
self.chunk_container,
Some(f),
Some(s),
context_color,
glyphs,
font,
text,
font_size,
);
}
}
(None, Some(s)) => {
self.bd.get_mut().draw_glyphs(
start,
self.sc,
self.chunk_container,
None,
Some(s),
context_color,
glyphs,
font,
text,
font_size,
);
}
(Some(f), None) => {
self.bd.get_mut().draw_glyphs(
start,
self.sc,
self.chunk_container,
Some(f),
None,
context_color,
glyphs,
font,
text,
font_size,
);
}
(None, None) => {
// Draw with black by default.
self.bd.get_mut().draw_glyphs(
start,
self.sc,
self.chunk_container,
Some(&Fill::default()),
None,
context_color,
glyphs,
font,
text,
font_size,
);
}
}
}
}
/// Draw some text using the currently active fill and/or stroke.
///
/// This is a high-level method which allows you to just provide some text, which will
/// then be rendered into a single line. However, this approach has restrictions:
///
/// - It will not perform BIDI resolution and only supports a single script, meaning that you
/// must ensure that your text does not contain multiple scripts.
/// - It will only use the single font you provided to draw the text, no font fallback will
/// be performed.
///
/// If you need more advanced control over how your text looks,
/// you can use the `fill_glyphs` method.
#[cfg(feature = "simple-text")]
pub fn draw_text(
&mut self,
start: Point,
font: Font,
font_size: f32,
text: &str,
outlined: bool,
direction: TextDirection,
) {
let glyphs = naive_shape(text, font.clone(), direction);
self.draw_glyphs(start, &glyphs, font, text, font_size, outlined);
}
/// Set the location that should be assumed for subsequent operations.
pub fn set_location(&mut self, location: Location) {
self.sc.set_location(location);
}
/// Reset the location that should be assumed for subsequent operations.
pub fn reset_location(&mut self) {
self.sc.reset_location();
}
/// Return the current transformation matrix.
pub fn ctm(&self) -> Transform {
self.bd.get().cur_transform()
}
/// Concatenate a new transform to the current transformation matrix.
pub fn push_transform(&mut self, transform: &Transform) {
self.push_instructions.push(PushInstruction::Transform);
self.bd.get_mut().save_graphics_state();
self.bd.get_mut().concat_transform(transform);
}
/// Push a new blend mode.
pub fn push_blend_mode(&mut self, blend_mode: BlendMode) {
self.push_instructions.push(PushInstruction::BlendMode);
self.bd.get_mut().save_graphics_state();
self.bd.get_mut().set_blend_mode(blend_mode.to_pdf());
}
/// Push overprint settings. Paths, text and images drawn until the matching
/// [`Surface::pop`] carry them, and so do groups pushed inside. The `/op`, `/OP` and
/// `/OPM` entries are always written, so `Overprint::default()` turns overprint off
/// inside an outer `push_overprint`.
pub fn push_overprint(&mut self, overprint: Overprint) {
self.push_instructions.push(PushInstruction::Overprint);
self.bd.get_mut().save_graphics_state();
self.bd.get_mut().set_overprint(overprint);
}
/// Push a new clip path.
pub fn push_clip_path(&mut self, path: &Path, clip_rule: &FillRule) {
self.push_instructions.push(PushInstruction::ClipPath);
self.bd.get_mut().push_clip_path(&path.0, clip_rule);
}
/// Push a new mask.
pub fn push_mask(&mut self, mask: Mask) {
self.push_instructions
.push(PushInstruction::Mask(Box::new(mask)));
self.bd
.sub_builders
.push(ContentBuilder::new(Transform::identity(), true, self.sc));
}
#[cfg(feature = "pdf")]
/// Embed a single PDF page with the given dimensions.
pub fn draw_pdf_page(&mut self, pdf: &PdfDocument, size: Size, page_idx: usize) {
use crate::geom::Rect;
let obj_ref = self.sc.embed_pdf_page_as_xobject(pdf, page_idx);
// If the user provided an invalid page index, we will detect this later on anyway, so
// just use dummy dimensions here.
let (page_width, page_height) = pdf
.pages()
.get(page_idx)
.map(|p| p.render_dimensions())
.unwrap_or((1.0, 1.0));
let transform =
Transform::from_scale(size.width() / page_width, size.height() / page_height);
// We applied a transform to invert the y-axis initially, but since the embedded PDF also uses
// a y-up coordinate system, we need to "invert" this.
self.push_transform(&transform);
self.push_transform(&Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, page_height));
self.bd.get_mut().draw_xobject_by_reference(
self.sc,
self.chunk_container,
Rect::from_xywh(0.0, 0.0, page_width, page_height).unwrap(),
obj_ref,
);
self.pop();
self.pop();
}
/// Push a new opacity, meaning that each subsequent graphics object will be
/// rendered with a base opacity.
///
/// This stacks, meaning that if you do `push_opacity(0.5)` twice, the resulting
/// base opacity will be 0.25.
pub fn push_opacity(&mut self, opacity: NormalizedF32) {
self.push_instructions
.push(PushInstruction::Opacity(opacity));
if opacity != NormalizedF32::ONE {
self.bd
.sub_builders
.push(ContentBuilder::new(Transform::identity(), true, self.sc));
}
}
/// Push a new isolated layer. Shorthand for [`Surface::push_group`] with only
/// `isolated` set.
pub fn push_isolated(&mut self) {
self.push_group(GroupOptions {
isolated: true,
knockout: false,
});
}
/// Push a new transparency group with the given attributes. Everything drawn until
/// the matching [`Surface::pop`] goes into the group.
pub fn push_group(&mut self, options: GroupOptions) {
self.push_instructions.push(PushInstruction::Group(options));
self.bd
.sub_builders
.push(ContentBuilder::new(Transform::identity(), true, self.sc));
}
/// Pop the last `push` instruction.
///
/// # Panics
/// Panics if the there wasn't a corresponding `push` to the `pop`.
pub fn pop(&mut self) {
match self.push_instructions.pop().unwrap() {
PushInstruction::Transform => self.bd.get_mut().restore_graphics_state(),
PushInstruction::Opacity(o) => {
if o != NormalizedF32::ONE {
let stream = self.bd.sub_builders.pop().unwrap().finish(self.sc);
self.bd
.get_mut()
.draw_opacified(self.sc, self.chunk_container, o, stream);
}
}
PushInstruction::ClipPath => self.bd.get_mut().pop_clip_path(),
PushInstruction::BlendMode => self.bd.get_mut().restore_graphics_state(),
PushInstruction::Overprint => self.bd.get_mut().restore_graphics_state(),
PushInstruction::Mask(mask) => {
let stream = self.bd.sub_builders.pop().unwrap().finish(self.sc);
self.bd
.get_mut()
.draw_masked(self.sc, self.chunk_container, *mask, stream)
}
PushInstruction::Group(options) => {
let stream = self.bd.sub_builders.pop().unwrap().finish(self.sc);
self.bd
.get_mut()
.draw_group(self.sc, self.chunk_container, options, stream);
}
}
}
#[cfg(feature = "raster-images")]
/// Draw a new bitmap image.
pub fn draw_image(&mut self, image: Image, size: Size) {
self.bd
.get_mut()
.draw_image(image, size, self.sc, self.chunk_container);
}
/// Draw a new graphic.
///
/// Drawing the same graphic multiple times is very cheap in terms of
/// file size.
pub fn draw_graphic(&mut self, graphic: Graphic) {
self.bd.get_mut().draw_xobject(
self.sc,
self.chunk_container,
graphic.x_object,
&ExtGState::new(),
)
}
pub(crate) fn draw_shading(&mut self, shading: &ShadingFunction) {
self.bd
.get_mut()
.draw_shading(shading, self.sc, self.chunk_container);
}
/// A convenience method for `std::mem::drop`.
///
/// # Panics
/// Panics if the push/pop difference is not 0.
pub fn finish(self) {}
pub(crate) fn draw_opacified_stream(&mut self, opacity: NormalizedF32, stream: Stream) {
self.bd
.get_mut()
.draw_opacified(self.sc, self.chunk_container, opacity, stream)
}
/// Return the current transformation matrix of the surface.
pub fn cur_transform(&self) -> Transform {
self.bd.get().cur_transform()
}
fn context_color(&self) -> rgb::Color {
self.fill
.as_ref()
.and_then(|f| f.paint.as_rgb())
.or_else(|| self.stroke.as_ref().and_then(|s| s.paint.as_rgb()))
.unwrap_or_default()
}
fn has_complex_fill_or_stroke(&self) -> bool {
// Things can go wrong and yield inconsistent results in different PDF viewers
// for some fill/stroke combinations. So in some cases, we need to fill and stroke
// separately.
// 1) We have an opacity on the stroke, because they render inconsistently across different
// viewers.
// 2) We have a linear gradient with opacities in at least one stop, because then we need
// to invoke a soft mask, in which case the fill/stroke would both be affected by this.
let check_paint = |paint: &Paint| match &paint.0 {
InnerPaint::Color(_) => false,
InnerPaint::LinearGradient(l) => {
l.stops.iter().any(|s| s.opacity != NormalizedF32::ONE)
}
InnerPaint::RadialGradient(r) => {
r.stops.iter().any(|s| s.opacity != NormalizedF32::ONE)
}
InnerPaint::SweepGradient(r) => r.stops.iter().any(|s| s.opacity != NormalizedF32::ONE),
InnerPaint::MeshGradient(m) => m
.patches
.iter()
.any(|p| p.opacities.iter().any(|o| *o != NormalizedF32::ONE)),
InnerPaint::Pattern(_) => false,
};
let complex_stroke = self
.stroke
.as_ref()
.is_some_and(|s| s.opacity != NormalizedF32::ONE || check_paint(&s.paint));
let complex_fill = self.fill.as_ref().is_some_and(|f| check_paint(&f.paint));
complex_fill || complex_stroke
}
}
impl Drop for Surface<'_> {
fn drop(&mut self) {
// Keep the document poisoned if cleanup unwinds, including an invariant failure
// during an explicit finish. Successful cleanup preserves any earlier poison.
let poisoned = std::mem::replace(&mut self.sc.poisoned, true);
// A drawing panic can leave surface state incomplete. Discard it while
// unwinding; finalization here could panic again and abort the process.
// The caller must discard the whole document after a caught panic.
if std::thread::panicking() {
return;
}
let root_builder = std::mem::replace(
&mut self.bd.root_builder,
ContentBuilder::new(Transform::identity(), false, self.sc),
);
let num_mcids = match self.page_identifier {
Some(pi) => pi.mcid,
None => 0,
};
assert!(self.bd.sub_builders.is_empty());
assert!(self.push_instructions.is_empty());
assert!(!root_builder.active_marked_content);
(self.finish_fn)(root_builder.finish(self.sc), num_mcids);
self.sc.poisoned = poisoned;
}
}
/// Holds the different content streams we are currently building. In the usual case,
/// this only contains the current page stream as the root builder, but the sub builders
/// will be used if we are for example creating a mask/pattern, or an XObject.
struct Builders {
pub(crate) root_builder: ContentBuilder,
pub(crate) sub_builders: Vec<ContentBuilder>,
}
impl Builders {
fn new(root_builder: ContentBuilder) -> Self {
Self {
root_builder,
sub_builders: vec![],
}
}
fn get_mut(&mut self) -> &mut ContentBuilder {
self.sub_builders
.last_mut()
.unwrap_or(&mut self.root_builder)
}
fn get(&self) -> &ContentBuilder {
self.sub_builders.last().unwrap_or(&self.root_builder)
}
}
pub(crate) enum PushInstruction {
Transform,
Opacity(NormalizedF32),
ClipPath,
BlendMode,
Overprint,
Mask(Box<Mask>),
Group(GroupOptions),
}