Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vpx): keep alpha channels for rgba bmp #73

Merged
merged 1 commit into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 35 additions & 18 deletions src/vpx/expanded.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bytes::{Buf, BufMut, BytesMut};
use std::collections::HashSet;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt::{Display, Formatter};
use std::io::{self, Read, Write};
use std::iter::Zip;
Expand All @@ -10,6 +11,7 @@ use std::{fs::File, path::Path};

use cfb::CompoundFile;
use flate2::read::ZlibDecoder;
use image::DynamicImage;
use serde::de;
use serde_json::Value;

Expand Down Expand Up @@ -322,24 +324,32 @@ fn write_image_bmp(
height: u32,
) -> io::Result<()> {
let decompressed_bgra = from_lzw_blocks(lzw_compressed_data);

// assert that alpha is 255 for all pixels
for bgra in decompressed_bgra.chunks_exact(4) {
assert_eq!(bgra[3], 255);
}

// convert to RGBA
let decompressed_rgba: Vec<u8> = swap_red_and_blue(&decompressed_bgra);

let rgba_image = image::RgbaImage::from_raw(width, height, decompressed_rgba)
.expect("Decompressed image data does not match dimensions");
let dynamic_image = image::DynamicImage::ImageRgba8(rgba_image);

// convert to RGB
let rgb_image = dynamic_image.to_rgb8();

// save the image
rgb_image.save(file_path).map_err(|image_error| {
let dynamic_image = DynamicImage::ImageRgba8(rgba_image);

let uses_alpha = decompressed_bgra.chunks_exact(4).any(|bgra| bgra[3] != 255);
let image_to_save = if uses_alpha {
// One example is the table "Guns N Roses (Data East 1994).vpx"
// that contains vp9 images with non-255 alpha values.
// They are actually labeled as sRGBA in the Visual Pinball image manager.
// However, when Visual Pinball itself exports the image it uses 255 alpha values.
let file_name = file_path
.file_name()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
eprintln!(
"Image {} has non-opaque pixels, writing as RGBA BMP that might not be supported by all applications",
file_name
);
dynamic_image
} else {
let rgb_image = dynamic_image.to_rgb8();
DynamicImage::ImageRgb8(rgb_image)
};
image_to_save.save(file_path).map_err(|image_error| {
io::Error::new(
io::ErrorKind::Other,
format!(
Expand Down Expand Up @@ -409,18 +419,25 @@ fn read_image_bmp(data: &[u8], width: u32, height: u32) -> io::Result<Vec<u8>> {
|image_error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to read bitmap: {}", image_error),
format!("Failed to read BMP image: {}", image_error),
)
},
)?;

// make assertions on the image dimensions
assert_eq!(image.width(), width, "Image width does not match");
assert_eq!(image.height(), height, "Image height does not match");
assert_eq!(image.color(), image::ColorType::Rgb8, "Image is not RGB");

// get the raw image data
let raw_rgba = image.to_rgba8().into_raw();
let raw_rgba = match image.color() {
image::ColorType::Rgb8 => image.to_rgba8().into_raw(),
image::ColorType::Rgba8 => image.to_rgba8().into_raw(),
other => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("BMP image uses {:?}, expecting Rgb8 or Rgba8 format", other),
))
}
};

// convert to BGRA
let raw_bgra: Vec<u8> = swap_red_and_blue(&raw_rgba);
Expand Down
4 changes: 3 additions & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,10 @@ fn biff_tags_and_hashes(reader: &mut BiffReader) -> Vec<(String, usize, usize, u
}
"BITS" => {
let data = reader.data_until("ALTV".as_bytes());
// Looks like vpinball encodes de lzw stream in a slightly different way.
// Looks like vpinball encodes de lzw stream in a slightly different way. Ending
// up with the same compressed size but different compressed data.
// However, vpinball can also read the standard lzw stream we write.
// So for these images we look at the raw data hash.
let decompressed = from_lzw_blocks(&data);
let hash = hash_data(&decompressed);
tags.push((
Expand Down
1 change: 1 addition & 0 deletions tests/vpx_read_extract_assemble_write_compare_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod test {
// * Johnny Mnemonic (Williams 1995) VPW v1.0.2.vpx - animated frames that overlap with primitive names
// * Future Spa (Bally 1979) v4.3.vpx - NaN in table setup values
// * InvaderTable_2.260.vpx - Symbol fonts
// * Guns N Roses (Data East 1994).vpx - contains BMP with non-255 alpha values
let filtered: Vec<&PathBuf> = paths
.iter()
// .filter(|path| {
Expand Down