From f84290b294a6f40b11b90c683c4226f3d73aa346 Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 08:23:56 -0400 Subject: [PATCH 01/25] perf(render): undo PNG predictors a byte row at a time (#5430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vendored hayro-syntax ran PNG predictors through the bit-level BitChunks/BitWriter machinery TIFF predictors need, although PNG filters are defined on bytes (ISO 32000-2 §7.4.4.4, PNG specification §9). A 993 x 936 RGB image took about 63 ms to unfilter whatever its row filters; undone on byte slices it takes about 3 ms (Up) to 16 ms (Paeth) in a release build. It also fixes a row that ends part-way through a pixel (three 4-bit samples at an odd width): the bit-level path dropped its last byte, each later row started a byte early and the picture came out sheared. TIFF predictor 2 keeps the old path. Vendored hayro-syntax patch 29. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/render/src/raster/tests.rs | 3 + .../src/raster/tests/png_predicted_images.rs | 69 ++++ vendor/README.md | 2 +- vendor/hayro-syntax/src/filter/lzw_flate.rs | 336 ++++++++++++------ 4 files changed, 308 insertions(+), 102 deletions(-) create mode 100644 crates/render/src/raster/tests/png_predicted_images.rs diff --git a/crates/render/src/raster/tests.rs b/crates/render/src/raster/tests.rs index a4e4404e6b..68b0c0cb08 100644 --- a/crates/render/src/raster/tests.rs +++ b/crates/render/src/raster/tests.rs @@ -7004,6 +7004,9 @@ mod default_colour_spaces; // (vendored hayro-syntax patch 28, hayro-interpret patch 55). mod devicen_inks; +// #5430: PNG-predicted Flate images are undone byte by byte (vendored hayro-syntax patch 29). +mod png_predicted_images; + // #3776: isolated DeviceCMYK groups blend in CMYK (vendored hayro-interpret patch 70, hayro // patch 27). mod isolated_cmyk_groups; diff --git a/crates/render/src/raster/tests/png_predicted_images.rs b/crates/render/src/raster/tests/png_predicted_images.rs new file mode 100644 index 0000000000..14675adab0 --- /dev/null +++ b/crates/render/src/raster/tests/png_predicted_images.rs @@ -0,0 +1,69 @@ +//! #5430: Flate images with PNG predictors (ISO 32000-2 §7.4.4.4) are undone a byte row at a +//! time (vendored hayro-syntax patch 29). The bit-level decoder they went through before dropped +//! the last byte of each filtered row that ends part-way through a pixel, so every later row +//! started a byte early and the picture came out sheared. + +use super::rgb_at; +use crate::{PageRenderer, RenderConfig, RenderRequest, RequestKind}; +use flate2::{Compression, write::ZlibEncoder}; +use std::io::Write; +use std::sync::Arc; + +/// The page is 150 × 200 pt, drawn at 72 dpi; the image fills it. +const WIDTH: usize = 150; +const HEIGHT: usize = 200; + +/// The colour of each of the image's four rows, top first, as 4-bit samples. +const ROWS: [[u8; 3]; 4] = [[15, 0, 0], [0, 15, 0], [0, 0, 15], [15, 15, 0]]; + +#[test] +fn a_four_bit_rgb_image_of_odd_width_with_up_predicted_rows_is_not_sheared() { + // Three 4-bit RGB pixels are 4.5 bytes, so each row is five bytes and ends half-way through a + // two-byte pixel. + let rows: Vec> = ROWS + .iter() + .map(|rgb| { + let nibbles: Vec = (0..3).flat_map(|_| rgb.iter().copied()).chain([0]).collect(); + nibbles.chunks(2).map(|pair| (pair[0] << 4) | pair[1]).collect() + }) + .collect(); + let mut predicted = Vec::new(); + let mut above = vec![0u8; 5]; + for row in &rows { + predicted.push(2); + predicted.extend(row.iter().zip(&above).map(|(b, a)| b.wrapping_sub(*a))); + above.clone_from(row); + } + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&predicted).unwrap(); + let data = encoder.finish().unwrap(); + + let content = "q 150 0 0 200 0 0 cm /Im0 Do Q"; + let mut pdf = format!( + "%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\ + 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\ + 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 150 200] /Resources << /XObject << /Im0 5 0 R >> >> /Contents 4 0 R >> endobj\n\ + 4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\ + 5 0 obj << /Type /XObject /Subtype /Image /Width 3 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 4 \ + /Filter /FlateDecode /DecodeParms << /Predictor 15 /Colors 3 /BitsPerComponent 4 /Columns 3 >> /Length {} >> stream\n", + content.len(), + data.len() + ) + .into_bytes(); + pdf.extend_from_slice(&data); + pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF"); + + let mut renderer = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); + let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false }); + assert!(page.error.is_none(), "{:?}", page.error); + assert_eq!((page.width, page.height), (WIDTH as u32, HEIGHT as u32)); + + // Each image pixel is 50 pt square; look at the middle of each. + for (row, rgb) in ROWS.iter().enumerate() { + let expected = rgb.map(|sample| sample * 17); + for column in 0..3 { + let (x, y) = (column * 50 + 25, row * 50 + 25); + assert_eq!(rgb_at(&page.rgba, WIDTH, x, y), expected, "image row {row}, column {column}"); + } + } +} diff --git a/vendor/README.md b/vendor/README.md index 84b2546bf3..dfba101330 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -43,7 +43,7 @@ Other resource categories retain their existing parent-chain lookup. | hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) `/Interpolate` governs the pre-resize too (#624): a minified image that does not request interpolation is pre-resized with nearest-neighbour, planned without the Catmull-Rom filter limits (`image_resampling_size` takes the flag); a Type 3 glyph image is sampled bicubically only when interpolated; a minified image with a matching soft mask is nearest-sampled straight into premultiplied RGBA; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them; (13) reading colours (#897): `RenderSettings::color_replacement` takes a `ColorReplacement` (text colour, background, `only_black`, `line_art`) that remaps solid text and line-art paint while rendering, glyph outlines counting as text and stencil-mask ink as line art; images, image alpha masks, soft masks and pattern cells keep their colours; `render_pdf` gains a `color_replacement: Option` argument (pass `None` for true colours); the upstream-only `[dev-dependencies.log]` entry is removed from `Cargo.toml` (log is a normal dependency since the logging patch), so `cargo test -p hayro` runs this crate's unit tests; (14) a shrunk, interpolated, axis-aligned image is snapped to whole device pixels: shrunk once to its rounded device rectangle and drawn 1:1 at the rounded corner, instead of a ceil-sized raster resampled again at a fractional scale and offset (`snap_minified_image`; scans looked soft next to Acrobat); (15) a soft mask of another size than its image is filtered when shrunk, whatever its `/Interpolate` (#1649): such a mask carries the shape on its own (Word saves text as a tiny black image with a glyph-bitmap mask), and nearest-neighbour dropped every stroke thinner than the shrink factor; (16) under a soft mask or a blend mode, axial and radial shadings are sampled instead of drawn as native gradients (#2128): vello_cpu 0.0.8 copies an opaque gradient over fully covered tiles without the mask or blend mode, so PowerPoint's faded gradient fills came out solid; (17) the renderer keeps annotation paints out of the reading-colours mapping between `begin_annotations` and `end_annotations` (#1548); (18) the page renders as one transparency group; under reading colours its background is painted inside it as the initial backdrop, so a Multiply highlight blends with the dark background instead of staying an opaque block, and otherwise the background stays outside, so a blend mode over an empty part of the page keeps its colour instead of blending with the white paper (#2970); (19) `set_paint` samples a mesh shading's texture over its triangles alone (`EncodedShadingPattern::extent`, padded rather than repeated) and skips a mesh with none on the canvas: a BricsCAD plan's 310 one-triangle `sh` each covered the whole tile, 2 s per 1024 px tile at any zoom (the watchdog gave the page up on a busy machine), now about 70 ms; (20) a tiling pattern whose step is too large for a cell pixmap, where what is painted (a fill, or a stroke with its width) can only show the first cell (PowerPoint's `/XStep 2147480100` for a picture fill drawn once), has its `/BBox` drawn at full scale with a transparent border and padded instead of tiled (`shows_one_cell`; a cell whose scale isn't finite and positive, from a `/BBox` too thin to divide by, keeps the old path: `usable_cell_scale`); shrinking the cell to fit the 3000 px limit of (3) left the picture under a pixel, so network diagrams lost their pictures (#3349); (22) knockout groups: while one is innermost, fills, strokes, glyph outlines and shadings are drawn with `Compose::Copy` (weighted by coverage), so each element replaces what earlier elements of the group drew instead of compositing over it; an image or nested group, drawn into a layer of its own, first has what is under its shape cleared (`knockout_image` for an unmasked image's rectangle, only when `draw_image` will draw it, decoded at the size it is drawn; a knockout erase pass that draws a nested group's content with every paint clearing instead of painting, a shading fill's clear clipped to the shading's `/BBox` as its paint is) and then composites over it (#2971); (23) backport of hayro 0.8: shadings that `as_svg_gradient` can represent are painted as a `vello_cpu` linear or two-point radial gradient instead of a texture sampled pixel by pixel through the shading function (InDesign fades are luminosity soft masks of one full-page axial `sh`: 40 on a report page took 15 s at 144 dpi, now under 1 s); without upstream's half-pixel shift, which suits vello_cpu 0.3 but moves 0.0.8's gradients off pixel centres; (25) a tiling cell is drawn at the scale that makes one `/XStep` (`/YStep`) exactly the cell pixmap's whole number of pixels (`tiling_cell_pixels`, at least 1 and at most the cap), so the repeated pixmap puts cells at whole multiples of the step at any device scale; drawing at the unrounded scale repeated the cell up to half a pixel off at every step, and the error grew with distance from the pattern's origin (ISO 32000-2 8.7.3.1) | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::non_interpolated_images_are_not_smoothed_when_minified`, `raster::tests::non_interpolated_gray_images_are_not_smoothed_when_minified`, `raster::tests::non_integer_minification_samples_the_same_columns`, `raster::tests::non_integer_minification_samples_colour_and_mask_from_the_same_columns`, `raster::tests::non_interpolated_alpha_images_keep_their_colours_and_mask_when_minified`, `raster::tests::non_interpolated_type3_glyph_images_are_not_smoothed_when_minified`, `raster::tests::image_resampling_nearest_accepts_wide_sources_the_filter_refuses`, `raster::tests::wide_non_interpolated_sources_draw_after_shrinking`; (11) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (12) `raster::tests::preview_renders_skip_tiny_images_but_finals_draw_everything`; (13) `color_replacement_tests::{black_maps_to_text_colour, white_maps_to_background, midgrey_interpolates, alpha_copied_verbatim, only_black_threshold, line_art_false_skips_paths_but_not_text}`, `color_replacement_render_tests::{color_replacement_maps_text_and_line_art, color_replacement_maps_stencil_mask_ink}`; (14) `raster::tests::interpolated_minified_image_edges_snap_to_whole_pixels` (`cargo test -p hayro`); (15) `raster::tests::shrunk_soft_mask_larger_than_its_image_keeps_thin_strokes`; (16) `raster::tests::soft_mask_applies_to_a_shading_pattern_fill`; (17), (18) `raster::tests::reading_colors_keep_highlight_annotations_readable`; (19) `mesh_extent_tests::{a_mesh_paints_its_triangles_alone, a_mesh_keeps_the_graphics_state_alpha, a_mesh_off_the_canvas_paints_nothing}` (`cargo test -p hayro`); (20) `raster::tests::a_tiling_pattern_drawn_once_shows_its_cell`, `raster::tests::a_large_step_still_tiles_where_the_next_cell_shows`, `raster::tests::a_tiling_cell_too_thin_to_scale_keeps_the_old_path`; (22) `raster::tests::knockout_groups_replace_what_their_elements_cover`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (25) `tiling_cell_placement::{far_cells_of_a_fill_sit_at_whole_steps, far_cells_sit_at_whole_steps_under_a_turned_scaled_matrix, far_cells_of_a_stroke_sit_at_whole_steps, a_cell_pixmap_spans_exactly_one_step}` (`cargo test -p pdfcraft-render --test it`) | | hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer); (4) a segment header's long-form referred-to segment count is capped at `u16::MAX` and a larger count is refused before its retention or referred-to bytes are read: the 29-bit count went straight to `Vec::with_capacity`, a reservation of about 2 GiB whose failure aborts the process (from storytold/craft-libs#176) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate`; (4) `segment::tests::test_referred_to_count_is_capped` (`cargo test --manifest-path vendor/hayro-jbig2/Cargo.toml`) | | lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589); (4) the object lookup keeps its cycle history through compressed objects, requires an ordinary in-file container, and caps the chain of indirect lookups at 16 (a chain of new objects, each a `/Length` compressed in a container whose own `/Length` names the next, or a stream whose `/Length` names the next stream, overflowed the stack: a dev build's 2 MiB thread within 800 hops); (5) object-stream members are parsed within their own extent (up to the next distinct offset), an entry whose offset repeats an earlier entry's is skipped (so a repeated object number can resolve to its earlier entry), and offsets are processed in index order; (6) `find_latest_trailer`: a rebuild takes a `/Type /XRef` stream dictionary as the trailer when the file has no `trailer` keyword (the latest in the file wins, as a later `trailer` would), so a concatenated or truncated file keeps its /Info and /ID instead of failing to load; a candidate whose /Root is the catalog beats one whose /Root is another dictionary, and an in-file /Root must match its generation (#2365) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding`; (4) `inspect::tests::compressed_object_self_container_does_not_recurse`, `inspect::tests::compressed_object_mutual_containers_do_not_recurse`, `inspect::tests::object_stream_length_cycle_does_not_recurse`, `inspect::tests::compressed_object_container_chain_does_not_recurse`, `inspect::tests::compressed_length_chain_does_not_recurse`, `inspect::tests::plain_stream_length_chain_does_not_recurse`, `inspect::tests::compressed_objects_and_indirect_stream_length_still_load`, `inspect::tests::compressed_length_in_a_container_with_an_indirect_length_still_loads`; (5) `inspect::tests::object_stream_member_extents_bound_owned_strings`, `inspect::tests::object_stream_member_extents_preserve_index_order_and_whitespace`; (6) `inspect::tests::rebuilt_file_keeps_the_info_of_its_cross_reference_stream`, `inspect::tests::rebuilt_file_prefers_a_trailer_whose_root_is_the_catalog` | -| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); an EI followed by a delimiter (EI%..., EI[...) ends the image like one followed by white space; only a regular character after EI skips the candidate; (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW, RunLength and ASCII85 never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had; (14) revision 2–4 passwords are tried as given and then in PDFDocEncoding (`legacy_passwords` in `crypto/mod.rs`, which resolves the upstream `TODO: Convert to PDFDocEncoding`): passwords arrive as UTF-8 text, so a password with characters outside ASCII (`Çok güzel`, `ılık€`) written in PDFDocEncoding, as ISO 32000-2 §7.6.4.3.2 asks and qpdf and Acrobat write it, was always wrong; (15) page content is read a piece at a time (`page::ContentReader`, `Page::content_reader`): plain streams and Flate streams without a predictor are decoded as they are read, with a budget of 64 times their compressed size and never less than `MAX_DECODED_STREAM` (a stream that expands past it is cut and reported); other streams are decoded whole, as before, and share `MAX_DECODED_STREAM`. The content tokenizer reads a window at a time (`UntypedIter::new_window`, `TypedIter::needs_more`, `TypedIter::committed`): an instruction that runs past the window is not decided until more data follows, which `Reader` records by flagging reads and `at_end` past the end of the data, including through the clones the parser makes. The Flate decoder keeps its own 32 KiB history, so it pauses at any output size. `ContentReader::new` reuses raw/decrypted stream bytes for budget accounting and piece creation, and `decoded_raw_within` applies the existing Whole-stream filters and limits to those bytes without decrypting twice; (16) revision 5–6 (AES-256) passwords are tried in their SASLprep form (RFC 4013, then UTF-8, at most 127 bytes, as ISO 32000-2 §7.6.4.3.3 Algorithm 2.A asks and pdfcraft-crypt writes them) and then, if that differs, as given (`aes256_passwords` in `crypto/mod.rs`, which resolves the upstream `TODO: Do the above.`; `stringprep`, already in the tree through pdfcraft-crypt, is an optional dependency enabled by `std`, and without `std` passwords are used as given): the renderer used the password as typed, so one that SASLprep changes (a decomposed `café`, a no-break space, the fullwidth `ab` a Japanese IME types in full-width mode) was wrong, although pdfcraft-cos accepted it: protecting a document with such an open password was rolled back ("the password is incorrect"), and a file with such a password did not open with it.; (17) CCITT `/Rows 0` means the height is not predetermined (ISO 32000-2 Table 11), not zero rows: scanned Group 4 pages written that way rendered blank; (18) The streaming Flate decoder (`lzw_flate::flate::fallback`) copies a back-reference in runs of up to its distance (`extend_from_within`), not byte by byte. The copy reads only bytes already in the output, and a copy whose source does not exist still writes nothing, so the bytes are those a byte-by-byte copy writes. Inflating the A1 sample's content stream (278.6 MB) takes about 14% less time (0.66 s to 0.56 s); the output is identical; (19) CCITT `/Rows` below the image's `/Height` decodes to the height: a plotter driver wrote `/Rows` equal to `/Columns`, so only the first rows were decoded and the rest drawn black (#3308) (20) `stream` followed by spaces or tabs and then the line break (`stream \r\n`, as Microsoft Reporting Services writes it) starts the data after the line break; the content stream was taken to start at the space and the page rendered blank.; (21) a JP2 colour specification naming an enumerated colour space the decoder refuses (CIEJab in a veraPDF fixture, YCbCr, PhotoYCC, YPbPr, bilevel…) no longer fails the image: it is decoded again from a copy whose `colr` names the space the decoder assumes without one, greyscale, sRGB or CMYK by component count, as MuPDF and Poppler draw it (`without_refused_enumerated_colour`; the page drew blank, #3306); (22) an object stream's index of `objnum offset` pairs is parsed once per stream and kept beside its decoded data (`ObjectStreamIndexes`, `XRef::with_object_stream_indexes`), and an offset past `usize` is malformed instead of wrapping: `XRef::get_with` parsed the whole index again for every object it fetched from the stream, so reading all of a stream of N objects took N² steps (a Firefox page whose 65,542 objects share one stream took 37 s to draw and 40 s to extract its text); (23) the inline-image end-of-data look-ahead no longer rejects a candidate `EI` because a byte above 0x7F lies between it and the next one: when the candidate is the end, those bytes are the content stream that follows the image, and its strings carry such bytes (WinAnsi `(Grüße) Tj`, the two-byte codes of any Type 0 font), so the operator check that follows decides alone, bounded as in (4) (a scanned page whose OCR text held an accented word and a later capital "EI" — EINGANG, BEI, KLEIN — drew and extracted nothing after the image: 40 of 275 pages across five real documents); (24) `xref::prev_offset`: a trailer `/Prev` written as a reference `N G R` is read as the offset that object holds when the section's own entries place it (iText's `indirectPrev.pdf`; each section's entries are read before its `Prev` is followed), and otherwise as offset N (tiff2pdf; JHOVE's PDF-HUL-157 files, #2366), as `pdfcraft-cos` and other readers take it, so the earlier sections' pages are seen instead of one; (25) direct arrays and dictionaries share a reader-local nesting cap of 128, counted while skipping and while parsing a dictionary (its lenient repair reads, rather than skips, an object found where a key belongs, so `<< << …` recursed through that read); deeper objects return `None` instead of overflowing the stack (including unused trailer values, trailer entries, content operands and marked-content properties); a chain of 65 direct stitching functions now stops at this cap, before the 64-function guard; (26) an xref stream subsection must be backed by its decoded data: all-zero `/W` widths (an entry reads no bytes) and a declared count longer than the data are refused before any entry is inserted, where a tiny stream with a large `/Size` or `/Index` count inserted that many entries (from storytold/craft-libs#239); a subsection running past object number `i32::MAX` is refused instead of overflowing or turning negative; (27) an xref stream's type field is read big-endian at its full `/W[0]` width, where only its first byte was read and wider fields shifted every later entry, and `/W[0]` over 8 bytes is refused like `/W[1]` (from storytold/craft-libs#241); (28) a content instruction keeps up to 256 operands (`MAX_OPERANDS`) instead of 10, and past that drops the oldest instead of ending the content stream: the eleventh operand of a twelve-ink DeviceN `scn` (ISO 32000-2 §8.6.6.5 allows any number of components) blanked the rest of the page (#4260) | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`, `content::tests::inline_image_ei_may_be_followed_by_a_delimiter`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices`; (13) `raster::tests::user_unit_scales_the_page_and_everything_on_it`; (14) `crypto::pdfcraft_password_tests`, `pdfcraft-engine` `tests::legacy_passwords_with_turkish_characters_open`; (15) `content::tests::windows_keep_every_instruction_at_every_split`, `content::tests::windows_keep_every_instruction_in_pieces_of_any_size`, `content::tests::windows_match_the_slice_on_random_content`, `content::tests::windows_drop_blanks_and_keep_a_comment_that_may_go_on`, `page::content_reader_tests::content_past_the_cap_is_read_to_the_end`, `page::content_reader_tests::a_stream_that_expands_past_the_budget_is_cut_there`, `page::content_reader_tests::a_small_expansion_under_the_cap_is_not_cut`, `raster::tests::page_content_past_the_old_cap_renders`, `raster::tests::content_reader_returns_the_same_bytes_in_any_pieces`, `raster::tests::content_bombs_stop_at_the_budget_and_say_so`, `page::content_reader_tests::plain_and_whole_pieces_keep_their_bytes`, `page::content_reader_tests::repeated_content_reference_counts_compressed_bytes_once`, `page::content_reader_tests::whole_cap_still_counts_later_unique_streams`; (16) `crypto::pdfcraft_password_tests::aes256_passwords_are_tried_in_their_saslprep_form_then_as_given`, `crypto::pdfcraft_password_tests::aes256_passwords_open_as_typed_and_in_their_saslprep_form`, `pdfcraft-engine` `tests::aes256_open_passwords_that_saslprep_changes_protect_and_reopen_as_typed`, `tests::aes256_files_whose_passwords_saslprep_changes_open_as_typed`; (17) `raster::tests::group4_fax_with_rows_zero_decodes_to_the_image_height`; (18) `filter::lzw_flate::tests::back_reference_longer_than_its_distance_repeats_its_source`, `back_reference_cut_by_the_limit_resumes_in_the_next_call` and `back_reference_reads_bytes_written_by_an_earlier_call`; each fails when the copy returns the wrong remaining count; (19) `raster::tests::group4_fax_with_rows_below_its_height_decodes_to_the_image_height`; (20) `raster::tests::space_before_the_line_break_after_stream_still_renders`; (21) `raster::tests::a_jp2_image_in_a_refused_enumerated_colour_space_is_drawn`, `filter::jpx::tests::refused_enumerated_colour_spaces_become_the_component_counts_default`, `filter::jpx::tests::what_the_decoder_handles_or_cant_be_read_is_left_alone` (`cargo test --manifest-path vendor/hayro-syntax/Cargo.toml`); (22) `raster::tests::objects_in_one_large_object_stream_are_read_in_linear_time`; (23) `text::tests::text_after_an_inline_image_survives_a_non_ascii_byte_before_a_later_ei`, `content::tests::an_inline_image_ends_at_its_own_ei_when_later_text_holds_a_non_ascii_byte` (hayro-syntax); (24) `xref::tests::prev_as_reference_reaches_the_earlier_section`, `xref::tests::prev_as_reference_to_an_object_holding_the_offset_reads_that_offset`, `pdfcraft-cos` `document::tests::a_prev_written_as_a_reference_still_reaches_the_earlier_sections`, `document::tests::a_prev_reference_to_an_object_holding_the_offset_reads_that_offset`; (25) `raster::tests::deeply_nested_direct_objects_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_content_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_the_trailer_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_as_marked_content_properties_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_are_read_up_to_the_nesting_cap`, `raster::tests::nesting_at_the_cap_fits_a_512_kib_stack` (native only), `raster::tests::function_limits_depth_guard_refuses_a_chain_of_references`; (26) `xref::tests::xref_stream_entries_need_backing_bytes`; (27) `xref::tests::xref_stream_type_field_uses_the_full_w0_width`, `xref::tests::xref_stream_loader_rejects_oversized_w0` (`cargo test --manifest-path vendor/hayro-syntax/Cargo.toml`); (28) `content::ops::tests::scn_with_twelve_components_keeps_them_all`, `content::ops::tests::operands_past_the_cap_keep_the_latest_in_order` (hayro-syntax), `raster::tests::devicen_inks::a_twelve_ink_devicen_colour_paints`, `raster::tests::devicen_inks::a_four_ink_devicen_colour_paints`, `raster::tests::devicen_inks::an_operator_with_hundreds_of_operands_does_not_end_the_content` | +| hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); an EI followed by a delimiter (EI%..., EI[...) ends the image like one followed by white space; only a regular character after EI skips the candidate; (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW, RunLength and ASCII85 never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had; (14) revision 2–4 passwords are tried as given and then in PDFDocEncoding (`legacy_passwords` in `crypto/mod.rs`, which resolves the upstream `TODO: Convert to PDFDocEncoding`): passwords arrive as UTF-8 text, so a password with characters outside ASCII (`Çok güzel`, `ılık€`) written in PDFDocEncoding, as ISO 32000-2 §7.6.4.3.2 asks and qpdf and Acrobat write it, was always wrong; (15) page content is read a piece at a time (`page::ContentReader`, `Page::content_reader`): plain streams and Flate streams without a predictor are decoded as they are read, with a budget of 64 times their compressed size and never less than `MAX_DECODED_STREAM` (a stream that expands past it is cut and reported); other streams are decoded whole, as before, and share `MAX_DECODED_STREAM`. The content tokenizer reads a window at a time (`UntypedIter::new_window`, `TypedIter::needs_more`, `TypedIter::committed`): an instruction that runs past the window is not decided until more data follows, which `Reader` records by flagging reads and `at_end` past the end of the data, including through the clones the parser makes. The Flate decoder keeps its own 32 KiB history, so it pauses at any output size. `ContentReader::new` reuses raw/decrypted stream bytes for budget accounting and piece creation, and `decoded_raw_within` applies the existing Whole-stream filters and limits to those bytes without decrypting twice; (16) revision 5–6 (AES-256) passwords are tried in their SASLprep form (RFC 4013, then UTF-8, at most 127 bytes, as ISO 32000-2 §7.6.4.3.3 Algorithm 2.A asks and pdfcraft-crypt writes them) and then, if that differs, as given (`aes256_passwords` in `crypto/mod.rs`, which resolves the upstream `TODO: Do the above.`; `stringprep`, already in the tree through pdfcraft-crypt, is an optional dependency enabled by `std`, and without `std` passwords are used as given): the renderer used the password as typed, so one that SASLprep changes (a decomposed `café`, a no-break space, the fullwidth `ab` a Japanese IME types in full-width mode) was wrong, although pdfcraft-cos accepted it: protecting a document with such an open password was rolled back ("the password is incorrect"), and a file with such a password did not open with it.; (17) CCITT `/Rows 0` means the height is not predetermined (ISO 32000-2 Table 11), not zero rows: scanned Group 4 pages written that way rendered blank; (18) The streaming Flate decoder (`lzw_flate::flate::fallback`) copies a back-reference in runs of up to its distance (`extend_from_within`), not byte by byte. The copy reads only bytes already in the output, and a copy whose source does not exist still writes nothing, so the bytes are those a byte-by-byte copy writes. Inflating the A1 sample's content stream (278.6 MB) takes about 14% less time (0.66 s to 0.56 s); the output is identical; (19) CCITT `/Rows` below the image's `/Height` decodes to the height: a plotter driver wrote `/Rows` equal to `/Columns`, so only the first rows were decoded and the rest drawn black (#3308) (20) `stream` followed by spaces or tabs and then the line break (`stream \r\n`, as Microsoft Reporting Services writes it) starts the data after the line break; the content stream was taken to start at the space and the page rendered blank.; (21) a JP2 colour specification naming an enumerated colour space the decoder refuses (CIEJab in a veraPDF fixture, YCbCr, PhotoYCC, YPbPr, bilevel…) no longer fails the image: it is decoded again from a copy whose `colr` names the space the decoder assumes without one, greyscale, sRGB or CMYK by component count, as MuPDF and Poppler draw it (`without_refused_enumerated_colour`; the page drew blank, #3306); (22) an object stream's index of `objnum offset` pairs is parsed once per stream and kept beside its decoded data (`ObjectStreamIndexes`, `XRef::with_object_stream_indexes`), and an offset past `usize` is malformed instead of wrapping: `XRef::get_with` parsed the whole index again for every object it fetched from the stream, so reading all of a stream of N objects took N² steps (a Firefox page whose 65,542 objects share one stream took 37 s to draw and 40 s to extract its text); (23) the inline-image end-of-data look-ahead no longer rejects a candidate `EI` because a byte above 0x7F lies between it and the next one: when the candidate is the end, those bytes are the content stream that follows the image, and its strings carry such bytes (WinAnsi `(Grüße) Tj`, the two-byte codes of any Type 0 font), so the operator check that follows decides alone, bounded as in (4) (a scanned page whose OCR text held an accented word and a later capital "EI" — EINGANG, BEI, KLEIN — drew and extracted nothing after the image: 40 of 275 pages across five real documents); (24) `xref::prev_offset`: a trailer `/Prev` written as a reference `N G R` is read as the offset that object holds when the section's own entries place it (iText's `indirectPrev.pdf`; each section's entries are read before its `Prev` is followed), and otherwise as offset N (tiff2pdf; JHOVE's PDF-HUL-157 files, #2366), as `pdfcraft-cos` and other readers take it, so the earlier sections' pages are seen instead of one; (25) direct arrays and dictionaries share a reader-local nesting cap of 128, counted while skipping and while parsing a dictionary (its lenient repair reads, rather than skips, an object found where a key belongs, so `<< << …` recursed through that read); deeper objects return `None` instead of overflowing the stack (including unused trailer values, trailer entries, content operands and marked-content properties); a chain of 65 direct stitching functions now stops at this cap, before the 64-function guard; (26) an xref stream subsection must be backed by its decoded data: all-zero `/W` widths (an entry reads no bytes) and a declared count longer than the data are refused before any entry is inserted, where a tiny stream with a large `/Size` or `/Index` count inserted that many entries (from storytold/craft-libs#239); a subsection running past object number `i32::MAX` is refused instead of overflowing or turning negative; (27) an xref stream's type field is read big-endian at its full `/W[0]` width, where only its first byte was read and wider fields shifted every later entry, and `/W[0]` over 8 bytes is refused like `/W[1]` (from storytold/craft-libs#241); (28) a content instruction keeps up to 256 operands (`MAX_OPERANDS`) instead of 10, and past that drops the oldest instead of ending the content stream: the eleventh operand of a twelve-ink DeviceN `scn` (ISO 32000-2 §8.6.6.5 allows any number of components) blanked the rest of the page (#4260); (29) PNG predictors (`lzw_flate::unfilter_png`) are undone a byte row at a time on slices, as the PNG filters are defined, instead of through the bit-level readers TIFF predictors need: a 993 × 936 RGB image took about 63 ms to unfilter whatever its row filters, and now takes about 3 ms (Up) to 16 ms (Paeth) in a release build (#5430); a row ending part-way through a pixel (three 4-bit samples, an odd width) keeps its last byte, where the bit-level readers dropped it and each later row started a byte early, shearing the picture | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`, `content::tests::inline_image_ei_may_be_followed_by_a_delimiter`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices`; (13) `raster::tests::user_unit_scales_the_page_and_everything_on_it`; (14) `crypto::pdfcraft_password_tests`, `pdfcraft-engine` `tests::legacy_passwords_with_turkish_characters_open`; (15) `content::tests::windows_keep_every_instruction_at_every_split`, `content::tests::windows_keep_every_instruction_in_pieces_of_any_size`, `content::tests::windows_match_the_slice_on_random_content`, `content::tests::windows_drop_blanks_and_keep_a_comment_that_may_go_on`, `page::content_reader_tests::content_past_the_cap_is_read_to_the_end`, `page::content_reader_tests::a_stream_that_expands_past_the_budget_is_cut_there`, `page::content_reader_tests::a_small_expansion_under_the_cap_is_not_cut`, `raster::tests::page_content_past_the_old_cap_renders`, `raster::tests::content_reader_returns_the_same_bytes_in_any_pieces`, `raster::tests::content_bombs_stop_at_the_budget_and_say_so`, `page::content_reader_tests::plain_and_whole_pieces_keep_their_bytes`, `page::content_reader_tests::repeated_content_reference_counts_compressed_bytes_once`, `page::content_reader_tests::whole_cap_still_counts_later_unique_streams`; (16) `crypto::pdfcraft_password_tests::aes256_passwords_are_tried_in_their_saslprep_form_then_as_given`, `crypto::pdfcraft_password_tests::aes256_passwords_open_as_typed_and_in_their_saslprep_form`, `pdfcraft-engine` `tests::aes256_open_passwords_that_saslprep_changes_protect_and_reopen_as_typed`, `tests::aes256_files_whose_passwords_saslprep_changes_open_as_typed`; (17) `raster::tests::group4_fax_with_rows_zero_decodes_to_the_image_height`; (18) `filter::lzw_flate::tests::back_reference_longer_than_its_distance_repeats_its_source`, `back_reference_cut_by_the_limit_resumes_in_the_next_call` and `back_reference_reads_bytes_written_by_an_earlier_call`; each fails when the copy returns the wrong remaining count; (19) `raster::tests::group4_fax_with_rows_below_its_height_decodes_to_the_image_height`; (20) `raster::tests::space_before_the_line_break_after_stream_still_renders`; (21) `raster::tests::a_jp2_image_in_a_refused_enumerated_colour_space_is_drawn`, `filter::jpx::tests::refused_enumerated_colour_spaces_become_the_component_counts_default`, `filter::jpx::tests::what_the_decoder_handles_or_cant_be_read_is_left_alone` (`cargo test --manifest-path vendor/hayro-syntax/Cargo.toml`); (22) `raster::tests::objects_in_one_large_object_stream_are_read_in_linear_time`; (23) `text::tests::text_after_an_inline_image_survives_a_non_ascii_byte_before_a_later_ei`, `content::tests::an_inline_image_ends_at_its_own_ei_when_later_text_holds_a_non_ascii_byte` (hayro-syntax); (24) `xref::tests::prev_as_reference_reaches_the_earlier_section`, `xref::tests::prev_as_reference_to_an_object_holding_the_offset_reads_that_offset`, `pdfcraft-cos` `document::tests::a_prev_written_as_a_reference_still_reaches_the_earlier_sections`, `document::tests::a_prev_reference_to_an_object_holding_the_offset_reads_that_offset`; (25) `raster::tests::deeply_nested_direct_objects_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_content_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_the_trailer_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_as_marked_content_properties_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_are_read_up_to_the_nesting_cap`, `raster::tests::nesting_at_the_cap_fits_a_512_kib_stack` (native only), `raster::tests::function_limits_depth_guard_refuses_a_chain_of_references`; (26) `xref::tests::xref_stream_entries_need_backing_bytes`; (27) `xref::tests::xref_stream_type_field_uses_the_full_w0_width`, `xref::tests::xref_stream_loader_rejects_oversized_w0` (`cargo test --manifest-path vendor/hayro-syntax/Cargo.toml`); (28) `content::ops::tests::scn_with_twelve_components_keeps_them_all`, `content::ops::tests::operands_past_the_cap_keep_the_latest_in_order` (hayro-syntax), `raster::tests::devicen_inks::a_twelve_ink_devicen_colour_paints`, `raster::tests::devicen_inks::a_four_ink_devicen_colour_paints`, `raster::tests::devicen_inks::an_operator_with_hundreds_of_operands_does_not_end_the_content`; (29) `filter::lzw_flate::pdfcraft_png_predictor_tests` (hayro-syntax: every filter type at 1–16 bits and 1–4 colours against the PNG specification's byte-by-byte definition, a row ending part-way through a pixel, Average and Paeth over 6-byte pixels), `raster::tests::png_predicted_images::a_four_bit_rgb_image_of_odd_width_with_up_predicted_rows_is_not_sheared` | | winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. (2) on Wayland, a seat's text input is destroyed when the seat goes away, not whenever it loses a capability: losing any capability (a touch screen or tablet unplugged, a remote-desktop session ending) destroyed it, and only an added capability recreated it, so IME stopped working until restart. Port of [winit#4747](https://github.com/rust-windowing/winit/pull/4747); (3) on Windows, an IME composition reports its cursor from `GCS_CURSORPOS` only when `WM_IME_COMPOSITION` says it is set, and at the end of the composition otherwise: the Microsoft Korean IME never sets it, so the cursor read as 0 (egui then drew no caret while composing; it now draws it after the syllable). Port of [winit#4746](https://github.com/rust-windowing/winit/pull/4746); (4) on Windows, a hidden window is not maximized until it is shown, and a hidden window shown while maximized gets one `ShowWindow(SW_MAXIMIZE)`: `SW_MAXIMIZE` shows a hidden window, so PdfCraft, which eframe keeps hidden until its first frame is drawn, appeared unpainted (white) when it restored a maximized window, and could lose its focus. Port of [winit#4587](https://github.com/rust-windowing/winit/pull/4587) (the later, focus-safe form of [winit#4575](https://github.com/rust-windowing/winit/pull/4575)); (5) on X11, when allowing or disallowing IME replaces the focused window's input context (egui does it each time a text field gains or loses focus), the new context is focused: no focus event follows, so it stayed unfocused until the window lost and regained focus. Port of [winit#4727](https://github.com/rust-windowing/winit/pull/4727); (6) on macOS, `insertText` commits through IME when a composition was started (`pending_commit`, set by `setMarkedText`) rather than when marked text is left: input methods that end a composition with `unmarkText` before `insertText` lost the text, and Apple Korean dropped the key typed after a syllable (`한` then `5` or `;`); other `insertText` calls stay `KeyboardInput`; a commit outside a key press (a candidate clicked with the mouse, dictation) clears the marked text and leaves no key to swallow, as in [winit#4748](https://github.com/rust-windowing/winit/pull/4748) (without it the next key, such as Backspace, was lost). `validAttributesForMarkedText` lists the underline and marked-clause attributes, without which some input methods don't compose. Port of [winit#4650](https://github.com/rust-windowing/winit/pull/4650), the 0.30 backport of [winit#4651](https://github.com/rust-windowing/winit/pull/4651) (merged on winit master), with the underline attribute spelled as AppKit's `NSUnderlineStyleAttributeName` (`NSUnderline`); (7) on macOS, disallowing IME with a composition in progress (focus leaving a text field) tells the input method to discard its marked text (`discardMarkedText`): it kept composing it once IME was allowed again, so the abandoned syllable came back (`하`, then `가` typed in the next field, became `학`, then `하` + `가`; Japanese `か` then `さ` became `傘`). Port of [winit#4745](https://github.com/rust-windowing/winit/pull/4745); (8) on macOS, text inserted outside a key press without a composition (the emoji picker) is reported as `Ime::Commit`; it was dropped. Port of the emoji-picker part of [winit#4749](https://github.com/rust-windowing/winit/pull/4749) (0.30 backport of [winit#4748](https://github.com/rust-windowing/winit/pull/4748)); its `selectedRange` change for dictation is left out because it switches on press-and-hold for every held key (an accent menu instead of key repeat, and the chosen accent added after the repeated letters); (9) on macOS, a key whose text was committed through IME isn't also sent as `KeyboardInput` unless AppKit forwards it as a command (Enter): Apple Korean answers Space with no syllable in progress with `setMarkedText(" ")`, `insertText(" ")`, `setMarkedText("")`, and the last call reset winit's state, so the space arrived twice. Same goal as [winit#4478](https://github.com/rust-windowing/winit/pull/4478), whose changes don't reach this call order (macOS 14); to be proposed upstream there; (10) on macOS, when the first key in a fresh app reaches Apple Korean's `insertText` as a raw compatibility jamo instead of starting a composition, that key is interpreted once more, synchronously, before it is sent as `KeyboardInput` (g k s typed ㅎㅏㄴ instead of 한); only an unmodified, non-repeated first key whose one callback inserts that same jamo, with IME enabled and nothing composed, focus and input source unchanged, is retried (`ime_startup.rs`). Port of [winit#4744](https://github.com/rust-windowing/winit/pull/4744), with its policy tests.; (11) on X11 without `Xft.dpi`, the scale factor comes from the X screen's configured DPI (its size in pixels and millimetres) before the output's physical density from RandR: Xorg can deliberately run a dense panel at 96 DPI, and only winit applications then looked zoomed; `Xft.dpi` still wins, `WINIT_X11_SCALE_FACTOR` still overrides, and a screen reporting no size keeps the RandR fallback. PdfCraft patch (#1150, replacing the launcher pin proposed in #908). (12) on macOS, `WinitView` answers `mouseDownCanMoveWindow` with the window's `movable_by_window_background` attribute (NO unless asked): NSView answers YES for a view that isn't opaque, so with the transparent title bar and full-size content view PdfCraft uses, AppKit moved the window for any drag that began in the 28 pt title-bar band before the app saw it, and document tabs (which live in that band) could not be dragged to reorder them. The empty strip still moves the window through `ViewportCommand::StartDrag`. PdfCraft patch. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build; (13) on Wayland, pixel-only wheel, wheel-tilt and continuous-source scroll events report `Moved` rather than starting an unterminated finger gesture, so egui follows modifier release instead of keeping Ctrl zoom latched (#991). Finger gestures and unknown sources retain their existing phase policy | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11; (2) `pdfcraft` `tests/winit_wayland_text_input.rs`; the capability change itself needs a Wayland session and an input method; (3) `pdfcraft` `tests/winit_windows_ime_cursor.rs`; composing itself needs the Microsoft Korean IME; (4) `pdfcraft` `tests/winit_windows_hidden_maximized.rs`; the start-up itself needs Windows and a window that was closed maximized; (5) `pdfcraft` `tests/winit_x11_ime_focus.rs`; switching fields itself needs an X11 session and an input method (IBus, fcitx5); (6) `pdfcraft` `tests/winit_macos_ime_commit.rs`; composing itself needs macOS and an input method; (7) `pdfcraft` `tests/winit_macos_ime_discard.rs`; composing itself needs macOS and an input method; (8) `pdfcraft` `tests/winit_macos_emoji_picker.rs`; the picker itself needs macOS; (9) `pdfcraft` `tests/winit_macos_korean_space.rs`; composing itself needs macOS and Apple Korean; (10) `pdfcraft` `tests/winit_macos_korean_first_key.rs` and `ime_startup.rs`'s own tests (run them with `rustc --test`); typing itself needs macOS and Apple Korean; (11) `pdfcraft` `tests/winit_x11_dpi.rs` keeps the patch in the build, and `util::randr` tests the policy (run them in the vendored crate); the scale itself needs an X11 session; (12) `pdfcraft` `tests/winit_macos_title_bar_drag.rs` keeps the patch in the build; the drag itself needs macOS and a pointer; (13) `pdfcraft` `tests/winit_wayland_scroll.rs` compiles the production phase policy and exercises modifier release through egui; live COSMIC confirmation remains open | | egui-wgpu | 0.36.2 | MIT OR Apache-2.0 | (1) `winit::surface_fit`: a window's surface is configured within the device's `max_texture_dimension_2d`, both sides shrunk by one factor that egui then draws at (`pixels_per_point × render_scale`), so the whole window is drawn into the surface (on DX12 it is stretched over the window and lines up with the pointer; wgpu's GL backend copies it unscaled into a corner); the longer side becomes exactly the limit; the depth and MSAA textures follow, and a recreated surface starts again from the window's own size. egui-wgpu configured the window's size as is, and `Surface::configure` panics beyond the limit: a window restored at another monitor's scale (opening an Outlook attachment on a mixed-DPI laptop asked an 8192-pixel device for 8600 × 3423, #577) or stretched across monitors closed PdfCraft, before its window appeared when it happened at start-up ([emilk/egui#8361](https://github.com/emilk/egui/issues/8361)). The published crate leaves out egui's licence files; `LICENSE-APACHE` and `LICENSE-MIT` are from the 0.36.2 tag; (2) `winit::catch_errors`: a new window's surface is configured inside wgpu error scopes (validation, out of memory, internal), and a failure is returned as `WgpuError::ConfigureSurface` (for the root window at start-up eframe returns it as `Error::Wgpu`, which PdfCraft retries with OpenGL); nothing is kept for the window. wgpu's default handler panicked instead, and eframe configures the root surface before the app is created, so a driver that can't configure one closed PdfCraft at launch (a 2015 Intel GPU through wgpu's GL backend timed out waiting for the GPU to go idle, `GpuWaitTimeout`, #519). Unchanged, so still a panic: an error in a later reconfigure (resize, a lost surface), a device lost during the first configure (wgpu reports it only to a callback), and a window shown at zero size (first configured on resize) | (1) `pdfcraft` `tests::a_window_larger_than_the_gpu_limit_gets_a_surface_within_it`, `tests::surface_fit_never_empties_or_overflows_a_surface`, and `tests::vendor_provenance_egui_wgpu_surface_fit_identity`, which keeps the patch in the build and in the paths that size a surface; (2) `pdfcraft` `tests::a_gpu_error_while_a_window_is_set_up_is_returned_not_a_panic` (a real device: an error inside `catch_errors` is returned, not a panic) and `tests::vendor_provenance_egui_wgpu_catch_errors_identity`, which keeps it around `add_surface`'s configure | | vello_common | 0.0.8 | Apache-2.0 OR MIT | (1) `clip::intersect` (the clip step every clipped path passes through) starts its row walk at the first row both paths have strips in, not the earlier of the two first rows. The walk visited every row from the top of the clip for each path (on the A1 sample's full-page canvas, about half its strip rows per stroke), and rows where one path has no strips produce no output, so the strips are unchanged (vello_common 0.1 starts at the later row too). Its cost grew with the page, and a full-page render of the A1 sample was about 30% faster; the output is byte-identical | (1) `clip::tests::intersect_walks_only_rows_both_paths_have_strips` counts the rows the walk visits (`cargo test --manifest-path vendor/vello_common/Cargo.toml`) and fails without the patch; the render crate's pixel tests run through the patched crate | diff --git a/vendor/hayro-syntax/src/filter/lzw_flate.rs b/vendor/hayro-syntax/src/filter/lzw_flate.rs index f1ab1aae73..5a0027597b 100644 --- a/vendor/hayro-syntax/src/filter/lzw_flate.rs +++ b/vendor/hayro-syntax/src/filter/lzw_flate.rs @@ -1008,15 +1008,16 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { return None; } - let (bit_size, chunk_len) = if is_png_predictor { - ( - 8, - // PdfCraft patch: in usize (the u8 product overflowed). - (usize::from(params.colors) * usize::from(params.bits_per_component)).div_ceil(8), - ) - } else { - (params.bits_per_component, params.colors as usize) - }; + if is_png_predictor { + // PdfCraft patch: PNG predictors work on bytes whatever the bit depth, so they are + // undone a row at a time on byte slices instead of through the bit-level readers. + // In usize (the u8 product overflowed). + let bpp = (usize::from(params.colors) * usize::from(params.bits_per_component)) + .div_ceil(8); + return Some(unfilter_png(&data, row_len, bpp)); + } + + let (bit_size, chunk_len) = (params.bits_per_component, params.colors as usize); let zero_row = vec![0; row_len]; let mut prev_row = BitChunks::new(&zero_row, bit_size, chunk_len)?; let zero_col = BitChunk::new(0, chunk_len); @@ -1024,58 +1025,7 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { let mut writer = BitWriter::new(&mut out, bit_size)?; for in_row in data.chunks_exact(total_row_len) { - if is_png_predictor { - let predictor = in_row[0]; - let in_data = &in_row[1..]; - let in_data_chunks = BitChunks::new(in_data, bit_size, chunk_len)?; - - match predictor { - 1 => apply::( - prev_row, - zero_col.clone(), - zero_col.clone(), - in_data_chunks, - &mut writer, - chunk_len, - bit_size, - )?, - 2 => apply::( - prev_row, - zero_col.clone(), - zero_col.clone(), - in_data_chunks, - &mut writer, - chunk_len, - bit_size, - )?, - 3 => apply::( - prev_row, - zero_col.clone(), - zero_col.clone(), - in_data_chunks, - &mut writer, - chunk_len, - bit_size, - )?, - 4 => apply::( - prev_row, - zero_col.clone(), - zero_col.clone(), - in_data_chunks, - &mut writer, - chunk_len, - bit_size, - )?, - _ => { - // Just copy the data. - let mut reader = BitReader::new(in_data); - - while let Some(data) = reader.read(bit_size) { - writer.write(data); - } - } - } - } else if i == 2 { + if i == 2 { apply::( prev_row, zero_col.clone(), @@ -1104,6 +1054,98 @@ fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { } } +/// PdfCraft patch: undoes PNG row filters (ISO 32000-2 §7.4.4.4, PNG specification §9) in +/// rows of `row_len` bytes, each after its filter-type byte, with `bpp` bytes per complete pixel +/// (at least 1). An unknown filter type copies the row as it is. Bytes after the last complete +/// row are dropped. +fn unfilter_png(data: &[u8], row_len: usize, bpp: usize) -> Vec { + let bpp = bpp.max(1); + let in_row_len = row_len.saturating_add(1); + let num_rows = data.len() / in_row_len; + let mut out = vec![0; num_rows.saturating_mul(row_len)]; + let zero_row = vec![0; row_len]; + let mut prev: &[u8] = &zero_row; + + for (cur, in_row) in out + .chunks_exact_mut(row_len) + .zip(data.chunks_exact(in_row_len)) + { + let Some((&filter, src)) = in_row.split_first() else { + break; + }; + + match filter { + 1 => { + let mut left: &[u8] = &zero_row; + for (px, src) in cur.chunks_mut(bpp).zip(src.chunks(bpp)) { + for ((c, s), l) in px.iter_mut().zip(src).zip(left) { + *c = s.wrapping_add(*l); + } + left = px; + } + } + 2 => { + for ((c, s), u) in cur.iter_mut().zip(src).zip(prev) { + *c = s.wrapping_add(*u); + } + } + 3 => { + let mut left: &[u8] = &zero_row; + for ((px, src), up) in cur + .chunks_mut(bpp) + .zip(src.chunks(bpp)) + .zip(prev.chunks(bpp)) + { + for (((c, s), l), u) in px.iter_mut().zip(src).zip(left).zip(up) { + // The sum is taken before halving, without wrapping. + let avg = (u16::from(*l) + u16::from(*u)) / 2; + *c = s.wrapping_add(avg as u8); + } + left = px; + } + } + 4 => { + let mut left: &[u8] = &zero_row; + let mut up_left: &[u8] = &zero_row; + for ((px, src), up) in cur + .chunks_mut(bpp) + .zip(src.chunks(bpp)) + .zip(prev.chunks(bpp)) + { + for ((((c, s), l), u), ul) in + px.iter_mut().zip(src).zip(left).zip(up).zip(up_left) + { + *c = s.wrapping_add(paeth_byte(*l, *u, *ul)); + } + left = px; + up_left = up; + } + } + _ => { + for (c, s) in cur.iter_mut().zip(src) { + *c = *s; + } + } + } + + prev = cur; + } + + out +} + +/// The Paeth predictor on bytes: the neighbour nearest `left + up - up_left`, preferring left, +/// then up. +fn paeth_byte(left: u8, up: u8, up_left: u8) -> u8 { + let (a, b, c) = (i16::from(left), i16::from(up), i16::from(up_left)); + let pa = (b - c).abs(); + let pb = (a - c).abs(); + let pc = (a + b - c - c).abs(); + + let not_left = if pb <= pc { up } else { up_left }; + if pa <= pb && pa <= pc { left } else { not_left } +} + fn apply<'a, T: Predictor>( prev_row: BitChunks<'a>, mut prev_col: BitChunk, @@ -1153,46 +1195,6 @@ impl Predictor for Sub { } } -struct Up; -impl Predictor for Up { - fn predict(cur_row: u16, prev_row: u16, _: u16, _: u16) -> u16 { - cur_row.wrapping_add(prev_row) - } -} - -struct Avg; -impl Predictor for Avg { - fn predict(cur_row: u16, prev_row: u16, prev_col: u16, _: u16) -> u16 { - cur_row.wrapping_add(((prev_col as u32 + prev_row as u32) / 2) as u16) - } -} - -struct Paeth; -impl Predictor for Paeth { - fn predict(cur_row: u16, prev_row: u16, prev_col: u16, top_left: u16) -> u16 { - fn paeth(a: u16, b: u16, c: u16) -> u16 { - let a = a as i32; - let b = b as i32; - let c = c as i32; - - let p = a + b - c; - let pa = (p - a).abs(); - let pb = (p - b).abs(); - let pc = (p - c).abs(); - - if pa <= pb && pa <= pc { - a as u16 - } else if pb <= pc { - b as u16 - } else { - c as u16 - } - } - - cur_row.wrapping_add(paeth(prev_col, prev_row, top_left)) - } -} - #[cfg(test)] #[rustfmt::skip] mod tests { @@ -1357,3 +1359,135 @@ mod tests { ); } } + +/// PdfCraft patch tests: PNG predictors are undone byte by byte (`unfilter_png`). +#[cfg(test)] +mod pdfcraft_png_predictor_tests { + use crate::filter::lzw_flate::{PredictorParams, apply_predictor}; + + /// PNG unfiltering written out from the PNG specification (§9.2–9.4), one byte at a time. + fn reference(data: &[u8], row_len: usize, bpp: usize) -> Vec { + let mut out: Vec = Vec::new(); + for (r, row) in data.chunks_exact(row_len + 1).enumerate() { + let start = r * row_len; + for x in 0..row_len { + let left = if x >= bpp { out[start + x - bpp] } else { 0 }; + let up = if r > 0 { out[start + x - row_len] } else { 0 }; + let up_left = if r > 0 && x >= bpp { + out[start + x - row_len - bpp] + } else { + 0 + }; + let predicted = match row[0] { + 1 => left, + 2 => up, + 3 => ((u16::from(left) + u16::from(up)) / 2) as u8, + 4 => { + let p = i16::from(left) + i16::from(up) - i16::from(up_left); + let (pa, pb, pc) = ( + (p - i16::from(left)).abs(), + (p - i16::from(up)).abs(), + (p - i16::from(up_left)).abs(), + ); + if pa <= pb && pa <= pc { + left + } else if pb <= pc { + up + } else { + up_left + } + } + _ => 0, + }; + out.push(row[1 + x].wrapping_add(predicted)); + } + } + out + } + + #[test] + fn png_predictors_match_the_specification_at_every_depth_and_pixel_size() { + let mut seed: u32 = 0x2545_f491; + let mut next = move || { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + seed + }; + for case in 0..400 { + let colors = 1 + (next() % 4) as u8; + let bits_per_component = [1u8, 2, 4, 8, 16][(next() % 5) as usize]; + let columns = 1 + (next() % 40) as usize; + let bpp = (usize::from(colors) * usize::from(bits_per_component)).div_ceil(8); + let row_len = + (columns * usize::from(colors) * usize::from(bits_per_component)).div_ceil(8); + let rows = 1 + (next() % 6) as usize; + let mut data = Vec::new(); + for _ in 0..rows { + // Filter types 0–4, and 5–7, which are not PNG filters and copy the row. + data.push((next() % 8) as u8); + data.extend((0..row_len).map(|_| next() as u8)); + } + // A partial row at the end is not decoded. + let tail = (next() % 3) as usize; + data.extend((0..tail.min(row_len)).map(|_| next() as u8)); + + let params = PredictorParams { + predictor: 10 + (next() % 6) as u8, + colors, + bits_per_component, + columns, + early_change: false, + }; + let got = apply_predictor(data.clone(), ¶ms).unwrap(); + assert_eq!( + got, + reference(&data, row_len, bpp), + "case {case}: {colors} colours at {bits_per_component} bits, {columns} columns" + ); + } + } + + #[test] + fn a_row_ending_part_way_through_a_pixel_keeps_its_last_byte() { + // Three 4-bit samples are a byte and a half, so a three-column row is five bytes and ends + // half-way through a two-byte pixel. Each row is filtered Up. + let data = [ + 2, 10, 11, 12, 13, 14, // + 2, 20, 21, 22, 23, 24, // + 2, 30, 31, 32, 33, 34, + ]; + let params = PredictorParams { + predictor: 15, + colors: 3, + bits_per_component: 4, + columns: 3, + early_change: false, + }; + assert_eq!( + apply_predictor(data.to_vec(), ¶ms).unwrap(), + [10, 11, 12, 13, 14, 30, 32, 34, 36, 38, 60, 63, 66, 69, 72] + ); + } + + #[test] + fn png_average_and_paeth_carry_the_left_pixel_across_a_wide_row() { + // Three 16-bit samples (6 bytes) per pixel: the left neighbour is six bytes back. + let row_len = 6 * 3; + let mut data = vec![3u8]; + data.extend((0..row_len).map(|i| (i * 37 + 200) as u8)); + data.push(4); + data.extend((0..row_len).map(|i| (255 - i * 11) as u8)); + let params = PredictorParams { + predictor: 15, + colors: 3, + bits_per_component: 16, + columns: 3, + early_change: false, + }; + assert_eq!( + apply_predictor(data.clone(), ¶ms).unwrap(), + reference(&data, row_len, 6) + ); + } +} From 83b230ae730b9d8fe8e8d94972ea75d593a682ea Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 10:17:23 -0400 Subject: [PATCH 02/25] M12: PRC reader crate: container, bit primitives, schema (#1297) New standalone L0 crate pdfcraft-prc: the PRC container walk (file header, file-structure descriptions, per-section zlib inflation under a 512 MiB ceiling), the bit-level primitives of the compressed sections, and the schema interpreter a newer producer uses to describe fields it added (step and nesting ceilings). The Double prefix-code table is not in this commit: Double reads return UnknownDoubleCode, and the tests that need the table are ignored with that reason. Adapted from pdfcer (MIT, same author), crates/pdfcer-3d at 0c9742b5936232af89cd707138ef149dc1f4a660. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- Cargo.lock | 9 + crates/prc/Cargo.toml | 18 ++ crates/prc/README.md | 23 ++ crates/prc/src/acof.rs | 98 +++++++ crates/prc/src/bits.rs | 421 +++++++++++++++++++++++++++ crates/prc/src/container.rs | 325 +++++++++++++++++++++ crates/prc/src/error.rs | 38 +++ crates/prc/src/lib.rs | 26 ++ crates/prc/src/schema.rs | 525 ++++++++++++++++++++++++++++++++++ crates/prc/src/testw.rs | 101 +++++++ crates/prc/tests/container.rs | 114 ++++++++ crates/prc/tests/proptests.rs | 41 +++ xtask/src/layers.rs | 1 + 13 files changed, 1740 insertions(+) create mode 100644 crates/prc/Cargo.toml create mode 100644 crates/prc/README.md create mode 100644 crates/prc/src/acof.rs create mode 100644 crates/prc/src/bits.rs create mode 100644 crates/prc/src/container.rs create mode 100644 crates/prc/src/error.rs create mode 100644 crates/prc/src/lib.rs create mode 100644 crates/prc/src/schema.rs create mode 100644 crates/prc/src/testw.rs create mode 100644 crates/prc/tests/container.rs create mode 100644 crates/prc/tests/proptests.rs diff --git a/Cargo.lock b/Cargo.lock index d2f4dadad0..e5abfe499e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4551,6 +4551,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "pdfcraft-prc" +version = "0.15.0" +dependencies = [ + "flate2", + "proptest", + "thiserror 2.0.21", +] + [[package]] name = "pdfcraft-preflight" version = "0.17.0" 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..0f8a01872f --- /dev/null +++ b/crates/prc/README.md @@ -0,0 +1,23 @@ +# 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: every read is bounded and 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 +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. +- Schema programs (how a newer producer describes fields it added) run under a step ceiling (2^22) and a nesting ceiling (64). +- `Unsupported` names constructs the reader cannot step over (schema pointer fields, `Value_CurveIs3D`). +- Not yet in the crate: the `Double` code table, so `BitReader::double` returns `UnknownDoubleCode`; the product tree and tessellation readers. diff --git a/crates/prc/src/acof.rs b/crates/prc/src/acof.rs new file mode 100644 index 0000000000..dff5d4a6f2 --- /dev/null +++ b/crates/prc/src/acof.rs @@ -0,0 +1,98 @@ +//! 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 are not in this crate yet: until they are, [`lookup`] finds no code, so every +//! `Double` read fails with `UnknownDoubleCode` and nothing else changes. + +use std::collections::HashMap; +use std::sync::OnceLock; + +/// 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)] +#[cfg_attr(not(test), expect(dead_code, reason = "built only by the table's rows, which are not in the crate yet"))] +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); + +/// The table's rows. +const TABLE: &[Row] = &[]; + +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)).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)| **v == Entry::Exponent(e << 20)).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] + #[ignore = "the Double table is not in the crate yet"] + 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/bits.rs b/crates/prc/src/bits.rs new file mode 100644 index 0000000000..a94a16dc9a --- /dev/null +++ b/crates/prc/src/bits.rs @@ -0,0 +1,421 @@ +//! 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}; + +/// A cursor over one inflated section. +#[derive(Debug, Clone)] +pub struct BitReader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> BitReader<'a> { + /// A reader positioned at the first bit of `data`. + pub fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + /// 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 { + 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] + #[ignore = "the Double table is not in the crate yet"] + 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] + #[ignore = "the Double table is not in the crate yet"] + 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] + #[ignore = "the Double table is not in the crate yet"] + 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] + #[ignore = "the Double table is not in the crate yet"] + 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] + #[ignore = "the Double table is not in the crate yet"] + 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 without_the_table_a_double_is_an_error_not_a_panic() { + let mut r = BitReader::new(&[0x40, 0, 0]); + assert_eq!(r.double(), Err(PrcError::UnknownDoubleCode)); + assert!(matches!(BitReader::new(&[0x40]).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..8a44921334 --- /dev/null +++ b/crates/prc/src/container.rs @@ -0,0 +1,325 @@ +//! 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. +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]. + fn blocks(&mut self, what: &'static str) -> 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)?); + Ok(self.take(len, what)?.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). +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")?; + let pictures = h.blocks("file structure pictures")?; + 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.left = self.left.checked_sub(out.len()).ok_or(PrcError::TooLarge { limit: self.limit })?; + Ok(out) + } +} + +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. + /// + /// # 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 uncompressed_files = r.blocks("uncompressed files")?; + if mf_end < mf_start { + return Err(PrcError::Malformed("model file ends before it starts".into())); + } + + let mut budget = Budget { left: limit, limit }; + 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.min(data.len())), "model file")?; + Ok(PrcFile { header, file_structures, model_file, uncompressed_files }) + } +} diff --git a/crates/prc/src/error.rs b/crates/prc/src/error.rs new file mode 100644 index 0000000000..eaad2bb6e1 --- /dev/null +++ b/crates/prc/src/error.rs @@ -0,0 +1,38 @@ +//! 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 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, + }, + /// 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/lib.rs b/crates/prc/src/lib.rs new file mode 100644 index 0000000000..1a62f9f272 --- /dev/null +++ b/crates/prc/src/lib.rs @@ -0,0 +1,26 @@ +//! 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. +//! +//! 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), schema +//! programs by a step and 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 bits; +mod container; +mod error; +mod schema; +#[cfg(test)] +mod testw; + +pub use bits::BitReader; +pub use container::{FileStructure, MAX_FILE_STRUCTURES, MAX_INFLATED_BYTES, PRC_READER_VERSION, PrcFile, PrcHeader, SectionKind, UniqueId}; +pub use error::PrcError; +pub use schema::Schema; diff --git a/crates/prc/src/schema.rs b/crates/prc/src/schema.rs new file mode 100644 index 0000000000..a93554334b --- /dev/null +++ b/crates/prc/src/schema.rs @@ -0,0 +1,525 @@ +//! `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; + +/// Steps (tokens executed, loop iterations included) one +/// [`Schema::skip_added_fields`] call may take. +const MAX_STEPS: u64 = 1 << 22; + +/// 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. + 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 { + let entity_type = r.unsigned_integer()?; + let len = r.unsigned_integer()? as usize; + if len > r.remaining() { + return Err(PrcError::Truncated("a schema program")); + } + 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. + /// + /// # Errors + /// [`PrcError::Unsupported`] for a token that needs exact geometry; + /// [`PrcError::Malformed`] for an ill-formed program or one past the step + /// or depth ceiling; 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, steps: 0 }; + exec.program(program, 0) + } +} + +struct Exec<'s, 'r, 'd> { + schema: &'s Schema, + r: &'r mut BitReader<'d>, + vars: HashMap, + loop_counter: i64, + steps: u64, +} + +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> { + self.steps += 1; + if self.steps > MAX_STEPS { + 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 => { + 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); + } + Ok(match t { + 29 => a.wrapping_mul(b), + 30 => a.checked_div(b).ok_or_else(|| malformed("division by zero"))?, + 31 => a.wrapping_add(b), + 32 => a.wrapping_sub(b), + 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 + } + + 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] + #[ignore = "the Double table is not in the crate yet"] + 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] + #[ignore = "the Double table is not in the crate yet"] + 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 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(_)))); + } + + #[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(_)))); + } +} diff --git a/crates/prc/src/testw.rs b/crates/prc/src/testw.rs new file mode 100644 index 0000000000..4706a20ef8 --- /dev/null +++ b/crates/prc/src/testw.rs @@ -0,0 +1,101 @@ +//! 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 + } + + /// 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() + } +} diff --git a/crates/prc/tests/container.rs b/crates/prc/tests/container.rs new file mode 100644 index 0000000000..dc9970704b --- /dev/null +++ b/crates/prc/tests/container.rs @@ -0,0 +1,114 @@ +//! The PRC container walk [WD 6.1-6.2] against synthetic streams. + +#![allow(clippy::unwrap_used)] + +use std::io::Write; + +use pdfcraft_prc::{PrcError, PrcFile, SectionKind, UniqueId}; + +fn zlib(data: &[u8]) -> Vec { + let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + e.write_all(data).unwrap(); + e.finish().unwrap() +} + +fn u32s(out: &mut Vec, vs: &[u32]) { + for v in vs { + out.extend_from_slice(&v.to_le_bytes()); + } +} + +/// One file structure with the given section payloads and model file. +fn build(sections: [&[u8]; 5], model: &[u8], padding: usize) -> Vec { + const HEADER_LEN: usize = 107; + let mut fs_header = b"PRC".to_vec(); + u32s(&mut fs_header, &[8137, 8137, 5, 6, 7, 8, 0, 0, 0, 0, 1, 3]); + fs_header.extend_from_slice(b"\xff\xd8\xff"); + + let mut body = fs_header.clone(); + let mut offs = vec![HEADER_LEN]; + for s in sections { + offs.push(HEADER_LEN + body.len()); + body.extend(zlib(s)); + body.extend(std::iter::repeat_n(0u8, padding)); + } + let mf_start = HEADER_LEN + body.len(); + body.extend(zlib(model)); + 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| o as u32).collect::>()); + u32s(&mut out, &[mf_start as u32, mf_end as u32, 0]); + assert_eq!(out.len(), HEADER_LEN); + out.extend(body); + out +} + +const SECTIONS: [&[u8]; 5] = [b"globals", b"tree", b"tess", b"", b"extra"]; + +#[test] +fn the_container_walk_inflates_every_section() { + for padding in [0, 3] { + let f = PrcFile::parse(&build(SECTIONS, b"model file", padding)).unwrap(); + assert_eq!(f.header.min_version_for_read, 8137); + assert_eq!(f.header.file_id, UniqueId([1, 2, 3, 4])); + assert_eq!(f.file_structures.len(), 1); + let fs = &f.file_structures[0]; + assert_eq!(fs.id, UniqueId([5, 6, 7, 8])); + assert_eq!(fs.pictures, vec![b"\xff\xd8\xff".to_vec()]); + for (kind, want) in SectionKind::ALL.into_iter().zip(SECTIONS) { + assert_eq!(fs.section(kind), want, "{}", kind.name()); + } + assert_eq!(f.model_file, b"model file"); + } +} + +#[test] +fn damaged_containers_are_refused_by_name() { + let good = build(SECTIONS, b"m", 0); + assert_eq!(PrcFile::parse(b"U3D\0").unwrap_err(), PrcError::NotPrc); + assert!(matches!(PrcFile::parse(&good[..40]), Err(PrcError::Truncated(_)))); + + // Reserved field (offset 63) not zero. + let mut bad = good.clone(); + bad[63] = 1; + assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_)))); + + // File-structure id disagrees with its description (FS header id at 107+11). + let mut bad = good.clone(); + bad[118] ^= 0xff; + assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_)))); + + // A section that is not zlib: corrupt the globals stream's header byte. + let globals = u32::from_le_bytes(good[75..79].try_into().unwrap()) as usize; + let mut bad = good.clone(); + bad[globals] = 0; + assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Inflate { section: "globals", .. }))); + + // Offsets out of order. + let mut bad = good; + bad[75..79].copy_from_slice(&1u32.to_le_bytes()); + assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_)))); +} + +#[test] +fn the_inflation_ceiling_is_reported_not_exceeded() { + let f = build([&[0u8; 600], &[0u8; 600], b"", b"", b""], b"", 0); + assert!(PrcFile::parse_with_limit(&f, 1200).is_ok()); + assert_eq!(PrcFile::parse_with_limit(&f, 1199).unwrap_err(), PrcError::TooLarge { limit: 1199 }); +} + +#[test] +fn an_offset_or_count_past_the_data_is_an_error_not_a_panic() { + let mut bad = build(SECTIONS, b"m", 0); + // The model file's start (header offset 95) far past the end, its end further still. + bad[95..99].copy_from_slice(&u32::MAX.to_le_bytes()); + bad[99..103].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!(PrcFile::parse(&bad).is_err()); + // A section count that claims more offsets than the data holds. + let mut bad = build(SECTIONS, b"m", 0); + bad[67..71].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Truncated(_)))); +} diff --git a/crates/prc/tests/proptests.rs b/crates/prc/tests/proptests.rs new file mode 100644 index 0000000000..e33e095bb2 --- /dev/null +++ b/crates/prc/tests/proptests.rs @@ -0,0 +1,41 @@ +//! Arbitrary bytes through every reader: an error or a value, never a panic. + +use pdfcraft_prc::{BitReader, PrcFile, Schema}; +use proptest::prelude::*; + +proptest! { + #[test] + fn any_bytes_parse_or_fail_cleanly(data in proptest::collection::vec(any::(), 0..512), magic in any::()) { + let mut data = data; + if magic && data.len() >= 3 { + data[..3].copy_from_slice(b"PRC"); + } + let _ = PrcFile::parse_with_limit(&data, 1 << 20); + } + + #[test] + fn any_bits_read_or_fail_cleanly(data in proptest::collection::vec(any::(), 0..64), n in 0u32..40) { + let mut r = BitReader::new(&data); + let _ = r.unsigned_integer(); + let _ = r.integer(); + let _ = r.integer_vbn(n); + let _ = r.double_vbn(0.5, n); + let _ = r.nbits_then_unsigned(); + let _ = r.string(); + let _ = r.float_as_bytes(); + let _ = r.double(); + let _ = r.skip_bits(usize::try_from(n).unwrap_or(0)); + prop_assert!(r.position() <= data.len() * 8); + } + + #[test] + fn any_schema_runs_or_fails_cleanly(data in proptest::collection::vec(any::(), 0..256), entity in 0u32..4) { + let mut r = BitReader::new(&data); + if let Ok(schema) = Schema::read(&mut r) { + let types: Vec = schema.entity_types().chain([entity]).collect(); + for t in types { + let _ = schema.skip_added_fields(t, &mut r.clone()); + } + } + } +} diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index b961ebef66..4f59c1dc94 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -34,6 +34,7 @@ pub const TABLE: &[(&str, Class)] = &[ ("filters", Class::Standalone(0)), ("crypt", Class::Standalone(0)), ("arlington", Class::Standalone(0)), + ("prc", Class::Standalone(0)), // L1 object layer ("cos", Class::Standalone(1)), // L2 core model From ab552430a7abce749b1ba6d0a959f5ff907fb2f1 Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 10:22:49 -0400 Subject: [PATCH 03/25] M12: PRC reader: the Double code table from prc-rs (MIT) (#1297) Every PRC Double decodes through a fixed 2077-row prefix-code table (working draft 11.17.2). The rows come from prc-rs src/double.rs at 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d under its MIT licence, whose notice stays in the file header; ATTRIBUTION.toml records it. The table is a complete prefix code, so the empty-table stub test is replaced by a truncation test. The exponent lookup masks the 11 exponent bits, because the last exponent row's upper word is 0x7ff80000. Revert proof: with the table emptied, eight Double tests fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- ATTRIBUTION.md | 3 +- ATTRIBUTION.toml | 11 + crates/prc/README.md | 3 +- crates/prc/src/acof.rs | 15 +- crates/prc/src/acof/table.rs | 2113 ++++++++++++++++++++++++++++++++++ crates/prc/src/bits.rs | 13 +- crates/prc/src/schema.rs | 2 - 7 files changed, 2137 insertions(+), 23 deletions(-) create mode 100644 crates/prc/src/acof/table.rs diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index fc88e8b664..dea7c69d36 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 but the translation catalogs, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (289) +## In this repository (290) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -231,6 +231,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 76f4cf40a8..16c2d266dc 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -2437,6 +2437,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 = "crates/prc/src/acof/table.rs" +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/crates/prc/README.md b/crates/prc/README.md index 0f8a01872f..edb67bdad8 100644 --- a/crates/prc/README.md +++ b/crates/prc/README.md @@ -20,4 +20,5 @@ BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_ - `MAX_INFLATED_BYTES` (512 MiB) bounds every section together; `TooLarge` reports the limit hit. - Schema programs (how a newer producer describes fields it added) run under a step ceiling (2^22) and a nesting ceiling (64). - `Unsupported` names constructs the reader cannot step over (schema pointer fields, `Value_CurveIs3D`). -- Not yet in the crate: the `Double` code table, so `BitReader::double` returns `UnknownDoubleCode`; the product tree and tessellation readers. +- Not yet in the crate: the product tree and tessellation readers. +- 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`. diff --git a/crates/prc/src/acof.rs b/crates/prc/src/acof.rs index dff5d4a6f2..fc11a663a6 100644 --- a/crates/prc/src/acof.rs +++ b/crates/prc/src/acof.rs @@ -2,13 +2,13 @@ //! //! 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 are not in this crate yet: until they are, [`lookup`] finds no code, so every -//! `Double` read fails with `UnknownDoubleCode` and nothing else changes. +//! 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; @@ -17,7 +17,6 @@ pub(crate) const MAX_CODE_BITS: u32 = 22; /// What a code decodes to. #[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(not(test), expect(dead_code, reason = "built only by the table's rows, which are not in the crate yet"))] pub(crate) enum Entry { /// A frequent value, returned with the sign bit applied. Double(f64), @@ -29,9 +28,6 @@ pub(crate) enum Entry { /// One row: code length, code, and what it decodes to. type Row = (u32, u32, Entry); -/// The table's rows. -const TABLE: &[Row] = &[]; - fn key(nbits: u32, code: u32) -> u32 { (nbits << 24) | code } @@ -54,7 +50,7 @@ fn build(rows: &[Row]) -> Option> { fn table() -> Option<&'static HashMap> { static MAP: OnceLock>> = OnceLock::new(); - MAP.get_or_init(|| build(TABLE)).as_ref() + MAP.get_or_init(|| build(table::TABLE)).as_ref() } /// The entry for an `nbits`-long code, if one exists. @@ -65,7 +61,7 @@ pub(crate) fn lookup(nbits: u32, code: u32) -> Option { /// 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)| **v == Entry::Exponent(e << 20)).map(|(&k, _)| (k >> 24, k & 0xff_ffff)) + table()?.iter().find(|(_, v)| matches!(v, Entry::Exponent(hi) if (hi >> 20) & 0x7ff == e)).map(|(&k, _)| (k >> 24, k & 0xff_ffff)) } #[cfg(test)] @@ -87,7 +83,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] fn the_table_is_complete() { let t = table().unwrap(); assert_eq!(t.len(), ROWS); 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/bits.rs b/crates/prc/src/bits.rs index a94a16dc9a..b4a2ff352b 100644 --- a/crates/prc/src/bits.rs +++ b/crates/prc/src/bits.rs @@ -281,7 +281,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] fn zero_is_the_two_bit_code_with_no_sign() { let mut w = W::default(); w.put(0b01, 2).put(0b1111_1111, 8); @@ -292,7 +291,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] fn exponent_with_literal_mantissa() { let v = -1234.5678f64; let le = v.to_bits().to_le_bytes(); @@ -307,7 +305,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] fn exponent_without_mantissa_is_a_power_of_two() { let (n, c) = exponent_code(1023 + 3); let mut w = W::default(); @@ -316,7 +313,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] 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); @@ -336,7 +332,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] fn a_back_reference_past_the_value_is_refused() { let (n, c) = exponent_code(1023); let mut w = W::default(); @@ -346,10 +341,10 @@ mod tests { } #[test] - fn without_the_table_a_double_is_an_error_not_a_panic() { - let mut r = BitReader::new(&[0x40, 0, 0]); - assert_eq!(r.double(), Err(PrcError::UnknownDoubleCode)); - assert!(matches!(BitReader::new(&[0x40]).double(), Err(PrcError::Truncated(_)))); + 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] diff --git a/crates/prc/src/schema.rs b/crates/prc/src/schema.rs index a93554334b..686cc8e056 100644 --- a/crates/prc/src/schema.rs +++ b/crates/prc/src/schema.rs @@ -361,7 +361,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] 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); @@ -373,7 +372,6 @@ mod tests { } #[test] - #[ignore = "the Double table is not in the crate yet"] fn vector_extent_and_box_tokens_read_their_doubles() { // Vec3, Extent1D, Extent2D, BBox: 3 + 2 + 4 + 6 Doubles. let mut d = W::default(); From 7f302667900b2f3a33b672dd491cadbe0e606a2d Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 10:34:43 -0400 Subject: [PATCH 04/25] M12: PRC reader: tessellation and arrays (#1297) Read the tessellation section of a PRC file structure: FileStructure::tessellations() returns each entity as a Tessellation. - TESS_3D becomes a TriangleMesh: positions, triangles from the triangle/fan/strip blocks (one-normal and textured variants included), one triangle range per face, stored normals and texture coordinates with per-corner indices, and per-triangle line-attribute styles. - TESS_3D_Wire becomes polylines (closing and continuous flags); vertex colours are read past, sized by points (closing point included). - TESS_Markup is read past. - TESS_3D_Compressed is read field by field to its end so the entities after it decode; its triangles are not rebuilt (later step). - The compressed Character/Short/Integer/Indice arrays and the Huffman container they use (WD 11.6-11.9, 12.2) live in arrays.rs; the base fields every entity shares (attributes, name, user data) in entity.rs. Never-panic hardening beyond the source this is adapted from: - Huffman symbol width must be 1..=16; the leaf table and element count are bounded by the payload before anything is allocated. - A face may carry at most 8 texture-coordinate sets (an unbounded count allocated per set and padded per triangle). - Every count is checked against the remaining bits; indice sums and deltas use checked arithmetic. A few reading choices (Huffman bit order and code-length width, indice running sums, 64-bit attribute order, three version gates, the is_segment_color bit, the compressed field order) follow prc-rs (MIT); each is marked `// prc-rs:` in the source. Tests are synthetic, written with the crate's test bit writer. Each fix was reverted and its test seen failing (17 reversions). Adapted from pdfcer (MIT, same author), pdfcer-3d at 0c9742b5936232af89cd707138ef149dc1f4a660. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/prc/README.md | 9 +- crates/prc/src/arrays.rs | 336 +++++++++++++++++ crates/prc/src/entity.rs | 121 ++++++ crates/prc/src/lib.rs | 4 + crates/prc/src/tess.rs | 419 +++++++++++++++++++++ crates/prc/src/tess/tests.rs | 581 +++++++++++++++++++++++++++++ crates/prc/src/tess/triangulate.rs | 258 +++++++++++++ crates/prc/src/testw.rs | 65 ++++ 8 files changed, 1792 insertions(+), 1 deletion(-) create mode 100644 crates/prc/src/arrays.rs create mode 100644 crates/prc/src/entity.rs create mode 100644 crates/prc/src/tess.rs create mode 100644 crates/prc/src/tess/tests.rs create mode 100644 crates/prc/src/tess/triangulate.rs diff --git a/crates/prc/README.md b/crates/prc/README.md index edb67bdad8..cb3865d3fd 100644 --- a/crates/prc/README.md +++ b/crates/prc/README.md @@ -13,6 +13,7 @@ PrcFile::parse(data) -> Result // header, fil 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 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 ``` @@ -20,5 +21,11 @@ BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_ - `MAX_INFLATED_BYTES` (512 MiB) bounds every section together; `TooLarge` reports the limit hit. - Schema programs (how a newer producer describes fields it added) run under a step ceiling (2^22) and a nesting ceiling (64). - `Unsupported` names constructs the reader cannot step over (schema pointer fields, `Value_CurveIs3D`). -- Not yet in the crate: the product tree and tessellation readers. +- `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`. +- Not yet in the crate: the product tree and rebuilding compressed meshes. - 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/arrays.rs b/crates/prc/src/arrays.rs new file mode 100644 index 0000000000..da6c52521b --- /dev/null +++ b/crates/prc/src/arrays.rs @@ -0,0 +1,336 @@ +//! 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")); + } + let mut out = Vec::with_capacity(count); + 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")); + } + } + 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")); + } + (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")); + } + (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")); + } + (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 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]); + } +} diff --git a/crates/prc/src/entity.rs b/crates/prc/src/entity.rs new file mode 100644 index 0000000000..e6f488c8f8 --- /dev/null +++ b/crates/prc/src/entity.rs @@ -0,0 +1,121 @@ +//! 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; + +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, +} + +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 } + } + + /// 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)); + } + 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/lib.rs b/crates/prc/src/lib.rs index 1a62f9f272..fd49302234 100644 --- a/crates/prc/src/lib.rs +++ b/crates/prc/src/lib.rs @@ -13,10 +13,13 @@ #![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 schema; +mod tess; #[cfg(test)] mod testw; @@ -24,3 +27,4 @@ pub use bits::BitReader; pub use container::{FileStructure, MAX_FILE_STRUCTURES, MAX_INFLATED_BYTES, PRC_READER_VERSION, PrcFile, PrcHeader, SectionKind, UniqueId}; pub use error::PrcError; pub use schema::Schema; +pub use tess::{Tessellation, TriangleMesh, TriangleStyle}; diff --git a/crates/prc/src/tess.rs b/crates/prc/src/tess.rs new file mode 100644 index 0000000000..42704de652 --- /dev/null +++ b/crates/prc/src/tess.rs @@ -0,0 +1,419 @@ +//! 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; +use crate::entity::{Ctx, malformed}; +use crate::{FileStructure, PrcError, SectionKind}; +use triangulate::{Corners, Face}; + +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> { + let schema = self.schema()?; + let r = BitReader::new(self.section(SectionKind::Tessellation)); + 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 }; + // Every colour but the first is at least its reuse bit. + if count.saturating_mul(1 + 8 * bytes) > self.r.remaining().saturating_add(8 * bytes) { + 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(); + for f in &faces { + 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() { + face.behaviour = u16::try_from(self.r.unsigned_integer()?).unwrap_or(0); + } + 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]>) { + let normals = points(coords); + 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.chunks_exact(2).map(|c| [c.first().copied().unwrap_or(0.0), c.get(1).copied().unwrap_or(0.0)]).collect(); + 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) +} + +fn points(coords: &[f64]) -> Vec<[f64; 3]> { + coords.chunks_exact(3).map(|c| [0, 1, 2].map(|i| c.get(i).copied().unwrap_or(0.0))).collect() +} + +/// 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)] +mod tests; diff --git a/crates/prc/src/tess/tests.rs b/crates/prc/src/tess/tests.rs new file mode 100644 index 0000000000..ee202e6a2a --- /dev/null +++ b/crates/prc/src/tess/tests.rs @@ -0,0 +1,581 @@ +#![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) { + w.uint(entity).bit(false).uint(12); + for c in SQUARE { + w.double(c); + } +} + +#[derive(Default)] +struct Mesh<'a> { + recalc: bool, + indices: &'a [u32], + faces: &'a [(u32, u32, &'a [u32])], + textures: u32, + uvs: &'a [f64], +} + +/// A TESS_3D over [`SQUARE`] at authoring version `v`. +fn tess_3d(w: &mut W, v: u32, m: &Mesh<'_>) { + square(w, 172); + 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(_)))); + 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()); +} + +#[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(_))); +} + +/// 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 { + 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(0x30); + } + b.uint(0); + wire(&mut b, false); + decode(§ion(2, &b), &Schema::default(), 8137).unwrap() +} + +#[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) { + 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(10, 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 { + 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 mut globals = W::default(); + globals.uint(0); + let sections: [&[u8]; 5] = [&globals.bytes(), 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]]); +} diff --git a/crates/prc/src/tess/triangulate.rs b/crates/prc/src/tess/triangulate.rs new file mode 100644 index 0000000000..56917d3dc8 --- /dev/null +++ b/crates/prc/src/tess/triangulate.rs @@ -0,0 +1,258 @@ +//! 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) + } + + /// 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)); + } + } +} + +/// 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.chunks_exact(3).filter_map(|c| match c { + [a, b, c] => Some([*a, *b, *c]), + _ => None, + })), + 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 index 4706a20ef8..37e90606b6 100644 --- a/crates/prc/src/testw.rs +++ b/crates/prc/src/testw.rs @@ -94,8 +94,73 @@ impl W { 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); + } +} From c571b1346936bbe75876818cbc2f7253f92b8377 Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 10:56:03 -0400 Subject: [PATCH 05/25] M12: PRC reader: product tree and styles (#1297) Walk the PRC product tree into placed, coloured tessellations. - PrcFile::placements / placements_with(StyleAlpha, EntityOverrides): every drawable tessellation reached from the model file's roots, placed by its occurrence path's locations and then the representation item's local coordinate systems (a 4x4 Matrix) [WD 7.3.10.1, 7.6.3.2]. - Colours: styles and materials from the file structure globals, the father/son inheritance bits [WD 7.2.4.2], style transparency combined with material alpha by StyleAlpha, per-face styles and topology references via Placement::triangle_colours. A prototype's part is coloured by its own structure's styles. A colour index counts the colour array's doubles (pdf-issues #816). - Occurrences' entity references: colour, hide or relocate the entity they name, in the occurrence's subtree, everywhere, or not at all. - PrcFile::model_tree: the assembly tree as a model-tree panel lists it, with name sources, visibility and each node's placement range. - TriangleMesh::transformed applies a Matrix: positions, cofactor-mapped normals, winding flipped under a mirror. Never panics: occurrence and prototype cycles are Malformed; depth 64, 2^20 visits, MAX_PLACEMENTS and a face-override ceiling bound the walk; counts are checked against the remaining data before allocating. Markups, views, scene lights and clipping planes are Unsupported; textures are read past. Field orders the working draft leaves open follow prc-rs (MIT); each is marked `// prc-rs:`, as are the step-1 container, Double and schema choices that did. Adapted from pdfcer (MIT, same author), crates/pdfcer-3d/src/tree and vec3.rs at 0c9742b5936232af89cd707138ef149dc1f4a660. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/prc/README.md | 13 +- crates/prc/src/bits.rs | 2 + crates/prc/src/container.rs | 3 +- crates/prc/src/entity.rs | 7 +- crates/prc/src/geometry.rs | 179 ++++++++++ crates/prc/src/lib.rs | 8 +- crates/prc/src/schema.rs | 2 + crates/prc/src/tess.rs | 2 +- crates/prc/src/tess/tests.rs | 7 + crates/prc/src/tree/mod.rs | 293 ++++++++++++++++ crates/prc/src/tree/node.rs | 68 ++++ crates/prc/src/tree/overrides.rs | 128 +++++++ crates/prc/src/tree/read.rs | 451 ++++++++++++++++++++++++ crates/prc/src/tree/style.rs | 348 +++++++++++++++++++ crates/prc/src/tree/tests.rs | 576 +++++++++++++++++++++++++++++++ crates/prc/src/tree/walk.rs | 262 ++++++++++++++ 16 files changed, 2344 insertions(+), 5 deletions(-) create mode 100644 crates/prc/src/geometry.rs create mode 100644 crates/prc/src/tree/mod.rs create mode 100644 crates/prc/src/tree/node.rs create mode 100644 crates/prc/src/tree/overrides.rs create mode 100644 crates/prc/src/tree/read.rs create mode 100644 crates/prc/src/tree/style.rs create mode 100644 crates/prc/src/tree/tests.rs create mode 100644 crates/prc/src/tree/walk.rs diff --git a/crates/prc/README.md b/crates/prc/README.md index cb3865d3fd..614231cf64 100644 --- a/crates/prc/README.md +++ b/crates/prc/README.md @@ -14,6 +14,10 @@ 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 ``` @@ -26,6 +30,13 @@ BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_ `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`. -- Not yet in the crate: the product tree and rebuilding compressed meshes. +- 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`. +- Trees carrying markups (PMI), views, scene lights or clipping planes are `Unsupported`; textures are read past. +- 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/bits.rs b/crates/prc/src/bits.rs index b4a2ff352b..a1efe7a64f 100644 --- a/crates/prc/src/bits.rs +++ b/crates/prc/src/bits.rs @@ -201,6 +201,8 @@ impl<'a> BitReader<'a> { /// [`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 { diff --git a/crates/prc/src/container.rs b/crates/prc/src/container.rs index 8a44921334..15c70af8fc 100644 --- a/crates/prc/src/container.rs +++ b/crates/prc/src/container.rs @@ -181,7 +181,7 @@ impl<'a> Bytes<'a> { /// A stored `u32` offset or count as a `usize` (saturating on a 16-bit target, where no data /// that large could exist). -fn offset(v: u32) -> usize { +pub(crate) fn offset(v: u32) -> usize { usize::try_from(v).unwrap_or(usize::MAX) } @@ -316,6 +316,7 @@ impl PrcFile { } let mut budget = Budget { left: limit, limit }; + // 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::, _>>()?; diff --git a/crates/prc/src/entity.rs b/crates/prc/src/entity.rs index e6f488c8f8..1ad53dbd72 100644 --- a/crates/prc/src/entity.rs +++ b/crates/prc/src/entity.rs @@ -4,6 +4,7 @@ use crate::PrcError; use crate::bits::BitReader; use crate::schema::Schema; +use crate::tree::Graphics; const ATTRIBUTE: u32 = 201; @@ -21,11 +22,15 @@ pub(crate) struct Ctx<'a, 's> { /// 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 } + Ctx { r, schema, version, name: None, uid: 0, graphics: Graphics::default() } } /// Reads an entity type code; any value but `want` is malformed. diff --git a/crates/prc/src/geometry.rs b/crates/prc/src/geometry.rs new file mode 100644 index 0000000000..3328bf10f1 --- /dev/null +++ b/crates/prc/src/geometry.rs @@ -0,0 +1,179 @@ +//! 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)) + } + + /// 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 determinant) reverses each + /// triangle so the outside stays counter-clockwise. + /// + /// ``` + /// 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 det = m.det3(); + let sign = if det < 0.0 { -1.0 } else { 1.0 }; + 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 det < 0.0 { + 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]]); + } + + 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 index fd49302234..8e1a749944 100644 --- a/crates/prc/src/lib.rs +++ b/crates/prc/src/lib.rs @@ -3,7 +3,9 @@ //! //! 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. +//! 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. //! @@ -18,13 +20,17 @@ mod bits; mod container; mod entity; mod error; +mod geometry; mod schema; mod tess; #[cfg(test)] mod testw; +mod tree; pub use bits::BitReader; 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 index 686cc8e056..ba58474f3d 100644 --- a/crates/prc/src/schema.rs +++ b/crates/prc/src/schema.rs @@ -213,6 +213,8 @@ impl Exec<'_, '_, '_> { 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)?; } diff --git a/crates/prc/src/tess.rs b/crates/prc/src/tess.rs index 42704de652..f41ea0a260 100644 --- a/crates/prc/src/tess.rs +++ b/crates/prc/src/tess.rs @@ -416,4 +416,4 @@ fn point_at(pts: &[[f64; 3]], idx: u32) -> Result<[f64; 3], PrcError> { } #[cfg(test)] -mod tests; +pub(crate) mod tests; diff --git a/crates/prc/src/tess/tests.rs b/crates/prc/src/tess/tests.rs index ee202e6a2a..ad7f497e82 100644 --- a/crates/prc/src/tess/tests.rs +++ b/crates/prc/src/tess/tests.rs @@ -223,6 +223,13 @@ fn too_many_texture_sets_are_refused_before_anything_is_allocated() { 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); diff --git a/crates/prc/src/tree/mod.rs b/crates/prc/src/tree/mod.rs new file mode 100644 index 0000000000..6967ba40ea --- /dev/null +++ b/crates/prc/src/tree/mod.rs @@ -0,0 +1,293 @@ +//! 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, views, scene lights and clipping planes are not read past; a tree carrying any +//! of them is [`PrcError::Unsupported`]. + +// 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; +use crate::container::offset; +use crate::entity::{Ctx, malformed}; +use crate::geometry::{Matrix, Vec3}; +use crate::{PrcError, PrcFile, Schema, SectionKind, UniqueId}; + +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 CARTESIAN_TRANSFORMATION: u32 = 202; +const ENTITY_REFERENCE: u32 = 203; +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 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 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; +/// `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). + 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`] for a tree carrying markups (PMI), views, scene lights or + /// clipping planes, or, when an item is placed by a reference coordinate system, globals + /// carrying fonts or fill patterns; [`PrcError::Malformed`] for an 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> { + if !self.stores_tree() { + return Ok((Vec::new(), Vec::new())); + } + let mut trees = Vec::with_capacity(self.file_structures.len()); + for fs in &self.file_structures { + let schema = fs.schema()?; + let tree = Ctx::new(BitReader::new(fs.section(SectionKind::Tree)), &schema, fs.authoring_version).file_structure_tree()?; + // The globals carry the coordinate systems and the colours; colours alone are not + // worth refusing the placements over. + let globals = (|| { + let mut r = BitReader::new(fs.section(SectionKind::Globals)); + Schema::read(&mut r)?; + Ctx::new(r, &schema, fs.authoring_version).globals() + })(); + let globals = match globals { + Ok(g) => g, + Err(e) if tree.parts.iter().flatten().any(|i| !i.local.is_empty()) => return Err(e), + Err(_) => Globals::default(), + }; + trees.push((fs.id, tree, globals)); + } + let mut r = BitReader::new(&self.model_file); + 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)) + } +} + +#[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..6c8047b989 --- /dev/null +++ b/crates/prc/src/tree/read.rs @@ -0,0 +1,451 @@ +//! 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. + 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 are + /// [`PrcError::Unsupported`]. + // prc-rs: the scene's field order, and `is_absolute` from version 8137. + fn scene_display_parameters(&mut self) -> Result<(), PrcError> { + self.expect_type(SCENE_DISPLAY_PARAMETERS)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_active + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("scene lights")); + } + if self.r.bit()? { + self.camera()?; + } + if self.r.bit()? { + self.vector3()?; // rotation centre + } + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("scene clipping planes")); + } + 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) + } + + /// `MarkupData` [WD 7.3.10.4]: only the empty form reads past. + fn markup_data(&mut self) -> Result<(), PrcError> { + for _ in 0..4 { + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("tree markups (PMI)")); + } + } + Ok(()) + } + + fn views(&mut self) -> Result<(), PrcError> { + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("tree views")); + } + Ok(()) + } + + /// `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 mut target = None; + if self.r.bit()? { + match self.r.unsigned_integer()? { + REFERENCE_ON_PRC_BASE => { + let kind = self.r.unsigned_integer()?; + let fs = self.file_identifier()?; + let uid = self.r.unsigned_integer()?; + target = Some(Target::Entity { kind, fs, uid }); + } + REFERENCE_ON_TOPOLOGY => target = self.reference_on_topology()?, + t => return Err(malformed(format!("entity type {t} as reference data"))), + } + } + self.schema.skip_added_fields(ENTITY_REFERENCE, &mut self.r)?; + self.user_data()?; + Ok(target.map(|target| Override { graphics, hidden, local, target })) + } + + /// `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. + 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