This is an experiment I started a while ago before life happened. It
reduces memory usage of fast evaluation (-o2 and lower), bringing it
inline with normal (slow) evaluation. It does this by not retaining the
filtered image data of the evaluations, but instead retaining the row
filters that were used in each line so it can be quickly re-filtered
when required. This does incur a tiny performance penalty, but it's
negligible even at `-o0`.
Although this is fully functional, there are a few rough spots in the
code so I'm just opening it as a draft for now.
```
PR -sao6
70.08 real 614.36 user 2.66 sys
2024243200 maximum resident set size
1839339520 peak memory footprint
PR -sao2
11.16 real 60.85 user 1.33 sys
1982562304 maximum resident set size
1842173824 peak memory footprint
PR -sao2 -t1
55.11 real 53.85 user 0.76 sys
429457408 maximum resident set size
245008064 peak memory footprint
master -sao6
67.70 real 616.07 user 2.72 sys
2043379712 maximum resident set size
1838340416 peak memory footprint
master -sao2
11.53 real 60.63 user 1.25 sys
2753396736 maximum resident set size
2283741440 peak memory footprint
master -sao2 -t1
54.29 real 53.55 user 0.72 sys
626311168 maximum resident set size
305252544 peak memory footprint
```
Note that this involves some refactoring of `RowFilter` and the new
`FilterStrategy`. These are breaking changes so it will ultimately be
destined for v10. One advantage to this new structure is it opens the
door for future changes such as allowing the Brute strategy to take a
parameter for the number of lines.
665 lines
17 KiB
Rust
665 lines
17 KiB
Rust
#[cfg(feature = "zopfli")]
|
|
use std::num::NonZeroU8;
|
|
use std::{
|
|
fs::remove_file,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use indexmap::indexset;
|
|
use oxipng::{internal_tests::*, *};
|
|
|
|
const GRAY: u8 = 0;
|
|
const RGB: u8 = 2;
|
|
const INDEXED: u8 = 3;
|
|
|
|
fn get_opts(input: &Path) -> (OutFile, Options) {
|
|
let options = Options {
|
|
force: true,
|
|
fast_evaluation: false,
|
|
filter: indexset! {FilterStrategy::NONE},
|
|
..Default::default()
|
|
};
|
|
(OutFile::from_path(input.with_extension("out.png")), options)
|
|
}
|
|
|
|
/// Add callback to allow checks before the output file is deleted again
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn test_it_converts_callbacks<CBPRE, CBPOST>(
|
|
input: PathBuf,
|
|
output: &OutFile,
|
|
opts: &oxipng::Options,
|
|
color_type_in: u8,
|
|
bit_depth_in: BitDepth,
|
|
color_type_out: u8,
|
|
bit_depth_out: BitDepth,
|
|
mut callback_pre: CBPRE,
|
|
mut callback_post: CBPOST,
|
|
) where
|
|
CBPOST: FnMut(&PngData),
|
|
CBPRE: FnMut(&PngData),
|
|
{
|
|
let parse_opts = Options {
|
|
fix_errors: true,
|
|
..Default::default()
|
|
};
|
|
let png = PngData::new(&input, &parse_opts).unwrap();
|
|
|
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in);
|
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in);
|
|
|
|
callback_pre(&png);
|
|
|
|
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)
|
|
}
|
|
};
|
|
|
|
callback_post(&png);
|
|
|
|
assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out);
|
|
assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out);
|
|
|
|
remove_file(output).ok();
|
|
}
|
|
|
|
/// Shim for new callback functionality
|
|
fn test_it_converts(
|
|
input: PathBuf,
|
|
output: &OutFile,
|
|
opts: &oxipng::Options,
|
|
color_type_in: u8,
|
|
bit_depth_in: BitDepth,
|
|
color_type_out: u8,
|
|
bit_depth_out: BitDepth,
|
|
) {
|
|
test_it_converts_callbacks(
|
|
input,
|
|
output,
|
|
opts,
|
|
color_type_in,
|
|
bit_depth_in,
|
|
color_type_out,
|
|
bit_depth_out,
|
|
|_| {},
|
|
|_| {},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn verbose_mode() {
|
|
use std::cell::RefCell;
|
|
#[cfg(not(feature = "parallel"))]
|
|
use std::sync::mpsc::{Sender, channel as unbounded};
|
|
|
|
#[cfg(feature = "parallel")]
|
|
use crossbeam_channel::{Sender, unbounded};
|
|
use log::{Level, LevelFilter, Log, Metadata, Record, set_logger, set_max_level};
|
|
|
|
// Rust runs tests in parallel by default.
|
|
// We want to make sure that we verify only logs from our test.
|
|
//
|
|
// For that, we store an Option in a thread-local variable and
|
|
// initialise it with Some(sender) only on threads spawned within
|
|
// our test.
|
|
thread_local! {
|
|
static VERBOSE_LOGS: RefCell<Option<Sender<String>>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
struct LogTester;
|
|
|
|
impl Log for LogTester {
|
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
|
metadata.level() <= Level::Debug
|
|
}
|
|
|
|
fn log(&self, record: &Record) {
|
|
if record.level() == Level::Debug {
|
|
VERBOSE_LOGS.with(|logs| {
|
|
// If current thread has a storage for logs, add our line.
|
|
// Otherwise our handler is invoked from an unrelated test.
|
|
if let Some(logs) = logs.borrow().as_ref() {
|
|
logs.send(record.args().to_string()).unwrap();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn flush(&self) {}
|
|
}
|
|
|
|
set_logger(&LogTester).unwrap();
|
|
set_max_level(LevelFilter::Debug);
|
|
|
|
let input = PathBuf::from("tests/files/verbose_mode.png");
|
|
let (output, opts) = get_opts(&input);
|
|
|
|
let (sender, receiver) = unbounded();
|
|
|
|
let thread_init = move || {
|
|
// Initialise logs storage for all threads within our test.
|
|
VERBOSE_LOGS.with(|logs| *logs.borrow_mut() = Some(sender.clone()));
|
|
};
|
|
let thread_exec = move || {
|
|
test_it_converts(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
);
|
|
};
|
|
|
|
#[cfg(feature = "parallel")]
|
|
rayon::ThreadPoolBuilder::new()
|
|
.start_handler(move |_| thread_init())
|
|
.num_threads(rayon::current_num_threads() + 1)
|
|
.build()
|
|
.unwrap()
|
|
.install(move || rayon::spawn(thread_exec));
|
|
|
|
#[cfg(not(feature = "parallel"))]
|
|
std::thread::spawn(move || {
|
|
thread_init();
|
|
thread_exec();
|
|
});
|
|
|
|
let logs: Vec<_> = receiver.into_iter().collect();
|
|
let expected_prefixes = [
|
|
" 500x400 pixels, PNG format",
|
|
" 8-bit RGB, non-interlaced",
|
|
" IDAT size = 113794 bytes",
|
|
" File size = 114708 bytes",
|
|
"Trying 1 filters with zc = ",
|
|
"Found better result:",
|
|
" zc = 11, f = None",
|
|
" IDAT size = ",
|
|
" file size = ",
|
|
];
|
|
assert_eq!(logs.len(), expected_prefixes.len());
|
|
for (i, log) in logs.into_iter().enumerate() {
|
|
let expected_prefix = expected_prefixes[i];
|
|
assert!(
|
|
log.starts_with(expected_prefix),
|
|
"logs[{i}] = {log:?} doesn't start with {expected_prefix:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn count_chunk(png: &PngData, name: &[u8; 4]) -> usize {
|
|
png.aux_chunks
|
|
.iter()
|
|
.filter(|chunk| &chunk.name == name)
|
|
.count()
|
|
}
|
|
|
|
#[test]
|
|
fn strip_chunks_list() {
|
|
let input = PathBuf::from("tests/files/strip_chunks_list.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::Strip(indexset![*b"iCCP", *b"tEXt"]);
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 3);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 1);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 0);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 1);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 0);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn strip_chunks_safe() {
|
|
let input = PathBuf::from("tests/files/strip_chunks_safe.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::Safe;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 3);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 1);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 0);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 0);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 0);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn strip_chunks_all() {
|
|
let input = PathBuf::from("tests/files/strip_chunks_all.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::All;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 3);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 1);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 0);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 0);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 0);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn strip_chunks_none() {
|
|
let input = PathBuf::from("tests/files/strip_chunks_none.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::None;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 3);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 1);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"tEXt"), 3);
|
|
assert_eq!(count_chunk(png, b"iTXt"), 1);
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interlacing_0_to_1() {
|
|
let input = PathBuf::from("tests/files/interlacing_0_to_1.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.interlace = Some(Interlacing::Adam7);
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
|
},
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interlacing_1_to_0() {
|
|
let input = PathBuf::from("tests/files/interlacing_1_to_0.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.interlace = Some(Interlacing::None);
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
|
},
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interlacing_0_to_1_small_files() {
|
|
let input = PathBuf::from("tests/files/interlacing_0_to_1_small_files.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.interlace = Some(Interlacing::Adam7);
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
|
},
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interlacing_1_to_0_small_files() {
|
|
let input = PathBuf::from("tests/files/interlacing_1_to_0_small_files.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.interlace = Some(Interlacing::None);
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
|
},
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interlaced_0_to_1_other_filter_mode() {
|
|
let input = PathBuf::from("tests/files/interlaced_0_to_1_other_filter_mode.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.interlace = Some(Interlacing::Adam7);
|
|
opts.filter = indexset! {FilterStrategy::PAETH};
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Sixteen,
|
|
GRAY,
|
|
BitDepth::Sixteen,
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
|
},
|
|
|png| {
|
|
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preserve_attrs() {
|
|
let input = PathBuf::from("tests/files/preserve_attrs.png");
|
|
|
|
#[cfg(feature = "filetime")]
|
|
let meta_input = input
|
|
.metadata()
|
|
.expect("unable to get file metadata for output file");
|
|
#[cfg(feature = "filetime")]
|
|
let atime_canon = filetime::FileTime::from_last_access_time(&meta_input);
|
|
#[cfg(feature = "filetime")]
|
|
let mtime_canon = filetime::FileTime::from_last_modification_time(&meta_input);
|
|
|
|
let (mut output, opts) = get_opts(&input);
|
|
if let OutFile::Path { preserve_attrs, .. } = &mut output {
|
|
*preserve_attrs = true;
|
|
}
|
|
|
|
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
|
Ok(_) => (),
|
|
Err(x) => panic!("{}", x),
|
|
};
|
|
let output = output.path().unwrap();
|
|
assert!(output.exists());
|
|
|
|
#[cfg(feature = "filetime")]
|
|
let meta_output = output
|
|
.metadata()
|
|
.expect("unable to get file metadata for output file");
|
|
#[cfg(feature = "filetime")]
|
|
assert_eq!(
|
|
&atime_canon,
|
|
&filetime::FileTime::from_last_access_time(&meta_output),
|
|
"expected access time to be identical to that of input",
|
|
);
|
|
#[cfg(feature = "filetime")]
|
|
assert_eq!(
|
|
&mtime_canon,
|
|
&filetime::FileTime::from_last_modification_time(&meta_output),
|
|
"expected modification time to be identical to that of input",
|
|
);
|
|
|
|
match PngData::new(output, &opts) {
|
|
Ok(x) => x,
|
|
Err(x) => {
|
|
remove_file(output).ok();
|
|
panic!("{}", x)
|
|
}
|
|
};
|
|
|
|
remove_file(output).ok();
|
|
|
|
// TODO: Actually check permissions
|
|
}
|
|
|
|
#[test]
|
|
fn fix_errors() {
|
|
let input = PathBuf::from("tests/files/fix_errors.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.fix_errors = true;
|
|
|
|
test_it_converts(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_grayscale_change() {
|
|
let input = PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.grayscale_reduction = false;
|
|
|
|
test_it_converts(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_adobe_rgb_disallow_gray() {
|
|
let input = PathBuf::from("tests/files/profile_adobe_rgb_disallow_gray.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::Safe;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_srgb_allow_gray() {
|
|
let input = PathBuf::from("tests/files/profile_srgb_allow_gray.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::Safe;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
GRAY,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 0);
|
|
assert_eq!(count_chunk(png, b"sRGB"), 0);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_srgb_no_strip_disallow_gray() {
|
|
let input = PathBuf::from("tests/files/profile_srgb_no_strip_disallow_gray.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::None;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
INDEXED,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_gray_disallow_color() {
|
|
let input = PathBuf::from("tests/files/profile_gray_disallow_color.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.strip = StripChunks::Safe;
|
|
|
|
test_it_converts_callbacks(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
GRAY,
|
|
BitDepth::Eight,
|
|
GRAY,
|
|
BitDepth::Eight,
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
|png| {
|
|
assert_eq!(count_chunk(png, b"iCCP"), 1);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_bit_depth_change() {
|
|
let input = PathBuf::from("tests/files/palette_4_should_be_palette_2.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.bit_depth_reduction = false;
|
|
|
|
test_it_converts(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
INDEXED,
|
|
BitDepth::Four,
|
|
INDEXED,
|
|
BitDepth::Four,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scale_16() {
|
|
let input = PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.scale_16 = true;
|
|
|
|
test_it_converts(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Sixteen,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "zopfli")]
|
|
fn zopfli_mode() {
|
|
let input = PathBuf::from("tests/files/zopfli_mode.png");
|
|
let (output, mut opts) = get_opts(&input);
|
|
opts.deflate = Deflaters::Zopfli {
|
|
iterations: NonZeroU8::new(15).unwrap(),
|
|
};
|
|
|
|
test_it_converts(
|
|
input,
|
|
&output,
|
|
&opts,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
RGB,
|
|
BitDepth::Eight,
|
|
);
|
|
}
|