diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e805109..2545202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,32 +6,77 @@ on: name: CI env: CARGO_TERM_COLOR: always - HOST: x86_64-unknown-linux-gnu RUSTFLAGS: "-D warnings" jobs: tests: runs-on: ubuntu-latest strategy: matrix: - rust: [stable, beta, nightly, 1.62.0] # MSRV=1.62 + rust: [stable, beta, nightly, 1.67.0] steps: - uses: actions/checkout@v2 - with: {submodules: true} + with: { submodules: true } - uses: actions-rs/toolchain@v1 - with: {profile: minimal, toolchain: '${{ matrix.rust }}', override: true} + with: + { profile: minimal, toolchain: "${{ matrix.rust }}", override: true } - run: cargo test + tests-s390x: + runs-on: ubuntu-latest + strategy: + matrix: + features: [default, reference] + steps: + - uses: actions/checkout@v2 + with: { submodules: true } + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + target: s390x-unknown-linux-gnu + components: rust-src + - run: sudo apt-get update && sudo apt-get install -y qemu-user-static gcc-s390x-linux-gnu + - run: | + if [ "${{ matrix.features }}" = "reference" ]; then + echo "FEATURES=alloc,reference" >> $GITHUB_ENV + else + echo "FEATURES=alloc" >> $GITHUB_ENV + fi + - run: | + cargo build \ + --target s390x-unknown-linux-gnu \ + --tests \ + -Zbuild-std \ + --no-default-features \ + --features=${FEATURES} + env: + CARGO_TARGET_S390X_UNKNOWN_LINUX_GNU_LINKER: "s390x-linux-gnu-gcc" + RUSTFLAGS: "-D warnings -C target-feature=+crt-static" + - run: | + failed=0 + for test in ./target/s390x-unknown-linux-gnu/debug/deps/*-[0-9a-f][0-9a-f]*; do + if [ -x "$test" ]; then + echo "Running $test" + qemu-s390x-static "$test" || failed=1 + fi + done + exit $failed reference: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - with: {submodules: true} + with: { submodules: true } - uses: actions-rs/toolchain@v1 - with: {profile: minimal, toolchain: stable, override: true} + with: { profile: minimal, toolchain: stable, override: true } - run: cargo test --features=reference clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 - with: {profile: minimal, toolchain: beta, override: true, components: clippy} + with: + profile: minimal + toolchain: beta + override: true + components: clippy - run: cargo clippy diff --git a/Cargo.toml b/Cargo.toml index a2f6155..9aa7bc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,30 +11,34 @@ homepage = "https://github.com/aldanor/qoi-rust" documentation = "https://docs.rs/qoi" categories = ["multimedia::images", "multimedia::encoding"] keywords = ["qoi", "graphics", "image", "encoding"] -exclude = [ - "assets/*", -] -rust-version = "1.62.0" +exclude = ["assets/*"] +rust-version = "1.67.0" [features] default = ["std"] -alloc = [] # provides access to `Vec` without enabling `std` mode -std = [] # std mode (enabled by default) - provides access to `std::io`, `Error` and `Vec` -reference = [] # follows reference encoder implementation precisely, but may be slightly slower +# provides access to `Vec` without enabling `std` mode +alloc = [] +# std mode (enabled by default) - provides access to `std::io`, `Error` and `Vec` +std = [] +# follows reference encoder implementation precisely, but may be slower +reference = [] [dependencies] -bytemuck = "1.12" +bytemuck = "1.22" [workspace] members = ["libqoi", "bench"] [dev-dependencies] +# external anyhow = "1.0" -png = "0.17" -walkdir = "2.3" cfg-if = "1.0" -rand = "0.8" -libqoi = { path = "libqoi"} +png = "0.17" +rand = "0.9" +rand_distr = "0.5" +walkdir = "2.5" +# internal +libqoi = { path = "libqoi" } [lib] name = "qoi" diff --git a/libqoi/build.rs b/libqoi/build.rs index f69cb6a..71ee19e 100644 --- a/libqoi/build.rs +++ b/libqoi/build.rs @@ -12,7 +12,7 @@ fn main() { .include("../ext/qoi") .define("QOI_NO_STDIO", None) .define("QOI_IMPLEMENTATION", None) - .flag("-Wno-unsequenced") + .flag_if_supported("-Wno-unsequenced") .opt_level(3) .compile("qoi"); } diff --git a/rustfmt.toml b/rustfmt.toml index 0160865..8c52771 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,7 +1,5 @@ use_small_heuristics = "Max" use_field_init_shorthand = true use_try_shorthand = true -empty_item_single_line = true edition = "2018" -unstable_features = true -fn_args_layout = "Compressed" +fn_params_layout = "Compressed" diff --git a/src/decode.rs b/src/decode.rs index 029c590..be97803 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -244,7 +244,7 @@ impl<'a> Bytes<'a> { } } -impl<'a> Reader for Bytes<'a> { +impl Reader for Bytes<'_> { #[inline] fn decode_header(&mut self) -> Result
{ let header = Header::decode(self.0)?; diff --git a/src/header.rs b/src/header.rs index ccdb1cc..c1c44b6 100644 --- a/src/header.rs +++ b/src/header.rs @@ -67,7 +67,7 @@ impl Header { /// Serializes the header into a bytes array. #[inline] - pub(crate) fn encode(&self) -> [u8; QOI_HEADER_SIZE] { + pub fn encode(&self) -> [u8; QOI_HEADER_SIZE] { let mut out = [0; QOI_HEADER_SIZE]; out[..4].copy_from_slice(&QOI_MAGIC.to_be_bytes()); out[4..8].copy_from_slice(&self.width.to_be_bytes()); @@ -79,7 +79,7 @@ impl Header { /// Deserializes the header from a byte array. #[inline] - pub(crate) fn decode(data: impl AsRef<[u8]>) -> Result { + pub fn decode(data: impl AsRef<[u8]>) -> Result { let data = data.as_ref(); if unlikely(data.len() < QOI_HEADER_SIZE) { return Err(Error::UnexpectedBufferEnd); diff --git a/src/lib.rs b/src/lib.rs index f77506b..3fb7a35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ clippy::module_name_repetitions, clippy::cargo_common_metadata, clippy::doc_markdown, - clippy::return_self_not_must_use, + clippy::return_self_not_must_use )] #![cfg_attr(not(any(feature = "std", test)), no_std)] #[cfg(all(feature = "alloc", not(any(feature = "std", test))))] diff --git a/src/pixel.rs b/src/pixel.rs index d103494..fb87d5a 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -124,12 +124,12 @@ impl Pixel { { // credits for the initial idea: @zakarumych let v = if N == 4 { - u32::from_ne_bytes(cast(self.0)) + u32::from_le_bytes(cast(self.0)) } else { - u32::from_ne_bytes([self.0[0], self.0[1], self.0[2], 0xff]) + u32::from_le_bytes([self.0[0], self.0[1], self.0[2], 0xff]) } as u64; let s = ((v & 0xff00_ff00) << 32) | (v & 0x00ff_00ff); - s.wrapping_mul(0x0300_0700_0005_000b_u64).to_le().swap_bytes() as u8 & 63 + (s.wrapping_mul(0x0300_0700_0005_000b_u64) >> 56) as u8 & 63 } #[inline] @@ -152,11 +152,11 @@ impl Pixel { let (vr_2, vg_2, vb_2) = (vr.wrapping_add(2), vg.wrapping_add(2), vb.wrapping_add(2)); if vr_2 | vg_2 | vb_2 | 3 == 3 { - buf.write_one(QOI_OP_DIFF | vr_2 << 4 | vg_2 << 2 | vb_2) + buf.write_one(QOI_OP_DIFF | (vr_2 << 4) | (vg_2 << 2) | vb_2) } else { let (vg_r_8, vg_b_8) = (vg_r.wrapping_add(8), vg_b.wrapping_add(8)); if vg_r_8 | vg_b_8 | 15 == 15 { - buf.write_many(&[QOI_OP_LUMA | vg_32, vg_r_8 << 4 | vg_b_8]) + buf.write_many(&[QOI_OP_LUMA | vg_32, (vg_r_8 << 4) | vg_b_8]) } else { buf.write_many(&[QOI_OP_RGB, self.r(), self.g(), self.b()]) } @@ -170,6 +170,12 @@ impl Pixel { } } +impl Default for Pixel { + fn default() -> Self { + Self::new() + } +} + impl From> for [u8; N] { #[inline(always)] fn from(px: Pixel) -> Self { diff --git a/src/utils.rs b/src/utils.rs index 60fa24e..787ae96 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -59,7 +59,7 @@ impl<'a> BytesMut<'a> { } } -impl<'a> Writer for BytesMut<'a> { +impl Writer for BytesMut<'_> { #[inline] fn write_one(self, v: u8) -> Result { Ok(BytesMut::write_one(self, v)) diff --git a/tests/test_gen.rs b/tests/test_gen.rs index 08767d4..fd015c5 100644 --- a/tests/test_gen.rs +++ b/tests/test_gen.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_endian = "big", allow(unused_imports, dead_code))] + mod common; use bytemuck::cast_slice; @@ -5,11 +7,7 @@ use std::borrow::Cow; use std::fmt::Debug; use cfg_if::cfg_if; -use rand::{ - distributions::{Distribution, Standard}, - rngs::StdRng, - Rng, SeedableRng, -}; +use rand::{rngs::StdRng, Rng, SeedableRng}; use libqoi::{qoi_decode, qoi_encode}; use qoi::consts::{ @@ -46,7 +44,7 @@ impl GenState { } pub fn pick_from_index(&self, rng: &mut impl Rng) -> [u8; N] { - self.index[rng.gen_range(0_usize..64)] + self.index[rng.random_range(0_usize..64)] } pub fn zero() -> [u8; N] { @@ -68,7 +66,7 @@ struct ImageGen { impl ImageGen { pub fn new_random(rng: &mut impl Rng) -> Self { - let p: [f64; 6] = rng.gen(); + let p: [f64; 6] = rng.random(); let t = p.iter().sum::(); Self { p_new: p[0] / t, @@ -87,18 +85,15 @@ impl ImageGen { } } - fn generate_const(&self, rng: &mut R, min_len: usize) -> Vec - where - Standard: Distribution<[u8; N]>, - { + fn generate_const(&self, rng: &mut R, min_len: usize) -> Vec { let mut s = GenState::::with_capacity(min_len); let zero = GenState::::zero(); while s.len < min_len { - let mut p = rng.gen_range(0.0..1.0); + let mut p = rng.random_range(0.0..1.0); if p < self.p_new { - s.write(rng.gen()); + s.write(rng.random()); continue; } p -= self.p_new; @@ -112,7 +107,7 @@ impl ImageGen { if p < self.p_repeat { let px = s.prev; - let n_repeat = rng.gen_range(1_usize..=70); + let n_repeat = rng.random_range(1_usize..=70); for _ in 0..n_repeat { s.write(px); } @@ -122,9 +117,9 @@ impl ImageGen { if p < self.p_diff { let mut px = s.prev; - px[0] = px[0].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); - px[1] = px[1].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); - px[2] = px[2].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); + px[0] = px[0].wrapping_add(rng.random_range(0_u8..4).wrapping_sub(2)); + px[1] = px[1].wrapping_add(rng.random_range(0_u8..4).wrapping_sub(2)); + px[2] = px[2].wrapping_add(rng.random_range(0_u8..4).wrapping_sub(2)); s.write(px); continue; } @@ -132,9 +127,9 @@ impl ImageGen { if p < self.p_luma { let mut px = s.prev; - let vg = rng.gen_range(0_u8..64).wrapping_sub(32); - let vr = rng.gen_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); - let vb = rng.gen_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); + let vg = rng.random_range(0_u8..64).wrapping_sub(32); + let vr = rng.random_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); + let vb = rng.random_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); px[0] = px[0].wrapping_add(vr); px[1] = px[1].wrapping_add(vg); px[2] = px[2].wrapping_add(vb); @@ -276,13 +271,14 @@ fn check_roundtrip( } #[test] +#[cfg(target_endian = "little")] // takes too long on big-endian fn test_generated() { let mut rng = StdRng::seed_from_u64(0); let mut n_pixels = 0; while n_pixels < 20_000_000 { - let min_len = rng.gen_range(1..=5000); - let channels = rng.gen_range(3..=4); + let min_len = rng.random_range(1..=5000); + let channels = rng.random_range(3..=4); let gen = ImageGen::new_random(&mut rng); let img = gen.generate(&mut rng, channels, min_len); diff --git a/tests/test_misc.rs b/tests/test_misc.rs index 720adf4..0a93f61 100644 --- a/tests/test_misc.rs +++ b/tests/test_misc.rs @@ -1,6 +1,30 @@ +use qoi::{ + consts::{QOI_OP_RGB, QOI_OP_RUN}, + decode_to_vec, Channels, ColorSpace, Header, Result, +}; + #[test] fn test_new_encoder() { // this used to fail due to `Bytes` not being `pub` let arr = [0u8]; let _ = qoi::Decoder::new(&arr[..]); -} \ No newline at end of file +} + +#[test] +fn test_start_with_qoi_op_run() -> Result<()> { + let header = Header::try_new(3, 1, Channels::Rgba, ColorSpace::Linear)?; + let mut qoi_data: Vec<_> = header.encode().into_iter().collect(); + qoi_data.extend([QOI_OP_RUN | 1, QOI_OP_RGB, 10, 20, 30]); + qoi_data.extend([0; 7]); + qoi_data.push(1); + let (_, decoded) = decode_to_vec(&qoi_data)?; + assert_eq!(decoded, vec![0, 0, 0, 255, 0, 0, 0, 255, 10, 20, 30, 255]); + Ok(()) +} + +#[cfg(target_endian = "big")] +#[test] +fn test_big_endian() { + // so we can see it in the CI logs + assert_eq!(u16::to_be_bytes(1), [0, 1]); +} diff --git a/tests/test_ref.rs b/tests/test_ref.rs index 59a7315..fbfaa56 100644 --- a/tests/test_ref.rs +++ b/tests/test_ref.rs @@ -1,11 +1,10 @@ use std::fs::{self, File}; use std::path::{Path, PathBuf}; -use anyhow::{bail, Result}; use cfg_if::cfg_if; use walkdir::{DirEntry, WalkDir}; -use qoi::{decode_to_vec, encode_to_vec}; +use qoi::{decode_to_vec, encode_to_vec, Result}; fn find_qoi_png_pairs(root: impl AsRef) -> Vec<(PathBuf, PathBuf)> { let root = root.as_ref(); @@ -29,7 +28,7 @@ fn find_qoi_png_pairs(root: impl AsRef) -> Vec<(PathBuf, PathBuf)> { WalkDir::new(root) .follow_links(true) .into_iter() - .filter_map(Result::ok) + .filter_map(std::result::Result::ok) .map(DirEntry::into_path) .filter_map(|p| check_qoi_png_pair(&p)), ) @@ -45,18 +44,18 @@ struct Image { } impl Image { - fn from_png(filename: &Path) -> Result { - let decoder = png::Decoder::new(File::open(filename)?); - let mut reader = decoder.read_info()?; + fn from_png(filename: &Path) -> Self { + let decoder = png::Decoder::new(File::open(filename).unwrap()); + let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; - let info = reader.next_frame(&mut buf)?; + let info = reader.next_frame(&mut buf).unwrap(); let bytes = &buf[..info.buffer_size()]; - Ok(Self { + Self { width: info.width, height: info.height, channels: info.color_type.samples() as u8, data: bytes.to_vec(), - }) + } } } @@ -67,7 +66,7 @@ fn compare_slices(name: &str, desc: &str, result: &[u8], expected: &[u8]) -> Res if let Some(i) = (0..result.len().min(expected.len())).position(|i| result[i] != expected[i]) { - bail!( + panic!( "{}: {} mismatch at byte {}: expected {:?}, got {:?}", name, desc, @@ -76,7 +75,7 @@ fn compare_slices(name: &str, desc: &str, result: &[u8], expected: &[u8]) -> Res &result[i..(i + 4).min(result.len())], ); } else { - bail!( + panic!( "{}: {} length mismatch: expected {}, got {}", name, desc, @@ -94,10 +93,10 @@ fn test_reference_images() -> Result<()> { for (qoi_path, png_path) in &pairs { let png_name = png_path.file_name().unwrap_or_default().to_string_lossy(); - let img = Image::from_png(png_path)?; + let img = Image::from_png(png_path); println!("{} {} {} {}", png_name, img.width, img.height, img.channels); let encoded = encode_to_vec(&img.data, img.width, img.height)?; - let expected = fs::read(qoi_path)?; + let expected = fs::read(qoi_path).unwrap(); assert_eq!(encoded.len(), expected.len()); // this should match regardless cfg_if! { if #[cfg(feature = "reference")] {