//! Фильтры «Свет» для веб-интерфейса: сборка кривых и попиксельная обработка. //! //! Модуль собирается в WebAssembly (`wasm32-unknown-unknown`) и загружается //! браузером, чтобы предпросмотр правок в просмотрщике не ждал серверного //! рендера. Математика совпадает с серверной (backend/internal/photos/edit.go). #![no_std] use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } /// Каналы: 0 — общая кривая, 1 — красный, 2 — зелёный, 3 — синий. const CHANNELS: usize = 4; static mut LUTS: [[u8; 256]; CHANNELS] = [[0; 256]; CHANNELS]; #[inline(always)] fn clamp01(v: f32) -> f32 { if v < 0.0 { 0.0 } else if v > 1.0 { 1.0 } else { v } } #[inline(always)] fn clampi(v: i32, lo: i32, hi: i32) -> i32 { if v < lo { lo } else if v > hi { hi } else { v } } /// Строит таблицу соответствия 0..255 по точкам кривой. /// /// `points` — пары (x, y) в диапазоне 0..1, отсортированные по x. /// Точки вне диапазона обрезаются, дубликаты по x пропускаются. #[no_mangle] pub extern "C" fn build_lut(points: *const f32, count: u32, channel: u32) { unsafe { let luts = &mut *core::ptr::addr_of_mut!(LUTS); let ch = (channel as usize).min(CHANNELS - 1); let lut = &mut luts[ch]; if count < 2 { for (i, slot) in lut.iter_mut().enumerate() { *slot = i as u8; } return; } let n = count as usize; let mut seg = 0usize; for v in 0..256 { let x = v as f32 / 255.0; while seg + 1 < n && x > *points.add((seg + 1) * 2) { seg += 1; } let x0 = *points.add(seg * 2); let y0 = *points.add(seg * 2 + 1); let x1 = *points.add((seg + 1) * 2); let y1 = *points.add((seg + 1) * 2 + 1); let y = if x <= x0 { y0 } else if x >= x1 { y1 } else if x1 - x0 < 1e-6 { y1 } else { let t = (x - x0) / (x1 - x0); y0 + t * (y1 - y0) }; lut[v] = (clamp01(y) * 255.0 + 0.5) as u8; } } } /// Применяет правки «Свет» к пикселям RGBA в памяти. /// /// Коэффициенты вычисляет вызывающий код: модуль не использует transcendental /// функции, поэтому работает без математической библиотеки. #[no_mangle] #[allow(clippy::too_many_arguments)] pub extern "C" fn apply( data: *mut u8, len: u32, exposure_mul: f32, contrast_k: f32, highlights: f32, shadows: f32, whites: f32, blacks: f32, ) { unsafe { let luts = &*core::ptr::addr_of!(LUTS); let master = &luts[0]; let red = &luts[1]; let green = &luts[2]; let blue = &luts[3]; let buf = core::slice::from_raw_parts_mut(data, len as usize); let mut i = 0usize; while i + 3 < buf.len() { if buf[i + 3] != 0 { buf[i] = adjust( buf[i] as f32, exposure_mul, contrast_k, highlights, shadows, whites, blacks, master, red, ); buf[i + 1] = adjust( buf[i + 1] as f32, exposure_mul, contrast_k, highlights, shadows, whites, blacks, master, green, ); buf[i + 2] = adjust( buf[i + 2] as f32, exposure_mul, contrast_k, highlights, shadows, whites, blacks, master, blue, ); } i += 4; } } } /// Обрабатывает один канал: экспозиция, контраст, света, тени, белые, чёрные /// и обе тональные кривые (общая, затем канальная). #[inline(always)] #[allow(clippy::too_many_arguments)] fn adjust( value: f32, exposure_mul: f32, contrast_k: f32, highlights: f32, shadows: f32, whites: f32, blacks: f32, master: &[u8; 256], channel: &[u8; 256], ) -> u8 { let mut v = value * exposure_mul; v = 128.0 + (v - 128.0) * contrast_k; let mut w = clamp01((v - 128.0) / 127.0); v += highlights * w * w; w = clamp01((128.0 - v) / 128.0); v += shadows * w * w; w = clamp01((v - 192.0) / 63.0); v += whites * w; w = clamp01((64.0 - v) / 64.0); v += blacks * w; let idx = clampi((v + 0.5) as i32, 0, 255) as usize; let v = master[idx] as f32; let idx = clampi((v + 0.5) as i32, 0, 255) as usize; channel[idx] }