mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 23:59:16 +00:00
Merge part 5: /VE expressions and page transparency group
# Conflicts: # vendor/README.craft-reader.md
This commit is contained in:
commit
a672020a96
6 files changed
+199
-14
No files matched your search
@@ -1,10 +1,12 @@
|
||||
//! Portable regressions adapted from PdfCraft 7437bde378f43856eada40af25b983812e615e8a and
|
||||
//! a2ec27b4775f53c40cad3456936d9406611e3a8d (MIT OR Apache-2.0).
|
||||
#![cfg(feature = "raster")]
|
||||
use craft_pdf::interpret::InterpreterSettings;
|
||||
use craft_pdf::{
|
||||
open_slice,
|
||||
raster::{self, Options, RenderCache},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Pixels {
|
||||
width: u32,
|
||||
@@ -567,3 +569,86 @@ fn mesh_triangles_sharing_an_edge_leave_no_seam() {
|
||||
assert!(spilled.is_empty(), "shaded pixels wholly across the diagonal: {spilled:?}");
|
||||
assert!(missing.is_empty(), "unshaded pixels wholly inside the triangle: {missing:?}");
|
||||
}
|
||||
|
||||
/// PdfCraft ledger hayro-interpret (40) (#2987): an optional content membership dictionary
|
||||
/// with a visibility expression (`/VE`, ISO 32000-2 §8.11.2.2) shows its content only when the
|
||||
/// expression is true, and the expression takes precedence over `/OCGs` and `/P`. A malformed
|
||||
/// expression, one naming an object that isn't a listed group, or one nested too deep falls
|
||||
/// back to `/OCGs`.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
fn visibility_expressions_decide_what_a_membership_dictionary_shows() {
|
||||
let render = |ocmd: &str, off: &[i32]| {
|
||||
let catalog = "<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R 6 0 R] /D << /Order [5 0 R 6 0 R] >> >> >>";
|
||||
let objects = format!("5 0 obj << /Type /OCG /Name (A) >> endobj\n6 0 obj << /Type /OCG /Name (B) >> endobj\n7 0 obj {ocmd} endobj");
|
||||
let pdf = String::from_utf8(one_page(
|
||||
"/MediaBox [0 0 100 100] /Resources << /Properties << /M1 7 0 R >> >>",
|
||||
b"/OC /M1 BDC 1 0 0 rg 0 0 100 100 re f EMC",
|
||||
objects.as_bytes(),
|
||||
))
|
||||
.unwrap()
|
||||
.replacen("<< /Type /Catalog /Pages 2 0 R >>", catalog, 1);
|
||||
let doc = open_slice(pdf.as_bytes(), None).unwrap();
|
||||
let settings = InterpreterSettings { ocg_overrides: Arc::new(off.iter().map(|n| (*n, 0, false)).collect()), ..Default::default() };
|
||||
let options = Options { background: [255; 4], scale: [0.5, 0.5], ..Default::default() };
|
||||
let raster = raster::render(&doc.pages()[0], &RenderCache::new(), &settings, &options).unwrap();
|
||||
raster.bytes()[..4] == [255, 0, 0, 255]
|
||||
};
|
||||
let and = "<< /Type /OCMD /VE [/And 5 0 R 6 0 R] >>";
|
||||
assert!(render(and, &[]), "And: both on");
|
||||
assert!(!render(and, &[5]), "And: one off");
|
||||
let or = "<< /Type /OCMD /VE [/Or 5 0 R 6 0 R] >>";
|
||||
assert!(render(or, &[5]), "Or: one on");
|
||||
assert!(!render(or, &[5, 6]), "Or: both off");
|
||||
let not = "<< /Type /OCMD /VE [/Not 5 0 R] >>";
|
||||
assert!(!render(not, &[]), "Not: the group is on");
|
||||
assert!(render(not, &[5]), "Not: the group is off");
|
||||
let nested = "<< /Type /OCMD /VE [/And [/Not 5 0 R] 6 0 R] >>";
|
||||
assert!(render(nested, &[5]), "nested: A off, B on");
|
||||
assert!(!render(nested, &[]), "nested: A on");
|
||||
let both = "<< /Type /OCMD /OCGs [5 0 R] /P /AllOn /VE [/Not 5 0 R] >>";
|
||||
assert!(render(both, &[5]), "the expression, not /P, decides");
|
||||
// Malformed: an unknown operator, a wrong operand count, a bare group, a string operand,
|
||||
// a missing object, and the page instead of a group.
|
||||
for bad in ["[/Xor 5 0 R]", "[/Not 5 0 R 6 0 R]", "[/And]", "[5 0 R]", "[/And (5)]", "[/Or 8 0 R]", "[/Or 3 0 R]"] {
|
||||
let ocmd = format!("<< /Type /OCMD /OCGs [5 0 R] /VE {bad} >>");
|
||||
assert!(render(&ocmd, &[]), "{bad}: /OCGs on");
|
||||
assert!(!render(&ocmd, &[5]), "{bad}: /OCGs off");
|
||||
}
|
||||
let deep = format!("<< /Type /OCMD /OCGs [5 0 R] /VE {}5 0 R{} >>", "[/Not ".repeat(41), "]".repeat(41));
|
||||
assert!(!render(&deep, &[5]), "too deep: /OCGs decides");
|
||||
}
|
||||
|
||||
/// PdfCraft ledger hayro (18) (#2972): a page's objects composite with a transparent backdrop
|
||||
/// and the page group is then put on the paper, so a blend mode only blends where something
|
||||
/// was drawn. A green fill with `/BM /Screen` (or Lighten, Difference…) half over a red square
|
||||
/// takes the mode's color over the red and stays plain green over the empty page.
|
||||
#[cfg_attr(not(target_arch = "wasm32"), test)]
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||
fn blend_modes_over_an_empty_part_of_the_page_keep_their_color() {
|
||||
for (mode, over_red) in [
|
||||
("Normal", Some([0, 255, 0])),
|
||||
("Multiply", Some([0, 0, 0])),
|
||||
("Screen", Some([255, 255, 0])),
|
||||
("Lighten", Some([255, 255, 0])),
|
||||
("Difference", Some([255, 255, 0])),
|
||||
("Exclusion", Some([255, 255, 0])),
|
||||
("ColorDodge", Some([255, 0, 0])),
|
||||
("Overlay", Some([255, 0, 0])),
|
||||
// The non-separable modes mix hue, saturation and luminosity; over an empty part of
|
||||
// the page they too keep the source color.
|
||||
("Hue", None),
|
||||
("Saturation", None),
|
||||
("Color", None),
|
||||
("Luminosity", None),
|
||||
] {
|
||||
let page = format!("/MediaBox [0 0 300 200] /Resources << /ExtGState << /G1 << /Type /ExtGState /BM /{mode} >> >> >>");
|
||||
let p = render(&one_page(&page, b"1 0 0 rg 20 20 150 150 re f /G1 gs 0 1 0 rg 100 50 150 120 re f", b""));
|
||||
// Pixel rows run top down: PDF (210, 100) and (130, 100) are rows 100 from the top.
|
||||
assert_eq!(pixel(&p, 210, 100), [0, 255, 0, 255], "{mode} over the empty page");
|
||||
if let Some(over_red) = over_red {
|
||||
assert_eq!(pixel(&p, 130, 100)[..3], over_red, "{mode} over the red square");
|
||||
}
|
||||
assert_eq!(pixel(&p, 280, 10), [255, 255, 255, 255], "{mode}: the paper stays white");
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -236,6 +236,19 @@ image, its header and the terminator fit within `MAX_DECODED_STREAM`, and otherw
|
||||
back to the look-ahead. Its unit tests in `content::tests` come with it. PdfCraft's raster
|
||||
test is not ported: the windowed tokenizer tests cover the same content streams.
|
||||
|
||||
Rendering fixes from the [PdfCraft revision 0b670caf4a090e9d83a7f41a84b4cb2144a2d33e ledger](https://github.com/storytold/pdfcraft/blob/0b670caf4a090e9d83a7f41a84b4cb2144a2d33e/vendor/README.md),
|
||||
each marked `PdfCraft patch` in the source: hayro-interpret (40) (PdfCraft #2987), an optional
|
||||
content membership dictionary with a visibility expression (`/VE`: `/And`, `/Or` and `/Not`
|
||||
over groups the document lists in `/OCProperties /OCGs`) shows its content when the
|
||||
expression is true, ahead of `/OCGs` and `/P`; a malformed expression, or one nested deeper
|
||||
than 32 levels, is ignored and `/OCGs` and `/P` decide; and hayro (18) (PdfCraft #2972), the
|
||||
page renders as one transparency group over a transparent backdrop that is then put on the
|
||||
background, so a blend mode over an empty part of the page keeps its color; with a color
|
||||
replacement (reading colors) the background stays inside the group as its backdrop. A plate
|
||||
render (`InterpreterSettings::plate`) also keeps the background inside, marked `Craft patch`,
|
||||
so blend modes on a separation's film blend with the paper. The PdfCraft raster tests of both
|
||||
are adapted in `tests/upstream_raster.rs`.
|
||||
|
||||
This remains the patched 0.7 backend. It retains the existing Bradford Lab conversion,
|
||||
floating component precision and codec versions, rather than claiming full 0.8
|
||||
equivalence. The independent color oracles, backend tests and facade contracts cover
|
||||
|
||||
Vendored
+1
-1
@@ -398,7 +398,7 @@
|
||||
"path": "src/ocg.rs",
|
||||
"source_sha256": "439455f65b8ad6ed9b0306536c2f75fa5f3fdda42ed4ca351ec3d340c38dc330",
|
||||
"source_git_blob": "c59b70df4be6a83485360dacd76bead9f80c7f94",
|
||||
"sha256": "439455f65b8ad6ed9b0306536c2f75fa5f3fdda42ed4ca351ec3d340c38dc330"
|
||||
"sha256": "802fd4c330f5066bcb2070aa0986dc869cd6f1f443e8e92857a598f7336266b7"
|
||||
},
|
||||
{
|
||||
"path": "src/pattern.rs",
|
||||
|
||||
Vendored
+74
-2
@@ -1,9 +1,14 @@
|
||||
use hayro_syntax::object::dict::keys::{BASE_STATE, D, OCGS, OCMD, OCPROPERTIES, OFF, ON, P, TYPE};
|
||||
use hayro_syntax::object::{Array, Dict, Name, ObjectIdentifier};
|
||||
use hayro_syntax::object::dict::keys::{
|
||||
BASE_STATE, D, OCGS, OCMD, OCPROPERTIES, OFF, ON, P, TYPE, VE,
|
||||
};
|
||||
use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) struct OcgState {
|
||||
inactive_ocgs: HashSet<ObjectIdentifier>,
|
||||
/// PdfCraft patch (40): every group the document lists in `/OCProperties /OCGs`, so a
|
||||
/// visibility expression naming anything else is known to be malformed.
|
||||
known_ocgs: HashSet<ObjectIdentifier>,
|
||||
visibility_stack: Vec<bool>,
|
||||
}
|
||||
|
||||
@@ -11,6 +16,7 @@ impl OcgState {
|
||||
fn dummy() -> Self {
|
||||
Self {
|
||||
inactive_ocgs: HashSet::default(),
|
||||
known_ocgs: HashSet::default(),
|
||||
visibility_stack: vec![],
|
||||
}
|
||||
}
|
||||
@@ -25,6 +31,15 @@ impl OcgState {
|
||||
};
|
||||
|
||||
let mut inactive = HashSet::new();
|
||||
let known: HashSet<ObjectIdentifier> = oc_properties
|
||||
.get::<Array<'_>>(OCGS)
|
||||
.map(|ocgs| {
|
||||
ocgs.raw_iter()
|
||||
.filter_map(|item| item.as_obj_ref())
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let base_state = config
|
||||
.get::<Name<'_>>(BASE_STATE)
|
||||
@@ -61,6 +76,7 @@ impl OcgState {
|
||||
|
||||
Self {
|
||||
inactive_ocgs: inactive,
|
||||
known_ocgs: known,
|
||||
visibility_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -72,6 +88,17 @@ impl OcgState {
|
||||
}
|
||||
|
||||
pub(crate) fn begin_ocmd(&mut self, ocmd: &Dict<'_>) {
|
||||
// PdfCraft patch (40): a visibility expression takes precedence over /OCGs and /P
|
||||
// (ISO 32000-2 §8.11.2.2); a malformed one is ignored and they decide instead.
|
||||
if let Some(visible) = ocmd
|
||||
.get::<Array<'_>>(VE)
|
||||
.and_then(|ve| self.visibility_expression(&ve, 0))
|
||||
{
|
||||
let visible = self.is_visible() && visible;
|
||||
self.visibility_stack.push(visible);
|
||||
return;
|
||||
}
|
||||
|
||||
let policy = ocmd
|
||||
.get::<Name<'_>>(P)
|
||||
.and_then(|n| OcmdPolicy::from_name(n.as_ref()))
|
||||
@@ -104,6 +131,48 @@ impl OcgState {
|
||||
self.visibility_stack.push(visible);
|
||||
}
|
||||
|
||||
/// PdfCraft patch (40): evaluate a visibility expression (`/VE`, PDF 1.6): `[/And e…]`,
|
||||
/// `[/Or e…]` or `[/Not e]`, each `e` an optional content group (by reference) or a nested
|
||||
/// expression. `None` for a malformed expression: an unknown operator, a `/Not` without
|
||||
/// exactly one operand, an `/And` or `/Or` without any, an operand that is neither (a
|
||||
/// reference to an object the document doesn't list as a group is neither), or nesting
|
||||
/// deeper than `MAX_VISIBILITY_EXPRESSION_DEPTH`.
|
||||
fn visibility_expression(&self, expr: &Array<'_>, depth: usize) -> Option<bool> {
|
||||
if depth > MAX_VISIBILITY_EXPRESSION_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let mut items = expr.raw_iter();
|
||||
let operator = match items.next()? {
|
||||
MaybeRef::NotRef(Object::Name(name)) => name,
|
||||
_ => return None,
|
||||
};
|
||||
let (mut count, mut all, mut any) = (0usize, true, false);
|
||||
for item in items {
|
||||
let value = match item {
|
||||
MaybeRef::Ref(r) => {
|
||||
let id: ObjectIdentifier = r.into();
|
||||
if !self.known_ocgs.contains(&id) {
|
||||
return None;
|
||||
}
|
||||
!self.inactive_ocgs.contains(&id)
|
||||
}
|
||||
MaybeRef::NotRef(Object::Array(nested)) => {
|
||||
self.visibility_expression(&nested, depth + 1)?
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
count = count.saturating_add(1);
|
||||
all &= value;
|
||||
any |= value;
|
||||
}
|
||||
match (operator.as_ref(), count) {
|
||||
(b"And", 1..) => Some(all),
|
||||
(b"Or", 1..) => Some(any),
|
||||
(b"Not", 1) => Some(!all),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn begin_ocg(&mut self, props: &Dict<'_>, ref_id: ObjectIdentifier) {
|
||||
match props.get::<Name<'_>>(TYPE).as_deref() {
|
||||
Some(OCMD) => self.begin_ocmd(props),
|
||||
@@ -140,6 +209,9 @@ impl Default for OcgState {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (40): how deep visibility expressions may nest; deeper ones are malformed.
|
||||
const MAX_VISIBILITY_EXPRESSION_DEPTH: usize = 32;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum BaseState {
|
||||
On,
|
||||
|
||||
Vendored
+1
-1
@@ -62,7 +62,7 @@
|
||||
"path": "src/lib.rs",
|
||||
"source_sha256": "682db2a32fbf4d875c0e9dd7ac5dc875a6b09ade375efbce6f104de9197de078",
|
||||
"source_git_blob": "6333ed5808ae0cf9cf158e4730e973c1d7089bab",
|
||||
"sha256": "682db2a32fbf4d875c0e9dd7ac5dc875a6b09ade375efbce6f104de9197de078"
|
||||
"sha256": "8396b90516603079bb43012621e8089621fec58fdd3d9c9aa9a8d2aa840265da"
|
||||
},
|
||||
{
|
||||
"path": "src/renderer.rs",
|
||||
|
||||
Vendored
+25
-10
@@ -254,15 +254,22 @@ pub fn render_into<'a>(
|
||||
interpreter_settings.preview,
|
||||
);
|
||||
|
||||
// PdfCraft patch (18): the page renders as one transparency group with the background
|
||||
// painted *inside* it, as the initial backdrop. A group isolates what it wraps from what
|
||||
// was painted before it, so with the background outside, a Multiply highlight blended
|
||||
// only with the group's transparent backdrop and stayed its own colour — over a dark
|
||||
// reading-colours background the whole quad became one flat bar and the words under it
|
||||
// vanished. Inside the group the highlight multiplies with the background too: the words
|
||||
// keep the highlight's colour and the band between them takes the background (white
|
||||
// backgrounds are unaffected — multiplying with white is the identity).
|
||||
device.push_transparency_group(1.0, None, BlendMode::Normal);
|
||||
// PdfCraft patch (18): the page renders as one transparency group (the page group). Its
|
||||
// objects composite with an initially transparent backdrop, and the group is then put on
|
||||
// the paper (ISO 32000-2, page group), so a blend mode only blends where something was
|
||||
// drawn: a /BM /Screen fill over an empty part of the page keeps its color (#2970).
|
||||
// Under reading colors the background is painted *inside* the group instead, as its
|
||||
// initial backdrop, so a Multiply highlight blends with the dark background: outside it,
|
||||
// the highlight kept its own flat color and the words under it vanished into the band
|
||||
// (#1548). Reading colors replace the page's colors anyway; for document colors the
|
||||
// background stays outside, or every separable mode but Normal, Multiply, Darken and
|
||||
// HardLight would blend with the white paper. Craft patch: a plate render keeps the
|
||||
// background inside as well, so blend modes on a separation's film blend with the paper.
|
||||
let backdrop_inside =
|
||||
render_settings.color_replacement.is_some() || interpreter_settings.plate.is_some();
|
||||
if backdrop_inside {
|
||||
device.push_transparency_group(1.0, None, BlendMode::Normal);
|
||||
}
|
||||
|
||||
device.ctx.set_paint(render_settings.bg_color);
|
||||
device
|
||||
@@ -274,11 +281,19 @@ pub fn render_into<'a>(
|
||||
path: clip_path,
|
||||
fill: FillRule::NonZero,
|
||||
});
|
||||
if !backdrop_inside {
|
||||
device.push_transparency_group(1.0, None, BlendMode::Normal);
|
||||
}
|
||||
|
||||
interpret_page(page, &mut state, &mut device);
|
||||
|
||||
if !backdrop_inside {
|
||||
device.pop_transparency_group();
|
||||
}
|
||||
device.pop_clip_path();
|
||||
device.pop_transparency_group();
|
||||
if backdrop_inside {
|
||||
device.pop_transparency_group();
|
||||
}
|
||||
|
||||
let mut resources = vello_cpu::Resources::default();
|
||||
device.ctx.render_to_buffer(
|
||||
|
||||
Reference in new issue
Block a user