diff --git a/crates/bin/src/lib.rs b/crates/bin/src/lib.rs index ea13971..3bc1f38 100644 --- a/crates/bin/src/lib.rs +++ b/crates/bin/src/lib.rs @@ -4,6 +4,7 @@ use tracing::{debug, info, trace}; pub fn process_once( provider: &dyn ClipboardProvider, opts: &ShrinkOptions, + extra_mimes: &[String], last_hash: &mut Option<[u8; 32]>, ) -> Result<(), Box> { let (data, mime_type) = match provider.capture() { @@ -38,10 +39,15 @@ pub fn process_once( result.mime_type ); - provider.distribute(&[(&result.data, result.mime_type.as_str())])?; + // Primary MIME type first, then any aliases the user wants to lie about. + let mut items: Vec<(&[u8], &str)> = vec![(&result.data, result.mime_type.as_str())]; + for alias in extra_mimes { + if alias != &result.mime_type { + items.push((&result.data, alias.as_str())); + } + } + provider.distribute(&items)?; - // Track hash of OUTPUT. After distributing webp-only, next capture will - // find image/webp (from our subprocess) → same hash → skip. No loop. *last_hash = Some(image_hash(&result.data)); Ok(()) } diff --git a/crates/bin/src/main.rs b/crates/bin/src/main.rs index 0b0d38f..f4dc751 100644 --- a/crates/bin/src/main.rs +++ b/crates/bin/src/main.rs @@ -17,6 +17,7 @@ async fn main() { }; let poll = Duration::from_millis(cfg.general.poll_ms); + let extra_mimes = cfg.general.extra_mimes.clone(); let opts = ShrinkOptions { quality: cfg.general.quality, target_format: cfg.general.format.into(), @@ -27,7 +28,7 @@ async fn main() { info!("k-shrink daemon started (poll={}ms)", cfg.general.poll_ms); loop { - if let Err(e) = k_shrink::process_once(&backend, &opts, &mut last_hash) { + if let Err(e) = k_shrink::process_once(&backend, &opts, &extra_mimes, &mut last_hash) { error!("error: {e}"); } tokio::time::sleep(poll).await; diff --git a/crates/bin/tests/integration_test.rs b/crates/bin/tests/integration_test.rs index 5d658d9..25a4e02 100644 --- a/crates/bin/tests/integration_test.rs +++ b/crates/bin/tests/integration_test.rs @@ -59,7 +59,7 @@ fn image_bytes_are_shrunk_and_distributed_as_webp() { let mock = MockClipboard::new(png, "image/png"); let mut last_hash = None; - process_once(&mock, &webp_opts(), &mut last_hash).unwrap(); + process_once(&mock, &webp_opts(), &[], &mut last_hash).unwrap(); let dist = mock.distributed.lock().unwrap(); assert_eq!(dist.len(), 1, "exactly one item distributed"); @@ -74,14 +74,14 @@ fn same_webp_output_not_reprocessed() { let mock = MockClipboard::new(png, "image/png"); let mut last_hash = None; - process_once(&mock, &webp_opts(), &mut last_hash).unwrap(); + process_once(&mock, &webp_opts(), &[], &mut last_hash).unwrap(); // After distributing, our subprocess serves image/webp. // Simulate next tick: clipboard returns the webp we just wrote. let webp_data = mock.distributed.lock().unwrap()[0].0.clone(); let mock2 = MockClipboard::new(webp_data, "image/webp"); - process_once(&mock2, &webp_opts(), &mut last_hash).unwrap(); + process_once(&mock2, &webp_opts(), &[], &mut last_hash).unwrap(); // hash(webp) == last_hash → skipped assert_eq!(mock2.distributed.lock().unwrap().len(), 0); @@ -94,7 +94,7 @@ fn non_image_mime_not_processed() { let mock = MockClipboard::new(b"hello world".to_vec(), "text/plain"); let mut last_hash = None; - process_once(&mock, &webp_opts(), &mut last_hash).unwrap(); + process_once(&mock, &webp_opts(), &[], &mut last_hash).unwrap(); assert_eq!(mock.distributed.lock().unwrap().len(), 0); } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b3dd517..72df0cb 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -89,6 +89,17 @@ pub struct GeneralConfig { /// Minimum: 100. Default: `500` #[serde(default = "default_poll_ms")] pub poll_ms: u64, + + /// Additional MIME types to advertise alongside the real one. + /// The compressed bytes are served under all listed types — the actual + /// format does not change. Useful when the paste target only requests a + /// specific type (e.g. a browser that asks for "image/png") but can + /// still decode the real format via content sniffing. + /// + /// Example: `extra_mimes = ["image/png", "image/jpeg"]` + /// Default: `[]` + #[serde(default)] + pub extra_mimes: Vec, } fn default_format() -> OutputFormat { @@ -109,6 +120,7 @@ impl Default for GeneralConfig { format: default_format(), quality: default_quality(), poll_ms: default_poll_ms(), + extra_mimes: Vec::new(), } } } @@ -209,6 +221,7 @@ poll_ms = 200 format: OutputFormat::Webp, quality: 200, poll_ms: 500, + extra_mimes: vec![], }, }; assert!(matches!(validate(c), Err(ConfigError::InvalidQuality(200)))); @@ -221,6 +234,7 @@ poll_ms = 200 format: OutputFormat::Webp, quality: 80, poll_ms: 50, + extra_mimes: vec![], }, }; assert!(matches!(validate(c), Err(ConfigError::InvalidPollMs(50))));