Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 57 additions & 6 deletions crates/krilla/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::graphics::shading_function::{
use crate::graphics::shading_pattern::ShadingPattern;
use crate::graphics::tiling_pattern::TilingPattern;
use crate::graphics::xobject::XObject;
use crate::interchange::tagging::ContentTag;
use crate::interchange::tagging::{ContentTag, XObjectTagId};
use crate::num::NormalizedF32;
use crate::resource;
use crate::resource::{Resource, ResourceDictionaryBuilder};
Expand Down Expand Up @@ -55,6 +55,12 @@ pub(crate) struct ContentBuilder {
/// A temporary buffer that's reused across the builder.
scratch: Vec<u8>,
pub(crate) active_marked_content: bool,
/// If set, this sub-builder is tagged and will produce an XObject with StructParents.
pub(crate) xobject_tag_id: Option<XObjectTagId>,
/// The page index inherited from the parent surface (for MCR /Pg entries).
pub(crate) page_index: Option<usize>,
/// Per-XObject MCID counter, incremented by start_tagged in sub-builder context.
pub(crate) mcid_counter: i32,
}

/// Stores either a device-specific color space,
Expand All @@ -78,6 +84,9 @@ impl ContentBuilder {
bbox: None,
scratch: Vec::new(),
active_marked_content: false,
xobject_tag_id: None,
page_index: None,
mcid_counter: 0,
}
}

Expand Down Expand Up @@ -857,10 +866,26 @@ impl ContentBuilder {
);
}

pub(crate) fn draw_masked(&mut self, sc: &mut SerializeContext, mask: Mask, stream: Stream) {
pub(crate) fn draw_masked(
&mut self,
sc: &mut SerializeContext,
mask: Mask,
stream: Stream,
xobject_tag_id: Option<XObjectTagId>,
page_index: Option<usize>,
num_mcids: i32,
) {
let state = ExtGState::new().mask(mask, sc);
self.uses_mask = true;
let x_object = XObject::new(stream, false, true, None);
let x_object = XObject::new(
stream,
false,
true,
None,
xobject_tag_id,
page_index,
num_mcids,
);
self.draw_xobject(sc, x_object, &state);
}

Expand All @@ -869,17 +894,43 @@ impl ContentBuilder {
sc: &mut SerializeContext,
opacity: NormalizedF32,
stream: Stream,
xobject_tag_id: Option<XObjectTagId>,
page_index: Option<usize>,
num_mcids: i32,
) {
let state = ExtGState::new()
.stroking_alpha(opacity)
.non_stroking_alpha(opacity);
let x_object = XObject::new(stream, true, false, None);
let x_object = XObject::new(
stream,
true,
false,
None,
xobject_tag_id,
page_index,
num_mcids,
);
self.draw_xobject(sc, x_object, &state);
}

pub(crate) fn draw_isolated(&mut self, sc: &mut SerializeContext, stream: Stream) {
pub(crate) fn draw_isolated(
&mut self,
sc: &mut SerializeContext,
stream: Stream,
xobject_tag_id: Option<XObjectTagId>,
page_index: Option<usize>,
num_mcids: i32,
) {
let state = ExtGState::new();
let x_object = XObject::new(stream, true, false, None);
let x_object = XObject::new(
stream,
true,
false,
None,
xobject_tag_id,
page_index,
num_mcids,
);
self.draw_xobject(sc, x_object, &state);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/krilla/src/graphics/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Graphic {
/// from wherever the graphic is invoked.
pub fn new(stream: Stream, isolated: bool) -> Self {
Self {
x_object: XObject::new(stream, isolated, false, None),
x_object: XObject::new(stream, isolated, false, None, None, None, 0),
}
}
}
11 changes: 9 additions & 2 deletions crates/krilla/src/graphics/mask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,15 @@ impl Cacheable for Mask {
fn serialize(self, sc: &mut SerializeContext, root_ref: Ref) -> Deferred<Chunk> {
let mut chunk = Chunk::new();

let x_object =
sc.register_cacheable(XObject::new(self.stream, false, true, self.custom_bbox));
let x_object = sc.register_cacheable(XObject::new(
self.stream,
false,
true,
self.custom_bbox,
None,
None,
0,
));

let mut dict = chunk.indirect(root_ref).dict();
dict.pair(Name(b"Type"), Name(b"Mask"));
Expand Down
26 changes: 26 additions & 0 deletions crates/krilla/src/graphics/xobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::chunk_container::ChunkContainerFn;
use crate::configure::ValidationError;
use crate::geom::Rect;
use crate::graphics::color::{rgb, DEVICE_RGB};
use crate::interchange::tagging::XObjectTagId;
use crate::resource;
use crate::resource::{Resource, Resourceable};
use crate::serialize::{Cacheable, MaybeDeviceColorSpace, SerializeContext};
Expand All @@ -19,6 +20,12 @@ struct Repr {
isolated: bool,
transparency_group_color_space: bool,
custom_bbox: Option<Rect>,
/// If set, this XObject contains tagged content and needs StructParents.
xobject_tag_id: Option<XObjectTagId>,
/// The page index where this XObject is drawn (for parent tree lookup).
page_index: Option<usize>,
/// Number of MCIDs allocated in this XObject's content stream.
num_mcids: i32,
}

#[derive(Debug, Hash, Clone, Eq, PartialEq)]
Expand All @@ -30,6 +37,9 @@ impl XObject {
isolated: bool,
mut transparency_group_color_space: bool,
custom_bbox: Option<Rect>,
xobject_tag_id: Option<XObjectTagId>,
page_index: Option<usize>,
num_mcids: i32,
) -> Self {
// In case a mask was invoked in the content stream, we _always_ create
// a new transparency group. Please see <https://github.com/typst/typst/issues/5509>.
Expand All @@ -48,6 +58,9 @@ impl XObject {
isolated,
transparency_group_color_space,
custom_bbox,
xobject_tag_id,
page_index,
num_mcids,
})))
}

Expand Down Expand Up @@ -78,6 +91,15 @@ impl Cacheable for XObject {
sc.register_validation_error(ValidationError::Transparency(sc.location));
}

// Register the XObject's tag ref and struct parent before the deferred closure.
let struct_parents_key = if let Some(tag_id) = self.0.xobject_tag_id {
let page_index = self.0.page_index.unwrap();
sc.register_xobject_tag_ref(tag_id, root_ref);
sc.register_xobject_struct_parent(tag_id, page_index, self.0.num_mcids)
} else {
None
};

let serialize_settings = sc.serialize_settings();

// "Ordinarily, the CS entry may be present only for isolated transparency groups (those
Expand Down Expand Up @@ -113,6 +135,10 @@ impl Cacheable for XObject {
.to_pdf_rect(),
);

if let Some(key) = struct_parents_key {
x_object.struct_parents(key);
}

if use_transparency_group {
let mut group = x_object.group();
let transparency = group.transparency();
Expand Down
9 changes: 9 additions & 0 deletions crates/krilla/src/interchange/tagging/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ impl Output for Node {
ai.page_index, ai.annot_index
)
}
Node::Leaf(Identifier(IdentifierInner::Real(IdentifierType::XObjectIdentifier(
xi,
)))) => {
writeln!(
f,
"{indent}- XObject: page={} tag_id={} mcid={}",
xi.page_index, xi.xobject_tag_id.0, xi.mcid
)
}
Node::Leaf(Identifier(IdentifierInner::Dummy)) => writeln!(f, "{indent}- Artifact"),
}
}
Expand Down
57 changes: 57 additions & 0 deletions crates/krilla/src/interchange/tagging/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,34 @@ impl<'a> SpanTag<'a> {
}
}

/// A unique identifier for a tagged sub-builder (XObject).
/// Assigned when creating a sub-builder for transparency groups.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct XObjectTagId(pub(crate) usize);

/// Tracks MCIDs within a specific XObject content stream.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct XObjectTagIdentifier {
/// The page where the XObject is drawn (for MCR /Pg reference).
pub(crate) page_index: usize,
/// The unique XObject tag ID.
pub(crate) xobject_tag_id: XObjectTagId,
/// The marked content identifier within this XObject.
pub(crate) mcid: i32,
}

impl From<XObjectTagIdentifier> for IdentifierType {
fn from(value: XObjectTagIdentifier) -> Self {
IdentifierType::XObjectIdentifier(value)
}
}

impl From<XObjectTagIdentifier> for Identifier {
fn from(value: XObjectTagIdentifier) -> Self {
Identifier(IdentifierInner::Real(value.into()))
}
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct PageTagIdentifier {
pub(crate) page_index: usize,
Expand Down Expand Up @@ -382,9 +410,11 @@ impl AnnotationIdentifier {
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum IdentifierType {
PageIdentifier(PageTagIdentifier),
AnnotationIdentifier(AnnotationIdentifier),
XObjectIdentifier(XObjectTagIdentifier),
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -1184,6 +1214,33 @@ fn serialize_children(
.page(page_ref)
.object(*annotation_ref);
}
IdentifierType::XObjectIdentifier(xi) => {
let page_ref = sc
.page_infos()
.get(xi.page_index)
.unwrap_or_else(|| panic!(
"tag tree contains xobject identifier from page {}, but document only has {} pages",
xi.page_index + 1,
sc.page_infos().len()
))
.ref_();

let xobj_ref = sc.xobject_tag_ref(xi.xobject_tag_id).unwrap_or_else(|| {
panic!("xobject tag ref not found for {:?}", xi.xobject_tag_id)
});

if parent_tree_map.contains_key(&xi.into()) {
panic!("identifier {xi:?} appears twice in the tag tree");
}

parent_tree_map.insert(xi.into(), parent_ref);

struct_children
.marked_content_ref()
.marked_content_id(xi.mcid)
.page(page_ref)
.stream(xobj_ref);
}
},
}
}
Expand Down
Loading