Skip to content

Commit 36bc2f9

Browse files
committed
Super mega fast image crop by doing direct PNG decoding
1 parent 08826e9 commit 36bc2f9

File tree

6 files changed

+140
-29
lines changed

6 files changed

+140
-29
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ crc32fast = "1"
1616
deflate = "1"
1717
image = { version = "0.25", default-features = false, features = ["png"] }
1818
inflate = "0.4"
19+
png = "0.17.16"
1920
thiserror = "2"

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
use png::DecodingError;
12
use std::io;
23
use thiserror::Error;
34

45
#[derive(Error, Debug)]
56
pub enum DmiError {
67
#[error("IO error: {0}")]
78
Io(#[from] io::Error),
9+
#[error("PNG decoding error: {0}")]
10+
PngDecoding(#[from] DecodingError),
811
#[error("Image-processing error: {0}")]
912
Image(#[from] image::error::ImageError),
1013
#[error("FromUtf8 error: {0}")]

src/icon.rs

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::dirs::{Dirs, ALL_DIRS, CARDINAL_DIRS};
22
use crate::{error::DmiError, ztxt, RawDmi, RawDmiMetadata};
3+
use ::png::{ColorType, Decoder, Transformations};
34
use image::codecs::png;
4-
use image::{imageops, DynamicImage};
5+
use image::{imageops, RgbaImage};
56
use std::collections::HashMap;
67
use std::io::prelude::*;
78
use std::io::Cursor;
@@ -208,30 +209,105 @@ impl Icon {
208209
}
209210

210211
fn load_internal<R: Read + Seek>(reader: R, load_images: bool) -> Result<Icon, DmiError> {
211-
let (base_image, dmi_meta) = if load_images {
212+
let (dmi_meta, rgba_bytes) = if load_images {
212213
let raw_dmi = RawDmi::load(reader)?;
213-
let mut rawdmi_temp = vec![];
214-
raw_dmi.save(&mut rawdmi_temp)?;
215-
let chunk_ztxt = match raw_dmi.chunk_ztxt {
216-
Some(chunk) => chunk,
217-
None => {
218-
return Err(DmiError::Generic(String::from(
219-
"Error loading icon: no zTXt chunk found.",
220-
)))
214+
215+
let mut total_bytes = 45;
216+
if let Some(chunk_plte) = &raw_dmi.chunk_plte {
217+
total_bytes += chunk_plte.data.len() + 12
218+
}
219+
if let Some(other_chunks) = &raw_dmi.other_chunks {
220+
for chunk in other_chunks {
221+
total_bytes += chunk.data.len() + 12
222+
}
223+
}
224+
for idat in &raw_dmi.chunks_idat {
225+
total_bytes += idat.data.len() + 12;
226+
}
227+
228+
// Reconstruct the PNG
229+
let mut png_data = Vec::with_capacity(total_bytes);
230+
png_data.extend_from_slice(&raw_dmi.header);
231+
png_data.extend_from_slice(&raw_dmi.chunk_ihdr.data_length);
232+
png_data.extend_from_slice(&raw_dmi.chunk_ihdr.chunk_type);
233+
png_data.extend_from_slice(&raw_dmi.chunk_ihdr.data);
234+
png_data.extend_from_slice(&raw_dmi.chunk_ihdr.crc);
235+
if let Some(plte) = &raw_dmi.chunk_plte {
236+
png_data.extend_from_slice(&plte.data_length);
237+
png_data.extend_from_slice(&plte.chunk_type);
238+
png_data.extend_from_slice(&plte.data);
239+
png_data.extend_from_slice(&plte.crc);
240+
}
241+
if let Some(other_chunks) = &raw_dmi.other_chunks {
242+
for chunk in other_chunks {
243+
png_data.extend_from_slice(&chunk.data_length);
244+
png_data.extend_from_slice(&chunk.chunk_type);
245+
png_data.extend_from_slice(&chunk.data);
246+
png_data.extend_from_slice(&chunk.crc);
247+
}
248+
}
249+
for idat in &raw_dmi.chunks_idat {
250+
png_data.extend_from_slice(&idat.data_length);
251+
png_data.extend_from_slice(&idat.chunk_type);
252+
png_data.extend_from_slice(&idat.data);
253+
png_data.extend_from_slice(&idat.crc);
254+
}
255+
png_data.extend_from_slice(&raw_dmi.chunk_iend.data_length);
256+
png_data.extend_from_slice(&raw_dmi.chunk_iend.chunk_type);
257+
png_data.extend_from_slice(&raw_dmi.chunk_iend.crc);
258+
259+
let mut png_decoder = Decoder::new(std::io::Cursor::new(png_data));
260+
png_decoder.set_transformations(Transformations::EXPAND | Transformations::ALPHA);
261+
let mut png_reader = png_decoder.read_info()?;
262+
let mut rgba_buf = vec![0u8; png_reader.output_buffer_size()];
263+
let info = png_reader.next_frame(&mut rgba_buf)?;
264+
265+
// EXPAND and ALPHA do not expand grayscale images into RGBA. We can just do this manually.
266+
match info.color_type {
267+
ColorType::GrayscaleAlpha => {
268+
if rgba_buf.len() as u32 != info.width * info.height * 2 {
269+
return Err(DmiError::Generic(String::from("GrayscaleAlpha buffer length mismatch")));
270+
}
271+
let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize);
272+
for chunk in rgba_buf.chunks(2) {
273+
let gray = chunk[0];
274+
let alpha = chunk[1];
275+
new_buf.push(gray);
276+
new_buf.push(gray);
277+
new_buf.push(gray);
278+
new_buf.push(alpha);
279+
}
280+
rgba_buf = new_buf;
281+
}
282+
ColorType::Grayscale => {
283+
if rgba_buf.len() as u32 != info.width * info.height {
284+
return Err(DmiError::Generic(String::from("Grayscale buffer length mismatch")));
285+
}
286+
let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize);
287+
for gray in rgba_buf {
288+
new_buf.push(gray);
289+
new_buf.push(gray);
290+
new_buf.push(gray);
291+
new_buf.push(255);
292+
}
293+
rgba_buf = new_buf;
294+
}
295+
ColorType::Rgba => {}
296+
_ => {
297+
return Err(DmiError::Generic(format!("Unsupported ColorType (must be RGBA or convertible to RGBA): {:#?}", info.color_type)));
221298
}
299+
}
300+
301+
let dmi_meta = RawDmiMetadata {
302+
chunk_ihdr: raw_dmi.chunk_ihdr,
303+
chunk_ztxt: raw_dmi.chunk_ztxt.ok_or_else(|| {
304+
DmiError::Generic(String::from("Error loading icon: no zTXt chunk found."))
305+
})?,
222306
};
223-
(
224-
Some(image::load_from_memory_with_format(
225-
&rawdmi_temp,
226-
image::ImageFormat::Png,
227-
)?),
228-
RawDmiMetadata {
229-
chunk_ihdr: raw_dmi.chunk_ihdr,
230-
chunk_ztxt,
231-
},
232-
)
307+
308+
(dmi_meta, Some(rgba_buf))
233309
} else {
234-
(None, RawDmi::load_meta(reader)?)
310+
(RawDmi::load_meta(reader)?, None)
235311
};
236312

237313
let chunk_ztxt = &dmi_meta.chunk_ztxt;
@@ -315,7 +391,7 @@ impl Icon {
315391
"\tdirs" => dirs = Some(value.parse::<u8>()?),
316392
"\tframes" => frames = Some(value.parse::<u32>()?),
317393
"\tdelay" => {
318-
let mut delay_vector = vec![];
394+
let mut delay_vector = Vec::with_capacity(frames.unwrap_or(0) as usize);
319395
let text_delays = value.split_terminator(',');
320396
for text_entry in text_delays {
321397
delay_vector.push(text_entry.parse::<f32>()?);
@@ -369,14 +445,32 @@ impl Icon {
369445
return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states}).")));
370446
};
371447

372-
let mut images = vec![];
448+
let mut images = Vec::with_capacity((frames * dirs as u32) as usize);
449+
450+
if let Some(rgba_bytes) = &rgba_bytes {
451+
const RGBA_PIXEL_STRIDE: usize = 4;
452+
let row_stride = img_width as usize * RGBA_PIXEL_STRIDE;
453+
let expected_buffer_len = row_stride * (img_height as usize);
454+
if rgba_bytes.len() != expected_buffer_len {
455+
panic!("{} != {}", rgba_bytes.len(), expected_buffer_len);
456+
}
373457

374-
if let Some(full_image) = base_image.as_ref() {
375-
for image_idx in index..(index + (frames * dirs as u32)) {
458+
for image_idx in index..next_index {
376459
let x = (image_idx % width_in_states) * width;
377-
//This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function.
378460
let y = (image_idx / width_in_states) * height;
379-
images.push(full_image.crop_imm(x, y, width, height));
461+
462+
let mut cropped =
463+
Vec::with_capacity((width * height * RGBA_PIXEL_STRIDE as u32) as usize);
464+
for row in y..(y + height) {
465+
let start = (row as usize * row_stride) + (x as usize * RGBA_PIXEL_STRIDE);
466+
let end = start + (width as usize * RGBA_PIXEL_STRIDE);
467+
cropped.extend_from_slice(&rgba_bytes[start..end]);
468+
}
469+
470+
let tile = image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(width, height, cropped)
471+
.ok_or_else(|| DmiError::Generic("Failed to create image tile".to_string()))?;
472+
473+
images.push(tile);
380474
}
381475
}
382476

@@ -593,7 +687,7 @@ pub struct IconState {
593687
pub name: String,
594688
pub dirs: u8,
595689
pub frames: u32,
596-
pub images: Vec<image::DynamicImage>,
690+
pub images: Vec<image::RgbaImage>,
597691
pub delay: Option<Vec<f32>>,
598692
pub loop_flag: Looping,
599693
pub rewind: bool,
@@ -605,7 +699,7 @@ pub struct IconState {
605699
impl IconState {
606700
/// Gets a specific DynamicImage from `images`, given a dir and frame.
607701
/// If the dir or frame is invalid, returns a DmiError.
608-
pub fn get_image(&self, dir: &Dirs, frame: u32) -> Result<&DynamicImage, DmiError> {
702+
pub fn get_image(&self, dir: &Dirs, frame: u32) -> Result<&RgbaImage, DmiError> {
609703
if self.frames < frame {
610704
return Err(DmiError::IconState(format!(
611705
"Specified frame \"{frame}\" is larger than the number of frames ({}) for icon_state \"{}\"",

tests/dmi_ops.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ use std::path::PathBuf;
44

55
#[test]
66
fn load_and_save_dmi() {
7+
8+
let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
9+
load_path.push("tests/resources/empty.dmi");
10+
let load_file =
11+
File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No empty dmi: {load_path:?}"));
12+
let _ = Icon::load(&load_file).expect("Unable to load empty dmi");
13+
14+
let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
15+
load_path.push("tests/resources/greyscale_alpha.dmi");
16+
let load_file =
17+
File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No greyscale_alpha dmi: {load_path:?}"));
18+
let _ = Icon::load(&load_file).expect("Unable to greyscale_alpha dmi");
19+
720
let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
821
load_path.push("tests/resources/load_test.dmi");
922
let load_file =

tests/resources/empty.dmi

216 Bytes
Binary file not shown.
252 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)