diff --git a/.github/workflows/oxipng.yml b/.github/workflows/oxipng.yml index 4c4d10af..3361fcb7 100644 --- a/.github/workflows/oxipng.yml +++ b/.github/workflows/oxipng.yml @@ -49,9 +49,9 @@ jobs: - target: i686-pc-windows-msvc os: windows-latest - target: x86_64-apple-darwin - os: macos-latest + os: macos-12 - target: aarch64-apple-darwin - os: macos-latest + os: macos-14 # ARM64 runner env: CARGO_BUILD_TARGET: ${{ matrix.target }} @@ -122,19 +122,11 @@ jobs: reporter: github-check fail_on_error: true - # There aren't good user-mode ARM64 emulators we can use on x64 macOS hosts. - # QEMU doesn't have any plans to add such support due to a lack of kernel - # syscall stability guarantees: https://gitlab.com/qemu-project/qemu/-/issues/1682 - name: Run tests - if: matrix.target != 'aarch64-apple-darwin' run: | cargo nextest run --release --features sanity-checks cargo test --doc --release --features sanity-checks - - name: Build tests (ARM64 macOS only) - if: matrix.target == 'aarch64-apple-darwin' - run: cargo test --release --features sanity-checks --no-run - - name: Build benchmarks run: cargo bench --no-run diff --git a/src/png/scan_lines.rs b/src/png/scan_lines.rs index 0c31dd4b..080c211e 100644 --- a/src/png/scan_lines.rs +++ b/src/png/scan_lines.rs @@ -24,20 +24,26 @@ impl<'a> Iterator for ScanLines<'a> { type Item = ScanLine<'a>; #[inline] fn next(&mut self) -> Option { - self.iter.next().map(|(len, pass, num_pixels)| { - let (data, rest) = self.raw_data.split_at(len); - self.raw_data = rest; - let (&filter, data) = if self.has_filter { - data.split_first().unwrap() - } else { - (&0, data) - }; - ScanLine { - filter, - data, - pass, - num_pixels, - } + let (len, pass, num_pixels) = self.iter.next()?; + debug_assert!(self.raw_data.len() >= len); + debug_assert!(!self.has_filter || len > 1); + // The data length should always be correct here but this check assures + // the compiler that it doesn't need to account for a potential panic + if self.raw_data.len() < len { + return None; + } + let (data, rest) = self.raw_data.split_at(len); + self.raw_data = rest; + let (&filter, data) = if self.has_filter { + data.split_first().unwrap() + } else { + (&0, data) + }; + Some(ScanLine { + filter, + data, + pass, + num_pixels, }) } } diff --git a/src/reduction/palette.rs b/src/reduction/palette.rs index aa903559..ce5459e3 100644 --- a/src/reduction/palette.rs +++ b/src/reduction/palette.rs @@ -14,9 +14,8 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option if png.ihdr.bit_depth != BitDepth::Eight { return None; } - let palette = match &png.ihdr.color_type { - ColorType::Indexed { palette } if palette.len() > 1 => palette, - _ => return None, + let ColorType::Indexed { palette } = &png.ihdr.color_type else { + return None; }; let mut used = [false; 256]; @@ -43,8 +42,9 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option let data = if did_change { // Reassign data bytes to new indices png.data.iter().map(|b| byte_map[*b as usize]).collect() - } else if condensed.len() < palette.len() { - // Data is unchanged but palette will be truncated + } else if condensed.len() != palette.len() { + // Data is unchanged but palette is different size + // Note the new palette could potentially be larger if the original had a missing entry png.data.clone() } else { // Nothing has changed @@ -183,23 +183,36 @@ pub fn sorted_palette_battiato(png: &PngImage) -> Option { // Find the most popular color on the image edges (the pixels neighboring the filter bytes) fn most_popular_edge_color(num_colors: usize, png: &PngImage) -> usize { - let mut counts = vec![0; num_colors]; + let mut counts = [0u32; 256]; for line in png.scan_lines(false) { - counts[line.data[0] as usize] += 1; - counts[line.data[line.data.len() - 1] as usize] += 1; + if let &[first, .., last] = line.data { + counts[first as usize] += 1; + counts[last as usize] += 1; + } } - counts.iter().enumerate().max_by_key(|(_, &v)| v).unwrap().0 + counts + .iter() + .copied() + .take(num_colors) + .enumerate() + .max_by_key(|&(_, v)| v) + .unwrap_or_default() + .0 } // Calculate co-occurences matrix -fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec> { - let mut matrix = vec![vec![0; num_colors]; num_colors]; +fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec> { + let mut matrix = vec![vec![0u32; num_colors]; num_colors]; let mut prev: Option = None; + let mut prev_val = None; for line in png.scan_lines(false) { for i in 0..line.data.len() { let val = line.data[i] as usize; - if i > 0 { - matrix[line.data[i - 1] as usize][val] += 1; + if val > num_colors { + continue; + } + if let Some(prev_val) = prev_val.replace(val) { + matrix[prev_val][val] += 1; } if let Some(prev) = &prev { matrix[prev.data[i] as usize][val] += 1; @@ -211,7 +224,7 @@ fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec> { } // Calculate edge list sorted by weight -fn weighted_edges(matrix: &[Vec]) -> Vec<(usize, usize)> { +fn weighted_edges(matrix: &[Vec]) -> Vec<(usize, usize)> { let mut edges = Vec::new(); for i in 0..matrix.len() { for j in 0..i { diff --git a/tests/files/palette_should_be_reduced_with_missing.png b/tests/files/palette_should_be_reduced_with_missing.png new file mode 100644 index 00000000..fd6d61b8 Binary files /dev/null and b/tests/files/palette_should_be_reduced_with_missing.png differ diff --git a/tests/reduction.rs b/tests/reduction.rs index 221e320d..7a6292c4 100644 --- a/tests/reduction.rs +++ b/tests/reduction.rs @@ -1049,6 +1049,43 @@ fn palette_should_be_reduced_with_both() { remove_file(output).ok(); } +#[test] +fn palette_should_be_reduced_with_missing() { + let input = PathBuf::from("tests/files/palette_should_be_reduced_with_missing.png"); + let (output, opts) = get_opts(&input); + + let png = PngData::new(&input, &opts).unwrap(); + + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 2); + } + + match oxipng::optimize(&InFile::Path(input), &output, &opts) { + Ok(_) => (), + Err(x) => panic!("{}", x), + }; + let output = output.path().unwrap(); + assert!(output.exists()); + + let png = match PngData::new(output, &opts) { + Ok(x) => x, + Err(x) => { + remove_file(output).ok(); + panic!("{}", x) + } + }; + + assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED); + assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Two); + if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { + assert_eq!(palette.len(), 3); + } + + remove_file(output).ok(); +} + #[test] fn rgba_16_reduce_alpha() { test_it_converts(