From a5202e11fca53b6b91483675ff9f74d3e353bde4 Mon Sep 17 00:00:00 2001 From: Streetblock Date: Fri, 9 Oct 2026 20:09:56 +0200 Subject: [PATCH] M2: normalize JPEG2000 samples without overflowing (#575) * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex * ROADMAP: keep the JPX entry with this session's entries --------- Co-authored-by: OpenAI Codex Co-authored-by: Brandon Thomas --- ROADMAP.md | 4 +- crates/render/src/raster.rs | 96 +++++++++++++++++++++++++++ vendor/README.md | 2 +- vendor/hayro-syntax/src/filter/jpx.rs | 77 +++++++++++++++++++-- vendor/hayro-syntax/src/math.rs | 25 ------- 5 files changed, 171 insertions(+), 33 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5020e708c..089ad1987 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -91,7 +91,7 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t | M | Milestone | Est. hours | Done | Remaining (h) | Notes | |---|---|---|---|---|---| | M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 92% | 2–4 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates | -| M2 | Model, render, text | 200–350 | 15% | 170–300 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input. Text extraction reaches word-F1 0.98 against pdftotext. Missing: model crate, fonts, DisplayList, renderer independent of hayro | +| M2 | Model, render, text | 200–350 | 15% | 170–300 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Missing: model crate, fonts, DisplayList, renderer independent of hayro | | M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 78% | 25–45 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs | | M3 | Viewer app (native + web) | 80–150 | 88% | 10–19 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel. | | M4 | Engine, history, save, organize | 100–180 | 95% | 9–17 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize. Missing: Replace pages, page transitions, recovery on the web | @@ -129,6 +129,8 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run - **2026-10-09 (M5, line endings):** Lines, connected lines and callouts draw all ten standard line-ending styles. `comment_add` accepts `endings`, and Comment properties can change them in one undo step. An unknown name is left undrawn, and a non-finite direction is rejected. Cloudy borders remain polygon-only, so `comment.line-endings` stays partial. Overall estimate unchanged (about 30–35%). - **2026-10-09 (M6, field rotation):** Regenerated form appearances honour `/MK /R` (0, 90, 180, 270). Filling or restyling a rotated field keeps the text, check mark and button caption turned, and Field Properties ▸ Position and `form_set_props` `rotation` set it, swapping width and height around the field's center when the axis changes. `form.appearance-regeneration` stays partial (Helvetica metrics are still approximate). Overall estimate unchanged (about 30–35%). +- **2026-10-09 (M2, JPX sample normalization):** JPEG 2000 rescaling uses bounded integer arithmetic for supported 1–32-bit samples, including the 32-bit boundary that previously shifted past the integer width. Zero-sized rows are refused before scaling. Small contributor-original codestreams exercise decoding and rendering; endpoint and standard-depth regressions cover the same helper. Overall estimate unchanged (about 30–35%). + - **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged. - **2026-10-09 (M1, raw-stream decoding limits):** COS checks unfiltered decoded data against the caller and per-stream limits before copying it, including empty filter arrays and streams whose Crypt filter was applied on load. Strict decoding uses the same raw-data guard. Tiny synthetic regressions cover exceeded, exact and zero-byte limits while retaining the filtered decoder behavior. Milestone and overall effort estimates unchanged (about 30–35%). diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 652ae6eac..d6ef00a39 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -1379,6 +1379,102 @@ trailer << /Root 1 0 R >> assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255]); } + /// Contributor-original JPEG 2000: one unsigned 8-bit pixel per component, one tile, + /// no decomposition, reversible wavelet, and one empty packet per component. Empty + /// coefficients decode to the midpoint (128) after the unsigned DC level shift. + fn jpx_midpoint_pdf(bpc: u8, components: u16) -> Vec { + let mut codestream = vec![0xff, 0x4f, 0xff, 0x51]; // SOC, SIZ. + codestream.extend_from_slice(&(38 + 3 * components).to_be_bytes()); + codestream.extend_from_slice(&0_u16.to_be_bytes()); // Rsiz. + for value in [1_u32, 1, 0, 0, 1, 1, 0, 0] { + codestream.extend_from_slice(&value.to_be_bytes()); + } + codestream.extend_from_slice(&components.to_be_bytes()); + for _ in 0..components { + codestream.extend_from_slice(&[7, 1, 1]); // Eight bits, no subsampling. + } + // COD: LRCP, one layer, no transform between components, 64x64 codeblocks, + // zero decomposition levels, reversible wavelet. QCD: no quantization. + codestream.extend_from_slice(&[0xff, 0x52, 0, 12, 0, 0, 0, 1, 0, 0, 4, 4, 0, 1]); + codestream.extend_from_slice(&[0xff, 0x5c, 0, 4, 0x40, 0x40]); + codestream.extend_from_slice(&[0xff, 0x90, 0, 10, 0, 0]); // SOT, tile zero. + codestream.extend_from_slice(&(14 + u32::from(components)).to_be_bytes()); + codestream.extend_from_slice(&[0, 1, 0xff, 0x93]); // One tile part, SOD. + codestream.extend(std::iter::repeat_n(0, usize::from(components))); + codestream.extend_from_slice(&[0xff, 0xd9]); // EOC. + + let content = "q 20 0 0 20 5 5 cm /Im1 Do Q 1 0 0 rg 0 0 4 4 re f"; + let mut pdf = format!( + "%PDF-1.7\n\ +1 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 40 40] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >> endobj\n\ +4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\ +5 0 obj << /Type /XObject /Subtype /Image /Width 1 /Height 1 /BitsPerComponent {bpc} /Filter /JPXDecode /Length {} >> stream\n", + content.len(), + codestream.len() + ) + .into_bytes(); + pdf.extend_from_slice(&codestream); + pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF"); + pdf + } + + #[test] + fn jpx_stream_bit_depth_boundaries_do_not_panic() { + use hayro::hayro_syntax::bit_reader::BitReader; + use hayro::hayro_syntax::object::stream::ImageDecodeParams; + + for (bpc, expected) in [(1, 1), (8, 128), (16, 32_896), (31, 1_077_952_576), (32, 2_155_905_152)] { + let pdf = Pdf::new(Arc::new(jpx_midpoint_pdf(bpc, 1))).unwrap(); + let stream = pdf.objects().into_iter().filter_map(|object| object.into_stream()).find(|stream| !stream.filters().is_empty()).unwrap(); + let decoded = stream.decoded_image(&ImageDecodeParams { bpc: Some(bpc), ..Default::default() }).unwrap(); + let image = decoded.image_data.unwrap(); + assert_eq!((image.width, image.height, image.bits_per_component), (1, 1, bpc)); + assert_eq!(decoded.data.len(), usize::from(bpc).div_ceil(8)); + assert_eq!(BitReader::new(&decoded.data).read(bpc), Some(expected), "{bpc}-bit midpoint"); + } + } + + #[test] + fn jpx_streams_reject_out_of_range_bit_depths() { + use hayro::hayro_syntax::object::stream::ImageDecodeParams; + + for bpc in [0, 33, 255] { + let pdf = Pdf::new(Arc::new(jpx_midpoint_pdf(bpc, 1))).unwrap(); + let stream = pdf.objects().into_iter().filter_map(|object| object.into_stream()).find(|stream| !stream.filters().is_empty()).unwrap(); + assert!(stream.decoded_image(&ImageDecodeParams { bpc: Some(bpc), ..Default::default() }).is_err()); + } + } + + #[test] + fn jpx_gray_rgb_and_alpha_keep_eight_bit_samples() { + use hayro::hayro_syntax::object::stream::ImageDecodeParams; + + for components in [1, 3, 4] { + let pdf = Pdf::new(Arc::new(jpx_midpoint_pdf(8, components))).unwrap(); + let stream = pdf.objects().into_iter().filter_map(|object| object.into_stream()).find(|stream| !stream.filters().is_empty()).unwrap(); + let decoded = stream.decoded_image(&ImageDecodeParams::default()).unwrap(); + let expected_colors = usize::from(components.min(3)); + assert_eq!(decoded.data.as_ref(), vec![128; expected_colors]); + let alpha = decoded.image_data.unwrap().alpha; + assert_eq!(alpha, (components == 4).then(|| vec![128])); + } + } + + #[test] + fn jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering() { + for bpc in [8, 32] { + let mut renderer = PageRenderer::new(Arc::new(jpx_midpoint_pdf(bpc, 1)), RenderConfig::default()); + let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 }); + assert!(page.error.is_none(), "{bpc} bits: {:?}", page.error); + assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "other page content draws"); + if bpc == 8 { + assert_eq!(&page.rgba[((25 * 40 + 15) * 4)..][..4], &[128, 128, 128, 255], "the valid image paints gray"); + } + } + } + /// From the nightly `cargo xtask fuzz` (CI caps each child at 4 GiB): a stencil mask claiming /// /W 4294967295 and a CCITT image claiming /Columns 4294967295 each allocated 4 GiB while /// decoding (locally: 9.6 GB and 4.3 GB). Vendored hayro patches `image_size_ok` and diff --git a/vendor/README.md b/vendor/README.md index 408621f92..64102ddc4 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -12,7 +12,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | 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 | (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` | | 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) | (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` | | 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) | (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` | -| 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); (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 | (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`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream` | +| 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); (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 | (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`; (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` | | 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. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (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 | Temporary: hayro is the bootstrap renderer and lopdf the bootstrap inspector (ADR-0004); M2.6 replaces them with our own devices and `cos`/`model`. diff --git a/vendor/hayro-syntax/src/filter/jpx.rs b/vendor/hayro-syntax/src/filter/jpx.rs index 4acc343b5..6a788591e 100644 --- a/vendor/hayro-syntax/src/filter/jpx.rs +++ b/vendor/hayro-syntax/src/filter/jpx.rs @@ -1,6 +1,5 @@ -use crate::bit_reader::BitWriter; +use crate::bit_reader::{BitWriter, bit_mask}; use crate::filter::FilterResult; -use crate::math::round_f32; use crate::object::stream::{ImageColorSpace, ImageData, ImageDecodeParams}; use alloc::borrow::Cow; use alloc::vec; @@ -95,12 +94,18 @@ fn scale( width: u32, height: u32, ) -> Option> { - if bit_per_component == 0 || bit_per_component > 32 { + if bit_per_component == 0 + || bit_per_component > 32 + || num_components == 0 + || width == 0 + || height == 0 + { return None; } - let div_factor = ((1 << 8) - 1) as f32; - let mul_factor = ((1_u32 << bit_per_component) - 1) as f32; + // PdfCraft patch: normalize all supported bit widths without an overflowing + // shift or floating-point endpoint rounding, including no_std builds. + let max_sample = u64::from(bit_mask(bit_per_component)); let bits_per_row = (width as usize) .checked_mul(num_components as usize)? @@ -112,7 +117,9 @@ fn scale( for bytes in data.chunks_exact(components_per_row) { for byte in bytes { - let scaled = round_f32((*byte as f32 / div_factor) * mul_factor) as u32; + // The numerator is at most 255 * u32::MAX + 127, which fits in + // u64. The rounded quotient is <= max_sample, so the cast is exact. + let scaled = ((u64::from(*byte) * max_sample + 127) / 255) as u32; writer.write(scaled)?; } @@ -124,3 +131,61 @@ fn scale( Some(input) } + +#[cfg(test)] +mod tests { + use super::scale; + use crate::bit_reader::{BitReader, bit_mask}; + use alloc::vec::Vec; + + #[test] + fn jpx_scale_accepts_32_bit_samples() { + assert_eq!( + scale(&[0, 255], 32, 1, 2, 1).unwrap(), + [0, 0, 0, 0, 255, 255, 255, 255] + ); + } + + #[test] + fn jpx_scale_keeps_supported_bit_depths() { + for bpc in 1_u8..=32 { + let bytes = scale(&[0, 255], bpc, 1, 2, 1).unwrap(); + assert_eq!(bytes.len(), (2 * usize::from(bpc)).div_ceil(8)); + let mut reader = BitReader::new(&bytes); + assert_eq!(reader.read(bpc), Some(0)); + assert_eq!(reader.read(bpc), Some(bit_mask(bpc))); + } + } + + #[test] + fn jpx_scale_rejects_out_of_range_bit_depths() { + for bpc in [0, 33, 255] { + assert!(scale(&[255], bpc, 1, 1, 1).is_none()); + } + } + + #[test] + fn jpx_scale_rejects_zero_dimensions_and_components() { + for (components, width, height) in [(0, 1, 1), (1, 0, 1), (1, 1, 0)] { + assert!(scale(&[255], 32, components, width, height).is_none()); + } + } + + #[test] + fn jpx_scale_preserves_one_eight_and_sixteen_bit_samples() { + let samples: Vec = (0..=255).collect(); + for bpc in [1, 8, 16] { + let bytes = scale(&samples, bpc, 1, 256, 1).unwrap(); + let mut reader = BitReader::new(&bytes); + for byte in &samples { + let expected = match bpc { + 1 => u32::from(*byte >= 128), + 8 => u32::from(*byte), + 16 => u32::from(*byte) * 257, + _ => unreachable!(), + }; + assert_eq!(reader.read(bpc), Some(expected), "{bpc}-bit sample {byte}"); + } + } + } +} diff --git a/vendor/hayro-syntax/src/math.rs b/vendor/hayro-syntax/src/math.rs index 60bf95e0e..12d4724f6 100644 --- a/vendor/hayro-syntax/src/math.rs +++ b/vendor/hayro-syntax/src/math.rs @@ -1,31 +1,6 @@ // Note that these polyfills can be very imprecise, but hopefully good enough // for the vast majority of cases. -#[cfg(feature = "images")] -#[inline(always)] -pub(crate) fn round_f32(x: f32) -> f32 { - #[cfg(feature = "std")] - { - x.round() - } - #[cfg(not(feature = "std"))] - { - if x >= 0.0 { - floor_f32(x + 0.5) - } else { - -floor_f32(-x + 0.5) - } - } -} - -#[cfg(all(feature = "images", not(feature = "std")))] -#[inline(always)] -fn floor_f32(x: f32) -> f32 { - let xi = x as i32; - let xf = xi as f32; - if x < xf { xf - 1.0 } else { xf } -} - #[inline(always)] pub(crate) fn trunc_f64(x: f64) -> f64 { #[cfg(feature = "std")]