use super::document::slot_of;
use super::ops::{paints_apart, path_rule};
use super::types::{Frame, GState, Kind, MAX_DEPTH, MAX_NODES, PendingEnvelope};
use super::{
AppearanceItem, Arc, BezPath, BlendMode, FillRule, HashMap, HashSet, LayerColor, Node, NodeId, NodeKind, Obj, Paint, PathEl, Rc, Reader, V, obj,
};
use vectorcraft_doc::TextWrap;
impl<'a> Reader<'a> {
// ---- frames ----------------------------------------------------------------------------
pub(super) fn open(&mut self, kind: Kind, hidden: bool) -> Result<(), String> {
if self.frames.len() >= MAX_DEPTH {
return Err("its objects are nested too deeply".into());
}
if matches!(kind, Kind::Layer(_)) {
self.ensure_space();
self.check_symbols();
}
let base = if matches!(kind, Kind::Obj(_)) { self.stack.len() } else { self.frames.last().map_or(0, |f| f.base) };
self.frames.push(Frame {
kind,
children: Vec::new(),
hidden,
after: self.after_object,
mesh_ended: self.mesh_ended,
gs: self.gs.clone(),
pending_unread_fill: self.pending_unread_fill,
pending_unread_stroke: self.pending_unread_stroke,
base,
clips: Vec::new(),
compound_members: false,
unread: Vec::new(),
state: None,
});
self.after_object = false;
Ok(())
}
/// Close the innermost frame, which must be of the kind `is` accepts.
fn close(&mut self, what: &str, is: impl Fn(&Kind) -> bool) -> Result {
match self.frames.last() {
Some(f) if is(&f.kind) => self.frames.pop().ok_or_else(|| format!("`{what}` closes nothing")),
_ => Err(format!("`{what}` closes something it didn't open")),
}
}
/// Add a node to the innermost container.
pub(crate) fn add(&mut self, node: Node, hidden: bool) -> Result<(), String> {
self.nodes += 1;
if self.nodes > MAX_NODES {
return Err(format!("it has more than {MAX_NODES} objects"));
}
if hidden {
self.hidden_ids.insert(node.id);
}
match self.frames.last_mut() {
Some(f) => f.children.push(node),
None => return Err("it has art outside its layers".into()),
}
self.after_object = true;
Ok(())
}
/// The last object read, while nothing else started.
pub(super) fn last_object(&mut self) -> Option<&mut Node> {
if !self.after_object {
return None;
}
self.frames.last_mut()?.children.last_mut()
}
pub(crate) fn new_node(&mut self, kind: NodeKind, gs: &GState) -> Node {
let mut n = Node::new(self.doc.alloc_id(), kind);
n.visible = !gs.hidden;
n.locked = gs.locked;
n
}
pub(super) fn end_container(&mut self, what: &str, hidden: bool) -> Result<(), String> {
self.multi_mask_container_ends()?;
let f = match what {
"U" => self.close(what, |k| matches!(k, Kind::Group))?,
"*U" => self.close(what, |k| matches!(k, Kind::Compound))?,
"*W" => self.close(what, |k| matches!(k, Kind::Wrap))?,
_ => self.close(what, |k| matches!(k, Kind::Clip))?,
};
let Frame {
kind,
children: mut kids,
gs,
clips,
hidden: opened_hidden,
compound_members,
unread,
pending_unread_fill,
pending_unread_stroke,
..
} = f;
if matches!(kind, Kind::Clip) {
// `Q` restores the painting state, not the transparency: the writer sets `Xy` only when
// the next object's differs from the last one written, wherever that was, so a state set
// inside a clipping group stays after it (a glow set to Screen, then a clipping path set
// back to Normal: the type after the group has no `Xy` and is Normal).
let now = &self.gs;
let transparency = (now.opacity, now.blend, now.isolate, now.knockout, now.knockout_shape);
self.gs = gs.clone();
(self.gs.opacity, self.gs.blend, self.gs.isolate, self.gs.knockout, self.gs.knockout_shape) = transparency;
self.pending_unread_fill = pending_unread_fill;
self.pending_unread_stroke = pending_unread_stroke;
}
// Wraparound text: every object in it other than type is a wrap object, before the text or
// after it, with no offset (the format gives none) unless its dictionary gives its options.
// One before the text is below it, so the text flows around it only once it is moved above.
if matches!(kind, Kind::Wrap) {
let graphics = kids.iter_mut().filter(|k| !matches!(k.kind, NodeKind::Text(_)) && slot_of(k.name.as_deref()).is_none());
for k in graphics.filter(|k| k.extras().wrap.is_none()) {
k.extras_mut().wrap = Some(TextWrap { offset: 0.0, invert: false });
}
}
// A group holding only clipping paths (`q … u W n U … Q`) doesn't clip on its own: `u`/`U` don't save
// the graphics state as `q`/`Q` do, so its paths clip the clipping group around it, as the file's PDF part draws it.
if matches!(kind, Kind::Group)
&& unread.is_empty()
&& !clips.is_empty()
&& clips.len() == kids.len()
&& matches!(self.frames.last(), Some(Frame { kind: Kind::Clip, .. }))
{
for kid in kids {
if let Some(parent) = self.frames.last_mut() {
parent.clips.push(parent.children.len());
}
self.add(kid, opened_hidden || hidden)?;
}
return Ok(());
}
// A compound path of clipping paths clips its clipping group.
let clipping = matches!(kind, Kind::Compound) && !clips.is_empty();
self.group_ended = !matches!(kind, Kind::Compound);
let mut painted = None;
let node = match kind {
// A compound path holding a group (its drawn look) is that look: its paths paint apart.
// Some files mark every group so, the plain one holding the subpaths too (#980): one
// paint for all its paths keeps it one compound path, its holes cut.
Kind::Compound if kids.iter().any(|k| self.looks.contains(&k.id) && paints_apart(k)) => {
let (children, clip) = self.clip_first(kids, &clips, true, self.nesting() + 1)?;
self.new_node(NodeKind::Group { children, clip }, &gs)
}
Kind::Compound => {
// A group in a compound path (`*u u … U *U`) only holds its paths.
let mut kids = ungrouped(kids);
// A compound clipping path keeps its subpaths' fill when they all paint alike. When they paint
// differently, the fills of the filled clipping ones are a compound path of their own, under what it clips.
let look = |n: &Node| (n.appearance.clone(), n.opacity, n.blend);
if clipping && kids.windows(2).any(|w| look(&w[0]) != look(&w[1])) {
let fills: Vec = kids
.iter_mut()
.filter(|k| matches!(k.kind, NodeKind::Path { clipping: true, .. }))
.filter_map(|k| self.fill_apart(k))
.collect();
painted = self.compound(fills, &gs);
}
let Some(n) = self.compound(kids, &gs) else {
if gs.hidden || !self.shown() {
self.hidden_unread.extend(unread);
} else {
self.unsupported.extend(unread);
}
return Ok(());
};
n
}
// A group with a clipping path (`q` … `Q`, or a `W` path in a group) is a clipping group.
_ => {
let (children, clip) = self.clip_first(kids, &clips, false, self.nesting() + 1)?;
self.new_node(NodeKind::Group { children, clip }, &gs)
}
};
if clipping && let Some(f) = self.frames.last_mut() {
f.clips.push(f.children.len());
}
if compound_members {
self.compound_groups.insert(node.id);
}
if !unread.is_empty() {
if !node.visible || !self.shown() {
self.hidden_unread.extend(unread);
} else {
self.unread.push((node.id, unread));
}
}
self.add(node, opened_hidden || hidden)?;
if let Some(p) = painted {
self.clip_fills.insert(p.id);
self.add(p, opened_hidden || hidden)?;
}
Ok(())
}
/// The compound path of `kids`, painted and made transparent as its subpaths say.
fn compound(&mut self, mut kids: Vec, gs: &GState) -> Option {
// The compound path's paint is its last path's (a gradient's instance wraps only
// that one); the last painted one, should the last have none.
let shown = kids.iter().rev().find(|k| !k.appearance.items.is_empty()).or(kids.last())?;
let (appearance, opacity, blend, shape, rule) =
(shown.appearance.clone(), shown.opacity, shown.blend, shown.knockout_shape, path_rule(shown));
for k in &mut kids {
k.opacity = 1.0;
k.blend = BlendMode::Normal;
k.knockout_shape = false;
k.visible = true;
k.locked = false;
}
let mut n = self.new_node(NodeKind::Compound { children: kids.into_iter().map(Arc::new).collect(), rule }, gs);
n.appearance = appearance;
n.opacity = opacity;
n.blend = blend;
n.knockout_shape = shape;
Some(n)
}
/// How deep in the document the objects the open frames hold are, less one: a dictionary adds no level, and an
/// opacity mask's art is two under the frames around the object it masks.
fn nesting(&self) -> usize {
self.frames
.iter()
.map(|f| match f.kind {
Kind::Obj(_) => 0,
Kind::Mask => 2,
_ => 1,
})
.sum()
}
/// `kids` with the clipping paths among them (indexes `clips`) first → the children and whether they clip.
///
/// Several clipping paths in a clipping group or a layer clip to their intersection, as each `W` reduces the clip
/// set by the ones before it: each clipping path after the first clips a clipping group nested in the one before
/// it, and the innermost one holds the art. As with one clipping path, every object in the group is clipped by all
/// of them, wherever it is written. The fill of a compound clipping path read apart from it (`clip_fills`) stays at
/// its path's level. In a compound path (`union`), several clipping paths are one compound path. `at`: how deep the
/// group or layer made of them is in the document.
fn clip_first(&mut self, kids: Vec, clips: &[usize], union: bool, at: usize) -> Result<(Vec>, bool), String> {
if union || clips.len() < 2 {
return Ok(self.clip_union(kids, clips));
}
let clips: std::collections::HashSet = clips.iter().copied().collect();
let (clip, rest): (Vec, Vec) = kids.into_iter().enumerate().partition(|(i, _)| clips.contains(i));
let innermost = clip.len() - 1;
// A fill read apart follows its clipping path; the art goes in the innermost level.
let level = |i: usize, n: &Node| {
if self.clip_fills.contains(&n.id) { clip.iter().rposition(|(c, _)| *c < i).unwrap_or(0) } else { innermost }
};
// Each nested clipping group adds a level over the nodes placed in it, the clipping paths included. Past the
// reader's limit, or past the document's when the nodes fit in it without those levels (the document would merge
// the deepest groups and drop their clipping paths), the editing data isn't read.
let deepest =
rest.iter().map(|(i, n)| level(*i, n) + depth(n)).chain(clip.iter().enumerate().map(|(k, (_, n))| k + depth(n))).max().unwrap_or(0);
let flat = rest.iter().map(|(_, n)| depth(n)).chain(clip.iter().map(|(_, n)| depth(n))).max().unwrap_or(0);
if at + deepest > MAX_DEPTH || (at + deepest > vectorcraft_doc::MAX_DEPTH && at + flat <= vectorcraft_doc::MAX_DEPTH) {
return Err("its clipping masks are nested too deeply".into());
}
// Each level: its clipping path, the fills read apart from it, which paint under the art they clip, then the art.
let mut fills_at: Vec> = clip.iter().map(|_| Vec::new()).collect();
let mut art_at: Vec> = clip.iter().map(|_| Vec::new()).collect();
for (i, n) in rest {
let k = level(i, &n);
let at = if self.clip_fills.contains(&n.id) { &mut fills_at } else { &mut art_at };
if let Some(v) = at.get_mut(k) {
v.push(n);
}
}
let mut children: Vec> = Vec::new();
let mut levels = clip.into_iter().map(|(_, n)| n).zip(fills_at.into_iter().zip(art_at)).rev().peekable();
while let Some((path, (fills, art))) = levels.next() {
let mut inner: Vec> = Vec::with_capacity(fills.len() + art.len() + children.len() + 1);
inner.push(Arc::new(path));
inner.extend(fills.into_iter().chain(art).map(Arc::new));
inner.append(&mut children);
if levels.peek().is_none() {
return Ok((inner, true));
}
children = vec![Arc::new(Node::new(self.doc.alloc_id(), NodeKind::Group { children: inner, clip: true }))];
}
Ok((children, false))
}
/// `kids` with the clipping paths among them (indexes `clips`) first, several as one compound
/// path → the children and whether they clip.
fn clip_union(&mut self, kids: Vec, clips: &[usize]) -> (Vec>, bool) {
let clips: std::collections::HashSet = clips.iter().copied().collect();
let (mut clip, rest): (Vec, Vec) = kids.into_iter().enumerate().partition(|(i, _)| clips.contains(i));
let mut fills = Vec::new();
let clip_node = match clip.len() {
0 => None,
1 => clip.pop().map(|(_, n)| n),
_ => {
let rule = clip.first().map_or(FillRule::NonZero, |(_, n)| path_rule(n));
// Each clipping path's fill is a path of its own, under the art it clips, as each one paints it. The
// compound path keeps the clipping paths' stroke, opacity and blend mode when they all have the same ones,
// and the knockout shape flag when they all have it: it has one appearance for all its paths.
for (_, n) in &mut clip {
fills.extend(self.fill_apart(n));
}
let shape = clip.iter().all(|(_, n)| n.knockout_shape);
let look = |n: &Node| (n.appearance.clone(), n.opacity, n.blend);
let shared =
clip.split_first().and_then(|((_, first), others)| others.iter().all(|(_, n)| look(n) == look(first)).then(|| look(first)));
let children = clip.into_iter().map(|(_, n)| Arc::new(n)).collect();
let mut n = Node::new(self.doc.alloc_id(), NodeKind::Compound { children, rule });
if let Some((appearance, opacity, blend)) = shared {
n.appearance = appearance;
n.opacity = opacity;
n.blend = blend;
}
n.knockout_shape = shape;
Some(n)
}
};
let clips = clip_node.is_some();
// A fill read apart paints under the art it clips.
let (apart, rest): (Vec, Vec) = rest.into_iter().partition(|(_, n)| self.clip_fills.contains(&n.id));
let children = clip_node.into_iter().chain(fills).chain(apart.into_iter().chain(rest).map(|(_, n)| n)).map(Arc::new).collect();
(children, clips)
}
/// Clipping path `n`'s fills as a path of their own, taken off `n`; `None` when it has no fill. The path keeps `n`'s
/// transparency, visibility, lock and opacity mask, and none of its other attributes.
pub(super) fn fill_apart(&mut self, n: &mut Node) -> Option {
let is_fill = |i: &AppearanceItem| matches!(i, AppearanceItem::Fill(_));
if !n.appearance.items.iter().any(is_fill) {
return None;
}
let mut kind = n.kind.clone();
if let NodeKind::Path { clipping, .. } = &mut kind {
*clipping = false;
}
let mut fill = Node::new(self.doc.alloc_id(), kind);
// The copies of a path read on `%_` lines are on those lines too, as `add` records them.
let hidden = self.hidden_ids.contains(&n.id);
if hidden {
self.hidden_ids.insert(fill.id);
}
// A compound clipping path's subpaths: copies with ids of their own, linked to the same swatches, that don't clip.
for c in fill.children_mut().into_iter().flatten() {
let mut copy = self.doc.reid(c);
self.reid_masks(&mut copy, 0);
if let NodeKind::Path { clipping, .. } = &mut copy.kind {
*clipping = false;
}
if hidden {
self.hidden_ids.insert(copy.id);
}
*c = Arc::new(copy);
}
(fill.opacity, fill.blend, fill.isolate, fill.knockout, fill.knockout_shape, fill.visible, fill.locked) =
(n.opacity, n.blend, n.isolate, n.knockout, n.knockout_shape, n.visible, n.locked);
// The opacity mask's art is a copy with ids of its own, linked to the same swatches.
if let Some(mut mask) = n.extras().mask.clone() {
let mut art = self.doc.reid(&mask.art);
self.reid_masks(&mut art, 0);
mask.art = Arc::new(art);
fill.extras_mut().mask = Some(mask);
}
let (mut fills, mut rest) = ((*n.appearance).clone(), (*n.appearance).clone());
fills.items.retain(is_fill);
rest.items.retain(|i| !is_fill(i));
fill.appearance = fills.into();
n.appearance = rest.into();
// The new path, its subpaths and their points count toward the objects read, as a multi-layer mask's copies do.
let mut weight = fill.count_with_masks();
fill.walk(&mut |p| {
if let NodeKind::Path { path, .. } = &p.kind {
weight = weight.saturating_add(path.anchor_count());
}
});
self.nodes = self.nodes.saturating_add(weight);
Some(fill)
}
pub(super) fn end_layer(&mut self) -> Result<(), String> {
self.pending_unread_fill = false;
self.pending_unread_stroke = false;
let mut f = self.close("LB", |k| matches!(k, Kind::Layer(_)))?;
let state = self.layer_state.take();
self.container_undo = None;
// An `Xy` right after the layer's last mesh or blend, then `LB`: the layer's, not theirs.
if let (Some(_), Some(before)) = (state, self.mesh_undo.take())
&& let Some(mesh) = f.children.last_mut().filter(|n| matches!(n.kind, NodeKind::Mesh(_) | NodeKind::Blend { .. }))
{
(mesh.opacity, mesh.blend, mesh.isolate, mesh.knockout, mesh.knockout_shape) = before;
}
let Kind::Layer(a) = f.kind else { return Ok(()) };
let (children, clip) = self.clip_first(f.children, &f.clips, false, self.nesting() + 1)?;
let color = a.color.unwrap_or(LayerColor::Preset(0));
let mut layer = Node::new(
self.doc.alloc_id(),
NodeKind::Layer { color, template: a.template, printable: a.printable, children, clip, preview: a.preview, dim_images: a.dim },
);
layer.name = Some(a.name.clone());
layer.visible = a.visible;
layer.locked = a.locked;
// The layer's own opacity, blend and knockout, written after its art (#2003).
if let Some((opacity, blend, isolate, knockout, shape)) = state {
(layer.opacity, layer.blend, layer.isolate, layer.knockout, layer.knockout_shape) = (opacity, blend, isolate, knockout, shape);
}
match self.frames.last_mut() {
Some(parent) => {
parent.children.push(layer);
self.after_object = true;
}
None => {
self.doc.layers.push(Arc::new(layer));
self.after_object = false;
}
}
Ok(())
}
/// An operator the reader doesn't know is scoped to its top-level object when possible.
pub(super) fn note_unknown(&mut self, w: &str, hidden: bool) {
let op: String = w.chars().take(24).collect();
// A pattern definition or an owning mask can be on `%_` lines while still
// determining visible art. Unknown operators there must remain unread.
if (self.gs.hidden && self.pattern.is_none()) || (hidden && self.pattern.is_none() && !self.in_mask()) || !self.shown() {
if self.hidden_unread.len() < 16 {
self.hidden_unread.insert(format!("`{op}`"));
}
} else if !self.defer_to_mask(&format!("`{op}`")) && !self.mark_unread(format!("`{op}`")) && self.unknown.len() < 16 {
self.unknown.insert(op);
}
}
/// `:` after `/Type`: a dictionary opens.
pub(super) fn begin_obj(&mut self, hidden: bool) -> Result<(), String> {
let ty = match self.stack.pop() {
Some(V::Name(n)) => n,
Some(other) => {
self.stack.push(other);
String::new()
}
None => String::new(),
};
self.open(Kind::Obj(Box::new(Obj { ty, ..Obj::default() })), hidden)?;
// A dictionary's state starts afresh (styles set theirs inside).
self.gs = GState::default();
self.pending_unread_fill = false;
self.pending_unread_stroke = false;
Ok(())
}
/// Capture mask art in document coordinates, without adding it to the owner's siblings.
pub(super) fn end_mask(&mut self) -> Result<(), String> {
self.end_path_quietly();
let f = self.close("X+", |k| matches!(k, Kind::Mask))?;
// The mask's art is one level under the object it masks.
let (children, clip) = self.clip_first(f.children, &f.clips, false, self.nesting() + 2)?;
// One shown object is the art itself. A hidden object or a guide stays in a group: the canvas draws the art's root
// whether it is shown or not and whether guides show, and a group doesn't draw a hidden member or a left-out guide.
let art = match children.as_slice() {
[only] if !clip && only.visible && !matches!(only.kind, NodeKind::Path { guide: true, .. }) => only.clone(),
_ => Arc::new(self.new_node(NodeKind::Group { children, clip }, &f.gs)),
};
self.gs = f.gs;
self.pending_unread_fill = f.pending_unread_fill;
self.pending_unread_stroke = f.pending_unread_stroke;
self.after_object = f.after;
self.mesh_ended = f.mesh_ended;
let Some(Frame { kind: Kind::Obj(o), .. }) = self.frames.last_mut() else {
return Err("mask art ends outside a mask dictionary".into());
};
if o.art.replace(art).is_some() {
return Err("a mask has more than one art block".into());
}
o.unread = f.unread;
Ok(())
}
/// `;`: the innermost dictionary closes.
pub(super) fn end_obj(&mut self) -> Result<(), String> {
if !matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) {
return Ok(());
}
let Some(f) = self.frames.pop() else { return Ok(()) };
let Frame { kind, children, base, hidden, gs, pending_unread_fill, pending_unread_stroke, after, mesh_ended, state, .. } = f;
let Kind::Obj(mut o) = kind else { return Ok(()) };
obj::close(&mut o, &mut self.stack, base);
self.obj_hidden = hidden;
// Art made inside a dictionary (type's paths, a mask's art) isn't kept, and doesn't end what
// follows the object the dictionary is about: a mesh's state comes after its mask.
self.gs = gs;
self.pending_unread_fill = pending_unread_fill;
self.pending_unread_stroke = pending_unread_stroke;
if let Some(state) = state {
self.keep_state(state);
}
self.after_object = after;
self.mesh_ended = mesh_ended;
if matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(_), .. })) {
return self.push(V::Obj(Rc::new(*o)));
}
match o.ty.as_str() {
"ArtDictionary" => {
let envelope = o.get("AIDeformOptionsKey").is_some();
self.art_dictionary(&o);
let before = self.envelope_art.take();
if envelope {
let mut slots = vec![];
for child in &children {
envelope_text_slots(child, &mut slots);
}
let typed = !slots.is_empty() || before.as_ref().is_some_and(has_text_slot);
for mut slot in slots {
// The editing data keeps the source text inside the envelope's options;
// its visible result is the outlined art the PDF page carries.
slot.visible = false;
self.add(slot, false)?;
}
// An envelope of type keeps its drawn art and its type, hidden, as above: the
// type is filled in after reading, which the envelope can't wait for.
if typed {
return Ok(());
}
if self.envelopes.len() >= crate::import::ai::envelope::MAX_PENDING {
self.warn(crate::import::ai::envelope::DRAWN);
} else {
let siblings = self.frames.last().map_or(0, |f| f.children.len());
self.envelopes.push(PendingEnvelope { obj: *o, before, inner: children, depth: self.frames.len(), siblings });
}
}
}
"Document" => self.document(&o),
"AI11Text" => self.text_slot(&o)?,
"SymbolInstance" => self.symbol_instance(&o)?,
"ForeignObject" => self.foreign_art(&o)?,
_ => {}
}
Ok(())
}
/// Give `n` and its descendants ids of this document, and unlink their swatches.
pub(super) fn reid(&mut self, n: &mut Node) {
n.id = self.doc.alloc_id();
for it in &mut n.appearance.items {
let p = match it {
AppearanceItem::Fill(f) => &mut f.paint,
AppearanceItem::Stroke(s) => &mut s.paint,
};
if let Paint::Gradient(g) = p {
g.swatch = None;
}
p.map_links(&mut |_, link, _| {
*link = None;
true
});
}
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut()) {
self.reid(Arc::make_mut(&mut m.art));
}
if let Some(children) = n.children_mut() {
for c in children {
self.reid(Arc::make_mut(c));
}
}
}
/// Note something the data has that isn't read: an error on a layer that shows, left out of a
/// layer that doesn't.
pub(super) fn unreadable(&mut self, what: &str, hidden: bool) {
if (!hidden || self.in_mask()) && !self.gs.hidden && self.shown() {
if !self.defer_to_mask(what) && !self.mark_unread(what.to_string()) {
self.unsupported.insert(what.into());
}
} else {
self.hidden_unread.insert(what.into());
}
}
/// Keep what an opacity mask's art doesn't read with the mask, until its dictionary says
/// whether the mask is enabled. Whether art of a mask is being read. Operators it doesn't know
/// are kept up to 16, as [`Reader::note_unknown`] keeps them.
fn defer_to_mask(&mut self, what: &str) -> bool {
let Some(f) = self.frames.iter_mut().rev().find(|f| matches!(f.kind, Kind::Mask)) else { return false };
let full = what.starts_with('`') && f.unread.iter().filter(|w| w.starts_with('`')).count() >= 16;
if !full && !f.unread.iter().any(|w| w == what) {
f.unread.push(what.into());
}
true
}
/// Mark the first object container below the innermost layer.
pub(super) fn mark_unread(&mut self, what: String) -> bool {
let Some(layer) = self.frames.iter().rposition(|f| matches!(f.kind, Kind::Layer(_))) else { return false };
let Some(frame) = self.frames.get_mut(layer + 1).filter(|f| matches!(f.kind, Kind::Group | Kind::Wrap | Kind::Compound | Kind::Clip)) else {
return false;
};
if !frame.unread.contains(&what) {
frame.unread.push(what);
}
true
}
/// Stand in for one unread object with a box that the page can match.
pub(super) fn unread_box(&mut self, what: &str, bounds: super::Rect, hidden: bool) -> Result