mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
Merge #6504: M5: comments without an appearance read style values given by reference, and a callout without /RD is drawn (view.render-annotation-appearances)
# Conflicts: # crates/annot/src/draw.rs # parity/acrobat-features.toml
This commit is contained in:
commit
b75ca7539d
6 files changed
+413
-43
No files matched your search
@@ -4,7 +4,9 @@
|
||||
//! objects it points at, so the same code serves new comments and restyled ones. Drawings are in page space with
|
||||
//! `/BBox = /Rect` (identity matrix), except note icons, which are drawn in a 20 × 20 box.
|
||||
//! It returns `None` for anything it cannot draw faithfully (cloudy borders, unknown line
|
||||
//! endings), so callers never replace an appearance with a worse one.
|
||||
//! endings), so callers never replace an appearance with a worse one. It resolves `/QuadPoints`
|
||||
//! itself; for the other values, callers that hold the document follow references first
|
||||
//! (`draw::with_direct_values`).
|
||||
//!
|
||||
//! The note icons are PdfCraft's own drawings (AGENTS.md §1). Text boxes use the standard
|
||||
//! Helvetica font with WinAnsi encoding, and draw Thai from the craft-fonts Thai face as Type 3
|
||||
@@ -1296,7 +1298,11 @@ fn draw_free_text(context: &AppearanceContext<'_>, mut drawing: AppearanceDrawin
|
||||
let mut rect = rect;
|
||||
if d.contains(b"CL") {
|
||||
let cl = nums(d, b"CL").filter(|l| l.len() == 4 || l.len() == 6)?;
|
||||
let rd = nums(d, b"RD").filter(|r| r.len() == 4 && r.iter().all(|x| *x >= 0.0))?;
|
||||
// `/RD` is optional (Table 177; null is absent, §7.3.9): without it the text box is `/Rect`.
|
||||
let rd = match d.get(b"RD") {
|
||||
None | Some(Object::Null) => vec![0.0; 4],
|
||||
Some(_) => nums(d, b"RD").filter(|r| r.len() == 4 && r.iter().all(|x| *x >= 0.0))?,
|
||||
};
|
||||
rect = [full[0] + rd[0], full[1] + rd[1], full[2] - rd[2], full[3] - rd[3]];
|
||||
if rect[2] - rect[0] < 1.0 || rect[3] - rect[1] < 1.0 {
|
||||
return None;
|
||||
|
||||
+204
-31
@@ -34,8 +34,16 @@ pub(crate) fn draw_appearance(doc: &mut Document, r: ObjRef) -> Result<(), Annot
|
||||
let d = annot_dict(doc, r);
|
||||
let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned();
|
||||
let page = appearance::is_drawn_from_points(&d).then(|| page_box_listing(doc, r)).flatten();
|
||||
let Some(stream) = build_appearance(doc, &d, page) else { return Err(AnnotError::Unsupported(subtype)) };
|
||||
apply_appearance(doc, r, &d, stream)
|
||||
let direct = with_direct_values(doc, &d);
|
||||
let Some(stream) = build_appearance(doc, &d, &direct, page) else { return Err(AnnotError::Unsupported(subtype)) };
|
||||
let built = Built::new(stream, &direct);
|
||||
apply_appearance(doc, r, &d, built)
|
||||
}
|
||||
|
||||
/// Whether [`set_appearance`] can draw the annotation `d`: read as the display reads it, with
|
||||
/// references followed and the same fallback. Edits check this before changing anything.
|
||||
pub(crate) fn can_draw(doc: &Document, d: &Dict) -> bool {
|
||||
build_appearance(doc, d, &with_direct_values(doc, d), None).is_some()
|
||||
}
|
||||
|
||||
/// The visible region (`Page::crop`) of the page that lists `r` among its annotations: the page
|
||||
@@ -77,11 +85,18 @@ fn page_with_inherited_boxes(doc: &Document, page: ObjRef) -> pdfcraft_model::Pa
|
||||
}
|
||||
|
||||
/// Draw the appearance for an annotation dictionary on a page whose visible region is `page`
|
||||
/// (used by annotations drawn through points of their own, [`appearance::build_on_page`]): the expensive half of [`set_appearance`].
|
||||
/// Pure from the dictionary, the objects it points at and the page, so [`AppearanceCache`] can
|
||||
/// reuse it.
|
||||
fn build_appearance(doc: &Document, d: &Dict, page: Option<[f64; 4]>) -> Option<Stream> {
|
||||
let mut stream = appearance::build_on_page(doc, d, page)?;
|
||||
/// (used by annotations drawn through points of their own, [`appearance::build_on_page`]): the
|
||||
/// expensive half of [`set_appearance`]. `direct` is `d` with references followed
|
||||
/// ([`with_direct_values`]). Pure from the dictionary, the objects it points at and the page, so
|
||||
/// [`AppearanceCache`] can reuse it.
|
||||
fn build_appearance(doc: &Document, d: &Dict, direct: &Dict, page: Option<[f64; 4]>) -> Option<Stream> {
|
||||
// References followed first. Where that turns up something the builder doesn't draw (a
|
||||
// cloudy `/BE` on a square), the drawing made without them stays, rather than none.
|
||||
let mut stream = match appearance::build_on_page(doc, direct, page) {
|
||||
Some(stream) => stream,
|
||||
None if !std::ptr::eq(direct, d) => appearance::build_on_page(doc, d, page)?,
|
||||
None => return None,
|
||||
};
|
||||
// Picture-stamp restyling must retain the placement's page-axis correction.
|
||||
if d.get(b"PCPicture").is_some() {
|
||||
let matrix = d.get(b"AP").and_then(|ap| {
|
||||
@@ -97,12 +112,170 @@ fn build_appearance(doc: &Document, d: &Dict, page: Option<[f64; 4]>) -> Option<
|
||||
Some(stream)
|
||||
}
|
||||
|
||||
/// Entries that link the annotation to other objects rather than describe how it looks: left as
|
||||
/// references by [`with_direct_values`].
|
||||
const LINK_KEYS: [&[u8]; 5] = [b"P", b"Popup", b"IRT", b"Parent", b"AP"];
|
||||
|
||||
/// How deep inside an entry [`with_direct_values`] follows references: `/InkList` is an array of
|
||||
/// arrays, `/Border` may end in a dash array.
|
||||
const INLINE_DEPTH: usize = 4;
|
||||
|
||||
/// Values [`with_direct_values`] copies out of referenced arrays and dictionaries, or walks
|
||||
/// before finding one too large to copy, for one annotation, so a file can't have a large object
|
||||
/// copied or scanned for every entry that points at it.
|
||||
pub(crate) const INLINE_BUDGET: usize = 1 << 16;
|
||||
|
||||
/// Values copied or walked for all the annotations one display pass draws
|
||||
/// ([`with_missing_appearances_cached`]), so a large object that many annotations point at is
|
||||
/// not copied, or scanned and found too large, for each of them without bound. Past it,
|
||||
/// references are left as they are.
|
||||
pub(crate) const PASS_BUDGET: usize = 1 << 22;
|
||||
|
||||
/// `d` with the references among the values [`appearance::build`] reads followed. Any value may be
|
||||
/// given by reference (ISO 32000-2 §7.3.10): a border style dictionary (`/BS 5 0 R`, Table 168), a
|
||||
/// colour, `/RD`, `/CL`, a `/W` inside `/BS`. The builder reads only direct values (apart from
|
||||
/// `/QuadPoints`, which it resolves itself), so it draws from this copy. The entries in
|
||||
/// [`LINK_KEYS`] and streams stay references; past [`INLINE_DEPTH`] levels or [`INLINE_BUDGET`]
|
||||
/// copied values a reference is left as it is, which the builder treats as unreadable. `d` itself
|
||||
/// when nothing is a reference.
|
||||
pub(crate) fn with_direct_values<'d>(doc: &Document, d: &'d Dict) -> std::borrow::Cow<'d, Dict> {
|
||||
with_direct_values_within(doc, d, &mut INLINE_BUDGET.clone())
|
||||
}
|
||||
|
||||
/// [`with_direct_values`], copying at most [`INLINE_BUDGET`] values and no more than `pass` has
|
||||
/// left, which is reduced by what was copied.
|
||||
pub(crate) fn with_direct_values_within<'d>(doc: &Document, d: &'d Dict, pass: &mut usize) -> std::borrow::Cow<'d, Dict> {
|
||||
let allowed = INLINE_BUDGET.min(*pass);
|
||||
let mut budget = allowed;
|
||||
let mut out: Option<Dict> = None;
|
||||
for (key, value) in d.iter() {
|
||||
if LINK_KEYS.contains(&key.as_slice()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(direct) = direct_value(doc, value, INLINE_DEPTH, &mut budget) {
|
||||
out.get_or_insert_with(|| d.clone()).set(key.clone(), direct);
|
||||
}
|
||||
}
|
||||
*pass = pass.saturating_sub(allowed.saturating_sub(budget));
|
||||
out.map_or(std::borrow::Cow::Borrowed(d), std::borrow::Cow::Owned)
|
||||
}
|
||||
|
||||
/// The number of values in `o`, counting into its arrays and dictionaries (not through
|
||||
/// references, which are charged when followed), or, past `limit`, `Err` with the values it
|
||||
/// visited or queued before finding that out (at least one), so a rejected walk is charged too.
|
||||
/// Walks at most `limit` values and holds no more than `limit` waiting.
|
||||
fn weight(o: &Object, limit: usize) -> Result<usize, usize> {
|
||||
let mut count = 0usize;
|
||||
let mut stack = vec![o];
|
||||
while let Some(o) = stack.pop() {
|
||||
let spent = count.saturating_add(stack.len()).max(1);
|
||||
count = match count.checked_add(1).filter(|c| *c <= limit) {
|
||||
Some(c) => c,
|
||||
None => return Err(spent),
|
||||
};
|
||||
let inner = match o {
|
||||
Object::Array(items) => items.len(),
|
||||
Object::Dict(entries) => entries.len(),
|
||||
_ => continue,
|
||||
};
|
||||
// Every value waiting is counted later, so more than `limit` in all is over it already.
|
||||
if count.saturating_add(stack.len()).saturating_add(inner) > limit {
|
||||
return Err(spent.saturating_add(1));
|
||||
}
|
||||
match o {
|
||||
Object::Array(items) => stack.extend(items.iter()),
|
||||
Object::Dict(entries) => stack.extend(entries.iter().map(|(_, v)| v)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// `o` with references followed `depth` levels into arrays and dictionaries, or `None` when there
|
||||
/// is nothing to follow (or it may not be: a stream, an over-budget object, a chain of references).
|
||||
fn direct_value(doc: &Document, o: &Object, depth: usize, budget: &mut usize) -> Option<Object> {
|
||||
let held;
|
||||
let (base, followed) = match o {
|
||||
Object::Ref(_) => {
|
||||
// A spent budget follows nothing more, not even to find out what is there.
|
||||
if *budget == 0 {
|
||||
return None;
|
||||
}
|
||||
held = doc.resolve(o);
|
||||
if matches!(&*held, Object::Stream(_) | Object::Ref(_)) {
|
||||
return None;
|
||||
}
|
||||
// Everything copied counts, inner arrays and dictionaries too, and so does the walk
|
||||
// that finds an object too large: many references to one would otherwise each walk
|
||||
// it again for free.
|
||||
match weight(&held, *budget) {
|
||||
Ok(n) => *budget = budget.checked_sub(n)?,
|
||||
Err(spent) => {
|
||||
*budget = budget.saturating_sub(spent);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
(&*held, true)
|
||||
}
|
||||
other => (other, false),
|
||||
};
|
||||
let Some(next) = depth.checked_sub(1) else { return followed.then(|| base.clone()) };
|
||||
match base {
|
||||
Object::Array(items) => {
|
||||
let mut changed: Option<Vec<Object>> = None;
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if let Some(direct) = direct_value(doc, item, next, budget) {
|
||||
let copy = changed.get_or_insert_with(|| items.clone());
|
||||
if let Some(slot) = copy.get_mut(i) {
|
||||
*slot = direct;
|
||||
}
|
||||
}
|
||||
}
|
||||
changed.map(Object::Array).or_else(|| followed.then(|| base.clone()))
|
||||
}
|
||||
Object::Dict(entries) => {
|
||||
let mut changed: Option<Dict> = None;
|
||||
for (key, value) in entries.iter() {
|
||||
if let Some(direct) = direct_value(doc, value, next, budget) {
|
||||
changed.get_or_insert_with(|| entries.clone()).set(key.clone(), direct);
|
||||
}
|
||||
}
|
||||
changed.map(Object::Dict).or_else(|| followed.then(|| base.clone()))
|
||||
}
|
||||
_ => followed.then(|| base.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A built appearance and what storing it needs from the dictionary it was drawn from, read
|
||||
/// with references followed, so a cached one is stored without following them again.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Built {
|
||||
stream: Stream,
|
||||
/// The first character the standard fonts can't draw (it came out as `?`).
|
||||
missing: Option<char>,
|
||||
/// A note's or attachment's `/Rect`, as written.
|
||||
rect: Option<Vec<f64>>,
|
||||
/// Points (/QuadPoints, /L, /Vertices, /InkList) past /Rect: its appearance covers them, so
|
||||
/// /Rect does too.
|
||||
markup_rect: Option<[f64; 4]>,
|
||||
}
|
||||
|
||||
impl Built {
|
||||
fn new(stream: Stream, direct: &Dict) -> Self {
|
||||
let missing = appearance::undrawable_text(direct).map(|(_, c)| c);
|
||||
let rect = matches!(direct.name(b"Subtype"), Some(b"Text" | b"FileAttachment"))
|
||||
.then(|| direct.get(b"Rect")?.as_array()?.iter().map(|o| o.as_f64()).collect::<Option<Vec<f64>>>())
|
||||
.flatten();
|
||||
let markup_rect = appearance::widened_rect(direct, &stream);
|
||||
Built { stream, missing, rect, markup_rect }
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a built appearance on the annotation: the cheap half of [`set_appearance`].
|
||||
fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, mut stream: Stream) -> Result<(), AnnotError> {
|
||||
// Characters the standard fonts can't draw came out as `?`: mark the annotation with the first.
|
||||
let missing = appearance::undrawable_text(d).map(|(_, c)| c);
|
||||
// Points (/QuadPoints, /L, /Vertices, /InkList) past /Rect: its appearance covers them, so /Rect does too.
|
||||
let markup_rect = appearance::widened_rect(d, &stream);
|
||||
fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, built: Built) -> Result<(), AnnotError> {
|
||||
// `missing`: characters the standard fonts can't draw came out as `?`; the annotation is
|
||||
// marked with the first.
|
||||
let Built { mut stream, missing, rect, markup_rect } = built;
|
||||
// A text box's Thai glyphs are streams of their own.
|
||||
pdfcraft_fonts::shaped_text::store_streams(doc, &mut stream.dict);
|
||||
let ap = doc.add(Object::Stream(stream));
|
||||
@@ -115,14 +288,10 @@ fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, mut stream: Stream)
|
||||
// A note icon has a fixed size (`NOTE_SIZE`); viewers draw it at the upper-left corner of `/Rect`.
|
||||
// The appearance's box is mapped onto `/Rect`, so any other `/Rect` (a large one, or one with
|
||||
// no width, as LaTeX `pdfcomment` writes) would stretch the icon or shrink it to nothing.
|
||||
let icon_rect = matches!(d.name(b"Subtype"), Some(b"Text" | b"FileAttachment"))
|
||||
.then(|| d.get(b"Rect")?.as_array()?.iter().map(|o| o.as_f64()).collect::<Option<Vec<f64>>>())
|
||||
.flatten()
|
||||
.filter(|q| q.len() == 4 && finite(q))
|
||||
.map(|q| {
|
||||
let (x, top) = (q[0].min(q[2]), q[1].max(q[3]));
|
||||
[x, top - NOTE_SIZE, x + NOTE_SIZE, top]
|
||||
});
|
||||
let icon_rect = rect.filter(|q| q.len() == 4 && finite(q)).map(|q| {
|
||||
let (x, top) = (q[0].min(q[2]), q[1].max(q[3]));
|
||||
[x, top - NOTE_SIZE, x + NOTE_SIZE, top]
|
||||
});
|
||||
doc.update_dict(r, |d| {
|
||||
d.set(b"AP".to_vec(), Object::Dict(apd));
|
||||
if let Some(rect) = icon_rect.or(markup_rect) {
|
||||
@@ -148,7 +317,7 @@ fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, mut stream: Stream)
|
||||
pub struct AppearanceCache {
|
||||
/// Each appearance with the page region it was drawn for (see [`appearance::build_on_page`]):
|
||||
/// a changed crop box draws it again.
|
||||
entries: std::collections::HashMap<ObjRef, (Option<[f64; 4]>, Stream)>,
|
||||
entries: std::collections::HashMap<ObjRef, (Option<[f64; 4]>, Built)>,
|
||||
bytes: usize,
|
||||
hits: u64,
|
||||
builds: u64,
|
||||
@@ -163,20 +332,21 @@ impl AppearanceCache {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
fn get(&mut self, r: ObjRef, crop: Option<[f64; 4]>) -> Option<Stream> {
|
||||
let stream = self.entries.get(&r).filter(|(drawn_for, _)| *drawn_for == crop)?.1.clone();
|
||||
fn get(&mut self, r: ObjRef, crop: Option<[f64; 4]>) -> Option<Built> {
|
||||
let built = self.entries.get(&r).filter(|(drawn_for, _)| *drawn_for == crop)?.1.clone();
|
||||
self.hits += 1;
|
||||
Some(stream)
|
||||
Some(built)
|
||||
}
|
||||
|
||||
fn insert(&mut self, r: ObjRef, crop: Option<[f64; 4]>, stream: &Stream) {
|
||||
fn insert(&mut self, r: ObjRef, crop: Option<[f64; 4]>, built: &Built) {
|
||||
let stream = &built.stream;
|
||||
self.builds += 1;
|
||||
if self.entries.len() >= 1_000_000 {
|
||||
self.clear();
|
||||
}
|
||||
if self.bytes.saturating_add(stream.raw.len()) <= Self::MAX_BYTES {
|
||||
self.bytes += stream.raw.len();
|
||||
self.entries.insert(r, (crop, stream.clone()));
|
||||
self.entries.insert(r, (crop, built.clone()));
|
||||
}
|
||||
// Else over budget: keep serving the entries already cached instead of growing.
|
||||
}
|
||||
@@ -212,6 +382,7 @@ pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCac
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
// Pages' visible regions, read once and only when an annotation drawn through its points is.
|
||||
let mut crops: Option<std::collections::HashMap<ObjRef, [f64; 4]>> = None;
|
||||
let mut budget = PASS_BUDGET;
|
||||
for page in page_refs(doc).ok()? {
|
||||
for r in annots(doc, page).iter().filter_map(Object::as_ref) {
|
||||
if !seen.insert(r) {
|
||||
@@ -228,9 +399,9 @@ pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCac
|
||||
let crop = appearance::is_drawn_from_points(&d)
|
||||
.then(|| crops.get_or_insert_with(|| pdfcraft_model::pages(doc).iter().map(|p| (p.obj, p.crop(doc))).collect()).get(&page).copied())
|
||||
.flatten();
|
||||
if let Some(stream) = cache.get(r, crop) {
|
||||
if let Some(built) = cache.get(r, crop) {
|
||||
// Fails only when `r` isn't a dictionary, which the cached build ruled out.
|
||||
apply_appearance(copy.get_or_insert_with(|| doc.clone()), r, &d, stream).ok();
|
||||
apply_appearance(copy.get_or_insert_with(|| doc.clone()), r, &d, built).ok();
|
||||
continue;
|
||||
}
|
||||
// Another program's stamp gets a drawing for display only (ISO 32000-2 Table 184). Its
|
||||
@@ -240,10 +411,12 @@ pub fn with_missing_appearances_cached(doc: &Document, cache: &mut AppearanceCac
|
||||
bare.remove(b"AP");
|
||||
appearance::display_stamp(&bare)
|
||||
};
|
||||
let Some(stream) = build_appearance(doc, &d, crop).or_else(other_stamp) else { continue };
|
||||
cache.insert(r, crop, &stream);
|
||||
let direct = with_direct_values_within(doc, &d, &mut budget);
|
||||
let Some(stream) = build_appearance(doc, &d, &direct, crop).or_else(other_stamp) else { continue };
|
||||
let built = Built::new(stream, &direct);
|
||||
cache.insert(r, crop, &built);
|
||||
// Fails only when `r` isn't a dictionary, which the build just ruled out.
|
||||
apply_appearance(copy.get_or_insert_with(|| doc.clone()), r, &d, stream).ok();
|
||||
apply_appearance(copy.get_or_insert_with(|| doc.clone()), r, &d, built).ok();
|
||||
}
|
||||
}
|
||||
copy
|
||||
|
||||
@@ -245,7 +245,10 @@ pub fn props(doc: &Document, page: usize, index: usize) -> Option<Props> {
|
||||
let p = page_ref(doc, page).ok()?;
|
||||
let entry = annots(doc, p).get(index).cloned()?;
|
||||
let obj = doc.resolve(&entry);
|
||||
let d = obj.as_dict()?;
|
||||
// Values given by reference (`/C 7 0 R`, `/BS 5 0 R`, ISO 32000-2 §7.3.10) read as the display draws them.
|
||||
let raw = obj.as_dict()?;
|
||||
let direct = crate::draw::with_direct_values(doc, raw);
|
||||
let d = direct.as_ref();
|
||||
let subtype = String::from_utf8_lossy(d.name(b"Subtype")?).into_owned();
|
||||
let color = if subtype == "FreeText" { Some(appearance::parse_da(d).0) } else { drawn_color(d, b"C") };
|
||||
let width = matches!(subtype.as_str(), "Square" | "Circle" | "Line" | "Ink" | "Polygon" | "PolyLine" | "FreeText").then(|| {
|
||||
@@ -263,7 +266,7 @@ pub fn props(doc: &Document, page: usize, index: usize) -> Option<Props> {
|
||||
width,
|
||||
icon: (subtype == "Text").then(|| d.name(b"Name").and_then(|n| NoteIcon::from_name(&String::from_utf8_lossy(n))).unwrap_or(NoteIcon::Note)),
|
||||
modified: text_value(doc, d, b"M"),
|
||||
restylable: appearance::build(doc, d).is_some(),
|
||||
restylable: crate::draw::can_draw(doc, raw),
|
||||
locked: annotation_flags(doc, d) & FLAG_LOCKED != 0,
|
||||
fillable: fillable(&subtype),
|
||||
fill: fill_of(d),
|
||||
|
||||
@@ -785,7 +785,7 @@ fn style_one(doc: &mut Document, r: ObjRef, change: StyleChange, meta: &Meta) ->
|
||||
let d = annot_dict(doc, r);
|
||||
let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned();
|
||||
// Check before changing anything: a stale appearance would contradict the new style.
|
||||
if appearance::build(doc, &d).is_none() {
|
||||
if !crate::draw::can_draw(doc, &d) {
|
||||
return Err(AnnotError::Unsupported(subtype.clone()));
|
||||
}
|
||||
if color.is_some_and(|c| !finite(&c)) || opacity.is_some_and(|o| !o.is_finite()) || width.is_some_and(|w| !w.is_finite()) {
|
||||
@@ -805,8 +805,12 @@ fn style_one(doc: &mut Document, r: ObjRef, change: StyleChange, meta: &Meta) ->
|
||||
// So does a text box inside an oval or a circle, which goes around it again.
|
||||
let shape = if free_text { BorderShape::of(&d) } else { BorderShape::Rectangle };
|
||||
let typewriter = free_text && (d.name(b"IT") == Some(b"FreeTextTypeWriter") || shape != BorderShape::Rectangle) && !d.contains(b"CL");
|
||||
// A border style given by reference (`/BS 5 0 R`, ISO 32000-2 §7.3.10): its width and other
|
||||
// entries, which a new width keeps.
|
||||
let direct = crate::draw::with_direct_values(doc, &d);
|
||||
let (old_width, old_bs) = (border_width_of(&direct), direct.get(b"BS").and_then(Object::as_dict).cloned());
|
||||
let regrow = width.filter(|_| typewriter).and_then(|w| {
|
||||
let by = w.max(0.0) - if d.contains(b"BS") || d.contains(b"Border") { border_width_of(&d) } else { 0.0 };
|
||||
let by = w.max(0.0) - if d.contains(b"BS") || d.contains(b"Border") { old_width } else { 0.0 };
|
||||
let rect = grow(text_box_of(&d)?, by);
|
||||
(by != 0.0 && finite(&rect) && rect[2] - rect[0] >= 1.0 && rect[3] - rect[1] >= 1.0).then_some((rect, w.max(0.0)))
|
||||
});
|
||||
@@ -830,9 +834,9 @@ fn style_one(doc: &mut Document, r: ObjRef, change: StyleChange, meta: &Meta) ->
|
||||
set_part_keys(d, appearance::PartOpacity { line: o, fill: o, text: o });
|
||||
}
|
||||
if let Some(w) = width {
|
||||
let old_w = border_width_of(d);
|
||||
let old_w = old_width;
|
||||
let w = w.max(0.0);
|
||||
let mut bs = d.get(b"BS").and_then(|b| b.as_dict()).cloned().unwrap_or_default();
|
||||
let mut bs = old_bs.unwrap_or_default();
|
||||
bs.set(b"W".to_vec(), Object::Real(w));
|
||||
d.set(b"BS".to_vec(), Object::Dict(bs));
|
||||
d.remove(b"Border");
|
||||
@@ -946,7 +950,7 @@ pub fn set_fill(doc: &mut Document, page: usize, index: usize, fill: Option<Rgb>
|
||||
return Err(AnnotError::Invalid("only rectangles, ovals, polygons and text boxes can be filled".into()));
|
||||
}
|
||||
// Check before changing anything: a stale appearance would contradict the new fill.
|
||||
if appearance::build(doc, &d).is_none() {
|
||||
if !crate::draw::can_draw(doc, &d) {
|
||||
return Err(AnnotError::Unsupported(subtype));
|
||||
}
|
||||
if fill.is_some_and(|c| !finite(&c)) {
|
||||
@@ -987,7 +991,7 @@ pub fn set_part_opacity(doc: &mut Document, page: usize, index: usize, fill: Opt
|
||||
return Err(AnnotError::Invalid("invalid opacity".into()));
|
||||
}
|
||||
// Check before changing anything: a stale appearance would contradict the new opacity.
|
||||
if appearance::build(doc, &d).is_none() {
|
||||
if !crate::draw::can_draw(doc, &d) {
|
||||
return Err(AnnotError::Unsupported(subtype));
|
||||
}
|
||||
refuse_foreign_text(doc, &d)?;
|
||||
|
||||
@@ -1530,6 +1530,190 @@ fn malformed_optional_interior_color_does_not_hide_shape_strokes() {
|
||||
assert!(list(&doc, 0)[0].get(b"AP").is_none(), "display generation leaves the source document unchanged");
|
||||
}
|
||||
|
||||
/// Any value may be given by reference (ISO 32000-2 §7.3.10). A Square whose border style
|
||||
/// dictionary (Table 168), colour and dash array are indirect objects is drawn with them, not
|
||||
/// with the 1 pt solid default; a reference cycle inside `/BS` is not followed for ever.
|
||||
#[test]
|
||||
fn indirect_border_style_and_colour_are_drawn() {
|
||||
let doc = document_of(&[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R] >>",
|
||||
b"<< /Type /Annot /Subtype /Square /P 3 0 R /Rect [20 20 180 180] /BS 5 0 R /C 6 0 R >>",
|
||||
b"<< /Type /Border /W 7 0 R /S /D /D 8 0 R /Self 5 0 R >>",
|
||||
b"[0 0 1]",
|
||||
b"6",
|
||||
b"[4 2]",
|
||||
]);
|
||||
let display = with_missing_appearances(&doc).expect("the square gets a display appearance");
|
||||
let shown = list(&display, 0).remove(0);
|
||||
let content = ap_content(&display, &shown);
|
||||
assert!(content.contains("0 0 1 RG\n6 w\n[4 2] 0 d\n"), "the indirect /BS, /W, /D and /C are used: {content}");
|
||||
assert!(content.contains("23 23 154 154 re"), "the 6 pt border is inset by half its width: {content}");
|
||||
assert_eq!(shown.get(b"BS"), Some(&Object::Ref(ObjRef::new(5, 0))), "the display copy keeps the reference");
|
||||
assert_eq!(shown.get(b"P"), Some(&Object::Ref(ObjRef::new(3, 0))));
|
||||
let props = props(&doc, 0, 0).expect("props");
|
||||
assert_eq!(props.width, Some(6.0), "the properties show the width drawn");
|
||||
}
|
||||
|
||||
/// A followed reference the builder can't draw (a cloudy border effect on a square, which it
|
||||
/// leaves alone) keeps the drawing made without it instead of none.
|
||||
#[test]
|
||||
fn an_undrawable_indirect_value_keeps_the_plain_drawing() {
|
||||
let doc = document_of(&[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R] >>",
|
||||
b"<< /Type /Annot /Subtype /Square /Rect [20 20 180 180] /BE 5 0 R /C [1 0 0] >>",
|
||||
b"<< /S /C /I 1 >>",
|
||||
]);
|
||||
let display = with_missing_appearances(&doc).expect("the square is still drawn");
|
||||
let shown = list(&display, 0).remove(0);
|
||||
assert!(ap_content(&display, &shown).contains("1 0 0 RG\n1 w\n"));
|
||||
}
|
||||
|
||||
/// Following references copies a bounded number of values: an indirect array past the budget
|
||||
/// stays a reference (and the comment is not drawn from it), a chain that never ends is not
|
||||
/// followed, and links to other objects are never copied.
|
||||
#[test]
|
||||
fn direct_values_are_bounded() {
|
||||
let mut doc = fixture();
|
||||
let huge = doc.add(Object::Array(vec![Object::Int(0); (1 << 16) + 1]));
|
||||
let chain = doc.add(Object::Null);
|
||||
doc.set(chain, Object::Ref(chain));
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Subtype".to_vec(), Object::name("Square"));
|
||||
d.set(b"Rect".to_vec(), Object::Ref(doc.add(num_array(&[0.0, 0.0, 10.0, 10.0]))));
|
||||
d.set(b"Vertices".to_vec(), Object::Ref(huge));
|
||||
d.set(b"IC".to_vec(), Object::Ref(chain));
|
||||
d.set(b"Popup".to_vec(), Object::Ref(huge));
|
||||
let direct = crate::draw::with_direct_values(&doc, &d);
|
||||
assert_eq!(direct.get(b"Rect").and_then(Object::as_array).map(Vec::len), Some(4), "a small array is followed");
|
||||
assert_eq!(direct.get(b"Vertices"), Some(&Object::Ref(huge)), "over the budget: left as a reference");
|
||||
assert_eq!(direct.get(b"IC"), Some(&Object::Ref(chain)), "a chain of references is left alone");
|
||||
assert_eq!(direct.get(b"Popup"), Some(&Object::Ref(huge)), "a link is not copied");
|
||||
let mut plain = Dict::new();
|
||||
plain.set(b"W".to_vec(), Object::Int(2));
|
||||
assert!(matches!(crate::draw::with_direct_values(&doc, &plain), std::borrow::Cow::Borrowed(_)), "nothing to follow: no copy");
|
||||
}
|
||||
|
||||
/// A new width for a comment whose border style is an indirect object (§7.3.10) keeps that
|
||||
/// dictionary's other entries (a dashed style stays dashed) and leaves the shared object alone.
|
||||
#[test]
|
||||
fn new_width_keeps_an_indirect_border_style() {
|
||||
let mut doc = document_of(&[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R] >>",
|
||||
b"<< /Type /Annot /Subtype /Square /Rect [20 20 180 180] /BS 5 0 R /C [1 0 0] >>",
|
||||
b"<< /W 6 /S /D /D [4 2] >>",
|
||||
]);
|
||||
set_style(&mut doc, 0, 0, StyleChange::new(None, None, Some(3.0), None), &meta("")).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let d = list(&doc, 0).remove(0);
|
||||
let bs = d.get(b"BS").and_then(Object::as_dict).expect("a direct /BS");
|
||||
assert_eq!(bs.get(b"W").and_then(Object::as_f64), Some(3.0));
|
||||
assert_eq!(bs.name(b"S"), Some(b"D".as_slice()), "the dashed style is kept");
|
||||
assert!(ap_content(&doc, &d).contains("3 w\n[4 2] 0 d\n"));
|
||||
let shared = doc.get(ObjRef::new(5, 0));
|
||||
assert_eq!(shared.as_dict().and_then(|b| b.get(b"W")).and_then(Object::as_f64), Some(6.0), "the shared object is unchanged");
|
||||
}
|
||||
|
||||
/// A colour given by reference (`/C 6 0 R`, §7.3.10) is read by the properties and the edits as
|
||||
/// the display draws it: the properties show it, the appearance can be restyled and filled.
|
||||
#[test]
|
||||
fn an_indirect_colour_reads_and_restyles_as_drawn() {
|
||||
let mut doc = document_of(&[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R] >>",
|
||||
b"<< /Type /Annot /Subtype /Square /Rect [20 20 180 180] /C 5 0 R /IC 6 0 R >>",
|
||||
b"[0 0 1]",
|
||||
b"[1 1 0]",
|
||||
]);
|
||||
let p = props(&doc, 0, 0).expect("props");
|
||||
assert_eq!((p.color, p.fill, p.restylable), (Some([0.0, 0.0, 1.0]), Some([1.0, 1.0, 0.0]), true));
|
||||
set_style(&mut doc, 0, 0, StyleChange::new(None, None, Some(3.0), None), &meta("")).expect("restylable");
|
||||
set_fill(&mut doc, 0, 0, Some([0.0, 1.0, 0.0]), &meta("")).expect("fillable");
|
||||
let d = list(&doc, 0).remove(0);
|
||||
let content = ap_content(&doc, &d);
|
||||
assert!(content.contains("0 0 1 RG") && content.contains("0 1 0 rg") && content.contains("3 w"), "{content}");
|
||||
}
|
||||
|
||||
/// References are followed into everything they point at, and all of it is counted: a referenced
|
||||
/// array of two arrays of 40,000 numbers is over the per-annotation budget. One display pass
|
||||
/// copies at most [`crate::draw::PASS_BUDGET`] values in all, so an array many annotations point
|
||||
/// at is not copied again for each of them.
|
||||
#[test]
|
||||
fn following_references_is_bounded_per_value_and_per_pass() {
|
||||
let mut doc = fixture();
|
||||
let inner = Object::Array(vec![Object::Int(0); 40_000]);
|
||||
let nested = doc.add(Object::Array(vec![inner.clone(), inner]));
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Subtype".to_vec(), Object::name("Square"));
|
||||
d.set(b"Junk".to_vec(), Object::Ref(nested));
|
||||
let direct = crate::draw::with_direct_values(&doc, &d);
|
||||
assert!(direct.get(b"Junk") == Some(&Object::Ref(nested)), "80,003 values: left as a reference");
|
||||
|
||||
let shared = doc.add(Object::Array(vec![Object::Int(0); 60_000]));
|
||||
d.set(b"Junk".to_vec(), Object::Ref(shared));
|
||||
let mut pass = crate::draw::PASS_BUDGET;
|
||||
let copies = (0..100).filter(|_| matches!(crate::draw::with_direct_values_within(&doc, &d, &mut pass), std::borrow::Cow::Owned(_))).count();
|
||||
assert_eq!(copies, crate::draw::PASS_BUDGET / 60_001, "copies stop when the pass budget is spent");
|
||||
}
|
||||
|
||||
/// Finding an object too large to copy costs the walk that found it out: an array of two arrays
|
||||
/// of 40,000 numbers that many annotations point at is walked about 40,000 values deep before it
|
||||
/// is rejected, and that is charged to the annotation and to the pass, so after a bounded number
|
||||
/// of such walks the pass has no room left to walk it again. A reference to it repeated in one annotation
|
||||
/// is not walked again in full, and a small referenced colour after it is still read.
|
||||
#[test]
|
||||
fn a_rejected_large_object_is_charged_to_the_budgets() {
|
||||
let mut doc = fixture();
|
||||
let inner = Object::Array(vec![Object::Int(0); 40_000]);
|
||||
let nested = doc.add(Object::Array(vec![inner.clone(), inner]));
|
||||
let red = doc.add(Object::Array(vec![Object::Int(1), Object::Int(0), Object::Int(0)]));
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Subtype".to_vec(), Object::name("Square"));
|
||||
d.set(b"Junk".to_vec(), Object::Array(vec![Object::Ref(nested); 1_000]));
|
||||
d.set(b"C".to_vec(), Object::Ref(red));
|
||||
let mut pass = crate::draw::PASS_BUDGET;
|
||||
let direct = crate::draw::with_direct_values_within(&doc, &d, &mut pass);
|
||||
assert_eq!(direct.get(b"C"), Some(&Object::Array(vec![Object::Int(1), Object::Int(0), Object::Int(0)])));
|
||||
let charged = crate::draw::PASS_BUDGET - pass;
|
||||
assert!((40_000..=crate::draw::INLINE_BUDGET).contains(&charged), "the rejected walk is charged once: {charged}");
|
||||
d.set(b"Junk".to_vec(), Object::Ref(nested));
|
||||
for _ in 0..crate::draw::PASS_BUDGET / 40_000 {
|
||||
crate::draw::with_direct_values_within(&doc, &d, &mut pass);
|
||||
}
|
||||
assert!(pass < 40_000, "after {} annotations the pass has no room for another full walk: {pass}", crate::draw::PASS_BUDGET / 40_000 + 1);
|
||||
}
|
||||
|
||||
/// `/RD` is optional (ISO 32000-2 Table 177): a callout without it has its text box at `/Rect`
|
||||
/// and is drawn, box, text and leader line. A callout line and `/RD` given by reference are read too.
|
||||
#[test]
|
||||
fn callout_without_rd_is_drawn() {
|
||||
let doc = document_of(&[
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 400 400] >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R] >>",
|
||||
b"<< /Type /Annot /Subtype /FreeText /IT /FreeTextCallout /Rect [100 100 300 160] /CL [20 20 60 60 100 130] /LE /OpenArrow /C [1 1 0] /DA (0 g /Helv 12 Tf) /Contents (callout) >>",
|
||||
b"<< /Type /Annot /Subtype /FreeText /IT /FreeTextCallout /Rect [10 200 300 300] /CL 7 0 R /RD 8 0 R /C [1 1 0] /DA (0 g /Helv 12 Tf) /Contents (indirect) >>",
|
||||
b"<< /Type /Annot /Subtype /FreeText /IT /FreeTextCallout /Rect [10 300 300 390] /CL [5 305 10 350] /RD [1 2] /C [1 1 0] /DA (0 g /Helv 12 Tf) /Contents (bad) >>",
|
||||
b"[15 205 40 250 100 250]",
|
||||
b"[90 0 0 0]",
|
||||
]);
|
||||
let display = with_missing_appearances(&doc).expect("the callouts get a display appearance");
|
||||
let shown = list(&display, 0);
|
||||
let plain = ap_content(&display, &shown[0]);
|
||||
assert!(plain.contains("1 1 0 rg\n100 100 200 60 re f"), "the yellow box fills /Rect: {plain}");
|
||||
assert!(plain.contains("20 20 m\n60 60 l\n100 130 l\nS"), "the leader line: {plain}");
|
||||
assert!(plain.contains("(callout) Tj"), "the text: {plain}");
|
||||
let indirect = ap_content(&display, &shown[1]);
|
||||
assert!(indirect.contains("100 200 200 100 re f") && indirect.contains("15 205 m"), "indirect /CL and /RD: {indirect}");
|
||||
assert!(shown[2].get(b"AP").is_none(), "a malformed /RD is still not guessed at");
|
||||
}
|
||||
|
||||
/// #3175: viewers draw a note without an appearance as a fixed-size icon at the upper-left corner
|
||||
/// of its `/Rect`. The display copy must not stretch the icon over a large `/Rect` or shrink it
|
||||
/// to nothing in one with no width.
|
||||
|
||||
@@ -818,8 +818,8 @@ milestone = "M2"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/render/src/raster/tests.rs::widget_appearance_states", "crates/render/src/raster/tests.rs::highlights_without_an_appearance_are_drawn", "crates/engine/src/tests.rs::comments_without_appearances_are_drawn_but_not_saved", "crates/engine/src/tests.rs::cli_display_adds_missing_appearances_without_changing_the_source", "crates/engine/src/tests.rs::fields_left_to_the_viewer_are_displayed_from_their_values", "crates/forms/src/tests.rs::need_appearances_draws_every_text_and_choice_field_in_the_display_copy", "crates/forms/src/tests.rs::the_field_appearance_cache_sees_every_input", "crates/engine/src/tests.rs::buttons_and_combs_left_to_the_viewer_are_displayed", "crates/forms/src/tests.rs::check_boxes_without_an_appearance_show_their_caption_symbol", "crates/forms/src/tests.rs::push_buttons_without_an_appearance_show_their_caption_and_frame", "crates/forms/src/tests.rs::need_appearances_draws_check_boxes_and_radio_buttons_from_their_captions", "crates/engine/src/tests.rs::need_appearances_buttons_are_displayed_from_their_captions", "crates/render/src/raster/tests.rs::invisible_unknown_annotations_and_popups_paint_nothing", "crates/print/src/tests.rs::invisible_annotations_of_unknown_types_do_not_print", "crates/engine/src/tests.rs::stamps_without_appearances_draw_the_stamp_they_name", "crates/automation/tests/it/automation.rs::page_render_draws_stamps_without_an_appearance", "crates/annot/src/tests.rs::a_four_entry_colour_with_a_null_is_cmyk_with_zero", "crates/forms/src/tests.rs::a_four_entry_mk_colour_with_a_null_is_cmyk_with_zero", "crates/annot/src/tests.rs::text_markup_whose_quads_reach_past_its_rect_is_drawn_over_them", "crates/annot/src/tests.rs::text_markup_reach_stops_at_the_page_and_the_quad_cap", "crates/annot/src/tests.rs::text_markup_reach_reads_quads_like_the_drawing", "crates/annot/src/tests.rs::ink_whose_strokes_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::polyline_whose_vertices_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::polygon_whose_vertices_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::line_whose_points_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::path_reach_stops_at_the_point_cap_and_is_saved_on_restyle"]
|
||||
notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup; text markup, lines, polylines, polygons and ink through their /QuadPoints, /L, /Vertices or /InkList where these reach past /Rect, as far as the crop box) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved; a four-entry colour with a non-number in it (/C, /IC, /MK /BC and /BG written [1 0 0 null]) is read as CMYK with that entry 0; a stamp from another program draws the stamp its /Name names (the fourteen standard names of ISO 32000-2 Table 184, Draft when there is none) or, for any other name, a grey frame with both diagonals; text and choice fields are drawn from their values when the form sets /NeedAppearances (every one) or a widget has no /N (#3470), also display-only and cached per widget (a field edit on a 1000-field /NeedAppearances form takes ~13 ms against ~7 ms on a form that needs none) (a read-only document still gets the renderer's Highlight fallback only); check boxes and radio buttons with no appearance for their /AS are drawn display-only from /MK (background, border, and when on the /MK /CA ZapfDingbats symbol in the /DA colour, a check or a dot by default), and when the form sets /NeedAppearances so is every one that has an /MK, over the appearance the file carries (ISO 32000-2 Table 224; one without /MK keeps its own), and push buttons with none from /MK and /BS with their caption; Link borders without /AP and printing are not covered yet; an annotation of a non-standard type with the Invisible flag (ISO 32000-2 Table 167) is neither drawn nor printed, and a popup's own /AP is never painted on the page (its parent's text shows in the popup window, §12.5.6.14)"
|
||||
evidence = ["crates/render/src/raster/tests.rs::widget_appearance_states", "crates/render/src/raster/tests.rs::highlights_without_an_appearance_are_drawn", "crates/engine/src/tests.rs::comments_without_appearances_are_drawn_but_not_saved", "crates/engine/src/tests.rs::cli_display_adds_missing_appearances_without_changing_the_source", "crates/engine/src/tests.rs::fields_left_to_the_viewer_are_displayed_from_their_values", "crates/forms/src/tests.rs::need_appearances_draws_every_text_and_choice_field_in_the_display_copy", "crates/forms/src/tests.rs::the_field_appearance_cache_sees_every_input", "crates/engine/src/tests.rs::buttons_and_combs_left_to_the_viewer_are_displayed", "crates/forms/src/tests.rs::check_boxes_without_an_appearance_show_their_caption_symbol", "crates/forms/src/tests.rs::push_buttons_without_an_appearance_show_their_caption_and_frame", "crates/forms/src/tests.rs::need_appearances_draws_check_boxes_and_radio_buttons_from_their_captions", "crates/engine/src/tests.rs::need_appearances_buttons_are_displayed_from_their_captions", "crates/render/src/raster/tests.rs::invisible_unknown_annotations_and_popups_paint_nothing", "crates/print/src/tests.rs::invisible_annotations_of_unknown_types_do_not_print", "crates/engine/src/tests.rs::stamps_without_appearances_draw_the_stamp_they_name", "crates/automation/tests/it/automation.rs::page_render_draws_stamps_without_an_appearance", "crates/annot/src/tests.rs::a_four_entry_colour_with_a_null_is_cmyk_with_zero", "crates/forms/src/tests.rs::a_four_entry_mk_colour_with_a_null_is_cmyk_with_zero", "crates/annot/src/tests.rs::text_markup_whose_quads_reach_past_its_rect_is_drawn_over_them", "crates/annot/src/tests.rs::text_markup_reach_stops_at_the_page_and_the_quad_cap", "crates/annot/src/tests.rs::text_markup_reach_reads_quads_like_the_drawing", "crates/annot/src/tests.rs::ink_whose_strokes_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::polyline_whose_vertices_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::polygon_whose_vertices_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::line_whose_points_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::path_reach_stops_at_the_point_cap_and_is_saved_on_restyle", "crates/annot/src/tests.rs::indirect_border_style_and_colour_are_drawn", "crates/annot/src/tests.rs::callout_without_rd_is_drawn"]
|
||||
notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup; text markup, lines, polylines, polygons and ink through their /QuadPoints, /L, /Vertices or /InkList where these reach past /Rect, as far as the crop box) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved, with style values given by reference (a /BS, colour, /RD or /CL as an indirect object, ISO 32000-2 §7.3.10) followed and a callout without /RD (optional, Table 177) boxed at its /Rect; a four-entry colour with a non-number in it (/C, /IC, /MK /BC and /BG written [1 0 0 null]) is read as CMYK with that entry 0; a stamp from another program draws the stamp its /Name names (the fourteen standard names of ISO 32000-2 Table 184, Draft when there is none) or, for any other name, a grey frame with both diagonals; text and choice fields are drawn from their values when the form sets /NeedAppearances (every one) or a widget has no /N (#3470), also display-only and cached per widget (a field edit on a 1000-field /NeedAppearances form takes ~13 ms against ~7 ms on a form that needs none) (a read-only document still gets the renderer's Highlight fallback only); check boxes and radio buttons with no appearance for their /AS are drawn display-only from /MK (background, border, and when on the /MK /CA ZapfDingbats symbol in the /DA colour, a check or a dot by default), and when the form sets /NeedAppearances so is every one that has an /MK, over the appearance the file carries (ISO 32000-2 Table 224; one without /MK keeps its own), and push buttons with none from /MK and /BS with their caption; Link borders without /AP and printing are not covered yet; an annotation of a non-standard type with the Invisible flag (ISO 32000-2 Table 167) is neither drawn nor printed, and a popup's own /AP is never painted on the page (its parent's text shows in the popup window, §12.5.6.14)"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-error-isolation"
|
||||
|
||||
Reference in new issue
Block a user