Merge branch 'master' into docs/repology-packaging-status
This commit is contained in:
commit
e8716e6fd1
5 changed files with 86 additions and 38 deletions
12
.github/workflows/oxipng.yml
vendored
12
.github/workflows/oxipng.yml
vendored
|
|
@ -49,9 +49,9 @@ jobs:
|
||||||
- target: i686-pc-windows-msvc
|
- target: i686-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-12
|
||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-14 # ARM64 runner
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
CARGO_BUILD_TARGET: ${{ matrix.target }}
|
||||||
|
|
@ -122,19 +122,11 @@ jobs:
|
||||||
reporter: github-check
|
reporter: github-check
|
||||||
fail_on_error: true
|
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
|
- name: Run tests
|
||||||
if: matrix.target != 'aarch64-apple-darwin'
|
|
||||||
run: |
|
run: |
|
||||||
cargo nextest run --release --features sanity-checks
|
cargo nextest run --release --features sanity-checks
|
||||||
cargo test --doc --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
|
- name: Build benchmarks
|
||||||
run: cargo bench --no-run
|
run: cargo bench --no-run
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,14 @@ impl<'a> Iterator for ScanLines<'a> {
|
||||||
type Item = ScanLine<'a>;
|
type Item = ScanLine<'a>;
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.iter.next().map(|(len, 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);
|
let (data, rest) = self.raw_data.split_at(len);
|
||||||
self.raw_data = rest;
|
self.raw_data = rest;
|
||||||
let (&filter, data) = if self.has_filter {
|
let (&filter, data) = if self.has_filter {
|
||||||
|
|
@ -32,12 +39,11 @@ impl<'a> Iterator for ScanLines<'a> {
|
||||||
} else {
|
} else {
|
||||||
(&0, data)
|
(&0, data)
|
||||||
};
|
};
|
||||||
ScanLine {
|
Some(ScanLine {
|
||||||
filter,
|
filter,
|
||||||
data,
|
data,
|
||||||
pass,
|
pass,
|
||||||
num_pixels,
|
num_pixels,
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage>
|
||||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
if png.ihdr.bit_depth != BitDepth::Eight {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let palette = match &png.ihdr.color_type {
|
let ColorType::Indexed { palette } = &png.ihdr.color_type else {
|
||||||
ColorType::Indexed { palette } if palette.len() > 1 => palette,
|
return None;
|
||||||
_ => return None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut used = [false; 256];
|
let mut used = [false; 256];
|
||||||
|
|
@ -43,8 +42,9 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage>
|
||||||
let data = if did_change {
|
let data = if did_change {
|
||||||
// Reassign data bytes to new indices
|
// Reassign data bytes to new indices
|
||||||
png.data.iter().map(|b| byte_map[*b as usize]).collect()
|
png.data.iter().map(|b| byte_map[*b as usize]).collect()
|
||||||
} else if condensed.len() < palette.len() {
|
} else if condensed.len() != palette.len() {
|
||||||
// Data is unchanged but palette will be truncated
|
// 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()
|
png.data.clone()
|
||||||
} else {
|
} else {
|
||||||
// Nothing has changed
|
// Nothing has changed
|
||||||
|
|
@ -183,23 +183,36 @@ pub fn sorted_palette_battiato(png: &PngImage) -> Option<PngImage> {
|
||||||
|
|
||||||
// Find the most popular color on the image edges (the pixels neighboring the filter bytes)
|
// 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 {
|
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) {
|
for line in png.scan_lines(false) {
|
||||||
counts[line.data[0] as usize] += 1;
|
if let &[first, .., last] = line.data {
|
||||||
counts[line.data[line.data.len() - 1] as usize] += 1;
|
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
|
// Calculate co-occurences matrix
|
||||||
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<usize>> {
|
fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<u32>> {
|
||||||
let mut matrix = vec![vec![0; num_colors]; num_colors];
|
let mut matrix = vec![vec![0u32; num_colors]; num_colors];
|
||||||
let mut prev: Option<ScanLine> = None;
|
let mut prev: Option<ScanLine> = None;
|
||||||
|
let mut prev_val = None;
|
||||||
for line in png.scan_lines(false) {
|
for line in png.scan_lines(false) {
|
||||||
for i in 0..line.data.len() {
|
for i in 0..line.data.len() {
|
||||||
let val = line.data[i] as usize;
|
let val = line.data[i] as usize;
|
||||||
if i > 0 {
|
if val > num_colors {
|
||||||
matrix[line.data[i - 1] as usize][val] += 1;
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(prev_val) = prev_val.replace(val) {
|
||||||
|
matrix[prev_val][val] += 1;
|
||||||
}
|
}
|
||||||
if let Some(prev) = &prev {
|
if let Some(prev) = &prev {
|
||||||
matrix[prev.data[i] as usize][val] += 1;
|
matrix[prev.data[i] as usize][val] += 1;
|
||||||
|
|
@ -211,7 +224,7 @@ fn co_occurrence_matrix(num_colors: usize, png: &PngImage) -> Vec<Vec<usize>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate edge list sorted by weight
|
// Calculate edge list sorted by weight
|
||||||
fn weighted_edges(matrix: &[Vec<usize>]) -> Vec<(usize, usize)> {
|
fn weighted_edges(matrix: &[Vec<u32>]) -> Vec<(usize, usize)> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for i in 0..matrix.len() {
|
for i in 0..matrix.len() {
|
||||||
for j in 0..i {
|
for j in 0..i {
|
||||||
|
|
|
||||||
BIN
tests/files/palette_should_be_reduced_with_missing.png
Normal file
BIN
tests/files/palette_should_be_reduced_with_missing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 B |
|
|
@ -1049,6 +1049,43 @@ fn palette_should_be_reduced_with_both() {
|
||||||
remove_file(output).ok();
|
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]
|
#[test]
|
||||||
fn rgba_16_reduce_alpha() {
|
fn rgba_16_reduce_alpha() {
|
||||||
test_it_converts(
|
test_it_converts(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue