M10: a layer the document marks screen-only stays off the printed sheet (print.print-ready-pdf) (#6387)

This commit is contained in:
mtlcl410 authored and GitHub committed 2026-11-09 08:48:28 +08:00
1 parent 53eec5cd24
commit 1a28606c64
10 files changed
+334 -25

No files matched your search

+112 -3
View File
@@ -6,10 +6,102 @@ use pdfcraft_render::{Layer, LayerOp, RenderPool};
use crate::{Document, display_config, render_threads};
/// Most usage application dictionaries (`/AS` entries) that [`print_layer_states`] reads, and
/// most category names and groups it reads in all of them together (entries can share one
/// array, so the count is over every entry, not each).
const MAX_AUTO_STATES: usize = 1024;
const MAX_AUTO_ITEMS: usize = 65_536;
/// The usage dictionary entry and its state key that a `/Category` name of a usage application
/// dictionary reads when printing (ISO 32000-2 Table 101): Print, View and Export each name a
/// state of the group's own. Zoom, User and Language depend on the magnification, the user and
/// the system language, which printing doesn't fix, so they give no recommendation here.
fn print_category(name: &[u8]) -> Option<(&'static [u8], &'static [u8])> {
match name {
b"Print" => Some((b"Print", b"PrintState")),
b"View" => Some((b"View", b"ViewState")),
b"Export" => Some((b"Export", b"ExportState")),
_ => None,
}
}
/// The layers as they print: `layers` as shown, then the default configuration's auto state for
/// the Print event (ISO 32000-2 §8.11.4.4, Table 101). Each usage application dictionary
/// (`/AS` entry) whose `/Event` is `/Print` recommends, for each group in its `/OCGs` and each of
/// its categories ([`print_category`]), the state the group's usage dictionary gives (for the
/// Print category, `/Usage /Print /PrintState`). A group with recommendations is on only if all
/// of them, in all the entries naming it, are ON; other groups keep their state. `/Category` is
/// an array; a single name is read as one. `None` when no `/AS` entry is for printing.
pub(crate) fn print_layer_states(cos: &pdfcraft_cos::Document, layers: &[Layer]) -> Option<Vec<Layer>> {
use pdfcraft_cos::Object;
let root = cos.trailer().get(b"Root").map(|r| cos.resolve(r))?;
let properties = root.as_dict()?.get(b"OCProperties").map(|p| cos.resolve(p))?;
let config = properties.as_dict()?.get(b"D").map(|d| cos.resolve(d))?;
let auto = config.as_dict()?.get(b"AS").map(|a| cos.resolve(a))?;
let mut printing = false;
let mut recommended: std::collections::HashMap<(u32, u16), bool> = std::collections::HashMap::new();
let mut items_left = MAX_AUTO_ITEMS;
for entry in auto.as_array()?.iter().take(MAX_AUTO_STATES) {
let entry = cos.resolve(entry);
let Some(entry) = entry.as_dict() else { continue };
if entry.get(b"Event").map(|e| cos.resolve(e)).as_deref().and_then(Object::as_name) != Some(b"Print") {
continue;
}
printing = true;
let categories = entry.get(b"Category").map(|c| cos.resolve(c));
let mut read: Vec<(&[u8], &[u8])> = Vec::new();
let mut consider = |name: Option<&[u8]>| {
if let Some(category) = name.and_then(print_category).filter(|c| !read.contains(c)) {
read.push(category);
}
};
match categories.as_deref() {
Some(Object::Array(names)) => {
let count = names.len().min(items_left);
items_left -= count;
names.iter().take(count).for_each(|n| consider(cos.resolve(n).as_name()));
}
Some(Object::Name(n)) => consider(Some(n.as_slice())),
_ => {}
}
if read.is_empty() {
continue;
}
let Some(groups) = entry.get(b"OCGs").map(|g| cos.resolve(g)) else { continue };
for group in groups.as_array().into_iter().flatten() {
let Some(left) = items_left.checked_sub(1) else { break };
items_left = left;
let Some(id) = group.as_ref() else { continue };
let ocg = cos.resolve(group);
let usage = ocg.as_dict().and_then(|g| g.get(b"Usage").map(|u| cos.resolve(u)));
for (entry_key, state_key) in &read {
// No state given (a Print category without `/PrintState`): no recommendation, so
// the group is left as it is.
let state = usage
.as_deref()
.and_then(|u| u.as_dict().and_then(|u| u.get(entry_key).map(|e| cos.resolve(e))))
.and_then(|e| e.as_dict().and_then(|e| e.get(state_key).map(|s| cos.resolve(s))))
.and_then(|s| match s.as_name() {
Some(b"ON") => Some(true),
Some(b"OFF") => Some(false),
_ => None,
});
if let Some(on) = state {
let all_on = recommended.entry((id.num, id.generation)).or_insert(true);
*all_on = *all_on && on;
}
}
}
}
printing.then(|| layers.iter().map(|l| Layer { visible: recommended.get(&l.id).copied().unwrap_or(l.visible), ..l.clone() }).collect())
}
/// Write `layers`' visibility into the default optional-content configuration of `cos` (a copy
/// made for printing): `/BaseState /ON`, `/ON` the visible layers, `/OFF` the hidden ones. Its
/// other entries (`/Order`, `/RBGroups`, `/AS`…) are kept. A document without `/OCProperties`
/// is left alone.
/// made for printing): `/BaseState /ON`, `/ON` the visible layers, `/OFF` the hidden ones. The
/// states written are final, so what would set them again goes: the auto state (`/AS`), and the
/// layers' own `/Usage`, which some readers apply when they draw (a `/ViewState /ON` turns a layer
/// that prints off back on). The configuration's other entries (`/Order`, `/RBGroups`…) are kept.
/// A document without `/OCProperties` is left alone.
pub(crate) fn set_default_layer_state(cos: &mut pdfcraft_cos::Document, layers: &[Layer]) {
use pdfcraft_cos::{Dict, ObjRef, Object};
let Some(root_ref) = cos.trailer().get(b"Root").and_then(Object::as_ref) else { return };
@@ -22,18 +114,35 @@ pub(crate) fn set_default_layer_state(cos: &mut pdfcraft_cos::Document, layers:
config.set(b"BaseState".to_vec(), Object::name("ON"));
config.set(b"ON".to_vec(), refs(true));
config.set(b"OFF".to_vec(), refs(false));
config.remove(b"AS");
properties.set(b"D".to_vec(), Object::Dict(config));
root.set(b"OCProperties".to_vec(), Object::Dict(properties));
cos.set(root_ref, Object::Dict(root));
for layer in layers {
let id = ObjRef::new(layer.id.0, layer.id.1);
let Some(mut group) = cos.get(id).as_dict().cloned() else { continue };
if group.remove(b"Usage").is_some() {
cos.set(id, Object::Dict(group));
}
}
}
/// Render `doc` with the layer visibility in its `info.layers`.
pub(crate) fn use_layer_choices(doc: &mut Document) {
let overrides: Vec<(i32, i32, bool)> = doc.info.layers.iter().map(|l| (l.id.0 as i32, l.id.1 as i32, l.visible)).collect();
doc.config.layers = Arc::new(overrides);
doc.config.print_layers = print_preview_layers(doc.editor.as_ref().map(|e| &e.cos), &doc.info.layers);
doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), display_config(&doc.config));
}
/// The layer overrides a print preview draws with (`RenderConfig::print_layers`): the layers as
/// they print ([`print_layer_states`]), so the preview shows the page as it prints. `None` when
/// the document has no auto state for printing (or no structure to read it from).
pub(crate) fn print_preview_layers(cos: Option<&pdfcraft_cos::Document>, layers: &[Layer]) -> Option<Arc<Vec<(i32, i32, bool)>>> {
let printed = print_layer_states(cos?, layers)?;
Some(Arc::new(printed.iter().map(|l| (l.id.0 as i32, l.id.1 as i32, l.visible)).collect()))
}
/// Apply a set-layer-visibility action to `layers`, one change at a time, so a toggle flips the
/// state the changes before it left. Returns whether any layer ended up changed.
pub(crate) fn apply_layer_state(layers: &mut [Layer], groups: &[Vec<(u32, u16)>], changes: &[(LayerOp, (u32, u16))], preserve_rb: bool) -> bool {
+1 -1
View File
@@ -150,7 +150,7 @@ pub use edit::{BATCH_LABELS, EDIT_LABELS, Edit};
pub use error::{EditError, SourceProblem};
pub use fill_sign::{displayed_offset, text_box_at, typed_signature_graphic, typed_signature_shape};
pub use history::Label;
use layers::{apply_layer_state, set_default_layer_state, use_layer_choices};
use layers::{apply_layer_state, print_layer_states, print_preview_layers, set_default_layer_state, use_layer_choices};
pub use open::{OpenContext, PreparedDocument};
pub use page_filter::{PageOrientation, PageParity, filter_pages};
pub use pictures::{MarkFile, image_dimensions};
+5 -2
View File
@@ -8,7 +8,8 @@ use pdfcraft_render::{DocInfo, OpenError, RenderConfig, RenderPool, inspect};
use crate::history::{Editor, Keys, TextBlocksCache};
use crate::{
AppearanceCache, DocId, Document, Session, StatusCaptions, TrustStore, XFA_SCRIPTS_OFF, XfaLayout, display_bytes, display_config, guard,
info_fields, js, note_warnings, recovered_render_password, render_threads, same_pages, signatures_of, xfa, xfa_layout, xfa_values_from_datasets,
info_fields, js, note_warnings, print_preview_layers, recovered_render_password, render_threads, same_pages, signatures_of, xfa, xfa_layout,
xfa_values_from_datasets,
};
/// Immutable session state needed while a document is prepared away from its owning thread.
@@ -286,7 +287,7 @@ impl Session {
/// The last step of opening: build the document record and register it.
fn build_document(context: &OpenContext, data: DocumentOpenData<'_>) -> Document {
let DocumentOpenData { name, path, bytes, mut info, structure: cos, render_password, password, xfa } = data;
let config = RenderConfig { password: render_password.as_deref().map(Arc::from), ..Default::default() };
let mut config = RenderConfig { password: render_password.as_deref().map(Arc::from), ..Default::default() };
let (mut editor, read_only_reason) = match cos {
Ok(Ok(cos)) => {
let keys = Keys { render: render_password.clone(), reopen: password.map(str::to_owned) };
@@ -321,6 +322,8 @@ impl Session {
}
}
}
// The print preview draws the layers as they print.
config.print_layers = print_preview_layers(editor.as_ref().map(|e| &e.cos), &info.layers);
let display = display_bytes(editor.as_mut(), &bytes, &mut signatures, &context.captions);
let renderer = RenderPool::new(display.clone(), render_threads(), display_config(&config));
let (form, adopted) = editor
+8 -3
View File
@@ -11,7 +11,7 @@ use pdfcraft_print::{Content, Raster};
use pdfcraft_render::{PRINT_TAG, PageRenderer, RenderRequest};
use crate::export::encode_png;
use crate::{Document, guard};
use crate::{Document, guard, print_layer_states};
/// `doc`'s pages `pages` (0-based, each once, in the order given) as an image-only PDF, one
/// page per entry, the size of the source page as displayed, drawing what `content` prints.
@@ -22,11 +22,16 @@ pub fn rasterize(doc: &Document, pages: &[usize], raster: &Raster, content: Cont
let mut config = src.config.clone();
// The copy below holds only what prints, so the viewer's own comment hiding plays no part.
config.hide_comments = false;
let printed = guard(|| -> Result<Vec<u8>, String> {
let (printed, layers) = guard(|| -> Result<(Vec<u8>, Option<Vec<pdfcraft_render::Layer>>), String> {
let mut cos = pdfcraft_cos::Document::open_with_password(src.bytes.clone(), config.password.as_deref()).map_err(|e| e.to_string())?;
pdfcraft_print::keep_printed(&mut cos, content);
write_incremental(&cos, &SaveOptions::default()).map_err(|e| e.to_string())
// The layers as the vector print sets them: as shown, then the Print event's auto state.
let layers = print_layer_states(&cos, &doc.info.layers);
Ok((write_incremental(&cos, &SaveOptions::default()).map_err(|e| e.to_string())?, layers))
})??;
if let Some(layers) = layers {
config.layers = std::sync::Arc::new(layers.iter().map(|l| (l.id.0 as i32, l.id.1 as i32, l.visible)).collect());
}
let mut renderer = PageRenderer::new(std::sync::Arc::new(printed), config);
let mut images: Vec<(String, Vec<u8>)> = Vec::with_capacity(pages.len());
let mut used: Option<f64> = None;
+2 -2
View File
@@ -14,8 +14,8 @@ use crate::{
LOW_RESOLUTION_PRINT_DPI, Label, ObjectKind, OptimizeReport, RecoverySnapshot, SignOptions, SourceKind, SplitPart, StatusCaptions, SummarySort,
TrustStore, XFA_REMOVED_PREFIX, XFA_SCRIPTS_OFF, apply_layer_state, carry_verdict_marks, comment_list, comment_summary, decode_text,
display_bytes, display_config, guard, info_fields, js, local_utc_offset, note_warnings, open_source, open_source_with, optimize, outline_rows,
parse_ymd, print, print_raster, render_threads, scan_colours, set_default_layer_state, shown_under_redactions, signatures_of, source_kind,
sync_xfa_state, undrawn_notices, use_layer_choices, writable, xfa, xfa_sync_datasets,
parse_ymd, print, print_layer_states, print_raster, render_threads, scan_colours, set_default_layer_state, shown_under_redactions, signatures_of,
source_kind, sync_xfa_state, undrawn_notices, use_layer_choices, writable, xfa, xfa_sync_datasets,
};
mod editing;
+2 -1
View File
@@ -6,7 +6,7 @@ use super::{
inspect, js, keys_after, note_warnings, render_threads, run_edit, scan_colours, scope_of, shown_under_redactions, signatures_of, sync_xfa_state,
undrawn_notices, uses_scripts, write_incremental, xfa, xfa_sync_datasets,
};
use crate::{EditNotice, MarkKind};
use crate::{EditNotice, MarkKind, print_preview_layers};
/// Whether the edit, or any edit inside a batch, deletes pages.
fn deletes_pages(edit: &Edit) -> bool {
@@ -545,6 +545,7 @@ impl Session {
}
}
doc.info = info;
doc.config.print_layers = print_preview_layers(Some(&editor.cos), &doc.info.layers);
doc.fonts_stale = false;
let mut form = pdfcraft_forms::fields(&editor.cos);
xfa::mark_script_buttons(&editor.cos, &mut form);
+7 -3
View File
@@ -1,7 +1,7 @@
use super::{
Arc, CombineSource, Combined, DataFormat, DocId, EditError, Hidden, ImageResolution, LOW_RESOLUTION_PRINT_DPI, OptimizeReport, SaveOptions,
Session, SourceKind, SplitPart, decode_text, guard, open_source, open_source_with, optimize, print, print_raster, set_default_layer_state,
source_kind, writable, write_full,
Session, SourceKind, SplitPart, decode_text, guard, open_source, open_source_with, optimize, print, print_layer_states, print_raster,
set_default_layer_state, source_kind, writable, write_full,
};
impl Session {
@@ -167,7 +167,11 @@ impl Session {
// #3046: the layers print as they are shown. Once the Layers panel (or a set-layer
// action) has changed one, the print copy's default configuration says which are on, so a
// hidden layer stays off on paper too; untouched, the document's own configuration prints.
if !doc.config.layers.is_empty() {
// On top of either, the configuration's auto state for the Print event sets the layers it
// names (`print_layer_states`), as printing does in a viewer (ISO 32000-2 §8.11.4.4).
if let Some(printed) = print_layer_states(&cos, &doc.info.layers) {
set_default_layer_state(&mut cos, &printed);
} else if !doc.config.layers.is_empty() {
set_default_layer_state(&mut cos, &doc.info.layers);
}
// Signature states as the screen shows them, so not when its display copy could not be
+179
View File
@@ -3441,6 +3441,185 @@ fn printing_keeps_hidden_layers_hidden() {
assert!(red > 100, "shown again, Layer A prints again: {red} red");
}
/// A PDF of `objs`, numbered from 1, the first being the catalog.
fn pdf_of_objects(objs: &[Vec<u8>]) -> Vec<u8> {
let mut pdf = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(pdf.len());
pdf.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
pdf.extend_from_slice(o);
pdf.extend_from_slice(b"\nendobj\n");
}
let xref = pdf.len();
pdf.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
pdf.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
pdf.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
pdf
}
/// ISO 32000-2 §8.11.4.4: the default configuration's auto state for the Print event sets the
/// layers it names when the document prints. Red is on screen but `/PrintState /OFF`, green is off
/// on screen but `/PrintState /ON`: on paper, vector or as an image, green prints and red doesn't.
/// Two blue squares are on screen and `/PrintState /OFF` too, but no Print recommendation reaches
/// them (one is named only in the View entry, the other in a Print entry without the Print
/// category), so they print as shown.
#[test]
fn printing_applies_the_print_auto_state() {
let content = b"/OC /A BDC 1 0 0 rg 10 10 80 80 re f EMC /OC /B BDC 0 1 0 rg 110 10 80 80 re f EMC \
/OC /C BDC 0 0 1 rg 210 10 40 40 re f EMC /OC /D BDC 0 0 1 rg 210 55 40 40 re f EMC";
let screen_only = b"/Usage << /View << /ViewState /ON >> /Print << /PrintState /OFF >> >> >>";
let objs: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R 6 0 R 7 0 R 8 0 R] /D << /OFF [6 0 R] \
/AS [<< /Event /Print /OCGs [5 0 R 6 0 R] /Category [/Print] >> << /Event /View /OCGs [5 0 R 6 0 R 7 0 R] /Category [/View] >> \
<< /Event /Print /OCGs [8 0 R] /Category [/Language] >>] >> >> >>"
.to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 100] /Contents 4 0 R \
/Resources << /Properties << /A 5 0 R /B 6 0 R /C 7 0 R /D 8 0 R >> >> >>"
.to_vec(),
[format!("<< /Length {} >>\nstream\n", content.len()).as_bytes(), content, b"\nendstream"].concat(),
[b"<< /Type /OCG /Name (View only) ".as_slice(), screen_only].concat(),
b"<< /Type /OCG /Name (Print only) /Usage << /View << /ViewState /OFF >> /Print << /PrintState /ON >> >> >>".to_vec(),
[b"<< /Type /OCG /Name (View entry) ".as_slice(), screen_only].concat(),
[b"<< /Type /OCG /Name (Other category) ".as_slice(), screen_only].concat(),
];
let pdf = pdf_of_objects(&objs);
let colours = |page: &pdfcraft_render::RenderedPage| {
let px = page.rgba.as_chunks::<4>().0;
let red = px.iter().filter(|p| p[0] > 200 && p[1] < 60 && p[2] < 60).count();
let green = px.iter().filter(|p| p[1] > 200 && p[0] < 60 && p[2] < 60).count();
let blue = px.iter().filter(|p| p[2] > 200 && p[0] < 60 && p[1] < 60).count();
(red, green, blue)
};
let mut s = Session::new();
let id = s.open("auto-state.pdf", None, Arc::new(pdf), None).unwrap();
let (red, green, shown_blue) = colours(&render_page(&s, id));
assert!(red > 100 && green == 0, "on screen, red shows and green doesn't: {red} red, {green} green");
// The print dialog's preview shows the page as it prints, the view as it shows; so after a
// Layers panel change too.
let preview = |s: &Session, tag: u64| {
let doc = s.get(id).unwrap();
let mut renderer = pdfcraft_render::PageRenderer::new(doc.display.clone(), display_config(&doc.config));
colours(&renderer.render(pdfcraft_render::RenderRequest { page: 0, scale: 2.0, tag, ..Default::default() }))
};
let view_entry = s.get(id).unwrap().info.layers.iter().position(|l| l.name == "View entry").unwrap();
let mut printed_blue = Vec::new();
for _ in 0..2 {
let (red, green, blue) = preview(&s, pdfcraft_render::PRINT_TAG);
assert!(red == 0 && green > 100, "the preview shows the print: {red} red, {green} green");
printed_blue.push(blue);
let (red, green, _) = preview(&s, 0);
assert!(red > 100 && green == 0, "the view is unchanged: {red} red, {green} green");
// Hidden after the first round, shown again after the second.
assert!(s.set_layer_visible(id, view_entry, printed_blue.len() == 2));
}
// Hidden in the panel, View entry (no Print recommendation) leaves the preview too.
assert!(printed_blue[1] * 4 < printed_blue[0] * 3, "the preview follows the panel: {printed_blue:?} blue");
for raster in [None, Some(print::Raster { dpi: 72.0, grayscale: false })] {
let bytes = Arc::new(s.print_pdf(id, &print::Settings { pages: vec![0], raster, ..Default::default() }).unwrap());
let mut out = Session::new();
let printed = out.open("print.pdf", None, bytes.clone(), None).unwrap();
let (red, green, blue) = colours(&render_page(&out, printed));
assert_eq!(red, 0, "the view-only layer doesn't print (as image: {})", raster.is_some());
assert!(green > 100, "the print-only layer prints (as image: {}): {green} green", raster.is_some());
// Both blue squares print: the one shown on screen alone is half of them.
assert!(
blue * 4 > shown_blue * 3,
"the layers no Print recommendation reaches print as shown (as image: {}): {blue} of {shown_blue} blue",
raster.is_some()
);
// The vector copy's states are final: nothing a reader applies when drawing is left to
// turn them back (the auto state, or a layer's `/Usage /View /ViewState`).
let layers = &out.get(printed).unwrap().info.layers;
if raster.is_none() {
let cos = pdfcraft_cos::Document::open(bytes).unwrap();
assert_eq!(layers.len(), 4);
for layer in layers {
let group = cos.get(pdfcraft_cos::ObjRef::new(layer.id.0, layer.id.1));
assert!(group.as_dict().is_some_and(|g| !g.contains(b"Usage")), "{} keeps its /Usage", layer.name);
}
let root = cos.trailer().get(b"Root").map(|r| cos.resolve(r)).unwrap();
let properties = root.as_dict().and_then(|r| r.get(b"OCProperties")).map(|p| cos.resolve(p)).unwrap();
let config = properties.as_dict().and_then(|p| p.get(b"D")).map(|d| cos.resolve(d)).unwrap();
assert!(config.as_dict().is_some_and(|d| !d.contains(b"AS")), "the print copy keeps /AS");
}
}
}
/// The print auto state of `layers` (object numbers from 5) in a document whose default
/// configuration has `auto` as its `/AS` and `groups` as the layers' dictionaries.
fn print_states_of(auto: &str, groups: &[&str]) -> Vec<bool> {
let ocgs: String = (0..groups.len()).map(|i| format!("{} 0 R ", i + 5)).collect();
let mut objs: Vec<Vec<u8>> = vec![
format!("<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [{ocgs}] /D << /AS {auto} >> >> >>").into_bytes(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>".to_vec(),
b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
];
objs.extend(groups.iter().map(|g| g.as_bytes().to_vec()));
let cos = pdfcraft_cos::Document::open(Arc::new(pdf_of_objects(&objs))).unwrap();
let layers: Vec<Layer> = (0..groups.len()).map(|i| Layer { id: (i as u32 + 5, 0), name: format!("{i}"), visible: true, initial: true }).collect();
crate::print_layer_states(&cos, &layers).expect("there are Print entries").iter().map(|l| l.visible).collect()
}
/// ISO 32000-2 §8.11.4.4: a group prints only if every category of every Print entry naming it
/// recommends ON. A Print category (`/PrintState /ON`) and a View category of another Print entry
/// (`/ViewState /OFF`) disagree, so the group stays off; with both ON it prints. A `/Category`
/// written as a single name (Table 101 asks for an array) is read as one.
#[test]
fn a_layer_prints_only_if_every_recommendation_is_on() {
let auto = "[<< /Event /Print /OCGs [5 0 R 6 0 R] /Category [/Print] >> << /Event /Print /OCGs [5 0 R 6 0 R] /Category [/View] >> \
<< /Event /Print /OCGs [7 0 R] /Category /Print >>]";
let states = print_states_of(
auto,
&[
"<< /Type /OCG /Name (Disagree) /Usage << /View << /ViewState /OFF >> /Print << /PrintState /ON >> >> >>",
"<< /Type /OCG /Name (Agree) /Usage << /View << /ViewState /ON >> /Print << /PrintState /ON >> >> >>",
"<< /Type /OCG /Name (Single name) /Usage << /Print << /PrintState /OFF >> >> >>",
],
);
assert_eq!(states, [false, true, false]);
}
/// At most 1,024 `/AS` entries are read: an entry past them that would turn the layer off is not
/// reached, while the same entry within them is.
#[test]
fn the_print_auto_state_reads_a_bounded_number_of_entries() {
let layer = "<< /Type /OCG /Name (Layer) /Usage << /Print << /PrintState /OFF >> >> >>";
let other = "<< /Type /OCG /Name (Other) >>";
let auto = |before: usize| {
let filler = "<< /Event /Print /OCGs [6 0 R] /Category [/Print] >> ".repeat(before);
format!("[{filler} << /Event /Print /OCGs [5 0 R] /Category [/Print] >>]")
};
assert_eq!(print_states_of(&auto(1023), &[layer, other]), [false, true], "the 1,024th entry is read");
assert_eq!(print_states_of(&auto(1024), &[layer, other]), [true, true], "the 1,025th entry is not");
}
/// Reading the print auto state is bounded over all `/AS` entries together: 1,024 Print entries
/// sharing one `/Category` array of 70,000 names, `/Print` last, read 65,536 names in all, find no
/// Print category and recommend nothing, instead of reading the array 1,024 times.
#[test]
fn the_print_auto_state_reads_a_bounded_number_of_categories() {
let names = format!("[{} /Print]", "/View ".repeat(70_000));
let entries = "<< /Event /Print /OCGs [5 0 R] /Category 6 0 R >> ".repeat(1024);
let objs: Vec<Vec<u8>> = vec![
format!("<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R] /D << /AS [{entries}] >> >> >>").into_bytes(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>".to_vec(),
b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
b"<< /Type /OCG /Name (Layer) /Usage << /Print << /PrintState /OFF >> >> >>".to_vec(),
names.into_bytes(),
];
let cos = pdfcraft_cos::Document::open(Arc::new(pdf_of_objects(&objs))).unwrap();
let layer = Layer { id: (5, 0), name: "Layer".into(), visible: true, initial: true };
let printed = crate::print_layer_states(&cos, std::slice::from_ref(&layer)).expect("there are Print entries");
assert!(printed.iter().all(|l| l.visible), "past the bound nothing is read, so the layer prints as shown");
}
#[test]
fn printing_lays_out_sheets_that_render() {
let (s, id) = session_with(5);
+14 -6
View File
@@ -87,6 +87,10 @@ pub struct RenderConfig {
/// `hayro_syntax::MAX_PAGE_JBIG2_WORK`. Those past it aren't drawn, like an image that can't
/// be decoded.
pub jbig2_work: Option<u64>,
/// Layer visibility overrides for print-preview rasters ([`PRINT_TAG`]), when the document
/// prints its layers differently from how they are shown (the Print event's auto state, ISO
/// 32000-2 §8.11.4.4): the preview shows the page as it prints. `None`: previews use `layers`.
pub print_layers: Option<Arc<Vec<(i32, i32, bool)>>>,
}
impl RenderConfig {
@@ -355,16 +359,20 @@ fn render_page<'a>(
config: &RenderConfig,
req: RenderRequest,
) -> Result<Output, (String, bool)> {
// Print previews share the viewer pool, but must use the output font policy. Keep the
// caller's cancellation/warning hooks, layers and annotation settings. A fresh cache is
// essential: hayro keys cached fonts by PDF dictionary, not by the font resolver, so the
// viewer cache could supply an installed face even after changing the resolver (and a
// cached missing font from print preview could blank the viewer on switching back).
// Print previews share the viewer pool, but must use the output font policy and the layers
// as they print (`print_layers`). Keep the caller's cancellation/warning hooks and annotation
// settings. A fresh cache is essential: hayro keys cached fonts by PDF dictionary, not by the
// font resolver, so the viewer cache could supply an installed face even after changing the
// resolver (and a cached missing font from print preview could blank the viewer on switching
// back); nothing drawn with the viewer's layers is reused either.
let print_settings;
let print_cache;
let (settings, cache) = if config.system_fonts && req.tag & PRINT_TAG != 0 {
let print = req.tag & PRINT_TAG != 0;
let print_layers = config.print_layers.as_ref().filter(|_| print);
let (settings, cache) = if (config.system_fonts && print) || print_layers.is_some() {
print_settings = InterpreterSettings {
font_resolver: RenderConfig { system_fonts: false, ..config.clone() }.settings().font_resolver,
ocg_overrides: print_layers.cloned().unwrap_or_else(|| settings.ocg_overrides.clone()),
..settings.clone()
};
print_cache = RenderCache::new();
+4 -4
View File
@@ -8345,8 +8345,8 @@ milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = []
evidence = ["crates/ui-egui/tests/it/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf"]
notes = "live sheet preview with sheet navigation"
evidence = ["crates/ui-egui/tests/it/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/engine/src/tests.rs::printing_applies_the_print_auto_state"]
notes = "live sheet preview with sheet navigation; layers are drawn as they print (the Print event's auto state, ISO 32000-2 §8.11.4.4)"
[[feature]]
id = "print.grayscale"
@@ -8393,8 +8393,8 @@ milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = ["doc_print"]
evidence = ["crates/ui-egui/tests/it/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/automation/tests/it/automation.rs::printing_through_tools", "crates/engine/src/tests.rs::printing_lays_out_sheets_that_render"]
notes = "Save as PDF from the dialog"
evidence = ["crates/ui-egui/tests/it/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/automation/tests/it/automation.rs::printing_through_tools", "crates/engine/src/tests.rs::printing_lays_out_sheets_that_render", "crates/engine/src/tests.rs::printing_keeps_hidden_layers_hidden", "crates/engine/src/tests.rs::printing_applies_the_print_auto_state", "crates/engine/src/tests.rs::a_layer_prints_only_if_every_recommendation_is_on"]
notes = "Save as PDF from the dialog. Layers print as shown in the Layers panel, then as the default configuration's auto state for the Print event sets them (/AS, /PrintState; ISO 32000-2 §8.11.4.4), vector or as image; the print copy keeps those states in /D without /AS or the layers' /Usage, and the dialog's preview shows them. A Print entry's Print, View and Export categories are read; Zoom, User and Language give no recommendation when printing."
[[feature]]
id = "print.print-pages-from-thumbnails"