|
1 | 1 | use std::cell::{BorrowError, BorrowMutError, Ref, RefCell, RefMut}; |
| 2 | +use std::collections::VecDeque; |
2 | 3 | use std::fmt; |
3 | 4 | use std::io::{Read, Write}; |
4 | 5 | use std::ops::{Deref, DerefMut}; |
@@ -121,6 +122,12 @@ pub struct Image { |
121 | 122 | operations: Vec<&'static str>, |
122 | 123 | } |
123 | 124 |
|
| 125 | +#[derive(Debug, Clone)] |
| 126 | +pub struct ImageCache { |
| 127 | + capacity: usize, |
| 128 | + entries: VecDeque<Gc<Image>>, |
| 129 | +} |
| 130 | + |
124 | 131 | #[derive(Debug, Clone, PartialEq, Eq)] |
125 | 132 | pub struct ImageError { |
126 | 133 | message: String, |
@@ -148,6 +155,23 @@ impl From<std::io::Error> for ImageError { |
148 | 155 | } |
149 | 156 | } |
150 | 157 |
|
| 158 | +pub trait RuntimeImageRef { |
| 159 | + fn with_image<R>(&self, f: impl FnOnce(&Image) -> R) -> R; |
| 160 | +} |
| 161 | + |
| 162 | +impl RuntimeImageRef for Image { |
| 163 | + fn with_image<R>(&self, f: impl FnOnce(&Image) -> R) -> R { |
| 164 | + f(self) |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +impl RuntimeImageRef for Gc<Image> { |
| 169 | + fn with_image<R>(&self, f: impl FnOnce(&Image) -> R) -> R { |
| 170 | + let image = self.read(); |
| 171 | + f(&image) |
| 172 | + } |
| 173 | +} |
| 174 | + |
151 | 175 | #[derive(Debug, Clone)] |
152 | 176 | pub struct JsonValue { |
153 | 177 | inner: serde_json::Value, |
@@ -411,29 +435,59 @@ pub fn image_sharpen(image: &mut Image) { |
411 | 435 | image.operations.push("sharpen"); |
412 | 436 | } |
413 | 437 |
|
414 | | -pub fn image_save<P: RuntimePath + ?Sized>(image: &Image, path: &P) -> Result<(), ImageError> { |
415 | | - let mut bytes = image.bytes.clone(); |
416 | | - bytes.extend_from_slice(b"\n# rsscript-image-ops:"); |
417 | | - bytes.extend_from_slice(image.operations.join(",").as_bytes()); |
418 | | - if let (Some(width), Some(height)) = (image.width, image.height) { |
419 | | - bytes.extend_from_slice(format!(";size={width}x{height}").as_bytes()); |
420 | | - } |
| 438 | +pub fn image_save<I: RuntimeImageRef + ?Sized, P: RuntimePath + ?Sized>( |
| 439 | + image: &I, |
| 440 | + path: &P, |
| 441 | +) -> Result<(), ImageError> { |
| 442 | + let bytes = image.with_image(|image| { |
| 443 | + let mut bytes = image.bytes.clone(); |
| 444 | + bytes.extend_from_slice(b"\n# rsscript-image-ops:"); |
| 445 | + bytes.extend_from_slice(image.operations.join(",").as_bytes()); |
| 446 | + if let (Some(width), Some(height)) = (image.width, image.height) { |
| 447 | + bytes.extend_from_slice(format!(";size={width}x{height}").as_bytes()); |
| 448 | + } |
| 449 | + bytes |
| 450 | + }); |
421 | 451 | std::fs::write(path.as_path(), bytes)?; |
422 | 452 | Ok(()) |
423 | 453 | } |
424 | 454 |
|
425 | | -pub fn image_inspect(image: &Image) { |
426 | | - let size = image |
427 | | - .width |
428 | | - .zip(image.height) |
429 | | - .map(|(width, height)| format!("{width}x{height}")) |
430 | | - .unwrap_or_else(|| "unknown".to_string()); |
431 | | - println!( |
432 | | - "image bytes={} size={} ops={}", |
433 | | - image.bytes.len(), |
434 | | - size, |
435 | | - image.operations.join(",") |
436 | | - ); |
| 455 | +pub fn image_inspect<I: RuntimeImageRef + ?Sized>(image: &I) { |
| 456 | + let summary = image.with_image(|image| { |
| 457 | + let size = image |
| 458 | + .width |
| 459 | + .zip(image.height) |
| 460 | + .map(|(width, height)| format!("{width}x{height}")) |
| 461 | + .unwrap_or_else(|| "unknown".to_string()); |
| 462 | + format!( |
| 463 | + "image bytes={} size={} ops={}", |
| 464 | + image.bytes.len(), |
| 465 | + size, |
| 466 | + image.operations.join(",") |
| 467 | + ) |
| 468 | + }); |
| 469 | + println!("{summary}"); |
| 470 | +} |
| 471 | + |
| 472 | +pub fn image_cache_new(capacity: i64) -> ImageCache { |
| 473 | + ImageCache { |
| 474 | + capacity: capacity.max(0) as usize, |
| 475 | + entries: VecDeque::new(), |
| 476 | + } |
| 477 | +} |
| 478 | + |
| 479 | +pub fn image_cache_store(cache: &mut ImageCache, image: &Gc<Image>) { |
| 480 | + if cache.capacity == 0 { |
| 481 | + return; |
| 482 | + } |
| 483 | + while cache.entries.len() >= cache.capacity { |
| 484 | + cache.entries.pop_front(); |
| 485 | + } |
| 486 | + cache.entries.push_back(image.clone()); |
| 487 | +} |
| 488 | + |
| 489 | +pub fn image_cache_len(cache: &ImageCache) -> i64 { |
| 490 | + cache.entries.len() as i64 |
437 | 491 | } |
438 | 492 |
|
439 | 493 | pub fn json_parse(text: &str) -> Result<JsonValue, JsonError> { |
@@ -953,6 +1007,28 @@ mod tests { |
953 | 1007 | let _ = std::fs::remove_file(output); |
954 | 1008 | } |
955 | 1009 |
|
| 1010 | + #[test] |
| 1011 | + fn image_cache_runtime_hooks_retain_managed_images_with_capacity() { |
| 1012 | + let mut cache = super::image_cache_new(1); |
| 1013 | + let first = super::manage(super::Image { |
| 1014 | + bytes: b"first".to_vec(), |
| 1015 | + width: None, |
| 1016 | + height: None, |
| 1017 | + operations: vec!["test"], |
| 1018 | + }); |
| 1019 | + let second = super::manage(super::Image { |
| 1020 | + bytes: b"second".to_vec(), |
| 1021 | + width: None, |
| 1022 | + height: None, |
| 1023 | + operations: vec!["test"], |
| 1024 | + }); |
| 1025 | + |
| 1026 | + super::image_cache_store(&mut cache, &first); |
| 1027 | + super::image_cache_store(&mut cache, &second); |
| 1028 | + |
| 1029 | + assert_eq!(super::image_cache_len(&cache), 1); |
| 1030 | + } |
| 1031 | + |
956 | 1032 | #[test] |
957 | 1033 | fn http_runtime_hooks_create_request_and_response() { |
958 | 1034 | let request = super::request_new("/users"); |
|
0 commit comments