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 <codex@openai.com>

* ROADMAP: keep the JPX entry with this session's entries

---------

Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Streetblock
2026-10-09 14:09:56 -04:00
committed by GitHub
co-authored by OpenAI Codex Brandon Thomas
parent f1af1e2e79
commit a5202e11fc
5 changed files with 171 additions and 33 deletions
+3 -1
View File
@@ -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:<br>- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;<br>- 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.<br>Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.<br>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:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>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%).
+96
View File
@@ -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<u8> {
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
+1 -1
View File
@@ -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`.
+71 -6
View File
@@ -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<Vec<u8>> {
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<u8> = (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}");
}
}
}
}
-25
View File
@@ -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")]