mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +00:00
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.
707 lines
26 KiB
Rust
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),
|
|
}
|