diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index a8541c1942..c70d84e624 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (255) +## In this repository (256) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -203,6 +203,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) | | `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) | | `crates/fonts/src/std14_metrics.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. | +| `crates/prc/src/acof/table.rs` | PRC Double prefix-code table (as a Rust table) | Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2) | MIT | Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in crates/prc/src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header. | Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data. | | `vendor/hayro-cmap/assets/cmaps.brotli` | Adobe CMap resources (predefined CJK CMaps) | Adobe | BSD-3-Clause | https://github.com/adobe-type-tools/cmap-resources (via hayro-cmap 0.1.0 assets/) | Character-code mapping tables required to read CJK PDFs. Non-visual data, allowed by AGENTS.md §1.1 | | `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | | `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 783ecc6c9f..9638749929 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -2156,6 +2156,17 @@ adobe_data = true usage = "Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree." sha256 = "f225e0b044875b8fa6375b6b5ad935f0d99fcc1ad03ecf9f0baae2cedbfca4d2" +[[asset]] +path = "crates/prc/src/acof/table.rs" +title = "PRC Double prefix-code table (as a Rust table)" +author = "Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2)" +source = "Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in crates/prc/src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header." +licence = "MIT" +licence_file = "licenses/prc-rs-LICENSE.txt" +kind = "data" +usage = "Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data." +sha256 = "8e705c5cde6a3ef778e8aca69759e38845b228b3f7f12949e587b19855d2114c" + [[bundled]] crate = "epaint_default_fonts" version = "0.37.0" diff --git a/Cargo.lock b/Cargo.lock index 4df93ed3fe..fb7e5a759d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4551,6 +4551,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "pdfcraft-prc" +version = "0.18.1" +dependencies = [ + "flate2", + "proptest", + "thiserror 2.0.21", +] + [[package]] name = "pdfcraft-preflight" version = "0.18.1" diff --git a/LICENSES.toml b/LICENSES.toml index d9ceb3180e..0617ca5d57 100644 --- a/LICENSES.toml +++ b/LICENSES.toml @@ -88,6 +88,13 @@ destination = "licenses/egui-emoji-icon-font-mit-license.txt" sha256 = "b9d2c1d909aa149996fd4c91dcb92b2362a04431640c1d200959da94caf8cde1" origin = "https://raw.githubusercontent.com/emilk/egui/0.36.2/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt" +[[document]] +source = "licenses/prc-rs-LICENSE.txt" +licences = ["MIT"] +destination = "licenses/prc-rs-LICENSE.txt" +sha256 = "ddb846513d92153d8fa16b18ed3e7c86953a582f0d2f49f02b29f1bd569dac58" +origin = "https://raw.githubusercontent.com/ralovich/prc-rs/287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d/LICENSE" + [[document]] source = "vendor/hayro-interpret/LICENSE-APACHE" licences = ["Apache-2.0"] diff --git a/ROADMAP.md b/ROADMAP.md index 6ef9e2c369..00dd8fdee0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -231,6 +231,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-11-03 (M7, #5835):** In image crop mode, a right-click on the image, its crop box or its handles keeps the crop and opens no menu, so the menu no longer switches as the pointer moves. A right-click elsewhere clears the selection and opens the page menu. UI tests cover all four page rotations, overlapping images, a 45° image, and a check that the Done/Cancel/Remove Crop pills at a page edge still keep the crop. +- **2026-11-02 (M12, #1297):** `model_add` embeds a U3D or PRC file, recognised by its header, as a 3D annotation with a chosen activation and a poster (an image or PDF page fitted in the box, or a labelled frame); an older file's catalog `/Version` is raised to what the model needs. Views, a rendered poster and a UI command are later steps. Estimates unchanged. + - **2026-11-02 (M10, #5743):** macOS print jobs name their title and owner (`com.apple.print.JobInfo.PMJobName`, `PMJobOwner`), as the system print dialog does, because a Kyocera filter crashes on a job without them. Checked by a test and through `doc_print` against a stand-in `lp`; not yet on a physical printer. - **2026-11-01 (M7, #5657):** Header/footer, watermark and background additions and replacements stage the complete call before committing. Synthetic later-page failures preserve exact incremental bytes, object count, dirty state and allocation cursor; success controls retain other marks and shared/inherited resources. Estimates unchanged. diff --git a/crates/annot/src/appearance.rs b/crates/annot/src/appearance.rs index 909250ef26..89c0f70f48 100644 --- a/crates/annot/src/appearance.rs +++ b/crates/annot/src/appearance.rs @@ -278,7 +278,7 @@ fn ext_gstate(opacity: f64, multiply: bool) -> Option { Some(res) } -fn form(bbox: [f64; 4], content: &[u8], resources: Dict) -> Stream { +pub(crate) fn form(bbox: [f64; 4], content: &[u8], resources: Dict) -> Stream { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); @@ -288,6 +288,18 @@ fn form(bbox: [f64; 4], content: &[u8], resources: Dict) -> Stream { Stream::flate(d, content) } +/// The `cm` that draws a picture into `at`: an image (its unit square) or a form XObject whose +/// `/Matrix` maps it to `size` points (`None`, or a side that is not positive, draws nothing). +pub(crate) fn picture_place(at: [f64; 4], image: bool, size: Option<(f64, f64)>) -> Option { + let [x0, y0, x1, y1] = at; + let (w, h) = (x1 - x0, y1 - y0); + if image { + return Some(format!("{} 0 0 {} {} {} cm", n(w), n(h), n(x0), n(y0))); + } + let (sw, sh) = size.filter(|(sw, sh)| *sw > 0.0 && *sh > 0.0)?; + Some(format!("{} 0 0 {} {} {} cm", n(w / sw), n(h / sh), n(x0), n(y0))) +} + /// The page `/Rotate` an image signature's picture is turned back by (`/PCPictureRotate`, written /// when the signature is added to a turned page): 0, 90, 180 or 270. pub(crate) fn picture_turn(d: &Dict) -> i64 { @@ -325,7 +337,7 @@ pub fn undrawable_text(d: &Dict) -> Option<(&'static str, char)> { } /// `/Font` resources naming `/Helv`: standard Helvetica with WinAnsiEncoding. -fn helvetica_resource() -> Object { +pub(crate) fn helvetica_resource() -> Object { let mut font = Dict::new(); font.set(b"Type".to_vec(), Object::name("Font")); font.set(b"Subtype".to_vec(), Object::name("Type1")); @@ -786,15 +798,12 @@ fn draw_stamp(context: &AppearanceContext<'_>, mut drawing: AppearanceDrawing) - // the standard counter-rotation as its form `/Matrix`: viewers fit the turned // bounding box to `/Rect` (ISO 32000-2 §12.5.5), so it reads upright as displayed. if let Some(pic) = d.get(b"PCPicture").and_then(Object::as_ref) { - let [x0, y0, x1, y1] = rect; - let (w, h) = (x1 - x0, y1 - y0); let image = matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))); - let place = if image { - format!("{} 0 0 {} {} {} cm", n(w), n(h), n(x0), n(y0)) - } else { - let size = nums(d, b"PCPictureSize").filter(|s| s.len() == 2 && s[0] > 0.0 && s[1] > 0.0)?; - format!("{} 0 0 {} {} {} cm", n(w / size[0]), n(h / size[1]), n(x0), n(y0)) - }; + let size = nums(d, b"PCPictureSize").and_then(|s| match s.as_slice() { + [w, h] if *w > 0.0 && *h > 0.0 => Some((*w, *h)), + _ => None, + }); + let place = picture_place(rect, image, size)?; c.push_str(&format!("q {place} /Pic Do Q\n")); let mut xo = Dict::new(); xo.set(b"Pic".to_vec(), Object::Ref(pic)); diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 7bd0894c66..5b7492bea2 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -23,6 +23,7 @@ mod draw; mod info; mod lookup; mod modify; +mod threed; mod types; #[cfg(test)] @@ -41,6 +42,7 @@ pub use modify::{ add_reply, add_text_replacement, deletable, delete_annotation, erase_ink, fillable, goes_with, lift_annotation, move_annotation, set_contents, set_fill, set_font_size, set_locked, set_marked, set_points, set_rect, set_review_state, set_style, with_replies_and_popups, }; +pub use threed::{Activation as Activation3d, MAX_MODEL_BYTES, ModelFormat, NewModel3d, PosterPicture, add_3d_model, check_model}; pub use types::{ AttachIcon, FillMark, LineEnding, Markup, Meta, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, ReviewState, Rgb, Shape, StampGroup, StampKind, Style, StyleChange, TextReplacement, rect_quad, @@ -70,6 +72,12 @@ pub enum AnnotError { /// with a font that has those characters. The message names the character. #[error("{0}")] Undrawable(String), + /// A 3D model file that is neither U3D (starts with `U3D` and a zero byte) nor PRC (starts `PRC`). + #[error("not a 3D model: the file must be U3D (ECMA-363) or PRC (ISO 14739-1)")] + NotAModel, + /// A 3D model larger than `MAX_MODEL_BYTES`. + #[error("the 3D model is {size} bytes; the largest accepted is {max}")] + ModelTooLarge { size: usize, max: usize }, #[error("{0}")] Cos(#[from] pdfcraft_cos::CosError), } diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 32d28fc6ec..2ff781a3a5 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -2251,6 +2251,141 @@ fn freehand_highlight_is_compatible_ink_with_marker_blending() { } } +/// A 3D annotation is PDF 1.6 and a PRC model 1.7 (ISO 32000-2 §13.6): an older document's +/// catalog says so, and a newer one is left as it is. +#[test] +fn a_3d_model_raises_an_older_document_to_its_version() { + let catalog_version = |doc: &Document| { + let root = doc.root().unwrap(); + doc.get(root).as_dict().unwrap().name(b"Version").map(<[u8]>::to_vec) + }; + let add = |doc: &mut Document, model: &[u8]| { + let new = NewModel3d { page: 0, rect: [100.0, 100.0, 300.0, 300.0], model, activation: Activation3d::default(), poster: None, contents: "" }; + add_3d_model(doc, &new, &meta("m")).unwrap(); + }; + let mut doc = fixture(); + add(&mut doc, b"U3D\0rest"); + assert_eq!(catalog_version(&reopen(&doc)), None, "a 1.7 file needs nothing for U3D"); + for (model, version) in [(&b"U3D\0rest"[..], &b"1.6"[..]), (b"PRC rest", b"1.7")] { + let mut doc = fixture(); + doc.set_version("1.4"); + add(&mut doc, model); + assert_eq!(catalog_version(&reopen(&doc)).as_deref(), Some(version)); + } + let mut doc = fixture(); + doc.set_version("2.0"); + add(&mut doc, b"PRC rest"); + assert_eq!(catalog_version(&reopen(&doc)), None, "a 2.0 file takes PRC as it is"); +} + +/// Before 2.0, PRC is an Adobe extension to PDF 1.7 (§7.12): a PRC model declares it, keeping +/// other extensions and a later ADBE level; U3D and a 2.0 file need nothing. +#[test] +fn a_prc_model_declares_the_adobe_extension_before_pdf_2() { + let adbe = |doc: &Document| { + let root = doc.root().unwrap(); + let ext = doc.get(root).as_dict().unwrap().get(b"Extensions").map(|e| doc.resolve(e).as_dict().unwrap().clone()); + ext.map(|e| { + let adbe = e.get(b"ADBE").map(|a| a.as_dict().unwrap().clone()); + (e.get(b"XYZ").is_some(), adbe.map(|a| (a.name(b"BaseVersion").map(<[u8]>::to_vec), a.get(b"ExtensionLevel").and_then(Object::as_int)))) + }) + }; + let add = |doc: &mut Document, model: &[u8]| { + let new = NewModel3d { page: 0, rect: [100.0, 100.0, 300.0, 300.0], model, activation: Activation3d::default(), poster: None, contents: "" }; + add_3d_model(doc, &new, &meta("m")).unwrap(); + }; + let mut doc = fixture(); + add(&mut doc, b"PRC rest"); + assert_eq!(adbe(&reopen(&doc)), Some((false, Some((Some(b"1.7".to_vec()), Some(1)))))); + let mut doc = fixture(); + add(&mut doc, b"U3Drest"); + assert_eq!(adbe(&reopen(&doc)), None, "U3D is in ISO 32000-1"); + let mut doc = fixture(); + doc.set_version("2.0"); + add(&mut doc, b"PRC rest"); + assert_eq!(adbe(&reopen(&doc)), None, "PRC is in ISO 32000-2"); + + let mut doc = fixture(); + let root = doc.root().unwrap(); + let mut level3 = Dict::new(); + level3.set(b"BaseVersion".to_vec(), Object::name("1.7")); + level3.set(b"ExtensionLevel".to_vec(), Object::Int(3)); + let mut ext = Dict::new(); + ext.set(b"ADBE".to_vec(), Object::Dict(level3)); + ext.set(b"XYZ".to_vec(), Object::Dict(Dict::new())); + doc.update_dict(root, |c| c.set(b"Extensions".to_vec(), Object::Dict(ext.clone()))).unwrap(); + add(&mut doc, b"PRC rest"); + assert_eq!(adbe(&reopen(&doc)), Some((true, Some((Some(b"1.7".to_vec()), Some(3))))), "a later level is kept"); + ext.remove(b"ADBE"); + let mut doc = fixture(); + doc.update_dict(root, |c| c.set(b"Extensions".to_vec(), Object::Dict(ext))).unwrap(); + add(&mut doc, b"PRC rest"); + assert_eq!(adbe(&reopen(&doc)), Some((true, Some((Some(b"1.7".to_vec()), Some(1))))), "other extensions are kept"); + + let mut doc = fixture(); + let mut ext = Dict::new(); + ext.set(b"ADBE".to_vec(), Object::Array(vec![Object::Dict(Dict::new())])); + let ext = doc.add(Object::Dict(ext)); + doc.update_dict(root, |c| c.set(b"Extensions".to_vec(), Object::Ref(ext))).unwrap(); + add(&mut doc, b"PRC rest"); + let doc = reopen(&doc); + let ext = doc.resolve(doc.get(doc.root().unwrap()).as_dict().unwrap().get(b"Extensions").unwrap()); + assert!(ext.as_dict().unwrap().get(b"ADBE").unwrap().as_array().is_some(), "an array of ADBE extensions is left alone"); +} + +#[test] +fn a_picture_is_placed_by_its_box_and_a_form_needs_a_positive_size() { + let at = [10.0, 20.0, 110.0, 70.0]; + assert_eq!(appearance::picture_place(at, true, None).as_deref(), Some("100 0 0 50 10 20 cm"), "an image fills its unit square"); + assert_eq!(appearance::picture_place(at, false, Some((200.0, 25.0))).as_deref(), Some("0.5 0 0 2 10 20 cm")); + for size in [None, Some((0.0, 25.0)), Some((200.0, -1.0))] { + assert_eq!(appearance::picture_place(at, false, size), None, "{size:?}"); + } +} + +/// The poster's box must lie in the rectangle, and a form poster needs a size to scale by. +#[test] +fn a_3d_poster_outside_its_rectangle_or_without_a_size_is_refused() { + let picture = |at: [f64; 4], size: (f64, f64)| PosterPicture { xobject: ObjRef { num: 6, generation: 0 }, image: false, size, at }; + for (poster, why) in [ + (picture([50.0, 100.0, 250.0, 300.0], (10.0, 10.0)), "starts left of the rectangle"), + (picture([100.0, 100.0, 300.0, 301.0], (10.0, 10.0)), "ends above it"), + (picture([100.0, 100.0, 300.0, 300.0], (0.0, 10.0)), "has no width"), + (picture([100.0 - 2e-7, 150.0, 100.0 - 1e-7, 200.0], (10.0, 10.0)), "lies outside within the rounding slack"), + ] { + let mut doc = fixture(); + let new = NewModel3d { + page: 0, + rect: [100.0, 100.0, 300.0, 300.0], + model: b"PRC rest", + activation: Activation3d::default(), + poster: Some(poster), + contents: "", + }; + assert!(matches!(add_3d_model(&mut doc, &new, &meta("m")), Err(AnnotError::Invalid(_))), "{why}"); + assert_eq!(list(&doc, 0).len(), 1, "{why}: nothing added"); + } +} + +/// A poster box fitted to the rectangle in floating point ends an ulp outside it: a 7×3 picture +/// centred in [0 100 3.6 110.8] spans -2.2e-16 to 3.6000000000000005. It is taken, inside the +/// rectangle. +#[test] +fn a_3d_poster_fitted_to_its_rectangle_with_rounding_is_taken() { + let mut doc = fixture(); + let at = [-2.220446049250313e-16, 104.62857142857143, 3.6000000000000005, 106.17142857142858]; + let poster = PosterPicture { xobject: ObjRef { num: 6, generation: 0 }, image: false, size: (7.0, 3.0), at }; + let new = NewModel3d { + page: 0, + rect: [0.0, 100.0, 3.6, 110.8], + model: b"PRC rest", + activation: Activation3d::default(), + poster: Some(poster), + contents: "", + }; + assert_eq!(add_3d_model(&mut doc, &new, &meta("m")), Ok(1)); +} + #[test] fn indirect_typewriter_names_keep_outer_whitespace_in_summaries() { let mut doc = fixture(); diff --git a/crates/annot/src/threed.rs b/crates/annot/src/threed.rs new file mode 100644 index 0000000000..cf828388c0 --- /dev/null +++ b/crates/annot/src/threed.rs @@ -0,0 +1,292 @@ +//! 3D annotations (ISO 32000-2 §13.6.2): a U3D or PRC model shown in a rectangle on a page, +//! with a poster (`/AP /N`) for printing, for readers without 3D, and until it is activated. + +use super::create::{finite, normalize, num_array}; +use super::lookup::{annots, page_ref, set_annots}; +use super::types::{FLAG_PRINT, Meta}; +use super::{AnnotError, Dict, Document, ObjRef, Object, PdfString, Stream, appearance, n}; + +/// The largest model accepted, in bytes: well past real U3D/PRC files, and bounded because the +/// model is held and compressed in memory. +pub const MAX_MODEL_BYTES: usize = 256 << 20; + +/// What the poster says when there is no picture. +const PLACEHOLDER_LABEL: &str = "3D model"; + +/// A 3D stream's data format (§13.6.3): ECMA-363 U3D or ISO 14739-1 PRC. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ModelFormat { + U3d, + Prc, +} + +impl ModelFormat { + /// The format a model file's first bytes declare: U3D files start `U3D\0` (ECMA-363 file + /// header block type), PRC files start `PRC`. + /// + /// # Errors + /// + /// [`AnnotError::NotAModel`] for anything else. + pub fn detect(bytes: &[u8]) -> Result { + if bytes.starts_with(b"U3D\0") { + Ok(Self::U3d) + } else if bytes.starts_with(b"PRC") { + Ok(Self::Prc) + } else { + Err(AnnotError::NotAModel) + } + } + + /// The 3D stream's `/Subtype`. + pub fn subtype(self) -> &'static str { + match self { + Self::U3d => "U3D", + Self::Prc => "PRC", + } + } + + /// The PDF version a document holding it needs (§13.6): a 3D annotation is 1.6, a PRC stream + /// 1.7 under Adobe's extension, declared by [`add_3d_model`] (PDF 2.0 takes PRC as standard). + pub fn version(self) -> &'static str { + match self { + Self::U3d => "1.6", + Self::Prc => "1.7", + } + } +} + +/// When a reader turns the poster into the live model (§13.6.2, the activation dictionary's +/// `/A`). The standard's default is [`Self::Click`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Activation { + /// `/XA`: when the user clicks it; it prints as its poster. + #[default] + Click, + /// `/PO`: when the page opens. + PageOpen, + /// `/PV`: when the page becomes visible. + PageVisible, +} + +impl Activation { + /// Every activation, with the word a tool argument names it by. + pub const ALL: [(Self, &'static str); 3] = [(Self::Click, "click"), (Self::PageOpen, "page_open"), (Self::PageVisible, "page_visible")]; + + /// The `/A` name. + pub fn name(self) -> &'static str { + match self { + Self::Click => "XA", + Self::PageOpen => "PO", + Self::PageVisible => "PV", + } + } +} + +/// A picture already in the document (an image XObject, or a form XObject whose `/Matrix` maps it +/// to `size` points) drawn into `at`, a box inside the annotation's rectangle. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PosterPicture { + pub xobject: ObjRef, + pub image: bool, + pub size: (f64, f64), + pub at: [f64; 4], +} + +/// A 3D model to add on `page` (0-based) in `rect` (user space). +#[derive(Clone, Debug, PartialEq)] +pub struct NewModel3d<'a> { + pub page: usize, + pub rect: [f64; 4], + pub model: &'a [u8], + pub activation: Activation, + /// The poster's picture; `None` draws a labelled frame. + pub poster: Option, + /// `/Contents`: the annotation's alternate description (§12.5.2). + pub contents: &'a str, +} + +/// Check a model before anything is added: its size and its format. +/// +/// # Errors +/// +/// [`AnnotError::ModelTooLarge`] past [`MAX_MODEL_BYTES`], else as [`ModelFormat::detect`]. +pub fn check_model(model: &[u8]) -> Result { + check_model_size(model, MAX_MODEL_BYTES)?; + ModelFormat::detect(model) +} + +fn check_model_size(model: &[u8], max: usize) -> Result<(), AnnotError> { + if model.len() > max { + return Err(AnnotError::ModelTooLarge { size: model.len(), max }); + } + Ok(()) +} + +/// Add a 3D annotation: the model in a Flate-compressed 3D stream referenced from `/3DD` (so the +/// annotation has its own instance, §13.6.2), `/3DA` stating only `/A`, and the poster as `/AP /N`. +/// No view (`/3DV`, `/VA`) is written: readers open on the model's own default view. A document +/// older than [`ModelFormat::version`] is raised to it. Returns its index in the page's `/Annots`. +/// +/// # Errors +/// +/// As [`check_model`]; [`AnnotError::NoSuchPage`]; [`AnnotError::Invalid`] for a rectangle with no +/// area or a poster box outside it. +pub fn add_3d_model(doc: &mut Document, new: &NewModel3d<'_>, meta: &Meta) -> Result { + let format = check_model(new.model)?; + let page = page_ref(doc, new.page)?; + let rect = normalize(new.rect); + if !finite(&new.rect) || rect[2] - rect[0] < 1.0 || rect[3] - rect[1] < 1.0 { + return Err(AnnotError::Invalid("invalid rectangle for the 3D model (each side at least 1 pt)".into())); + } + let poster = match &new.poster { + Some(p) => picture_poster(rect, p)?, + None => placeholder_poster(rect), + }; + let mut stream = Dict::new(); + stream.set(b"Type".to_vec(), Object::name("3D")); + stream.set(b"Subtype".to_vec(), Object::name(format.subtype())); + let stream = doc.add(Object::Stream(Stream::flate(stream, new.model))); + let poster = doc.add(Object::Stream(poster)); + let d = annotation_dict(new, rect, page, stream, poster, meta); + let r = doc.add(Object::Dict(d)); + let mut list = annots(doc, page); + let index = list.len(); + list.push(Object::Ref(r)); + set_annots(doc, page, list)?; + at_least_version(doc, format.version())?; + if format == ModelFormat::Prc && !at_least(doc, "2.0") { + declare_prc_extension(doc)?; + } + Ok(index) +} + +/// Whether the header or the catalog `/Version` says `min` or later (§7.7.2). +fn at_least(doc: &Document, min: &str) -> bool { + let reached = |v: &[u8]| v.len() >= 3 && v >= min.as_bytes(); + let catalog = doc.root().and_then(|root| doc.get(root).as_dict().and_then(|c| c.get(b"Version").cloned())); + let catalog = catalog.and_then(|v| doc.resolve(&v).as_name().map(<[u8]>::to_vec)); + reached(doc.version().as_bytes()) || catalog.as_deref().is_some_and(reached) +} + +/// PRC came before PDF 2.0 as an Adobe extension to 1.7: such a file declares +/// `/Extensions /ADBE` with ExtensionLevel 1 or later (§7.12). Other extensions and an ADBE entry +/// that already has a level are kept; one that is not a dictionary (an array of them) is left alone. +fn declare_prc_extension(doc: &mut Document) -> Result<(), AnnotError> { + let Some(root) = doc.root() else { return Ok(()) }; + let catalog = doc.get(root); + // `None` when present but not a dictionary; an empty one when absent. + let dict_or_new = |o: Option<&Object>| match o.map(|o| doc.resolve(o)) { + None => Some(Dict::new()), + Some(o) => o.as_dict().cloned(), + }; + let Some(mut extensions) = dict_or_new(catalog.as_dict().and_then(|c| c.get(b"Extensions"))) else { return Ok(()) }; + let Some(mut adbe) = dict_or_new(extensions.get(b"ADBE")) else { return Ok(()) }; + if adbe.get(b"ExtensionLevel").map(|l| doc.resolve(l)).and_then(|l| l.as_int()).is_some_and(|l| l >= 1) { + return Ok(()); + } + adbe.set(b"BaseVersion".to_vec(), Object::name("1.7")); + adbe.set(b"ExtensionLevel".to_vec(), Object::Int(1)); + extensions.set(b"ADBE".to_vec(), Object::Dict(adbe)); + doc.update_dict(root, |c| c.set(b"Extensions".to_vec(), Object::Dict(extensions)))?; + Ok(()) +} + +/// A document whose header and catalog `/Version` both say less than `min` gets `/Version min` in +/// its catalog (§7.7.2: the later of the two applies). +fn at_least_version(doc: &mut Document, min: &str) -> Result<(), AnnotError> { + let Some(root) = doc.root() else { return Ok(()) }; + if !at_least(doc, min) { + doc.update_dict(root, |c| c.set(b"Version".to_vec(), Object::name(min)))?; + } + Ok(()) +} + +fn annotation_dict(new: &NewModel3d<'_>, rect: [f64; 4], page: ObjRef, stream: ObjRef, poster: ObjRef, meta: &Meta) -> Dict { + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("Annot")); + d.set(b"Subtype".to_vec(), Object::name("3D")); + d.set(b"Rect".to_vec(), num_array(&rect)); + d.set(b"P".to_vec(), Object::Ref(page)); + d.set(b"F".to_vec(), Object::Int(FLAG_PRINT)); + if !new.contents.is_empty() { + d.set(b"Contents".to_vec(), PdfString::text(new.contents)); + } + if !meta.id.is_empty() { + d.set(b"NM".to_vec(), PdfString::text(&meta.id)); + } + if let Some(date) = &meta.date { + d.set(b"M".to_vec(), PdfString::literal(date.as_bytes().to_vec())); + } + d.set(b"3DD".to_vec(), Object::Ref(stream)); + let mut activation = Dict::new(); + activation.set(b"A".to_vec(), Object::name(new.activation.name())); + d.set(b"3DA".to_vec(), Object::Dict(activation)); + let mut ap = Dict::new(); + ap.set(b"N".to_vec(), Object::Ref(poster)); + d.set(b"AP".to_vec(), Object::Dict(ap)); + d +} + +/// The poster drawing `p` into its box, which must lie within `rect`. +fn picture_poster(rect: [f64; 4], p: &PosterPicture) -> Result { + let at = normalize(p.at); + // A box fitted to `rect` in floating point can end a few ulps outside it: allow that, then clamp. + let slack = rect.iter().fold(1.0_f64, |m, v| m.max(v.abs())) * 1e-9; + let near = finite(&p.at) && at[0] >= rect[0] - slack && at[1] >= rect[1] - slack && at[2] <= rect[2] + slack && at[3] <= rect[3] + slack; + let at = [at[0].max(rect[0]), at[1].max(rect[1]), at[2].min(rect[2]), at[3].min(rect[3])]; + if !(near && at[2] > at[0] && at[3] > at[1]) { + return Err(AnnotError::Invalid("the poster must fit inside the 3D model's rectangle".into())); + } + let place = appearance::picture_place(at, p.image, Some(p.size)).ok_or_else(|| AnnotError::Invalid("the poster picture has no size".into()))?; + let mut xobjects = Dict::new(); + xobjects.set(b"Pic".to_vec(), Object::Ref(p.xobject)); + let mut resources = Dict::new(); + resources.set(b"XObject".to_vec(), Object::Dict(xobjects)); + Ok(appearance::form(rect, format!("q {place} /Pic Do Q\n").as_bytes(), resources)) +} + +/// A light frame with "3D model" centred in it, shrunk to fit (left out when even 2 pt won't). +fn placeholder_poster(rect: [f64; 4]) -> Stream { + let [x0, y0, x1, y1] = rect; + let (w, h) = (x1 - x0, y1 - y0); + let mut c = + format!("0.94 g {} {} {} {} re f\n0.45 G 1 w {} {} {} {} re S\n", n(x0), n(y0), n(w), n(h), n(x0 + 0.5), n(y0 + 0.5), n(w - 1.0), n(h - 1.0)); + let mut resources = Dict::new(); + let unit = appearance::text_width(PLACEHOLDER_LABEL, 1.0); + let size = if unit > 0.0 { (w * 0.8 / unit).min(h * 0.4).min(14.0) } else { 0.0 }; + if size >= 2.0 { + let (tx, ty) = (x0 + (w - unit * size) / 2.0, y0 + (h - size * 0.7) / 2.0); + c.push_str(&format!("BT 0.3 g /Helv {} Tf {} {} Td ({PLACEHOLDER_LABEL}) Tj ET\n", n(size), n(tx), n(ty))); + resources.set(b"Font".to_vec(), appearance::helvetica_resource()); + } + appearance::form(rect, c.as_bytes(), resources) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn model_size_is_capped() { + assert_eq!(check_model_size(&[0; 8], 8), Ok(())); + assert_eq!(check_model_size(&[0; 9], 8), Err(AnnotError::ModelTooLarge { size: 9, max: 8 })); + } + + #[test] + fn the_format_is_read_from_the_header() { + assert_eq!(ModelFormat::detect(b"U3D\0rest"), Ok(ModelFormat::U3d)); + assert_eq!(ModelFormat::detect(b"PRC8800"), Ok(ModelFormat::Prc)); + for junk in [&b""[..], b"U3D", b"U3DX", b"PR", b"%PDF-1.7", b"prc"] { + assert_eq!(ModelFormat::detect(junk), Err(AnnotError::NotAModel), "{junk:?}"); + } + } + + #[test] + fn the_placeholder_label_shrinks_to_fit_and_is_dropped_when_it_cannot() { + let wide = String::from_utf8(placeholder_poster([0.0, 0.0, 300.0, 200.0]).decoded().unwrap().to_vec()).unwrap(); + assert!(wide.contains("/Helv 14 Tf") && wide.contains("(3D model) Tj"), "{wide}"); + let tiny = placeholder_poster([0.0, 0.0, 4.0, 4.0]); + assert!(!String::from_utf8(tiny.decoded().unwrap().to_vec()).unwrap().contains("Tj")); + assert!(tiny.dict.get(b"Resources").and_then(Object::as_dict).is_none_or(|r| r.get(b"Font").is_none())); + } +} diff --git a/crates/automation/src/bookmark_io.rs b/crates/automation/src/bookmark_io.rs new file mode 100644 index 0000000000..2a06d912f4 --- /dev/null +++ b/crates/automation/src/bookmark_io.rs @@ -0,0 +1,209 @@ +//! Bookmark trees in and out of documents: `bookmark_export`, `bookmark_import`, `bookmark_copy`. +//! +//! The portable form is JSON: `{"bookmarks": [node, …]}`, where a node is `title`, `page` +//! (1-based, or `null` for one that goes nowhere), `view` (`{"type", "values"}`: a destination +//! type and its values in PDF user space, as the file stores them), `color`, `bold`, `italic`, +//! `open` and `children`. What export writes, import takes back unchanged. A file also carries +//! `"truncated": true` when the export left out bookmarks nested past the listing limit. +use crate::{Args, Automation, Result, ToolError, failed}; +use pdfcraft_engine::{DocId, Edit, OutlineNode}; +use serde_json::{Value, json}; + +/// Deepest nesting a bookmark tree may have (the listing reaches 33 levels). +const MAX_DEPTH: usize = 33; + +/// The keys a node may have: the ones [`node_json`] writes. +const NODE_KEYS: [&str; 8] = ["title", "page", "view", "color", "bold", "italic", "open", "children"]; + +impl Automation { + pub(crate) fn bookmark_export(&self, a: &Args) -> Result { + let id = self.doc(a)?.id; + let path = a.opt_path("path")?.unwrap_or_default(); + let tree = self.outline_of(id, &path)?; + let mut out = json!({ "bookmarks": tree.nodes.iter().map(node_json).collect::>(), "truncated": tree.truncated }); + if let Some(file) = a.opt_str("out")? { + let target = self.resolve(file, true)?; + let mut saved = json!({ "bookmarks": out["bookmarks"] }); + if tree.truncated { + saved["truncated"] = json!(true); + } + let text = serde_json::to_vec_pretty(&saved).map_err(failed)?; + crate::write_atomic(&target, &text)?; + out["path"] = json!(target); + } + Ok(out) + } + + pub(crate) fn bookmark_import(&mut self, a: &Args) -> Result { + let (nodes, truncated) = match (a.get("bookmarks"), a.opt_str("file")?) { + (Some(list), None) => (nodes_of(list)?, false), + (None, Some(file)) => { + let path = self.resolve(file, false)?; + let value: Value = serde_json::from_slice(&crate::read_input(&path)?).map_err(|e| failed(format!("not a bookmark file: {e}")))?; + let list = value.get("bookmarks").ok_or_else(|| failed("not a bookmark file: it has no \"bookmarks\" list"))?; + (nodes_of(list)?, value.get("truncated") == Some(&Value::Bool(true))) + } + _ => return Err(ToolError::InvalidArgs("give either bookmarks or file".into())), + }; + let mut out = self.write_tree(a, nodes)?; + out["truncated"] = json!(truncated); + Ok(out) + } + + pub(crate) fn bookmark_copy(&mut self, a: &Args) -> Result { + let from = a.int("from")?; + let from = DocId(u64::try_from(from).map_err(|_| ToolError::InvalidArgs("from must be positive".into()))?); + let path = a.opt_path("path")?.unwrap_or_default(); + let tree = self.outline_of(from, &path)?; + let mut out = self.write_tree(a, tree.nodes)?; + out["truncated"] = json!(tree.truncated); + Ok(out) + } + + fn outline_of(&self, id: DocId, path: &[usize]) -> Result { + match self.session.outline_tree(id, path) { + // The engine names the path 0-based; the caller gave it 1-based. + Some(tree) => tree.map_err(|e| match e { + pdfcraft_organize::OutlineError::NoSuchBookmark(_) => failed(format!("there is no bookmark at {:?}", one_based(path))), + e => failed(e), + }), + None => Err(failed(format!("document {} is not open, or its bookmarks can't be read for editing", id.0))), + } + } + + /// Write `nodes` into `doc` under `parent` at `position` (or in place of the outline with + /// `replace`), and say which of them went nowhere because the document lacks their page. + fn write_tree(&mut self, a: &Args, bookmarks: Vec) -> Result { + let parent = a.opt_path("parent")?.unwrap_or_default(); + let index = a.opt_int("position")?.map_or(usize::MAX, |p| usize::try_from(p.max(1) - 1).unwrap_or(usize::MAX)); + let replace = a.opt_bool("replace")?.unwrap_or(false); + let count = self.doc(a)?.info.pages.len(); + let unmapped = pdfcraft_engine::outline_unmapped(&bookmarks, count); + let mut out = self.apply(a, Edit::ImportBookmarks { parent, index, bookmarks, replace })?; + out["unmapped"] = json!(unmapped.iter().map(|p| one_based(p)).collect::>()); + Ok(out) + } +} + +fn one_based(path: &[usize]) -> Vec { + path.iter().map(|i| i.saturating_add(1)).collect() +} + +/// A node as JSON, leaving out what is at its default (no view, black, regular, no children). +fn node_json(n: &OutlineNode) -> Value { + let mut v = json!({ "title": n.title, "page": n.page.map(|p| p.saturating_add(1)), "open": n.open }); + if let Some((kind, values)) = &n.view { + v["view"] = json!({ "type": String::from_utf8_lossy(kind), "values": values }); + } + if let Some(c) = n.color { + v["color"] = json!(c); + } + if n.bold { + v["bold"] = json!(true); + } + if n.italic { + v["italic"] = json!(true); + } + if !n.children.is_empty() { + v["children"] = json!(n.children.iter().map(node_json).collect::>()); + } + v +} + +fn nodes_of(list: &Value) -> Result> { + let mut count = 0; + list_of(list, &[], &mut count) +} + +/// The nodes of one level; `at` is the 1-based path of their parent, for messages. +fn list_of(list: &Value, at: &[usize], count: &mut usize) -> Result> { + let items = list.as_array().ok_or_else(|| bad(at, "children must be a list of bookmarks"))?; + if at.len() >= MAX_DEPTH && !items.is_empty() { + return Err(bad(at, &format!("bookmarks may nest at most {MAX_DEPTH} levels deep"))); + } + *count = count.saturating_add(items.len()); + if *count > pdfcraft_engine::MAX_IMPORT_BOOKMARKS { + return Err(ToolError::InvalidArgs(format!("at most {} bookmarks can be imported at once", pdfcraft_engine::MAX_IMPORT_BOOKMARKS))); + } + let mut out = Vec::with_capacity(items.len()); + for (i, item) in items.iter().enumerate() { + let mut path = at.to_vec(); + path.push(i.saturating_add(1)); + out.push(node_of(item, &path, count)?); + } + Ok(out) +} + +/// One node and, through [`list_of`], its children (recursion bounded by `MAX_DEPTH`). +fn node_of(v: &Value, at: &[usize], count: &mut usize) -> Result { + let o = v.as_object().ok_or_else(|| bad(at, "must be an object"))?; + if let Some(k) = o.keys().find(|k| !NODE_KEYS.contains(&k.as_str())) { + return Err(bad(at, &format!("unknown key {k:?} (a bookmark has {})", NODE_KEYS.join(", ")))); + } + let field = |k: &str| o.get(k).filter(|x| !x.is_null()); + // An empty title is kept: files have them, and export writes them as "". + let title = field("title").and_then(Value::as_str).ok_or_else(|| bad(at, "needs a title"))?.to_string(); + let page = match field("page") { + None => None, + Some(p) => Some(p.as_u64().filter(|p| *p >= 1).and_then(|p| usize::try_from(p - 1).ok()).ok_or_else(|| bad(at, "page must be 1 or more"))?), + }; + let flag = |k: &str| field(k).map_or(Ok(None), |x| x.as_bool().map(Some).ok_or_else(|| bad(at, &format!("{k} must be true or false")))); + let color = match field("color") { + None => None, + Some(c) => { + let rgb: Option> = c.as_array().and_then(|c| c.iter().map(|x| x.as_f64().map(|x| x as f32)).collect()); + match rgb.as_deref() { + Some(&[r, g, b]) if [r, g, b].iter().all(|c| (0.0..=1.0).contains(c)) => Some([r, g, b]), + _ => return Err(bad(at, "color must be [r, g, b], each from 0 to 1")), + } + } + }; + let children = match field("children") { + None => Vec::new(), + Some(list) => list_of(list, at, count)?, + }; + Ok(OutlineNode { + title, + page, + view: field("view").map(|x| view_of(x, at)).transpose()?, + color, + italic: flag("italic")?.unwrap_or(false), + bold: flag("bold")?.unwrap_or(false), + open: flag("open")?.unwrap_or(true), + children, + }) +} + +/// `{"type": "XYZ", "values": [left, top, zoom]}`: a destination type with exactly its values. +fn view_of(v: &Value, at: &[usize]) -> Result<(Vec, Vec>)> { + let kind = v.get("type").and_then(Value::as_str).ok_or_else(|| bad(at, "view needs a type such as XYZ or Fit"))?; + let values = match v.get("values").filter(|x| !x.is_null()) { + None => Vec::new(), + Some(list) => { + let list = list.as_array().filter(|l| l.len() <= 4).ok_or_else(|| bad(at, "view values must be a list of at most 4 numbers or nulls"))?; + list.iter() + .map(|x| match x { + Value::Null => Ok(None), + x => x.as_f64().map(Some).ok_or_else(|| bad(at, "view values must be numbers or null")), + }) + .collect::>()? + } + }; + let arity = pdfcraft_engine::outline_view_arity(kind.as_bytes()).ok_or_else(|| bad(at, VIEW_TYPES))?; + if values.len() != arity { + return Err(bad(at, &format!("a {kind} view takes {arity} values (numbers, or null to keep the reader's)"))); + } + if kind == "FitR" && values.iter().any(Option::is_none) { + return Err(bad(at, "a FitR view needs all four values: left, bottom, right, top")); + } + Ok((kind.as_bytes().to_vec(), values)) +} + +const VIEW_TYPES: &str = "view type must be XYZ (3 values: left, top, zoom), Fit, FitB (none), FitH, FitV, FitBH, FitBV (1) or FitR (4)"; + +fn bad(at: &[usize], what: &str) -> ToolError { + if at.is_empty() { + return ToolError::InvalidArgs(format!("bookmarks: {what}")); + } + ToolError::InvalidArgs(format!("bookmark {at:?}: {what}")) +} diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index ae46be87d0..135d0868ee 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -75,7 +75,7 @@ fn to_user(p: &PageInfo, x: f64, y: f64) -> [f64; 2] { [ux as f64, uy as f64] } -fn rect_to_user(p: &PageInfo, r: [f64; 4]) -> [f64; 4] { +pub(crate) fn rect_to_user(p: &PageInfo, r: [f64; 4]) -> [f64; 4] { let (a, b) = (to_user(p, r[0], r[1]), to_user(p, r[2], r[3])); [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])] } diff --git a/crates/automation/src/dispatch.rs b/crates/automation/src/dispatch.rs index f3c51d22e0..d7aa15745a 100644 --- a/crates/automation/src/dispatch.rs +++ b/crates/automation/src/dispatch.rs @@ -196,6 +196,9 @@ impl Automation { out } "bookmark_list" => self.bookmark_listing(&a)?, + "bookmark_export" => self.bookmark_export(&a)?, + "bookmark_import" => self.bookmark_import(&a)?, + "bookmark_copy" => self.bookmark_copy(&a)?, "bookmark_add" => { let page = self.page(&a)?; let parent = a.opt_path("parent")?.unwrap_or_default(); @@ -769,6 +772,7 @@ impl Automation { "comment_list" => self.comment_list(&a)?, "comment_add" => self.comment_add(&a)?, "stamp_custom" => self.stamp_custom(&a)?, + "model_add" => self.model_add(&a)?, "comment_reply" => self.comment_reply(&a)?, "comment_set_status" => self.comment_set_status(&a)?, "sign_list" => self.sign_list(&a)?, diff --git a/crates/automation/src/file_access.rs b/crates/automation/src/file_access.rs index 067a593888..dab2760f5d 100644 --- a/crates/automation/src/file_access.rs +++ b/crates/automation/src/file_access.rs @@ -394,8 +394,18 @@ mod tests { // Helpers that are only called with a resolved path. `doc_batch` (batch.rs) resolves its // inputs and out_dir, walks only inside them (skipping links) and resolves every // discovered file again before `process` reads it or `publish` writes it. - const RESOLVED_BY_CALLER: &[&str] = - &["write_atomic", "write_atomic_with", "read_input", "read_input_bytes", "read_review_sidecar", "process", "linked", "walk", "publish"]; + const RESOLVED_BY_CALLER: &[&str] = &[ + "write_atomic", + "write_atomic_with", + "read_input", + "read_input_bytes", + "read_model", + "read_review_sidecar", + "process", + "linked", + "walk", + "publish", + ]; let mut dirs = vec![Path::new(env!("CARGO_MANIFEST_DIR")).join("src")]; let mut files = vec![]; while let Some(dir) = dirs.pop() { diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 5bcb032fce..3b6275d5c8 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -27,6 +27,7 @@ mod links; #[cfg(feature = "mcp")] pub mod mcp; mod measure; +mod model3d; mod printing; mod reading; mod redact; @@ -50,6 +51,7 @@ use file_access::foreign_share; use text_cache::TextCache; mod assemble; +mod bookmark_io; mod dispatch; mod documents; mod error; diff --git a/crates/automation/src/mcp.rs b/crates/automation/src/mcp.rs index 84cc21b258..b033871867 100644 --- a/crates/automation/src/mcp.rs +++ b/crates/automation/src/mcp.rs @@ -557,7 +557,7 @@ fn tool_json(t: &crate::ToolDef) -> Value { "title": t.title, "description": t.description, "inputSchema": t.input_schema, - "annotations": { "title": t.title, "readOnlyHint": t.read_only, "destructiveHint": t.destructive, "idempotentHint": t.read_only || matches!(t.name, "doc_save" | "doc_optimize" | "doc_export_data" | "measure_export" | "doc_export_images" | "doc_export_office" | "doc_export_all_images" | "doc_export_text" | "image_save"), "openWorldHint": false }, + "annotations": { "title": t.title, "readOnlyHint": t.read_only, "destructiveHint": t.destructive, "idempotentHint": t.read_only || matches!(t.name, "doc_save" | "doc_optimize" | "doc_export_data" | "measure_export" | "doc_export_images" | "doc_export_office" | "doc_export_all_images" | "doc_export_text" | "image_save" | "bookmark_export"), "openWorldHint": false }, }) } diff --git a/crates/automation/src/model3d.rs b/crates/automation/src/model3d.rs new file mode 100644 index 0000000000..2d89e4ae60 --- /dev/null +++ b/crates/automation/src/model3d.rs @@ -0,0 +1,114 @@ +//! 3D model tools (Acrobat: Rich Media ▸ Add 3D, AGENTS.md §3). + +use std::path::Path; + +use pdfcraft_engine::{Activation3d, Edit, MAX_3D_MODEL_BYTES, MarkFile, Model3dFormat}; +use serde_json::Value; + +use crate::comments::rect_to_user; +use crate::{Args, Automation, Result, ToolError, failed}; + +/// The largest side of a 3D model's box, in points: the largest page PDF allows (ISO 32000-2 Annex C). +const MAX_MODEL_SIDE: f64 = 14_400.0; + +impl Automation { + /// A U3D or PRC model in `rect` on `page`, its poster the picture at `poster` (a PDF page or an + /// image) fitted inside the box, or a labelled frame. + pub(crate) fn model_add(&mut self, a: &Args) -> Result { + let page = self.page(a)?; + let info = self.doc(a)?.info.pages.get(page).cloned().ok_or_else(|| ToolError::InvalidArgs(format!("page {} does not exist", page + 1)))?; + let r = a.nums::<4>("rect")?.ok_or_else(|| ToolError::InvalidArgs("missing argument rect".into()))?; + let (w, h) = ((r[2] - r[0]).abs(), (r[3] - r[1]).abs()); + if !((1.0..=MAX_MODEL_SIDE).contains(&w) && (1.0..=MAX_MODEL_SIDE).contains(&h)) { + return Err(ToolError::InvalidArgs(format!( + "rect must be [x0, y0, x1, y1] with each side at least 1 pt and at most {MAX_MODEL_SIDE} pt" + ))); + } + let activation = match a.opt_str("activation")? { + None => Activation3d::default(), + Some(word) => Activation3d::ALL + .iter() + .find(|(_, w)| *w == word) + .map(|(act, _)| *act) + .ok_or_else(|| ToolError::InvalidArgs(format!("activation must be one of: {}", Activation3d::ALL.map(|(_, w)| w).join(", "))))?, + }; + let path = self.resolve(a.str("path")?, false)?; + let model = read_model(&path, MAX_3D_MODEL_BYTES)?; + let file = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + let name = a.opt_str("name")?.map(str::to_owned).unwrap_or_else(|| file.rsplit_once('.').map_or(file.clone(), |(s, _)| s.to_owned())); + let poster = match a.opt_str("poster")? { + None => None, + Some(p) => { + let path = self.resolve(p, false)?; + let bytes = crate::read_input(&path)?; + let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + let page = usize::try_from(a.opt_int("poster_page")?.unwrap_or(1).max(1)).unwrap_or(1).saturating_sub(1); + Some(MarkFile { name, bytes: std::sync::Arc::new(bytes), page }) + } + }; + let edit = Edit::AddModel3d { page, rect: rect_to_user(&info, r), model: std::sync::Arc::new(model), poster, activation, name }; + self.apply(a, edit) + } +} + +/// Read a resolved model file of at most `max` bytes. Its size (from the file system) and its first +/// bytes are checked before the rest is read, so a large file that is not a model is never loaded. +fn read_model(path: &Path, max: usize) -> Result> { + use std::io::Read as _; + let io = |e: std::io::Error| failed(format!("{}: {e}", path.display())); + let too_large = |size: u64| failed(format!("the 3D model is {size} bytes; the largest accepted is {max}")); + let cap = u64::try_from(max).unwrap_or(u64::MAX); + let mut file = std::fs::File::open(path).map_err(io)?; + if let Ok(meta) = file.metadata() + && meta.is_file() + && meta.len() > cap + { + return Err(too_large(meta.len())); + } + let mut bytes = Vec::new(); + (&mut file).take(4).read_to_end(&mut bytes).map_err(io)?; + Model3dFormat::detect(&bytes).map_err(failed)?; + let rest = cap.saturating_add(1).saturating_sub(bytes.len() as u64); + file.take(rest).read_to_end(&mut bytes).map_err(io)?; + if bytes.len() > max { + return Err(failed(format!("the 3D model is more than {max} bytes, the largest accepted"))); + } + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn file(dir: &Path, name: &str, bytes: &[u8]) -> std::path::PathBuf { + let path = dir.join(name); + std::fs::write(&path, bytes).unwrap(); + path + } + + #[test] + fn a_model_is_read_whole_when_it_fits() { + let dir = tempfile::tempdir().unwrap(); + let mut prc = b"PRC".to_vec(); + prc.extend_from_slice(&[7; 40]); + assert_eq!(read_model(&file(dir.path(), "m.prc", &prc), 64).unwrap(), prc); + assert_eq!(read_model(&file(dir.path(), "m.u3d", b"U3D\0x"), 6).unwrap(), b"U3D\0x"); + } + + #[test] + fn a_model_past_the_cap_is_refused_by_its_size_before_it_is_read() { + let dir = tempfile::tempdir().unwrap(); + let path = file(dir.path(), "big.prc", &[b'P', b'R', b'C', 0, 0, 0, 0, 0, 0, 0]); + let err = read_model(&path, 4).unwrap_err().to_string(); + assert!(err.contains("is 10 bytes") && err.contains("largest accepted is 4"), "the file's own size, from the file system: {err}"); + } + + #[test] + fn a_file_that_is_not_a_model_is_refused_by_its_header() { + let dir = tempfile::tempdir().unwrap(); + for junk in [&b"%PDF-1.7 a page"[..], b"PR", b"", b"U3DX and more"] { + let err = read_model(&file(dir.path(), "junk.bin", junk), 64).unwrap_err().to_string(); + assert!(err.contains("not a 3D model"), "{junk:?}: {err}"); + } + } +} diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index fd6ec1a940..dd4b0f14c3 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -323,6 +323,50 @@ pub fn tools() -> Vec { }), &["doc", "title", "page"], )), + t( + "bookmark_export", + "Export bookmarks", + "The bookmark tree, or the subtree at path, as a list of nodes bookmark_import takes back unchanged. A node has title, page (1-based, null when it goes nowhere), open, and when set view ({type, values}: a destination type from ISO 32000-2 §12.3.2.2 Table 149 and its values in PDF user space, as the file stores them), color ([r, g, b], 0-1), bold, italic and children. truncated is true when bookmarks nested deeper than 33 levels were left out. With out, the tree is also written there as a JSON file {\"bookmarks\": [...]}, with \"truncated\": true when that is so; out is overwritten.", + ) + .destructive() + .with(schema( + json!({ "doc": doc(), "path": path("The bookmark whose subtree to export (omit for the whole outline)"), "out": path_arg() }), + &["doc"], + )), + t( + "bookmark_import", + "Import bookmarks", + "Add a tree of bookmarks, as bookmark_export gives it (bookmarks) or as a JSON file it wrote (file), under a parent bookmark (or at the top level) at a position, or with replace in place of the whole outline. A bookmark whose page this document lacks is kept without a destination and listed in unmapped (1-based paths into the given tree). A view takes exactly its type's values (XYZ 3, FitR 4, FitH/FitV/FitBH/FitBV 1, Fit/FitB none). Give exactly one of bookmarks and file. A title may be empty but not missing; a key a node does not have is refused. Every node is checked before anything is written: at most 100000 bookmarks, nested at most 33 levels counting the parent's. truncated is true when the file says its export left bookmarks out. Undoable.", + ) + .destructive() + .with(schema( + json!({ + "doc": doc(), + "bookmarks": { "type": "array", "items": { "type": "object" }, "minItems": 1, "description": "Nodes as bookmark_export returns them." }, + "file": path_arg(), + "parent": path("Parent bookmark (omit for the top level)"), + "position": { "type": "integer", "minimum": 1, "description": "1-based position among the parent's children (default: last)." }, + "replace": { "type": "boolean", "description": "Replace the document's bookmarks instead of adding to them (default false; leave parent out)." } + }), + &["doc"], + )), + t( + "bookmark_copy", + "Copy bookmarks from another document", + "Copy the bookmarks of document from (or the subtree at path) into doc, by page number: under a parent bookmark (or at the top level) at a position, or with replace in place of doc's outline. A bookmark whose page doc lacks is kept without a destination and listed in unmapped (1-based paths into the copied tree). truncated is true when bookmarks nested deeper than 33 levels were left out of the copy. Undoable.", + ) + .destructive() + .with(schema( + json!({ + "doc": doc(), + "from": { "type": "integer", "minimum": 1, "description": "Id of the document to copy from." }, + "path": path("The bookmark whose subtree to copy (omit for the whole outline)"), + "parent": path("Parent bookmark in doc (omit for the top level)"), + "position": { "type": "integer", "minimum": 1, "description": "1-based position among the parent's children (default: last)." }, + "replace": { "type": "boolean", "description": "Replace doc's bookmarks instead of adding to them (default false; leave parent out)." } + }), + &["doc", "from"], + )), t("bookmark_rename", "Rename a bookmark", "Change a bookmark's title. Undoable.") .with(schema(json!({ "doc": doc(), "path": path("The bookmark"), "title": { "type": "string", "minLength": 1 } }), &["doc", "path", "title"])), t("bookmark_delete", "Delete a bookmark", "Delete a bookmark and the bookmarks under it. Undoable.") @@ -469,6 +513,20 @@ pub fn tools() -> Vec { }), &["doc", "page", "path"], )), + t("model_add", "Add a 3D model", "Add a 3D model (Acrobat: Rich Media ▸ Add 3D): the U3D or PRC file at path, recognised by its first bytes, as a 3D annotation in rect [x0, y0, x1, y1] (points from the top-left of the displayed page; each side 1–14400 pt; at most 256 MiB). Its poster, what prints and what readers show until the model is activated, is the picture at poster (a PDF page, poster_page default 1, or an image) fitted inside rect keeping its shape and centred; without poster, a frame labelled \"3D model\". activation: click (default), page_open or page_visible. name is its description (default: the file name). No views are added: readers open the model on its own default view. Undoable.") + .with(schema( + json!({ + "doc": doc(), + "page": { "type": "integer", "minimum": 1 }, + "path": { "type": "string" }, + "rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 }, + "poster": { "type": "string" }, + "poster_page": { "type": "integer", "minimum": 1 }, + "activation": { "type": "string", "enum": ["click", "page_open", "page_visible"] }, + "name": { "type": "string" }, + }), + &["doc", "page", "path", "rect"], + )), t("form_set_image", "Set an image field's picture", "Show an image file (PNG, JPEG, TIFF, GIF or BMP) in an image field or button, scaled to fit and centred (what clicking an image field does). On an XFA form the picture is kept in its data too (base64, as Designer saves it) and the form shows it from there. Undoable.") .with(schema(json!({ "doc": doc(), "field": { "type": "string" }, "path": { "type": "string" } }), &["doc", "field", "path"])), t( diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs index 861664d842..c7e4835590 100644 --- a/crates/automation/tests/it/automation.rs +++ b/crates/automation/tests/it/automation.rs @@ -10300,6 +10300,31 @@ fn changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_ assert_eq!(saved_selection_indices(&dir.join("letters.pdf"), "letters"), Some(vec![0, 1]), "B and C are now items 0 and 1"); } +/// Clearing a field's length limit through `form_set_props` (`"max_length": 0`) leaves it with +/// none, even when its parent field has one it would otherwise inherit. +#[test] +fn clearing_a_kids_length_limit_through_tools_does_not_bring_back_its_parents() { + let objs = [ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 400 300] >>", + "<< /Type /Page /Parent 2 0 R /Annots [6 0 R] >>", + "<< /Fields [5 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >>", + "<< /T (address) /FT /Tx /MaxLen 3 /Kids [6 0 R] >>", + "<< /Type /Annot /Subtype /Widget /T (city) /Parent 5 0 R /MaxLen 8 /Rect [10 250 150 268] /P 3 0 R >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>", + ]; + let dir = workdir("kid-max-len"); + std::fs::write(dir.join("form.pdf"), pdf_of(&objs)).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "form.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "address.city", "max_length": 0 })); + ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "address.city": "Absalom" } })); + let fields = ok(&mut a, "form_fields", json!({ "doc": doc })); + let city = fields["fields"].as_array().unwrap().iter().find(|f| f["name"] == "address.city").unwrap().clone(); + assert!(city.get("max_length").is_none(), "no limit, not the parent's 3: {city}"); + assert_eq!(city["value"], "Absalom", "the value is not cut to the parent's 3 characters"); +} + /// Clearing a field's default through `form_set_props` (`"default": ""`) leaves it with none, /// even when its parent field has one it would otherwise inherit. #[test] @@ -10748,3 +10773,168 @@ fn multiline_fill_sign_engine_round_trip_preserves_lines_and_appearance() { assert_eq!(rendered, re.call("page_render", &json!({"doc":reopened,"page":1,"dpi":72})).unwrap(), "saved appearances are identical"); std::fs::remove_dir_all(dir).unwrap(); } + +/// Export, then import into another document and copy between open documents: the tree comes +/// back unchanged, a page the target lacks is reported, and the import undoes in one step. +#[test] +fn bookmarks_export_import_and_copy_between_documents() { + let dir = workdir("bookmark-io"); + let mut a = auto(&dir); + let src = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "bookmark_add", json!({ "doc": src, "title": "Intro", "page": 1 })); + ok(&mut a, "bookmark_add", json!({ "doc": src, "title": "Detail", "page": 3, "parent": [1] })); + ok(&mut a, "bookmark_add", json!({ "doc": src, "title": "End", "page": 2 })); + ok(&mut a, "bookmark_set_style", json!({ "doc": src, "path": [1], "color": [1, 0, 0], "bold": true })); + let exported = ok(&mut a, "bookmark_export", json!({ "doc": src, "out": "marks.json" })); + assert_eq!(exported["truncated"], false); + let tree = exported["bookmarks"].clone(); + assert_eq!((tree[0]["title"].clone(), tree[0]["color"].clone(), tree[0]["bold"].clone()), (json!("Intro"), json!([1.0, 0.0, 0.0]), json!(true))); + assert_eq!((tree[0]["children"][0]["title"].clone(), tree[0]["children"][0]["page"].clone()), (json!("Detail"), json!(3))); + assert_eq!(tree[0]["view"]["type"], "XYZ"); + let subtree = ok(&mut a, "bookmark_export", json!({ "doc": src, "path": [1, 1] }))["bookmarks"].clone(); + assert_eq!(subtree, json!([tree[0]["children"][0]])); + + // b.pdf has two pages: Detail goes to the third, so it arrives without a destination. + let dst = ok(&mut a, "doc_open", json!({ "path": "b.pdf" }))["doc"].as_u64().unwrap(); + let imported = ok(&mut a, "bookmark_import", json!({ "doc": dst, "file": "marks.json" })); + assert_eq!(imported["unmapped"], json!([[1, 1]])); + let back = ok(&mut a, "bookmark_export", json!({ "doc": dst }))["bookmarks"].clone(); + assert_eq!(back[0]["children"][0]["page"], json!(null)); + assert_eq!((back[0]["title"].clone(), back[1].clone()), (tree[0]["title"].clone(), tree[1].clone())); + ok(&mut a, "edit_undo", json!({ "doc": dst })); + assert_eq!(ok(&mut a, "bookmark_list", json!({ "doc": dst }))["bookmarks"], json!([])); + + // Into a document with every page, the tree is unchanged, wherever it is copied from. + let same = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "bookmark_add", json!({ "doc": same, "title": "Old", "page": 1 })); + let copied = ok(&mut a, "bookmark_copy", json!({ "doc": same, "from": src, "replace": true })); + assert_eq!(copied["unmapped"], json!([])); + assert_eq!(ok(&mut a, "bookmark_export", json!({ "doc": same }))["bookmarks"], tree); + ok(&mut a, "bookmark_import", json!({ "doc": same, "bookmarks": subtree, "parent": [2], "position": 1 })); + assert_eq!(ok(&mut a, "bookmark_list", json!({ "doc": same }))["bookmarks"][1]["children"][0]["title"], "Detail"); +} + +/// A one-page document whose outline has an untitled item, a /FitR destination with three of +/// its four numbers, and a chain nested `deep` levels below them. +fn odd_outline(deep: usize) -> Vec { + let first_deep = 7; + let mut objs = vec![ + "<< /Type /Catalog /Pages 2 0 R /Outlines 4 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".into(), + "<< /Type /Page /Parent 2 0 R >>".into(), + "<< /Type /Outlines /First 5 0 R /Last 6 0 R >>".into(), + "<< /Title () /Parent 4 0 R /Next 6 0 R /Dest [3 0 R /Fit] >>".into(), + format!( + "<< /Title (Short) /Parent 4 0 R /Prev 5 0 R /Dest [3 0 R /FitR 0 0 10]{} >>", + if deep > 0 { format!(" /First {first_deep} 0 R /Last {first_deep} 0 R") } else { String::new() } + ), + ]; + for i in 0..deep { + let (me, parent) = (first_deep + i, if i == 0 { 6 } else { first_deep + i - 1 }); + let child = if i + 1 < deep { format!(" /First {0} 0 R /Last {0} 0 R", me + 1) } else { String::new() }; + objs.push(format!("<< /Title (Level) /Parent {parent} 0 R /Dest [3 0 R /Fit]{child} >>")); + } + pdf_of(&objs.iter().map(String::as_str).collect::>()) +} + +/// What a real file holds and the JSON form can't say exactly still copies and round-trips: an +/// empty title stays empty, a short /FitR keeps its page and drops the view, and bookmarks past +/// the listing depth are reported as left out at every step. +#[test] +fn odd_bookmarks_round_trip_and_a_cut_tree_says_so() { + let dir = workdir("bookmark-io-odd"); + std::fs::write(dir.join("odd.pdf"), odd_outline(0)).unwrap(); + std::fs::write(dir.join("deep.pdf"), odd_outline(40)).unwrap(); + let mut a = auto(&dir); + let src = ok(&mut a, "doc_open", json!({ "path": "odd.pdf" }))["doc"].as_u64().unwrap(); + let tree = ok(&mut a, "bookmark_export", json!({ "doc": src, "out": "odd.json" }))["bookmarks"].clone(); + assert_eq!((tree[0]["title"].clone(), tree[1]["page"].clone(), tree[1].get("view")), (json!(""), json!(1), None)); + let dst = ok(&mut a, "doc_open", json!({ "path": "b.pdf" }))["doc"].as_u64().unwrap(); + assert_eq!(ok(&mut a, "bookmark_import", json!({ "doc": dst, "file": "odd.json", "replace": true }))["truncated"], false); + // A destination with no view is written as XYZ with nulls: the same page, the reader's view. + let back = ok(&mut a, "bookmark_export", json!({ "doc": dst }))["bookmarks"].clone(); + assert_eq!((back[0].clone(), back[1]["title"].clone(), back[1]["page"].clone()), (tree[0].clone(), json!("Short"), json!(1))); + assert_eq!(back[1]["view"], json!({ "type": "XYZ", "values": [null, null, null] })); + let copied = ok(&mut a, "bookmark_copy", json!({ "doc": dst, "from": src, "replace": true })); + assert_eq!((copied["unmapped"].clone(), copied["truncated"].clone()), (json!([]), json!(false))); + match a.call("bookmark_export", &json!({ "doc": src, "path": [3] })) { + Err(ToolError::Failed(m)) => assert!(m.contains("no bookmark at [3]"), "{m}"), + other => panic!("expected no bookmark, got {other:?}"), + } + + let deep = ok(&mut a, "doc_open", json!({ "path": "deep.pdf" }))["doc"].as_u64().unwrap(); + assert_eq!(ok(&mut a, "bookmark_export", json!({ "doc": deep, "out": "deep.json" }))["truncated"], true); + let saved: Value = serde_json::from_slice(&std::fs::read(dir.join("deep.json")).unwrap()).unwrap(); + assert_eq!(saved["truncated"], true); + assert_eq!(ok(&mut a, "bookmark_import", json!({ "doc": dst, "file": "deep.json", "replace": true }))["truncated"], true); + assert_eq!(ok(&mut a, "bookmark_copy", json!({ "doc": dst, "from": deep, "replace": true }))["truncated"], true); + let export = tools().into_iter().find(|t| t.name == "bookmark_export").unwrap(); + assert!(export.destructive && !export.read_only, "bookmark_export overwrites out"); + std::fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn a_misspelt_bookmark_key_is_refused_not_dropped() { + let dir = workdir("bookmark-io-key"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let tree = json!([{ "title": "Parent", "children": [{ "title": "Red", "page": 1, "colour": [1, 0, 0] }] }]); + match a.call("bookmark_import", &json!({ "doc": doc, "bookmarks": tree })) { + Err(ToolError::InvalidArgs(m)) => assert!(m.contains("[1, 1]") && m.contains("\"colour\""), "{m}"), + other => panic!("expected InvalidArgs, got {other:?}"), + } + assert_eq!(ok(&mut a, "bookmark_list", json!({ "doc": doc }))["bookmarks"], json!([])); + std::fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn a_bad_bookmark_tree_is_refused_whole_and_names_the_node() { + let dir = workdir("bookmark-io-bad"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let refused = |a: &mut Automation, bookmarks: Value| match a.call("bookmark_import", &json!({ "doc": doc, "bookmarks": bookmarks })) { + Err(ToolError::InvalidArgs(m)) => m, + other => panic!("expected InvalidArgs, got {other:?}"), + }; + let fine = json!({ "title": "Fine", "page": 1 }); + let m = refused(&mut a, json!([fine, { "title": "Parent", "children": [{ "page": 1 }] }])); + assert!(m.contains("[2, 1]") && m.contains("title"), "{m}"); + let m = refused(&mut a, json!([{ "title": 5 }])); + assert!(m.contains("[1]") && m.contains("title"), "{m}"); + let m = refused(&mut a, json!([fine, { "title": "Bad", "page": 0 }])); + assert!(m.contains("[2]") && m.contains("page"), "{m}"); + let m = refused(&mut a, json!([{ "title": "Bad", "color": [2, 0, 0] }])); + assert!(m.contains("color"), "{m}"); + let m = refused(&mut a, json!([{ "title": "Bad", "view": { "type": "XYZ", "values": [1, 2, 3, 4, 5] } }])); + assert!(m.contains("view"), "{m}"); + let mut deep = json!({ "title": "Leaf" }); + for _ in 0..40 { + deep = json!({ "title": "Level", "children": [deep] }); + } + assert!(refused(&mut a, json!([deep])).contains("33 levels")); + let m = refused(&mut a, json!([fine, { "title": "Z", "page": 1, "view": { "type": "Zoom" } }])); + assert!(m.contains("[2]") && m.contains("XYZ (3 values"), "{m}"); + let m = refused(&mut a, json!([fine, { "title": "Z", "page": 1, "view": { "type": "FitH", "values": [1, 2] } }])); + assert!(m.contains("[2]") && m.contains("FitH view takes 1"), "{m}"); + let m = refused(&mut a, json!([{ "title": "R", "view": { "type": "FitR", "values": [0, null, 1, 1] } }])); + assert!(m.contains("all four"), "{m}"); + assert!(matches!(a.call("bookmark_import", &json!({ "doc": doc })), Err(ToolError::InvalidArgs(_)))); + assert!(matches!(a.call("bookmark_import", &json!({ "doc": doc, "bookmarks": [fine], "file": "x.json" })), Err(ToolError::InvalidArgs(_)))); + // A real bookmark file outside the root is refused for being outside, not for being missing. + let outside = dir.with_extension("outside.json"); + std::fs::write(&outside, br#"{"bookmarks": [{"title": "Outside", "page": 1}]}"#).unwrap(); + let name = format!("../{}", outside.file_name().unwrap().to_string_lossy()); + match a.call("bookmark_import", &json!({ "doc": doc, "file": name })) { + Err(ToolError::Failed(m)) => assert!(m.contains("outside the allowed directory"), "{m}"), + other => panic!("expected the root to refuse it, got {other:?}"), + } + let _ = std::fs::remove_file(&outside); + let out = dir.with_extension("out.json"); + let name = format!("../{}", out.file_name().unwrap().to_string_lossy()); + match a.call("bookmark_export", &json!({ "doc": doc, "out": name })) { + Err(ToolError::Failed(m)) => assert!(m.contains("outside the allowed directory"), "{m}"), + other => panic!("expected the root to refuse it, got {other:?}"), + } + assert!(!out.exists()); + assert_eq!(ok(&mut a, "bookmark_list", json!({ "doc": doc }))["bookmarks"], json!([])); +} diff --git a/crates/automation/tests/it/automation_roots.rs b/crates/automation/tests/it/automation_roots.rs index a96870ca02..e65c582a73 100644 --- a/crates/automation/tests/it/automation_roots.rs +++ b/crates/automation/tests/it/automation_roots.rs @@ -145,6 +145,8 @@ fn tools_that_read_a_path_are_held_to_the_read_root() { ok(&mut server, "doc_open", json!({"path": s(&base.join("in/a.pdf"))})); let (secret, pic, gone) = (s(&base.join("outside/secret.pdf")), s(&base.join("outside/pic.png")), s(&base.join("outside/gone.bin"))); let out = s(&base.join("out/result.pdf")); + std::fs::write(base.join("in/m.prc"), b"PRC model").unwrap(); + let model = s(&base.join("in/m.prc")); for (tool, args) in [ ("page_insert_file", json!({"doc": 1, "path": secret, "at": 1})), ("page_replace", json!({"doc": 1, "pages": [1], "path": secret})), @@ -153,6 +155,8 @@ fn tools_that_read_a_path_are_held_to_the_read_root() { ("doc_create", json!({"from": "images", "paths": [pic]})), ("stamp_custom", json!({"doc": 1, "page": 1, "path": secret, "at": [10, 10]})), ("stamp_custom", json!({"doc": 1, "page": 1, "path": secret, "rect": [10, 10, 493, 300]})), + ("model_add", json!({"doc": 1, "page": 1, "path": secret, "rect": [10, 10, 100, 100]})), + ("model_add", json!({"doc": 1, "page": 1, "path": model, "rect": [10, 10, 100, 100], "poster": pic})), ("page_add_image", json!({"doc": 1, "page": 1, "path": pic})), ("image_edit", json!({"doc": 1, "page": 1, "image": 1, "action": "replace", "path": pic})), ("comment_add", json!({"doc": 1, "page": 1, "type": "attachment", "at": [10, 10], "path": secret})), diff --git a/crates/automation/tests/it/main.rs b/crates/automation/tests/it/main.rs index d36dbe93a9..f396601edb 100644 --- a/crates/automation/tests/it/main.rs +++ b/crates/automation/tests/it/main.rs @@ -8,6 +8,7 @@ mod batch; mod button_caption; mod form_bulk; mod hostile_args; +mod model_3d; mod optimize_content_boundaries; mod redact_after_stray_q_form; mod redact_text_after_q; diff --git a/crates/automation/tests/it/model_3d.rs b/crates/automation/tests/it/model_3d.rs new file mode 100644 index 0000000000..179192f421 --- /dev/null +++ b/crates/automation/tests/it/model_3d.rs @@ -0,0 +1,120 @@ +//! `model_add` embeds a U3D or PRC model as a 3D annotation (ISO 32000-2 §13.6.2) with a poster, +//! end to end: the tool, the saved file, undo, and the refusals. + +use pdfcraft_automation::{Automation, Content, ToolError}; +use pdfcraft_cos::{Dict, Document, Object, Stream}; +use serde_json::{Value, json}; + +fn tool(a: &mut Automation, name: &str, args: Value) -> Value { + match a.call(name, &args).unwrap().remove(0) { + Content::Json(value) => value, + other => panic!("expected JSON: {other:?}"), + } +} + +fn png(w: u32, h: u32) -> Vec { + let mut out = Vec::new(); + let mut encoder = png::Encoder::new(&mut out, w, h); + encoder.set_color(png::ColorType::Rgb); + encoder.set_depth(png::BitDepth::Eight); + encoder.write_header().unwrap().write_image_data(&vec![90; (w * h * 3) as usize]).unwrap(); + out +} + +fn stream(doc: &Document, obj: &Object) -> Stream { + match doc.resolve(obj).as_ref() { + Object::Stream(s) => s.clone(), + other => panic!("expected a stream: {other:?}"), + } +} + +/// The 3D annotations on the first page of the saved file. +fn saved_3d(dir: &std::path::Path) -> (Document, Vec) { + let doc = Document::open(std::sync::Arc::new(std::fs::read(dir.join("out.pdf")).unwrap())).unwrap(); + let page = &pdfcraft_model::pages(&doc)[0]; + let list = page.dict.get(b"Annots").map(|a| doc.resolve(a).as_array().cloned().unwrap()).unwrap_or_default(); + let found = list.iter().map(|o| doc.resolve(o).as_dict().unwrap().clone()).filter(|d| d.name(b"Subtype") == Some(&b"3D"[..])).collect(); + (doc, found) +} + +#[test] +fn model_add_embeds_the_model_with_a_fitted_poster() { + let dir = tempfile::tempdir().unwrap(); + let mut prc = b"PRC".to_vec(); + prc.extend_from_slice(&[7; 200]); + std::fs::write(dir.path().join("bracket.prc"), &prc).unwrap(); + std::fs::write(dir.path().join("poster.png"), png(80, 40)).unwrap(); + let mut a = Automation::new().with_root(dir.path()).unwrap(); + let doc = tool(&mut a, "doc_create", json!({ "from": "blank", "width": 600, "height": 800 }))["doc"].as_u64().unwrap(); + // A 300 × 300 box from the top-left of the page: user space [100, 400, 400, 700]. + let call = + json!({ "doc": doc, "page": 1, "path": "bracket.prc", "rect": [100, 100, 400, 400], "poster": "poster.png", "activation": "page_open" }); + tool(&mut a, "model_add", call); + tool(&mut a, "doc_save", json!({ "doc": doc, "path": "out.pdf" })); + + let (saved, found) = saved_3d(dir.path()); + let [annot] = found.as_slice() else { panic!("one 3D annotation: {found:?}") }; + let rect: Vec = annot.get(b"Rect").unwrap().as_array().unwrap().iter().filter_map(Object::as_f64).collect(); + assert_eq!(rect, [100.0, 400.0, 400.0, 700.0]); + assert_eq!(annot.get(b"Contents").and_then(Object::as_string).map(|s| s.to_text()).as_deref(), Some("bracket")); + let model = stream(&saved, annot.get(b"3DD").unwrap()); + assert_eq!(model.dict.name(b"Type"), Some(&b"3D"[..])); + assert_eq!(model.dict.name(b"Subtype"), Some(&b"PRC"[..])); + assert_eq!(model.dict.name(b"Filter"), Some(&b"FlateDecode"[..])); + assert_eq!(model.decoded().unwrap().to_vec(), prc, "the model is stored unchanged"); + let activation = saved.resolve(annot.get(b"3DA").unwrap()).as_dict().unwrap().clone(); + assert_eq!(activation.name(b"A"), Some(&b"PO"[..])); + // The 80 × 40 picture fitted in the 300 × 300 box: 300 × 150, centred top to bottom. + let ap = saved.resolve(annot.get(b"AP").unwrap()).as_dict().unwrap().clone(); + let poster = stream(&saved, ap.get(b"N").unwrap()); + let content = String::from_utf8(poster.decoded().unwrap().to_vec()).unwrap(); + assert!(content.contains("300 0 0 150 100 475 cm /Pic Do"), "{content}"); + + assert_eq!(tool(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Add 3D model"); + tool(&mut a, "doc_save", json!({ "doc": doc, "path": "out.pdf" })); + assert!(saved_3d(dir.path()).1.is_empty(), "undo removes it"); +} + +#[test] +fn model_add_without_a_poster_draws_a_labelled_frame_and_activates_on_click() { + let dir = tempfile::tempdir().unwrap(); + let mut u3d = b"U3D\0".to_vec(); + u3d.extend_from_slice(&[0; 60]); + std::fs::write(dir.path().join("part.u3d"), &u3d).unwrap(); + let mut a = Automation::new().with_root(dir.path()).unwrap(); + let doc = tool(&mut a, "doc_create", json!({ "from": "blank", "width": 600, "height": 800 }))["doc"].as_u64().unwrap(); + tool(&mut a, "model_add", json!({ "doc": doc, "page": 1, "path": "part.u3d", "rect": [50, 50, 250, 200], "name": "Part" })); + tool(&mut a, "doc_save", json!({ "doc": doc, "path": "out.pdf" })); + + let (saved, found) = saved_3d(dir.path()); + let [annot] = found.as_slice() else { panic!("one 3D annotation: {found:?}") }; + assert_eq!(stream(&saved, annot.get(b"3DD").unwrap()).dict.name(b"Subtype"), Some(&b"U3D"[..])); + let activation = saved.resolve(annot.get(b"3DA").unwrap()).as_dict().unwrap().clone(); + assert_eq!(activation.name(b"A"), Some(&b"XA"[..]), "the standard's default, stated"); + let ap = saved.resolve(annot.get(b"AP").unwrap()).as_dict().unwrap().clone(); + let content = String::from_utf8(stream(&saved, ap.get(b"N").unwrap()).decoded().unwrap().to_vec()).unwrap(); + assert!(content.contains("re S") && content.contains("(3D model) Tj"), "{content}"); +} + +#[test] +fn model_add_refuses_what_is_not_a_model_and_adds_nothing() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("notes.txt"), b"PR not a model").unwrap(); + std::fs::write(dir.path().join("part.prc"), b"PRC....").unwrap(); + let mut a = Automation::new().with_root(dir.path()).unwrap(); + let doc = tool(&mut a, "doc_create", json!({ "from": "blank", "width": 600, "height": 800 }))["doc"].as_u64().unwrap(); + let not_a_model = a.call("model_add", &json!({ "doc": doc, "page": 1, "path": "notes.txt", "rect": [10, 10, 100, 100] })); + assert!(matches!(¬_a_model, Err(ToolError::Failed(m)) if m.contains("U3D") && m.contains("PRC")), "{not_a_model:?}"); + for (args, why) in [ + (json!({ "rect": [10, 10, 10, 100] }), "zero width"), + (json!({ "rect": [0, 0, 20000, 100] }), "wider than a PDF page can be"), + (json!({}), "no rect"), + (json!({ "rect": [10, 10, 100, 100], "activation": "now" }), "an unknown activation"), + ] { + let mut call = json!({ "doc": doc, "page": 1, "path": "part.prc" }); + call.as_object_mut().unwrap().extend(args.as_object().unwrap().clone()); + assert!(matches!(a.call("model_add", &call), Err(ToolError::InvalidArgs(_))), "{why}"); + } + tool(&mut a, "doc_save", json!({ "doc": doc, "path": "out.pdf" })); + assert!(saved_3d(dir.path()).1.is_empty()); +} diff --git a/crates/cos/src/document/objects.rs b/crates/cos/src/document/objects.rs index 2e211927ce..62ef1e74cc 100644 --- a/crates/cos/src/document/objects.rs +++ b/crates/cos/src/document/objects.rs @@ -679,8 +679,9 @@ impl Document { /// /// Used when the working snapshot was restored from an earlier revision and the next /// incremental save must still append to this file (the last saved revision). Objects - /// that already match are left untouched. Reads go through private readers so neither - /// document's editing cache is filled. + /// that already match are left untouched, except members of an object stream of this file + /// whose stream number `current` frees or reuses: those are written again on their own. + /// Reads go through private readers so neither document's editing cache is filled. pub fn overlay_changes_from(&mut self, current: &Document) -> Result<(), CosError> { let saved_nums = self.object_numbers(); let current_nums = current.object_numbers(); @@ -704,6 +705,22 @@ impl Document { } } } + // An object stream of this file that is freed or replaced takes its members with it, so + // a member that is otherwise unchanged is written again on its own. + let gone = |n: u32| freed.binary_search(&n).is_ok() || changed.binary_search(&n).is_ok(); + let orphaned: Vec = current_nums + .iter() + .copied() + .filter(|&num| changed.binary_search(&num).is_err()) + .filter(|&num| { + let packed_in = match self.xref_entry(num) { + Some(XrefEntry::InStream { stream, .. }) => Some(stream), + _ => None, + }; + packed_in.is_some_and(gone) + }) + .collect(); + changed.extend(orphaned); for num in freed { let generation = self.generation(num); self.free(ObjRef::new(num, generation)); diff --git a/crates/cos/src/document/tests.rs b/crates/cos/src/document/tests.rs index 991323d607..f12437baaf 100644 --- a/crates/cos/src/document/tests.rs +++ b/crates/cos/src/document/tests.rs @@ -1846,6 +1846,32 @@ fn a_stream_whose_length_is_itself_and_never_ends_loads_quickly() { rx.recv_timeout(std::time::Duration::from_secs(20)).expect("loading must not retry exponentially"); } +#[test] +fn a_restored_revision_keeps_what_the_saved_file_held_in_object_streams() { + // The earlier revision: an empty document read back from a file whose catalog and pages + // sit in an object stream. + let earlier = Document::open(Arc::new(crate::write_full(&Document::new_empty(), &crate::SaveOptions::default()).unwrap())).unwrap(); + // The saved revision adds an object the catalog links to, so a full save numbers its objects + // anew; an undo then returns to the earlier graph. + let mut later = earlier.clone(); + let extra = later.add(Object::Int(7)); + let root = later.root().unwrap(); + later.update_dict(root, |c| c.set(b"Extra".to_vec(), Object::Ref(extra))).unwrap(); + let saved = crate::write_full(&later, &crate::SaveOptions::default()).unwrap(); + let mut base = Document::open(Arc::new(saved)).unwrap(); + assert!( + base.object_numbers().iter().any(|&n| matches!(base.xref_entry(n), Some(XrefEntry::InStream { .. }))), + "the saved file holds objects in a stream" + ); + base.overlay_changes_from(&earlier).unwrap(); + let file = crate::write_incremental(&base, &crate::SaveOptions::default()).unwrap(); + let reread = Document::open(Arc::new(file)).unwrap(); + let catalog = reread.get(reread.root().unwrap()).as_dict().unwrap().clone(); + assert!(catalog.get(b"Extra").is_none(), "the restored catalog"); + let pages = reread.resolve(catalog.get(b"Pages").unwrap()); + assert_eq!(pages.as_dict().and_then(|d| d.name(b"Type")), Some(&b"Pages"[..]), "the pages survive: {pages:?}"); +} + /// The cache keeps the sum of what its objects hold as they come and go, so neither reader has /// to measure the cache again on every read (#5873). #[test] diff --git a/crates/engine/src/edit.rs b/crates/engine/src/edit.rs index 8f43689aa9..ea43733401 100644 --- a/crates/engine/src/edit.rs +++ b/crates/engine/src/edit.rs @@ -6,7 +6,7 @@ use crate::{ AddedContent, AddedText, Background, BoxSpec, EditError, FieldProps, FieldValue, FillMark, HeaderFooter, Hidden, ImageEdit, InitialView, Label, LineEnding, LinkAction, LinkStyle, MarkFile, MarkKind, Markup, NewAnnotation, NewField, NoteIcon, ObjectTarget, PageBox, Protection, ReviewState, Rgb, Shape, Standard, Style, TabOrder, TransitionSpec, Watermark, a11y, copy_image, decode_data_file, embed_image, field_table, fit_picture, - image_content_type, is_xfa_data_file, js, mark_source, measure, open_source, strip_xfa, unrestricted, xfa, xfa_after_data_change, + image_content_type, is_xfa_data_file, js, mark_source, measure, model_poster, open_source, strip_xfa, unrestricted, xfa, xfa_after_data_change, }; mod labels; @@ -119,6 +119,15 @@ pub enum Edit { bold: Option, italic: Option, }, + /// Write `bookmarks` as children of `parent` from child `index` on, or with `replace` in + /// place of the whole outline (`parent` must then be `[]`). A bookmark whose page this + /// document lacks is kept without a destination (see [`pdfcraft_organize::import_outline`]). + ImportBookmarks { + parent: Vec, + index: usize, + bookmarks: Vec, + replace: bool, + }, /// Bookmarks for the tagged headings (H, H1–H6), nested by level under a new first /// "Untitled" bookmark (New Bookmarks from Structure). BookmarksFromStructure, @@ -157,6 +166,17 @@ pub enum Edit { file: MarkFile, author: String, }, + /// Rich Media ▸ Add 3D: a U3D or PRC `model` as a 3D annotation in `rect` on `page`, named + /// `name` (its alternate description). Its poster is the picture in `poster` (a PDF page or an + /// image) fitted inside `rect`, keeping its shape and centred, or a labelled frame without one. + AddModel3d { + page: usize, + rect: [f64; 4], + model: Arc>, + poster: Option, + activation: pdfcraft_annot::Activation3d, + name: String, + }, /// Delete the comment at `index` in the page's `/Annots`, with its pop-up and replies. DeleteAnnotation { page: usize, diff --git a/crates/engine/src/edit/labels.rs b/crates/engine/src/edit/labels.rs index 5343804ef3..7a51f9078d 100644 --- a/crates/engine/src/edit/labels.rs +++ b/crates/engine/src/edit/labels.rs @@ -38,6 +38,7 @@ pub const EDIT_LABELS: &[&str] = &[ "Set bookmark destination", "Colour", "Style", + "Import bookmarks", "New bookmarks from structure", "New bookmarks from text styles", "Number pages", @@ -68,6 +69,7 @@ pub const EDIT_LABELS: &[&str] = &[ "Add inserted text", "Add file attachment", "Add stamp", + "Add 3D model", "Delete comment", "Edit comment", "Reply", @@ -193,6 +195,7 @@ impl Edit { // The bookmark Style and Colour menus' own names, already said in every language. Edit::SetBookmarkColor { .. } => Label::of("Colour"), Edit::SetBookmarkStyle { .. } => Label::of("Style"), + Edit::ImportBookmarks { .. } => Label::of("Import bookmarks"), Edit::BookmarksFromStructure => Label::of("New bookmarks from structure"), Edit::BookmarksFromTextStyles { .. } => Label::of("New bookmarks from text styles"), Edit::NumberPages { .. } => Label::of("Number pages"), @@ -205,6 +208,7 @@ impl Edit { Edit::SetMeasurementScale { .. } => Label::of("Set measurement scale"), Edit::AddAnnotation(a) => Label::of(add_annotation(&a.shape)), Edit::AddCustomStamp { .. } => Label::of("Add stamp"), + Edit::AddModel3d { .. } => Label::of("Add 3D model"), Edit::DeleteAnnotation { .. } => Label::of("Delete comment"), Edit::SetAnnotationContents { .. } => Label::of("Edit comment"), Edit::ReplyToAnnotation { .. } => Label::of("Reply"), diff --git a/crates/engine/src/edit/run.rs b/crates/engine/src/edit/run.rs index 5ed70aed7a..8ef6795f96 100644 --- a/crates/engine/src/edit/run.rs +++ b/crates/engine/src/edit/run.rs @@ -1,6 +1,6 @@ use super::{ AddedContent, Arc, Edit, EditError, FieldValue, ImageEdit, NewAnnotation, Shape, a11y, copy_image, decode_data_file, embed_image, fit_picture, - image_content_type, is_xfa_data_file, js, mark_source, measure, open_source, strip_xfa, unrestricted, xfa, xfa_after_data_change, + image_content_type, is_xfa_data_file, js, mark_source, measure, model_poster, open_source, strip_xfa, unrestricted, xfa, xfa_after_data_change, }; use crate::SourceProblem; use crate::sources::{comment_source, import_pdf_comments}; @@ -27,6 +27,7 @@ pub(crate) fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Re | Edit::SetBookmarkPage { .. } | Edit::SetBookmarkColor { .. } | Edit::SetBookmarkStyle { .. } + | Edit::ImportBookmarks { .. } | Edit::BookmarksFromStructure | Edit::NumberPages { .. } => { if p.assemble() { @@ -49,6 +50,7 @@ pub(crate) fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Re Edit::AddMeasurement(_) | Edit::AddAnnotation(_) | Edit::AddCustomStamp { .. } + | Edit::AddModel3d { .. } | Edit::DeleteAnnotation { .. } | Edit::SetAnnotationContents { .. } | Edit::ReplyToAnnotation { .. } @@ -454,6 +456,9 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E Edit::SetBookmarkPage { path, page } => pdfcraft_organize::set_bookmark_page(doc, path, *page)?, Edit::SetBookmarkColor { path, color } => pdfcraft_organize::set_bookmark_color(doc, path, *color)?, Edit::SetBookmarkStyle { path, bold, italic } => pdfcraft_organize::set_bookmark_style(doc, path, *bold, *italic)?, + Edit::ImportBookmarks { parent, index, bookmarks, replace } => { + pdfcraft_organize::import_outline(doc, parent, *index, bookmarks, *replace)?; + } Edit::BookmarksFromStructure => { let entries: Vec<_> = pdfcraft_a11y::headings(doc) .into_iter() @@ -509,6 +514,16 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E let index = pdfcraft_annot::add_annotation(doc, &new, &cx.meta())?; pdfcraft_annot::orient_image_stamp(doc, *page, index, rotation)?; } + Edit::AddModel3d { page, rect, model, poster, activation, name } => { + // A file that is not a model is refused before its poster is brought in. + pdfcraft_annot::check_model(model)?; + let poster = match poster { + Some(file) => Some(model_poster(doc, *rect, file)?), + None => None, + }; + let new = pdfcraft_annot::NewModel3d { page: *page, rect: *rect, model, activation: *activation, poster, contents: name }; + pdfcraft_annot::add_3d_model(doc, &new, &cx.meta())?; + } Edit::DeleteAnnotation { page, index } => pdfcraft_annot::delete_annotation(doc, *page, *index)?, Edit::SetAnnotationContents { page, index, text } => pdfcraft_annot::set_contents(doc, *page, *index, text, &cx.meta())?, Edit::ReplyToAnnotation { page, index, text, author } => { diff --git a/crates/engine/src/history.rs b/crates/engine/src/history.rs index fafa65b4c2..8869e5d1a7 100644 --- a/crates/engine/src/history.rs +++ b/crates/engine/src/history.rs @@ -54,6 +54,7 @@ pub(crate) fn scope_of(edit: &Edit) -> Scope { Edit::AddMeasurement(_) | Edit::AddAnnotation(_) | Edit::AddCustomStamp { .. } + | Edit::AddModel3d { .. } | Edit::DeleteAnnotation { .. } | Edit::SetAnnotationContents { .. } | Edit::ReplyToAnnotation { .. } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index fe139164be..8698dfd804 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -71,7 +71,10 @@ pub use signature_scan::{PhotoRect, ScanError, SignaturePhoto, Turn}; pub use stamp_preview::{CommentLayer, StampPreview}; pub use text_preview::{TextEditPreview, UnmarkedPages}; -pub use pdfcraft_organize::{BoxSpec, FieldConflicts, PageBox, SplitBy, Transition, TransitionSpec, mime_for_name, split_ranges}; +pub use pdfcraft_organize::{ + BoxSpec, FieldConflicts, MAX_IMPORT_BOOKMARKS, OutlineNode, OutlineTree, PageBox, SplitBy, Transition, TransitionSpec, mime_for_name, + split_ranges, unmapped as outline_unmapped, view_arity as outline_view_arity, +}; pub use pdfcraft_organize::LabelStyle; pub use pdfcraft_organize::Standard; @@ -108,9 +111,10 @@ pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_missing, scr pub use pdfcraft_annot::appearance as annot_text; pub use pdfcraft_annot::links::{Highlight as LinkHighlight, LinkAction, LinkItem, LinkStyle}; pub use pdfcraft_annot::{ - AppearanceCache, AttachIcon, FillMark, LineEnding, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, Props as CommentProps, - ReviewState, Rgb, Shape, StampGroup, StampKind, Style, rect_quad, + Activation3d, AppearanceCache, AttachIcon, FillMark, LineEnding, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, + Props as CommentProps, ReviewState, Rgb, Shape, StampGroup, StampKind, Style, rect_quad, }; +pub use pdfcraft_annot::{MAX_MODEL_BYTES as MAX_3D_MODEL_BYTES, ModelFormat as Model3dFormat}; pub use pdfcraft_forms::detect; pub use pdfcraft_optimize as optimize; pub use pdfcraft_preflight as pdfa; @@ -140,7 +144,7 @@ use layers::{apply_layer_state, 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}; -use pictures::{embed_image, fit_picture, mark_source}; +use pictures::{embed_image, fit_picture, mark_source, model_poster}; use protection::unrestricted; pub use protection::{Changes, Printing, Protection, SecuritySummary}; pub use session::Session; diff --git a/crates/engine/src/pictures.rs b/crates/engine/src/pictures.rs index 49d76f07c0..d8b8835dc7 100644 --- a/crates/engine/src/pictures.rs +++ b/crates/engine/src/pictures.rs @@ -60,6 +60,18 @@ pub(crate) fn mark_source(doc: &mut pdfcraft_cos::Document, f: &MarkFile) -> Res } } +/// A 3D model's poster: the picture in `file` brought into `doc` and fitted inside `rect` +/// (user space), keeping its shape, and centred. It is drawn in user space and not counter-rotated +/// (unlike a stamp), so on a page with `/Rotate` it turns with the page, as the model does. +pub(crate) fn model_poster(doc: &mut pdfcraft_cos::Document, rect: [f64; 4], file: &MarkFile) -> Result { + let src = mark_source(doc, file)?; + let (sw, sh) = src.size; + if !(sw.is_finite() && sh.is_finite() && sw > 0.0 && sh > 0.0) { + return Err(EditError::Source(format!("{}: the poster has no area", file.name))); + } + Ok(pdfcraft_annot::PosterPicture { xobject: src.xobject, image: src.image, size: (sw, sh), at: fit_picture(rect, sw, sh) }) +} + /// The dimensions of an image from its bytes (JPEG, PNG, TIFF), without decoding the full raster. pub fn image_dimensions(bytes: &[u8]) -> Option<(u32, u32)> { let format = image::guess_format(bytes).ok()?; diff --git a/crates/engine/src/session/view.rs b/crates/engine/src/session/view.rs index c0e7417c8e..5ef135d6c8 100644 --- a/crates/engine/src/session/view.rs +++ b/crates/engine/src/session/view.rs @@ -168,6 +168,14 @@ impl Session { Some(BookmarkPage { rows, next: (end < all.len()).then_some(end), truncated: doc.info.outline_more }) } + /// The bookmark at `path` with everything under it, or the whole outline for `[]`, as a tree + /// another document can take ([`crate::Edit::ImportBookmarks`]). `None` when no document has + /// `id`, or its structure could not be read for editing. + pub fn outline_tree(&self, id: DocId, path: &[usize]) -> Option> { + let editor = self.get(id)?.editor.as_ref()?; + Some(pdfcraft_organize::outline_tree(&editor.cos, path)) + } + /// Top-level bookmarks as split points: (first page of each part, its bookmark's title). pub fn bookmark_splits(&self, id: DocId) -> Vec<(usize, String)> { let Some(doc) = self.get(id) else { return Vec::new() }; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 5d95486c32..6c54131d02 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -549,6 +549,85 @@ fn pdf_page_stamps_fit_inside_an_explicit_box() { assert!(content.contains("/Pic Do") && !content.contains("BI"), "{content}"); } +/// #1297: a 3D model on a page keeps the whole box as its rectangle and draws a PDF poster as a +/// form XObject fitted inside it, keeping its shape and centred. +#[test] +fn a_3d_model_gets_a_fitted_vector_poster_and_a_file_that_is_not_one_is_refused() { + let mut session = Session::new().with_clock(|| 1_700_000_000); + let blank = session.create_blank(595.0, 842.0, 1).unwrap(); + let id = session.open_new("model.pdf", blank).unwrap(); + let mut prc = b"PRC".to_vec(); + prc.extend_from_slice(&[1; 64]); + let add = |model: Vec| Edit::AddModel3d { + page: 0, + rect: [56.0, 400.0, 539.0, 700.0], + model: Arc::new(model), + poster: Some(MarkFile { name: "poster.pdf".into(), bytes: Arc::new(half_red_page()), page: 0 }), + activation: Activation3d::default(), + name: "Bracket".into(), + }; + assert!(session.apply(id, add(b"%PDF-1.7 not a model".to_vec())).is_err()); + session.apply(id, add(prc)).unwrap(); + + let doc = pdfcraft_cos::Document::open(session.save_bytes(id).unwrap()).unwrap(); + let page = &pdfcraft_model::pages(&doc)[0]; + let annots = doc.resolve(page.dict.get(b"Annots").unwrap()); + let [only] = annots.as_array().unwrap().as_slice() else { panic!("the refused model added nothing: {annots:?}") }; + let annot = doc.get(only.as_ref().unwrap()); + let annot = annot.as_dict().unwrap(); + assert_eq!(annot.name(b"Subtype"), Some(&b"3D"[..])); + let rect: Vec = annot.get(b"Rect").unwrap().as_array().unwrap().iter().filter_map(pdfcraft_cos::Object::as_f64).collect(); + assert_eq!(rect, [56.0, 400.0, 539.0, 700.0], "the whole box, not the fitted picture"); + assert_eq!(doc.resolve(annot.get(b"3DA").unwrap()).as_dict().unwrap().name(b"A"), Some(&b"XA"[..])); + let ap = doc.resolve(annot.get(b"AP").unwrap()); + let pdfcraft_cos::Object::Stream(normal) = doc.get(ap.as_dict().unwrap().reference(b"N").unwrap()).as_ref().clone() else { + panic!("an appearance stream") + }; + let pic = doc.resolve(normal.dict.get(b"Resources").unwrap()).as_dict().unwrap().clone(); + let pic = doc.resolve(pic.get(b"XObject").unwrap()).as_dict().unwrap().reference(b"Pic").unwrap(); + let pdfcraft_cos::Object::Stream(picture) = doc.get(pic).as_ref().clone() else { panic!("the picture is a stream") }; + assert_eq!(picture.dict.name(b"Subtype"), Some(&b"Form"[..]), "the page is kept as a form XObject"); + // The 80 × 40 page in the 483 × 300 box: 483 × 241.5 from y 429.25. + let content = String::from_utf8(normal.decoded().unwrap().to_vec()).unwrap(); + assert!(content.contains(" 56 429.25 cm /Pic Do"), "{content}"); +} + +/// A PDF poster whose page has no area is refused rather than drawn as nothing. +#[test] +fn a_3d_poster_page_without_area_is_refused() { + let mut session = Session::new().with_clock(|| 1_700_000_000); + let blank = session.create_blank(595.0, 842.0, 1).unwrap(); + let id = session.open_new("model.pdf", blank).unwrap(); + let flat = String::from_utf8(half_red_page()).unwrap().replace("[0 0 80 40]", "[0 0 80 0 ]"); + let add = Edit::AddModel3d { + page: 0, + rect: [56.0, 400.0, 539.0, 700.0], + model: Arc::new(b"PRC rest".to_vec()), + poster: Some(MarkFile { name: "flat.pdf".into(), bytes: Arc::new(flat.into_bytes()), page: 0 }), + activation: Activation3d::default(), + name: "Bracket".into(), + }; + assert!(matches!(session.apply(id, add), Err(EditError::Source(_)))); + assert!(session.get(id).unwrap().info.annotations.is_empty(), "nothing was added"); +} + +/// Adding a 3D model is a comment change: refused without the comments permission, and only the +/// comments are rebuilt after it. +#[test] +fn a_3d_model_needs_the_comments_permission_and_changes_the_comments() { + let edit = Edit::AddModel3d { + page: 0, + rect: [0.0, 0.0, 100.0, 100.0], + model: Arc::new(b"PRC rest".to_vec()), + poster: None, + activation: Activation3d::default(), + name: String::new(), + }; + let p = pdfcraft_cos::Permissions { bits: 0, owner: false }; + assert_eq!(check_permission(&edit, &p), Err(EditError::NotPermitted("comments"))); + assert_eq!(crate::history::scope_of(&edit), crate::history::Scope::Comments); +} + fn page_texts(s: &Session, id: DocId) -> Vec { let doc = s.get(id).unwrap(); let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; @@ -10369,3 +10448,35 @@ fn fill_sign_multiline_text_survives_engine_save_reopen_and_render() { assert_eq!(reopened.get(again).unwrap().info.annotations[0].contents.as_deref(), Some(text), "reopening preserves all blank lines"); assert_eq!(before.rgba, render_page(&reopened, again).rgba, "the saved appearance is identical"); } + +/// A whole tree goes in as one undoable step, and a refused tree changes nothing. +#[test] +fn imported_bookmarks_undo_in_one_step() { + let (mut s, id) = session_with(2); + let node = |title: &str, page, children| pdfcraft_organize::OutlineNode { + title: title.into(), + page, + view: None, + color: None, + italic: false, + bold: false, + open: true, + children, + }; + let bookmarks = vec![node("Intro", Some(0), vec![node("Detail", Some(5), vec![])]), node("End", Some(1), vec![])]; + s.apply(id, Edit::ImportBookmarks { parent: vec![], index: 0, bookmarks: bookmarks.clone(), replace: false }).unwrap(); + assert_eq!(outline_titles(&s.get(id).unwrap().info.outline), ["Intro→1[Detail→0]", "End→2"]); + assert_eq!(s.get(id).unwrap().can_undo(), Some("Import bookmarks")); + assert_eq!(s.outline_tree(id, &[]).unwrap().unwrap().nodes[0].title, "Intro"); + s.undo(id).unwrap(); + assert!(s.get(id).unwrap().info.outline.is_empty()); + s.apply(id, Edit::ImportBookmarks { parent: vec![], index: 0, bookmarks: bookmarks.clone(), replace: false }).unwrap(); + s.apply(id, Edit::ImportBookmarks { parent: vec![], index: 0, bookmarks: vec![node("New", Some(1), vec![])], replace: true }).unwrap(); + assert_eq!(outline_titles(&s.get(id).unwrap().info.outline), ["New→2"]); + s.undo(id).unwrap(); + assert_eq!(outline_titles(&s.get(id).unwrap().info.outline), ["Intro→1[Detail→0]", "End→2"]); + s.undo(id).unwrap(); + let err = s.apply(id, Edit::ImportBookmarks { parent: vec![4], index: 0, bookmarks, replace: false }).unwrap_err(); + assert!(err.to_string().contains("no bookmark"), "{err}"); + assert!(s.get(id).unwrap().info.outline.is_empty()); +} diff --git a/crates/forms/src/author/edit.rs b/crates/forms/src/author/edit.rs index 6bb62cfb73..14659c4f52 100644 --- a/crates/forms/src/author/edit.rs +++ b/crates/forms/src/author/edit.rs @@ -234,6 +234,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result Result d.set(b"MaxLen".to_vec(), Object::Int(m as i64)), + // An absent key would inherit the parent's limit; zero is "no limit" (`read.rs`). + None if parent_limit => d.set(b"MaxLen".to_vec(), Object::Int(0)), None => { d.remove(b"MaxLen"); } diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 3db463a7e1..5a11cbd34c 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -5487,6 +5487,29 @@ fn sorting_keeps_which_of_two_items_with_one_export_value_is_selected() { assert_eq!(selection_indices(&doc, &list), Some(vec![2])); } +#[test] +fn clearing_a_kids_length_limit_does_not_bring_back_its_parents() { + let mut doc = fixture(); + let city = field(&fields(&doc), "address.city").clone(); + let parent = doc.get(city.obj).as_dict().unwrap().reference(b"Parent").unwrap(); + doc.update_dict(parent, |d| d.set(b"MaxLen".to_vec(), Object::Int(3))).unwrap(); + set_props(&mut doc, "address.city", &FieldProps { max_len: Some(Some(8)), ..FieldProps::default() }).unwrap(); + assert_eq!(field(&fields(&doc), "address.city").max_len, Some(8)); + set_props(&mut doc, "address.city", &FieldProps { max_len: Some(None), ..FieldProps::default() }).unwrap(); + let mut doc = reopen(&doc); + let f = field(&fields(&doc), "address.city").clone(); + assert_eq!(f.max_len, None, "no limit, not the parent's 3"); + let own = doc.get(f.obj).as_dict().and_then(|d| d.get(b"MaxLen")).and_then(Object::as_int); + assert_eq!(own, Some(0), "the kid holds an explicit unlimited of its own"); + set_value(&mut doc, "address.city", &FieldValue::Text("Absalom".into())).unwrap(); + assert_eq!(field(&fields(&reopen(&doc)), "address.city").value, ["Absalom"], "not cut to 3 characters"); + // Without a parent limit, clearing still removes the key. + set_props(&mut doc, "name", &FieldProps { max_len: Some(Some(4)), ..FieldProps::default() }).unwrap(); + set_props(&mut doc, "name", &FieldProps { max_len: Some(None), ..FieldProps::default() }).unwrap(); + let name = field(&fields(&doc), "name").clone(); + assert!(doc.get(name.obj).as_dict().unwrap().get(b"MaxLen").is_none()); +} + #[test] fn clearing_a_kids_default_does_not_bring_back_its_parents() { let mut doc = fixture(); diff --git a/crates/organize/src/lib.rs b/crates/organize/src/lib.rs index b890965d0c..3b3d56cd83 100644 --- a/crates/organize/src/lib.rs +++ b/crates/organize/src/lib.rs @@ -20,6 +20,7 @@ mod dedupe; mod import; mod labels; mod outline; +mod outline_io; mod pdfx; mod prune; mod transition; @@ -40,6 +41,7 @@ pub use outline::{ Bookmark, BookmarkPage, OutlineEntry, OutlineError, add_bookmark, add_bookmark_at, add_bookmark_tree, bookmark_page, bookmarks, delete_bookmark, move_bookmark, rename_bookmark, set_bookmark_color, set_bookmark_open, set_bookmark_page, set_bookmark_style, top_level_bookmarks, }; +pub use outline_io::{MAX_IMPORT_BOOKMARKS, OutlineNode, OutlineTree, import_outline, outline_tree, unmapped, view_arity}; pub use transition::{TRANSITION_DIRECTIONS, TRANSITION_STYLES, Transition, TransitionSpec, set_transition}; pub use view::{InitialView, displays_doc_title, initial_view, set_initial_view}; diff --git a/crates/organize/src/outline.rs b/crates/organize/src/outline.rs index ff7d3d8da7..13c02eb662 100644 --- a/crates/organize/src/outline.rs +++ b/crates/organize/src/outline.rs @@ -19,7 +19,7 @@ use crate::{OrganizeError, walk}; /// Deepest level a listing enters (the top level is 0). Bookmarks deeper than this are not /// listed; edits still reach them by path. -const LIST_DEPTH: usize = 32; +pub(crate) const LIST_DEPTH: usize = 32; /// Deepest name-tree node read when looking up named destinations. const NAME_TREE_DEPTH: usize = 32; @@ -72,6 +72,14 @@ pub enum OutlineError { BadColor, #[error("this document has no tagged headings to make bookmarks from")] NoEntries, + #[error("there are no bookmarks to add")] + NothingToImport, + #[error("the bookmark at {0:?} has a view that is not a known destination type with its values")] + BadView(Vec), + #[error("bookmarks may nest at most {0} levels deep")] + TooDeep(usize), + #[error("at most {0} bookmarks can be added at once")] + TooManyBookmarks(usize), #[error("{0}")] Organize(#[from] OrganizeError), #[error("{0}")] @@ -449,7 +457,7 @@ impl From for OrganizeError { /// Make `kids` the children of `parent`, in order: rewires `/Parent`, `/Prev`, `/Next`, /// `/First` and `/Last`. -fn relink(doc: &mut Document, parent: ObjRef, kids: &[ObjRef]) -> Result<()> { +pub(crate) fn relink(doc: &mut Document, parent: ObjRef, kids: &[ObjRef]) -> Result<()> { for (i, k) in kids.iter().enumerate() { put(doc, *k, b"Parent", Some(Object::Ref(parent)))?; put(doc, *k, b"Prev", i.checked_sub(1).map(|p| Object::Ref(kids[p])))?; @@ -473,7 +481,7 @@ struct Counting { /// Recompute every `/Count` (§12.3.3: open items count their visible descendants, closed items /// the negative of what opening them would show; the root counts all visible items). -fn recount(doc: &mut Document) -> Result<()> { +pub(crate) fn recount(doc: &mut Document) -> Result<()> { let Some(root) = outline_root(doc) else { return Ok(()) }; let mut seen = HashSet::from([root]); let kids = children_of(doc, root, &mut seen); @@ -714,7 +722,7 @@ pub fn move_bookmark(doc: &mut Document, from: &[usize], to_parent: &[usize], in /// Open `parent` so a bookmark just added or moved into it shows (the root is always open). /// Only the sign of `/Count` matters here: [`recount`] sets its value. -fn show_children(doc: &mut Document, parent: ObjRef, parent_path: &[usize]) -> Result<()> { +pub(crate) fn show_children(doc: &mut Document, parent: ObjRef, parent_path: &[usize]) -> Result<()> { if !parent_path.is_empty() && doc.get(parent).as_dict().and_then(|d| d.int(b"Count")).is_none_or(|c| c <= 0) { put(doc, parent, b"Count", Some(Object::Int(1)))?; } diff --git a/crates/organize/src/outline_io.rs b/crates/organize/src/outline_io.rs new file mode 100644 index 0000000000..1fc3afc4f3 --- /dev/null +++ b/crates/organize/src/outline_io.rs @@ -0,0 +1,261 @@ +//! Moving a whole outline (or one bookmark's subtree) out of a document and into another +//! (ISO 32000-2 §12.3.3 outlines, §12.3.2.2 explicit destinations). +//! +//! [`outline_tree`] reads bookmarks as a portable tree of [`OutlineNode`]s: titles, nesting, open +//! state, colour, style and destinations as page index plus view in the file's own user space, +//! so writing a tree back with [`import_outline`] gives the same structure. A node whose page the +//! target lacks keeps its place in the tree without a destination, and [`unmapped`] names it, so +//! nothing is dropped silently. Not carried: actions other than a GoTo, named destinations (they +//! travel resolved to their page), and structure links (`/SE`). + +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; + +use crate::outline::{LIST_DEPTH, children, container, recount, relink, show_children}; +use crate::{OrganizeError, OutlineError, walk}; + +/// Most bookmarks one import writes. +pub const MAX_IMPORT_BOOKMARKS: usize = 100_000; + +/// The destination types (Table 149) and how many numbers follow each. +const VIEWS: [(&[u8], usize); 8] = [(b"XYZ", 3), (b"Fit", 0), (b"FitH", 1), (b"FitV", 1), (b"FitR", 4), (b"FitB", 0), (b"FitBH", 1), (b"FitBV", 1)]; + +/// One bookmark and the bookmarks under it, independent of any document. +#[derive(Clone, Debug, PartialEq)] +pub struct OutlineNode { + pub title: String, + /// The 0-based page it goes to; `None` for a bookmark that goes nowhere in its document. + pub page: Option, + /// Where on the page: a destination type from Table 149 and exactly as many values as that + /// type takes, in PDF user space (`None` keeps the reader's value). `None` goes to the page at + /// the reader's position and zoom. + pub view: Option<(Vec, Vec>)>, + /// The title's colour (`/C`), each component from 0 to 1; `None` is the default black. + pub color: Option<[f32; 3]>, + pub italic: bool, + pub bold: bool, + /// Shown expanded, when it has children. + pub open: bool, + pub children: Vec, +} + +/// The tree [`outline_tree`] read. +#[derive(Clone, Debug, PartialEq)] +pub struct OutlineTree { + pub nodes: Vec, + /// Bookmarks nested deeper than the listing reaches (32 levels) were left out. + pub truncated: bool, +} + +/// The bookmark at `path` with everything under it, or the whole outline for `[]`. +pub fn outline_tree(doc: &Document, path: &[usize]) -> Result { + // A whole outline's top level sits at depth 0; a subtree's root is the bookmark at `path`. + let base = path.len().max(1); + let mut truncated = false; + // The open nodes, outermost first, each with its depth. + let mut open: Vec<(usize, OutlineNode)> = Vec::new(); + let mut roots = Vec::new(); + for b in crate::bookmarks(doc).into_iter().filter(|b| b.path.starts_with(path) && !b.path.is_empty()) { + let depth = b.path.len().saturating_sub(base); + if b.path.len() > LIST_DEPTH && has_children(doc, b.obj) { + truncated = true; + } + while open.last().is_some_and(|(d, _)| *d >= depth) { + close(&mut open, &mut roots); + } + let view = b.view.and_then(|(kind, values)| portable_view(kind, values)); + let node = + OutlineNode { title: b.title, page: b.page, view, color: b.color, italic: b.italic, bold: b.bold, open: b.open, children: Vec::new() }; + open.push((depth, node)); + } + while !open.is_empty() { + close(&mut open, &mut roots); + } + if !path.is_empty() && roots.is_empty() { + return Err(OutlineError::NoSuchBookmark(path.to_vec())); + } + Ok(OutlineTree { nodes: roots, truncated }) +} + +fn close(open: &mut Vec<(usize, OutlineNode)>, roots: &mut Vec) { + if let Some((_, node)) = open.pop() { + match open.last_mut() { + Some((_, parent)) => parent.children.push(node), + None => roots.push(node), + } + } +} + +fn has_children(doc: &Document, r: ObjRef) -> bool { + doc.get(r).as_dict().is_some_and(|d| d.reference(b"First").is_some()) +} + +/// A destination's view as [`import_outline`] takes it: a known type with exactly its number of +/// values (extra ones dropped, missing ones `None`). An unknown type, or a FitR missing one of +/// its four values (the rectangle can't be kept), gives `None`. +fn portable_view(kind: Vec, mut values: Vec>) -> Option<(Vec, Vec>)> { + let (_, arity) = VIEWS.iter().find(|(k, _)| *k == kind.as_slice())?; + values.resize(*arity, None); + if kind.as_slice() == b"FitR" && values.iter().any(Option::is_none) { + return None; + } + Some((kind, values)) +} + +/// The nodes of `nodes` whose page `page_count` pages don't have, as paths into `nodes` +/// (0-based child indices), in tree order. [`import_outline`] keeps these without a destination. +pub fn unmapped(nodes: &[OutlineNode], page_count: usize) -> Vec> { + let mut out = Vec::new(); + visit(nodes, |path, node| { + if node.page.is_some_and(|p| p >= page_count) { + out.push(path.to_vec()); + } + }); + out +} + +/// How many values a view of destination type `kind` takes (Table 149), or `None` for a type +/// that is not one of them. +pub fn view_arity(kind: &[u8]) -> Option { + VIEWS.iter().find(|(k, _)| *k == kind).map(|(_, arity)| *arity) +} + +/// Call `f` with each node's path and the node, parents first. Iterative. +fn visit<'a>(nodes: &'a [OutlineNode], mut f: impl FnMut(&[usize], &'a OutlineNode)) { + // One entry per open level: its nodes and how many of them have been visited. + let mut stack: Vec<(&'a [OutlineNode], usize)> = vec![(nodes, 0)]; + // The visited node's path, kept in step with the stack rather than rebuilt per node. + let mut path: Vec = Vec::new(); + while let Some(level) = stack.last_mut() { + let list = level.0; + let index = level.1; + let Some(node) = list.get(index) else { + stack.pop(); + continue; + }; + level.1 = index.saturating_add(1); + path.truncate(stack.len().saturating_sub(1)); + path.push(index); + f(&path, node); + if !node.children.is_empty() { + stack.push((&node.children, 0)); + } + } +} + +/// Check every node before anything is written: depth (counting the `base` levels above the +/// tree), count, colour and view. +fn check(nodes: &[OutlineNode], base: usize) -> Result<(), OutlineError> { + let mut problem = None; + let mut count = 0usize; + visit(nodes, |path, node| { + count = count.saturating_add(1); + if problem.is_some() { + return; + } + if base.saturating_add(path.len()) > LIST_DEPTH.saturating_add(1) { + problem = Some(OutlineError::TooDeep(LIST_DEPTH.saturating_add(1))); + } else if node.color.is_some_and(|c| !c.iter().all(|v| (0.0..=1.0).contains(v))) { + problem = Some(OutlineError::BadColor); + } else if node.view.as_ref().is_some_and(|v| !view_is_valid(v)) { + problem = Some(OutlineError::BadView(path.to_vec())); + } + }); + if count > MAX_IMPORT_BOOKMARKS { + return Err(OutlineError::TooManyBookmarks(MAX_IMPORT_BOOKMARKS)); + } + problem.map_or(Ok(()), Err) +} + +/// A known type with its number of values, each finite; `/FitR` needs all four. +fn view_is_valid((kind, values): &(Vec, Vec>)) -> bool { + VIEWS.iter().any(|(k, arity)| *k == kind.as_slice() && *arity == values.len()) + && values.iter().flatten().all(|v| v.is_finite()) + && (kind.as_slice() != b"FitR" || values.iter().all(Option::is_some)) +} + +/// Write `nodes` as children of the bookmark at `parent_path` (`[]` = top level), starting at +/// child `index` (past the end appends), or, with `replace`, in place of the outline's +/// top-level bookmarks (then `parent_path` must be `[]`). Nodes whose page this document lacks +/// are written without a destination (see [`unmapped`]). Everything is checked before anything +/// is written. Returns the path of the first node written. +pub fn import_outline( + doc: &mut Document, + parent_path: &[usize], + index: usize, + nodes: &[OutlineNode], + replace: bool, +) -> Result, OutlineError> { + if nodes.is_empty() { + return Err(OutlineError::NothingToImport); + } + if replace && !parent_path.is_empty() { + return Err(OrganizeError::Invalid("replace applies to the whole outline: leave the parent out".into()).into()); + } + check(nodes, parent_path.len())?; + let pages: Vec = walk(doc)?.into_iter().map(|(r, _)| r).collect(); + let parent = container(doc, parent_path)?; + let top = write_nodes(doc, &pages, nodes)?; + let mut kids = if replace { Vec::new() } else { children(doc, parent) }; + let at = index.min(kids.len()); + kids.splice(at..at, top); + relink(doc, parent, &kids)?; + show_children(doc, parent, parent_path)?; + recount(doc)?; + let mut path = parent_path.to_vec(); + path.push(at); + Ok(path) +} + +/// Add one item per node, linked under each other, and return the top-level items in order. +fn write_nodes(doc: &mut Document, pages: &[ObjRef], nodes: &[OutlineNode]) -> Result, OutlineError> { + // Each written item with its node, then its children's items once they are written. + let mut written: Vec<(ObjRef, Vec)> = Vec::new(); + let mut top = Vec::new(); + // (the nodes of one level, the next to write, the index in `written` of their parent) + let mut stack: Vec<(&[OutlineNode], usize, Option)> = vec![(nodes, 0, None)]; + while let Some(level) = stack.last_mut() { + let (list, parent) = (level.0, level.2); + let Some(node) = list.get(level.1) else { + stack.pop(); + continue; + }; + level.1 = level.1.saturating_add(1); + let item = doc.add(Object::Dict(item_dict(node, pages))); + match parent.and_then(|p| written.get_mut(p)) { + Some((_, kids)) => kids.push(item), + None => top.push(item), + } + written.push((item, Vec::new())); + if !node.children.is_empty() { + stack.push((&node.children, 0, Some(written.len().saturating_sub(1)))); + } + } + for (item, kids) in &written { + relink(doc, *item, kids)?; + } + Ok(top) +} + +/// The item's own entries; the links and `/Count`'s value come from [`relink`] and [`recount`] +/// (only the sign of the `/Count` written here matters: it keeps the open state). +fn item_dict(node: &OutlineNode, pages: &[ObjRef]) -> Dict { + let mut d = Dict::new(); + d.set(b"Title".to_vec(), Object::String(PdfString::text(&node.title))); + if let Some(page) = node.page.and_then(|p| pages.get(p)) { + let (kind, values) = node.view.clone().unwrap_or_else(|| (b"XYZ".to_vec(), vec![None; 3])); + let mut dest = vec![Object::Ref(*page), Object::Name(kind)]; + dest.extend(values.into_iter().map(|v| v.map_or(Object::Null, Object::Real))); + d.set(b"Dest".to_vec(), Object::Array(dest)); + } + if let Some(c) = node.color.filter(|c| *c != [0.0; 3]) { + d.set(b"C".to_vec(), Object::Array(c.iter().map(|v| Object::Real(f64::from(*v))).collect())); + } + let flags = i64::from(node.italic) | i64::from(node.bold) << 1; + if flags != 0 { + d.set(b"F".to_vec(), Object::Int(flags)); + } + if !node.children.is_empty() { + d.set(b"Count".to_vec(), Object::Int(if node.open { 1 } else { -1 })); + } + d +} diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 33dc262fce..d5ff2116ff 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -3813,3 +3813,175 @@ fn contents_pieces_join_with_a_space_across_a_trailing_comment() { ctrl_names.sort(); assert_eq!(ctrl_names, [b"ImHidden".to_vec(), b"ImShown".to_vec()]); } + +/// Intro (closed, red bold, at a point) > [Part (italic, FitH), Figure (FitR)], then Results. +fn outline_source() -> Document { + let mut d = doc_a(); + crate::add_bookmark_at(&mut d, &[], 0, "Intro", 0, Some(20.0), Some(380.0)).unwrap(); + crate::add_bookmark(&mut d, &[0], 0, "Part", 1).unwrap(); + crate::add_bookmark(&mut d, &[0], 1, "Figure", 2).unwrap(); + crate::add_bookmark(&mut d, &[], 1, "Results", 2).unwrap(); + crate::set_bookmark_color(&mut d, &[0], Some([1.0, 0.0, 0.0])).unwrap(); + crate::set_bookmark_style(&mut d, &[0], Some(true), None).unwrap(); + crate::set_bookmark_style(&mut d, &[0, 0], None, Some(true)).unwrap(); + let pages = crate::walk(&d).unwrap(); + let (part, figure) = (crate::bookmarks(&d)[1].obj, crate::bookmarks(&d)[2].obj); + let dest = |p: usize, rest: Vec| Object::Array([vec![Object::Ref(pages[p].0)], rest].concat()); + d.update_dict(part, |x| x.set(b"Dest".to_vec(), dest(1, vec![Object::name("FitH"), Object::Real(300.0)]))).unwrap(); + let fit_r = vec![Object::name("FitR"), Object::Int(10), Object::Int(20), Object::Int(110), Object::Int(220)]; + d.update_dict(figure, |x| x.set(b"Dest".to_vec(), dest(2, fit_r))).unwrap(); + crate::set_bookmark_open(&mut d, &[0], false).unwrap(); + d +} + +fn node(title: &str, page: Option, children: Vec) -> crate::OutlineNode { + crate::OutlineNode { title: title.into(), page, view: None, color: None, italic: false, bold: false, open: true, children } +} + +/// The `/Prev`, `/Last` and `/Parent` links agree with `/First`–`/Next` at every level. +fn assert_back_links(doc: &Document) { + let root = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Outlines").unwrap(); + let mut stack = vec![root]; + while let Some(parent) = stack.pop() { + let kids = crate::outline::children(doc, parent); + let d = doc.get(parent).as_dict().cloned().unwrap(); + assert_eq!(d.reference(b"Last"), kids.last().copied()); + for (i, k) in kids.iter().enumerate() { + let kd = doc.get(*k).as_dict().cloned().unwrap(); + assert_eq!(kd.reference(b"Prev"), i.checked_sub(1).map(|p| kids[p])); + assert_eq!(kd.reference(b"Parent"), Some(parent)); + } + stack.extend(kids); + } +} + +#[test] +fn an_exported_outline_imports_into_another_document_unchanged() { + let source = outline_source(); + let tree = crate::outline_tree(&source, &[]).unwrap(); + assert!(!tree.truncated); + let intro = &tree.nodes[0]; + assert_eq!((intro.title.as_str(), intro.page, intro.open, intro.bold, intro.color), ("Intro", Some(0), false, true, Some([1.0, 0.0, 0.0]))); + assert_eq!(intro.view, Some((b"XYZ".to_vec(), vec![Some(20.0), Some(380.0), None]))); + assert_eq!(intro.children[0].view, Some((b"FitH".to_vec(), vec![Some(300.0)]))); + assert!(intro.children[0].italic); + assert_eq!(intro.children[1].view, Some((b"FitR".to_vec(), vec![Some(10.0), Some(20.0), Some(110.0), Some(220.0)]))); + + let mut target = doc_a(); + assert_eq!(crate::import_outline(&mut target, &[], 0, &tree.nodes, false).unwrap(), vec![0]); + assert_back_links(&target); + let back = full_roundtrip(&target); + assert_eq!(crate::outline_tree(&back, &[]).unwrap(), tree); + // The closed parent stays closed: its count is negative and the root counts only what shows. + let first = crate::bookmarks(&back)[0].obj; + let root = back.get(back.root().unwrap()).as_dict().unwrap().reference(b"Outlines").unwrap(); + assert_eq!((count_of(&back, first), count_of(&back, root)), (Some(-2), Some(2))); +} + +#[test] +fn a_subtree_exports_alone_and_a_missing_path_is_refused() { + let source = outline_source(); + let tree = crate::outline_tree(&source, &[0]).unwrap(); + assert_eq!(tree.nodes.len(), 1); + assert_eq!(tree.nodes[0].title, "Intro"); + assert_eq!(tree.nodes[0].children.iter().map(|n| n.title.as_str()).collect::>(), ["Part", "Figure"]); + let leaf = crate::outline_tree(&source, &[0, 1]).unwrap(); + assert_eq!((leaf.nodes.len(), leaf.nodes[0].title.as_str()), (1, "Figure")); + assert_eq!(crate::outline_tree(&source, &[5]), Err(crate::OutlineError::NoSuchBookmark(vec![5]))); + assert_eq!(crate::outline_tree(&doc_a(), &[]).unwrap().nodes, []); +} + +#[test] +fn a_page_the_target_lacks_keeps_its_bookmark_without_a_destination() { + let tree = crate::outline_tree(&outline_source(), &[]).unwrap(); + // doc_b has two pages: Figure and Results go to the third. + assert_eq!(crate::unmapped(&tree.nodes, 2), [vec![0, 1], vec![1]]); + let mut target = doc_b(); + crate::import_outline(&mut target, &[], 0, &tree.nodes, false).unwrap(); + let listed = crate::bookmarks(&target); + assert_eq!(titles(&listed), ["Intro[Part,Figure]", "Results"]); + assert_eq!(listed.iter().map(|b| b.page).collect::>(), [Some(0), Some(1), None, None]); + assert!(target.get(listed[2].obj).as_dict().unwrap().get(b"Dest").is_none()); +} + +#[test] +fn a_fit_r_missing_a_value_copies_with_its_page_and_no_view() { + let mut src = outline_source(); + let pages = crate::walk(&src).unwrap(); + let figure = crate::bookmarks(&src)[2].obj; + let short = vec![Object::Ref(pages[2].0), Object::name("FitR"), Object::Int(10), Object::Int(20), Object::Int(110)]; + src.update_dict(figure, |x| x.set(b"Dest".to_vec(), Object::Array(short))).unwrap(); + let tree = crate::outline_tree(&src, &[]).unwrap(); + let copied = &tree.nodes[0].children[1]; + assert_eq!((copied.page, copied.view.clone()), (Some(2), None)); + let mut target = doc_a(); + crate::import_outline(&mut target, &[], 0, &tree.nodes, false).unwrap(); + assert_eq!(titles(&crate::bookmarks(&target)), ["Intro[Part,Figure]", "Results"]); +} + +#[test] +fn an_import_appends_inserts_or_replaces() { + let mut d = doc_a(); + crate::add_bookmark(&mut d, &[], 0, "Old A", 0).unwrap(); + crate::add_bookmark(&mut d, &[], 1, "Old B", 1).unwrap(); + let new = [node("New", Some(2), vec![node("Child", Some(1), vec![])])]; + assert_eq!(crate::import_outline(&mut d, &[], 1, &new, false).unwrap(), vec![1]); + assert_eq!(titles(&crate::bookmarks(&d)), ["Old A", "New[Child]", "Old B"]); + assert_eq!(crate::import_outline(&mut d, &[2], 9, &new, false).unwrap(), vec![2, 0]); + assert_eq!(titles(&crate::bookmarks(&d)), ["Old A", "New[Child]", "Old B[New[Child]]"]); + assert_back_links(&d); + assert_eq!(crate::import_outline(&mut d, &[], 5, &new, true).unwrap(), vec![0]); + assert_eq!(titles(&crate::bookmarks(&d)), ["New[Child]"]); + assert_back_links(&d); + let refused = crate::import_outline(&mut d, &[0], 0, &new, true); + assert!(matches!(refused, Err(crate::OutlineError::Organize(crate::OrganizeError::Invalid(_))))); + assert_eq!(crate::import_outline(&mut d, &[], 0, &[], false), Err(crate::OutlineError::NothingToImport)); + assert_eq!(crate::import_outline(&mut d, &[7], 0, &new, false), Err(crate::OutlineError::NoSuchBookmark(vec![7]))); +} + +#[test] +fn a_bad_node_anywhere_refuses_the_whole_import() { + let mut d = doc_a(); + crate::add_bookmark(&mut d, &[], 0, "Kept", 0).unwrap(); + let before = crate::bookmarks(&d); + let with = |f: &dyn Fn(&mut crate::OutlineNode)| { + let mut child = node("Child", Some(0), vec![]); + f(&mut child); + vec![node("Fine", Some(0), vec![]), node("Parent", Some(0), vec![child])] + }; + let bad_views: [(&[u8], Vec>); 4] = + [(b"XYZ", vec![Some(1.0)]), (b"Zoom", vec![]), (b"FitH", vec![Some(f64::NAN)]), (b"FitR", vec![Some(0.0), None, Some(1.0), Some(1.0)])]; + for (kind, values) in bad_views { + let nodes = with(&|n| n.view = Some((kind.to_vec(), values.clone()))); + assert_eq!(crate::import_outline(&mut d, &[], 0, &nodes, false), Err(crate::OutlineError::BadView(vec![1, 0])), "{kind:?}"); + } + let nodes = with(&|n| n.color = Some([0.5, 1.5, 0.0])); + assert_eq!(crate::import_outline(&mut d, &[], 0, &nodes, false), Err(crate::OutlineError::BadColor)); + let nodes = with(&|n| n.color = Some([f32::NAN, 0.0, 0.0])); + assert_eq!(crate::import_outline(&mut d, &[], 0, &nodes, false), Err(crate::OutlineError::BadColor)); + let mut deep = node("Leaf", None, vec![]); + for _ in 0..33 { + deep = node("Level", None, vec![deep]); + } + assert_eq!(crate::import_outline(&mut d, &[], 0, &[deep], false), Err(crate::OutlineError::TooDeep(33))); + // 33 levels fit at the top, but not under a bookmark: the deepest would be past the listing. + let mut chain = node("Leaf", None, vec![]); + for _ in 0..32 { + chain = node("Level", None, vec![chain]); + } + assert_eq!(crate::import_outline(&mut d, &[0], 0, std::slice::from_ref(&chain), false), Err(crate::OutlineError::TooDeep(33))); + let many = vec![node("Item", None, vec![]); crate::MAX_IMPORT_BOOKMARKS + 1]; + assert_eq!(crate::import_outline(&mut d, &[], 0, &many, false), Err(crate::OutlineError::TooManyBookmarks(crate::MAX_IMPORT_BOOKMARKS))); + assert_eq!(crate::bookmarks(&d), before); +} + +#[test] +fn an_outline_deeper_than_the_listing_is_exported_as_truncated() { + let tree = crate::outline_tree(&open(deep_outline(40)), &[]).unwrap(); + assert!(tree.truncated); + assert!(!crate::outline_tree(&open(deep_outline(33)), &[]).unwrap().truncated); + // What was exported imports back: 33 levels is the deepest an import takes. + let mut target = doc_a(); + crate::import_outline(&mut target, &[], 0, &tree.nodes, false).unwrap(); + assert_eq!(crate::bookmarks(&target).len(), 33); +} diff --git a/crates/prc/Cargo.toml b/crates/prc/Cargo.toml new file mode 100644 index 0000000000..741c0ac5bd --- /dev/null +++ b/crates/prc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pdfcraft-prc" +description = "PRC 3D model reader (ISO 14739-1): container, bit-level primitives, schema" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +flate2.workspace = true +thiserror.workspace = true + +[dev-dependencies] +proptest.workspace = true diff --git a/crates/prc/README.md b/crates/prc/README.md new file mode 100644 index 0000000000..40bdab335b --- /dev/null +++ b/crates/prc/README.md @@ -0,0 +1,43 @@ +# pdfcraft-prc + +A reader for PRC (ISO 14739-1), the 3D model format a PDF `/3D` stream of `/Subtype /PRC` carries (ISO 32000-2 §13.6.3). + +- **Layer:** L0, standalone. No dependency on any other PdfCraft crate. +- **Licence:** MIT OR Apache-2.0. Inflation through `flate2` with the pure-Rust `miniz_oxide` backend. +- `#![forbid(unsafe_code)]`. Never panics on input: inflated bytes, decoded elements, schema steps and walk depth are capped, and damage returns `PrcError`. + +## API + +```rust +PrcFile::parse(data) -> Result // header, file structures, model file +PrcFile::parse_with_limit(data, max_inflated) -> Result +FileStructure::section(SectionKind) -> &[u8] // inflated globals / tree / tessellation / geometry / extra geometry +FileStructure::schema() -> Result // the globals section's leading schema +FileStructure::tessellations() -> Result, PrcError> // meshes, wires, markups, compressed meshes +PrcFile::placements() / placements_with(StyleAlpha, EntityOverrides) -> Result, PrcError> +PrcFile::model_tree() -> Result, PrcError> // the assembly tree as a model-tree panel lists it +Placement::triangle_colours(&TriangleMesh) -> Option>> +TriangleMesh::transformed(&Matrix) -> TriangleMesh +Schema::read(&mut BitReader) / Schema::skip_added_fields(entity_type, &mut BitReader) +BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_vbn, nbits_then_unsigned, string, float_as_bytes, double +``` + +- `MAX_INFLATED_BYTES` (512 MiB) bounds every section together; `TooLarge` reports the limit hit. +- `MAX_DECODED_ELEMENTS` (2^24) bounds the array elements one pass decodes (a tree walk, or one `tessellations` call), since a single bit can decode to a 4-8 byte element; `TooManyElements` reports it. +- Schema programs (how a newer producer describes fields it added) run under a nesting ceiling (64) and `MAX_SCHEMA_STEPS` (2^22), shared like `MAX_DECODED_ELEMENTS` by every program one pass runs. +- `Unsupported` names constructs the reader cannot step over (schema pointer fields, `Value_CurveIs3D`). +- `Tessellation::Mesh` is a `TriangleMesh`: positions, triangles, one triangle range per face, the stored normals and + texture coordinates with each triangle's corner indices into them, and per-triangle line-attribute styles. + `Wire` is polylines; `Markup` is read past. +- `Tessellation::Compressed` is read to its end so later entities decode; its triangles are not rebuilt yet. +- A face may carry at most 8 texture-coordinate sets; more is `Malformed`. Optimised vertex colours are `Unsupported`. +- The product tree: each drawable tessellation placed by its occurrence path's locations and the item's coordinate + systems (a 4x4 `Matrix`), coloured by the style inheritance rules, with occurrences' entity references applied + (`EntityOverrides`: subtree, everywhere or ignored) and style transparency combined with material alpha by `StyleAlpha`. + A prototype's part is coloured by its own file structure's styles. Each `ModelNode` lists its name, where the name + came from, its visibility and the range of placements it drew. +- Occurrence and prototype cycles are `Malformed`; the walk is capped at depth 64, 2^20 visits and `MAX_PLACEMENTS`. +- A tree's markups (PMI), views, scene lights and clipping planes are read past, not modelled; so are textures. +- Not yet in the crate: rebuilding compressed meshes, textures. +- The `Double` code table's rows come from [prc-rs](https://github.com/ralovich/prc-rs) (MIT); its notice is in `src/acof/table.rs`. + A few reading choices also follow prc-rs; each is marked `// prc-rs:` in the source. diff --git a/crates/prc/src/acof.rs b/crates/prc/src/acof.rs new file mode 100644 index 0000000000..fc11a663a6 --- /dev/null +++ b/crates/prc/src/acof.rs @@ -0,0 +1,93 @@ +//! The prefix-code table behind the PRC `Double` [WD 11.17.2]. +//! +//! The table has 2077 rows: 29 frequent doubles and one row per IEEE binary64 exponent +//! (0..=2047). Each row is a code of 1 to 22 bits and the value it decodes to. +//! The rows come from prc-rs under its MIT licence (see [`table`]). + +use std::collections::HashMap; +use std::sync::OnceLock; + +mod table; + +/// Rows a complete table has. +const ROWS: usize = 2077; + +/// The longest code, in bits. +pub(crate) const MAX_CODE_BITS: u32 = 22; + +/// What a code decodes to. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum Entry { + /// A frequent value, returned with the sign bit applied. + Double(f64), + /// The upper 32 bits of a binary64 (exponent set, mantissa zero); the mantissa follows + /// in the stream. + Exponent(u32), +} + +/// One row: code length, code, and what it decodes to. +type Row = (u32, u32, Entry); + +fn key(nbits: u32, code: u32) -> u32 { + (nbits << 24) | code +} + +/// The rows keyed by `(nbits, code)`, or `None` when the table is incomplete or not prefix +/// free (then no `Double` decodes, rather than some decoding wrongly). +fn build(rows: &[Row]) -> Option> { + let mut map = HashMap::with_capacity(rows.len()); + for &(nbits, code, entry) in rows { + if nbits == 0 || nbits > MAX_CODE_BITS || code >> nbits != 0 || map.insert(key(nbits, code), entry).is_some() { + return None; + } + } + let prefix_free = map.keys().all(|&k| { + let (n, c) = (k >> 24, k & 0xff_ffff); + (1..n).all(|m| !map.contains_key(&key(m, c >> (n - m)))) + }); + (map.len() == ROWS && prefix_free).then_some(map) +} + +fn table() -> Option<&'static HashMap> { + static MAP: OnceLock>> = OnceLock::new(); + MAP.get_or_init(|| build(table::TABLE)).as_ref() +} + +/// The entry for an `nbits`-long code, if one exists. +pub(crate) fn lookup(nbits: u32, code: u32) -> Option { + table()?.get(&key(nbits, code)).copied() +} + +/// The `(nbits, code)` of IEEE exponent `e`'s row, the test writer's half of [`lookup`]. +#[cfg(test)] +pub(crate) fn exponent_code(e: u32) -> Option<(u32, u32)> { + table()?.iter().find(|(_, v)| matches!(v, Entry::Exponent(hi) if (hi >> 20) & 0x7ff == e)).map(|(&k, _)| (k >> 24, k & 0xff_ffff)) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + #[test] + fn an_incomplete_or_ambiguous_table_decodes_nothing() { + assert!(build(&[(2, 0b01, Entry::Double(0.0))]).is_none(), "one row is not the table"); + let rows: Vec = (0..ROWS as u32).map(|i| (22, i, Entry::Exponent(i))).collect(); + assert!(build(&rows).is_some(), "2077 distinct 22-bit codes are prefix free"); + let mut ambiguous = rows.clone(); + ambiguous[0] = (1, 0, Entry::Double(1.0)); // a prefix of every code starting 0 + assert!(build(&ambiguous).is_none()); + let mut too_long = rows; + too_long[0] = (2, 0b111, Entry::Double(1.0)); // a code wider than its length + assert!(build(&too_long).is_none()); + } + + #[test] + fn the_table_is_complete() { + let t = table().unwrap(); + assert_eq!(t.len(), ROWS); + assert_eq!(t.values().filter(|e| matches!(e, Entry::Double(_))).count(), 29); + assert!((0..2048).all(|e| exponent_code(e).is_some())); + assert_eq!(lookup(2, 0b01), Some(Entry::Double(0.0))); + } +} diff --git a/crates/prc/src/acof/table.rs b/crates/prc/src/acof/table.rs new file mode 100644 index 0000000000..28cff50a2e --- /dev/null +++ b/crates/prc/src/acof/table.rs @@ -0,0 +1,2113 @@ +//! The 2077 rows of the PRC `Double` prefix-code table [WD 11.17.2], as `(code length, code, entry)`. +//! +//! The rows are taken from prc-rs (, `src/double.rs` at commit +//! 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d), used under its MIT licence: +//! +//! Copyright (c) 2025,2026 Kristof Ralovich +//! +//! Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +//! documentation files (the "Software"), to deal in the Software without restriction, including without limitation +//! the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +//! to permit persons to whom the Software is furnished to do so, subject to the following conditions: +//! +//! The above copyright notice and this permission notice shall be included in all copies or substantial portions of +//! the Software. +//! +//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +//! THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +//! TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//! SOFTWARE. + +use super::{Entry, Row}; + +/// A frequent value: the binary64 with upper and lower words `hi`, `lo`. +const fn d(hi: u32, lo: u32) -> Entry { + Entry::Double(f64::from_bits(((hi as u64) << 32) | lo as u64)) +} + +/// An exponent row: the upper word of a binary64 whose mantissa follows in the stream. +const fn e(hi: u32) -> Entry { + Entry::Exponent(hi) +} + +#[rustfmt::skip] +pub(super) const TABLE: &[Row] = &[ + (2, 0x1, d(0x00000000, 0x00000000)), + (22, 0xd1d32, e(0x00000000)), + (22, 0xd1d33, e(0x00100000)), + (22, 0xf78d8, e(0x00200000)), + (22, 0xf78d9, e(0x00300000)), + (22, 0xf78da, e(0x00400000)), + (22, 0xf78db, e(0x00500000)), + (22, 0xf78dc, e(0x00600000)), + (22, 0xf78dd, e(0x00700000)), + (22, 0xf78de, e(0x00800000)), + (22, 0xf78df, e(0x00900000)), + (22, 0xf78e0, e(0x00a00000)), + (22, 0xf78e1, e(0x00b00000)), + (22, 0xf78e2, e(0x00c00000)), + (22, 0xf78e3, e(0x00d00000)), + (22, 0xf78e4, e(0x00e00000)), + (22, 0xf78e5, e(0x00f00000)), + (22, 0xf78e6, e(0x01000000)), + (22, 0xf78e7, e(0x01100000)), + (22, 0xf78e8, e(0x01200000)), + (22, 0xf78e9, e(0x01300000)), + (22, 0xf78ea, e(0x01400000)), + (22, 0xf78eb, e(0x01500000)), + (22, 0xf78ec, e(0x01600000)), + (22, 0xf78ed, e(0x01700000)), + (22, 0xf78ee, e(0x01800000)), + (22, 0xf78ef, e(0x01900000)), + (22, 0xf78f0, e(0x01a00000)), + (22, 0xf78f1, e(0x01b00000)), + (22, 0xf78f2, e(0x01c00000)), + (22, 0xf78f3, e(0x01d00000)), + (22, 0xf78f4, e(0x01e00000)), + (22, 0xf78f5, e(0x01f00000)), + (22, 0xf78f6, e(0x02000000)), + (22, 0xf78f7, e(0x02100000)), + (22, 0xf78f8, e(0x02200000)), + (22, 0xf78f9, e(0x02300000)), + (22, 0xf78fa, e(0x02400000)), + (22, 0xf78fb, e(0x02500000)), + (22, 0xf78fc, e(0x02600000)), + (22, 0xf78fd, e(0x02700000)), + (22, 0xf78fe, e(0x02800000)), + (22, 0xf78ff, e(0x02900000)), + (22, 0x3a8300, e(0x02a00000)), + (22, 0x3a8301, e(0x02b00000)), + (22, 0x3a8302, e(0x02c00000)), + (22, 0x3a8303, e(0x02d00000)), + (22, 0x3a8304, e(0x02e00000)), + (22, 0x3a8305, e(0x02f00000)), + (22, 0x3a8306, e(0x03000000)), + (22, 0x3a8307, e(0x03100000)), + (22, 0x3a8308, e(0x03200000)), + (22, 0x3a8309, e(0x03300000)), + (22, 0x3a830a, e(0x03400000)), + (22, 0x3a830b, e(0x03500000)), + (22, 0x3a830c, e(0x03600000)), + (22, 0x3a830d, e(0x03700000)), + (22, 0x3a830e, e(0x03800000)), + (22, 0x3a830f, e(0x03900000)), + (22, 0x3a8310, e(0x03a00000)), + (22, 0x3a8311, e(0x03b00000)), + (22, 0x3a8312, e(0x03c00000)), + (22, 0x3a8313, e(0x03d00000)), + (22, 0x3a8314, e(0x03e00000)), + (22, 0x3a8315, e(0x03f00000)), + (22, 0x3a8316, e(0x04000000)), + (22, 0x3a8317, e(0x04100000)), + (22, 0x3a8318, e(0x04200000)), + (22, 0x3a8319, e(0x04300000)), + (22, 0x3a831a, e(0x04400000)), + (22, 0x3a831b, e(0x04500000)), + (22, 0x3a831c, e(0x04600000)), + (22, 0x3a831d, e(0x04700000)), + (22, 0x3a831e, e(0x04800000)), + (22, 0x3a831f, e(0x04900000)), + (22, 0x3a8320, e(0x04a00000)), + (22, 0x3a8321, e(0x04b00000)), + (22, 0x3a8322, e(0x04c00000)), + (22, 0x3a8323, e(0x04d00000)), + (22, 0x3a8324, e(0x04e00000)), + (22, 0x3a8325, e(0x04f00000)), + (22, 0x3a8326, e(0x05000000)), + (22, 0x3a8327, e(0x05100000)), + (22, 0x3a8328, e(0x05200000)), + (22, 0x3a8329, e(0x05300000)), + (22, 0x3a832a, e(0x05400000)), + (22, 0x3a832b, e(0x05500000)), + (22, 0x3a832c, e(0x05600000)), + (22, 0x3a832d, e(0x05700000)), + (22, 0x3a832e, e(0x05800000)), + (22, 0x3a832f, e(0x05900000)), + (22, 0x3a8330, e(0x05a00000)), + (22, 0x3a8331, e(0x05b00000)), + (22, 0x3a8332, e(0x05c00000)), + (22, 0x3a8333, e(0x05d00000)), + (22, 0x3a8334, e(0x05e00000)), + (22, 0x3a8335, e(0x05f00000)), + (22, 0x3a8336, e(0x06000000)), + (22, 0x3a8337, e(0x06100000)), + (22, 0x3a8338, e(0x06200000)), + (22, 0x3a8339, e(0x06300000)), + (22, 0x3a833a, e(0x06400000)), + (22, 0x3a833b, e(0x06500000)), + (22, 0x3a833c, e(0x06600000)), + (22, 0x3a833d, e(0x06700000)), + (22, 0x3a833e, e(0x06800000)), + (22, 0x3a833f, e(0x06900000)), + (22, 0x3a8340, e(0x06a00000)), + (22, 0x3a8341, e(0x06b00000)), + (22, 0x3a8342, e(0x06c00000)), + (22, 0x3a8343, e(0x06d00000)), + (22, 0x3a8344, e(0x06e00000)), + (22, 0x3a8345, e(0x06f00000)), + (22, 0x3a8346, e(0x07000000)), + (22, 0x3a8347, e(0x07100000)), + (22, 0x3a8348, e(0x07200000)), + (22, 0x3a8349, e(0x07300000)), + (22, 0x3a834a, e(0x07400000)), + (22, 0x3a834b, e(0x07500000)), + (22, 0x3a834c, e(0x07600000)), + (22, 0x3a834d, e(0x07700000)), + (22, 0x3a834e, e(0x07800000)), + (22, 0x3a834f, e(0x07900000)), + (22, 0x3a8350, e(0x07a00000)), + (22, 0x3a8351, e(0x07b00000)), + (22, 0x3a8352, e(0x07c00000)), + (22, 0x3a8353, e(0x07d00000)), + (22, 0x3a8354, e(0x07e00000)), + (22, 0x3a8355, e(0x07f00000)), + (22, 0x3a8356, e(0x08000000)), + (22, 0x3a8357, e(0x08100000)), + (22, 0x3a8358, e(0x08200000)), + (22, 0x3a8359, e(0x08300000)), + (22, 0x3a835a, e(0x08400000)), + (22, 0x3a835b, e(0x08500000)), + (22, 0x3a835c, e(0x08600000)), + (22, 0x3a835d, e(0x08700000)), + (22, 0x3a835e, e(0x08800000)), + (22, 0x3a835f, e(0x08900000)), + (22, 0x3a8360, e(0x08a00000)), + (22, 0x3a8361, e(0x08b00000)), + (22, 0x3a8362, e(0x08c00000)), + (22, 0x3a8363, e(0x08d00000)), + (22, 0x3a8364, e(0x08e00000)), + (22, 0x3a8365, e(0x08f00000)), + (22, 0x3a8366, e(0x09000000)), + (22, 0x3a8367, e(0x09100000)), + (22, 0x3a8368, e(0x09200000)), + (22, 0x3a8369, e(0x09300000)), + (22, 0x3a836a, e(0x09400000)), + (22, 0x3a836b, e(0x09500000)), + (22, 0x3a836c, e(0x09600000)), + (22, 0x3a836d, e(0x09700000)), + (22, 0x3a836e, e(0x09800000)), + (22, 0x3a836f, e(0x09900000)), + (22, 0x3a8370, e(0x09a00000)), + (22, 0x3a8371, e(0x09b00000)), + (22, 0x3a8372, e(0x09c00000)), + (22, 0x3a8373, e(0x09d00000)), + (22, 0x3a8374, e(0x09e00000)), + (22, 0x3a8375, e(0x09f00000)), + (22, 0x3a8376, e(0x0a000000)), + (22, 0x3a8377, e(0x0a100000)), + (22, 0x3a8378, e(0x0a200000)), + (22, 0x3a8379, e(0x0a300000)), + (22, 0x3a837a, e(0x0a400000)), + (22, 0x3a837b, e(0x0a500000)), + (22, 0x3a837c, e(0x0a600000)), + (22, 0x3a837d, e(0x0a700000)), + (22, 0x3a837e, e(0x0a800000)), + (22, 0x3a837f, e(0x0a900000)), + (22, 0x3a8380, e(0x0aa00000)), + (22, 0x3a8381, e(0x0ab00000)), + (22, 0x3a8382, e(0x0ac00000)), + (22, 0x3a8383, e(0x0ad00000)), + (22, 0x3a8384, e(0x0ae00000)), + (22, 0x3a8385, e(0x0af00000)), + (22, 0x3a8386, e(0x0b000000)), + (22, 0x3a8387, e(0x0b100000)), + (22, 0x3a8388, e(0x0b200000)), + (22, 0x3a8389, e(0x0b300000)), + (22, 0x3a838a, e(0x0b400000)), + (22, 0x3a838b, e(0x0b500000)), + (22, 0x3a838c, e(0x0b600000)), + (22, 0x3a838d, e(0x0b700000)), + (22, 0x3a838e, e(0x0b800000)), + (22, 0x3a838f, e(0x0b900000)), + (22, 0x3a8390, e(0x0ba00000)), + (22, 0x3a8391, e(0x0bb00000)), + (22, 0x3a8392, e(0x0bc00000)), + (22, 0x3a8393, e(0x0bd00000)), + (22, 0x3a8394, e(0x0be00000)), + (22, 0x3a8395, e(0x0bf00000)), + (22, 0x3a8396, e(0x0c000000)), + (22, 0x3a8397, e(0x0c100000)), + (22, 0x3a8398, e(0x0c200000)), + (22, 0x3a8399, e(0x0c300000)), + (22, 0x3a839a, e(0x0c400000)), + (22, 0x3a839b, e(0x0c500000)), + (22, 0x3a839c, e(0x0c600000)), + (22, 0x3a839d, e(0x0c700000)), + (22, 0x3a839e, e(0x0c800000)), + (22, 0x3a839f, e(0x0c900000)), + (22, 0x3a83a0, e(0x0ca00000)), + (22, 0x3a83a1, e(0x0cb00000)), + (22, 0x3a83a2, e(0x0cc00000)), + (22, 0x3a83a3, e(0x0cd00000)), + (22, 0x3a83a4, e(0x0ce00000)), + (22, 0x3a83a5, e(0x0cf00000)), + (22, 0x3a83a6, e(0x0d000000)), + (22, 0x3a83a7, e(0x0d100000)), + (22, 0x3a83a8, e(0x0d200000)), + (22, 0x3a83a9, e(0x0d300000)), + (22, 0x3a83aa, e(0x0d400000)), + (22, 0x3a83ab, e(0x0d500000)), + (22, 0x3a83ac, e(0x0d600000)), + (22, 0x3a83ad, e(0x0d700000)), + (22, 0x3a83ae, e(0x0d800000)), + (22, 0x3a83af, e(0x0d900000)), + (22, 0x3a83b0, e(0x0da00000)), + (22, 0x3a83b1, e(0x0db00000)), + (22, 0x3a83b2, e(0x0dc00000)), + (22, 0x3a83b3, e(0x0dd00000)), + (22, 0x3a83b4, e(0x0de00000)), + (22, 0x3a83b5, e(0x0df00000)), + (22, 0x3a83b6, e(0x0e000000)), + (22, 0x3a83b7, e(0x0e100000)), + (22, 0x3a83b8, e(0x0e200000)), + (22, 0x3a83b9, e(0x0e300000)), + (22, 0x3a83ba, e(0x0e400000)), + (22, 0x3a83bb, e(0x0e500000)), + (22, 0x3a83bc, e(0x0e600000)), + (22, 0x3a83bd, e(0x0e700000)), + (22, 0x3a83be, e(0x0e800000)), + (22, 0x3a83bf, e(0x0e900000)), + (22, 0x3a83c0, e(0x0ea00000)), + (22, 0x3a83c1, e(0x0eb00000)), + (22, 0x3a83c2, e(0x0ec00000)), + (22, 0x3a83c3, e(0x0ed00000)), + (22, 0x3a83c4, e(0x0ee00000)), + (22, 0x3a83c5, e(0x0ef00000)), + (22, 0x3a83c6, e(0x0f000000)), + (22, 0x3a83c7, e(0x0f100000)), + (22, 0x3a83c8, e(0x0f200000)), + (22, 0x3a83c9, e(0x0f300000)), + (22, 0x3a83ca, e(0x0f400000)), + (22, 0x3a83cb, e(0x0f500000)), + (22, 0x3a83cc, e(0x0f600000)), + (22, 0x3a83cd, e(0x0f700000)), + (22, 0x3a83ce, e(0x0f800000)), + (22, 0x3a83cf, e(0x0f900000)), + (22, 0x3a83d0, e(0x0fa00000)), + (22, 0x3a83d1, e(0x0fb00000)), + (22, 0x3a83d2, e(0x0fc00000)), + (22, 0x3a83d3, e(0x0fd00000)), + (22, 0x3a83d4, e(0x0fe00000)), + (22, 0x3a83d5, e(0x0ff00000)), + (22, 0x3a83d6, e(0x10000000)), + (22, 0x3a83d7, e(0x10100000)), + (22, 0x3a83d8, e(0x10200000)), + (22, 0x3a83d9, e(0x10300000)), + (22, 0x3a83da, e(0x10400000)), + (22, 0x3a83db, e(0x10500000)), + (22, 0x3a83dc, e(0x10600000)), + (22, 0x3a83dd, e(0x10700000)), + (22, 0x3a83de, e(0x10800000)), + (22, 0x3a83df, e(0x10900000)), + (22, 0x3a83e0, e(0x10a00000)), + (22, 0x3a83e1, e(0x10b00000)), + (22, 0x3a83e2, e(0x10c00000)), + (22, 0x3a83e3, e(0x10d00000)), + (22, 0x3a83e4, e(0x10e00000)), + (22, 0x3a83e5, e(0x10f00000)), + (22, 0x3a83e6, e(0x11000000)), + (22, 0x3a83e7, e(0x11100000)), + (22, 0x3a83e8, e(0x11200000)), + (22, 0x3a83e9, e(0x11300000)), + (22, 0x3a83ea, e(0x11400000)), + (22, 0x3a83eb, e(0x11500000)), + (22, 0x3a83ec, e(0x11600000)), + (22, 0x3a83ed, e(0x11700000)), + (22, 0x3a83ee, e(0x11800000)), + (22, 0x3a83ef, e(0x11900000)), + (22, 0x3a83f0, e(0x11a00000)), + (22, 0x3a83f1, e(0x11b00000)), + (22, 0x3a83f2, e(0x11c00000)), + (22, 0x3a83f3, e(0x11d00000)), + (22, 0x3a83f4, e(0x11e00000)), + (22, 0x3a83f5, e(0x11f00000)), + (22, 0x3a83f6, e(0x12000000)), + (22, 0x3a83f7, e(0x12100000)), + (22, 0x3a83f8, e(0x12200000)), + (22, 0x3a83f9, e(0x12300000)), + (22, 0x3a83fa, e(0x12400000)), + (22, 0x3a83fb, e(0x12500000)), + (22, 0x3a83fc, e(0x12600000)), + (22, 0x3a83fd, e(0x12700000)), + (22, 0x3a83fe, e(0x12800000)), + (22, 0x3a83ff, e(0x12900000)), + (22, 0x3a8400, e(0x12a00000)), + (22, 0x3a8401, e(0x12b00000)), + (22, 0x3a8402, e(0x12c00000)), + (22, 0x3a8403, e(0x12d00000)), + (22, 0x3a8404, e(0x12e00000)), + (22, 0x3a8405, e(0x12f00000)), + (22, 0x3a8406, e(0x13000000)), + (22, 0x3a8407, e(0x13100000)), + (22, 0x3a8408, e(0x13200000)), + (22, 0x3a8409, e(0x13300000)), + (22, 0x3a840a, e(0x13400000)), + (22, 0x3a840b, e(0x13500000)), + (22, 0x3a840c, e(0x13600000)), + (22, 0x3a840d, e(0x13700000)), + (22, 0x3a840e, e(0x13800000)), + (22, 0x3a840f, e(0x13900000)), + (22, 0x3a8410, e(0x13a00000)), + (22, 0x3a8411, e(0x13b00000)), + (22, 0x3a8412, e(0x13c00000)), + (22, 0x3a8413, e(0x13d00000)), + (22, 0x3a8414, e(0x13e00000)), + (22, 0x3a8415, e(0x13f00000)), + (22, 0x3a8416, e(0x14000000)), + (22, 0x3a8417, e(0x14100000)), + (22, 0x3a8418, e(0x14200000)), + (22, 0x3a8419, e(0x14300000)), + (22, 0x3a841a, e(0x14400000)), + (22, 0x3a841b, e(0x14500000)), + (22, 0x3a841c, e(0x14600000)), + (22, 0x3a841d, e(0x14700000)), + (22, 0x3a841e, e(0x14800000)), + (22, 0x3a841f, e(0x14900000)), + (22, 0x3a8420, e(0x14a00000)), + (22, 0x3a8421, e(0x14b00000)), + (22, 0x3a8422, e(0x14c00000)), + (22, 0x3a8423, e(0x14d00000)), + (22, 0x3a8424, e(0x14e00000)), + (22, 0x3a8425, e(0x14f00000)), + (22, 0x3a8426, e(0x15000000)), + (22, 0x3a8427, e(0x15100000)), + (22, 0x3a8428, e(0x15200000)), + (22, 0x3a8429, e(0x15300000)), + (22, 0x3a842a, e(0x15400000)), + (22, 0x3a842b, e(0x15500000)), + (22, 0x3a842c, e(0x15600000)), + (22, 0x3a842d, e(0x15700000)), + (22, 0x3a842e, e(0x15800000)), + (22, 0x3a842f, e(0x15900000)), + (22, 0x3a8430, e(0x15a00000)), + (22, 0x3a8431, e(0x15b00000)), + (22, 0x3a8432, e(0x15c00000)), + (22, 0x3a8433, e(0x15d00000)), + (22, 0x3a8434, e(0x15e00000)), + (22, 0x3a8435, e(0x15f00000)), + (22, 0x3a8436, e(0x16000000)), + (22, 0x3a8437, e(0x16100000)), + (22, 0x3a8438, e(0x16200000)), + (22, 0x3a8439, e(0x16300000)), + (22, 0x3a843a, e(0x16400000)), + (22, 0x3a843b, e(0x16500000)), + (22, 0x3a843c, e(0x16600000)), + (22, 0x3a843d, e(0x16700000)), + (22, 0x3a843e, e(0x16800000)), + (22, 0x3a843f, e(0x16900000)), + (22, 0x3a8440, e(0x16a00000)), + (22, 0x3a8441, e(0x16b00000)), + (22, 0x3a8442, e(0x16c00000)), + (22, 0x3a8443, e(0x16d00000)), + (22, 0x3a8444, e(0x16e00000)), + (22, 0x3a8445, e(0x16f00000)), + (22, 0x3a8446, e(0x17000000)), + (22, 0x3a8447, e(0x17100000)), + (22, 0x3a8448, e(0x17200000)), + (22, 0x3a8449, e(0x17300000)), + (22, 0x3a844a, e(0x17400000)), + (22, 0x3a844b, e(0x17500000)), + (22, 0x3a844c, e(0x17600000)), + (22, 0x3a844d, e(0x17700000)), + (22, 0x3a844e, e(0x17800000)), + (22, 0x3a844f, e(0x17900000)), + (22, 0x3a8450, e(0x17a00000)), + (22, 0x3a8451, e(0x17b00000)), + (22, 0x3a8452, e(0x17c00000)), + (22, 0x3a8453, e(0x17d00000)), + (22, 0x3a8454, e(0x17e00000)), + (22, 0x3a8455, e(0x17f00000)), + (22, 0x3a8456, e(0x18000000)), + (22, 0x3a8457, e(0x18100000)), + (22, 0x3a8458, e(0x18200000)), + (22, 0x3a8459, e(0x18300000)), + (22, 0x3a845a, e(0x18400000)), + (22, 0x3a845b, e(0x18500000)), + (22, 0x3a845c, e(0x18600000)), + (22, 0x3a845d, e(0x18700000)), + (22, 0x3a845e, e(0x18800000)), + (22, 0x3a845f, e(0x18900000)), + (22, 0x3a8460, e(0x18a00000)), + (22, 0x3a8461, e(0x18b00000)), + (22, 0x3a8462, e(0x18c00000)), + (22, 0x3a8463, e(0x18d00000)), + (22, 0x3a8464, e(0x18e00000)), + (22, 0x3a8465, e(0x18f00000)), + (22, 0x3a8466, e(0x19000000)), + (22, 0x3a8467, e(0x19100000)), + (22, 0x3a8468, e(0x19200000)), + (22, 0x3a8469, e(0x19300000)), + (22, 0x3a846a, e(0x19400000)), + (22, 0x3a846b, e(0x19500000)), + (22, 0x3a846c, e(0x19600000)), + (22, 0x3a846d, e(0x19700000)), + (22, 0x3a846e, e(0x19800000)), + (22, 0x3a846f, e(0x19900000)), + (22, 0x3a8470, e(0x19a00000)), + (22, 0x3a8471, e(0x19b00000)), + (22, 0x3a8472, e(0x19c00000)), + (22, 0x3a8473, e(0x19d00000)), + (22, 0x3a8474, e(0x19e00000)), + (22, 0x3a8475, e(0x19f00000)), + (22, 0x3a8476, e(0x1a000000)), + (22, 0x3a8477, e(0x1a100000)), + (22, 0x3a8478, e(0x1a200000)), + (22, 0x3a8479, e(0x1a300000)), + (22, 0x3a847a, e(0x1a400000)), + (22, 0x3a847b, e(0x1a500000)), + (22, 0x3a847c, e(0x1a600000)), + (22, 0x3a847d, e(0x1a700000)), + (22, 0x3a847e, e(0x1a800000)), + (22, 0x3a847f, e(0x1a900000)), + (22, 0x3a8480, e(0x1aa00000)), + (22, 0x3a8481, e(0x1ab00000)), + (22, 0x3a8482, e(0x1ac00000)), + (22, 0x3a8483, e(0x1ad00000)), + (22, 0x3a8484, e(0x1ae00000)), + (22, 0x3a8485, e(0x1af00000)), + (22, 0x3a8486, e(0x1b000000)), + (22, 0x3a8487, e(0x1b100000)), + (22, 0x3a8488, e(0x1b200000)), + (22, 0x3a8489, e(0x1b300000)), + (22, 0x3a848a, e(0x1b400000)), + (22, 0x3a848b, e(0x1b500000)), + (22, 0x3a848c, e(0x1b600000)), + (22, 0x3a848d, e(0x1b700000)), + (22, 0x3a848e, e(0x1b800000)), + (22, 0x3a848f, e(0x1b900000)), + (22, 0x3a8490, e(0x1ba00000)), + (22, 0x3a8491, e(0x1bb00000)), + (22, 0x3a8492, e(0x1bc00000)), + (22, 0x3a8493, e(0x1bd00000)), + (22, 0x3a8494, e(0x1be00000)), + (22, 0x3a8495, e(0x1bf00000)), + (22, 0x3a8496, e(0x1c000000)), + (22, 0x3a8497, e(0x1c100000)), + (22, 0x3a8498, e(0x1c200000)), + (22, 0x3a8499, e(0x1c300000)), + (22, 0x3a849a, e(0x1c400000)), + (22, 0x3a849b, e(0x1c500000)), + (22, 0x3a849c, e(0x1c600000)), + (22, 0x3a849d, e(0x1c700000)), + (22, 0x3a849e, e(0x1c800000)), + (22, 0x3a849f, e(0x1c900000)), + (22, 0x3a84a0, e(0x1ca00000)), + (22, 0x3a84a1, e(0x1cb00000)), + (22, 0x3a84a2, e(0x1cc00000)), + (22, 0x3a84a3, e(0x1cd00000)), + (22, 0x3a84a4, e(0x1ce00000)), + (22, 0x3a84a5, e(0x1cf00000)), + (22, 0x3a84a6, e(0x1d000000)), + (22, 0x3a84a7, e(0x1d100000)), + (22, 0x3a84a8, e(0x1d200000)), + (22, 0x3a84a9, e(0x1d300000)), + (22, 0x3a84aa, e(0x1d400000)), + (22, 0x3a84ab, e(0x1d500000)), + (22, 0x3a84ac, e(0x1d600000)), + (22, 0x3a84ad, e(0x1d700000)), + (22, 0x3a84ae, e(0x1d800000)), + (22, 0x3a84af, e(0x1d900000)), + (22, 0x3a84b0, e(0x1da00000)), + (22, 0x3a84b1, e(0x1db00000)), + (22, 0x3a84b2, e(0x1dc00000)), + (22, 0x3a84b3, e(0x1dd00000)), + (22, 0x3a84b4, e(0x1de00000)), + (22, 0x3a84b5, e(0x1df00000)), + (22, 0x3a84b6, e(0x1e000000)), + (22, 0x3a84b7, e(0x1e100000)), + (22, 0x3a84b8, e(0x1e200000)), + (22, 0x3a84b9, e(0x1e300000)), + (22, 0x3a84ba, e(0x1e400000)), + (22, 0x3a84bb, e(0x1e500000)), + (22, 0x3a84bc, e(0x1e600000)), + (22, 0x3a84bd, e(0x1e700000)), + (22, 0x3a84be, e(0x1e800000)), + (22, 0x3a84bf, e(0x1e900000)), + (22, 0x3a84c0, e(0x1ea00000)), + (22, 0x3a84c1, e(0x1eb00000)), + (22, 0x3a84c2, e(0x1ec00000)), + (22, 0x3a84c3, e(0x1ed00000)), + (22, 0x3a84c4, e(0x1ee00000)), + (22, 0x3a84c5, e(0x1ef00000)), + (22, 0x3a84c6, e(0x1f000000)), + (22, 0x3a84c7, e(0x1f100000)), + (22, 0x3a84c8, e(0x1f200000)), + (22, 0x3a84c9, e(0x1f300000)), + (22, 0x3a84ca, e(0x1f400000)), + (22, 0x3a84cb, e(0x1f500000)), + (22, 0x3a84cc, e(0x1f600000)), + (22, 0x3a84cd, e(0x1f700000)), + (22, 0x3a84ce, e(0x1f800000)), + (22, 0x3a84cf, e(0x1f900000)), + (22, 0x3a84d0, e(0x1fa00000)), + (22, 0x3a84d1, e(0x1fb00000)), + (22, 0x3a84d2, e(0x1fc00000)), + (22, 0x3a84d3, e(0x1fd00000)), + (22, 0x3a84d4, e(0x1fe00000)), + (22, 0x3a84d5, e(0x1ff00000)), + (22, 0x3a84d6, e(0x20000000)), + (22, 0x3a84d7, e(0x20100000)), + (22, 0x3a84d8, e(0x20200000)), + (22, 0x3a84d9, e(0x20300000)), + (22, 0x3a84da, e(0x20400000)), + (22, 0x3a84db, e(0x20500000)), + (22, 0x3a84dc, e(0x20600000)), + (22, 0x3a84dd, e(0x20700000)), + (22, 0x3a84de, e(0x20800000)), + (22, 0x3a84df, e(0x20900000)), + (22, 0x3a84e0, e(0x20a00000)), + (22, 0x3a84e1, e(0x20b00000)), + (22, 0x3a84e2, e(0x20c00000)), + (22, 0x3a84e3, e(0x20d00000)), + (22, 0x3a84e4, e(0x20e00000)), + (22, 0x3a84e5, e(0x20f00000)), + (22, 0x3a84e6, e(0x21000000)), + (22, 0x3a84e7, e(0x21100000)), + (22, 0x3a84e8, e(0x21200000)), + (22, 0x3a84e9, e(0x21300000)), + (22, 0x3a84ea, e(0x21400000)), + (22, 0x3a84eb, e(0x21500000)), + (22, 0x3a84ec, e(0x21600000)), + (22, 0x3a84ed, e(0x21700000)), + (22, 0x3a84ee, e(0x21800000)), + (22, 0x3a84ef, e(0x21900000)), + (22, 0x3a84f0, e(0x21a00000)), + (22, 0x3a84f1, e(0x21b00000)), + (22, 0x3a84f2, e(0x21c00000)), + (22, 0x3a84f3, e(0x21d00000)), + (22, 0x3a84f4, e(0x21e00000)), + (22, 0x3a84f5, e(0x21f00000)), + (22, 0x3a84f6, e(0x22000000)), + (22, 0x3a84f7, e(0x22100000)), + (22, 0x3a84f8, e(0x22200000)), + (22, 0x3a84f9, e(0x22300000)), + (22, 0x3a84fa, e(0x22400000)), + (22, 0x3a84fb, e(0x22500000)), + (22, 0x3a84fc, e(0x22600000)), + (22, 0x3a84fd, e(0x22700000)), + (22, 0x3a84fe, e(0x22800000)), + (22, 0x3a84ff, e(0x22900000)), + (22, 0x3a8500, e(0x22a00000)), + (22, 0x3a8501, e(0x22b00000)), + (22, 0x3a8502, e(0x22c00000)), + (22, 0x3a8503, e(0x22d00000)), + (22, 0x3a8504, e(0x22e00000)), + (22, 0x3a8505, e(0x22f00000)), + (22, 0x3a8506, e(0x23000000)), + (22, 0x3a8507, e(0x23100000)), + (22, 0x3a8508, e(0x23200000)), + (22, 0x3a8509, e(0x23300000)), + (22, 0x3a850a, e(0x23400000)), + (22, 0x3a850b, e(0x23500000)), + (22, 0x3a850c, e(0x23600000)), + (22, 0x3a850d, e(0x23700000)), + (22, 0x3a850e, e(0x23800000)), + (22, 0x3a850f, e(0x23900000)), + (22, 0x3a8510, e(0x23a00000)), + (22, 0x3a8511, e(0x23b00000)), + (22, 0x3a8512, e(0x23c00000)), + (22, 0x3a8513, e(0x23d00000)), + (22, 0x3a8514, e(0x23e00000)), + (22, 0x3a8515, e(0x23f00000)), + (22, 0x3a8516, e(0x24000000)), + (22, 0x3a8517, e(0x24100000)), + (22, 0x3a8518, e(0x24200000)), + (22, 0x3a8519, e(0x24300000)), + (22, 0x3a851a, e(0x24400000)), + (22, 0x3a851b, e(0x24500000)), + (22, 0x3a851c, e(0x24600000)), + (22, 0x3a851d, e(0x24700000)), + (22, 0x3a851e, e(0x24800000)), + (22, 0x3a851f, e(0x24900000)), + (22, 0x3a8520, e(0x24a00000)), + (22, 0x3a8521, e(0x24b00000)), + (22, 0x3a8522, e(0x24c00000)), + (22, 0x3a8523, e(0x24d00000)), + (22, 0x3a8524, e(0x24e00000)), + (22, 0x3a8525, e(0x24f00000)), + (22, 0x3a8526, e(0x25000000)), + (22, 0x3a8527, e(0x25100000)), + (22, 0x3a8528, e(0x25200000)), + (22, 0x3a8529, e(0x25300000)), + (22, 0x3a852a, e(0x25400000)), + (22, 0x3a852b, e(0x25500000)), + (22, 0x3a852c, e(0x25600000)), + (22, 0x3a852d, e(0x25700000)), + (22, 0x3a852e, e(0x25800000)), + (22, 0x3a852f, e(0x25900000)), + (22, 0x3a8530, e(0x25a00000)), + (22, 0x3a8531, e(0x25b00000)), + (22, 0x3a8532, e(0x25c00000)), + (22, 0x3a8533, e(0x25d00000)), + (22, 0x3a8534, e(0x25e00000)), + (22, 0x3a8535, e(0x25f00000)), + (22, 0x3a8536, e(0x26000000)), + (22, 0x3a8537, e(0x26100000)), + (22, 0x3a8538, e(0x26200000)), + (22, 0x3a8539, e(0x26300000)), + (22, 0x3a853a, e(0x26400000)), + (22, 0x3a853b, e(0x26500000)), + (22, 0x3a853c, e(0x26600000)), + (22, 0x3a853d, e(0x26700000)), + (22, 0x3a853e, e(0x26800000)), + (22, 0x3a853f, e(0x26900000)), + (22, 0x3a8540, e(0x26a00000)), + (22, 0x3a8541, e(0x26b00000)), + (22, 0x3a8542, e(0x26c00000)), + (22, 0x3a8543, e(0x26d00000)), + (22, 0x3a8544, e(0x26e00000)), + (22, 0x3a8545, e(0x26f00000)), + (22, 0x3a8546, e(0x27000000)), + (22, 0x3a8547, e(0x27100000)), + (22, 0x3a8548, e(0x27200000)), + (22, 0x3a8549, e(0x27300000)), + (22, 0x3a854a, e(0x27400000)), + (22, 0x3a854b, e(0x27500000)), + (22, 0x3a854c, e(0x27600000)), + (22, 0x3a854d, e(0x27700000)), + (22, 0x3a854e, e(0x27800000)), + (22, 0x3a854f, e(0x27900000)), + (22, 0x3a8550, e(0x27a00000)), + (22, 0x3a8551, e(0x27b00000)), + (22, 0x3a8552, e(0x27c00000)), + (22, 0x3a8553, e(0x27d00000)), + (22, 0x3a8554, e(0x27e00000)), + (22, 0x3a8555, e(0x27f00000)), + (22, 0x3a8556, e(0x28000000)), + (22, 0x3a8557, e(0x28100000)), + (22, 0x3a8558, e(0x28200000)), + (22, 0x3a8559, e(0x28300000)), + (22, 0x3a855a, e(0x28400000)), + (22, 0x3a855b, e(0x28500000)), + (22, 0x3a855c, e(0x28600000)), + (22, 0x3a855d, e(0x28700000)), + (22, 0x3a855e, e(0x28800000)), + (22, 0x3a855f, e(0x28900000)), + (22, 0x3a8560, e(0x28a00000)), + (22, 0x3a8561, e(0x28b00000)), + (22, 0x3a8562, e(0x28c00000)), + (22, 0x3a8563, e(0x28d00000)), + (22, 0x3a8564, e(0x28e00000)), + (22, 0x3a8565, e(0x28f00000)), + (22, 0x3a8566, e(0x29000000)), + (22, 0x3a8567, e(0x29100000)), + (22, 0x3a8568, e(0x29200000)), + (22, 0x3a8569, e(0x29300000)), + (22, 0x3a856a, e(0x29400000)), + (22, 0x3a856b, e(0x29500000)), + (22, 0x3a856c, e(0x29600000)), + (22, 0x3a856d, e(0x29700000)), + (22, 0x3a856e, e(0x29800000)), + (22, 0x3a856f, e(0x29900000)), + (22, 0x3a8570, e(0x29a00000)), + (22, 0x3a8571, e(0x29b00000)), + (22, 0x3a8572, e(0x29c00000)), + (22, 0x3a8573, e(0x29d00000)), + (22, 0x3a8574, e(0x29e00000)), + (22, 0x3a8575, e(0x29f00000)), + (22, 0x3a8576, e(0x2a000000)), + (22, 0x3a8577, e(0x2a100000)), + (22, 0x3a8578, e(0x2a200000)), + (22, 0x3a8579, e(0x2a300000)), + (22, 0x3a857a, e(0x2a400000)), + (22, 0x3a857b, e(0x2a500000)), + (22, 0x3a857c, e(0x2a600000)), + (22, 0x3a857d, e(0x2a700000)), + (22, 0x3a857e, e(0x2a800000)), + (22, 0x3a857f, e(0x2a900000)), + (22, 0x3a8580, e(0x2aa00000)), + (22, 0x3a8581, e(0x2ab00000)), + (22, 0x3a8582, e(0x2ac00000)), + (22, 0x3a8583, e(0x2ad00000)), + (22, 0x3a8584, e(0x2ae00000)), + (22, 0x3a8585, e(0x2af00000)), + (22, 0x3a8586, e(0x2b000000)), + (22, 0x3a8587, e(0x2b100000)), + (22, 0x3a8588, e(0x2b200000)), + (22, 0x3a8589, e(0x2b300000)), + (22, 0x3a858a, e(0x2b400000)), + (22, 0x3a858b, e(0x2b500000)), + (22, 0x3a858c, e(0x2b600000)), + (22, 0x3a858d, e(0x2b700000)), + (22, 0x3a858e, e(0x2b800000)), + (22, 0x3a858f, e(0x2b900000)), + (22, 0x3a8590, e(0x2ba00000)), + (22, 0x3a8591, e(0x2bb00000)), + (22, 0x3a8592, e(0x2bc00000)), + (22, 0x3a8593, e(0x2bd00000)), + (22, 0x3a8594, e(0x2be00000)), + (22, 0x3a8595, e(0x2bf00000)), + (22, 0x3a8596, e(0x2c000000)), + (22, 0x3a8597, e(0x2c100000)), + (22, 0x3a8598, e(0x2c200000)), + (22, 0x3a8599, e(0x2c300000)), + (22, 0x3a859a, e(0x2c400000)), + (22, 0x3a859b, e(0x2c500000)), + (22, 0x3a859c, e(0x2c600000)), + (22, 0x3a859d, e(0x2c700000)), + (22, 0x3a859e, e(0x2c800000)), + (22, 0x3a859f, e(0x2c900000)), + (22, 0x3a85a0, e(0x2ca00000)), + (22, 0x3a85a1, e(0x2cb00000)), + (22, 0x3a85a2, e(0x2cc00000)), + (22, 0x3a85a3, e(0x2cd00000)), + (22, 0x3a85a4, e(0x2ce00000)), + (22, 0x3a85a5, e(0x2cf00000)), + (22, 0x3a85a6, e(0x2d000000)), + (22, 0x3a85a7, e(0x2d100000)), + (22, 0x3a85a8, e(0x2d200000)), + (22, 0x3a85a9, e(0x2d300000)), + (22, 0x3a85aa, e(0x2d400000)), + (22, 0x3a85ab, e(0x2d500000)), + (22, 0x3a85ac, e(0x2d600000)), + (22, 0x3a85ad, e(0x2d700000)), + (22, 0x3a85ae, e(0x2d800000)), + (22, 0x3a85af, e(0x2d900000)), + (22, 0x3a85b0, e(0x2da00000)), + (22, 0x3a85b1, e(0x2db00000)), + (22, 0x3a85b2, e(0x2dc00000)), + (22, 0x3a85b3, e(0x2dd00000)), + (22, 0x3a85b4, e(0x2de00000)), + (22, 0x3a85b5, e(0x2df00000)), + (22, 0x3a85b6, e(0x2e000000)), + (22, 0x3a85b7, e(0x2e100000)), + (22, 0x3a85b8, e(0x2e200000)), + (22, 0x3a85b9, e(0x2e300000)), + (22, 0x3a85ba, e(0x2e400000)), + (22, 0x3a85bb, e(0x2e500000)), + (22, 0x3a85bc, e(0x2e600000)), + (22, 0x3a85bd, e(0x2e700000)), + (22, 0x3a85be, e(0x2e800000)), + (22, 0x3a85bf, e(0x2e900000)), + (22, 0x3a85c0, e(0x2ea00000)), + (22, 0x3a85c1, e(0x2eb00000)), + (22, 0x3a85c2, e(0x2ec00000)), + (22, 0x3a85c3, e(0x2ed00000)), + (22, 0x3a85c4, e(0x2ee00000)), + (22, 0x3a85c5, e(0x2ef00000)), + (22, 0x3a85c6, e(0x2f000000)), + (22, 0x3a85c7, e(0x2f100000)), + (22, 0x3a85c8, e(0x2f200000)), + (22, 0x3a85c9, e(0x2f300000)), + (22, 0x3a85ca, e(0x2f400000)), + (22, 0x3a85cb, e(0x2f500000)), + (22, 0x3a85cc, e(0x2f600000)), + (22, 0x3a85cd, e(0x2f700000)), + (22, 0x3a85ce, e(0x2f800000)), + (22, 0x3a85cf, e(0x2f900000)), + (22, 0x3a85d0, e(0x2fa00000)), + (22, 0x3a85d1, e(0x2fb00000)), + (22, 0x3a85d2, e(0x2fc00000)), + (22, 0x3a85d3, e(0x2fd00000)), + (22, 0x3a85d4, e(0x2fe00000)), + (22, 0x3a85d5, e(0x2ff00000)), + (22, 0x3a85d6, e(0x30000000)), + (22, 0x3a85d7, e(0x30100000)), + (22, 0x3a85d8, e(0x30200000)), + (22, 0x3a85d9, e(0x30300000)), + (22, 0x3a85da, e(0x30400000)), + (22, 0x3a85db, e(0x30500000)), + (22, 0x3a85dc, e(0x30600000)), + (22, 0x3a85dd, e(0x30700000)), + (22, 0x3a85de, e(0x30800000)), + (22, 0x3a85df, e(0x30900000)), + (22, 0x3a85e0, e(0x30a00000)), + (22, 0x3a85e1, e(0x30b00000)), + (22, 0x3a85e2, e(0x30c00000)), + (22, 0x3a85e3, e(0x30d00000)), + (22, 0x3a85e4, e(0x30e00000)), + (22, 0x3a85e5, e(0x30f00000)), + (22, 0x3a85e6, e(0x31000000)), + (22, 0x3a85e7, e(0x31100000)), + (22, 0x3a85e8, e(0x31200000)), + (22, 0x3a85e9, e(0x31300000)), + (22, 0x3a85ea, e(0x31400000)), + (22, 0x3a85eb, e(0x31500000)), + (22, 0x3a85ec, e(0x31600000)), + (22, 0x3a85ed, e(0x31700000)), + (22, 0x3a85ee, e(0x31800000)), + (22, 0x3a85ef, e(0x31900000)), + (22, 0x3a85f0, e(0x31a00000)), + (22, 0x3a85f1, e(0x31b00000)), + (22, 0x3a85f2, e(0x31c00000)), + (22, 0x3a85f3, e(0x31d00000)), + (22, 0x3a85f4, e(0x31e00000)), + (22, 0x3a85f5, e(0x31f00000)), + (22, 0x3a85f6, e(0x32000000)), + (22, 0x3a85f7, e(0x32100000)), + (22, 0x3a85f8, e(0x32200000)), + (22, 0x3a85f9, e(0x32300000)), + (22, 0x3a85fa, e(0x32400000)), + (22, 0x3a85fb, e(0x32500000)), + (22, 0x3a85fc, e(0x32600000)), + (22, 0x3a85fd, e(0x32700000)), + (22, 0x3a85fe, e(0x32800000)), + (22, 0x3a85ff, e(0x32900000)), + (22, 0x3a8600, e(0x32a00000)), + (22, 0x3a8601, e(0x32b00000)), + (22, 0x3a8602, e(0x32c00000)), + (22, 0x3a8603, e(0x32d00000)), + (22, 0x3a8604, e(0x32e00000)), + (22, 0x3a8605, e(0x32f00000)), + (22, 0x3a8606, e(0x33000000)), + (22, 0x3a8607, e(0x33100000)), + (22, 0x3a8608, e(0x33200000)), + (22, 0x3a8609, e(0x33300000)), + (22, 0x3a860a, e(0x33400000)), + (22, 0x3a860b, e(0x33500000)), + (22, 0x3a860c, e(0x33600000)), + (22, 0x3a860d, e(0x33700000)), + (22, 0x3a860e, e(0x33800000)), + (22, 0x3a860f, e(0x33900000)), + (22, 0x3a8610, e(0x33a00000)), + (22, 0x3a8611, e(0x33b00000)), + (22, 0x3a8612, e(0x33c00000)), + (22, 0x3a8613, e(0x33d00000)), + (22, 0x3a8614, e(0x33e00000)), + (22, 0x3a8615, e(0x33f00000)), + (22, 0x3a8616, e(0x34000000)), + (22, 0x3a8617, e(0x34100000)), + (22, 0x3a8618, e(0x34200000)), + (22, 0x3a8619, e(0x34300000)), + (22, 0x3a861a, e(0x34400000)), + (22, 0x3a861b, e(0x34500000)), + (22, 0x3a861c, e(0x34600000)), + (22, 0x3a861d, e(0x34700000)), + (22, 0x3a861e, e(0x34800000)), + (22, 0x3a861f, e(0x34900000)), + (22, 0x3a8620, e(0x34a00000)), + (22, 0x3a8621, e(0x34b00000)), + (22, 0x3a8622, e(0x34c00000)), + (22, 0x3a8623, e(0x34d00000)), + (22, 0x3a8624, e(0x34e00000)), + (22, 0x3a8625, e(0x34f00000)), + (22, 0x3a8626, e(0x35000000)), + (22, 0x3a8627, e(0x35100000)), + (22, 0x3a8628, e(0x35200000)), + (22, 0x3a8629, e(0x35300000)), + (18, 0xf787, e(0x35400000)), + (18, 0xd1d4, e(0x35500000)), + (19, 0x77f9f, e(0x35600000)), + (18, 0x3b8fb, e(0x35700000)), + (19, 0x1ef1a, e(0x35800000)), + (21, 0x1d4315, e(0x35900000)), + (16, 0x3de2, e(0x35a00000)), + (22, 0x3a862c, e(0x35b00000)), + (22, 0x3a862d, e(0x35c00000)), + (22, 0x3a862e, e(0x35d00000)), + (22, 0x3a862f, e(0x35e00000)), + (22, 0x3a8630, e(0x35f00000)), + (22, 0x3a8631, e(0x36000000)), + (22, 0x3a8632, e(0x36100000)), + (22, 0x3a8633, e(0x36200000)), + (22, 0x3a8634, e(0x36300000)), + (22, 0x3a8635, e(0x36400000)), + (22, 0x3a8636, e(0x36500000)), + (22, 0x3a8637, e(0x36600000)), + (22, 0x3a8638, e(0x36700000)), + (21, 0x1d431d, e(0x36800000)), + (22, 0x3a8639, e(0x36900000)), + (22, 0x3a863c, e(0x36a00000)), + (16, 0x3de0, e(0x36b00000)), + (18, 0x3a95e, e(0x36c00000)), + (21, 0x1d431f, e(0x36d00000)), + (22, 0x3a863d, e(0x36e00000)), + (22, 0x3a8640, e(0x36f00000)), + (20, 0x34749, e(0x37000000)), + (20, 0x3474d, e(0x37100000)), + (22, 0x3a8641, e(0x37200000)), + (22, 0x3a8642, e(0x37300000)), + (22, 0x3a8643, e(0x37400000)), + (22, 0x3a8644, e(0x37500000)), + (22, 0x3a8645, e(0x37600000)), + (22, 0x3a8646, e(0x37700000)), + (22, 0x3a8647, e(0x37800000)), + (22, 0x3a8648, e(0x37900000)), + (22, 0x3a8649, e(0x37a00000)), + (22, 0x3a864a, e(0x37b00000)), + (22, 0x3a864b, e(0x37c00000)), + (22, 0x3a864c, e(0x37d00000)), + (22, 0x3a864d, e(0x37e00000)), + (22, 0x3a864e, e(0x37f00000)), + (22, 0x3a864f, e(0x38000000)), + (22, 0x3a8650, e(0x38100000)), + (22, 0x3a8651, e(0x38200000)), + (22, 0x3a8652, e(0x38300000)), + (22, 0x3a8653, e(0x38400000)), + (22, 0x3a8654, e(0x38500000)), + (22, 0x3a8655, e(0x38600000)), + (22, 0x3a8656, e(0x38700000)), + (22, 0x3a8657, e(0x38800000)), + (22, 0x3a8658, e(0x38900000)), + (22, 0x3a8659, e(0x38a00000)), + (22, 0x3a865a, e(0x38b00000)), + (22, 0x3a865b, e(0x38c00000)), + (22, 0x3a865c, e(0x38d00000)), + (22, 0x3a865d, e(0x38e00000)), + (21, 0x1d432f, e(0x38f00000)), + (22, 0x3a8660, e(0x39000000)), + (22, 0x3a8661, e(0x39100000)), + (22, 0x3a8662, e(0x39200000)), + (21, 0x1d4332, e(0x39300000)), + (18, 0xd1d5, e(0x39400000)), + (18, 0x3bfda, e(0x39500000)), + (15, 0x7528, e(0x39600000)), + (15, 0x7529, e(0x39700000)), + (18, 0x3a95f, e(0x39800000)), + (17, 0x1d434, e(0x39900000)), + (19, 0x750cd, e(0x39a00000)), + (18, 0x3a867, e(0x39b00000)), + (19, 0x771fd, e(0x39c00000)), + (15, 0x7764, e(0x39d00000)), + (20, 0xee3f9, e(0x39e00000)), + (18, 0x3bfdb, e(0x39f00000)), + (16, 0xeff7, e(0x3a000000)), + (18, 0xd1d6, e(0x3a100000)), + (22, 0x3a8663, e(0x3a200000)), + (22, 0x3a8666, e(0x3a300000)), + (18, 0x3b8fa, e(0x3a400000)), + (17, 0x1d435, e(0x3a500000)), + (17, 0x1dfe4, e(0x3a600000)), + (19, 0x750d8, e(0x3a700000)), + (18, 0x3a95b, e(0x3a800000)), + (19, 0x77f9e, e(0x3a900000)), + (19, 0x750d9, e(0x3aa00000)), + (18, 0xd1d7, e(0x3ab00000)), + (18, 0x3b8ff, e(0x3ac00000)), + (17, 0x1dc7c, e(0x3ad00000)), + (19, 0x750da, e(0x3ae00000)), + (17, 0x7bc2, e(0x3af00000)), + (18, 0x3bfca, e(0x3b000000)), + (19, 0x1a3a5, e(0x3b100000)), + (17, 0x1d4ac, e(0x3b200000)), + (18, 0x3a86e, e(0x3b300000)), + (17, 0x1d438, e(0x3b400000)), + (18, 0x3bfcb, e(0x3b500000)), + (19, 0x1a3a7, e(0x3b600000)), + (17, 0x1d4ae, e(0x3b700000)), + (18, 0x3bfce, e(0x3b800000)), + (18, 0xf78c, e(0x3b900000)), + (17, 0x1dfec, e(0x3ba00000)), + (17, 0x1d439, e(0x3bb00000)), + (17, 0x68e8, e(0x3bc00000)), + (18, 0xf786, e(0x3bd00000)), + (15, 0x771e, e(0x3be00000)), + (17, 0x1dfe6, e(0x3bf00000)), + (15, 0x77f8, e(0x3c000000)), + (14, 0x3bb3, e(0x3c100000)), + (14, 0x3b8e, e(0x3c200000)), + (14, 0x3a82, e(0x3c300000)), + (14, 0x3b96, e(0x3c400000)), + (13, 0x68f, e(0x3c500000)), + (12, 0x3d4, e(0x3c600000)), + (13, 0x1dca, e(0x3c700000)), + (12, 0x346, e(0x3c800000)), + (12, 0xee7, e(0x3c900000)), + (12, 0xeea, e(0x3ca00000)), + (11, 0x1ed, e(0x3cb00000)), + (12, 0x3df, e(0x3cc00000)), + (11, 0x1a2, e(0x3cd00000)), + (11, 0x56f, e(0x3ce00000)), + (11, 0xb9, e(0x3cf00000)), + (13, 0x7b2, e(0x3d000000)), + (13, 0x1dd8, e(0x3d100000)), + (13, 0x15ba, e(0x3d200000)), + (12, 0xee6, e(0x3d300000)), + (13, 0x15b8, e(0x3d400000)), + (14, 0xf79, e(0x3d500000)), + (14, 0x3a81, e(0x3d600000)), + (14, 0xd1c, e(0x3d700000)), + (15, 0x7765, e(0x3d800000)), + (14, 0xf54, e(0x3d900000)), + (13, 0x15b9, e(0x3da00000)), + (13, 0x7ab, e(0x3db00000)), + (15, 0x7500, e(0x3dc00000)), + (15, 0x1eaa, e(0x3dd00000)), + (15, 0x7501, e(0x3de00000)), + (15, 0x1eab, e(0x3df00000)), + (14, 0x3b97, e(0x3e000000)), + (15, 0x752a, e(0x3e100000)), + (15, 0x77fa, e(0x3e200000)), + (10, 0xf4, d(0x3e35798e, 0xe2308c3a)), + (14, 0x3a93, e(0x3e300000)), + (11, 0x77c, d(0x3e45798e, 0xe2308c3a)), + (13, 0x1dc6, e(0x3e400000)), + (13, 0x7bd, e(0x3e500000)), + (13, 0x1dff, e(0x3e600000)), + (12, 0xefe, e(0x3e700000)), + (8, 0xaf, d(0x3e8ad7f2, 0x9abcaf4a)), + (12, 0xeed, e(0x3e800000)), + (11, 0xb8, e(0x3e900000)), + (12, 0x3d8, e(0x3ea00000)), + (11, 0x1eb, e(0x3eb00000)), + (9, 0x1d2, d(0x3ec0c6f7, 0xa0b5ed8e)), + (13, 0x1d4b, e(0x3ec00000)), + (13, 0x7b3, e(0x3ed00000)), + (10, 0x5d, e(0x3ee00000)), + (12, 0xeeb, e(0x3ef00000)), + (11, 0x1ee, e(0x3f000000)), + (10, 0x5f, e(0x3f100000)), + (10, 0x2b6, e(0x3f200000)), + (9, 0x1de, e(0x3f300000)), + (10, 0xd0, d(0x3f454c98, 0x5f06f694)), + (6, 0x9, d(0x3f4a36e2, 0xeb1c432d)), + (8, 0xe8, e(0x3f400000)), + (4, 0xf, d(0x3f50624d, 0xd2f1a9fc)), + (8, 0xae, e(0x3f500000)), + (5, 0x16, d(0x3f60624d, 0xd2f1a9fc)), + (7, 0x1b, e(0x3f600000)), + (7, 0x76, e(0x3f700000)), + (7, 0xa, e(0x3f800000)), + (6, 0x8, e(0x3f900000)), + (6, 0xe, e(0x3fa00000)), + (11, 0x751, d(0x3fbe69ad, 0x42c3c9ee)), + (6, 0x4, e(0x3fb00000)), + (6, 0xc, e(0x3fc00000)), + (5, 0x3, e(0x3fd00000)), + (11, 0x777, d(0x3fe00000, 0x00000000)), + (9, 0x1d6, d(0x3fefffff, 0xf8000002)), + (4, 0x8, e(0x3fe00000)), + (4, 0x0, d(0x3ff00000, 0x00000000)), + (5, 0x13, e(0x3ff00000)), + (5, 0x1b, e(0x40000000)), + (9, 0x15a, d(0x401921fb, 0x54442d18)), + (5, 0x17, e(0x40100000)), + (5, 0x12, e(0x40200000)), + (11, 0x774, d(0x4035ee14, 0x80000000)), + (5, 0x19, e(0x40300000)), + (9, 0x1d3, d(0x404ca5dc, 0x1a63c1f8)), + (5, 0x1a, e(0x40400000)), + (11, 0x77e, d(0x405bb32f, 0xe0000000)), + (10, 0x5e, d(0x405c332f, 0xe0000000)), + (5, 0x18, e(0x40500000)), + (9, 0x1d7, d(0x40668000, 0x00000000)), + (5, 0x1c, e(0x40600000)), + (9, 0x1d5, d(0x40768000, 0x00000000)), + (5, 0x14, e(0x40700000)), + (11, 0x77d, d(0x408f4000, 0x00000000)), + (5, 0x5, e(0x40800000)), + (10, 0xd2, d(0x409233ff, 0xffffffff)), + (8, 0x3c, d(0x40923400, 0x00000000)), + (11, 0x753, d(0x40923400, 0x00000001)), + (10, 0xd3, d(0x4092abff, 0xffffffff)), + (8, 0x35, d(0x4092ac00, 0x00000000)), + (11, 0x770, d(0x4092ac00, 0x00000001)), + (8, 0x16, e(0x40900000)), + (12, 0xee2, e(0x40a00000)), + (12, 0xee4, e(0x40b00000)), + (7, 0x1f, d(0x40c81c80, 0x00000000)), + (8, 0xac, e(0x40c00000)), + (13, 0x15bb, e(0x40d00000)), + (22, 0x3a8667, e(0x40e00000)), + (22, 0x3a86d8, e(0x40f00000)), + (22, 0x3a86d9, e(0x41000000)), + (22, 0x3a86da, e(0x41100000)), + (17, 0x1dc7e, e(0x41200000)), + (22, 0x3a86db, e(0x41300000)), + (22, 0x3a86dc, e(0x41400000)), + (22, 0x3a86dd, e(0x41500000)), + (22, 0x3a86de, e(0x41600000)), + (22, 0x3a86df, e(0x41700000)), + (22, 0x3a86f0, e(0x41800000)), + (22, 0x3a86f1, e(0x41900000)), + (22, 0x3a86f2, e(0x41a00000)), + (22, 0x3a86f3, e(0x41b00000)), + (6, 0x2a, d(0x41cdcd64, 0xff800000)), + (22, 0x3a86f4, e(0x41c00000)), + (22, 0x3a86f5, e(0x41d00000)), + (22, 0x3a86f6, e(0x41e00000)), + (22, 0x3a86f7, e(0x41f00000)), + (22, 0x3a86f8, e(0x42000000)), + (22, 0x3a86f9, e(0x42100000)), + (22, 0x3a86fa, e(0x42200000)), + (22, 0x3a86fb, e(0x42300000)), + (22, 0x3a86fc, e(0x42400000)), + (22, 0x3a86fd, e(0x42500000)), + (22, 0x3a86fe, e(0x42600000)), + (22, 0x3a86ff, e(0x42700000)), + (22, 0x3a8740, e(0x42800000)), + (22, 0x3a8741, e(0x42900000)), + (22, 0x3a8742, e(0x42a00000)), + (22, 0x3a8743, e(0x42b00000)), + (22, 0x3a8744, e(0x42c00000)), + (22, 0x3a8745, e(0x42d00000)), + (22, 0x3a8746, e(0x42e00000)), + (22, 0x3a8747, e(0x42f00000)), + (22, 0x3a8748, e(0x43000000)), + (22, 0x3a8749, e(0x43100000)), + (22, 0x3a874a, e(0x43200000)), + (22, 0x3a874b, e(0x43300000)), + (22, 0x3a874c, e(0x43400000)), + (22, 0x3a874d, e(0x43500000)), + (22, 0x3a874e, e(0x43600000)), + (22, 0x3a874f, e(0x43700000)), + (22, 0x3a8750, e(0x43800000)), + (22, 0x3a8751, e(0x43900000)), + (22, 0x3a8752, e(0x43a00000)), + (22, 0x3a8753, e(0x43b00000)), + (22, 0x3a8754, e(0x43c00000)), + (22, 0x3a8755, e(0x43d00000)), + (22, 0x3a8756, e(0x43e00000)), + (22, 0x3a8757, e(0x43f00000)), + (22, 0x3a8758, e(0x44000000)), + (15, 0x1a3b, e(0x44100000)), + (22, 0x3a8759, e(0x44200000)), + (22, 0x3a875a, e(0x44300000)), + (22, 0x3a875b, e(0x44400000)), + (22, 0x3a875c, e(0x44500000)), + (22, 0x3a875d, e(0x44600000)), + (22, 0x3a875e, e(0x44700000)), + (22, 0x3a875f, e(0x44800000)), + (22, 0x3a8760, e(0x44900000)), + (22, 0x3a8761, e(0x44a00000)), + (22, 0x3a8762, e(0x44b00000)), + (22, 0x3a8763, e(0x44c00000)), + (22, 0x3a8764, e(0x44d00000)), + (22, 0x3a8765, e(0x44e00000)), + (22, 0x3a8766, e(0x44f00000)), + (22, 0x3a8767, e(0x45000000)), + (22, 0x3a8768, e(0x45100000)), + (22, 0x3a8769, e(0x45200000)), + (22, 0x3a876a, e(0x45300000)), + (22, 0x3a876b, e(0x45400000)), + (22, 0x3a876c, e(0x45500000)), + (22, 0x3a876d, e(0x45600000)), + (22, 0x3a876e, e(0x45700000)), + (22, 0x3a876f, e(0x45800000)), + (22, 0x3a8770, e(0x45900000)), + (22, 0x3a8771, e(0x45a00000)), + (22, 0x3a8772, e(0x45b00000)), + (22, 0x3a8773, e(0x45c00000)), + (22, 0x3a8774, e(0x45d00000)), + (22, 0x3a8775, e(0x45e00000)), + (22, 0x3a8776, e(0x45f00000)), + (22, 0x3a8777, e(0x46000000)), + (22, 0x3a8778, e(0x46100000)), + (22, 0x3a8779, e(0x46200000)), + (22, 0x3a877a, e(0x46300000)), + (22, 0x3a877b, e(0x46400000)), + (22, 0x3a877c, e(0x46500000)), + (22, 0x3a877d, e(0x46600000)), + (22, 0x3a877e, e(0x46700000)), + (22, 0x3a877f, e(0x46800000)), + (22, 0x3a8780, e(0x46900000)), + (22, 0x3a8781, e(0x46a00000)), + (22, 0x3a8782, e(0x46b00000)), + (22, 0x3a8783, e(0x46c00000)), + (22, 0x3a8784, e(0x46d00000)), + (22, 0x3a8785, e(0x46e00000)), + (22, 0x3a8786, e(0x46f00000)), + (22, 0x3a8787, e(0x47000000)), + (22, 0x3a8788, e(0x47100000)), + (22, 0x3a8789, e(0x47200000)), + (22, 0x3a878a, e(0x47300000)), + (22, 0x3a878b, e(0x47400000)), + (22, 0x3a878c, e(0x47500000)), + (22, 0x3a878d, e(0x47600000)), + (22, 0x3a878e, e(0x47700000)), + (22, 0x3a878f, e(0x47800000)), + (22, 0x3a8790, e(0x47900000)), + (22, 0x3a8791, e(0x47a00000)), + (22, 0x3a8792, e(0x47b00000)), + (22, 0x3a8793, e(0x47c00000)), + (22, 0x3a8794, e(0x47d00000)), + (22, 0x3a8795, e(0x47e00000)), + (22, 0x3a8796, e(0x47f00000)), + (22, 0x3a8797, e(0x48000000)), + (22, 0x3a8798, e(0x48100000)), + (22, 0x3a8799, e(0x48200000)), + (22, 0x3a879a, e(0x48300000)), + (22, 0x3a879b, e(0x48400000)), + (22, 0x3a879c, e(0x48500000)), + (22, 0x3a879d, e(0x48600000)), + (22, 0x3a879e, e(0x48700000)), + (22, 0x3a879f, e(0x48800000)), + (22, 0x3a87a0, e(0x48900000)), + (22, 0x3a87a1, e(0x48a00000)), + (22, 0x3a87a2, e(0x48b00000)), + (22, 0x3a87a3, e(0x48c00000)), + (22, 0x3a87a4, e(0x48d00000)), + (22, 0x3a87a5, e(0x48e00000)), + (22, 0x3a87a6, e(0x48f00000)), + (22, 0x3a87a7, e(0x49000000)), + (22, 0x3a87a8, e(0x49100000)), + (22, 0x3a87a9, e(0x49200000)), + (22, 0x3a87aa, e(0x49300000)), + (22, 0x3a87ab, e(0x49400000)), + (22, 0x3a87ac, e(0x49500000)), + (22, 0x3a87ad, e(0x49600000)), + (22, 0x3a87ae, e(0x49700000)), + (22, 0x3a87af, e(0x49800000)), + (22, 0x3a87b0, e(0x49900000)), + (22, 0x3a87b1, e(0x49a00000)), + (22, 0x3a87b2, e(0x49b00000)), + (22, 0x3a87b3, e(0x49c00000)), + (22, 0x3a87b4, e(0x49d00000)), + (22, 0x3a87b5, e(0x49e00000)), + (22, 0x3a87b6, e(0x49f00000)), + (22, 0x3a87b7, e(0x4a000000)), + (22, 0x3a87b8, e(0x4a100000)), + (22, 0x3a87b9, e(0x4a200000)), + (22, 0x3a87ba, e(0x4a300000)), + (22, 0x3a87bb, e(0x4a400000)), + (22, 0x3a87bc, e(0x4a500000)), + (22, 0x3a87bd, e(0x4a600000)), + (22, 0x3a87be, e(0x4a700000)), + (22, 0x3a87bf, e(0x4a800000)), + (22, 0x3a87c0, e(0x4a900000)), + (22, 0x3a87c1, e(0x4aa00000)), + (22, 0x3a87c2, e(0x4ab00000)), + (22, 0x3a87c3, e(0x4ac00000)), + (22, 0x3a87c4, e(0x4ad00000)), + (22, 0x3a87c5, e(0x4ae00000)), + (22, 0x3a87c6, e(0x4af00000)), + (22, 0x3a87c7, e(0x4b000000)), + (22, 0x3a87c8, e(0x4b100000)), + (22, 0x3a87c9, e(0x4b200000)), + (22, 0x3a87ca, e(0x4b300000)), + (22, 0x3a87cb, e(0x4b400000)), + (22, 0x3a87cc, e(0x4b500000)), + (22, 0x3a87cd, e(0x4b600000)), + (22, 0x3a87ce, e(0x4b700000)), + (22, 0x3a87cf, e(0x4b800000)), + (22, 0x3a87d0, e(0x4b900000)), + (22, 0x3a87d1, e(0x4ba00000)), + (22, 0x3a87d2, e(0x4bb00000)), + (22, 0x3a87d3, e(0x4bc00000)), + (22, 0x3a87d4, e(0x4bd00000)), + (22, 0x3a87d5, e(0x4be00000)), + (22, 0x3a87d6, e(0x4bf00000)), + (22, 0x3a87d7, e(0x4c000000)), + (22, 0x3a87d8, e(0x4c100000)), + (22, 0x3a87d9, e(0x4c200000)), + (22, 0x3a87da, e(0x4c300000)), + (22, 0x3a87db, e(0x4c400000)), + (22, 0x3a87dc, e(0x4c500000)), + (22, 0x3a87dd, e(0x4c600000)), + (22, 0x3a87de, e(0x4c700000)), + (22, 0x3a87df, e(0x4c800000)), + (22, 0x3a87e0, e(0x4c900000)), + (22, 0x3a87e1, e(0x4ca00000)), + (22, 0x3a87e2, e(0x4cb00000)), + (22, 0x3a87e3, e(0x4cc00000)), + (22, 0x3a87e4, e(0x4cd00000)), + (22, 0x3a87e5, e(0x4ce00000)), + (22, 0x3a87e6, e(0x4cf00000)), + (22, 0x3a87e7, e(0x4d000000)), + (22, 0x3a87e8, e(0x4d100000)), + (22, 0x3a87e9, e(0x4d200000)), + (22, 0x3a87ea, e(0x4d300000)), + (22, 0x3a87eb, e(0x4d400000)), + (22, 0x3a87ec, e(0x4d500000)), + (22, 0x3a87ed, e(0x4d600000)), + (22, 0x3a87ee, e(0x4d700000)), + (22, 0x3a87ef, e(0x4d800000)), + (22, 0x3a87f0, e(0x4d900000)), + (22, 0x3a87f1, e(0x4da00000)), + (22, 0x3a87f2, e(0x4db00000)), + (22, 0x3a87f3, e(0x4dc00000)), + (22, 0x3a87f4, e(0x4dd00000)), + (22, 0x3a87f5, e(0x4de00000)), + (22, 0x3a87f6, e(0x4df00000)), + (22, 0x3a87f7, e(0x4e000000)), + (22, 0x3a87f8, e(0x4e100000)), + (22, 0x3a87f9, e(0x4e200000)), + (22, 0x3a87fa, e(0x4e300000)), + (22, 0x3a87fb, e(0x4e400000)), + (22, 0x3a87fc, e(0x4e500000)), + (22, 0x3a87fd, e(0x4e600000)), + (22, 0x3a87fe, e(0x4e700000)), + (22, 0x3a87ff, e(0x4e800000)), + (22, 0x3a9000, e(0x4e900000)), + (22, 0x3a9001, e(0x4ea00000)), + (22, 0x3a9002, e(0x4eb00000)), + (22, 0x3a9003, e(0x4ec00000)), + (22, 0x3a9004, e(0x4ed00000)), + (22, 0x3a9005, e(0x4ee00000)), + (22, 0x3a9006, e(0x4ef00000)), + (22, 0x3a9007, e(0x4f000000)), + (22, 0x3a9008, e(0x4f100000)), + (22, 0x3a9009, e(0x4f200000)), + (22, 0x3a900a, e(0x4f300000)), + (22, 0x3a900b, e(0x4f400000)), + (22, 0x3a900c, e(0x4f500000)), + (22, 0x3a900d, e(0x4f600000)), + (22, 0x3a900e, e(0x4f700000)), + (22, 0x3a900f, e(0x4f800000)), + (22, 0x3a9010, e(0x4f900000)), + (22, 0x3a9011, e(0x4fa00000)), + (22, 0x3a9012, e(0x4fb00000)), + (22, 0x3a9013, e(0x4fc00000)), + (22, 0x3a9014, e(0x4fd00000)), + (22, 0x3a9015, e(0x4fe00000)), + (22, 0x3a9016, e(0x4ff00000)), + (22, 0x3a9017, e(0x50000000)), + (22, 0x3a9018, e(0x50100000)), + (22, 0x3a9019, e(0x50200000)), + (22, 0x3a901a, e(0x50300000)), + (22, 0x3a901b, e(0x50400000)), + (22, 0x3a901c, e(0x50500000)), + (22, 0x3a901d, e(0x50600000)), + (22, 0x3a901e, e(0x50700000)), + (22, 0x3a901f, e(0x50800000)), + (22, 0x3a9020, e(0x50900000)), + (22, 0x3a9021, e(0x50a00000)), + (22, 0x3a9022, e(0x50b00000)), + (22, 0x3a9023, e(0x50c00000)), + (22, 0x3a9024, e(0x50d00000)), + (22, 0x3a9025, e(0x50e00000)), + (22, 0x3a9026, e(0x50f00000)), + (22, 0x3a9027, e(0x51000000)), + (22, 0x3a9028, e(0x51100000)), + (22, 0x3a9029, e(0x51200000)), + (22, 0x3a902a, e(0x51300000)), + (22, 0x3a902b, e(0x51400000)), + (22, 0x3a902c, e(0x51500000)), + (22, 0x3a902d, e(0x51600000)), + (22, 0x3a902e, e(0x51700000)), + (22, 0x3a902f, e(0x51800000)), + (22, 0x3a9030, e(0x51900000)), + (22, 0x3a9031, e(0x51a00000)), + (22, 0x3a9032, e(0x51b00000)), + (22, 0x3a9033, e(0x51c00000)), + (22, 0x3a9034, e(0x51d00000)), + (22, 0x3a9035, e(0x51e00000)), + (22, 0x3a9036, e(0x51f00000)), + (22, 0x3a9037, e(0x52000000)), + (22, 0x3a9038, e(0x52100000)), + (22, 0x3a9039, e(0x52200000)), + (22, 0x3a903a, e(0x52300000)), + (22, 0x3a903b, e(0x52400000)), + (22, 0x3a903c, e(0x52500000)), + (22, 0x3a903d, e(0x52600000)), + (22, 0x3a903e, e(0x52700000)), + (22, 0x3a903f, e(0x52800000)), + (22, 0x3a9040, e(0x52900000)), + (22, 0x3a9041, e(0x52a00000)), + (22, 0x3a9042, e(0x52b00000)), + (22, 0x3a9043, e(0x52c00000)), + (22, 0x3a9044, e(0x52d00000)), + (22, 0x3a9045, e(0x52e00000)), + (22, 0x3a9046, e(0x52f00000)), + (22, 0x3a9047, e(0x53000000)), + (22, 0x3a9048, e(0x53100000)), + (22, 0x3a9049, e(0x53200000)), + (22, 0x3a904a, e(0x53300000)), + (22, 0x3a904b, e(0x53400000)), + (22, 0x3a904c, e(0x53500000)), + (22, 0x3a904d, e(0x53600000)), + (22, 0x3a904e, e(0x53700000)), + (22, 0x3a904f, e(0x53800000)), + (22, 0x3a9050, e(0x53900000)), + (22, 0x3a9051, e(0x53a00000)), + (22, 0x3a9052, e(0x53b00000)), + (22, 0x3a9053, e(0x53c00000)), + (22, 0x3a9054, e(0x53d00000)), + (22, 0x3a9055, e(0x53e00000)), + (22, 0x3a9056, e(0x53f00000)), + (22, 0x3a9057, e(0x54000000)), + (22, 0x3a9058, e(0x54100000)), + (22, 0x3a9059, e(0x54200000)), + (22, 0x3a905a, e(0x54300000)), + (22, 0x3a905b, e(0x54400000)), + (22, 0x3a905c, e(0x54500000)), + (22, 0x3a905d, e(0x54600000)), + (22, 0x3a905e, e(0x54700000)), + (22, 0x3a905f, e(0x54800000)), + (22, 0x3a9060, e(0x54900000)), + (22, 0x3a9061, e(0x54a00000)), + (22, 0x3a9062, e(0x54b00000)), + (22, 0x3a9063, e(0x54c00000)), + (22, 0x3a9064, e(0x54d00000)), + (22, 0x3a9065, e(0x54e00000)), + (22, 0x3a9066, e(0x54f00000)), + (22, 0x3a9067, e(0x55000000)), + (22, 0x3a9068, e(0x55100000)), + (22, 0x3a9069, e(0x55200000)), + (22, 0x3a906a, e(0x55300000)), + (22, 0x3a906b, e(0x55400000)), + (22, 0x3a906c, e(0x55500000)), + (22, 0x3a906d, e(0x55600000)), + (22, 0x3a906e, e(0x55700000)), + (22, 0x3a906f, e(0x55800000)), + (22, 0x3a9070, e(0x55900000)), + (22, 0x3a9071, e(0x55a00000)), + (22, 0x3a9072, e(0x55b00000)), + (22, 0x3a9073, e(0x55c00000)), + (22, 0x3a9074, e(0x55d00000)), + (22, 0x3a9075, e(0x55e00000)), + (22, 0x3a9076, e(0x55f00000)), + (22, 0x3a9077, e(0x56000000)), + (22, 0x3a9078, e(0x56100000)), + (22, 0x3a9079, e(0x56200000)), + (22, 0x3a907a, e(0x56300000)), + (22, 0x3a907b, e(0x56400000)), + (22, 0x3a907c, e(0x56500000)), + (22, 0x3a907d, e(0x56600000)), + (22, 0x3a907e, e(0x56700000)), + (22, 0x3a907f, e(0x56800000)), + (22, 0x3a9080, e(0x56900000)), + (22, 0x3a9081, e(0x56a00000)), + (22, 0x3a9082, e(0x56b00000)), + (22, 0x3a9083, e(0x56c00000)), + (22, 0x3a9084, e(0x56d00000)), + (22, 0x3a9085, e(0x56e00000)), + (22, 0x3a9086, e(0x56f00000)), + (22, 0x3a9087, e(0x57000000)), + (22, 0x3a9088, e(0x57100000)), + (22, 0x3a9089, e(0x57200000)), + (22, 0x3a908a, e(0x57300000)), + (22, 0x3a908b, e(0x57400000)), + (22, 0x3a908c, e(0x57500000)), + (22, 0x3a908d, e(0x57600000)), + (22, 0x3a908e, e(0x57700000)), + (22, 0x3a908f, e(0x57800000)), + (22, 0x3a9090, e(0x57900000)), + (22, 0x3a9091, e(0x57a00000)), + (22, 0x3a9092, e(0x57b00000)), + (22, 0x3a9093, e(0x57c00000)), + (22, 0x3a9094, e(0x57d00000)), + (22, 0x3a9095, e(0x57e00000)), + (22, 0x3a9096, e(0x57f00000)), + (22, 0x3a9097, e(0x58000000)), + (22, 0x3a9098, e(0x58100000)), + (22, 0x3a9099, e(0x58200000)), + (22, 0x3a909a, e(0x58300000)), + (22, 0x3a909b, e(0x58400000)), + (22, 0x3a909c, e(0x58500000)), + (22, 0x3a909d, e(0x58600000)), + (22, 0x3a909e, e(0x58700000)), + (22, 0x3a909f, e(0x58800000)), + (22, 0x3a90a0, e(0x58900000)), + (22, 0x3a90a1, e(0x58a00000)), + (22, 0x3a90a2, e(0x58b00000)), + (22, 0x3a90a3, e(0x58c00000)), + (22, 0x3a90a4, e(0x58d00000)), + (22, 0x3a90a5, e(0x58e00000)), + (22, 0x3a90a6, e(0x58f00000)), + (22, 0x3a90a7, e(0x59000000)), + (22, 0x3a90a8, e(0x59100000)), + (22, 0x3a90a9, e(0x59200000)), + (22, 0x3a90aa, e(0x59300000)), + (22, 0x3a90ab, e(0x59400000)), + (22, 0x3a90ac, e(0x59500000)), + (22, 0x3a90ad, e(0x59600000)), + (22, 0x3a90ae, e(0x59700000)), + (22, 0x3a90af, e(0x59800000)), + (22, 0x3a90b0, e(0x59900000)), + (22, 0x3a90b1, e(0x59a00000)), + (22, 0x3a90b2, e(0x59b00000)), + (22, 0x3a90b3, e(0x59c00000)), + (22, 0x3a90b4, e(0x59d00000)), + (22, 0x3a90b5, e(0x59e00000)), + (22, 0x3a90b6, e(0x59f00000)), + (22, 0x3a90b7, e(0x5a000000)), + (22, 0x3a90b8, e(0x5a100000)), + (22, 0x3a90b9, e(0x5a200000)), + (22, 0x3a90ba, e(0x5a300000)), + (22, 0x3a90bb, e(0x5a400000)), + (22, 0x3a90bc, e(0x5a500000)), + (22, 0x3a90bd, e(0x5a600000)), + (22, 0x3a90be, e(0x5a700000)), + (22, 0x3a90bf, e(0x5a800000)), + (22, 0x3a90c0, e(0x5a900000)), + (22, 0x3a90c1, e(0x5aa00000)), + (22, 0x3a90c2, e(0x5ab00000)), + (22, 0x3a90c3, e(0x5ac00000)), + (22, 0x3a90c4, e(0x5ad00000)), + (22, 0x3a90c5, e(0x5ae00000)), + (22, 0x3a90c6, e(0x5af00000)), + (22, 0x3a90c7, e(0x5b000000)), + (22, 0x3a90c8, e(0x5b100000)), + (22, 0x3a90c9, e(0x5b200000)), + (22, 0x3a90ca, e(0x5b300000)), + (22, 0x3a90cb, e(0x5b400000)), + (22, 0x3a90cc, e(0x5b500000)), + (22, 0x3a90cd, e(0x5b600000)), + (22, 0x3a90ce, e(0x5b700000)), + (22, 0x3a90cf, e(0x5b800000)), + (22, 0x3a90d0, e(0x5b900000)), + (22, 0x3a90d1, e(0x5ba00000)), + (22, 0x3a90d2, e(0x5bb00000)), + (22, 0x3a90d3, e(0x5bc00000)), + (22, 0x3a90d4, e(0x5bd00000)), + (22, 0x3a90d5, e(0x5be00000)), + (22, 0x3a90d6, e(0x5bf00000)), + (22, 0x3a90d7, e(0x5c000000)), + (22, 0x3a90d8, e(0x5c100000)), + (22, 0x3a90d9, e(0x5c200000)), + (22, 0x3a90da, e(0x5c300000)), + (22, 0x3a90db, e(0x5c400000)), + (22, 0x3a90dc, e(0x5c500000)), + (22, 0x3a90dd, e(0x5c600000)), + (22, 0x3a90de, e(0x5c700000)), + (22, 0x3a90df, e(0x5c800000)), + (22, 0x3a90e0, e(0x5c900000)), + (22, 0x3a90e1, e(0x5ca00000)), + (22, 0x3a90e2, e(0x5cb00000)), + (22, 0x3a90e3, e(0x5cc00000)), + (22, 0x3a90e4, e(0x5cd00000)), + (22, 0x3a90e5, e(0x5ce00000)), + (22, 0x3a90e6, e(0x5cf00000)), + (22, 0x3a90e7, e(0x5d000000)), + (22, 0x3a90e8, e(0x5d100000)), + (22, 0x3a90e9, e(0x5d200000)), + (22, 0x3a90ea, e(0x5d300000)), + (22, 0x3a90eb, e(0x5d400000)), + (22, 0x3a90ec, e(0x5d500000)), + (22, 0x3a90ed, e(0x5d600000)), + (22, 0x3a90ee, e(0x5d700000)), + (22, 0x3a90ef, e(0x5d800000)), + (22, 0x3a90f0, e(0x5d900000)), + (22, 0x3a90f1, e(0x5da00000)), + (22, 0x3a90f2, e(0x5db00000)), + (22, 0x3a90f3, e(0x5dc00000)), + (22, 0x3a90f4, e(0x5dd00000)), + (22, 0x3a90f5, e(0x5de00000)), + (22, 0x3a90f6, e(0x5df00000)), + (22, 0x3a90f7, e(0x5e000000)), + (22, 0x3a90f8, e(0x5e100000)), + (22, 0x3a90f9, e(0x5e200000)), + (22, 0x3a90fa, e(0x5e300000)), + (22, 0x3a90fb, e(0x5e400000)), + (22, 0x3a90fc, e(0x5e500000)), + (22, 0x3a90fd, e(0x5e600000)), + (22, 0x3a90fe, e(0x5e700000)), + (22, 0x3a90ff, e(0x5e800000)), + (22, 0x3a9100, e(0x5e900000)), + (22, 0x3a9101, e(0x5ea00000)), + (22, 0x3a9102, e(0x5eb00000)), + (22, 0x3a9103, e(0x5ec00000)), + (22, 0x3a9104, e(0x5ed00000)), + (22, 0x3a9105, e(0x5ee00000)), + (22, 0x3a9106, e(0x5ef00000)), + (22, 0x3a9107, e(0x5f000000)), + (22, 0x3a9108, e(0x5f100000)), + (22, 0x3a9109, e(0x5f200000)), + (22, 0x3a910a, e(0x5f300000)), + (22, 0x3a910b, e(0x5f400000)), + (22, 0x3a910c, e(0x5f500000)), + (22, 0x3a910d, e(0x5f600000)), + (22, 0x3a910e, e(0x5f700000)), + (22, 0x3a910f, e(0x5f800000)), + (22, 0x3a9110, e(0x5f900000)), + (22, 0x3a9111, e(0x5fa00000)), + (22, 0x3a9112, e(0x5fb00000)), + (22, 0x3a9113, e(0x5fc00000)), + (22, 0x3a9114, e(0x5fd00000)), + (22, 0x3a9115, e(0x5fe00000)), + (22, 0x3a9116, e(0x5ff00000)), + (22, 0x3a9117, e(0x60000000)), + (22, 0x3a9118, e(0x60100000)), + (22, 0x3a9119, e(0x60200000)), + (22, 0x3a911a, e(0x60300000)), + (22, 0x3a911b, e(0x60400000)), + (22, 0x3a911c, e(0x60500000)), + (22, 0x3a911d, e(0x60600000)), + (22, 0x3a911e, e(0x60700000)), + (22, 0x3a911f, e(0x60800000)), + (22, 0x3a9120, e(0x60900000)), + (22, 0x3a9121, e(0x60a00000)), + (22, 0x3a9122, e(0x60b00000)), + (22, 0x3a9123, e(0x60c00000)), + (22, 0x3a9124, e(0x60d00000)), + (22, 0x3a9125, e(0x60e00000)), + (22, 0x3a9126, e(0x60f00000)), + (22, 0x3a9127, e(0x61000000)), + (22, 0x3a9128, e(0x61100000)), + (22, 0x3a9129, e(0x61200000)), + (22, 0x3a912a, e(0x61300000)), + (22, 0x3a912b, e(0x61400000)), + (22, 0x3a912c, e(0x61500000)), + (22, 0x3a912d, e(0x61600000)), + (22, 0x3a912e, e(0x61700000)), + (22, 0x3a912f, e(0x61800000)), + (22, 0x3a9130, e(0x61900000)), + (22, 0x3a9131, e(0x61a00000)), + (22, 0x3a9132, e(0x61b00000)), + (22, 0x3a9133, e(0x61c00000)), + (22, 0x3a9134, e(0x61d00000)), + (22, 0x3a9135, e(0x61e00000)), + (22, 0x3a9136, e(0x61f00000)), + (22, 0x3a9137, e(0x62000000)), + (22, 0x3a9138, e(0x62100000)), + (22, 0x3a9139, e(0x62200000)), + (22, 0x3a913a, e(0x62300000)), + (22, 0x3a913b, e(0x62400000)), + (22, 0x3a913c, e(0x62500000)), + (22, 0x3a913d, e(0x62600000)), + (22, 0x3a913e, e(0x62700000)), + (22, 0x3a913f, e(0x62800000)), + (22, 0x3a9140, e(0x62900000)), + (22, 0x3a9141, e(0x62a00000)), + (22, 0x3a9142, e(0x62b00000)), + (22, 0x3a9143, e(0x62c00000)), + (22, 0x3a9144, e(0x62d00000)), + (22, 0x3a9145, e(0x62e00000)), + (22, 0x3a9146, e(0x62f00000)), + (22, 0x3a9147, e(0x63000000)), + (22, 0x3a9148, e(0x63100000)), + (22, 0x3a9149, e(0x63200000)), + (22, 0x3a914a, e(0x63300000)), + (22, 0x3a914b, e(0x63400000)), + (22, 0x3a914c, e(0x63500000)), + (22, 0x3a914d, e(0x63600000)), + (22, 0x3a914e, e(0x63700000)), + (22, 0x3a914f, e(0x63800000)), + (22, 0x3a9150, e(0x63900000)), + (22, 0x3a9151, e(0x63a00000)), + (22, 0x3a9152, e(0x63b00000)), + (22, 0x3a9153, e(0x63c00000)), + (22, 0x3a9154, e(0x63d00000)), + (22, 0x3a9155, e(0x63e00000)), + (22, 0x3a9156, e(0x63f00000)), + (22, 0x3a9157, e(0x64000000)), + (22, 0x3a9158, e(0x64100000)), + (22, 0x3a9159, e(0x64200000)), + (22, 0x3a915a, e(0x64300000)), + (22, 0x3a915b, e(0x64400000)), + (22, 0x3a915c, e(0x64500000)), + (22, 0x3a915d, e(0x64600000)), + (22, 0x3a915e, e(0x64700000)), + (22, 0x3a915f, e(0x64800000)), + (22, 0x3a9160, e(0x64900000)), + (22, 0x3a9161, e(0x64a00000)), + (22, 0x3a9162, e(0x64b00000)), + (22, 0x3a9163, e(0x64c00000)), + (22, 0x3a9164, e(0x64d00000)), + (22, 0x3a9165, e(0x64e00000)), + (22, 0x3a9166, e(0x64f00000)), + (22, 0x3a9167, e(0x65000000)), + (22, 0x3a9168, e(0x65100000)), + (22, 0x3a9169, e(0x65200000)), + (22, 0x3a916a, e(0x65300000)), + (22, 0x3a916b, e(0x65400000)), + (22, 0x3a916c, e(0x65500000)), + (22, 0x3a916d, e(0x65600000)), + (22, 0x3a916e, e(0x65700000)), + (22, 0x3a916f, e(0x65800000)), + (22, 0x3a9170, e(0x65900000)), + (22, 0x3a9171, e(0x65a00000)), + (22, 0x3a9172, e(0x65b00000)), + (22, 0x3a9173, e(0x65c00000)), + (22, 0x3a9174, e(0x65d00000)), + (22, 0x3a9175, e(0x65e00000)), + (22, 0x3a9176, e(0x65f00000)), + (22, 0x3a9177, e(0x66000000)), + (22, 0x3a9178, e(0x66100000)), + (22, 0x3a9179, e(0x66200000)), + (22, 0x3a917a, e(0x66300000)), + (22, 0x3a917b, e(0x66400000)), + (22, 0x3a917c, e(0x66500000)), + (22, 0x3a917d, e(0x66600000)), + (22, 0x3a917e, e(0x66700000)), + (22, 0x3a917f, e(0x66800000)), + (22, 0x3a9180, e(0x66900000)), + (22, 0x3a9181, e(0x66a00000)), + (22, 0x3a9182, e(0x66b00000)), + (22, 0x3a9183, e(0x66c00000)), + (22, 0x3a9184, e(0x66d00000)), + (22, 0x3a9185, e(0x66e00000)), + (22, 0x3a9186, e(0x66f00000)), + (22, 0x3a9187, e(0x67000000)), + (22, 0x3a9188, e(0x67100000)), + (22, 0x3a9189, e(0x67200000)), + (22, 0x3a918a, e(0x67300000)), + (22, 0x3a918b, e(0x67400000)), + (22, 0x3a918c, e(0x67500000)), + (22, 0x3a918d, e(0x67600000)), + (22, 0x3a918e, e(0x67700000)), + (22, 0x3a918f, e(0x67800000)), + (22, 0x3a9190, e(0x67900000)), + (22, 0x3a9191, e(0x67a00000)), + (22, 0x3a9192, e(0x67b00000)), + (22, 0x3a9193, e(0x67c00000)), + (22, 0x3a9194, e(0x67d00000)), + (22, 0x3a9195, e(0x67e00000)), + (22, 0x3a9196, e(0x67f00000)), + (22, 0x3a9197, e(0x68000000)), + (22, 0x3a9198, e(0x68100000)), + (22, 0x3a9199, e(0x68200000)), + (22, 0x3a919a, e(0x68300000)), + (22, 0x3a919b, e(0x68400000)), + (22, 0x3a919c, e(0x68500000)), + (22, 0x3a919d, e(0x68600000)), + (22, 0x3a919e, e(0x68700000)), + (22, 0x3a919f, e(0x68800000)), + (22, 0x3a91a0, e(0x68900000)), + (22, 0x3a91a1, e(0x68a00000)), + (22, 0x3a91a2, e(0x68b00000)), + (22, 0x3a91a3, e(0x68c00000)), + (22, 0x3a91a4, e(0x68d00000)), + (22, 0x3a91a5, e(0x68e00000)), + (22, 0x3a91a6, e(0x68f00000)), + (22, 0x3a91a7, e(0x69000000)), + (22, 0x3a91a8, e(0x69100000)), + (22, 0x3a91a9, e(0x69200000)), + (22, 0x3a91aa, e(0x69300000)), + (22, 0x3a91ab, e(0x69400000)), + (22, 0x3a91ac, e(0x69500000)), + (22, 0x3a91ad, e(0x69600000)), + (22, 0x3a91ae, e(0x69700000)), + (22, 0x3a91af, e(0x69800000)), + (22, 0x3a91b0, e(0x69900000)), + (22, 0x3a91b1, e(0x69a00000)), + (22, 0x3a91b2, e(0x69b00000)), + (22, 0x3a91b3, e(0x69c00000)), + (22, 0x3a91b4, e(0x69d00000)), + (22, 0x3a91b5, e(0x69e00000)), + (22, 0x3a91b6, e(0x69f00000)), + (22, 0x3a91b7, e(0x6a000000)), + (22, 0x3a91b8, e(0x6a100000)), + (22, 0x3a91b9, e(0x6a200000)), + (22, 0x3a91ba, e(0x6a300000)), + (22, 0x3a91bb, e(0x6a400000)), + (22, 0x3a91bc, e(0x6a500000)), + (22, 0x3a91bd, e(0x6a600000)), + (22, 0x3a91be, e(0x6a700000)), + (22, 0x3a91bf, e(0x6a800000)), + (22, 0x3a91c0, e(0x6a900000)), + (22, 0x3a91c1, e(0x6aa00000)), + (22, 0x3a91c2, e(0x6ab00000)), + (22, 0x3a91c3, e(0x6ac00000)), + (22, 0x3a91c4, e(0x6ad00000)), + (22, 0x3a91c5, e(0x6ae00000)), + (22, 0x3a91c6, e(0x6af00000)), + (22, 0x3a91c7, e(0x6b000000)), + (22, 0x3a91c8, e(0x6b100000)), + (22, 0x3a91c9, e(0x6b200000)), + (22, 0x3a91ca, e(0x6b300000)), + (22, 0x3a91cb, e(0x6b400000)), + (22, 0x3a91cc, e(0x6b500000)), + (22, 0x3a91cd, e(0x6b600000)), + (22, 0x3a91ce, e(0x6b700000)), + (22, 0x3a91cf, e(0x6b800000)), + (22, 0x3a91d0, e(0x6b900000)), + (22, 0x3a91d1, e(0x6ba00000)), + (22, 0x3a91d2, e(0x6bb00000)), + (22, 0x3a91d3, e(0x6bc00000)), + (22, 0x3a91d4, e(0x6bd00000)), + (22, 0x3a91d5, e(0x6be00000)), + (22, 0x3a91d6, e(0x6bf00000)), + (22, 0x3a91d7, e(0x6c000000)), + (22, 0x3a91d8, e(0x6c100000)), + (22, 0x3a91d9, e(0x6c200000)), + (22, 0x3a91da, e(0x6c300000)), + (22, 0x3a91db, e(0x6c400000)), + (22, 0x3a91dc, e(0x6c500000)), + (22, 0x3a91dd, e(0x6c600000)), + (22, 0x3a91de, e(0x6c700000)), + (22, 0x3a91df, e(0x6c800000)), + (22, 0x3a91e0, e(0x6c900000)), + (22, 0x3a91e1, e(0x6ca00000)), + (22, 0x3a91e2, e(0x6cb00000)), + (22, 0x3a91e3, e(0x6cc00000)), + (22, 0x3a91e4, e(0x6cd00000)), + (22, 0x3a91e5, e(0x6ce00000)), + (22, 0x3a91e6, e(0x6cf00000)), + (22, 0x3a91e7, e(0x6d000000)), + (22, 0x3a91e8, e(0x6d100000)), + (22, 0x3a91e9, e(0x6d200000)), + (22, 0x3a91ea, e(0x6d300000)), + (22, 0x3a91eb, e(0x6d400000)), + (22, 0x3a91ec, e(0x6d500000)), + (22, 0x3a91ed, e(0x6d600000)), + (22, 0x3a91ee, e(0x6d700000)), + (22, 0x3a91ef, e(0x6d800000)), + (22, 0x3a91f0, e(0x6d900000)), + (22, 0x3a91f1, e(0x6da00000)), + (22, 0x3a91f2, e(0x6db00000)), + (22, 0x3a91f3, e(0x6dc00000)), + (22, 0x3a91f4, e(0x6dd00000)), + (22, 0x3a91f5, e(0x6de00000)), + (22, 0x3a91f6, e(0x6df00000)), + (22, 0x3a91f7, e(0x6e000000)), + (22, 0x3a91f8, e(0x6e100000)), + (22, 0x3a91f9, e(0x6e200000)), + (22, 0x3a91fa, e(0x6e300000)), + (22, 0x3a91fb, e(0x6e400000)), + (22, 0x3a91fc, e(0x6e500000)), + (22, 0x3a91fd, e(0x6e600000)), + (22, 0x3a91fe, e(0x6e700000)), + (22, 0x3a91ff, e(0x6e800000)), + (22, 0x3a9200, e(0x6e900000)), + (22, 0x3a9201, e(0x6ea00000)), + (22, 0x3a9202, e(0x6eb00000)), + (22, 0x3a9203, e(0x6ec00000)), + (22, 0x3a9204, e(0x6ed00000)), + (22, 0x3a9205, e(0x6ee00000)), + (22, 0x3a9206, e(0x6ef00000)), + (22, 0x3a9207, e(0x6f000000)), + (22, 0x3a9208, e(0x6f100000)), + (22, 0x3a9209, e(0x6f200000)), + (22, 0x3a920a, e(0x6f300000)), + (22, 0x3a920b, e(0x6f400000)), + (22, 0x3a920c, e(0x6f500000)), + (22, 0x3a920d, e(0x6f600000)), + (22, 0x3a920e, e(0x6f700000)), + (22, 0x3a920f, e(0x6f800000)), + (22, 0x3a9210, e(0x6f900000)), + (22, 0x3a9211, e(0x6fa00000)), + (22, 0x3a9212, e(0x6fb00000)), + (22, 0x3a9213, e(0x6fc00000)), + (22, 0x3a9214, e(0x6fd00000)), + (22, 0x3a9215, e(0x6fe00000)), + (22, 0x3a9216, e(0x6ff00000)), + (22, 0x3a9217, e(0x70000000)), + (22, 0x3a9218, e(0x70100000)), + (22, 0x3a9219, e(0x70200000)), + (22, 0x3a921a, e(0x70300000)), + (22, 0x3a921b, e(0x70400000)), + (22, 0x3a921c, e(0x70500000)), + (22, 0x3a921d, e(0x70600000)), + (22, 0x3a921e, e(0x70700000)), + (22, 0x3a921f, e(0x70800000)), + (22, 0x3a9220, e(0x70900000)), + (22, 0x3a9221, e(0x70a00000)), + (22, 0x3a9222, e(0x70b00000)), + (22, 0x3a9223, e(0x70c00000)), + (22, 0x3a9224, e(0x70d00000)), + (22, 0x3a9225, e(0x70e00000)), + (22, 0x3a9226, e(0x70f00000)), + (22, 0x3a9227, e(0x71000000)), + (22, 0x3a9228, e(0x71100000)), + (22, 0x3a9229, e(0x71200000)), + (22, 0x3a922a, e(0x71300000)), + (22, 0x3a922b, e(0x71400000)), + (22, 0x3a922c, e(0x71500000)), + (22, 0x3a922d, e(0x71600000)), + (22, 0x3a922e, e(0x71700000)), + (22, 0x3a922f, e(0x71800000)), + (22, 0x3a9230, e(0x71900000)), + (22, 0x3a9231, e(0x71a00000)), + (22, 0x3a9232, e(0x71b00000)), + (22, 0x3a9233, e(0x71c00000)), + (22, 0x3a9234, e(0x71d00000)), + (22, 0x3a9235, e(0x71e00000)), + (22, 0x3a9236, e(0x71f00000)), + (22, 0x3a9237, e(0x72000000)), + (22, 0x3a9238, e(0x72100000)), + (22, 0x3a9239, e(0x72200000)), + (22, 0x3a923a, e(0x72300000)), + (22, 0x3a923b, e(0x72400000)), + (22, 0x3a923c, e(0x72500000)), + (22, 0x3a923d, e(0x72600000)), + (22, 0x3a923e, e(0x72700000)), + (22, 0x3a923f, e(0x72800000)), + (22, 0x3a9240, e(0x72900000)), + (22, 0x3a9241, e(0x72a00000)), + (22, 0x3a9242, e(0x72b00000)), + (22, 0x3a9243, e(0x72c00000)), + (22, 0x3a9244, e(0x72d00000)), + (22, 0x3a9245, e(0x72e00000)), + (22, 0x3a9246, e(0x72f00000)), + (22, 0x3a9247, e(0x73000000)), + (22, 0x3a9248, e(0x73100000)), + (22, 0x3a9249, e(0x73200000)), + (22, 0x3a924a, e(0x73300000)), + (22, 0x3a924b, e(0x73400000)), + (22, 0x3a924c, e(0x73500000)), + (22, 0x3a924d, e(0x73600000)), + (22, 0x3a924e, e(0x73700000)), + (22, 0x3a924f, e(0x73800000)), + (22, 0x3a9250, e(0x73900000)), + (22, 0x3a9251, e(0x73a00000)), + (22, 0x3a9252, e(0x73b00000)), + (22, 0x3a9253, e(0x73c00000)), + (22, 0x3a9254, e(0x73d00000)), + (22, 0x3a9255, e(0x73e00000)), + (22, 0x3a9256, e(0x73f00000)), + (22, 0x3a9257, e(0x74000000)), + (22, 0x3a9258, e(0x74100000)), + (22, 0x3a9259, e(0x74200000)), + (22, 0x3a925a, e(0x74300000)), + (22, 0x3a925b, e(0x74400000)), + (22, 0x3a925c, e(0x74500000)), + (22, 0x3a925d, e(0x74600000)), + (22, 0x3a925e, e(0x74700000)), + (22, 0x3a925f, e(0x74800000)), + (22, 0x3a9260, e(0x74900000)), + (22, 0x3a9261, e(0x74a00000)), + (22, 0x3a9262, e(0x74b00000)), + (22, 0x3a9263, e(0x74c00000)), + (22, 0x3a9264, e(0x74d00000)), + (22, 0x3a9265, e(0x74e00000)), + (22, 0x3a9266, e(0x74f00000)), + (22, 0x3a9267, e(0x75000000)), + (22, 0x3a9268, e(0x75100000)), + (22, 0x3a9269, e(0x75200000)), + (22, 0x3a926a, e(0x75300000)), + (22, 0x3a926b, e(0x75400000)), + (22, 0x3a926c, e(0x75500000)), + (22, 0x3a926d, e(0x75600000)), + (22, 0x3a926e, e(0x75700000)), + (22, 0x3a926f, e(0x75800000)), + (22, 0x3a9270, e(0x75900000)), + (22, 0x3a9271, e(0x75a00000)), + (22, 0x3a9272, e(0x75b00000)), + (22, 0x3a9273, e(0x75c00000)), + (22, 0x3a9274, e(0x75d00000)), + (22, 0x3a9275, e(0x75e00000)), + (22, 0x3a9276, e(0x75f00000)), + (22, 0x3a9277, e(0x76000000)), + (22, 0x3a9278, e(0x76100000)), + (22, 0x3a9279, e(0x76200000)), + (22, 0x3a927a, e(0x76300000)), + (22, 0x3a927b, e(0x76400000)), + (22, 0x3a927c, e(0x76500000)), + (22, 0x3a927d, e(0x76600000)), + (22, 0x3a927e, e(0x76700000)), + (22, 0x3a927f, e(0x76800000)), + (22, 0x3a9280, e(0x76900000)), + (22, 0x3a9281, e(0x76a00000)), + (22, 0x3a9282, e(0x76b00000)), + (22, 0x3a9283, e(0x76c00000)), + (22, 0x3a9284, e(0x76d00000)), + (22, 0x3a9285, e(0x76e00000)), + (22, 0x3a9286, e(0x76f00000)), + (22, 0x3a9287, e(0x77000000)), + (22, 0x3a9288, e(0x77100000)), + (22, 0x3a9289, e(0x77200000)), + (22, 0x3a928a, e(0x77300000)), + (22, 0x3a928b, e(0x77400000)), + (22, 0x3a928c, e(0x77500000)), + (22, 0x3a928d, e(0x77600000)), + (22, 0x3a928e, e(0x77700000)), + (22, 0x3a928f, e(0x77800000)), + (22, 0x3a9290, e(0x77900000)), + (22, 0x3a9291, e(0x77a00000)), + (22, 0x3a9292, e(0x77b00000)), + (22, 0x3a9293, e(0x77c00000)), + (22, 0x3a9294, e(0x77d00000)), + (22, 0x3a9295, e(0x77e00000)), + (22, 0x3a9296, e(0x77f00000)), + (22, 0x3a9297, e(0x78000000)), + (22, 0x3a9298, e(0x78100000)), + (22, 0x3a9299, e(0x78200000)), + (22, 0x3a929a, e(0x78300000)), + (22, 0x3a929b, e(0x78400000)), + (22, 0x3a929c, e(0x78500000)), + (22, 0x3a929d, e(0x78600000)), + (22, 0x3a929e, e(0x78700000)), + (22, 0x3a929f, e(0x78800000)), + (22, 0x3a92a0, e(0x78900000)), + (22, 0x3a92a1, e(0x78a00000)), + (22, 0x3a92a2, e(0x78b00000)), + (22, 0x3a92a3, e(0x78c00000)), + (22, 0x3a92a4, e(0x78d00000)), + (22, 0x3a92a5, e(0x78e00000)), + (22, 0x3a92a6, e(0x78f00000)), + (22, 0x3a92a7, e(0x79000000)), + (22, 0x3a92a8, e(0x79100000)), + (22, 0x3a92a9, e(0x79200000)), + (22, 0x3a92aa, e(0x79300000)), + (22, 0x3a92ab, e(0x79400000)), + (22, 0x3a92ac, e(0x79500000)), + (22, 0x3a92ad, e(0x79600000)), + (22, 0x3a92ae, e(0x79700000)), + (22, 0x3a92af, e(0x79800000)), + (22, 0x3a92b0, e(0x79900000)), + (22, 0x3a92b1, e(0x79a00000)), + (22, 0x3a92b2, e(0x79b00000)), + (22, 0x3a92b3, e(0x79c00000)), + (22, 0x3a92b4, e(0x79d00000)), + (22, 0x3a92b5, e(0x79e00000)), + (22, 0x3a92b6, e(0x79f00000)), + (22, 0x3a92b7, e(0x7a000000)), + (22, 0x3a92b8, e(0x7a100000)), + (22, 0x3a92b9, e(0x7a200000)), + (22, 0x3a92ba, e(0x7a300000)), + (22, 0x3a92bb, e(0x7a400000)), + (22, 0x3a92bc, e(0x7a500000)), + (22, 0x3a92bd, e(0x7a600000)), + (22, 0x3a92be, e(0x7a700000)), + (22, 0x3a92bf, e(0x7a800000)), + (22, 0x3a92c0, e(0x7a900000)), + (22, 0x3a92c1, e(0x7aa00000)), + (22, 0x3a92c2, e(0x7ab00000)), + (22, 0x3a92c3, e(0x7ac00000)), + (22, 0x3a92c4, e(0x7ad00000)), + (22, 0x3a92c5, e(0x7ae00000)), + (22, 0x3a92c6, e(0x7af00000)), + (22, 0x3a92c7, e(0x7b000000)), + (22, 0x3a92c8, e(0x7b100000)), + (22, 0x3a92c9, e(0x7b200000)), + (22, 0x3a92ca, e(0x7b300000)), + (22, 0x3a92cb, e(0x7b400000)), + (22, 0x3a92cc, e(0x7b500000)), + (22, 0x3a92cd, e(0x7b600000)), + (22, 0x3a92ce, e(0x7b700000)), + (22, 0x3a92cf, e(0x7b800000)), + (22, 0x3a92d0, e(0x7b900000)), + (22, 0x3a92d1, e(0x7ba00000)), + (22, 0x3a92d2, e(0x7bb00000)), + (22, 0x3a92d3, e(0x7bc00000)), + (22, 0x3a92d4, e(0x7bd00000)), + (22, 0x3a92d5, e(0x7be00000)), + (22, 0x3a92d6, e(0x7bf00000)), + (22, 0x3a92d7, e(0x7c000000)), + (22, 0x3a92d8, e(0x7c100000)), + (22, 0x3a92d9, e(0x7c200000)), + (22, 0x3a92da, e(0x7c300000)), + (22, 0x3a92db, e(0x7c400000)), + (22, 0x3a92dc, e(0x7c500000)), + (22, 0x3a92dd, e(0x7c600000)), + (22, 0x3a92de, e(0x7c700000)), + (22, 0x3a92df, e(0x7c800000)), + (22, 0x3a92e0, e(0x7c900000)), + (22, 0x3a92e1, e(0x7ca00000)), + (22, 0x3a92e2, e(0x7cb00000)), + (22, 0x3a92e3, e(0x7cc00000)), + (22, 0x3a92e4, e(0x7cd00000)), + (22, 0x3a92e5, e(0x7ce00000)), + (22, 0x3a92e6, e(0x7cf00000)), + (22, 0x3a92e7, e(0x7d000000)), + (22, 0x3a92e8, e(0x7d100000)), + (22, 0x3a92e9, e(0x7d200000)), + (22, 0x3a92ea, e(0x7d300000)), + (22, 0x3a92eb, e(0x7d400000)), + (22, 0x3a92ec, e(0x7d500000)), + (22, 0x3a92ed, e(0x7d600000)), + (22, 0x3a92ee, e(0x7d700000)), + (22, 0x3a92ef, e(0x7d800000)), + (22, 0x3a92f0, e(0x7d900000)), + (22, 0x3a92f1, e(0x7da00000)), + (22, 0x3a92f2, e(0x7db00000)), + (22, 0x3a92f3, e(0x7dc00000)), + (22, 0x3a92f4, e(0x7dd00000)), + (22, 0x3a92f5, e(0x7de00000)), + (22, 0x3a92f6, e(0x7df00000)), + (22, 0x3a92f7, e(0x7e000000)), + (22, 0x3a92f8, e(0x7e100000)), + (22, 0x3a92f9, e(0x7e200000)), + (22, 0x3a92fa, e(0x7e300000)), + (22, 0x3a92fb, e(0x7e400000)), + (22, 0x3a92fc, e(0x7e500000)), + (22, 0x3a92fd, e(0x7e600000)), + (22, 0x3a92fe, e(0x7e700000)), + (22, 0x3a92ff, e(0x7e800000)), + (22, 0x3a95a0, e(0x7e900000)), + (22, 0x3a95a1, e(0x7ea00000)), + (22, 0x3a95a2, e(0x7eb00000)), + (22, 0x3a95a3, e(0x7ec00000)), + (22, 0x3a95a4, e(0x7ed00000)), + (22, 0x3a95a5, e(0x7ee00000)), + (22, 0x3a95a6, e(0x7ef00000)), + (22, 0x3a95a7, e(0x7f000000)), + (22, 0x3a95a8, e(0x7f100000)), + (22, 0x3a95a9, e(0x7f200000)), + (22, 0x3a95aa, e(0x7f300000)), + (22, 0x3a95ab, e(0x7f400000)), + (22, 0x3a95ac, e(0x7f500000)), + (22, 0x3a95ad, e(0x7f600000)), + (22, 0x3a95ae, e(0x7f700000)), + (22, 0x3a95af, e(0x7f800000)), + (22, 0x3b8fe0, e(0x7f900000)), + (22, 0x3b8fe1, e(0x7fa00000)), + (22, 0x3b8fe2, e(0x7fb00000)), + (22, 0x3b8fe3, e(0x7fc00000)), + (21, 0x68e90, e(0x7fd00000)), + (21, 0x68e91, e(0x7fe00000)), + (21, 0x68e98, e(0x7ff80000)), +]; diff --git a/crates/prc/src/arrays.rs b/crates/prc/src/arrays.rs new file mode 100644 index 0000000000..de13bde6eb --- /dev/null +++ b/crates/prc/src/arrays.rs @@ -0,0 +1,420 @@ +//! The array primitives of compressed tessellation [WD 11.6-11.9] and the Huffman payload +//! they may carry [WD 12.2]. +//! +//! The WD gives only the Huffman writer. The reader decodes what producers write: each +//! leaf's code length is read in the width the payload's 8-bit field states (the WD writes a +//! fixed 8 bits), stored codes carry a leading root bit, and the payload is read LSB-first +//! although the stream around it is MSB-first. + +use std::collections::HashMap; + +use crate::PrcError; +use crate::bits::BitReader; + +/// Longest Huffman code [WD 12.2]. +const MAX_CODE_BITS: u32 = 32; + +/// Widest Huffman symbol a caller asks for: the arrays hold 2- to 16-bit values. +const MAX_SYMBOL_BITS: u32 = 16; + +fn malformed(what: &str) -> PrcError { + PrcError::Malformed(what.to_owned()) +} + +/// An LSB-first cursor over a Huffman payload. +struct Lsb<'a> { + data: &'a [u8], + pos: usize, + end: usize, +} + +impl Lsb<'_> { + fn bit(&mut self) -> Result { + if self.pos >= self.end { + return Err(PrcError::Truncated("a Huffman payload")); + } + let byte = self.data.get(self.pos / 8).ok_or(PrcError::Truncated("a Huffman payload"))?; + let b = u32::from((byte >> (self.pos % 8)) & 1); + self.pos += 1; + Ok(b) + } + + /// `n` ≤ 32 bits, the first read being the least significant. + fn bits(&mut self, n: u32) -> Result { + let mut v = 0u32; + for i in 0..n.min(32) { + v |= self.bit()? << i; + } + Ok(v) + } + + fn remaining(&self) -> usize { + self.end.saturating_sub(self.pos) + } +} + +/// The low `n` (1..=16) bits of `v` as a symbol, two's complement when `signed`. +fn symbol(v: u32, n: u32, signed: bool) -> i32 { + let v = i64::from(v); + let wide = if signed && n > 0 && v & (1 << (n - 1)) != 0 { v - (1i64 << n) } else { v }; + // n <= 16, so `wide` is within ±2^16. + i32::try_from(wide).unwrap_or(0) +} + +/// The Huffman container of a compressed Character/Short array [WD 11.6, 11.7, 12.2]: +/// symbols are `nbits` wide, sign-extended when `signed`. +fn huffman(r: &mut BitReader<'_>, nbits: u32, signed: bool) -> Result, PrcError> { + if nbits == 0 || nbits > MAX_SYMBOL_BITS { + return Err(malformed("Huffman symbol width")); + } + let words = r.unsigned_integer()? as usize; + if words == 0 { + return Ok(Vec::new()); + } + if words.saturating_mul(32) > r.remaining() { + return Err(PrcError::Truncated("a Huffman payload")); + } + // words * 32 bits remain, so words * 4 bytes cannot overflow. + let blob = (0..words * 4).map(|_| r.character()).collect::, _>>()?; + let last = r.unsigned_integer()?; + if last > 32 { + return Err(malformed("Huffman last-word bit count above 32")); + } + let end = (words - 1) * 32 + last as usize; + let mut b = Lsb { data: &blob, pos: 0, end }; + let codes = huffman_leaves(&mut b, nbits, signed)?; + if codes.is_empty() { + return Ok(Vec::new()); + } + let count = b.bits(32)? as usize; + // Every element is at least one bit. + if count > b.remaining() { + return Err(PrcError::Truncated("a Huffman element count")); + } + r.take_elements(count)?; + let mut out = Vec::with_capacity(count.min(1024)); + for _ in 0..count { + out.push(huffman_symbol(&mut b, &codes)?); + } + Ok(out) +} + +/// The leaf table: `(code length, stored code)` to symbol. +fn huffman_leaves(b: &mut Lsb<'_>, nbits: u32, signed: bool) -> Result, PrcError> { + let leaves = b.bits(nbits + 1)?; + if leaves == 0 { + return Ok(HashMap::new()); + } + // prc-rs: the code-length width is read from the payload, not fixed at 8 bits. + let len_width = b.bits(8)?; + if len_width == 0 || len_width > MAX_CODE_BITS { + return Err(malformed("Huffman code-length width")); + } + // Every leaf is at least nbits + 2 bits, so the payload bounds the table. + if (leaves as usize).saturating_mul((nbits + 2) as usize) > b.remaining() { + return Err(PrcError::Truncated("a Huffman payload")); + } + let mut codes = HashMap::new(); + for _ in 0..leaves { + let s = symbol(b.bits(nbits)?, nbits, signed); + let len = b.bits(len_width)?; + if len == 0 || len > MAX_CODE_BITS { + return Err(malformed("Huffman code length")); + } + let code = b.bits(len)?; + if codes.insert((len, code), s).is_some() { + return Err(malformed("duplicate Huffman code")); + } + } + // A leaf whose code begins another's would end that code early, leaving its unread bits + // in front of the next element. + let shadowed = codes.keys().any(|&(len, code)| (1..len).any(|l| codes.contains_key(&(l, code >> (len - l))))); + if shadowed { + return Err(malformed("a Huffman code that is a prefix of another")); + } + Ok(codes) +} + +/// One element: code bits, most significant first, until a leaf matches. +fn huffman_symbol(b: &mut Lsb<'_>, codes: &HashMap<(u32, u32), i32>) -> Result { + // prc-rs: the stored code's first bit read is its most significant (the root bit). + let (mut code, mut len) = (0u32, 0u32); + while len < MAX_CODE_BITS { + code = (code << 1) | b.bit()?; + len += 1; + if let Some(&s) = codes.get(&(len, code)) { + return Ok(s); + } + } + Err(malformed("a Huffman code matching no leaf")) +} + +/// A `CharacterArray(nbits)` [WD 11.6]. `compressed` is `None` when the flag is written, +/// else the caller's implicit value. Uncompressed elements are whole bytes, sign-extended +/// from 8 bits when `signed`. +pub(crate) fn character_array(r: &mut BitReader<'_>, nbits: u32, compressed: Option, signed: bool) -> Result, PrcError> { + let compressed = match compressed { + Some(c) => c, + None => r.bit()?, + }; + if compressed { + return huffman(r, nbits, signed); + } + let n = r.unsigned_integer()? as usize; + if n.saturating_mul(8) > r.remaining() { + return Err(PrcError::Truncated("a character array")); + } + r.take_elements(n)?; + (0..n) + .map(|_| { + let c = r.character()?; + Ok(if signed { i32::from(c.cast_signed()) } else { i32::from(c) }) + }) + .collect() +} + +/// A `ShortArray(nbits)` [WD 11.7]: uncompressed elements are a low then a high byte, read +/// unsigned. +pub(crate) fn short_array(r: &mut BitReader<'_>, nbits: u32) -> Result, PrcError> { + if r.bit()? { + return huffman(r, nbits, false); + } + let n = r.unsigned_integer()? as usize; + if n.saturating_mul(16) > r.remaining() { + return Err(PrcError::Truncated("a short array")); + } + r.take_elements(n)?; + (0..n) + .map(|_| { + let lo = r.character()?; + let hi = r.character()?; + Ok(i32::from(u16::from_le_bytes([lo, hi]))) + }) + .collect() +} + +/// A `CompressedIntegerArray` [WD 11.8]: a CharacterArray(6) of widths, then one +/// `IntegerWithVariableBitNumber` per width. +pub(crate) fn compressed_integer_array(r: &mut BitReader<'_>) -> Result, PrcError> { + let widths = character_array(r, 6, None, false)?; + widths + .into_iter() + .map(|w| { + let w = u32::try_from(w).map_err(|_| malformed("integer width"))?; + r.integer_vbn(w) + }) + .collect() +} + +/// A `CompressedIndiceArray` [WD 11.9], read as producers write it: the stored signed 6-bit +/// values are width differences summed into each element's width, and each element is a +/// delta from the previous (the WD's own reading desyncs from the third element). Indices +/// are never negative. +pub(crate) fn compressed_indice_array(r: &mut BitReader<'_>, compressed: Option) -> Result, PrcError> { + let diffs = character_array(r, 6, compressed, true)?; + let mut out = Vec::with_capacity(diffs.len()); + let (mut width, mut prev) = (0i64, 0i64); + for d in diffs { + // prc-rs: widths are running sums of the stored differences, values running deltas. + width = width.checked_add(i64::from(d)).ok_or_else(|| malformed("indice width"))?; + let w = u32::try_from(width).map_err(|_| malformed("indice width"))?; + let v = prev.checked_add(r.integer_vbn(w)?).ok_or_else(|| malformed("indice delta"))?; + out.push(u32::try_from(v).map_err(|_| malformed("an index outside 0..2^32"))?); + prev = v; + } + Ok(out) +} + +/// An `UncompressedBoolArray` of `n` raw bits (no length prefix). +pub(crate) fn bool_array(r: &mut BitReader<'_>, n: usize) -> Result, PrcError> { + if n > r.remaining() { + return Err(PrcError::Truncated("a boolean array")); + } + r.take_elements(n)?; + (0..n).map(|_| r.bit()).collect() +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use super::*; + use crate::testw::W; + + fn read(w: &W, f: impl FnOnce(&mut BitReader<'_>) -> Result) -> T { + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + let v = f(&mut r).unwrap(); + assert_eq!(r.position(), w.len(), "bits consumed"); + v + } + + #[test] + fn huffman_decodes_the_wd_example_with_root_bits() { + // WD 12.2's input {1,3,5,7,7,9,5,3,3}; codes carry the root bit. + let leaves = [(3, 0b100, 3), (7, 0b101, 3), (5, 0b110, 3), (1, 0b1110, 4), (9, 0b1111, 4)]; + let values = [1, 3, 5, 7, 7, 9, 5, 3, 3]; + let mut w = W::default(); + w.bit(true).huffman(4, 3, &leaves, &values); + assert_eq!(read(&w, |r| character_array(r, 4, None, false)), values); + } + + #[test] + fn huffman_symbols_sign_extend_only_when_signed() { + let leaves = [(0b111110, 0b10, 2), (1, 0b11, 2)]; + let mut w = W::default(); + w.huffman(6, 2, &leaves, &[62, 1, 62]); + assert_eq!(read(&w, |r| huffman(r, 6, true)), [-2, 1, -2]); + assert_eq!(read(&w, |r| huffman(r, 6, false)), [62, 1, 62]); + } + + #[test] + fn huffman_rejects_codes_that_match_no_leaf_and_empty_payloads_are_empty() { + let mut bad = W::default(); + bad.huffman_raw(2, 2, &[(1, 0b10, 2)], &[(0b11, 2)]); + let bytes = bad.bytes(); + assert!(matches!(huffman(&mut BitReader::new(&bytes), 2, false), Err(PrcError::Malformed(_) | PrcError::Truncated(_)))); + let mut empty = W::default(); + empty.uint(0); + assert!(read(&empty, |r| huffman(r, 2, false)).is_empty()); + } + + #[test] + fn a_huffman_element_count_past_the_payload_is_truncated_not_allocated() { + // One leaf, then an element count of 2^32 - 1 with no elements behind it. + let mut w = W::default(); + w.huffman_count(2, 2, &[(1, 0b10, 2)], u32::MAX); + let bytes = w.bytes(); + assert!(matches!(huffman(&mut BitReader::new(&bytes), 2, false), Err(PrcError::Truncated("a Huffman element count")))); + } + + #[test] + fn a_huffman_code_that_begins_another_is_malformed() { + // `10` would end every `100` one bit early. + let mut w = W::default(); + w.huffman(2, 2, &[(1, 0b10, 2), (2, 0b100, 3)], &[2, 1]); + let bytes = w.bytes(); + let r = huffman(&mut BitReader::new(&bytes), 2, false); + assert!(malformed_by(r, "a Huffman code that is a prefix of another")); + } + + #[test] + fn huffman_widths_out_of_range_are_errors() { + // Well-formed payloads at each width, which only the width check refuses. + for nbits in [0, 17] { + let mut w = W::default(); + w.huffman(nbits, 2, &[(1, 0b10, 2)], &[1]); + let bytes = w.bytes(); + assert!(matches!(huffman(&mut BitReader::new(&bytes), nbits, false), Err(PrcError::Malformed(_))), "{nbits}"); + } + } + + #[test] + fn uncompressed_arrays() { + let mut w = W::default(); + w.bit(false).uint(2).put(0xfe, 8).put(3, 8); + assert_eq!(read(&w, |r| character_array(r, 6, None, true)), [-2, 3]); + let mut s = W::default(); + s.bit(false).uint(1).put(0x34, 8).put(0x12, 8); + assert_eq!(read(&s, |r| short_array(r, 16)), [0x1234]); + } + + #[test] + fn integer_array_reads_one_width_per_value() { + let mut w = W::default(); + w.bit(false).uint(3).put(3, 8).put(0, 8).put(4, 8); + w.bit(true).put(3, 2); // -3 in 3 bits + w.bit(false).put(5, 3); // 5 in 4 bits + assert_eq!(read(&w, compressed_integer_array), [-3, 0, 5]); + } + + #[test] + fn indice_array_sums_widths_and_deltas() { + // Values 5, 6, 4: widths 4, 2, 3 stored as diffs 4, -2, +1. + let mut w = W::default(); + w.bit(false).uint(3).put(4, 8).put(0xfe, 8).put(1, 8); + w.bit(false).put(5, 3); // 5 + w.bit(false).put(1, 1); // +1 + w.bit(true).put(2, 2); // -2 + assert_eq!(read(&w, |r| compressed_indice_array(r, None)), [5, 6, 4]); + // The implicit-flag form reads no flag bit. + let mut i = W::default(); + i.uint(1).put(2, 8).bit(false).put(1, 1); + assert_eq!(read(&i, |r| compressed_indice_array(r, Some(false))), [1]); + // A negative running index is damage. + let mut n = W::default(); + n.bit(false).uint(1).put(2, 8).bit(true).put(1, 1); + let bytes = n.bytes(); + assert!(compressed_indice_array(&mut BitReader::new(&bytes), None).is_err()); + } + + #[test] + fn a_bool_array_longer_than_the_data_is_truncated() { + assert!(matches!(bool_array(&mut BitReader::new(&[0]), 9), Err(PrcError::Truncated(_)))); + assert_eq!(bool_array(&mut BitReader::new(&[0x80]), 2).unwrap(), [true, false]); + } + + #[test] + fn decoded_elements_are_charged_against_one_allowance() { + // One bit can decode to a 4-byte element, so the byte cap alone does not bound memory. + let leaves = [(0b111110, 0b10, 2), (1, 0b11, 2)]; + let mut w = W::default(); + w.huffman(6, 2, &leaves, &[62, 1, 62]); + let bytes = w.bytes(); + let mut r = BitReader::with_elements(&bytes, 2); + assert!(matches!(huffman(&mut r, 6, false), Err(PrcError::TooManyElements { .. }))); + let mut r = BitReader::with_elements(&bytes, 4); + assert_eq!(huffman(&mut r, 6, false).unwrap(), [62, 1, 62]); + assert_eq!(r.elements_left(), 1); + assert!(matches!(bool_array(&mut BitReader::with_elements(&[0], 1), 2), Err(PrcError::TooManyElements { .. }))); + } + + /// Uncompressed character and short arrays are charged against the allowance too, and a + /// character count past the data is refused by name before anything is read. + #[test] + fn uncompressed_arrays_are_charged_and_their_counts_checked() { + let mut w = W::default(); + w.uint(3).put(0, 8).put(1, 8).put(2, 8); + let chars = w.bytes(); + let read = |n| character_array(&mut BitReader::with_elements(&chars, n), 8, Some(false), false); + assert!(matches!(read(2), Err(PrcError::TooManyElements { .. }))); + assert_eq!(read(3).unwrap(), [0, 1, 2]); + let mut w = W::default(); + w.bit(false).uint(2).put(1, 8).put(0, 8).put(2, 8).put(0, 8); + let shorts = w.bytes(); + assert!(matches!(short_array(&mut BitReader::with_elements(&shorts, 1), 8), Err(PrcError::TooManyElements { .. }))); + assert_eq!(short_array(&mut BitReader::with_elements(&shorts, 2), 8).unwrap(), [1, 2]); + let mut w = W::default(); + w.uint(1000).put(7, 8); + let short_of_data = w.bytes(); + let res = character_array(&mut BitReader::new(&short_of_data), 8, Some(false), false); + assert!(matches!(res, Err(PrcError::Truncated("a character array"))), "{res:?}"); + } + + fn malformed_by(res: Result, PrcError>, what: &str) -> bool { + matches!(res, Err(PrcError::Malformed(m)) if m == what) + } + + #[test] + fn a_zero_code_length_width_or_length_is_refused_by_name() { + let mut w = W::default(); + w.huffman_raw(2, 0, &[(1, 0, 0)], &[]); + let bytes = w.bytes(); + assert!(malformed_by(huffman(&mut BitReader::new(&bytes), 2, false), "Huffman code-length width")); + let mut w = W::default(); + w.huffman_raw(2, 2, &[(1, 0, 0)], &[]); + let bytes = w.bytes(); + assert!(malformed_by(huffman(&mut BitReader::new(&bytes), 2, false), "Huffman code length")); + } + + #[test] + fn a_boolean_array_past_the_data_is_refused_before_it_is_read() { + let mut w = W::default(); + w.put(0b1011, 4); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + let n = r.remaining() + 1; + assert_eq!(bool_array(&mut r, n), Err(PrcError::Truncated("a boolean array"))); + assert_eq!(r.position(), 0, "nothing read"); + } +} diff --git a/crates/prc/src/bits.rs b/crates/prc/src/bits.rs new file mode 100644 index 0000000000..837d281f3b --- /dev/null +++ b/crates/prc/src/bits.rs @@ -0,0 +1,491 @@ +//! The bit-level reader for PRC compressed sections [WD 10.3, 11.1-11.17]. +//! +//! Values are consumed MSB-first from each inflated byte and straddle byte boundaries freely +//! [WD 10.3.1, 11.3]. Multi-byte integers are groups of eight bits, low group first. Two sign +//! conventions coexist: `Integer` is two's complement, the `*WithVariableBitNumber` types are +//! sign-magnitude. + +use crate::PrcError; +use crate::acof::{self, Entry}; + +/// The most array elements the readers of one PRC stream decode together (2^24): at most 8 +/// bytes each, so 128 MiB. +pub const MAX_DECODED_ELEMENTS: usize = 1 << 24; + +/// The most schema-program steps (tokens executed, loop iterations included) the readers of +/// one PRC stream run together. +pub const MAX_SCHEMA_STEPS: u64 = 1 << 22; + +/// What the readers of one stream may still spend, carried from each reader to the next. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Budget { + /// Array elements still to decode. + pub(crate) elements: usize, + /// Schema-program steps still to run. + pub(crate) steps: u64, +} + +impl Budget { + /// A whole stream's allowance. + pub(crate) const FULL: Self = Self { elements: MAX_DECODED_ELEMENTS, steps: MAX_SCHEMA_STEPS }; +} + +/// A cursor over one inflated section. +#[derive(Debug, Clone)] +pub struct BitReader<'a> { + data: &'a [u8], + pos: usize, + budget: Budget, +} + +impl<'a> BitReader<'a> { + /// A reader positioned at the first bit of `data`, allowed [`MAX_DECODED_ELEMENTS`] and + /// [`MAX_SCHEMA_STEPS`]. + pub fn new(data: &'a [u8]) -> Self { + Self::with_budget(data, Budget::FULL) + } + + /// A reader of `data` allowed `budget`: what an earlier reader of the same stream left. + pub(crate) fn with_budget(data: &'a [u8], budget: Budget) -> Self { + Self { data, pos: 0, budget } + } + + /// A reader of `data` allowed `elements` array elements and a full step allowance. + #[cfg(test)] + pub(crate) fn with_elements(data: &'a [u8], elements: usize) -> Self { + Self::with_budget(data, Budget { elements, ..Budget::FULL }) + } + + /// This reader, at its position, allowed `budget` instead: what readers of the same + /// stream left since it stopped. + pub(crate) fn with_remaining(mut self, budget: Budget) -> Self { + self.budget = budget; + self + } + + /// What this reader may still spend. + pub(crate) fn budget(&self) -> Budget { + self.budget + } + + /// Array elements this reader may still decode. + #[cfg(test)] + pub(crate) fn elements_left(&self) -> usize { + self.budget.elements + } + + /// Charge `n` decoded array elements against the allowance. + /// + /// # Errors + /// [`PrcError::TooManyElements`] when fewer than `n` are left. + pub(crate) fn take_elements(&mut self, n: usize) -> Result<(), PrcError> { + self.budget.elements = self.budget.elements.checked_sub(n).ok_or(PrcError::TooManyElements { limit: MAX_DECODED_ELEMENTS })?; + Ok(()) + } + + /// Charge one schema-program step; false when none is left. + pub(crate) fn take_step(&mut self) -> bool { + match self.budget.steps.checked_sub(1) { + Some(left) => { + self.budget.steps = left; + true + } + None => false, + } + } + + /// Bits consumed so far. + pub fn position(&self) -> usize { + self.pos + } + + /// Bits left before the end of the data. + pub fn remaining(&self) -> usize { + self.data.len().saturating_mul(8).saturating_sub(self.pos) + } + + /// Step over `n` bits of opaque data (`UserData` [WD 8.6]). + /// + /// # Errors + /// [`PrcError::Truncated`] when fewer than `n` bits remain; the cursor does not move. + pub fn skip_bits(&mut self, n: usize) -> Result<(), PrcError> { + if n > self.remaining() { + return Err(PrcError::Truncated("user data")); + } + self.pos = self.pos.saturating_add(n); + Ok(()) + } + + /// One bit; a PRC `Boolean` [WD 10.3.3]. + /// + /// # Errors + /// [`PrcError::Truncated`] at the end of the data. + pub fn bit(&mut self) -> Result { + let byte = self.data.get(self.pos / 8).ok_or(PrcError::Truncated("a bit"))?; + let b = (byte >> (7 - (self.pos % 8))) & 1 == 1; + // `pos / 8` indexed the data, so `pos + 1` is at most `8 * len`. + self.pos = self.pos.saturating_add(1); + Ok(b) + } + + /// `n` bits (at most 32), MSB-first; `UnsignedIntegerWithVariableBitNumber` [WD 11.13]. + /// + /// # Errors + /// [`PrcError::Truncated`] at the end of the data; [`PrcError::Malformed`] for `n > 32`. + pub fn bits(&mut self, n: u32) -> Result { + if n > 32 { + return Err(PrcError::Malformed(format!("{n}-bit field"))); + } + if self.remaining() < n as usize { + return Err(PrcError::Truncated("a bit field")); + } + let mut v: u64 = 0; + for _ in 0..n { + v = (v << 1) | u64::from(self.bit()?); + } + // n <= 32, so v fits. + Ok(u32::try_from(v).unwrap_or(u32::MAX)) + } + + fn byte(&mut self) -> Result { + Ok(u8::try_from(self.bits(8)?).unwrap_or(u8::MAX)) + } + + /// A `Character`: one byte [WD 10.3.4]. + /// + /// # Errors + /// [`PrcError::Truncated`]. + pub fn character(&mut self) -> Result { + self.byte() + } + + /// An `UnsignedInteger` [WD 11.10]: while a 1 bit precedes it, one more eight-bit group, + /// low group first. + /// + /// # Errors + /// [`PrcError::Truncated`]; [`PrcError::Malformed`] past four groups. + pub fn unsigned_integer(&mut self) -> Result { + let mut v: u32 = 0; + let mut shift = 0; + while self.bit()? { + if shift >= 32 { + return Err(PrcError::Malformed("UnsignedInteger over 32 bits".into())); + } + v |= u32::from(self.byte()?) << shift; + shift += 8; + } + Ok(v) + } + + /// An `Integer` [WD 11.11]: a 0 bit is zero; otherwise eight-bit groups, low group first, + /// each followed by a continue bit, sign-extended from the last group's top bit. + /// + /// # Errors + /// [`PrcError::Truncated`]; [`PrcError::Malformed`] past four groups. + pub fn integer(&mut self) -> Result { + if !self.bit()? { + return Ok(0); + } + let mut v: u32 = 0; + let mut shift = 0; + loop { + if shift >= 32 { + return Err(PrcError::Malformed("Integer over 32 bits".into())); + } + let group = self.byte()?; + v |= u32::from(group) << shift; + shift += 8; + if !self.bit()? { + if group & 0x80 != 0 && shift < 32 { + v |= u32::MAX << shift; + } + return Ok(v.cast_signed()); + } + } + } + + /// An `IntegerWithVariableBitNumber(n)` [WD 11.12]: a negative flag (1 = negative; the WD + /// prose says the opposite, its sample code and real files agree on this), then the + /// magnitude in `n - 1` bits. + /// + /// # Errors + /// [`PrcError::Truncated`]; [`PrcError::Malformed`] for `n > 33`. + pub fn integer_vbn(&mut self, n: u32) -> Result { + let Some(magnitude_bits) = n.checked_sub(1) else { + return Ok(0); + }; + let negative = self.bit()?; + let m = i64::from(self.bits(magnitude_bits)?); + Ok(if negative { -m } else { m }) + } + + /// A `DoubleWithVariableBitNumber(tolerance, n)` [WD 11.14]: a sign-magnitude quantum + /// count times `tolerance`. + /// + /// # Errors + /// As [`Self::integer_vbn`]. + pub fn double_vbn(&mut self, tolerance: f64, n: u32) -> Result { + Ok(self.integer_vbn(n)? as f64 * tolerance) + } + + /// `NumberOfBitsThenUnsignedInteger` [WD 11.15]: a 5-bit width, then the value in that + /// many bits. + /// + /// # Errors + /// [`PrcError::Truncated`]. + pub fn nbits_then_unsigned(&mut self) -> Result { + let n = self.bits(5)?; + self.bits(n) + } + + /// A `String` [WD 10.3.7, 11.4]: `None` for the null string, else UTF-8 (invalid + /// sequences replaced). + /// + /// # Errors + /// [`PrcError::Truncated`] when the byte count runs past the data. + pub fn string(&mut self) -> Result, PrcError> { + if !self.bit()? { + return Ok(None); + } + let len = self.unsigned_integer()? as usize; + if len.saturating_mul(8) > self.remaining() { + return Err(PrcError::Truncated("a String")); + } + let bytes = (0..len).map(|_| self.byte()).collect::, _>>()?; + Ok(Some(String::from_utf8_lossy(&bytes).into_owned())) + } + + /// `FloatAsBytes` [WD 11.5]: an IEEE binary32, least-significant byte first. + /// + /// # Errors + /// [`PrcError::Truncated`]. + pub fn float_as_bytes(&mut self) -> Result { + let b = [self.byte()?, self.byte()?, self.byte()?, self.byte()?]; + Ok(f32::from_le_bytes(b)) + } + + /// A `Double` [WD 11.17]: a prefix code from the Double table, then a sign bit and, for + /// an exponent code, a byte-wise mantissa with back-references. + /// + /// The two-bit zero code has no sign bit. Back-reference offsets 1 to 5 are all accepted, + /// distance 1 included (PDF Association pdf-issues #769). + /// + /// # Errors + /// [`PrcError::UnknownDoubleCode`], [`PrcError::Truncated`], or [`PrcError::Malformed`] + /// for a back-reference outside the value. + pub fn double(&mut self) -> Result { + // prc-rs: the decoding steps (code lookup, sign, exponent, byte-wise mantissa with + // back-references) follow prc-rs's. + let mut code = 0u32; + let mut entry = None; + for n in 1..=acof::MAX_CODE_BITS { + code = (code << 1) | u32::from(self.bit()?); + if let Some(e) = acof::lookup(n, code) { + if n == 2 && code == 0b01 { + return Ok(0.0); + } + entry = Some(e); + break; + } + } + let entry = entry.ok_or(PrcError::UnknownDoubleCode)?; + let negative = self.bit()?; + let upper = match entry { + Entry::Double(v) => return Ok(if negative { -v } else { v }), + Entry::Exponent(upper) => upper, + }; + let mut b = (u64::from(upper) << 32).to_le_bytes(); + if self.bit()? { + self.mantissa(&mut b)?; + } + let sign = if negative { 1u64 << 63 } else { 0 }; + Ok(f64::from_bits(u64::from_le_bytes(b) | sign)) + } + + /// Mantissa bits 51..0 into `b` (little-endian byte order): a nibble, then bytes 5 down + /// to 0, each a literal or a back-reference to a byte already set. + fn mantissa(&mut self, b: &mut [u8; 8]) -> Result<(), PrcError> { + let nibble = u8::try_from(self.bits(4)?).unwrap_or(0); + let [.., b6, _] = b; + *b6 |= nibble; + let at = |b: &[u8; 8], i: usize| b.get(i).copied().ok_or_else(|| PrcError::Malformed("Double back-reference past the value".into())); + for cbi in (0..=5usize).rev() { + let prev = at(b, cbi + 1)?; + let value = if self.bit()? { + self.byte()? + } else { + match self.bits(3)? { + // Fill this byte and every lower one with the previous byte. + 0 => { + b.iter_mut().take(cbi + 1).for_each(|x| *x = prev); + return Ok(()); + } + // As 0, but the lowest byte is a literal. + 6 => { + b.iter_mut().take(cbi + 1).skip(1).for_each(|x| *x = prev); + let low = self.byte()?; + let [b0, ..] = b; + *b0 = low; + return Ok(()); + } + off @ 1..=5 => at(b, cbi + off as usize)?, + _ => return Err(PrcError::Malformed("Double mantissa offset 7".into())), + } + }; + if let Some(x) = b.get_mut(cbi) { + *x = value; + } + } + Ok(()) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] +mod tests { + use super::*; + use crate::testw::W; + + /// The code for IEEE exponent `e`. + fn exponent_code(e: u32) -> (u32, u32) { + acof::exponent_code(e).expect("the table has a row per exponent") + } + + fn read_double(w: &W) -> f64 { + BitReader::new(&w.bytes()).double().unwrap() + } + + #[test] + fn zero_is_the_two_bit_code_with_no_sign() { + let mut w = W::default(); + w.put(0b01, 2).put(0b1111_1111, 8); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + assert_eq!(r.double().unwrap(), 0.0); + assert_eq!(r.position(), 2); + } + + #[test] + fn exponent_with_literal_mantissa() { + let v = -1234.5678f64; + let le = v.to_bits().to_le_bytes(); + let e = u32::try_from((v.to_bits() >> 52) & 0x7ff).unwrap(); + let (n, c) = exponent_code(e); + let mut w = W::default(); + w.put(u64::from(c), n).put(1, 1).put(1, 1).put(u64::from(le[6] & 0x0f), 4); + for byte in le[..6].iter().rev() { + w.put(1, 1).put(u64::from(*byte), 8); + } + assert_eq!(read_double(&w), v); + } + + #[test] + fn exponent_without_mantissa_is_a_power_of_two() { + let (n, c) = exponent_code(1023 + 3); + let mut w = W::default(); + w.put(u64::from(c), n).put(0, 1).put(0, 1); + assert_eq!(read_double(&w), 8.0); + } + + #[test] + fn fill_back_reference_and_literal_low_byte() { + // Mantissa bytes 6..0 = 0x05, 0xAA, 0xAA (offset 1), 0xAA (offset 2), fill 0xAA to byte 1, byte 0 = 0x11. + let (n, c) = exponent_code(1023); + let mut w = W::default(); + w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0x5, 4); + w.put(1, 1).put(0xAA, 8); + w.put(0, 1).put(1, 3); + w.put(0, 1).put(2, 3); + w.put(0, 1).put(6, 3).put(0x11, 8); + assert_eq!(read_double(&w), f64::from_le_bytes([0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xF5, 0x3F])); + + // Byte 5 literal, then fill 4..0. + let mut w = W::default(); + w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0x5, 4); + w.put(1, 1).put(0x77, 8).put(0, 1).put(0, 3); + assert_eq!(read_double(&w), f64::from_le_bytes([0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0xF5, 0x3F])); + } + + #[test] + fn a_back_reference_past_the_value_is_refused() { + let (n, c) = exponent_code(1023); + let mut w = W::default(); + w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0, 4); + w.put(0, 1).put(5, 3).put(0, 16); // byte 5 + offset 5 = byte 10 + assert!(matches!(BitReader::new(&w.bytes()).double(), Err(PrcError::Malformed(_)))); + } + + #[test] + fn a_truncated_double_is_an_error_not_a_panic() { + // No code is 8 bits or shorter and a prefix of 0x17, so one byte ends inside a code. + assert!(matches!(BitReader::new(&[0x17]).double(), Err(PrcError::Truncated(_)))); + assert!(matches!(BitReader::new(&[]).double(), Err(PrcError::Truncated(_)))); + } + + #[test] + fn integers_and_strings() { + let mut w = W::default(); + // UnsignedInteger 300 = groups 0x2C, 0x01. + w.put(1, 1).put(0x2C, 8).put(1, 1).put(0x01, 8).put(0, 1); + // Integer -2 = group 0xFE, stop. + w.put(1, 1).put(0xFE, 8).put(0, 1); + // Integer 200 = 0xC8 then 0x00 (top bit clear), stop. + w.put(1, 1).put(0xC8, 8).put(1, 1).put(0x00, 8).put(0, 1); + // IntegerWithVariableBitNumber(4) = -5. + w.put(1, 1).put(5, 3); + // String "hé". + w.put(1, 1).put(1, 1).put(3, 8).put(0, 1).put(0x68, 8).put(0xC3, 8).put(0xA9, 8); + // Null string. + w.put(0, 1); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + assert_eq!(r.unsigned_integer().unwrap(), 300); + assert_eq!(r.integer().unwrap(), -2); + assert_eq!(r.integer().unwrap(), 200); + assert_eq!(r.integer_vbn(4).unwrap(), -5); + assert_eq!(r.string().unwrap().as_deref(), Some("hé")); + assert_eq!(r.string().unwrap(), None); + } + + #[test] + fn the_writer_round_trips_through_the_reader() { + let mut w = W::default(); + w.uint(0).uint(u32::MAX).int(-129).int(i32::MIN).int(i32::MAX).string(Some("glass")).string(None); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + assert_eq!(r.unsigned_integer().unwrap(), 0); + assert_eq!(r.unsigned_integer().unwrap(), u32::MAX); + assert_eq!(r.integer().unwrap(), -129); + assert_eq!(r.integer().unwrap(), i32::MIN); + assert_eq!(r.integer().unwrap(), i32::MAX); + assert_eq!(r.string().unwrap().as_deref(), Some("glass")); + assert_eq!(r.string().unwrap(), None); + } + + #[test] + fn oversized_groups_and_widths_are_malformed() { + // Five UnsignedInteger groups. + let mut w = W::default(); + (0..5).for_each(|_| { + w.put(1, 1).put(0xFF, 8); + }); + w.put(0, 1); + assert!(matches!(BitReader::new(&w.bytes()).unsigned_integer(), Err(PrcError::Malformed(_)))); + assert!(matches!(BitReader::new(&[0; 8]).bits(33), Err(PrcError::Malformed(_)))); + assert!(matches!(BitReader::new(&[0; 8]).integer_vbn(34), Err(PrcError::Malformed(_)))); + assert_eq!(BitReader::new(&[]).integer_vbn(0), Ok(0)); + } + + #[test] + fn truncation_is_an_error_not_a_panic() { + let mut r = BitReader::new(&[0xFF]); + assert!(r.unsigned_integer().is_err()); + let mut r = BitReader::new(&[0xC0, 0x40]); + assert!(matches!(r.string(), Err(PrcError::Truncated(_)))); + let mut r = BitReader::new(&[0xAB]); + assert!(matches!(r.skip_bits(9), Err(PrcError::Truncated(_)))); + assert_eq!(r.position(), 0, "a refused skip does not move the cursor"); + r.skip_bits(8).unwrap(); + assert_eq!(r.remaining(), 0); + } +} diff --git a/crates/prc/src/container.rs b/crates/prc/src/container.rs new file mode 100644 index 0000000000..3ff18508f1 --- /dev/null +++ b/crates/prc/src/container.rs @@ -0,0 +1,340 @@ +//! The PRC container: file header, file-structure descriptions, section extents and +//! inflation [WD 6.1-6.2, 10.2]. +//! +//! Section extents are not stored: each section runs to the next one's start offset, and the +//! last one to the model file's start. Inflation stops at the zlib stream's end, so padding +//! between sections is tolerated. + +use std::io::Read; + +use crate::PrcError; +use crate::bits::BitReader; +use crate::schema::Schema; + +/// The PRC version this reader implements. +pub const PRC_READER_VERSION: u32 = 8137; + +/// The most file structures [`PrcFile::parse`] accepts. +pub const MAX_FILE_STRUCTURES: usize = 65_536; + +/// Ceiling on the total inflated size of every section in one PRC stream, together with the +/// picture and uncompressed blocks it copies. +pub const MAX_INFLATED_BYTES: usize = 512 * 1024 * 1024; + +/// Sections per file structure [WD 6.1.2]: its header, then the five in [`SectionKind`] +/// order. +const SECTIONS: usize = 6; + +/// A 128-bit PRC identifier [WD 10.2.4]: four little-endian `u32`s. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct UniqueId(pub [u32; 4]); + +/// The compressed sections of one file structure, in file order [WD 6.1.2, 6.2.1]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SectionKind { + /// Starts with the file structure's schema, then its globals entity. + Globals, + /// The product-structure tree. + Tree, + /// Tessellated (mesh) data. + Tessellation, + /// Exact (B-rep) geometry. + Geometry, + /// Supplementary geometry. + ExtraGeometry, +} + +impl SectionKind { + /// Every kind, in file order. + pub const ALL: [SectionKind; 5] = + [SectionKind::Globals, SectionKind::Tree, SectionKind::Tessellation, SectionKind::Geometry, SectionKind::ExtraGeometry]; + + /// The section's name, as used in errors. + pub fn name(self) -> &'static str { + match self { + SectionKind::Globals => "globals", + SectionKind::Tree => "tree", + SectionKind::Tessellation => "tessellation", + SectionKind::Geometry => "geometry", + SectionKind::ExtraGeometry => "extra geometry", + } + } + + fn index(self) -> usize { + match self { + SectionKind::Globals => 0, + SectionKind::Tree => 1, + SectionKind::Tessellation => 2, + SectionKind::Geometry => 3, + SectionKind::ExtraGeometry => 4, + } + } +} + +/// The uncompressed file header [WD 6.1.1]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct PrcHeader { + /// The oldest reader version the producer says can read the file. + pub min_version_for_read: u32, + /// The producer's PRC version. + pub authoring_version: u32, + /// The file's identifier. + pub file_id: UniqueId, + /// The producing application's identifier (all zero = unregistered). + pub application_id: UniqueId, +} + +/// One file structure: its uncompressed header fields and its five inflated sections +/// [WD 6.2]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct FileStructure { + /// The identifier; equals the file header's description of it. + pub id: UniqueId, + /// Per-structure reader version floor; may differ from the file's. + pub min_version_for_read: u32, + /// Per-structure producer version. + pub authoring_version: u32, + /// Picture payloads (JPEG, PNG, ...) carried uncompressed in the header, referenced by + /// index from picture entities [WD 6.2.2]. + pub pictures: Vec>, + sections: [Vec; 5], +} + +impl FileStructure { + /// The inflated bytes of one section. + pub fn section(&self, kind: SectionKind) -> &[u8] { + self.sections.get(kind.index()).map_or(&[], Vec::as_slice) + } + + /// The globals section's leading schema: the fields producers newer than this reader + /// added to each entity type [WD 6.3]. + /// + /// # Errors + /// As [`Schema::read`]. + pub fn schema(&self) -> Result { + Schema::read(&mut BitReader::new(self.section(SectionKind::Globals))) + } +} + +/// A parsed PRC stream: header, file structures and model-file section. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct PrcFile { + /// The file header. + pub header: PrcHeader, + /// File structures in physical order (referenced before referencing). + pub file_structures: Vec, + /// The inflated model-file section: its schema, then the model file entity. + pub model_file: Vec, + /// The `UncompressedFiles` blocks after the file header [WD 6.1.1], which picture + /// entities may also reference. + pub uncompressed_files: Vec>, +} + +/// A little-endian byte cursor over the uncompressed parts of the stream. +struct Bytes<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Bytes<'a> { + fn take(&mut self, n: usize, what: &'static str) -> Result<&'a [u8], PrcError> { + let end = self.pos.checked_add(n).ok_or(PrcError::Truncated(what))?; + let s = self.data.get(self.pos..end).ok_or(PrcError::Truncated(what))?; + self.pos = end; + Ok(s) + } + + fn u32(&mut self, what: &'static str) -> Result { + let s = self.take(4, what)?; + let b: [u8; 4] = s.try_into().map_err(|_| PrcError::Truncated(what))?; + Ok(u32::from_le_bytes(b)) + } + + fn id(&mut self, what: &'static str) -> Result { + Ok(UniqueId([self.u32(what)?, self.u32(what)?, self.u32(what)?, self.u32(what)?])) + } + + fn magic(&mut self) -> Result<(), PrcError> { + match self.take(3, "magic") { + Ok(b"PRC") => Ok(()), + _ => Err(PrcError::NotPrc), + } + } + + /// `UncompressedBlock`s: a count, then size-prefixed blocks [WD 10.2.2-3], each charged to + /// `budget` before it is copied. + fn blocks(&mut self, what: &'static str, budget: &mut Budget) -> Result>, PrcError> { + let n = offset(self.u32(what)?); + // Each block needs at least its 4-byte size, so `n` is bounded by the data left. + if n > self.data.len().saturating_sub(self.pos) / 4 { + return Err(PrcError::Truncated(what)); + } + (0..n) + .map(|_| { + let len = offset(self.u32(what)?); + let block = self.take(len, what)?; + budget.charge(block.len())?; + Ok(block.to_vec()) + }) + .collect() + } +} + +/// A stored `u32` offset or count as a `usize` (saturating on a 16-bit target, where no data +/// that large could exist). +pub(crate) fn offset(v: u32) -> usize { + usize::try_from(v).unwrap_or(usize::MAX) +} + +/// One file structure description of the file header [WD 6.2]: its id and the offsets of +/// its sections. +fn file_structure_description(r: &mut Bytes) -> Result<(UniqueId, Vec), PrcError> { + let id = r.id("file structure description")?; + if r.u32("file structure description")? != 0 { + return Err(PrcError::Malformed("file structure reserved field is not 0".into())); + } + let count = offset(r.u32("file structure description")?); + if count < SECTIONS { + return Err(PrcError::Malformed(format!("{count} sections in a file structure"))); + } + let mut offs = Vec::with_capacity(SECTIONS); + // Offsets past the six this reader uses are read and dropped; each read is bounded by + // the data, so a huge `count` ends in `Truncated`. + for i in 0..count { + let o = r.u32("section offset")?; + if i < SECTIONS { + offs.push(offset(o)); + } + } + Ok((id, offs)) +} + +/// Where one file structure's parts lie: its section offsets and the end of its last section. +struct Extent<'a> { + offs: &'a [usize], + last: usize, +} + +impl Extent<'_> { + fn bound(&self, i: usize) -> usize { + self.offs.get(i).copied().unwrap_or(self.last) + } +} + +/// The file structure `id` lying at `extent`: its header and its inflated sections, charged +/// to `budget`. +fn file_structure(data: &[u8], id: UniqueId, extent: &Extent, budget: &mut Budget) -> Result { + if extent.offs.windows(2).any(|w| w.first() > w.get(1)) || extent.bound(SECTIONS - 1) > extent.last { + return Err(PrcError::Malformed("file structure section offsets are not ascending".into())); + } + let header = data.get(..extent.bound(1)).ok_or(PrcError::Truncated("file structure header"))?; + let mut h = Bytes { data: header, pos: extent.bound(0) }; + h.magic()?; + let min_version_for_read = h.u32("file structure header")?; + let authoring_version = h.u32("file structure header")?; + if h.id("file structure header")? != id { + return Err(PrcError::Malformed("file structure id differs from its description".into())); + } + let _application = h.id("file structure header")?; + // Headers may overlap, so each structure's copy of its pictures is charged. + let pictures = h.blocks("file structure pictures", budget)?; + let mut sections: [Vec; 5] = Default::default(); + for (i, (slot, kind)) in sections.iter_mut().zip(SectionKind::ALL).enumerate() { + let end = if i + 2 < SECTIONS { extent.bound(i + 2) } else { extent.last }; + *slot = budget.inflate(data, (extent.bound(i + 1), end), kind.name())?; + } + Ok(FileStructure { id, min_version_for_read, authoring_version, pictures, sections }) +} + +/// What is left of the inflation ceiling, and the ceiling itself for the error. +struct Budget { + left: usize, + limit: usize, +} + +impl Budget { + /// The zlib stream in `data[range]`, its size charged to the budget. + fn inflate(&mut self, data: &[u8], range: (usize, usize), section: &'static str) -> Result, PrcError> { + let (start, end) = range; + if start == end { + return Ok(Vec::new()); + } + let src = data.get(start..end).ok_or_else(|| PrcError::Malformed(format!("{section} section {start}..{end} is outside the data")))?; + let mut out = Vec::new(); + // One byte past the budget is enough to know it was exceeded. + let cap = u64::try_from(self.left).unwrap_or(u64::MAX).saturating_add(1); + flate2::read::ZlibDecoder::new(src).take(cap).read_to_end(&mut out).map_err(|e| PrcError::Inflate { section, reason: e.to_string() })?; + self.charge(out.len())?; + Ok(out) + } + + /// Spend `n` bytes of the ceiling. + fn charge(&mut self, n: usize) -> Result<(), PrcError> { + self.left = self.left.checked_sub(n).ok_or(PrcError::TooLarge { limit: self.limit })?; + Ok(()) + } +} + +impl PrcFile { + /// Parse a PRC stream (the decoded data of a `/3D` stream with `/Subtype /PRC`). + /// + /// Newer `min_version_for_read` values are not refused: the fields read here are + /// unchanged across published versions, and the caller sees the versions in + /// [`PrcHeader`]. + /// + /// # Errors + /// [`PrcError::NotPrc`] without the magic; [`PrcError::Truncated`] or + /// [`PrcError::Malformed`] for a damaged header or offset table; [`PrcError::Inflate`] + /// for a section that is not zlib; [`PrcError::TooLarge`] past [`MAX_INFLATED_BYTES`] + /// in total, inflated sections and copied blocks together. + /// + /// # Examples + /// ``` + /// use pdfcraft_prc::{PrcError, PrcFile}; + /// assert_eq!(PrcFile::parse(b"%PDF"), Err(PrcError::NotPrc)); + /// ``` + pub fn parse(data: &[u8]) -> Result { + Self::parse_with_limit(data, MAX_INFLATED_BYTES) + } + + /// [`Self::parse`] with a caller-chosen ceiling on the total inflated size, for callers + /// with less memory to spend than [`MAX_INFLATED_BYTES`]. + /// + /// # Errors + /// As [`Self::parse`]; [`PrcError::TooLarge`] carries `limit`. + pub fn parse_with_limit(data: &[u8], limit: usize) -> Result { + let mut r = Bytes { data, pos: 0 }; + r.magic()?; + let header = PrcHeader { + min_version_for_read: r.u32("file header")?, + authoring_version: r.u32("file header")?, + file_id: r.id("file header")?, + application_id: r.id("file header")?, + }; + let n_fs = offset(r.u32("file structure count")?); + if n_fs == 0 || n_fs > MAX_FILE_STRUCTURES { + return Err(PrcError::Malformed(format!("{n_fs} file structures"))); + } + let descriptions = (0..n_fs).map(|_| file_structure_description(&mut r)).collect::, _>>()?; + let mf_start = offset(r.u32("model file offsets")?); + let mf_end = offset(r.u32("model file offsets")?); + let mut budget = Budget { left: limit, limit }; + let uncompressed_files = r.blocks("uncompressed files", &mut budget)?; + if mf_end < mf_start { + return Err(PrcError::Malformed("model file ends before it starts".into())); + } + if mf_end > data.len() { + return Err(PrcError::Malformed(format!("model file ends at {mf_end}, past the {}-byte stream", data.len()))); + } + + // prc-rs: the last section runs to the model file's start. + let last = mf_start.min(data.len()); + let file_structures = + descriptions.iter().map(|(id, offs)| file_structure(data, *id, &Extent { offs, last }, &mut budget)).collect::, _>>()?; + let model_file = budget.inflate(data, (mf_start, mf_end), "model file")?; + Ok(PrcFile { header, file_structures, model_file, uncompressed_files }) + } +} diff --git a/crates/prc/src/entity.rs b/crates/prc/src/entity.rs new file mode 100644 index 0000000000..9e3c6c2a7b --- /dev/null +++ b/crates/prc/src/entity.rs @@ -0,0 +1,127 @@ +//! What every section reader shares: the cursor, the schema, the authoring version, and the +//! base fields every PRC entity starts with [WD 7.2.3, 7.4.3, 8.6]. + +use crate::PrcError; +use crate::bits::BitReader; +use crate::schema::Schema; +use crate::tree::Graphics; + +const ATTRIBUTE: u32 = 201; + +/// A [`PrcError::Malformed`] naming `what`. +pub(crate) fn malformed(what: String) -> PrcError { + PrcError::Malformed(what) +} + +/// A section reader. +pub(crate) struct Ctx<'a, 's> { + pub(crate) r: BitReader<'a>, + pub(crate) schema: &'s Schema, + /// The file structure's authoring version; gates version-added fields. + pub(crate) version: u32, + /// The current name: what a `same_name` entity reuses; every entity's own name replaces + /// it [WD 7.2.3.4]. Each section starts with none. + pub(crate) name: Option, + /// The PRC unique id of the last `ContentPRCRefBase` read [WD 7.2.3.2]. + pub(crate) uid: u32, + /// The current graphics: what a `same_graphics` entity reuses [WD 7.2.4]. + pub(crate) graphics: Graphics, +} + +impl<'a, 's> Ctx<'a, 's> { + pub(crate) fn new(r: BitReader<'a>, schema: &'s Schema, version: u32) -> Self { + Ctx { r, schema, version, name: None, uid: 0, graphics: Graphics::default() } + } + + /// Reads an entity type code; any value but `want` is malformed. + pub(crate) fn expect_type(&mut self, want: u32) -> Result<(), PrcError> { + let t = self.r.unsigned_integer()?; + if t != want { + return Err(malformed(format!("entity type {t} where {want} belongs"))); + } + Ok(()) + } + + /// A count that must fit in the remaining data at `min_bits` per item, which bounds any + /// allocation sized by it. + pub(crate) fn count(&mut self, min_bits: usize, what: &'static str) -> Result { + let n = self.r.unsigned_integer()? as usize; + if n.saturating_mul(min_bits) > self.r.remaining() { + return Err(PrcError::Truncated(what)); + } + self.r.take_elements(n)?; + Ok(n) + } + + /// A counted array of `UnsignedInteger`s. + pub(crate) fn uints(&mut self, what: &'static str) -> Result, PrcError> { + let n = self.count(1, what)?; + (0..n).map(|_| self.r.unsigned_integer()).collect() + } + + /// A counted array of `Double`s. + pub(crate) fn doubles(&mut self, what: &'static str) -> Result, PrcError> { + let n = self.count(2, what)?; + (0..n).map(|_| self.r.double()).collect() + } + + /// `AttributeEntry` [WD 7.4.3]: a predefined title code or a string. + fn attribute_entry(&mut self) -> Result<(), PrcError> { + if self.r.bit()? { + self.r.unsigned_integer()?; + } else { + self.r.string()?; + } + Ok(()) + } + + /// One attribute value [WD 7.4.3], read past. + fn attribute_value(&mut self) -> Result<(), PrcError> { + match self.r.unsigned_integer()? { + 0 => {} + 1 | 3 => { + self.r.integer()?; + } + 2 => { + self.r.double()?; + } + 4 => { + self.r.string()?; + } + // prc-rs: a 64-bit integer is its high part, then its low part. + 5 => { + self.r.integer()?; + self.r.unsigned_integer()?; + } + k => return Err(malformed(format!("attribute value kind {k}"))), + } + Ok(()) + } + + /// `ContentPRCBase` [WD 7.2.3]: attributes, then the name; returns the entity's name, + /// which is also the current name afterwards. + pub(crate) fn content_prc_base(&mut self) -> Result, PrcError> { + let n = self.count(1, "attributes")?; + for _ in 0..n { + self.expect_type(ATTRIBUTE)?; + self.attribute_entry()?; + let pairs = self.count(2, "attribute pairs")?; + for _ in 0..pairs { + self.attribute_entry()?; + self.attribute_value()?; + } + self.schema.skip_added_fields(ATTRIBUTE, &mut self.r)?; + } + // `same_name` TRUE reuses the current name [WD 7.2.3.4]. + if !self.r.bit()? { + self.name = self.r.string()?; + } + Ok(self.name.clone()) + } + + /// `UserData` [WD 8.6]: a bit count, then that many opaque bits. + pub(crate) fn user_data(&mut self) -> Result<(), PrcError> { + let n = self.r.unsigned_integer()? as usize; + self.r.skip_bits(n) + } +} diff --git a/crates/prc/src/error.rs b/crates/prc/src/error.rs new file mode 100644 index 0000000000..c1d927a224 --- /dev/null +++ b/crates/prc/src/error.rs @@ -0,0 +1,46 @@ +//! The crate's error type. + +/// Why a PRC stream could not be read. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[non_exhaustive] +pub enum PrcError { + /// The data does not start with the `PRC` magic [WD 6.1.1]. + #[error("not a PRC stream: missing the \"PRC\" magic")] + NotPrc, + /// A value ran past the end of its byte or bit stream. + #[error("PRC data truncated while reading {0}")] + Truncated(&'static str), + /// A structural field holds a value the format forbids. + #[error("malformed PRC: {0}")] + Malformed(String), + /// No `Double` code of up to 22 bits matched [WD 11.17]. + #[error("PRC Double: no code matches the next 22 bits")] + UnknownDoubleCode, + /// A section is not a valid zlib stream. + #[error("PRC {section} section does not inflate: {reason}")] + Inflate { + /// The section's name. + section: &'static str, + /// The decoder's message. + reason: String, + }, + /// The inflated sections and copied blocks together exceed the ceiling + /// ([`crate::MAX_INFLATED_BYTES`] or the caller's limit). + #[error("PRC sections inflate past the {limit}-byte ceiling")] + TooLarge { + /// The ceiling that was hit. + limit: usize, + }, + /// The arrays read from one stream hold more elements than [`crate::MAX_DECODED_ELEMENTS`]. + /// One bit can decode to a 4- or 8-byte element, so the inflation ceiling alone does not + /// bound the memory the arrays take. + #[error("PRC arrays decode past the {limit}-element ceiling")] + TooManyElements { + /// The ceiling that was hit. + limit: usize, + }, + /// A construct this reader does not decode; the part of the file holding it cannot be + /// read past. + #[error("PRC {0} is not supported")] + Unsupported(&'static str), +} diff --git a/crates/prc/src/geometry.rs b/crates/prc/src/geometry.rs new file mode 100644 index 0000000000..c8d7680e05 --- /dev/null +++ b/crates/prc/src/geometry.rs @@ -0,0 +1,276 @@ +//! Model-space vectors and matrices, and their application to meshes. + +use crate::TriangleMesh; + +/// A point or direction in model space. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub(crate) struct Vec3(pub(crate) [f64; 3]); + +impl Vec3 { + /// The right-handed cross product `self × b`. + pub(crate) fn cross(self, b: Vec3) -> Vec3 { + let ([ax, ay, az], [bx, by, bz]) = (self.0, b.0); + Vec3([ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]) + } +} + +/// A 4×4 matrix, `m.0[row][col]`, acting on column vectors. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Matrix(pub [[f64; 4]; 4]); + +impl Default for Matrix { + fn default() -> Self { + Matrix::IDENTITY + } +} + +impl Matrix { + /// The identity matrix. + pub const IDENTITY: Matrix = Matrix([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]); + + /// The entry at `row`, `col`; 0 outside the matrix. + #[must_use] + pub fn at(&self, row: usize, col: usize) -> f64 { + self.0.get(row).and_then(|r| r.get(col)).copied().unwrap_or(0.0) + } + + /// `self × b`: `b` applied first. + #[must_use] + pub fn multiply(&self, b: &Matrix) -> Matrix { + let mut m = [[0.0; 4]; 4]; + for (i, row) in m.iter_mut().enumerate() { + for (j, cell) in row.iter_mut().enumerate() { + *cell = (0..4).map(|k| self.at(i, k) * b.at(k, j)).sum(); + } + } + Matrix(m) + } + + /// `self × (p, 1)`, divided by the homogeneous coordinate when it is neither 0 nor 1. + #[must_use] + pub fn transform_point(&self, p: [f64; 3]) -> [f64; 3] { + let [x, y, z] = p; + let row = |i: usize| self.at(i, 0) * x + self.at(i, 1) * y + self.at(i, 2) * z + self.at(i, 3); + let w = row(3); + let s = if w == 0.0 || w == 1.0 { 1.0 } else { 1.0 / w }; + [row(0) * s, row(1) * s, row(2) * s] + } + + /// The determinant of the upper-left 3×3 block. + fn det3(&self) -> f64 { + let a = |i, j| self.at(i, j); + a(0, 0) * (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1)) - a(0, 1) * (a(1, 0) * a(2, 2) - a(1, 2) * a(2, 0)) + + a(0, 2) * (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0)) + } + + /// The determinant of the whole matrix, by expansion along the last row. + fn det4(&self) -> f64 { + (0..4) + .map(|j| { + let cols: Vec = (0..4).filter(|&c| c != j).collect(); + let a = |i: usize, k: usize| cols.get(k).map_or(0.0, |&c| self.at(i, c)); + let minor = a(0, 0) * (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1)) - a(0, 1) * (a(1, 0) * a(2, 2) - a(1, 2) * a(2, 0)) + + a(0, 2) * (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0)); + let sign = if (3 + j) % 2 == 0 { 1.0 } else { -1.0 }; + sign * self.at(3, j) * minor + }) + .sum() + } + + /// Whether mapping points through this matrix turns them inside out. Where the point is + /// divided by `w`, the Jacobian's sign is the sign of the 4×4 determinant; a last row of + /// zeros (no division) falls back to the 3×3 block. Each row is first divided by its + /// largest entry, which keeps both signs while the products stay in range. + fn mirrors(&self) -> bool { + let scaled = Matrix(self.0.map(|row| { + let big = row.iter().fold(0.0_f64, |m, c| m.max(c.abs())); + if big.is_finite() && big > 0.0 { row.map(|c| c / big) } else { row } + })); + let det = scaled.det4(); + if det == 0.0 { scaled.det3() < 0.0 } else { det < 0.0 } + } + + /// Cofactor `(i, j)` of the upper-left 3×3 block (`i`, `j` < 3). + fn cofactor(&self, i: usize, j: usize) -> f64 { + let (r0, r1) = ((i + 1) % 3, (i + 2) % 3); + let (c0, c1) = ((j + 1) % 3, (j + 2) % 3); + self.at(r0, c0) * self.at(r1, c1) - self.at(r0, c1) * self.at(r1, c0) + } +} + +impl TriangleMesh { + /// This mesh with every position mapped through `m` and every stored normal through its + /// inverse transpose (renormalised); a mirroring `m` (negative 4×4 determinant, which a + /// negative `w` can cause too) reverses each triangle so the outside stays + /// counter-clockwise. A projective `m` (a last row other than + /// `[0, 0, 0, w]`) bends the surface per point, so the stored normals are dropped. + /// + /// ``` + /// use pdfcraft_prc::{Matrix, TriangleMesh}; + /// let mut mirror = Matrix::IDENTITY; + /// mirror.0[0][0] = -1.0; + /// let mut mesh = TriangleMesh::default(); + /// mesh.positions = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; + /// mesh.triangles = vec![[0, 1, 2]]; + /// let placed = mesh.transformed(&mirror); + /// assert_eq!(placed.positions, [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]); + /// assert_eq!(placed.triangles, [[0, 2, 1]]); + /// ``` + #[must_use] + pub fn transformed(&self, m: &Matrix) -> Self { + let mut out = self.clone(); + for p in &mut out.positions { + *p = m.transform_point(*p); + } + // The cofactor matrix is det times the inverse transpose, so its sign is corrected + // and the result renormalised. + let mirrors = m.mirrors(); + let sign = if mirrors { -1.0 } else { 1.0 }; + if m.at(3, 0) != 0.0 || m.at(3, 1) != 0.0 || m.at(3, 2) != 0.0 { + out.normals.clear(); + out.triangle_normals.clear(); + } + for n in &mut out.normals { + let [x, y, z] = *n; + let v = [0, 1, 2].map(|i| sign * (m.cofactor(i, 0) * x + m.cofactor(i, 1) * y + m.cofactor(i, 2) * z)); + let len = v.iter().map(|c| c * c).sum::().sqrt(); + *n = if len > 0.0 && len.is_finite() { v.map(|c| c / len) } else { v }; + } + if mirrors { + for t in &mut out.triangles { + t.swap(1, 2); + } + for t in &mut out.triangle_normals { + t.swap(1, 2); + } + for t in out.triangle_uvs.iter_mut().flatten().flatten() { + t.swap(1, 2); + } + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cross_follows_the_right_hand_rule() { + assert_eq!(Vec3([1.0, 0.0, 0.0]).cross(Vec3([0.0, 1.0, 0.0])), Vec3([0.0, 0.0, 1.0])); + assert_eq!(Vec3([0.0, 1.0, 0.0]).cross(Vec3([1.0, 0.0, 0.0])), Vec3([0.0, 0.0, -1.0])); + } + + #[test] + fn multiply_applies_the_right_operand_first() { + let mut t = Matrix::IDENTITY; + t.0[0][3] = 5.0; + let mut s = Matrix::IDENTITY; + s.0[0][0] = 2.0; + assert_eq!(t.multiply(&s).transform_point([1.0, 0.0, 0.0]), [7.0, 0.0, 0.0]); + assert_eq!(s.multiply(&t).transform_point([1.0, 0.0, 0.0]), [12.0, 0.0, 0.0]); + } + + #[test] + fn a_projective_point_is_divided_by_w() { + let mut m = Matrix::IDENTITY; + m.0[3][3] = 2.0; + assert_eq!(m.transform_point([2.0, 4.0, 6.0]), [1.0, 2.0, 3.0]); + m.0[3][3] = 0.0; + assert_eq!(m.transform_point([2.0, 4.0, 6.0]), [2.0, 4.0, 6.0]); + } + + #[test] + fn normals_take_the_inverse_transpose_and_mirror_with_their_triangle() { + // A shear in x by y turns the plane x = 0 into x = y; its x normal tilts to stay perpendicular. + let mut shear = Matrix::IDENTITY; + shear.0[0][1] = 1.0; + let mesh = TriangleMesh { + positions: vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + triangles: vec![[0, 1, 2]], + normals: vec![[1.0, 0.0, 0.0]], + triangle_normals: vec![[0, 0, 0]], + ..TriangleMesh::default() + }; + let n = shear.transformed_normal(&mesh); + let s = std::f64::consts::FRAC_1_SQRT_2; + assert!((n[0] - s).abs() < 1e-12 && (n[1] + s).abs() < 1e-12 && n[2] == 0.0, "{n:?}"); + + let mut mirror = Matrix::IDENTITY; + mirror.0[2][2] = -1.0; + let placed = mesh.transformed(&mirror); + assert_eq!(placed.triangles, [[0, 2, 1]]); + assert_eq!(placed.triangle_normals, [[0, 0, 0]]); + assert_eq!(placed.normals, [[1.0, 0.0, 0.0]]); + } + + #[test] + fn a_projective_transform_drops_the_stored_normals() { + let mesh = TriangleMesh { + positions: vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + triangles: vec![[0, 1, 2]], + normals: vec![[1.0, 0.0, 1.0]], + triangle_normals: vec![[0, 0, 0]], + ..TriangleMesh::default() + }; + let mut projective = Matrix::IDENTITY; + projective.0[3][0] = 1.0; + let placed = mesh.transformed(&projective); + assert!(placed.normals.is_empty() && placed.triangle_normals.is_empty()); + // A uniform w keeps them. + let mut uniform = Matrix::IDENTITY; + uniform.0[3][3] = 2.0; + assert_eq!(mesh.transformed(&uniform).triangle_normals, [[0, 0, 0]]); + } + + /// A negative `w` turns the mapped triangles inside out though the 3×3 block does not + /// mirror, so the winding is reversed by the sign of the whole determinant. + #[test] + fn a_negative_w_reverses_the_winding() { + let mesh = TriangleMesh { + positions: vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + triangles: vec![[0, 1, 2]], + normals: vec![[1.0, 0.0, 0.0]], + triangle_normals: vec![[0, 0, 0]], + ..TriangleMesh::default() + }; + let mut negated = Matrix::IDENTITY; + negated.0[3][3] = -1.0; + let placed = mesh.transformed(&negated); + assert_eq!(placed.positions, [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]); + assert_eq!(placed.triangles, [[0, 2, 1]]); + assert_eq!(placed.normals, [[-1.0, 0.0, 0.0]]); + // A projective w that is negative over the mesh: w = z - 3. + let mut projective = Matrix::IDENTITY; + projective.0[3][2] = 1.0; + projective.0[3][3] = -3.0; + assert_eq!(mesh.transformed(&projective).triangles, [[0, 2, 1]]); + // A positive w, uniform or projective, keeps the winding. + let mut positive = Matrix::IDENTITY; + positive.0[3][2] = 1.0; + positive.0[3][3] = 3.0; + assert_eq!(mesh.transformed(&positive).triangles, [[0, 1, 2]]); + } + + #[test] + fn a_mirror_whose_determinant_underflows_still_reverses_the_winding() { + let mesh = TriangleMesh { + positions: vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + triangles: vec![[0, 1, 2]], + ..TriangleMesh::default() + }; + let diagonal = |x: f64| { + let mut m = Matrix::IDENTITY; + (m.0[0][0], m.0[1][1], m.0[2][2]) = (x, 1e-200, 1e-200); + m + }; + assert_eq!(mesh.transformed(&diagonal(-1e200)).triangles, [[0, 2, 1]]); + assert_eq!(mesh.transformed(&diagonal(1e200)).triangles, [[0, 1, 2]]); + } + + impl Matrix { + fn transformed_normal(&self, mesh: &TriangleMesh) -> [f64; 3] { + mesh.transformed(self).normals.first().copied().unwrap_or_default() + } + } +} diff --git a/crates/prc/src/lib.rs b/crates/prc/src/lib.rs new file mode 100644 index 0000000000..d594e74065 --- /dev/null +++ b/crates/prc/src/lib.rs @@ -0,0 +1,37 @@ +//! A reader for PRC, the 3D model format a PDF `/3D` stream of `/Subtype /PRC` carries +//! (ISO 32000-2 §13.6.3, ISO 14739-1). +//! +//! Layer L0, standalone: this crate depends on no other PdfCraft crate. It reads the +//! uncompressed container, inflates each section, and decodes the bit-level primitives and +//! the schema a newer producer uses to describe fields this reader does not know; it decodes +//! tessellations and walks the product tree into placed, coloured meshes +//! ([`PrcFile::placements`], [`PrcFile::model_tree`]). +//! +//! Clause numbers in `[WD x.y]` refer to the PRC working draft, ISO/TC 171/SC 2 N570. +//! +//! Every read is bounded: inflation by [`MAX_INFLATED_BYTES`] (or a caller's limit), decoded +//! array elements by [`MAX_DECODED_ELEMENTS`] and schema-program steps by [`MAX_SCHEMA_STEPS`] +//! per reading pass, schema programs by a depth ceiling. Damaged data is an error, never a panic. +#![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + +mod acof; +mod arrays; +mod bits; +mod container; +mod entity; +mod error; +mod geometry; +mod schema; +mod tess; +#[cfg(test)] +mod testw; +mod tree; + +pub use bits::{BitReader, MAX_DECODED_ELEMENTS, MAX_SCHEMA_STEPS}; +pub use container::{FileStructure, MAX_FILE_STRUCTURES, MAX_INFLATED_BYTES, PRC_READER_VERSION, PrcFile, PrcHeader, SectionKind, UniqueId}; +pub use error::PrcError; +pub use geometry::Matrix; +pub use schema::Schema; +pub use tess::{Tessellation, TriangleMesh, TriangleStyle}; +pub use tree::{EntityOverrides, MAX_PLACEMENTS, ModelNode, NameSource, Placement, Rgba, StyleAlpha}; diff --git a/crates/prc/src/schema.rs b/crates/prc/src/schema.rs new file mode 100644 index 0000000000..2b505c14ea --- /dev/null +++ b/crates/prc/src/schema.rs @@ -0,0 +1,565 @@ +//! `FileStructureSchema` and its token interpreter [WD 6.3, 9]. +//! +//! A producer newer than the reader describes, per entity type, the fields it appended; the +//! reader runs that program after the fields it knows, to step over the rest [WD 9.1]. Token +//! values follow the WD 9.2 table as corrected by PDF Association pdf-issues #407 +//! (`Block_Start` = 19; 39 and 40 swallow one token). +//! +//! Choices the sources leave open, made here: +//! - `SimpleFor` reads its count as an `UnsignedInteger` [WD 9.3.17 pseudocode; pdf-issues +//! #575]: that read also decodes an `Integer`-written non-negative count, while a signed +//! read does not decode an unsigned one whose top group is 0x80 or higher. +//! - `Block_Version V` runs its block only when `V` is newer than +//! [`crate::PRC_READER_VERSION`]: an older block holds fields this reader already reads +//! natively [WD 9.3.20]. +//! - `Extent_1D` and `Extent_2D` read 2 and 4 `Double`s: `Interval` and `Domain` +//! [WD 8.2.2, 8.2.4]. +//! - Expression values are `i64`; a `Double` read as an operand truncates (NaN reads as 0). +//! - Variables are not block-scoped: a program that reuses an index after its block ends +//! reads the stale value instead of an error. +//! - Pointer tokens (12-14) and `Value_CurveIs3D` (28) need the exact-geometry reader and are +//! refused with [`PrcError::Unsupported`]. + +use std::collections::HashMap; + +use crate::PrcError; +use crate::bits::BitReader; + +/// Nesting depth of blocks, loops, expressions and `Father_Type` calls. +const MAX_DEPTH: u32 = 64; + +/// The per-entity-type programs at the start of a globals or model-file +/// bitstream [WD 6.3.1]. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Schema { + programs: HashMap>, +} + +impl Schema { + /// Read a schema: `UnsignedInteger` count, then per entry the entity + /// type, a token count and the tokens [WD 6.3.2]. + /// + /// # Errors + /// [`PrcError::Truncated`] when a count runs past the data; + /// [`PrcError::Malformed`] for an entity type listed twice; [`PrcError::TooManyElements`] + /// past the reader's allowance, one element per entry and per token. + pub fn read(r: &mut BitReader<'_>) -> Result { + let n = r.unsigned_integer()? as usize; + // Every entry takes at least two bits. + if n > r.remaining() / 2 { + return Err(PrcError::Truncated("the schema")); + } + let mut programs = HashMap::new(); + for _ in 0..n { + // The entry itself costs one element, so empty programs are not free. + r.take_elements(1)?; + let entity_type = r.unsigned_integer()?; + let len = r.unsigned_integer()? as usize; + if len > r.remaining() { + return Err(PrcError::Truncated("a schema program")); + } + r.take_elements(len)?; + let tokens = (0..len).map(|_| r.unsigned_integer()).collect::, _>>()?; + if programs.insert(entity_type, tokens).is_some() { + return Err(PrcError::Malformed(format!("schema lists entity type {entity_type} twice"))); + } + } + Ok(Self { programs }) + } + + /// True when no entity type has added fields (the common case: the file + /// was written at the version this reader implements). + pub fn is_empty(&self) -> bool { + self.programs.is_empty() + } + + /// The entity types the schema describes, unordered. + pub fn entity_types(&self) -> impl Iterator + '_ { + self.programs.keys().copied() + } + + /// Consume the fields a newer producer appended to `entity_type`, if the + /// schema describes it; a no-op otherwise. Each step is charged to `r`, so every + /// program run on one stream shares [`crate::MAX_SCHEMA_STEPS`]. + /// + /// # Errors + /// [`PrcError::Unsupported`] for a token that needs exact geometry; + /// [`PrcError::Malformed`] for an ill-formed program, one past the depth ceiling, or + /// one that runs out of the stream's steps; any read error from `r`. + pub fn skip_added_fields(&self, entity_type: u32, r: &mut BitReader<'_>) -> Result<(), PrcError> { + let Some(program) = self.programs.get(&entity_type) else { + return Ok(()); + }; + let mut exec = Exec { schema: self, r, vars: HashMap::new(), loop_counter: 0 }; + exec.program(program, 0) + } +} + +struct Exec<'s, 'r, 'd> { + schema: &'s Schema, + r: &'r mut BitReader<'d>, + vars: HashMap, + loop_counter: i64, +} + +fn malformed(what: &str) -> PrcError { + PrcError::Malformed(format!("schema program: {what}")) +} + +/// The token at `*pos`, advancing past it. +fn next(prog: &[u32], pos: &mut usize) -> Result { + let t = prog.get(*pos).copied().ok_or_else(|| malformed("ends mid-statement"))?; + *pos += 1; + Ok(t) +} + +impl Exec<'_, '_, '_> { + fn tick(&mut self, depth: u32) -> Result<(), PrcError> { + if !self.r.take_step() { + return Err(malformed("step ceiling reached")); + } + if depth > MAX_DEPTH { + return Err(malformed("nesting ceiling reached")); + } + Ok(()) + } + + fn program(&mut self, prog: &[u32], depth: u32) -> Result<(), PrcError> { + let mut pos = 0; + while pos < prog.len() { + self.stmt(prog, &mut pos, true, depth)?; + } + Ok(()) + } + + /// One data read for tokens 0-5 and 7-11, returning its value as an + /// operand. + fn read(&mut self, token: u32) -> Result { + let doubles = match token { + 0 => return Ok(i64::from(self.r.bit()?)), + 1 => return Ok(self.r.double()? as i64), + 2 => return Ok(i64::from(self.r.character()?)), + 3 => return Ok(i64::from(self.r.unsigned_integer()?)), + 4 => return Ok(i64::from(self.r.integer()?)), + 5 => { + self.r.string()?; + return Ok(0); + } + 7 => 2, + 8 => 3, + 9 => 2, + 10 => 4, + 11 => 6, + _ => return Err(malformed("not a data token")), + }; + for _ in 0..doubles { + self.r.double()?; + } + Ok(0) + } + + /// Statements until `Block_End`, which is consumed. + fn block(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> { + loop { + if prog.get(*pos) == Some(&21) { + *pos += 1; + return Ok(()); + } + self.stmt(prog, pos, run, depth + 1)?; + } + } + + /// Run `body` (one statement at `*pos`) `count` times; `*pos` ends past it. + fn repeat(&mut self, prog: &[u32], pos: &mut usize, count: i64, run: bool, depth: u32) -> Result<(), PrcError> { + let start = *pos; + self.stmt(prog, pos, false, depth + 1)?; + if run { + let saved = self.loop_counter; + for i in 0..count.max(0) { + self.tick(depth)?; + self.loop_counter = i; + let mut p = start; + self.stmt(prog, &mut p, true, depth + 1)?; + } + self.loop_counter = saved; + } + Ok(()) + } + + /// One statement; with `run` false, only step over its tokens. + fn stmt(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> { + self.tick(depth)?; + match next(prog, pos)? { + t @ (0..=5 | 7..=11) => { + if run { + self.read(t)?; + } + } + 6 => self.parent_program(prog, pos, run, depth)?, + 12 => { + next(prog, pos)?; + if run { + return Err(PrcError::Unsupported("a schema pointer field")); + } + } + 13 | 14 => { + if run { + return Err(PrcError::Unsupported("a schema pointer field")); + } + } + 15 => { + let count = self.expr(prog, pos, run, depth + 1)?; + self.repeat(prog, pos, count, run, depth)?; + } + 16 => { + // prc-rs: reads SimpleFor's count as an Integer; this reader deliberately reads an + // UnsignedInteger (module doc). + let count = if run { i64::from(self.r.unsigned_integer()?) } else { 0 }; + self.repeat(prog, pos, count, run, depth)?; + } + 17 => self.conditional(prog, pos, run, depth)?, + 19 => self.block(prog, pos, run, depth)?, + 20 => { + let version = next(prog, pos)?; + let newer = version > crate::PRC_READER_VERSION; + self.block(prog, pos, run && newer, depth)?; + } + 22 => { + let v = next(prog, pos)?; + if run { + self.vars.insert(v, 0); + } + } + 23 | 24 => { + let v = next(prog, pos)?; + let value = self.expr(prog, pos, run, depth + 1)?; + if run { + self.vars.insert(v, value); + } + } + 39 | 40 => { + next(prog, pos)?; + } + t => return Err(malformed(&format!("token {t} where a statement belongs"))), + } + Ok(()) + } + + /// Token 6: run the named type's own program, the fields it inherits. + fn parent_program(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> { + let parent = next(prog, pos)?; + if run && let Some(p) = self.schema.programs.get(&parent) { + self.tick(depth + 1)?; + let mut q = 0; + while q < p.len() { + self.stmt(p, &mut q, true, depth + 1)?; + } + } + Ok(()) + } + + /// Token 17: `if` with an optional token-18 `else`. + fn conditional(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> { + let cond = self.expr(prog, pos, run, depth + 1)? != 0; + self.stmt(prog, pos, run && cond, depth + 1)?; + if prog.get(*pos) == Some(&18) { + *pos += 1; + self.stmt(prog, pos, run && !cond, depth + 1)?; + } + Ok(()) + } + + /// One expression; with `run` false its value is 0 and nothing is read. + fn expr(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result { + self.tick(depth)?; + let t = next(prog, pos)?; + match t { + 0..=4 => { + if run { + self.read(t) + } else { + Ok(0) + } + } + 25 => { + let v = next(prog, pos)?; + if !run { + return Ok(0); + } + self.vars.get(&v).copied().ok_or_else(|| malformed(&format!("variable {v} read before it is set"))) + } + 26 => Ok(i64::from(next(prog, pos)?)), + 27 => Ok(self.loop_counter), + 28 => { + if run { + Err(PrcError::Unsupported("Value_CurveIs3D")) + } else { + Ok(0) + } + } + 29..=38 => { + let a = self.expr(prog, pos, run, depth + 1)?; + let b = self.expr(prog, pos, run, depth + 1)?; + if !run { + return Ok(0); + } + let overflow = || malformed("a schema expression overflows"); + Ok(match t { + 29 => a.checked_mul(b).ok_or_else(overflow)?, + 30 => a.checked_div(b).ok_or_else(|| malformed("division by zero"))?, + 31 => a.checked_add(b).ok_or_else(overflow)?, + 32 => a.checked_sub(b).ok_or_else(overflow)?, + 33 => i64::from(a < b), + 34 => i64::from(a <= b), + 35 => i64::from(a > b), + 36 => i64::from(a >= b), + 37 => i64::from(a == b), + _ => i64::from(a != b), + }) + } + t => Err(malformed(&format!("token {t} where a value belongs"))), + } + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::indexing_slicing)] +mod tests { + use super::*; + use crate::testw::W; + + fn schema(entries: &[(u32, &[u32])]) -> W { + let mut w = W::default(); + w.uint(entries.len() as u32); + for (t, toks) in entries { + w.uint(*t).uint(toks.len() as u32); + for k in *toks { + w.uint(*k); + } + } + w + } + + /// Each schema entry and its program's tokens are charged against the reader's element + /// allowance. + #[test] + fn program_tokens_are_charged_against_the_allowance() { + let bytes = schema(&[(172, &[1, 2, 3])]).bytes(); + assert!(matches!(Schema::read(&mut BitReader::with_elements(&bytes, 3)), Err(PrcError::TooManyElements { .. }))); + assert!(Schema::read(&mut BitReader::with_elements(&bytes, 4)).is_ok()); + let empty = schema(&[(172, &[]), (173, &[]), (174, &[])]).bytes(); + assert!(matches!(Schema::read(&mut BitReader::with_elements(&empty, 2)), Err(PrcError::TooManyElements { .. }))); + assert!(Schema::read(&mut BitReader::with_elements(&empty, 3)).is_ok()); + } + + fn run(entries: &[(u32, &[u32])], data: &W) -> (Result<(), PrcError>, usize) { + let mut w = schema(entries); + w.append(data); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + let s = Schema::read(&mut r).unwrap(); + let start = r.position(); + let res = s.skip_added_fields(172, &mut r); + (res, r.position() - start) + } + + #[test] + fn integer_string_and_boolean_tokens_consume_their_fields() { + let mut d = W::default(); + d.bit(true).uint(300).int(-7).string(Some("ab")).put(0x41, 8); + let want = d.len(); + d.uint(99); // not part of the added fields + let (res, used) = run(&[(172, &[0, 3, 4, 5, 2])], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn data_tokens_consume_their_fields() { + let mut d = W::default(); + d.bit(true).double(1.5).uint(300).int(-7).string(Some("ab")).double(2.0).double(3.0); + let want = d.len(); + d.uint(99); // not part of the added fields + let (res, used) = run(&[(172, &[0, 1, 3, 4, 5, 7])], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn vector_extent_and_box_tokens_read_their_doubles() { + // Vec3, Extent1D, Extent2D, BBox: 3 + 2 + 4 + 6 Doubles. + let mut d = W::default(); + for i in 0..15 { + d.double(f64::from(i) + 0.5); + } + let want = d.len(); + d.uint(99); + let (res, used) = run(&[(172, &[8, 9, 10, 11])], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn arithmetic_and_comparison_operators() { + // For (5 - 3) { UInt }, then If (2 < 2) { Bool } Else { Char }. + let mut d = W::default(); + d.uint(7).uint(8).put(0x41, 8); + let want = d.len(); + d.uint(99); + let prog = [15, 32, 26, 5, 26, 3, 3, 17, 33, 26, 2, 26, 2, 0, 18, 2]; + let (res, used) = run(&[(172, &prog)], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn an_absent_type_is_a_no_op() { + let mut w = schema(&[(300, &[3])]); + w.uint(5); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + let s = Schema::read(&mut r).unwrap(); + let at = r.position(); + s.skip_added_fields(172, &mut r).unwrap(); + assert_eq!(r.position(), at); + assert!(!s.is_empty()); + } + + #[test] + fn simple_for_reads_its_count_then_repeats_the_body() { + let mut d = W::default(); + d.int(3).uint(1).uint(2).uint(3); + let want = d.len(); + d.uint(77); + let (res, used) = run(&[(172, &[16, 3])], &d); + res.unwrap(); + assert_eq!(used, want); + + // A block body. + let mut d = W::default(); + d.int(2).bit(true).uint(9).bit(false).uint(8); + let want = d.len(); + let (res, used) = run(&[(172, &[16, 19, 0, 3, 21])], &d); + res.unwrap(); + assert_eq!(used, want); + + // A count whose top byte is 0x80 or higher, written either way. + for signed in [false, true] { + let mut d = W::default(); + if signed { + d.int(200); + } else { + d.uint(200); + } + (0..200).for_each(|_| { + d.bit(true); + }); + let want = d.len(); + d.uint(77); + let (res, used) = run(&[(172, &[16, 0])], &d); + res.unwrap(); + assert_eq!(used, want, "signed={signed}"); + } + } + + #[test] + fn if_else_and_variables_choose_the_branch_by_file_data() { + // DeclareAndSet v0 = ; if v0 > 1 { UInt } else { Character }. + let prog: &[u32] = &[24, 0, 3, 17, 35, 25, 0, 26, 1, 3, 18, 2]; + let mut d = W::default(); + d.uint(5).uint(4250); + let want = d.len(); + let (res, used) = run(&[(172, prog)], &d); + res.unwrap(); + assert_eq!(used, want); + + let mut d = W::default(); + d.uint(0).put(0xAB, 8); + let want = d.len(); + let (res, used) = run(&[(172, prog)], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn for_counts_by_expression_and_exposes_the_counter() { + // For (2 * 2) { if counter == 2 { UInt } } => one UInt read. + let prog: &[u32] = &[15, 29, 26, 2, 26, 2, 17, 37, 27, 26, 2, 3]; + let mut d = W::default(); + d.uint(1234); + let want = d.len(); + let (res, used) = run(&[(172, prog)], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn an_expression_that_overflows_is_malformed() { + // For ((2^31 * 2^31) * 4) { UInt }: 2^64 would wrap to 0 and read nothing. + let prog: &[u32] = &[15, 29, 29, 26, 1 << 31, 26, 1 << 31, 26, 4, 3]; + let (res, _) = run(&[(172, prog)], &W::default()); + assert!(matches!(res, Err(PrcError::Malformed(m)) if m.contains("overflows"))); + } + + #[test] + fn version_blocks_run_only_when_newer_than_the_reader() { + let mut d = W::default(); + d.uint(42); + let want = d.len(); + let (res, used) = run(&[(172, &[20, 8137, 3, 21, 20, 9000, 3, 21])], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn father_type_runs_the_parent_program_and_obsolete_tokens_swallow_one() { + let mut d = W::default(); + d.uint(1).uint(2); + let want = d.len(); + let (res, used) = run(&[(172, &[39, 3, 6, 50, 40, 8]), (50, &[3, 3])], &d); + res.unwrap(); + assert_eq!(used, want); + } + + #[test] + fn hostile_programs_are_refused_not_run_forever() { + let d = W::default(); + // For (1000000 * 1000000) { empty block }. + let prog: &[u32] = &[15, 29, 26, 1_000_000, 26, 1_000_000, 19, 21]; + assert!(matches!(run(&[(172, prog)], &d).0, Err(PrcError::Malformed(_)))); + // Father_Type recursing into itself. + assert!(matches!(run(&[(172, &[6, 172])], &d).0, Err(PrcError::Malformed(_)))); + // A pointer field. + assert!(matches!(run(&[(172, &[13])], &d).0, Err(PrcError::Unsupported(_)))); + // A stray Block_End, an unterminated block, a division by zero. + assert!(matches!(run(&[(172, &[21])], &d).0, Err(PrcError::Malformed(_)))); + assert!(matches!(run(&[(172, &[19, 0])], &d).0, Err(PrcError::Malformed(_)))); + assert!(matches!(run(&[(172, &[23, 0, 30, 26, 1, 26, 0])], &d).0, Err(PrcError::Malformed(_)))); + } + + /// Every program run on one reader draws on one step allowance. + #[test] + fn programs_on_one_stream_share_the_step_allowance() { + // For (2^19) { empty block }: about 2^20 steps, so the 2^22 allowance fits three runs. + let bytes = schema(&[(172, &[15, 26, 1 << 19, 19, 21])]).bytes(); + let mut r = BitReader::new(&bytes); + let s = Schema::read(&mut r).unwrap(); + let runs = (0..5).take_while(|_| s.skip_added_fields(172, &mut r).is_ok()).count(); + assert_eq!(runs, 3); + assert!(matches!(s.skip_added_fields(172, &mut r), Err(PrcError::Malformed(m)) if m.contains("step ceiling"))); + } + + #[test] + fn a_duplicated_entity_type_is_malformed() { + let bytes = schema(&[(172, &[3]), (172, &[3])]).bytes(); + assert!(matches!(Schema::read(&mut BitReader::new(&bytes)), Err(PrcError::Malformed(_)))); + } + + #[test] + fn an_entry_count_past_the_data_is_refused_before_any_entry_is_read() { + let mut w = W::default(); + w.uint(1_000).uint(172).uint(0); + let bytes = w.bytes(); + assert_eq!(Schema::read(&mut BitReader::new(&bytes)).unwrap_err(), PrcError::Truncated("the schema")); + } +} diff --git a/crates/prc/src/tess.rs b/crates/prc/src/tess.rs new file mode 100644 index 0000000000..c9bffb30f4 --- /dev/null +++ b/crates/prc/src/tess.rs @@ -0,0 +1,440 @@ +//! The tessellation section: `FileStructureTessellation` (305) and its entities, `TESS_3D` +//! (172) with its `TESS_Face`s (174), `TESS_3D_Wire` (175), `TESS_Markup` (176) and +//! `TESS_3D_Compressed` (173) [WD 7.3.7, 7.8]. +//! +//! `TESS_3D_Compressed` is read field by field to its end [WD 7.8.9.7] so the entities after +//! it decode; its triangles are not rebuilt. +//! +//! The schema runs after the fields of each concrete type read here (201, 172, 173, 174, 175, +//! 176, 305), not for the abstract levels (`PRCBase`, `TESS`). + +mod triangulate; + +use crate::arrays; +use crate::bits::{BitReader, Budget}; +use crate::entity::{Ctx, malformed}; +use crate::{FileStructure, PrcError, SectionKind}; +use triangulate::{Corners, Face, triangle_elements}; + +const FILE_STRUCTURE_TESSELLATION: u32 = 305; +const TESS_3D: u32 = 172; +const TESS_3D_COMPRESSED: u32 = 173; +const TESS_FACE: u32 = 174; +const TESS_3D_WIRE: u32 = 175; +const TESS_MARKUP: u32 = 176; + +/// prc-rs: `origin` exists in a compressed tessellation from this version (pdf-issues #705). +const ORIGIN_FROM: u32 = 7031; +/// prc-rs: `has_loops` exists from this file-structure authoring version. +const HAS_LOOPS_FROM: u32 = 7039; +/// prc-rs: `must_recalculate_normals` exists from this version. +const RECALCULATE_FROM: u32 = 7047; + +/// A wire header's count bits; the top four are flags [WD 7.8.7.5]. +const WIRE_COUNT_MASK: u32 = 0x0FFF_FFFF; +const WIRE_IS_CLOSING: u32 = 0x1000_0000; +const WIRE_IS_CONTINUOUS: u32 = 0x2000_0000; + +/// Normal angles wider than this are not a `ShortArray` [WD 7.8.9.7]. +const MAX_ANGLE_BITS: u32 = 16; + +/// One entry of the tessellation array; a representation item's `index_tessellation` selects +/// one by position. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum Tessellation { + /// A `TESS_3D` triangle mesh. + Mesh(TriangleMesh), + /// A `TESS_3D_Wire`: polylines. + Wire(Vec>), + /// A `TESS_Markup` (PMI drawing), read past but not decoded. + Markup, + /// A `TESS_3D_Compressed` mesh, read past but not rebuilt. + #[non_exhaustive] + Compressed { + /// The entity's triangle count. + triangles: usize, + }, +} + +/// A `TESS_3D` decoded to indexed triangles, in the entity's own frame (the representation +/// item's placement is not applied). +#[derive(Debug, Clone, Default, PartialEq)] +#[non_exhaustive] +pub struct TriangleMesh { + /// The coordinate array as points. + pub positions: Vec<[f64; 3]>, + /// Counter-clockwise triangles seen from the outside, as indices into + /// [`Self::positions`] [WD 7.8.5.2]. + pub triangles: Vec<[u32; 3]>, + /// `triangles` split by `TESS_Face`: face `i` is `triangles[faces[i].clone()]`. + pub faces: Vec>, + /// The producer asks the reader to compute normals (none are stored). + pub normals_recalculated: bool, + /// The stored vertex normals [WD 7.8.5.1], as written (not renormalised); empty when the + /// mesh stores none or any of its normal indices is unusable. + pub normals: Vec<[f64; 3]>, + /// Per triangle, the index into [`Self::normals`] of each corner's normal, in the corner + /// order of [`Self::triangles`]; empty exactly when [`Self::normals`] is. + pub triangle_normals: Vec<[u32; 3]>, + /// Per triangle, the style its face's line attributes give it [WD 7.8.6]; empty when no + /// face carries any. + pub triangle_styles: Vec, + /// The stored texture coordinates as (u, v) pairs [WD 7.8.5.1]; empty when the mesh + /// stores none. + pub uvs: Vec<[f64; 2]>, + /// Per texture-coordinate set, per triangle, the index into [`Self::uvs`] of each + /// corner's coordinate, in the corner order of [`Self::triangles`]; each set is as long + /// as [`Self::triangles`], `None` for a triangle whose face stores no coordinate for the + /// set. Empty when [`Self::uvs`] is. + pub triangle_uvs: Vec>>, +} + +/// The graphics a face's `line_attributes` give one triangle [WD 7.8.6]. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct TriangleStyle { + /// An index into the globals' style array plus one; 0 keeps the owner's graphics. + pub style: u32, + /// The face's behaviour bits [WD 5.4]. + pub behaviour: u16, +} + +impl FileStructure { + /// The tessellation section's entities, in the order a representation item's + /// `index_tessellation` counts them [WD 7.3.7]. + /// + /// # Errors + /// As [`crate::Schema::read`] for the globals' schema; [`PrcError::Truncated`] or + /// [`PrcError::Malformed`] for a damaged section; [`PrcError::Unsupported`] for + /// optimised vertex colours. + pub fn tessellations(&self) -> Result, PrcError> { + self.tessellations_within(Budget::FULL) + } + + /// As [`Self::tessellations`], the globals' schema and the section sharing `budget`. + pub(crate) fn tessellations_within(&self, budget: Budget) -> Result, PrcError> { + let mut globals = BitReader::with_budget(self.section(SectionKind::Globals), budget); + let schema = crate::Schema::read(&mut globals)?; + let r = BitReader::with_budget(self.section(SectionKind::Tessellation), globals.budget()); + Ctx::new(r, &schema, self.authoring_version).file_structure_tessellation() + } +} + +impl Ctx<'_, '_> { + /// `FileStructureTessellation` (305) [WD 7.3.7]. + pub(crate) fn file_structure_tessellation(&mut self) -> Result, PrcError> { + self.expect_type(FILE_STRUCTURE_TESSELLATION)?; + self.content_prc_base()?; + let n = self.count(1, "tessellations")?; + let mut out = Vec::with_capacity(n.min(1024)); + for _ in 0..n { + out.push(match self.r.unsigned_integer()? { + TESS_3D => Tessellation::Mesh(self.tess_3d()?), + TESS_3D_WIRE => Tessellation::Wire(self.wire()?), + TESS_MARKUP => { + self.markup()?; + Tessellation::Markup + } + TESS_3D_COMPRESSED => Tessellation::Compressed { triangles: self.tess_3d_compressed()? }, + t => return Err(malformed(format!("entity type {t} in the tessellation array"))), + }); + } + self.schema.skip_added_fields(FILE_STRUCTURE_TESSELLATION, &mut self.r)?; + self.user_data()?; + Ok(out) + } + + /// `ContentBaseTessData` [WD 7.8.4]: `is_calculated`, then coordinates. + fn base_tess_data(&mut self) -> Result, PrcError> { + self.r.bit()?; + self.doubles("tessellation coordinates") + } + + /// `VertexColors` [WD 7.8.7.2] for `count` points, read past. + fn vertex_colors(&mut self, count: usize, in_wire: bool) -> Result<(), PrcError> { + let rgba = self.r.bit()?; + // prc-rs: `is_segment_color` exists only inside a wire. + let per_segment = in_wire && self.r.bit()?; + if self.r.bit()? { + return Err(PrcError::Unsupported("optimised vertex colours")); + } + let count = if per_segment { count / 2 } else { count }; + let bytes = if rgba { 4 } else { 3 }; + // The first colour is `8 * bytes` bits; every later one is at least its reuse bit. + if count > 0 && (count - 1).saturating_add(8 * bytes) > self.r.remaining() { + return Err(PrcError::Truncated("vertex colours")); + } + for i in 0..count { + if i == 0 || !self.r.bit()? { + for _ in 0..bytes { + self.r.character()?; + } + } + } + Ok(()) + } + + /// `TESS_3D` (172) after its type code [WD 7.8.5]. + fn tess_3d(&mut self) -> Result { + let coords = self.base_tess_data()?; + let _has_faces = self.r.bit()?; + if self.version >= HAS_LOOPS_FROM { + let _has_loops = self.r.bit()?; + } + let recalc = self.version >= RECALCULATE_FROM && self.r.bit()?; + if recalc { + let _flags = self.r.character()?; + let _crease_angle = self.r.double()?; + } + let normal_coords = self.doubles("normal coordinates")?; + let _wire_indices = self.uints("wire indices")?; + let indices = self.uints("triangulated indices")?; + let n_faces = self.count(1, "faces")?; + let mut faces = Vec::with_capacity(n_faces.min(1024)); + for _ in 0..n_faces { + self.expect_type(TESS_FACE)?; + faces.push(self.face()?); + } + let texture = self.doubles("texture coordinates")?; + self.schema.skip_added_fields(TESS_3D, &mut self.r)?; + + let mut mesh = TriangleMesh { positions: points(&coords)?, normals_recalculated: recalc, ..TriangleMesh::default() }; + let styled = faces.iter().any(|f| !f.styles.is_empty()); + let mut corners = Corners::default(); + // Every triangle carries a corner triple for each texture set any face uses. + let sets = faces.iter().map(Face::texture_sets).max().unwrap_or(0); + let per_triangle = triangle_elements(!recalc, sets, styled); + for f in &faces { + // Faces may reuse one index range, so each face's triangles, with what they + // carry, are charged before they are made. + self.r.take_elements(f.triangle_count()?.saturating_mul(per_triangle))?; + let start = mesh.triangles.len(); + let entities = f.triangulate(&indices, !recalc, mesh.positions.len(), &mut mesh.triangles, &mut corners)?; + mesh.faces.push(start..mesh.triangles.len()); + if styled { + f.styles_into(&entities, &mut mesh.triangle_styles); + } + } + (mesh.normals, mesh.triangle_normals) = stored_normals(&normal_coords, &corners.normals, mesh.triangles.len()); + (mesh.uvs, mesh.triangle_uvs) = stored_uvs(&texture, corners.textures); + Ok(mesh) + } + + /// `TESS_Face` (174) after its type code [WD 7.8.6]. + fn face(&mut self) -> Result { + let styles = self.uints("face line attributes")?; + let _start_of_wire = self.r.unsigned_integer()?; + let _sizes_wire = self.uints("face wire sizes")?; + let flags = self.r.unsigned_integer()?; + let start = self.r.unsigned_integer()? as usize; + let data = self.uints("triangulated data")?; + let textures = self.r.unsigned_integer()? as usize; + let mut face = Face { flags, start, data, textures, styles, behaviour: 0 }; + if self.r.bit()? { + let n = face.point_count()?; + self.vertex_colors(n, false)?; + } + if !face.styles.is_empty() { + let bits = self.r.unsigned_integer()?; + face.behaviour = u16::try_from(bits).map_err(|_| malformed(format!("face behaviour {bits} is wider than 16 bits")))?; + } + self.schema.skip_added_fields(TESS_FACE, &mut self.r)?; + Ok(face) + } + + /// `TESS_3D_Wire` (175) after its type code [WD 7.8.7]. + fn wire(&mut self) -> Result>, PrcError> { + let pts = points(&self.base_tess_data()?)?; + let words = self.uints("wire indices")?; + let mut wires: Vec> = Vec::new(); + let mut colours = 0usize; + if words.is_empty() { + colours = pts.len(); + if !pts.is_empty() { + wires.push(pts.clone()); + } + } + let mut it = words.iter().copied(); + while let Some(header) = it.next() { + let n = (header & WIRE_COUNT_MASK) as usize; + let mut wire = Vec::with_capacity(n.min(words.len())); + for _ in 0..n { + let idx = it.next().ok_or_else(|| malformed("wire runs past its indices".into()))?; + wire.push(point_at(&pts, idx)?); + } + colours = colours.saturating_add(n); + if header & WIRE_IS_CLOSING != 0 { + colours = colours.saturating_add(1); + if let Some(&first) = wire.first() { + wire.push(first); + } + } + match wires.last_mut() { + Some(prev) if header & WIRE_IS_CONTINUOUS != 0 => prev.extend(wire), + _ => wires.push(wire), + } + } + if self.r.bit()? { + self.vertex_colors(colours, true)?; + } + self.schema.skip_added_fields(TESS_3D_WIRE, &mut self.r)?; + Ok(wires) + } + + /// `TESS_Markup` (176) after its type code [WD 7.8.8], read past. + fn markup(&mut self) -> Result<(), PrcError> { + self.base_tess_data()?; + self.uints("markup codes")?; + let n = self.count(1, "markup strings")?; + for _ in 0..n { + self.r.string()?; + } + self.r.string()?; + self.r.character()?; + self.schema.skip_added_fields(TESS_MARKUP, &mut self.r) + } + + /// `TESS_3D_Compressed` (173) after its type code [WD 7.8.9.7], read past; returns its + /// triangle count. prc-rs: the field order and version gates follow its schema. + /// + /// Sizes the WD leaves unstated: the face count is the largest face index + 1, + /// `normal_is_reversed` is one bit per triangle, and the point-reference array's + /// compressed flag is implicit (more than three references). + fn tess_3d_compressed(&mut self) -> Result { + let r = &mut self.r; + r.bit()?; // is_calculated + r.bit()?; // has_faces + r.double()?; // tolerance + if self.version >= ORIGIN_FROM { + for _ in 0..3 { + r.float_as_bytes()?; + } + } + arrays::compressed_integer_array(r)?; // point_array + let edge_status = arrays::character_array(r, 2, None, false)?; + let face_of = arrays::compressed_indice_array(r, None)?; + let t = face_of.len(); + if edge_status.len() != t && edge_status.len() != t.saturating_mul(3) { + return Err(malformed(format!("{} edge statuses for {t} triangles", edge_status.len()))); + } + let faces = face_of.iter().max().map_or(0, |&m| (m as usize).saturating_add(1)); + let n = r.unsigned_integer()? as usize; + let references = arrays::bool_array(r, n)?.iter().filter(|&&b| b).count(); + let refs = arrays::compressed_indice_array(r, Some(references > 3))?; + if refs.len() != references { + return Err(malformed(format!("{} point references for {references} flagged points", refs.len()))); + } + self.compressed_normals(t, faces)?; + let r = &mut self.r; + if r.bit()? { + arrays::bool_array(r, faces)?; // is_point_color_on_face + arrays::character_array(r, 8, None, false)?; // point_color_array + } + if r.bit()? { + arrays::bool_array(r, faces)?; // is_multiple_line_attribute_on_face + } + arrays::short_array(r, 16)?; // line_attributes + if !r.bit()? { + self.compressed_texture_parameter()?; + if !self.r.bit()? { + arrays::bool_array(&mut self.r, faces)?; // face_has_texture + } + } + if self.r.bit()? { + arrays::character_array(&mut self.r, 8, None, false)?; // behaviours + } + self.schema.skip_added_fields(TESS_3D_COMPRESSED, &mut self.r)?; + Ok(t) + } + + /// A compressed tessellation's normals, from `must_recalculate_normals` on, read past. + fn compressed_normals(&mut self, triangles: usize, faces: usize) -> Result<(), PrcError> { + let r = &mut self.r; + if r.bit()? { + arrays::bool_array(r, triangles)?; // normal_is_reversed + r.double()?; // crease_angle + r.character()?; // normal_recalculation_flags + return Ok(()); + } + let angle_bits = u32::from(r.character()?); + if angle_bits > MAX_ANGLE_BITS { + return Err(malformed(format!("{angle_bits}-bit normal angles"))); + } + let n = r.unsigned_integer()? as usize; + arrays::bool_array(r, n)?; // is_binary_normal + arrays::short_array(r, angle_bits)?; // normal angles + arrays::bool_array(r, faces)?; // is_face_planar + Ok(()) + } + + /// `CompressedTextureParameter` [WD 7.8.9.8-7.8.9.9; pdf-issues #729, #749], read past. + fn compressed_texture_parameter(&mut self) -> Result<(), PrcError> { + let words = self.count(32, "texture data")?; + // count() bounded words * 32 by the remaining bits. + self.r.skip_bits(words.saturating_mul(32))?; + self.r.unsigned_integer()?; // last_integer_used_bit_number + let n = self.count(5, "texture references")?; + for _ in 0..n { + self.r.nbits_then_unsigned()?; + } + self.r.double()?; // tolerance + let n = self.count(32, "texture parameters")?; + for _ in 0..n { + self.r.float_as_bytes()?; + } + Ok(()) + } +} + +/// The normal array and each triangle's corner normals, or both empty when any slot fails to +/// name a normal: a mesh shades from its stored normals wholly or not at all. +fn stored_normals(coords: &[f64], slots: &[[u32; 3]], triangles: usize) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) { + // Like a slot naming no normal, a partial last normal drops the stored normals. + let Ok(normals) = points(coords) else { + return (Vec::new(), Vec::new()); + }; + let named = |k: u32| k.is_multiple_of(3) && (k as usize / 3) < normals.len(); + if normals.is_empty() || slots.len() != triangles || !slots.iter().flatten().all(|&k| named(k)) { + return (Vec::new(), Vec::new()); + } + let corners = slots.iter().map(|t| t.map(|k| k / 3)).collect(); + (normals, corners) +} + +/// Per texture-coordinate set, each triangle's corner pair indices. +type UvSets = Vec>>; + +/// The texture coordinates as (u, v) pairs, and per set the corner slots turned into pair +/// indices (a slot is a `Double` offset, so even); a triangle with a slot naming no pair is +/// `None`. +fn stored_uvs(coords: &[f64], sets: UvSets) -> (Vec<[f64; 2]>, UvSets) { + let uvs: Vec<[f64; 2]> = coords.as_chunks::<2>().0.to_vec(); + if uvs.is_empty() { + return (Vec::new(), Vec::new()); + } + let pair = |k: u32| (k.is_multiple_of(2) && (k as usize / 2) < uvs.len()).then_some(k / 2); + let sets = + sets.into_iter().map(|set| set.into_iter().map(|tri| tri.and_then(|[a, b, c]| Some([pair(a)?, pair(b)?, pair(c)?]))).collect()).collect(); + (uvs, sets) +} + +/// `coords` as points; an array that does not divide into whole points is malformed. +fn points(coords: &[f64]) -> Result, PrcError> { + let (whole, rest) = coords.as_chunks::<3>(); + if !rest.is_empty() { + return Err(malformed(format!("{} coordinates are not whole points", coords.len()))); + } + Ok(whole.to_vec()) +} + +/// The point a `Double`-offset index names [WD 7.8.5.2: multiples of 3]. +fn point_at(pts: &[[f64; 3]], idx: u32) -> Result<[f64; 3], PrcError> { + if !idx.is_multiple_of(3) { + return Err(malformed(format!("point index {idx} is not a multiple of 3"))); + } + pts.get(idx as usize / 3).copied().ok_or_else(|| malformed(format!("point index {idx} past the coordinates"))) +} + +#[cfg(test)] +pub(crate) mod tests; diff --git a/crates/prc/src/tess/tests.rs b/crates/prc/src/tess/tests.rs new file mode 100644 index 0000000000..26219f2865 --- /dev/null +++ b/crates/prc/src/tess/tests.rs @@ -0,0 +1,738 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use super::triangulate::MAX_TEXTURE_SETS; +use super::*; +use crate::Schema; +use crate::testw::W; + +const SQUARE: [f64; 12] = [0., 0., 0., 1., 0., 0., 1., 1., 0., 0., 1., 0.]; +const ATTRIBUTE: u32 = 201; +const NORMAL_SINGLE: u32 = 0x4000_0000; + +/// A 305 section holding `body` (already-written entities, `n` of them). +fn section(n: u32, body: &W) -> W { + let mut w = W::default(); + w.uint(305).uint(0).bit(true).uint(n).append(body).uint(0); + w +} + +fn uints(w: &mut W, v: &[u32]) { + w.uint(u32::try_from(v.len()).unwrap()); + for &x in v { + w.uint(x); + } +} + +fn square(w: &mut W, entity: u32) { + coordinates(w, entity, &SQUARE); +} + +fn coordinates(w: &mut W, entity: u32, coords: &[f64]) { + w.uint(entity).bit(false).uint(u32::try_from(coords.len()).unwrap()); + for &c in coords { + w.double(c); + } +} + +#[derive(Default)] +struct Mesh<'a> { + recalc: bool, + indices: &'a [u32], + faces: &'a [(u32, u32, &'a [u32])], + textures: u32, + uvs: &'a [f64], + /// The coordinates; [`SQUARE`] when `None`. + coords: Option<&'a [f64]>, +} + +/// A TESS_3D over [`SQUARE`] at authoring version `v`. +fn tess_3d(w: &mut W, v: u32, m: &Mesh<'_>) { + coordinates(w, 172, m.coords.unwrap_or(&SQUARE)); + w.bit(true); + if v >= 7039 { + w.bit(false); + } + if v >= 7047 { + w.bit(m.recalc); + if m.recalc { + w.put(0, 8).double(0.5); + } + } + if m.recalc { + w.uint(0); + } else { + // Two normals: slot 0 is +z, slot 3 is -z. + w.uint(6); + for c in [0., 0., 1., 0., 0., -1.] { + w.double(c); + } + } + w.uint(0); + uints(w, m.indices); + w.uint(u32::try_from(m.faces.len()).unwrap()); + for &(flags, start, data) in m.faces { + w.uint(174).uint(0).uint(0).uint(0).uint(flags).uint(start); + uints(w, data); + w.uint(m.textures).bit(false); + } + w.uint(u32::try_from(m.uvs.len()).unwrap()); + for &c in m.uvs { + w.double(c); + } +} + +fn decode(w: &W, schema: &Schema, version: u32) -> Result, PrcError> { + let bytes = w.bytes(); + Ctx::new(BitReader::new(&bytes), schema, version).file_structure_tessellation() +} + +fn mesh(t: &Tessellation) -> &TriangleMesh { + match t { + Tessellation::Mesh(m) => m, + other => panic!("not a mesh: {other:?}"), + } +} + +fn one(indices: &[u32], faces: &[(u32, u32, &[u32])], textures: u32) -> TriangleMesh { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices, faces, textures, ..Mesh::default() }); + mesh(&decode(§ion(1, &b), &Schema::default(), 8137).unwrap()[0]).clone() +} + +#[test] +fn triangle_and_fan_with_normals() { + // One triangle (n,p x3), then a fan of 4 vertices (n,p each). + let m = one(&[3, 0, 0, 3, 3, 6, 0, 0, 3, 3, 0, 6, 3, 9], &[(0x2 | 0x4, 0, &[1, 1, 4])], 0); + assert_eq!(m.positions.len(), 4); + assert_eq!(m.positions[2], [1., 1., 0.]); + assert_eq!(m.triangles, [[0, 1, 2], [0, 1, 2], [0, 2, 3]]); + assert_eq!(m.faces, vec![0..3]); + assert!(!m.normals_recalculated); + assert_eq!(m.normals, [[0., 0., 1.], [0., 0., -1.]]); + // Each corner keeps the normal stored beside its point. + assert_eq!(m.triangle_normals, [[1, 0, 1], [0, 1, 0], [0, 0, 1]]); +} + +#[test] +fn strip_normals_follow_the_alternating_winding() { + let m = one(&[3, 0, 0, 3, 3, 6, 0, 9], &[(0x8, 0, &[1, 4])], 0); + assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3]]); + assert_eq!(m.triangle_normals, [[1, 0, 1], [1, 0, 0]]); +} + +#[test] +fn a_normal_index_past_the_array_drops_every_normal_but_no_triangle() { + let m = one(&[0, 0, 6, 3, 0, 6], &[(0x2, 0, &[1])], 0); + assert_eq!(m.triangles, [[0, 1, 2]]); + assert!(m.normals.is_empty()); + assert!(m.triangle_normals.is_empty()); +} + +#[test] +fn recalculated_normals_drop_the_normal_slots_and_strips_alternate() { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { recalc: true, indices: &[0, 3, 6, 9], faces: &[(0x8, 0, &[1, 4])], ..Mesh::default() }); + let t = decode(§ion(1, &b), &Schema::default(), 8137).unwrap(); + let m = mesh(&t[0]); + assert!(m.normals_recalculated); + assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3]]); + assert!(m.normals.is_empty() && m.triangle_normals.is_empty()); +} + +#[test] +fn version_gates_the_loop_and_recalculation_flags() { + for v in [7038, 7039, 7046, 7047, 8137] { + let mut b = W::default(); + tess_3d(&mut b, v, &Mesh { indices: &[0, 0, 0, 3, 0, 6], faces: &[(0x2, 0, &[1])], ..Mesh::default() }); + // A trailing wire: a misread flag desyncs it. + wire(&mut b, false); + let t = decode(§ion(2, &b), &Schema::default(), v).unwrap(); + assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2]], "version {v}"); + assert!(matches!(t[1], Tessellation::Wire(_)), "version {v}"); + } +} + +#[test] +fn one_normal_blocks_and_textures() { + // 0x20: n,p,p,p. 0x40 with NORMAL_Single: n then p per vertex. + // 0x2000 with one texture set: n,t,p,t,p,t,p. + let idx = [ + 0, 0, 3, 6, // 0x20 + 3, 0, 6, 9, // 0x40 single, 3 vertices + 0, 0, 9, 2, 0, 4, 3, // 0x2000 + ]; + let m = one(&idx, &[(0x20 | 0x40 | 0x2000, 0, &[1, 1, 3 | NORMAL_SINGLE, 1])], 1); + assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3], [3, 0, 1]]); + // A one-normal entity gives every corner its single normal. + assert_eq!(m.triangle_normals, [[0, 0, 0], [1, 1, 1], [0, 0, 0]]); +} + +#[test] +fn one_normal_fan_without_single_keeps_a_normal_per_vertex() { + let m = one(&[0, 0, 0, 3, 0, 6, 0, 9], &[(0x40, 0, &[1, 4])], 0); + assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3]]); +} + +#[test] +fn textured_fan_and_strip_carry_texture_slots_per_vertex() { + // 0x400 then 0x800, two texture sets: n,t,t,p per vertex. + let mut idx = Vec::new(); + for p in [0, 3, 6, 0, 3, 6, 9] { + idx.extend([0, 0, 2, p]); + } + let m = one(&idx, &[(0x400 | 0x800, 0, &[1, 3, 1, 4])], 2); + assert_eq!(m.triangles, [[0, 1, 2], [0, 1, 2], [2, 1, 3]]); +} + +/// The unit square as two textured triangles (one set; per vertex a normal, a texture slot, +/// a point), its corners at the picture's corners. +fn textured_square() -> W { + let mut body = W::default(); + let indices = [0, 0, 0, 0, 2, 3, 0, 4, 6, 0, 0, 0, 0, 4, 6, 0, 6, 9]; + tess_3d( + &mut body, + 8137, + &Mesh { indices: &indices, faces: &[(0x200, 0, &[2])], textures: 1, uvs: &[0., 0., 1., 0., 1., 1., 0., 1.], ..Mesh::default() }, + ); + section(1, &body) +} + +#[test] +fn a_textured_mesh_keeps_each_corners_coordinates() { + let t = decode(&textured_square(), &Schema::default(), 8137).unwrap(); + let m = mesh(&t[0]); + assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3]]); + assert_eq!(m.uvs, [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]); + assert_eq!(m.triangle_uvs, [vec![Some([0, 1, 2]), Some([0, 2, 3])]]); + // An odd slot names no pair: that triangle has no coordinates. + let mut b = W::default(); + tess_3d( + &mut b, + 8137, + &Mesh { indices: &[0, 1, 0, 0, 2, 3, 0, 4, 6], faces: &[(0x200, 0, &[1])], textures: 1, uvs: &[0.0; 6], ..Mesh::default() }, + ); + let t = decode(§ion(1, &b), &Schema::default(), 8137).unwrap(); + let m = mesh(&t[0]); + assert_eq!(m.triangles, [[0, 1, 2]]); + assert_eq!(m.triangle_uvs, [vec![None]]); +} + +#[test] +fn too_many_texture_sets_are_refused_before_anything_is_allocated() { + // An empty fan on a face claiming 2^32 - 1 texture sets: no slot bounds the count. + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &[0], faces: &[(0x400, 0, &[1, 0])], textures: u32::MAX, ..Mesh::default() }); + assert!( + matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(m)) if m == format!("{} texture-coordinate sets on a face", u32::MAX)) + ); + let sets = u32::try_from(MAX_TEXTURE_SETS).unwrap(); + let mut ok = W::default(); + tess_3d(&mut ok, 8137, &Mesh { indices: &[0], faces: &[(0x400, 0, &[1, 0])], textures: sets, ..Mesh::default() }); + assert!(decode(§ion(1, &ok), &Schema::default(), 8137).is_ok()); +} + +/// A tessellation section holding one TESS_3D: [`SQUARE`] as two faces of one triangle each. +pub(crate) fn two_face_square() -> W { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9], faces: &[(0x2, 0, &[1]), (0x2, 6, &[1])], ..Mesh::default() }); + section(1, &b) +} + +#[test] +fn two_faces_partition_the_triangles() { + let m = one(&[0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9], &[(0x2, 0, &[1]), (0x2, 6, &[1])], 0); + assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3]]); + assert_eq!(m.faces, [0..1, 1..2]); +} + +/// A wire over [`SQUARE`]: an open two-point wire continued by a closing two-point wire, +/// then optionally per-segment RGB colours. +fn wire(w: &mut W, colours: bool) { + square(w, 175); + uints(w, &[2, 0, 3, WIRE_IS_CONTINUOUS | WIRE_IS_CLOSING | 2, 6, 9]); + w.bit(colours); + if colours { + // 5 points (4 + the implicit closing one) -> 2 segment colours. + w.bit(false).bit(true).bit(false).put(0xff0000, 24); + w.bit(true); + } +} + +#[test] +fn wires_close_and_continue_and_colours_are_counted() { + for colours in [false, true] { + let mut b = W::default(); + wire(&mut b, colours); + // A second entity: a wrong colour count would desync it. + wire(&mut b, false); + let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap(); + for e in &t { + let Tessellation::Wire(w) = e else { panic!("not a wire") }; + assert_eq!(w, &[vec![[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.], [1., 1., 0.]]]); + } + } +} + +#[test] +fn a_closing_wire_colours_its_implicit_point() { + // Per-point RGB: 4 indexed points + 1 implicit = 5 colours. + let mut b = W::default(); + square(&mut b, 175); + uints(&mut b, &[2, 0, 3, WIRE_IS_CONTINUOUS | WIRE_IS_CLOSING | 2, 6, 9]); + b.bit(true).bit(false).bit(false).bit(false).put(1, 24); + for _ in 1..5 { + b.bit(true); + } + wire(&mut b, false); + let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap(); + assert!(matches!(t[1], Tessellation::Wire(_))); + assert_eq!(t[0], t[1]); +} + +#[test] +fn a_wire_index_must_name_a_point() { + let mut b = W::default(); + square(&mut b, 175); + uints(&mut b, &[2, 0, 12]); + b.bit(false); + assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(_)))); +} + +#[test] +fn a_wire_without_indices_is_the_coordinate_polyline() { + let mut b = W::default(); + square(&mut b, 175); + b.uint(0).bit(true); + // 4 implicit points -> 4 RGB colours, all "same" after the first. + b.bit(false).bit(false).bit(false).put(1, 24); + for _ in 1..4 { + b.bit(true); + } + wire(&mut b, false); + let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap(); + let Tessellation::Wire(w) = &t[0] else { panic!("not a wire") }; + assert_eq!(w.len(), 1); + assert_eq!(w[0].len(), 4); + assert!(matches!(t[1], Tessellation::Wire(_))); +} + +/// Reused colours cost one bit each, so a long run of them may end the section. +#[test] +fn reused_vertex_colours_may_be_the_last_data() { + let n = 40; + let mut b = W::default(); + b.uint(175).bit(false).uint(3 * n); + for i in 0..3 * n { + b.double(f64::from(i)); + } + // No indices: the polyline of all `n` points, one RGB colour then `n - 1` reuses. + b.uint(0).bit(true).bit(false).bit(false).bit(false).put(1, 24); + for _ in 1..n { + b.bit(true); + } + let t = decode(§ion(1, &b), &Schema::default(), 8137).unwrap(); + let Tessellation::Wire(w) = &t[0] else { panic!("not a wire") }; + assert_eq!(w[0].len(), n as usize); +} + +/// A square as one triangle plus one 4-vertex fan (two entities, three triangles), the face +/// carrying `attrs` and, when it has any, behaviour 0x30; then `colours` per-point colours +/// and a trailing wire. +fn styled_face(attrs: &[u32], colours: usize) -> Vec { + decode(&styled_face_section(attrs, colours, 0x30), &Schema::default(), 8137).unwrap() +} + +/// [`styled_face`]'s section, the face's behaviour `behaviour`. +fn styled_face_section(attrs: &[u32], colours: usize, behaviour: u32) -> W { + let mut b = W::default(); + square(&mut b, 172); + b.bit(true).bit(false).bit(false).uint(0).uint(0); + uints(&mut b, &[0, 0, 0, 3, 0, 6, 0, 0, 0, 3, 0, 6, 0, 9]); + b.uint(1).uint(174); + uints(&mut b, attrs); + b.uint(0).uint(0).uint(0x2 | 0x4).uint(0); + uints(&mut b, &[1, 1, 4]); + b.uint(0).bit(colours > 0); + if colours > 0 { + b.bit(false).bit(false).put(0x10_2030, 24); + for _ in 1..colours { + b.bit(true); + } + } + if !attrs.is_empty() { + b.uint(behaviour); + } + b.uint(0); + wire(&mut b, false); + section(2, &b) +} + +#[test] +fn face_vertex_colours_count_points_not_data() { + // A triangle plus a fan of 4 vertices = 7 points -> 7 colours. + let t = styled_face(&[], 7); + assert_eq!(mesh(&t[0]).triangles.len(), 3); + assert!(matches!(t[1], Tessellation::Wire(_))); +} + +fn styles(attrs: &[u32]) -> Vec<(u32, u16)> { + let t = styled_face(attrs, 0); + assert!(matches!(t[1], Tessellation::Wire(_))); + mesh(&t[0]).triangle_styles.iter().map(|g| (g.style, g.behaviour)).collect() +} + +#[test] +fn face_line_attributes_style_its_triangles() { + assert!(styles(&[]).is_empty()); + assert_eq!(styles(&[5]), [(5, 0x30); 3]); + // One per entity: the triangle, then both triangles of the fan. + assert_eq!(styles(&[4, 7]), [(4, 0x30), (7, 0x30), (7, 0x30)]); + // An entity styled 0 inherits, as do entities past the list. + assert_eq!(styles(&[4, 0, 9]), [(4, 0x30), (0, 0x30), (0, 0x30)]); +} + +#[test] +fn markup_is_read_past() { + let mut b = W::default(); + b.uint(176).bit(false).uint(3); + for c in [1., 2., 3.] { + b.double(c); + } + uints(&mut b, &[6, 0]); + b.uint(1).string(Some("R1")).string(Some("label")).put(7, 8); + wire(&mut b, false); + let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap(); + assert_eq!(t[0], Tessellation::Markup); + assert!(matches!(t[1], Tessellation::Wire(_))); +} + +#[test] +fn schema_fields_are_skipped_after_each_entity() { + // A newer producer added one UnsignedInteger to TESS_3D and one to TESS_Face. + let mut s = W::default(); + s.uint(2).uint(172).uint(1).uint(3).uint(174).uint(1).uint(3); + let sb = s.bytes(); + let schema = Schema::read(&mut BitReader::new(&sb)).unwrap(); + + let mut b = W::default(); + square(&mut b, 172); + b.bit(true).bit(false).bit(false).uint(0).uint(0); + uints(&mut b, &[0, 0, 0, 3, 0, 6]); + b.uint(1).uint(174).uint(0).uint(0).uint(0).uint(0x2).uint(0); + uints(&mut b, &[1]); + b.uint(0).bit(false).uint(99); // the face's added field + b.uint(0).uint(77); // texture count, then TESS_3D's added field + wire(&mut b, false); + let t = decode(§ion(2, &b), &schema, 8137).unwrap(); + assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2]]); + assert!(matches!(t[1], Tessellation::Wire(_))); + // Without the schema the same bytes do not decode to the same thing. + assert_ne!(decode(§ion(2, &b), &Schema::default(), 8137).ok(), Some(t)); +} + +#[test] +fn attributes_names_and_user_data_are_read_past() { + let mut w = W::default(); + w.uint(305).uint(1).uint(ATTRIBUTE); + w.bit(true).uint(2); // predefined title + w.uint(2); + w.bit(false).string(Some("k")).uint(4).string(Some("v")); + w.bit(true).uint(1).uint(5).int(-1).uint(9); + w.uint(42); // the attribute's schema-added field + w.bit(false).string(Some("tess")).uint(1); + wire(&mut w, false); + w.uint(5).put(0b10110, 5); + let mut sw = W::default(); + sw.uint(1).uint(ATTRIBUTE).uint(1).uint(3); + let sb = sw.bytes(); + let schema = Schema::read(&mut BitReader::new(&sb)).unwrap(); + let bytes = w.bytes(); + let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, 8137); + let t = ctx.file_structure_tessellation().unwrap(); + assert!(matches!(t[..], [Tessellation::Wire(_)])); + assert_eq!(ctx.name.as_deref(), Some("tess")); + assert_eq!(ctx.r.position(), w.len()); +} + +/// A one-triangle TESS_3D_Compressed. `full` takes every optional branch: stored normals, +/// colours, line attributes, texture, behaviours. Otherwise `three_t` picks the 3T edge form. +fn compressed(w: &mut W, v: u32, full: bool, three_t: bool) { + compressed_with_angles(w, v, full, three_t, 10); +} + +/// [`compressed`] whose `full` form stores `angle_bits`-bit normal angles. +fn compressed_with_angles(w: &mut W, v: u32, full: bool, three_t: bool, angle_bits: u8) { + w.uint(173).bit(false).bit(true).double(0.001); + if v >= 7031 { + w.put(0, 32).put(0, 32).put(0, 32); + } + // point_array: nine 2-bit values. + w.bit(false).uint(9); + for _ in 0..9 { + w.put(2, 8); + } + for _ in 0..9 { + w.bit(false).bit(true); + } + // edge_status_array: the 3T form (Huffman) or the T form. + if full { + w.bit(true).huffman(2, 2, &[(0, 0b10, 2)], &[0, 0, 0]); + } else if three_t { + w.bit(false).uint(3).put(0, 8).put(0, 8).put(0, 8); + } else { + w.bit(false).uint(1).put(0, 8); + } + // triangle_face_array [0]. `full`: four references, so the flagless reference array is + // Huffman; else none. + w.bit(false).uint(1).put(1, 8).bit(false); + if full { + w.uint(4).put(0b1111, 4); + w.huffman(6, 2, &[(1, 0b10, 2), (0, 0b11, 2)], &[1, 0, 0, 0]); + w.put(0, 4); + } else { + w.uint(3).put(0, 3).uint(0); + } + if full { + w.bit(false).put(u64::from(angle_bits), 8).uint(3).put(0b101, 3); + w.bit(true).huffman(10, 2, &[(5, 0b10, 2)], &[5, 5]); + w.bit(true); // is_face_planar + w.bit(true).bit(true).bit(false).uint(5); + for c in [1, 255, 0, 0, 128] { + w.put(c, 8); + } + w.bit(true).bit(false); + } else { + w.bit(true).bit(false).double(0.5).put(0, 8); + w.bit(false).bit(false); + } + w.bit(false).uint(1).put(1, 8).put(0, 8); // line_attribute_array + if full { + w.bit(false).uint(1).put(0, 32).uint(32); + w.uint(1).put(1, 5).bit(true).double(0.5).uint(2); + w.put(0, 32).put(0, 32).bit(false).bit(true); + w.bit(true).bit(false).uint(1).put(1, 8); + } else { + w.bit(true).bit(false); + } +} + +#[test] +fn a_compressed_mesh_is_read_to_its_end() { + for (full, three_t) in [(false, false), (false, true), (true, true)] { + for v in [7030, 7031, 8137] { + let mut b = W::default(); + compressed(&mut b, v, full, three_t); + let indices = [0, 0, 0, 3, 0, 6, 0, 0, 0, 3, 0, 6, 0, 9]; + tess_3d(&mut b, v, &Mesh { indices: &indices, faces: &[(0x2 | 0x4, 0, &[1, 1, 4])], ..Mesh::default() }); + let w = section(2, &b); + let bytes = w.bytes(); + let schema = Schema::default(); + let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, v); + let t = ctx.file_structure_tessellation().unwrap(); + assert!(matches!(t[..], [Tessellation::Compressed { triangles: 1, .. }, Tessellation::Mesh(_)]), "full={full} v={v}: {t:?}"); + assert_eq!(ctx.r.position(), w.len(), "full={full} v={v}"); + } + } +} + +#[test] +fn refusals_and_damage_are_errors() { + // Edge statuses neither T nor 3T long. + let mut c = W::default(); + c.uint(173).bit(false).bit(true).double(0.001); + c.put(0, 32).put(0, 32).put(0, 32); + c.bit(false).uint(0).bit(false).uint(2).put(0, 16); + c.bit(false).uint(1).put(1, 8).bit(false); + assert!(matches!(decode(§ion(1, &c), &Schema::default(), 8137), Err(PrcError::Malformed(_)))); + // Optimised vertex colours. + let mut o = W::default(); + square(&mut o, 175); + o.uint(0).bit(true).bit(false).bit(false).bit(true); + assert!(matches!(decode(§ion(1, &o), &Schema::default(), 8137), Err(PrcError::Unsupported(_)))); + // A point index that is not a multiple of 3, and one past the end. + for bad in [4, 12] { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0, 0, bad, 0, 6], faces: &[(0x2, 0, &[1])], ..Mesh::default() }); + assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(_)))); + } + // A fan whose count claims a billion points. + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0], faces: &[(0x4, 0, &[1, 0x3FFF_FFFF])], ..Mesh::default() }); + assert!(decode(§ion(1, &b), &Schema::default(), 8137).is_err()); + // A tessellation count far past the data. + let mut w = W::default(); + w.uint(305).uint(0).bit(true).uint(u32::MAX); + assert!(matches!(decode(&w, &Schema::default(), 8137), Err(PrcError::Truncated(_)))); + // A wrong entity type where the section's own belongs. + let mut w = W::default(); + w.uint(306); + assert!(matches!(decode(&w, &Schema::default(), 8137), Err(PrcError::Malformed(_)))); +} + +#[test] +fn every_truncation_of_a_section_is_an_error_not_a_panic() { + let mut b = W::default(); + compressed(&mut b, 8137, true, true); + wire(&mut b, true); + let bytes = section(2, &b).bytes(); + for end in 0..bytes.len() { + let cut = bytes.get(..end).unwrap(); + // The zero padding of the last byte may still decode; anything shorter must not. + let _ = Ctx::new(BitReader::new(cut), &Schema::default(), 8137).file_structure_tessellation(); + } + let cut = bytes.get(..bytes.len() / 2).unwrap(); + assert!(Ctx::new(BitReader::new(cut), &Schema::default(), 8137).file_structure_tessellation().is_err()); +} + +/// A one-file-structure PRC stream whose tessellation section is `tess`, with an empty +/// globals section schema. +fn prc_file(tess: &[u8]) -> Vec { + let mut globals = W::default(); + globals.uint(0); + prc_file_with(&globals.bytes(), tess) +} + +/// As [`prc_file`] with the globals section `globals`. +fn prc_file_with(globals: &[u8], tess: &[u8]) -> Vec { + use std::io::Write as _; + const HEADER_LEN: usize = 107; + let zlib = |data: &[u8]| { + let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + e.write_all(data).unwrap(); + e.finish().unwrap() + }; + let u32s = |out: &mut Vec, vs: &[u32]| vs.iter().for_each(|v| out.extend_from_slice(&v.to_le_bytes())); + let sections: [&[u8]; 5] = [globals, b"", tess, b"", b""]; + let mut body = b"PRC".to_vec(); + u32s(&mut body, &[8137, 8137, 5, 6, 7, 8, 0, 0, 0, 0, 0]); + let mut offs = vec![HEADER_LEN]; + for s in sections { + offs.push(HEADER_LEN + body.len()); + body.extend(zlib(s)); + } + let mf_start = HEADER_LEN + body.len(); + body.extend(zlib(b"")); + let mf_end = HEADER_LEN + body.len(); + let mut out = b"PRC".to_vec(); + u32s(&mut out, &[8137, 8137, 1, 2, 3, 4, 0, 0, 0, 0, 1]); + u32s(&mut out, &[5, 6, 7, 8, 0, 6]); + u32s(&mut out, &offs.iter().map(|&o| u32::try_from(o).unwrap()).collect::>()); + u32s(&mut out, &[u32::try_from(mf_start).unwrap(), u32::try_from(mf_end).unwrap(), 0]); + out.extend(body); + out +} + +#[test] +fn a_prc_file_yields_its_tessellations() { + let f = crate::PrcFile::parse(&prc_file(&textured_square().bytes())).unwrap(); + let t = f.file_structures[0].tessellations().unwrap(); + assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2], [0, 2, 3]]); +} + +#[test] +fn normal_angles_wider_than_16_bits_are_refused_by_name() { + let mut b = W::default(); + compressed_with_angles(&mut b, 8137, true, true, 17); + assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(m)) if m == "17-bit normal angles")); +} + +/// An entity's counted array is charged against the reader's element allowance. +#[test] +fn counted_entity_arrays_are_charged_against_the_allowance() { + let mut w = W::default(); + w.uint(3).uint(1).uint(2).uint(3); + let bytes = w.bytes(); + let schema = Schema::default(); + let read = |n| Ctx::new(BitReader::with_elements(&bytes, n), &schema, 8137).uints("indices"); + assert!(matches!(read(2), Err(PrcError::TooManyElements { .. }))); + assert_eq!(read(3).unwrap(), [1, 2, 3]); +} + +#[test] +fn a_face_behaviour_wider_than_16_bits_is_malformed() { + let r = decode(&styled_face_section(&[1], 0, 0x1_0030), &Schema::default(), 8137); + assert!(matches!(r, Err(PrcError::Malformed(m)) if m == "face behaviour 65584 is wider than 16 bits")); +} + +#[test] +fn coordinates_that_are_not_whole_points_are_malformed() { + let mut partial = SQUARE.to_vec(); + partial.push(7.0); + let mut b = W::default(); + let faces: &[(u32, u32, &[u32])] = &[(0x2, 0, &[1])]; + tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0, 0, 3, 0, 6], faces, coords: Some(&partial), ..Mesh::default() }); + let r = decode(§ion(1, &b), &Schema::default(), 8137); + assert!(matches!(r, Err(PrcError::Malformed(m)) if m == "13 coordinates are not whole points")); + // A partial last normal drops the stored normals, as a slot naming none does. + assert_eq!(stored_normals(&[0.0, 0.0, 1.0, 0.0], &[[0, 0, 0]], 1), (Vec::new(), Vec::new())); +} + +/// Elements a tessellation section spends from a full allowance. +fn spent(section: &W) -> usize { + let bytes = section.bytes(); + let schema = Schema::default(); + let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, 8137); + ctx.file_structure_tessellation().unwrap(); + crate::MAX_DECODED_ELEMENTS - ctx.r.elements_left() +} + +#[test] +fn faces_reusing_one_index_range_are_charged_for_each_triangle_they_make() { + const INDICES: [u32; 12] = [0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9]; + let square = |faces: &[(u32, u32, &[u32])]| { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &INDICES, faces, ..Mesh::default() }); + spent(§ion(1, &b)) + }; + let one = square(&[(0x2, 0, &[2])]); + let three = square(&[(0x2, 0, &[2]), (0x2, 0, &[2]), (0x2, 0, &[2])]); + // Per extra face: its count entry, its one data element and its two triangles, each + // 12 bytes of corners and 12 of normal slots. + assert_eq!(three - one, 2 * (2 + 2 * 4)); +} + +#[test] +fn reused_textured_faces_are_charged_for_every_corner_array() { + // The textured square, then one untextured triangle's normal and point slots. + const INDICES: [u32; 24] = [0, 0, 0, 0, 2, 3, 0, 4, 6, 0, 0, 0, 0, 4, 6, 0, 6, 9, 0, 0, 0, 3, 0, 6]; + let square = |faces: &[(u32, u32, &[u32])], sets: u32| { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &INDICES, faces, textures: sets, uvs: &[0.0; 8], ..Mesh::default() }); + spent(§ion(1, &b)) + }; + let face: (u32, u32, &[u32]) = (0x200, 0, &[2]); + let untextured: (u32, u32, &[u32]) = (0x2, 18, &[1]); + let one = square(&[face], 1); + let three = square(&[face, face, face], 1); + // Per extra face: its count, its data element and two triangles of corners, normal slots + // and one texture set, 16 bytes a corner triple. + assert_eq!(three - one, 2 * (2 + 2 * 6)); + // One textured face gives every triangle of the mesh a texture triple. + let mixed = square(&[face, untextured], 1); + assert_eq!(mixed - one, 2 + 6); +} + +#[test] +fn a_face_is_charged_before_it_is_triangulated() { + // One face claiming a million triangles over a 12-entry index array. + const INDICES: [u32; 12] = [0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9]; + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &INDICES, faces: &[(0x2, 0, &[1_000_000])], ..Mesh::default() }); + let bytes = section(1, &b).bytes(); + let schema = Schema::default(); + let r = Ctx::new(BitReader::with_elements(&bytes, 1000), &schema, 8137).file_structure_tessellation(); + assert!(matches!(r, Err(PrcError::TooManyElements { .. })), "{r:?}"); +} + +#[test] +fn the_globals_schema_and_the_tessellations_share_one_allowance() { + // A schema of one entry with three tokens: four elements. + let mut globals = W::default(); + globals.uint(1).uint(999).uint(3).uint(0).uint(0).uint(0); + let tess = textured_square(); + let f = crate::PrcFile::parse(&prc_file_with(&globals.bytes(), &tess.bytes())).unwrap(); + let need = spent(&tess) + 4; + let within = |elements| f.file_structures[0].tessellations_within(crate::bits::Budget { elements, ..crate::bits::Budget::FULL }); + assert!(within(need).is_ok()); + assert!(matches!(within(need - 1), Err(PrcError::TooManyElements { .. }))); +} diff --git a/crates/prc/src/tess/triangulate.rs b/crates/prc/src/tess/triangulate.rs new file mode 100644 index 0000000000..6611e4abf7 --- /dev/null +++ b/crates/prc/src/tess/triangulate.rs @@ -0,0 +1,282 @@ +//! A `TESS_Face`'s triangulated data turned into triangles [WD 7.8.5.5, 7.8.6]. + +use super::TriangleStyle; +use crate::PrcError; +use crate::entity::malformed; + +/// A fan/strip count's point-count bits; bit 30 is `NORMAL_Single` [WD 7.8.6.2]. +const COUNT_MASK: u32 = 0x3FFF_FFFF; +const NORMAL_SINGLE: u32 = 0x4000_0000; + +/// Texture-coordinate sets a face may carry. Each set costs a slot per point and a corner +/// triple per triangle of the whole mesh, so the count is capped. +pub(super) const MAX_TEXTURE_SETS: usize = 8; + +pub(super) struct Face { + pub(super) flags: u32, + pub(super) start: usize, + pub(super) data: Vec, + pub(super) textures: usize, + /// `line_attributes`, each a style index + 1: none = the owner's graphics, one = the + /// whole face's, more = one per triangulation entity [WD 7.8.6]. + pub(super) styles: Vec, + /// The behaviour bits read when `styles` is not empty. + pub(super) behaviour: u16, +} + +/// A `used_entities_flag` block's shape [WD 7.8.5.5]. +#[derive(Clone, Copy)] +enum Shape { + Triangles, + Fan, + Strip, +} + +/// One `used_entities_flag` block. +#[derive(Clone, Copy)] +struct Block { + bit: u32, + shape: Shape, + /// One normal per entity rather than per point. + one_normal: bool, + textured: bool, +} + +const fn block(bit: u32, shape: Shape, one_normal: bool, textured: bool) -> Block { + Block { bit, shape, one_normal, textured } +} + +/// The blocks in stream order, low bit first [WD 7.8.5.5]. +const BLOCKS: [Block; 12] = [ + block(0x0002, Shape::Triangles, false, false), + block(0x0004, Shape::Fan, false, false), + block(0x0008, Shape::Strip, false, false), + block(0x0020, Shape::Triangles, true, false), + block(0x0040, Shape::Fan, true, false), + block(0x0080, Shape::Strip, true, false), + block(0x0200, Shape::Triangles, false, true), + block(0x0400, Shape::Fan, false, true), + block(0x0800, Shape::Strip, false, true), + block(0x2000, Shape::Triangles, true, true), + block(0x4000, Shape::Fan, true, true), + block(0x8000, Shape::Strip, true, true), +]; + +/// One triangulation entity's index slots: per point its position, normal slot (when the +/// array stores normals) and texture slots. +#[derive(Default)] +struct Slots { + verts: Vec, + normals: Vec, + /// Per texture set, each point's slot. + textures: Vec>, +} + +/// What a mesh's triangles carry besides positions, built in step with them: corner normal +/// slots and, per texture set, corner texture slots (`None` for a triangle whose face stores +/// fewer sets). +#[derive(Default)] +pub(super) struct Corners { + pub(super) normals: Vec<[u32; 3]>, + pub(super) textures: Vec>>, +} + +impl Face { + /// Entity sizes per set block, from `TriangulatedData` [WD 7.8.6]. A triangle block is + /// one size word (its triangle count); fans and strips list a vertex count each. + fn entities(&self) -> Result)>, PrcError> { + let mut d = self.data.iter().copied(); + let mut next = || d.next().ok_or_else(|| malformed("triangulated data ends early".into())); + let mut out = Vec::new(); + for b in BLOCKS { + if self.flags & b.bit == 0 { + continue; + } + let sizes = match b.shape { + Shape::Triangles => vec![next()?], + Shape::Fan | Shape::Strip => { + let n = next()? as usize; + if n > self.data.len() { + return Err(malformed("triangulated data ends early".into())); + } + (0..n).map(|_| next()).collect::>()? + } + }; + out.push((b, sizes)); + } + Ok(out) + } + + /// Points the face's index slots name: the vertex-colour count (pdf-issues #820). + pub(super) fn point_count(&self) -> Result { + let mut n = 0usize; + for (b, sizes) in self.entities()? { + for s in sizes { + let s = (s & COUNT_MASK) as usize; + n = n.saturating_add(if matches!(b.shape, Shape::Triangles) { s.saturating_mul(3) } else { s }); + } + } + Ok(n) + } + + /// Triangles [`Self::triangulate`] makes: one per triangle-block count, two fewer than the + /// points of each fan and strip. + pub(super) fn triangle_count(&self) -> Result { + let mut n = 0usize; + for (b, sizes) in self.entities()? { + for s in sizes { + let s = (s & COUNT_MASK) as usize; + n = n.saturating_add(if matches!(b.shape, Shape::Triangles) { s } else { s.saturating_sub(2) }); + } + } + Ok(n) + } + + /// Texture sets the face's triangles carry: its count when one of its blocks is textured, + /// at most [`MAX_TEXTURE_SETS`] (a face past it is refused when triangulated). + pub(super) fn texture_sets(&self) -> usize { + if BLOCKS.iter().any(|b| b.textured && self.flags & b.bit != 0) { self.textures.min(MAX_TEXTURE_SETS) } else { 0 } + } + + /// Emit the face's triangles into `out`; `normals` = the index array stores normal slots, + /// in which case each triangle's corner normal slots go to `corners`, as do its corner + /// texture slots. Returns the triangles each triangulation entity emitted, in order: one + /// entity per triangle of a triangle block, per fan, per strip [WD 7.8.6]. + pub(super) fn triangulate( + &self, + indices: &[u32], + normals: bool, + n_points: usize, + out: &mut Vec<[u32; 3]>, + corners: &mut Corners, + ) -> Result, PrcError> { + if self.textures > MAX_TEXTURE_SETS { + return Err(malformed(format!("{} texture-coordinate sets on a face", self.textures))); + } + let mut entities = Vec::new(); + let mut slots = indices.get(self.start..).unwrap_or(&[]).iter().copied(); + for (b, sizes) in self.entities()? { + let t = if b.textured { self.textures } else { 0 }; + for word in sizes { + let count = (word & COUNT_MASK) as usize; + // Points in this entity, and whether each carries its own normal slot. + let (points, per_point_normal) = match b.shape { + Shape::Triangles => (count.saturating_mul(3), !b.one_normal), + Shape::Fan | Shape::Strip => (count, !b.one_normal || word & NORMAL_SINGLE == 0), + }; + if points.saturating_mul(t.saturating_add(1)) > indices.len() { + return Err(malformed("face runs past the triangulated index array".into())); + } + let s = read_slots(&mut slots, b.shape, points, t, normals.then_some(per_point_normal), n_points)?; + let made = emit(b.shape, &s, out, corners); + match b.shape { + Shape::Triangles => entities.extend(std::iter::repeat_n(1, made)), + Shape::Fan | Shape::Strip => entities.push(made), + } + } + } + Ok(entities) + } + + /// Append one [`TriangleStyle`] per triangle of the face, whose entities emitted + /// `entities[i]` triangles each. Entities past the face's styles inherit (style 0). + pub(super) fn styles_into(&self, entities: &[usize], out: &mut Vec) { + let whole = match self.styles.as_slice() { + [] => Some(0), + [one] => Some(*one), + _ => None, + }; + for (i, &n) in entities.iter().enumerate() { + let style = whole.unwrap_or_else(|| self.styles.get(i).copied().unwrap_or(0)); + out.extend(std::iter::repeat_n(TriangleStyle { style, behaviour: self.behaviour }, n)); + } + } +} + +/// The 8-byte elements of [`crate::MAX_DECODED_ELEMENTS`] one triangle holds: its corners +/// (12 bytes), its normal-slot corners when the index array stores them (12), a corner triple +/// per texture set (16 each) and its style when the mesh is styled (8). +pub(super) fn triangle_elements(normals: bool, sets: usize, styled: bool) -> usize { + let normals = if normals { 2 } else { 0 }; + 2usize.saturating_add(normals).saturating_add(sets.saturating_mul(2)).saturating_add(usize::from(styled)) +} + +/// One entity's `points` index slots. `normals` is `None` when the array stores no normal +/// slots, else whether every point has its own (rather than one per entity). +fn read_slots( + slots: &mut impl Iterator, + shape: Shape, + points: usize, + texture_sets: usize, + normals: Option, + n_points: usize, +) -> Result { + let mut take = || slots.next().ok_or_else(|| malformed("face runs past the triangulated index array".into())); + // The caller bounded points * (texture_sets + 1) by the index array's length. + let mut s = Slots { textures: vec![Vec::with_capacity(points); texture_sets], ..Slots::default() }; + let mut current = 0; + for k in 0..points { + let new_entity = match shape { + Shape::Triangles => k % 3 == 0, + Shape::Fan | Shape::Strip => k == 0, + }; + if let Some(per_point) = normals { + if per_point || new_entity { + current = take()?; + } + s.normals.push(current); + } + for set in &mut s.textures { + set.push(take()?); + } + let p = take()?; + if !p.is_multiple_of(3) || p as usize / 3 >= n_points { + return Err(malformed(format!("point index {p} is not a point"))); + } + s.verts.push(p / 3); + } + Ok(s) +} + +/// Append the triangles of one entity's `slots` to `out` and `corners`, keeping every +/// texture set as long as `out`. Returns how many it made. +fn emit(shape: Shape, slots: &Slots, out: &mut Vec<[u32; 3]>, corners: &mut Corners) -> usize { + let before = out.len(); + assemble(shape, &slots.verts, out); + assemble(shape, &slots.normals, &mut corners.normals); + let made = out.len().saturating_sub(before); + while corners.textures.len() < slots.textures.len() { + corners.textures.push(vec![None; before]); + } + for (k, set) in corners.textures.iter_mut().enumerate() { + match slots.textures.get(k) { + Some(own) => { + let mut tris = Vec::with_capacity(made); + assemble(shape, own, &mut tris); + set.extend(tris.into_iter().map(Some)); + } + None => set.extend(std::iter::repeat_n(None, made)), + } + } + made +} + +/// Append the triangles `shape` makes of `verts`. +fn assemble(shape: Shape, verts: &[u32], out: &mut Vec<[u32; 3]>) { + match shape { + Shape::Triangles => out.extend(verts.as_chunks::<3>().0.iter().copied()), + Shape::Fan => { + if let Some((&c, rim)) = verts.split_first() { + out.extend(rim.windows(2).filter_map(|w| match w { + [a, b] => Some([c, *a, *b]), + _ => None, + })); + } + } + Shape::Strip => out.extend(verts.windows(3).enumerate().filter_map(|(k, w)| match w { + [a, b, c] if k % 2 == 0 => Some([*a, *b, *c]), + [a, b, c] => Some([*b, *a, *c]), + _ => None, + })), + } +} diff --git a/crates/prc/src/testw.rs b/crates/prc/src/testw.rs new file mode 100644 index 0000000000..37e90606b6 --- /dev/null +++ b/crates/prc/src/testw.rs @@ -0,0 +1,166 @@ +//! Test-only PRC bit writer: the inverse of [`crate::BitReader`]. + +#![allow(clippy::expect_used)] + +use crate::acof; + +/// An MSB-first bit sink for building test streams. +#[derive(Debug, Default, Clone)] +pub(crate) struct W { + bits: Vec, +} + +impl W { + /// The low `n` bits of `v` (`n` ≤ 64), most significant first. + pub(crate) fn put(&mut self, v: u64, n: u32) -> &mut Self { + for i in (0..n).rev() { + self.bits.push((v >> i) & 1 == 1); + } + self + } + + /// One bit; a PRC `Boolean`. + pub(crate) fn bit(&mut self, b: bool) -> &mut Self { + self.bits.push(b); + self + } + + /// Bits written so far. + pub(crate) fn len(&self) -> usize { + self.bits.len() + } + + /// Every bit of `other`, unaligned. + pub(crate) fn append(&mut self, other: &Self) -> &mut Self { + self.bits.extend_from_slice(&other.bits); + self + } + + /// An `UnsignedInteger` [WD 11.10]. + pub(crate) fn uint(&mut self, mut v: u32) -> &mut Self { + while v != 0 { + self.bit(true).put(u64::from(v & 0xff), 8); + v >>= 8; + } + self.bit(false) + } + + /// An `Integer` [WD 11.11]: two's-complement groups, low first, each followed by a + /// continue bit. + pub(crate) fn int(&mut self, v: i32) -> &mut Self { + if v == 0 { + return self.bit(false); + } + self.bit(true); + let mut v = i64::from(v); + loop { + let group = v & 0xff; + self.put(group.cast_unsigned(), 8); + v >>= 8; + let done = (v == 0 && group & 0x80 == 0) || (v == -1 && group & 0x80 != 0); + self.bit(!done); + if done { + return self; + } + } + } + + /// A `String` [WD 11.4]: presence bit, byte count, raw UTF-8 bytes. + pub(crate) fn string(&mut self, s: Option<&str>) -> &mut Self { + let Some(s) = s else { + return self.bit(false); + }; + self.bit(true).uint(u32::try_from(s.len()).expect("a short test string")); + for b in s.bytes() { + self.put(u64::from(b), 8); + } + self + } + + /// A finite `Double` [WD 11.17]: zero as the two-bit zero code; anything else as its + /// exponent row, the sign, then the mantissa as literal bytes. + pub(crate) fn double(&mut self, v: f64) -> &mut Self { + if v == 0.0 { + return self.put(0b01, 2); + } + let bits = v.to_bits(); + let exponent = u32::try_from((bits >> 52) & 0x7ff).expect("11 bits"); + let (n, c) = acof::exponent_code(exponent).expect("the Double table has a row per exponent"); + let le = bits.to_le_bytes(); + self.put(u64::from(c), n).bit(v < 0.0).bit(true).put(u64::from(le[6] & 0x0f), 4); + for b in le[..6].iter().rev() { + self.bit(true).put(u64::from(*b), 8); + } + self + } + + /// A Huffman container [WD 12.2]: `leaves` are `(symbol, stored code, length)`, each value + /// is emitted as its leaf's code. + pub(crate) fn huffman(&mut self, nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)], values: &[i32]) -> &mut Self { + let mask = (1u32 << nbits) - 1; + let codes: Vec<(u32, u32)> = values + .iter() + .map(|&v| { + let l = leaves.iter().find(|l| l.0 & mask == v.cast_unsigned() & mask).expect("a leaf per value"); + (l.1, l.2) + }) + .collect(); + self.huffman_raw(nbits, len_width, leaves, &codes) + } + + /// As [`Self::huffman`] with each element's `(code, length)` given. + pub(crate) fn huffman_raw(&mut self, nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)], codes: &[(u32, u32)]) -> &mut Self { + let mut blob = Self::huffman_head(nbits, len_width, leaves); + lsb(&mut blob, u32::try_from(codes.len()).expect("a short test array"), 32); + for &(c, l) in codes { + for i in (0..l).rev() { + blob.push((c >> i) & 1 == 1); + } + } + self.huffman_words(blob) + } + + /// A Huffman container whose element count is `count` with no elements written. + pub(crate) fn huffman_count(&mut self, nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)], count: u32) -> &mut Self { + let mut blob = Self::huffman_head(nbits, len_width, leaves); + lsb(&mut blob, count, 32); + self.huffman_words(blob) + } + + fn huffman_head(nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)]) -> Vec { + let mut blob = Vec::new(); + lsb(&mut blob, u32::try_from(leaves.len()).expect("a short test table"), nbits + 1); + lsb(&mut blob, len_width, 8); + for &(s, c, l) in leaves { + lsb(&mut blob, s, nbits); + lsb(&mut blob, l, len_width); + lsb(&mut blob, c, l); + } + blob + } + + /// The payload as whole 32-bit words of LSB-first bytes, then the last word's used bits. + fn huffman_words(&mut self, mut blob: Vec) -> &mut Self { + let bits = blob.len(); + let words = bits.div_ceil(32); + blob.resize(words * 32, false); + self.uint(u32::try_from(words).expect("a short test payload")); + for byte in blob.chunks(8) { + let b = byte.iter().enumerate().fold(0u64, |a, (i, &x)| a | (u64::from(x) << i)); + self.put(b, 8); + } + self.uint(u32::try_from(bits - (words - 1) * 32).expect("at most 32")) + } + + /// The bits packed MSB-first, the last byte zero-padded. + pub(crate) fn bytes(&self) -> Vec { + self.bits.chunks(8).map(|c| c.iter().enumerate().fold(0u8, |a, (i, &b)| a | (u8::from(b) << (7 - i)))).collect() + } +} + +/// The low `n` bits of `v`, least significant first. +fn lsb(blob: &mut Vec, v: u32, n: u32) { + for i in 0..n { + blob.push((v >> i) & 1 == 1); + } +} diff --git a/crates/prc/src/tree/markup.rs b/crates/prc/src/tree/markup.rs new file mode 100644 index 0000000000..193c4fa219 --- /dev/null +++ b/crates/prc/src/tree/markup.rs @@ -0,0 +1,199 @@ +//! What a tree carries beside its geometry, read past and not modelled: markup data (PMI) +//! and views [WD 7.3.10.4, 7.7], and a scene's lights and clipping planes [WD 7.5.15-7.5.18, +//! 7.11.13]. The tree section has no length to skip by, so each is read field by field. + +use super::{ + AMBIENT_LIGHT, ANNOTATION_ITEM, ANNOTATION_REFERENCE, ANNOTATION_SET, DIRECTIONAL_LIGHT, LIGHT_INTENSITY_FROM, MARKUP_LINKED_ITEM, MAX_DEPTH, + MKP_LEADER, MKP_MARKUP, MKP_VIEW, POINT_LIGHT, SPOT_LIGHT, SURF_PLANE, VIEW_FLAGS_FROM, +}; +use crate::PrcError; +use crate::entity::{Ctx, malformed}; + +impl Ctx<'_, '_> { + /// `MarkupData` [WD 7.3.10.4]: linked items, leaders, markups, then annotation entities. + pub(super) fn markup_data(&mut self) -> Result<(), PrcError> { + let n = self.count(1, "markup linked items")?; + for _ in 0..n { + self.markup_linked_item()?; + } + let n = self.count(1, "leaders")?; + for _ in 0..n { + self.leader()?; + } + let n = self.count(1, "markups")?; + for _ in 0..n { + self.mkp_markup()?; + } + let n = self.count(1, "annotation entities")?; + for _ in 0..n { + self.annotation_entity(0)?; + } + Ok(()) + } + + /// A counted list of `MKP_View`s. + pub(super) fn views(&mut self) -> Result<(), PrcError> { + let n = self.count(1, "views")?; + for _ in 0..n { + self.view()?; + } + Ok(()) + } + + /// A counted list of `ReferenceOnPRCBase`s. + fn references(&mut self, what: &'static str) -> Result<(), PrcError> { + let n = self.count(1, what)?; + for _ in 0..n { + self.reference_on_prc_base()?; + } + Ok(()) + } + + /// `MISC_MarkupLinkedItem` (204) [WD 7.4.5]. + // prc-rs: a `has_reference_data` flag and its reference data follow the content entity + // reference (pdf-issues #722). + fn markup_linked_item(&mut self) -> Result<(), PrcError> { + self.expect_type(MARKUP_LINKED_ITEM)?; + self.base_with_graphics()?; + self.r.unsigned_integer()?; // local coordinate system + 1 + if self.r.bit()? { + self.reference_data()?; + } + if self.r.bit()? { + self.reference_data()?; + } + for _ in 0..4 { + self.r.bit()?; // show and delete, markup and leader + } + self.schema.skip_added_fields(MARKUP_LINKED_ITEM, &mut self.r)?; + self.user_data() + } + + /// `MKP_Leader` (503) [WD 7.7.5; pdf-issues #698]. + // prc-rs: the leader's field order. + fn leader(&mut self) -> Result<(), PrcError> { + self.expect_type(MKP_LEADER)?; + self.base_with_graphics()?; + for _ in 0..2 { + if self.r.bit()? { + self.reference_on_prc_base()?; // first, second linked item + } + } + self.r.unsigned_integer()?; // tessellation + 1 + self.schema.skip_added_fields(MKP_LEADER, &mut self.r)?; + self.user_data() + } + + /// `MKP_Markup` (502) [WD 7.7.4]. + // prc-rs: the markup's field order. + fn mkp_markup(&mut self) -> Result<(), PrcError> { + self.expect_type(MKP_MARKUP)?; + self.base_with_graphics()?; + self.r.unsigned_integer()?; // type + self.r.unsigned_integer()?; // subtype + self.references("markup linked items")?; + self.references("markup leaders")?; + self.r.unsigned_integer()?; // tessellation + 1 + self.schema.skip_added_fields(MKP_MARKUP, &mut self.r)?; + self.user_data() + } + + /// An annotation item (504), set (505) or reference (506) [WD 7.7.6-7.7.8]; sets nest. + // prc-rs: the annotation entities' field orders. + fn annotation_entity(&mut self, depth: usize) -> Result<(), PrcError> { + if depth > MAX_DEPTH { + return Err(malformed("annotation sets nest too deep".into())); + } + let t = self.r.unsigned_integer()?; + if !matches!(t, ANNOTATION_ITEM | ANNOTATION_SET | ANNOTATION_REFERENCE) { + return Err(malformed(format!("entity type {t} as an annotation"))); + } + self.base_with_graphics()?; + match t { + ANNOTATION_ITEM => self.reference_on_prc_base()?, + ANNOTATION_SET => { + let n = self.count(1, "annotation set")?; + for _ in 0..n { + self.annotation_entity(depth + 1)?; + } + } + _ => self.references("annotation reference")?, + } + self.schema.skip_added_fields(t, &mut self.r)?; + self.user_data() + } + + /// `MKP_View` (501) [WD 7.7.3]. + // prc-rs: the view's field order, and its flags, linked items and filters from version + // 8137. + fn view(&mut self) -> Result<(), PrcError> { + self.expect_type(MKP_VIEW)?; + self.base_with_graphics()?; + self.references("view annotations")?; + self.plane()?; + if self.r.bit()? { + self.scene_display_parameters()?; + } + if self.version >= VIEW_FLAGS_FROM { + for _ in 0..3 { + self.r.bit()?; // annotation view, default view, direction + } + self.references("view linked items")?; + let n = self.count(1, "view filters")?; + for _ in 0..n { + self.filter()?; + } + } + self.schema.skip_added_fields(MKP_VIEW, &mut self.r)?; + self.user_data() + } + + /// `SURF_Plane` (86) [WD 7.11.13; pdf-issues #658]: `ContentSurface` [WD 7.11.3.2], a + /// `Transformation3D` with no presence flag, the `Domain` [WD 8.2.4] and the + /// parameterisation's four coefficients. + // prc-rs: the plane's field order. + pub(super) fn plane(&mut self) -> Result<(), PrcError> { + self.expect_type(SURF_PLANE)?; + if self.r.bit()? { + self.content_prc_base()?; // base geometry: attributes and name + self.r.unsigned_integer()?; // identifier + } + self.r.unsigned_integer()?; // extend type + self.transformation_3d()?; + for _ in 0..8 { + self.r.double()?; // domain (4), coefficients (4) + } + self.schema.skip_added_fields(SURF_PLANE, &mut self.r) + } + + /// A light (731-734) [WD 7.5.15-7.5.18]. + // prc-rs: the lights' field orders, and a directional light's intensity from version 8030 + // (pdf-issues #745). + pub(super) fn light(&mut self) -> Result<(), PrcError> { + let t = self.r.unsigned_integer()?; + if !(AMBIENT_LIGHT..=SPOT_LIGHT).contains(&t) { + return Err(malformed(format!("entity type {t} as a light"))); + } + self.content_prc_ref_base()?; + for _ in 0..3 { + self.r.unsigned_integer()?; // ambient, diffuse, specular colour + 1 + } + if matches!(t, POINT_LIGHT | SPOT_LIGHT) { + self.vector3()?; // location + for _ in 0..3 { + self.r.double()?; // attenuation + } + } + if matches!(t, DIRECTIONAL_LIGHT | SPOT_LIGHT) { + self.vector3()?; // direction + } + if t == DIRECTIONAL_LIGHT && self.version >= LIGHT_INTENSITY_FROM { + self.r.double()?; + } + if t == SPOT_LIGHT { + self.r.double()?; // fall-off angle + self.r.double()?; // fall-off exponent + } + self.schema.skip_added_fields(t, &mut self.r) + } +} diff --git a/crates/prc/src/tree/markup_tests.rs b/crates/prc/src/tree/markup_tests.rs new file mode 100644 index 0000000000..dcd658bb62 --- /dev/null +++ b/crates/prc/src/tree/markup_tests.rs @@ -0,0 +1,280 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use super::tests::{Occ, PLAIN, S_RED, base, graphics, part_with, plate, ref_base, single}; +use crate::PrcError; +use crate::bits::BitReader; +use crate::entity::Ctx; +use crate::testw::W; +use crate::{Schema, Tessellation}; + +/// Read after each entity, to show it was read to its last bit and no further. +const SENTINEL: u32 = 0x5A5A; + +/// Runs `read` over `w` then the sentinel, at authoring version `version`. +fn read_at(w: &W, version: u32, read: impl FnOnce(&mut Ctx<'_, '_>) -> Result<(), PrcError>) -> Result<(), PrcError> { + let mut all = W::default(); + all.append(w).uint(SENTINEL); + let bytes = all.bytes(); + let schema = Schema::default(); + let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, version); + read(&mut ctx)?; + assert_eq!(ctx.r.unsigned_integer()?, SENTINEL, "read past the wrong number of bits"); + Ok(()) +} + +fn read(w: &W, read: impl FnOnce(&mut Ctx<'_, '_>) -> Result<(), PrcError>) -> Result<(), PrcError> { + read_at(w, 8137, read) +} + +/// Each truncation of `w` is an error, never a panic. +fn truncations_fail(w: &W, read: impl Fn(&mut Ctx<'_, '_>) -> Result<(), PrcError>) { + let bytes = w.bytes(); + let schema = Schema::default(); + for end in 0..bytes.len().saturating_sub(1) { + let mut ctx = Ctx::new(BitReader::new(&bytes[..end]), &schema, 8137); + assert!(read(&mut ctx).is_err(), "cut at {end}"); + } +} + +fn reference(w: &mut W, uid: u32) { + w.uint(205).uint(237).bit(true).uint(uid); +} + +fn markup_data(linked: &[W], leaders: &[W], markups: &[W], annotations: &[W]) -> W { + let mut w = W::default(); + for list in [linked, leaders, markups, annotations] { + w.uint(u32::try_from(list.len()).unwrap()); + for e in list { + w.append(e); + } + } + w +} + +fn linked_item() -> W { + let mut w = W::default(); + w.uint(204); + graphics(&mut w, None, 1, PLAIN); + w.uint(0).bit(true); + reference(&mut w, 7); + w.bit(true).uint(206).uint(149).bit(true).bit(true).uint(1).uint(1).uint(1).uint(0); + w.bit(true).bit(false).bit(true).bit(false).uint(0); + w +} + +fn leader() -> W { + let mut w = W::default(); + w.uint(503); + graphics(&mut w, None, 2, PLAIN); + w.bit(true); + reference(&mut w, 3); + w.bit(false).uint(4).uint(0); + w +} + +fn markup() -> W { + let mut w = W::default(); + w.uint(502); + graphics(&mut w, Some("Note"), 3, PLAIN); + w.uint(1).uint(2).uint(1); + reference(&mut w, 7); + w.uint(2); + reference(&mut w, 2); + reference(&mut w, 2); + w.uint(3).uint(0); + w +} + +fn annotation(t: u32, inner: impl FnOnce(&mut W)) -> W { + let mut w = W::default(); + w.uint(t); + graphics(&mut w, None, 4, PLAIN); + inner(&mut w); + w.uint(0); + w +} + +fn annotation_item() -> W { + annotation(504, |w| reference(w, 3)) +} + +fn annotation_set(members: &[W]) -> W { + annotation(505, |w| { + w.uint(u32::try_from(members.len()).unwrap()); + for m in members { + w.append(m); + } + }) +} + +fn annotation_reference() -> W { + annotation(506, |w| { + w.uint(1); + reference(w, 3); + }) +} + +fn plane(w: &mut W) { + w.uint(86).bit(true); + base(w, Some("Front")); + w.uint(9).uint(0).put(0x01, 8); + for v in [1., 2., 3., 0., 0., 1., 1., 1., 1., 0., 0.] { + w.double(v); + } +} + +fn light(t: u32) -> W { + let mut w = W::default(); + w.uint(t); + ref_base(&mut w, None, 5); + w.uint(1).uint(2).uint(3); + let doubles = match t { + 732 => 6, + 733 => 4, + 734 => 11, + _ => 0, + }; + for _ in 0..doubles { + w.double(0.5); + } + w +} + +/// A scene with `lights` and `planes` clipping planes; `is_absolute` is written from 8137. +fn scene(lights: &[W], planes: usize, version: u32) -> W { + let mut w = W::default(); + w.uint(741); + ref_base(&mut w, None, 6); + w.bit(true).uint(u32::try_from(lights.len()).unwrap()); + for l in lights { + w.append(l); + } + w.bit(false).bit(false).uint(u32::try_from(planes).unwrap()); + for _ in 0..planes { + plane(&mut w); + } + w.uint(0).uint(0).uint(0); + if version >= 8137 { + w.bit(true); + } + w +} + +fn filter(w: &mut W) { + w.uint(320); + ref_base(w, None, 8); + w.bit(true).bit(true).uint(0).bit(true).uint(0).uint(0); +} + +fn view(version: u32) -> W { + let mut w = W::default(); + w.uint(501); + graphics(&mut w, Some("Top"), 7, PLAIN); + w.uint(1); + reference(&mut w, 3); + plane(&mut w); + w.bit(true).append(&scene(&[light(731)], 1, version)); + if version >= 8137 { + w.bit(true).bit(false).bit(true).uint(1); + reference(&mut w, 7); + w.uint(1); + filter(&mut w); + } + w.uint(0); + w +} + +fn views(vs: &[W]) -> W { + let mut w = W::default(); + w.uint(u32::try_from(vs.len()).unwrap()); + for v in vs { + w.append(v); + } + w +} + +#[test] +fn a_markup_linked_item_is_read_past() { + let w = markup_data(&[linked_item()], &[], &[], &[]); + read(&w, |c| c.markup_data()).unwrap(); + truncations_fail(&w, |c| c.markup_data()); +} + +#[test] +fn a_leader_is_read_past() { + let w = markup_data(&[], &[leader()], &[], &[]); + read(&w, |c| c.markup_data()).unwrap(); + truncations_fail(&w, |c| c.markup_data()); +} + +#[test] +fn a_markup_is_read_past() { + let w = markup_data(&[], &[], &[markup()], &[]); + read(&w, |c| c.markup_data()).unwrap(); + truncations_fail(&w, |c| c.markup_data()); +} + +#[test] +fn annotation_items_sets_and_references_are_read_past() { + let set = annotation_set(&[annotation_item(), annotation_set(&[annotation_reference()])]); + let w = markup_data(&[], &[], &[], &[annotation_item(), set, annotation_reference()]); + read(&w, |c| c.markup_data()).unwrap(); + truncations_fail(&w, |c| c.markup_data()); +} + +#[test] +fn annotation_sets_nested_past_the_ceiling_are_an_error() { + let mut e = annotation_item(); + for _ in 0..=super::MAX_DEPTH { + e = annotation_set(&[e]); + } + let w = markup_data(&[], &[], &[], &[e]); + assert!(matches!(read(&w, |c| c.markup_data()), Err(PrcError::Malformed(_)))); +} + +#[test] +fn an_annotation_of_another_type_is_an_error() { + let w = markup_data(&[], &[], &[], &[annotation(502, |_| {})]); + assert!(matches!(read(&w, |c| c.markup_data()), Err(PrcError::Malformed(_)))); +} + +#[test] +fn a_view_is_read_past_with_its_flags_only_from_8137() { + let w = views(&[view(8137)]); + read(&w, |c| c.views()).unwrap(); + truncations_fail(&w, |c| c.views()); + read_at(&views(&[view(8030)]), 8030, |c| c.views()).unwrap(); +} + +#[test] +fn scene_lights_of_each_kind_are_read_past() { + let w = scene(&[light(731), light(732), light(733), light(734)], 0, 8137); + read(&w, |c| c.scene_display_parameters()).unwrap(); + truncations_fail(&w, |c| c.scene_display_parameters()); + let mut old = W::default(); + old.uint(733); + ref_base(&mut old, None, 5); + old.uint(1).uint(2).uint(3).double(0.).double(0.).double(1.); + read_at(&old, 8020, |c| c.light()).unwrap(); +} + +#[test] +fn scene_clipping_planes_are_read_past() { + let w = scene(&[], 2, 8137); + read(&w, |c| c.scene_display_parameters()).unwrap(); + truncations_fail(&w, |c| c.scene_display_parameters()); +} + +#[test] +fn a_tree_with_markups_and_views_still_places_its_parts() { + let mut tail = markup_data(&[linked_item()], &[leader()], &[markup()], &[annotation_set(&[annotation_item()])]); + tail.append(&views(&[view(8137)])); + let part = part_with(Some("Plate"), PLAIN, 1, |w| super::tests::poly_item(w, 7, 0, 1, (S_RED, super::SHOW)), &tail); + let occs = [Occ { name: Some("Asm"), sons: vec![1], markups: Some(tail.clone()), ..Occ::default() }, Occ { part: 1, ..Occ::default() }]; + let f = single(&[part, plate(S_RED)], &occs); + let nodes = f.model_tree().unwrap(); + assert_eq!(nodes.len(), 2); + let p = f.placements().unwrap(); + assert_eq!(p.len(), 1); + assert!(matches!(f.file_structures[0].tessellations().unwrap().first(), Some(Tessellation::Mesh(_)))); +} diff --git a/crates/prc/src/tree/mod.rs b/crates/prc/src/tree/mod.rs new file mode 100644 index 0000000000..3ed9d8eb20 --- /dev/null +++ b/crates/prc/src/tree/mod.rs @@ -0,0 +1,329 @@ +//! The product tree: the tree section (`FileStructureTree`, 304), the globals' styles and +//! coordinate systems (303) and the model file (301), walked into placed tessellations +//! [WD 7.3.3-7.3.11, 7.5, 7.6]. +//! +//! Markups (PMI), views, scene lights and clipping planes are read past and not modelled. + +// prc-rs: where the working draft leaves an entity's field order open, the order here is +// prc-rs's format description's; each such entity's reader carries its own marker. + +use std::sync::Arc; + +use crate::bits::{BitReader, Budget}; +use crate::container::offset; +use crate::entity::{Ctx, malformed}; +use crate::geometry::{Matrix, Vec3}; +use crate::{PrcError, PrcFile, Schema, SectionKind, UniqueId}; + +mod markup; +mod node; +mod overrides; +mod read; +mod style; +mod walk; + +pub use node::{ModelNode, NameSource}; +pub use overrides::EntityOverrides; +use overrides::{Override, Target}; +pub(crate) use style::Graphics; +use style::{Globals, Paint, resolve_style}; +pub use style::{Rgba, StyleAlpha}; +use walk::{At, Walk}; + +const BASE_WITH_GRAPHICS: u32 = 2; +const MODEL_FILE: u32 = 301; +const FILE_STRUCTURE: u32 = 302; +const FILE_STRUCTURE_GLOBALS: u32 = 303; +const FILE_STRUCTURE_TREE: u32 = 304; +const PRODUCT_OCCURRENCE: u32 = 310; +const PART_DEFINITION: u32 = 311; +const FILTER: u32 = 320; +const SURF_PLANE: u32 = 86; +const CARTESIAN_TRANSFORMATION: u32 = 202; +const ENTITY_REFERENCE: u32 = 203; +const MARKUP_LINKED_ITEM: u32 = 204; +const REFERENCE_ON_PRC_BASE: u32 = 205; +const REFERENCE_ON_TOPOLOGY: u32 = 206; +const GENERAL_TRANSFORMATION: u32 = 207; +const RI_BREP_MODEL: u32 = 232; +const RI_CURVE: u32 = 233; +const RI_DIRECTION: u32 = 234; +const RI_PLANE: u32 = 235; +const RI_POINT_SET: u32 = 236; +const RI_POLY_BREP_MODEL: u32 = 237; +const RI_POLY_WIRE: u32 = 238; +const RI_SET: u32 = 239; +const RI_COORDINATE_SYSTEM: u32 = 240; +const MKP_VIEW: u32 = 501; +const MKP_MARKUP: u32 = 502; +const MKP_LEADER: u32 = 503; +const ANNOTATION_ITEM: u32 = 504; +const ANNOTATION_SET: u32 = 505; +const ANNOTATION_REFERENCE: u32 = 506; +const STYLE: u32 = 701; +const MATERIAL: u32 = 702; +const PICTURE: u32 = 703; +const TEXTURE_APPLICATION: u32 = 711; +const TEXTURE_DEFINITION: u32 = 712; +const TEXTURE_TRANSFORMATION: u32 = 713; +const LINE_PATTERN: u32 = 721; +const AMBIENT_LIGHT: u32 = 731; +const POINT_LIGHT: u32 = 732; +const DIRECTIONAL_LIGHT: u32 = 733; +const SPOT_LIGHT: u32 = 734; +const SCENE_DISPLAY_PARAMETERS: u32 = 741; +const CAMERA: u32 = 742; +/// prc-rs: a scene's `is_absolute` exists from this authoring version. +const ABSOLUTE_SCENE_FROM: u32 = 8137; +/// prc-rs: a view's flags, linked items and filters exist from this authoring version. +const VIEW_FLAGS_FROM: u32 = 8137; +/// prc-rs: a directional light's intensity exists from this authoring version. +const LIGHT_INTENSITY_FROM: u32 = 8030; +/// `TextureDefinition` mapping type "operator" and function "blend", 0-based +/// (pdf-issues #485). +const TEXTURE_MAPPING_OPERATOR: i32 = 3; +const TEXTURE_FUNCTION_BLEND: i32 = 3; +/// Texture application mode: alpha test [WD 7.5.7]. +const TEXTURE_ALPHA_TEST: u8 = 0x02; + +/// Ceiling on `RI_Set` nesting, the occurrence walk's depth and a prototype chain's length. +const MAX_DEPTH: usize = 64; +/// Ceiling on occurrences visited in one walk: prototypes share subtrees, so a small file can +/// describe an exponential walk. +const MAX_VISITS: usize = 1 << 20; +/// Ceiling on placements one walk yields. +pub const MAX_PLACEMENTS: usize = 1 << 20; + +/// `GraphicsContent` behaviour bits [WD 7.2.4.2]. +const SHOW: u16 = 0x0001; +const SON_HERIT_COLOR: u16 = 0x0008; +const FATHER_HERIT_COLOR: u16 = 0x0010; +const REMOVED: u16 = 0x2000; +/// `product_behavior` SUPPRESSED [WD 7.3.10]. +const SUPPRESSED: u8 = 0x01; + +/// Per file structure, entry `b` is the paint of biased style `b`. +type Palettes = Arc<[Arc<[Option]>]>; + +/// A tessellation drawn at a place in the model: world = [`Self::matrix`] × the +/// tessellation's own coordinates. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct Placement { + /// Index into [`PrcFile::file_structures`]. + pub file_structure: usize, + /// Index into that file structure's [`tessellations`](crate::FileStructure::tessellations), + /// as the tree stores it: [`PrcFile::placements`] does not decode that section, so an + /// index past its end is the caller's to refuse. + pub tessellation: usize, + /// The occurrence path's locations, then the representation item's coordinate systems. + pub matrix: Matrix, + /// The colour of the style the tree resolves for this item; `None` when no style + /// reaches it or the style names no colour this reader resolves (a textured material + /// without a base). Faces with their own style are coloured by + /// [`Self::triangle_colours`]. + pub colour: Option, + /// The graphics from the root to this item, outermost first. + chain: Vec, + palettes: Palettes, + /// An occurrence's entity reference replacing the item's style. + item_override: Option, + /// Per face index, sorted, an entity reference replacing that face's style. + face_overrides: Vec<(u32, Graphics)>, +} + +/// The paint of the style `won` names, in its file structure's globals. +fn paint_of(palettes: &Palettes, won: Graphics) -> Option { + palettes.get(won.fs)?.get(offset(won.style)).copied().flatten() +} + +impl Placement { + /// Per triangle of `mesh` (this placement's tessellation), its colour as + /// [`Self::colour`] describes, with each face's own style taking part in the inheritance + /// [WD 7.2.4, 7.8.6]; `None` when no face of `mesh` carries a style or an override, so + /// every triangle is [`Self::colour`]. + #[must_use] + pub fn triangle_colours(&self, mesh: &crate::TriangleMesh) -> Option>> { + if mesh.triangle_styles.is_empty() && self.face_overrides.is_empty() { + return None; + } + let faces = overrides::triangle_faces(mesh); + let by_face = |k: usize| { + let f = faces.get(k)?; + let i = self.face_overrides.binary_search_by_key(f, |(i, _)| *i).ok()?; + self.face_overrides.get(i).map(|(_, g)| *g) + }; + let colour = |won| paint_of(&self.palettes, won).map(|p| p.rgba); + if mesh.triangle_styles.is_empty() { + let whole = self.item_override.unwrap_or_else(|| resolve_style(&self.chain)); + return Some((0..mesh.triangles.len()).map(|k| colour(by_face(k).unwrap_or(whole))).collect()); + } + let mut chain = self.chain.clone(); + chain.push(Graphics::default()); + let looks = mesh.triangle_styles.iter().enumerate().map(|(k, s)| { + if let Some(won) = by_face(k).or(self.item_override) { + return colour(won); + } + if let Some(last) = chain.last_mut() { + *last = Graphics { style: s.style, bits: s.behaviour, fs: self.file_structure }; + } + colour(resolve_style(&chain)) + }); + Some(looks.collect()) + } +} + +/// A representation item that can be drawn, flattened out of any `RI_Set`. +#[derive(Debug, Clone, PartialEq)] +struct Item { + /// `index_local_coordinate_system + 1` of each enclosing set and the item, outermost + /// first; 0 entries dropped. + local: Vec, + /// `index_tessellation + 1`; never 0 here. + tessellation: u32, + /// The part's graphics, each enclosing set's, then the item's own. + graphics: Vec, + /// The PRC unique id of each enclosing set, then the item's own. + uids: Vec, + /// A B-rep model's stored `(context, body)` ids [WD 7.6.2]. + brep: Option<(u32, u32)>, +} + +/// A `ProductOccurrence` (310) as far as placing and naming. +#[derive(Debug, Clone, PartialEq, Default)] +struct Product { + part: u32, + prototype: u32, + prototype_fs: Option, + external: u32, + external_fs: Option, + sons: Vec, + /// Its own graphics hide it (Show clear or Removed set). + hidden: bool, + suppressed: bool, + graphics: Graphics, + location: Option, + name: Option, + /// Its entity references that name a target. + overrides: Vec, +} + +/// One file structure's tree section. +#[derive(Debug, Clone, PartialEq, Default)] +struct Tree { + parts: Vec>, + /// Each part definition's name, parallel to `parts`. + part_names: Vec>, + products: Vec, +} + +/// The model file's roots: file structure id and biased occurrence index. +#[derive(Debug, Clone, PartialEq)] +struct ModelFile { + roots: Vec<(UniqueId, u32)>, +} + +impl PrcFile { + /// Where each drawable tessellation sits in the model: the product-occurrence tree walked + /// from the model file's roots, each occurrence's location composed onto its father's, + /// and each representation item's local coordinate systems applied last [WD 7.3.10.1, + /// 7.6.3.2], coloured with [`StyleAlpha::ZeroUnset`] and entity references applied by + /// [`EntityOverrides::Subtree`]. Suppressed occurrences and hidden entities are left + /// out; a tessellation several occurrences draw appears once per occurrence. Empty when + /// the file stores no tree ([`Self::stores_tree`]). + /// + /// # Errors + /// [`PrcError::Unsupported`] when an item or an entity reference names a reference + /// coordinate system and the globals carry fonts or fill patterns; [`PrcError::Malformed`] for a tree index naming no entity, + /// a cycle of occurrences or prototypes, or a tree past [`MAX_PLACEMENTS`] or the depth + /// ceiling; read errors as [`crate::FileStructure::tessellations`]. + pub fn placements(&self) -> Result, PrcError> { + self.placements_with(StyleAlpha::default(), EntityOverrides::default()) + } + + /// [`Self::placements`], combining style transparency with material alpha by `rule` and + /// applying occurrences' entity references by `scope`. + /// + /// # Errors + /// As [`Self::placements`]. + pub fn placements_with(&self, rule: StyleAlpha, scope: EntityOverrides) -> Result, PrcError> { + Ok(self.walk(rule, scope)?.0) + } + + /// The assembly tree as a model-tree panel lists it: every product occurrence the model + /// file's roots reach, depth first, with its display name and stored visibility. Hidden + /// and suppressed subtrees are listed (with `drawn` false) though [`Self::placements`] + /// leaves them out; each node's `placements` range indexes that list, so attributes each + /// placed mesh to its part. Empty when the file stores no tree. + /// + /// # Errors + /// As [`Self::placements`]. + pub fn model_tree(&self) -> Result, PrcError> { + Ok(self.walk(StyleAlpha::default(), EntityOverrides::default())?.1) + } + + /// Whether the file stores a tree: its model file holds an entity. ISO 14739-1 requires + /// one, but a tessellation-only file still draws (unplaced), so its absence reads as an + /// empty tree, not an error. + #[must_use] + pub fn stores_tree(&self) -> bool { + self.model_file.iter().any(|&b| b != 0) + } + + fn walk(&self, rule: StyleAlpha, scope: EntityOverrides) -> Result<(Vec, Vec), PrcError> { + self.walk_within(rule, scope, Budget::FULL) + } + + /// [`Self::walk`] with `budget` shared by all its readers. + pub(crate) fn walk_within(&self, rule: StyleAlpha, scope: EntityOverrides, budget: Budget) -> Result<(Vec, Vec), PrcError> { + if !self.stores_tree() { + return Ok((Vec::new(), Vec::new())); + } + // One allowance for the whole walk, so many sections cannot each spend a full one. + let mut left = budget; + let mut trees = Vec::with_capacity(self.file_structures.len()); + for fs in &self.file_structures { + // The globals open with the schema every reader of this structure uses: read it + // once, charged to the walk, and go on from there for the globals themselves. + let mut globals_reader = BitReader::with_budget(fs.section(SectionKind::Globals), left); + let schema = Schema::read(&mut globals_reader)?; + left = globals_reader.budget(); + let mut ctx = Ctx::new(BitReader::with_budget(fs.section(SectionKind::Tree), left), &schema, fs.authoring_version); + let tree = ctx.file_structure_tree()?; + left = ctx.r.budget(); + // The globals carry the coordinate systems and the colours; colours alone are not + // worth refusing the placements over. + let mut ctx = Ctx::new(globals_reader.with_remaining(left), &schema, fs.authoring_version); + let globals = ctx.globals(); + left = ctx.r.budget(); + let globals = match globals { + Ok(g) => g, + Err(e) if places_by_system(&tree) => return Err(e), + Err(_) => Globals::default(), + }; + trees.push((fs.id, tree, globals)); + } + let mut r = BitReader::with_budget(&self.model_file, left); + let schema = Schema::read(&mut r)?; + let model = Ctx::new(r, &schema, self.header.authoring_version).model_file()?; + let mut walk = Walk::new(trees.iter().map(|(id, t, g)| (*id, t, g)).collect(), rule, scope); + for (id, root) in model.roots { + let fs = trees.iter().position(|t| t.0 == id).ok_or_else(|| malformed("a root names an unknown file structure".into()))?; + if let Some(index) = root.checked_sub(1) { + walk.occurrence(fs, offset(index), &Matrix::IDENTITY, &[], At::ROOT)?; + } + } + Ok((walk.out, walk.nodes)) + } +} + +/// Whether an item of `tree`, or an entity reference one of its occurrences holds, names a +/// coordinate system: then the globals that store them must have been read. +fn places_by_system(tree: &Tree) -> bool { + tree.parts.iter().flatten().any(|i| !i.local.is_empty()) || tree.products.iter().flat_map(|p| &p.overrides).any(|o| o.local != 0) +} + +#[cfg(test)] +mod markup_tests; +#[cfg(test)] +mod tests; diff --git a/crates/prc/src/tree/node.rs b/crates/prc/src/tree/node.rs new file mode 100644 index 0000000000..19a8e04e5c --- /dev/null +++ b/crates/prc/src/tree/node.rs @@ -0,0 +1,68 @@ +//! The assembly tree as a list: what a model-tree panel shows. + +use std::ops::Range; + +/// Where a [`ModelNode`]'s name came from. PRC does not say which name a +/// viewer should show; this reader takes the first present of these, in order. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum NameSource { + /// The occurrence's own name. + Occurrence, + /// The name of an occurrence in its prototype chain. + Prototype, + /// The name of the part definition it draws. + Part, + /// No name anywhere: [`ModelNode::name`] is `None`. + Unnamed, +} + +/// One product occurrence of a PRC model's assembly tree, as +/// [`PrcFile::model_tree`](crate::PrcFile::model_tree) lists it: depth +/// first, a parent before its children, children in file order. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct ModelNode { + /// The name to show, by [`Self::name_from`]; `None` when unnamed. + pub name: Option, + /// Where [`Self::name`] came from. + pub name_from: NameSource, + /// Index of the parent node in the list; `None` for a root. + pub parent: Option, + /// 0 for a root. + pub depth: usize, + /// Index into [`crate::PrcFile::file_structures`] of the structure + /// holding this occurrence. + pub file_structure: usize, + /// The occurrence's 0-based index in that structure's tree. + pub occurrence: usize, + /// The occurrence's own graphics hide it (Show clear, or Removed) + /// [WD 7.2.4.2]: the visibility the file stores, which a viewer opens on. + pub hidden: bool, + /// The occurrence is suppressed (`product_behavior`) [WD 7.3.10]. + pub suppressed: bool, + /// Whether anything of this node is drawn: false when it or an + /// ancestor is hidden or suppressed. + pub drawn: bool, + /// Whether the occurrence (or its prototype) carries a part definition, + /// i.e. geometry of its own rather than only children. + pub has_part: bool, + /// The placements this node and its descendants draw: indices into + /// [`PrcFile::placements`](crate::PrcFile::placements) (or + /// [`placements_with`](crate::PrcFile::placements_with), which yields the same order): + /// the meshes attributed to this occurrence's part and its descendants' parts. Empty when + /// nothing under it is drawn. + pub placements: Range, +} + +impl ModelNode { + /// The label the node is listed under: its name, or `occurrence F:I` + /// (file structure, occurrence) when it has none. + #[must_use] + pub fn label(&self) -> String { + match &self.name { + Some(name) => name.clone(), + None => format!("occurrence {}:{}", self.file_structure, self.occurrence), + } + } +} diff --git a/crates/prc/src/tree/overrides.rs b/crates/prc/src/tree/overrides.rs new file mode 100644 index 0000000000..a85a83ba09 --- /dev/null +++ b/crates/prc/src/tree/overrides.rs @@ -0,0 +1,128 @@ +//! Product-occurrence entity references: the colour, visibility and placement an occurrence +//! imposes on an entity it reaches [WD 7.3.10.1, 7.4.4]. + +use super::{Graphics, Item, RI_BREP_MODEL, RI_COORDINATE_SYSTEM, UniqueId}; + +/// `PRC_TYPE_TOPO_Face` [WD 6.2]. +const TOPO_FACE: u32 = 149; + +/// Which entities an occurrence's entity references reach. ISO 14739-1 says a reference +/// overrides the referenced entity's properties, and not where. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum EntityOverrides { + /// Only where the occurrence's own subtree places the target, so one instance of a + /// shared part can be coloured alone. + #[default] + Subtree, + /// Every placement of the target anywhere in the model. + Everywhere, + /// Read past them: every entity keeps its own and inherited graphics. + Ignore, +} + +/// One `MISC_EntityReference` held by a product occurrence. +#[derive(Debug, Clone, PartialEq)] +pub(super) struct Override { + /// The reference's graphics; style 0 leaves the target's colour alone. + pub(super) graphics: Graphics, + /// The graphics hide the target (Show clear or Removed set). + pub(super) hidden: bool, + /// `index_local_coordinate_system + 1` replacing the target's; 0 none. + pub(super) local: u32, + pub(super) target: Target, +} + +/// What an entity reference names. +#[derive(Debug, Clone, PartialEq)] +pub(super) enum Target { + /// `ReferenceOnPRCBase`: the entity of type `kind` whose PRC unique id is `uid` in + /// structure `fs` (`None`: the occurrence's own). + Entity { kind: u32, fs: Option, uid: u32 }, + /// `ReferenceOnTopology` into the B-rep stored as `(context, body)`: its topological + /// items of type `kind` at `indices` (0-based). With `fs` `None` it matches that B-rep in + /// any structure the subtree reaches: producers write "same structure" for an assembly's + /// reference into a part's own structure. + Topology { kind: u32, fs: Option, brep: (u32, u32), indices: Vec }, +} + +impl Target { + /// The structure the reference names, if it names one. + pub(super) fn fs(&self) -> Option { + match self { + Target::Entity { fs, .. } | Target::Topology { fs, .. } => *fs, + } + } +} + +/// An entity reference in force: the structure of the occurrence holding it, the structure +/// it names (`None`: any), and the reference. +pub(super) type Active<'t> = (usize, Option, &'t Override); + +/// The overrides that reach one representation item. +#[derive(Debug, Default)] +pub(super) struct Reached { + /// Graphics replacing the item's resolved style, in its owner's structure. + pub(super) item: Option, + pub(super) hidden: bool, + /// The owner's structure and its local coordinate system. + pub(super) local: Option<(usize, u32)>, + /// Per face index, sorted, graphics replacing that face's style. + pub(super) faces: Vec<(u32, Graphics)>, +} + +impl Reached { + /// Applies `active` innermost first, so an outer occurrence's override wins over an + /// inner one's, as an assembly's appearance beats its sub-assembly's. + pub(super) fn collect(active: &[Active<'_>], fs: usize, item: &Item) -> Self { + let mut r = Reached::default(); + // A map, not a list searched per index: one reference may name millions of faces. + let mut faces = std::collections::BTreeMap::new(); + for &(owner, named, ov) in active.iter().rev() { + let in_fs = named.is_none_or(|n| n == fs); + let g = Graphics { fs: owner, ..ov.graphics }; + match &ov.target { + Target::Entity { kind, uid, .. } => { + if !in_fs || !is_item(*kind) || !item.uids.contains(uid) { + continue; + } + if ov.graphics.style != 0 { + r.item = Some(g); + } + r.hidden = ov.hidden; + if ov.local != 0 { + r.local = Some((owner, ov.local)); + } + } + Target::Topology { kind, brep, indices, .. } => { + if !in_fs || *kind != TOPO_FACE || item.brep != Some(*brep) || ov.graphics.style == 0 { + continue; + } + for &f in indices { + faces.insert(f, g); + } + } + } + } + r.faces = faces.into_iter().collect(); + r + } +} + +/// Representation-item types a `ReferenceOnPRCBase` can colour. +fn is_item(kind: u32) -> bool { + (RI_BREP_MODEL..=RI_COORDINATE_SYSTEM).contains(&kind) +} + +/// Per triangle of `mesh`, the index of the `TESS_Face` it was read from (`u32::MAX` for +/// none). +pub(super) fn triangle_faces(mesh: &crate::TriangleMesh) -> Vec { + let mut out = vec![u32::MAX; mesh.triangles.len()]; + for (i, range) in mesh.faces.iter().enumerate() { + let face = u32::try_from(i).unwrap_or(u32::MAX); + for f in out.get_mut(range.clone()).into_iter().flatten() { + *f = face; + } + } + out +} diff --git a/crates/prc/src/tree/read.rs b/crates/prc/src/tree/read.rs new file mode 100644 index 0000000000..ff3affaeee --- /dev/null +++ b/crates/prc/src/tree/read.rs @@ -0,0 +1,452 @@ +//! The tree section's and the model file's entities: part definitions, representation items, +//! product occurrences and their transformations and entity references [WD 7.3, 7.4, 7.6]. + +use super::{ + ABSOLUTE_SCENE_FROM, BASE_WITH_GRAPHICS, CAMERA, CARTESIAN_TRANSFORMATION, ENTITY_REFERENCE, FILE_STRUCTURE, FILE_STRUCTURE_TREE, FILTER, + GENERAL_TRANSFORMATION, Graphics, Item, MAX_DEPTH, MODEL_FILE, Matrix, ModelFile, Override, PART_DEFINITION, PRODUCT_OCCURRENCE, Product, + REFERENCE_ON_PRC_BASE, REFERENCE_ON_TOPOLOGY, REMOVED, RI_BREP_MODEL, RI_COORDINATE_SYSTEM, RI_CURVE, RI_DIRECTION, RI_PLANE, RI_POINT_SET, + RI_POLY_BREP_MODEL, RI_POLY_WIRE, RI_SET, SCENE_DISPLAY_PARAMETERS, SHOW, SUPPRESSED, Target, Tree, UniqueId, Vec3, +}; +use crate::PrcError; +use crate::entity::{Ctx, malformed}; + +impl Ctx<'_, '_> { + pub(super) fn vector3(&mut self) -> Result { + Ok(Vec3([self.r.double()?, self.r.double()?, self.r.double()?])) + } + + pub(super) fn unique_id(&mut self) -> Result { + let mut id = [0; 4]; + for v in &mut id { + *v = self.r.unsigned_integer()?; + } + Ok(UniqueId(id)) + } + + /// `ContentPRCRefBase` [WD 7.2.3.2; pdf-issues #697]: the base, then the CAD ids and the + /// PRC unique id, which becomes [`Ctx::uid`]. + pub(super) fn content_prc_ref_base(&mut self) -> Result<(), PrcError> { + self.content_prc_base()?; + self.r.unsigned_integer()?; // CAD identifier + self.r.unsigned_integer()?; // CAD persistent identifier + self.uid = self.r.unsigned_integer()?; + Ok(()) + } + + /// `PRCBaseWithGraphics` [WD 7.2.4; pdf-issues #405]: whether the entity's own graphics + /// hide it (Show clear or Removed set), and its graphics. Reused graphics + /// (`same_graphics`) are the current graphics and never hide. + pub(super) fn base_with_graphics(&mut self) -> Result<(bool, Graphics), PrcError> { + self.content_prc_ref_base()?; + let hidden = if self.r.bit()? { + false + } else { + self.r.unsigned_integer()?; // layer + 1 + let style = self.r.unsigned_integer()?; + let lo = u16::from(self.r.character()?); + let hi = u16::from(self.r.character()?); + let bits = lo | hi << 8; + self.graphics = Graphics { style, bits, fs: 0 }; + bits & SHOW == 0 || bits & REMOVED != 0 + }; + // prc-rs: schema additions to the base itself follow the graphics. + self.schema.skip_added_fields(BASE_WITH_GRAPHICS, &mut self.r)?; + Ok((hidden, self.graphics)) + } + + /// `CartesianTransformation` (202) or `GeneralTransformation` (207), type code included + /// [WD 7.4.8, 7.4.9, 7.4.11; pdf-issues #430]. + pub(super) fn transformation(&mut self) -> Result { + let t = self.r.unsigned_integer()?; + let m = match t { + GENERAL_TRANSFORMATION => { + // Column-major [WD 7.4.9]. + let mut m = Matrix::IDENTITY; + for col in 0..4 { + for row in &mut m.0 { + if let Some(c) = row.get_mut(col) { + *c = self.r.double()?; + } + } + } + m + } + CARTESIAN_TRANSFORMATION => self.transformation_3d()?, + t => return Err(malformed(format!("entity type {t} as a transformation"))), + }; + self.schema.skip_added_fields(t, &mut self.r)?; + Ok(m) + } + + /// The embedded `Transformation3D` [WD 7.4.11.1]: a behaviour byte, then each field its + /// bits name. + pub(super) fn transformation_3d(&mut self) -> Result { + let b = self.r.character()?; + let mut m = Matrix::IDENTITY; + let set_col = |m: &mut Matrix, col: usize, v: Vec3| { + for (row, x) in m.0.iter_mut().zip(v.0) { + if let Some(c) = row.get_mut(col) { + *c = x; + } + } + }; + if b & 0x01 != 0 { + let t = self.vector3()?; + set_col(&mut m, 3, t); + } + if b & 0x20 != 0 { + for col in 0..3 { + let v = self.vector3()?; + set_col(&mut m, col, v); + } + } else if b & 0x02 != 0 { + let x = self.vector3()?; + let y = self.vector3()?; + // Mirrored (0x04): the third axis is y × x. + let z = if b & 0x04 != 0 { y.cross(x) } else { x.cross(y) }; + set_col(&mut m, 0, x); + set_col(&mut m, 1, y); + set_col(&mut m, 2, z); + } + let mut scale = [1.0; 3]; + if b & 0x10 != 0 { + scale = self.vector3()?.0; + } + // prc-rs: with both scale bits set, both are read and multiply. + if b & 0x08 != 0 { + let s = self.r.double()?; + scale = scale.map(|v| v * s); + } + for row in m.0.iter_mut().take(3) { + for (c, s) in row.iter_mut().zip(scale) { + *c *= s; + } + } + if b & 0x40 != 0 { + let h = [self.r.double()?, self.r.double()?, self.r.double()?, self.r.double()?]; + if let Some(r) = m.0.get_mut(3) { + *r = h; + } + } + Ok(m) + } + + /// A polymorphic representation item [WD 7.6]; drawable ones are pushed onto `out` with + /// `local` (the enclosing sets' coordinate systems) prefixed. + // prc-rs: the item's own fields follow its local coordinate system and tessellation. + fn representation_item(&mut self, at: &Item, out: &mut Vec, depth: usize) -> Result<(), PrcError> { + if depth > MAX_DEPTH { + return Err(malformed("representation sets nest too deep".into())); + } + let t = self.r.unsigned_integer()?; + let (hidden, own) = self.base_with_graphics()?; + let mut item = at.clone(); + item.uids.push(self.uid); + item.graphics.push(own); + let cs = self.r.unsigned_integer()?; + let tess = self.r.unsigned_integer()?; + if cs != 0 { + item.local.push(cs); + } + let (drawable, brep) = if t == RI_SET { + let n = self.count(1, "representation set")?; + let mut members = Vec::new(); + for _ in 0..n { + self.representation_item(&item, &mut members, depth + 1)?; + } + if !hidden { + out.extend(members); + } + (false, None) + } else { + self.leaf_item_fields(t)? + }; + self.schema.skip_added_fields(t, &mut self.r)?; + self.user_data()?; + if drawable && !hidden && tess != 0 { + out.push(Item { tessellation: tess, brep, ..item }); + } + Ok(()) + } + + /// The type-specific fields of a representation item other than `RI_Set` [WD 7.6]; + /// whether the item can be drawn, and a B-rep model's `(context, body)`. + fn leaf_item_fields(&mut self, t: u32) -> Result<(bool, Option<(u32, u32)>), PrcError> { + match t { + RI_BREP_MODEL => { + let brep = self.exact_tolerance()?; + self.r.bit()?; // is_closed + return Ok((true, brep)); + } + RI_CURVE | RI_PLANE => { + self.exact_tolerance()?; + } + RI_DIRECTION => { + if self.r.bit()? { + self.vector3()?; // origin + } + self.vector3()?; + return Ok((false, None)); + } + RI_POINT_SET => { + let n = self.count(6, "point set")?; + for _ in 0..n { + self.vector3()?; + } + } + RI_POLY_BREP_MODEL => { + self.r.bit()?; // is_closed + } + RI_POLY_WIRE => {} + RI_COORDINATE_SYSTEM => { + self.transformation()?; + return Ok((false, None)); + } + t => return Err(malformed(format!("entity type {t} as a representation item"))), + } + Ok((true, None)) + } + + /// A B-rep, curve or plane item's optional exact-geometry `(context, body)` ids + /// [WD 7.6.2]. + fn exact_tolerance(&mut self) -> Result, PrcError> { + if self.r.bit()? { + return Ok(Some((self.r.unsigned_integer()?, self.r.unsigned_integer()?))); + } + Ok(None) + } + + /// `SceneDisplayParameters` (741) [WD 7.5.19]: read past, lights and clipping planes + /// included. + // prc-rs: the scene's field order, and `is_absolute` from version 8137. + pub(super) fn scene_display_parameters(&mut self) -> Result<(), PrcError> { + self.expect_type(SCENE_DISPLAY_PARAMETERS)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_active + let n = self.count(1, "scene lights")?; + for _ in 0..n { + self.light()?; + } + if self.r.bit()? { + self.camera()?; + } + if self.r.bit()? { + self.vector3()?; // rotation centre + } + let n = self.count(1, "scene clipping planes")?; + for _ in 0..n { + self.plane()?; + } + self.r.unsigned_integer()?; // background style + 1 + self.r.unsigned_integer()?; // default style + 1 + let n = self.count(2, "scene default styles")?; + for _ in 0..n { + self.r.unsigned_integer()?; // entity type + self.r.unsigned_integer()?; // style + 1 + } + if self.version >= ABSOLUTE_SCENE_FROM { + self.r.bit()?; // is_absolute + } + self.schema.skip_added_fields(SCENE_DISPLAY_PARAMETERS, &mut self.r) + } + + /// `Camera` (742) [WD 7.5.20]: read past. + // prc-rs: the camera's field order. + fn camera(&mut self) -> Result<(), PrcError> { + self.expect_type(CAMERA)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_orthographic + for _ in 0..3 { + self.vector3()?; // position, look, up + } + for _ in 0..6 { + self.r.double()?; // x, y, ratio, near, far, zoom + } + self.schema.skip_added_fields(CAMERA, &mut self.r) + } + + /// `MISC_EntityReference` (203) [WD 7.4.4, 7.4.10]; `None` when it names no target. + // prc-rs: the local coordinate system precedes the optional reference data. + fn entity_reference(&mut self) -> Result, PrcError> { + self.expect_type(ENTITY_REFERENCE)?; + let (hidden, graphics) = self.base_with_graphics()?; + let local = self.r.unsigned_integer()?; + let target = if self.r.bit()? { self.reference_data()? } else { None }; + self.schema.skip_added_fields(ENTITY_REFERENCE, &mut self.r)?; + self.user_data()?; + Ok(target.map(|target| Override { graphics, hidden, local, target })) + } + + /// `ReferenceData` [WD 7.4.10.2]: a `ReferenceOnPRCBase` (205) or a + /// `ReferenceOnTopology` (206), type code included; `None` when it names no target. + pub(super) fn reference_data(&mut self) -> Result, PrcError> { + let t = self.r.unsigned_integer()?; + let target = match t { + REFERENCE_ON_PRC_BASE => Some(self.reference_on_prc_base_body()?), + REFERENCE_ON_TOPOLOGY => self.reference_on_topology()?, + t => return Err(malformed(format!("entity type {t} as reference data"))), + }; + self.schema.skip_added_fields(t, &mut self.r)?; + Ok(target) + } + + /// `ReferenceOnPRCBase` (205) [WD 7.4.6] after its type code. + fn reference_on_prc_base_body(&mut self) -> Result { + let kind = self.r.unsigned_integer()?; + let fs = self.file_identifier()?; + let uid = self.r.unsigned_integer()?; + Ok(Target::Entity { kind, fs, uid }) + } + + /// `ReferenceOnPRCBase` (205) [WD 7.4.6], type code included: read past. + pub(super) fn reference_on_prc_base(&mut self) -> Result<(), PrcError> { + self.expect_type(REFERENCE_ON_PRC_BASE)?; + self.reference_on_prc_base_body()?; + self.schema.skip_added_fields(REFERENCE_ON_PRC_BASE, &mut self.r) + } + + /// `ReferenceOnTopology` (206) after its type code; `None` without a body. + fn reference_on_topology(&mut self) -> Result, PrcError> { + let kind = self.r.unsigned_integer()?; + if !self.r.bit()? { + return Ok(None); + } + let fs = self.file_identifier()?; + let brep = (self.r.unsigned_integer()?, self.r.unsigned_integer()?); + let indices = self.uints("topology reference indices")?; + Ok(Some(Target::Topology { kind, fs, brep, indices })) + } + + /// `ASM_Filter` (320) [WD 7.3.12]: read past. + // prc-rs: the filter's field order. + pub(super) fn filter(&mut self) -> Result<(), PrcError> { + self.expect_type(FILTER)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_active + self.r.bit()?; // layers inclusive + self.uints("filter layers")?; + self.r.bit()?; // entities inclusive + let n = self.count(1, "filter entities")?; + for _ in 0..n { + self.entity_reference()?; + } + self.schema.skip_added_fields(FILTER, &mut self.r)?; + self.user_data() + } + + /// `PartDefinition` (311): its drawable items and its name. + fn part_definition(&mut self) -> Result<(Vec, Option), PrcError> { + self.expect_type(PART_DEFINITION)?; + let (hidden, own) = self.base_with_graphics()?; + let name = self.name.clone(); + self.vector3()?; // bounding box + self.vector3()?; + let n = self.count(1, "representation items")?; + let mut items = Vec::new(); + let at = Item { local: Vec::new(), tessellation: 0, graphics: vec![own], uids: Vec::new(), brep: None }; + for _ in 0..n { + self.representation_item(&at, &mut items, 0)?; + } + self.markup_data()?; + self.views()?; + self.schema.skip_added_fields(PART_DEFINITION, &mut self.r)?; + self.user_data()?; + if hidden { + items.clear(); + } + Ok((items, name)) + } + + /// `FileIdentifier` [WD 7.3.10.2.2]: `None` for the same structure. + fn file_identifier(&mut self) -> Result, PrcError> { + if self.r.bit()? { Ok(None) } else { self.unique_id().map(Some) } + } + + /// `ProductOccurrence` (310) [WD 7.3.10]. + fn product_occurrence(&mut self) -> Result { + self.expect_type(PRODUCT_OCCURRENCE)?; + let (hidden, graphics) = self.base_with_graphics()?; + let mut p = Product { hidden, graphics, name: self.name.clone(), part: self.r.unsigned_integer()?, ..Product::default() }; + p.prototype = self.r.unsigned_integer()?; + if p.prototype != 0 { + p.prototype_fs = self.file_identifier()?; + } + p.external = self.r.unsigned_integer()?; + if p.external != 0 { + p.external_fs = self.file_identifier()?; + } + p.sons = self.uints("son occurrences")?; + p.suppressed = self.r.character()? & SUPPRESSED != 0; + // ProductInformation [WD 7.3.10.3]: unit flag, unit, load status, information. + self.r.bit()?; + self.r.double()?; + self.r.character()?; + self.r.integer()?; + if self.r.bit()? { + p.location = Some(self.transformation()?); + } + let n = self.count(1, "entity references")?; + for _ in 0..n { + p.overrides.extend(self.entity_reference()?); + } + self.markup_data()?; + self.views()?; + if self.r.bit()? { + self.filter()?; + } + let n = self.count(1, "display filters")?; + for _ in 0..n { + self.filter()?; + } + let n = self.count(1, "scene display parameters")?; + for _ in 0..n { + self.scene_display_parameters()?; + } + self.schema.skip_added_fields(PRODUCT_OCCURRENCE, &mut self.r)?; + self.user_data()?; + Ok(p) + } + + /// `FileStructureTree` (304) [WD 7.3.6], with its `FileStructure` (302) inside. + pub(super) fn file_structure_tree(&mut self) -> Result { + self.expect_type(FILE_STRUCTURE_TREE)?; + self.content_prc_base()?; + let n = self.count(1, "part definitions")?; + let mut tree = Tree::default(); + for _ in 0..n { + let (items, name) = self.part_definition()?; + tree.parts.push(items); + tree.part_names.push(name); + } + let n = self.count(1, "product occurrences")?; + for _ in 0..n { + tree.products.push(self.product_occurrence()?); + } + self.expect_type(FILE_STRUCTURE)?; + self.content_prc_base()?; + self.r.unsigned_integer()?; // next_available_index + self.r.unsigned_integer()?; // product_occurrence_index + 1 (the structure's root) + self.schema.skip_added_fields(FILE_STRUCTURE, &mut self.r)?; + self.schema.skip_added_fields(FILE_STRUCTURE_TREE, &mut self.r)?; + self.user_data()?; + Ok(tree) + } + + /// `ModelFile` (301) after its schema [WD 7.3.3; pdf-issues #724, #740]: `units_in_mm` is + /// always present. + pub(super) fn model_file(&mut self) -> Result { + self.expect_type(MODEL_FILE)?; + self.content_prc_base()?; + self.r.bit()?; // units_from_cad_file + self.r.double()?; // units_in_mm + let n = self.count(6, "root occurrences")?; + let mut roots = Vec::with_capacity(n); + for _ in 0..n { + let id = self.unique_id()?; + let index = self.r.unsigned_integer()?; + self.r.bit()?; // is_active + roots.push((id, index)); + } + Ok(ModelFile { roots }) + } +} diff --git a/crates/prc/src/tree/style.rs b/crates/prc/src/tree/style.rs new file mode 100644 index 0000000000..d313fb2f64 --- /dev/null +++ b/crates/prc/src/tree/style.rs @@ -0,0 +1,348 @@ +//! Graphics, styles, materials and the globals that hold them [WD 7.2.4, 7.3.5, 7.5]. + +use super::{ + FATHER_HERIT_COLOR, FILE_STRUCTURE_GLOBALS, LINE_PATTERN, MATERIAL, Matrix, PICTURE, RI_COORDINATE_SYSTEM, SON_HERIT_COLOR, STYLE, + TEXTURE_ALPHA_TEST, TEXTURE_APPLICATION, TEXTURE_DEFINITION, TEXTURE_FUNCTION_BLEND, TEXTURE_MAPPING_OPERATOR, TEXTURE_TRANSFORMATION, Vec3, +}; +use crate::PrcError; +use crate::container::offset; +use crate::entity::{Ctx, malformed}; + +/// One entity's `GraphicsContent` [WD 7.2.4]: `style` is `line_style_index + 1` (0 none), +/// `bits` the behaviour bits. `fs` is the file structure whose globals `style` indexes: the +/// one whose tree holds the entity. The walk sets it; a reader leaves it 0. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) struct Graphics { + pub(crate) style: u32, + pub(crate) bits: u16, + pub(crate) fs: usize, +} + +/// A straight (not premultiplied) colour, each channel 0-1; `a` is the opacity. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[non_exhaustive] +pub struct Rgba { + /// Red. + pub r: f64, + /// Green. + pub g: f64, + /// Blue. + pub b: f64, + /// Opacity: 0 transparent, 1 opaque. + pub a: f64, +} + +/// How a style's `transparency` combines with its material's diffuse alpha. ISO 14739-1 +/// defines both and not their combination, and gives a diffuse alpha of 0.0 no "unset" +/// meaning. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum StyleAlpha { + /// As [`Self::StyleWins`], except that a material diffuse alpha of exactly 0.0 under a + /// style with no transparency reads as unset, so opaque. Producers write 0.0 on the + /// materials of models meant to be seen; read literally, every part is invisible. + #[default] + ZeroUnset, + /// The style's transparency, when it has one, is the opacity; the material alpha, read + /// literally (0.0 invisible), applies only to a style without one. + StyleWins, + /// The opacity is material alpha × style transparency. + Multiply, +} + +/// A style's resolved colour, and whether its opacity comes from reading a zero material +/// alpha as unset. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct Paint { + pub(crate) rgba: Rgba, + pub(crate) alpha_unset: bool, +} + +/// A `Style` (701) as far as colour: `index` is `colour_or_material + 1`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Style { + pub(crate) is_material: bool, + pub(crate) index: u32, + pub(crate) transparency: Option, +} + +/// A `materials` entry as far as colour. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum Material { + /// `Material` (702): `diffuse + 1` (a colour index) and the diffuse alpha. + Plain { diffuse: u32, alpha: f64 }, + /// `TextureApplication` (711) [WD 7.5.6]: `material_generic_index + 1`, the material + /// under the texture. + Textured { base: u32 }, +} + +/// What `FileStructureGlobals` contributes to placing and colouring. +#[derive(Debug, Clone, PartialEq, Default)] +pub(crate) struct Globals { + /// Entry `i` places an item with biased local coordinate system `i + 1`. + pub(crate) systems: Vec, + /// RGB, `a` 1. + pub(crate) colours: Vec, + pub(crate) materials: Vec, + pub(crate) styles: Vec