1use std::num::NonZeroUsize;
2use std::path::PathBuf;
3use std::sync::Mutex;
4use std::time::Instant;
5
6use anyhow::Result;
7use lru::LruCache;
8
9use crate::preview::pipeline::pipeline::{GpuPreviewPipeline, Ready};
10use crate::preview::pipeline::params::PreviewParams;
11use crate::preview::pipeline::params::{color_space_to_u32, transfer_to_u32};
12use crate::terminal::{protocol, TerminalProtocol};
13
14pub const THUMBNAIL_WIDTH: u32 = 320;
15pub const THUMBNAIL_HEIGHT: u32 = 180;
16pub const CACHE_MAX_ENTRIES: usize = 1000;
17pub const CACHE_MAX_BYTES: usize = 50 * 1024 * 1024;
18
19pub fn aspect_fit(raw_w: u32, raw_h: u32) -> (u32, u32) {
22 let raw_aspect = raw_w as f64 / raw_h as f64;
23 let target_aspect = THUMBNAIL_WIDTH as f64 / THUMBNAIL_HEIGHT as f64;
24 if raw_aspect > target_aspect {
25 let h = (THUMBNAIL_WIDTH as f64 / raw_aspect) as u32;
26 (THUMBNAIL_WIDTH, h.max(1))
27 } else {
28 let w = (THUMBNAIL_HEIGHT as f64 * raw_aspect) as u32;
29 (w.max(1), THUMBNAIL_HEIGHT)
30 }
31}
32
33fn build_params(
34 width: u32,
35 height: u32,
36 raw_width: u32,
37 raw_height: u32,
38 black_level: f32,
39 white_level: f32,
40 bayer_phase: u32,
41) -> PreviewParams {
42 PreviewParams {
43 width,
44 height,
45 bayer_width: raw_width,
46 bayer_height: raw_height,
47 black_level,
48 white_level,
49 exposure: 0.0,
50 wb_r: 1.0, wb_g: 1.0, wb_b: 1.0,
51 contrast: 1.0,
52 saturation: 1.0,
53 shadows: 0.0,
54 highlights: 0.0,
55 _align0: 0.0, _align1: 0.0,
56 ccm_row0: [1.0, 0.0, 0.0, 0.0],
57 ccm_row1: [0.0, 1.0, 0.0, 0.0],
58 ccm_row2: [0.0, 0.0, 1.0, 0.0],
59 color_space: color_space_to_u32(&crate::color::ColorSpace::Rec709),
60 transfer: transfer_to_u32(&crate::color::TransferFunction::Gamma24),
61 adjust_enabled: 0,
62 bayer_phase,
63 compute_histogram: 0,
64 _pad0: 0, _pad1: 0, _pad2: 0, _pad3: 0, _pad4: 0, _pad5: 0, _pad6: 0,
65 }
66}
67
68static FALLBACK_PLACEHOLDER: &[u8] = include_bytes!("../assets/placeholder.sixel");
69
70#[derive(Clone)]
71pub struct CachedThumbnail {
72 pub sixel: Vec<u8>,
73 pub width: u32,
74 pub height: u32,
75 pub encode_time: Instant,
76}
77
78impl CachedThumbnail {
79 pub fn byte_size(&self) -> usize {
80 self.sixel.len()
81 }
82}
83
84pub struct ThumbnailCache {
85 inner: Mutex<LruCache<PathBuf, CachedThumbnail>>,
86 current_bytes: std::sync::atomic::AtomicUsize,
87 pub placeholder: Vec<u8>,
88}
89
90impl ThumbnailCache {
91 pub fn new() -> Self {
92 Self::new_with_placeholder(None)
93 }
94
95 pub fn new_with_placeholder(custom_path: Option<&std::path::Path>) -> Self {
96 let placeholder = match custom_path {
97 Some(p) => match std::fs::read(p) {
98 Ok(data) => {
99 tracing::info!("loaded custom placeholder from {}", p.display());
100 data
101 }
102 Err(e) => {
103 tracing::warn!("failed to load custom placeholder {}: {}; using bundled", p.display(), e);
104 FALLBACK_PLACEHOLDER.to_vec()
105 }
106 }
107 None => FALLBACK_PLACEHOLDER.to_vec(),
108 };
109
110 Self {
111 inner: Mutex::new(LruCache::new(NonZeroUsize::new(CACHE_MAX_ENTRIES).unwrap())),
112 current_bytes: std::sync::atomic::AtomicUsize::new(0),
113 placeholder,
114 }
115 }
116
117 pub fn get(&self, path: &PathBuf) -> Option<CachedThumbnail> {
118 let mut cache = self.inner.lock().unwrap();
119 cache.get(path).cloned()
120 }
121
122 pub fn insert(&self, path: PathBuf, thumbnail: CachedThumbnail) {
123 let size = thumbnail.byte_size();
124 let mut cache = self.inner.lock().unwrap();
125
126 let mut evict_bytes = 0usize;
128 while self.current_bytes.load(std::sync::atomic::Ordering::Relaxed) + size > CACHE_MAX_BYTES && !cache.is_empty() {
129 if let Some((_, evicted)) = cache.pop_lru() {
130 evict_bytes += evicted.byte_size();
131 }
132 }
133
134 if let Some(old) = cache.put(path, thumbnail) {
135 self.current_bytes.fetch_sub(old.byte_size(), std::sync::atomic::Ordering::Relaxed);
136 }
137
138 self.current_bytes.fetch_add(size, std::sync::atomic::Ordering::Relaxed);
139 if evict_bytes > 0 {
140 self.current_bytes.fetch_sub(evict_bytes, std::sync::atomic::Ordering::Relaxed);
141 }
142 }
143
144 pub fn clear(&self) {
145 let mut cache = self.inner.lock().unwrap();
146 cache.clear();
147 self.current_bytes.store(0, std::sync::atomic::Ordering::Relaxed);
148 }
149
150 pub fn len(&self) -> usize {
151 self.inner.lock().unwrap().len()
152 }
153}
154
155impl Default for ThumbnailCache {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161fn rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
163 let mut rgb = Vec::with_capacity(rgba.len() / 4 * 3);
164 for chunk in rgba.chunks(4) {
165 rgb.push(chunk[0]);
166 rgb.push(chunk[1]);
167 rgb.push(chunk[2]);
168 }
169 rgb
170}
171
172fn kitty_encode(rgba: &[u8], width: usize, height: usize) -> Vec<u8> {
180 use base64::Engine;
181 let rgb = rgba_to_rgb(rgba);
182 let b64 = base64::engine::general_purpose::STANDARD.encode(&rgb);
183 let header = format!("\x1b_Ga=t,i=0,r=1,f=24,s={},v={},m=0;", width, height);
184 let mut out = header.into_bytes();
185 out.extend_from_slice(b64.as_bytes());
186 out.extend_from_slice(b"\x1b\\");
187 out
188}
189
190fn encode_rgba_to_terminal(rgba: &[u8], width: usize, height: usize) -> Result<Vec<u8>> {
193 match protocol() {
194 TerminalProtocol::Kitty => Ok(kitty_encode(rgba, width, height)),
195 TerminalProtocol::Sixel => {
196 let s = icy_sixel::sixel_encode(rgba, width, height, &icy_sixel::EncodeOptions::default())
197 .map_err(|e| anyhow::anyhow!("sixel encode: {}", e))?;
198 Ok(s.into_bytes())
199 }
200 TerminalProtocol::TextFallback => {
201 Err(anyhow::anyhow!("Terminal does not support image display (sixel/kitty)"))
202 }
203 }
204}
205
206pub fn compute_thumbnail(
207 pipeline: &mut GpuPreviewPipeline<Ready>,
208 bayer: &[u16],
209 raw_width: u32,
210 raw_height: u32,
211 black_level: f32,
212 white_level: f32,
213 bayer_phase: u32,
214) -> Result<CachedThumbnail> {
215 if matches!(protocol(), TerminalProtocol::TextFallback) {
216 return Err(anyhow::anyhow!("Terminal does not support image display (sixel/kitty)"));
217 }
218 let (width, height) = aspect_fit(raw_width, raw_height);
219
220 let params = build_params(width, height, raw_width, raw_height, black_level, white_level, bayer_phase);
221
222 let (rgba, w, h) = pipeline.process_and_readback(bayer, ¶ms)?;
223
224 let encoded = encode_rgba_to_terminal(&rgba, w as usize, h as usize)?;
225
226 Ok(CachedThumbnail {
227 sixel: encoded,
228 width: w,
229 height: h,
230 encode_time: Instant::now(),
231 })
232}
233
234fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
239 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
240 t * t * (3.0 - 2.0 * t)
241}
242
243fn load_bayer(bayer: &[u16], raw_w: u32, x: i32, y: i32) -> f32 {
244 let cx = x.clamp(0, raw_w as i32 - 1);
245 let cy = y.clamp(0, (bayer.len() as u32 / raw_w).saturating_sub(1) as i32);
246 bayer[(cy as u32 * raw_w + cx as u32) as usize] as f32
247}
248
249fn bayer_color(x: i32, y: i32, phase: u32) -> i32 {
250 let even_row = (y & 1) == 0;
251 let even_col = (x & 1) == 0;
252 match phase {
253 0 => { if even_row { if even_col { 0 } else { 1 } }
255 else { if even_col { 1 } else { 2 } }
256 }
257 1 => { if even_row { if even_col { 1 } else { 0 } }
259 else { if even_col { 2 } else { 1 } }
260 }
261 2 => { if even_row { if even_col { 1 } else { 2 } }
263 else { if even_col { 0 } else { 1 } }
264 }
265 _ => { if even_row { if even_col { 2 } else { 1 } }
267 else { if even_col { 1 } else { 0 } }
268 }
269 }
270}
271
272fn demosaic_bilinear(bayer: &[u16], raw_w: u32, _raw_h: u32, phase: u32, x: i32, y: i32) -> [f32; 3] {
273 let c = bayer_color(x, y, phase);
274 let center = load_bayer(bayer, raw_w, x, y);
275 let n = load_bayer(bayer, raw_w, x, y - 1);
276 let s = load_bayer(bayer, raw_w, x, y + 1);
277 let w = load_bayer(bayer, raw_w, x - 1, y);
278 let e = load_bayer(bayer, raw_w, x + 1, y);
279 let nw = load_bayer(bayer, raw_w, x - 1, y - 1);
280 let ne = load_bayer(bayer, raw_w, x + 1, y - 1);
281 let sw = load_bayer(bayer, raw_w, x - 1, y + 1);
282 let se = load_bayer(bayer, raw_w, x + 1, y + 1);
283
284 let (r, g, b) = if c == 0 { (center, (n + s + w + e) * 0.25, (nw + ne + sw + se) * 0.25)
286 } else if c == 2 { ((nw + ne + sw + se) * 0.25, (n + s + w + e) * 0.25, center)
288 } else { let horiz_color = bayer_color(x - 1, y, phase);
290 let _vert_color = bayer_color(x, y - 1, phase);
291 if horiz_color == 0 { ((w + e) * 0.5, center, (n + s) * 0.5)
293 } else { ((n + s) * 0.5, center, (w + e) * 0.5)
295 }
296 };
297 [r, g, b]
298}
299
300fn apply_oetf(r: f32, g: f32, b: f32, tf: u32) -> [f32; 3] {
301 let oetf_ch = |x: f32| -> f32 {
302 match tf {
303 0 => x,
304 14 => x.max(0.0).powf(1.0 / 2.4),
305 1 => {
306 if x < 0.018 { 4.5 * x }
307 else { 1.099 * x.max(0.0).powf(0.45) - 0.099 }
308 }
309 2 => {
310 if x >= 0.01 { 0.432699 * (10.0 * x + 1.0).log10() + 0.037584 }
311 else { (x * 261.5 + 10.23) / 1023.0 }
312 }
313 3 => {
314 if x < 0.01 { 5.6 * x + 0.125 }
315 else { 0.241514 * (x + 0.00873).log10() + 0.598206 }
316 }
317 4 => {
318 if x > 0.010591 { 0.247190 * (5.555556 * x + 0.052272).log10() + 0.385537 }
319 else { 5.367655 * x + 0.092809 }
320 }
321 5 => {
322 let a: f32 = (262144.0 - 16.0) / 117.45;
323 let b_rev: f32 = (1023.0 - 95.0) / 1023.0;
324 let c_rev: f32 = 95.0 / 1023.0;
325 let s_rev = (7.0 * 0.6931471805599453 * f32::exp2(7.0 - 14.0 * c_rev / b_rev)) / (a * b_rev);
326 let t_rev = (f32::exp2(14.0 * (-c_rev / b_rev) + 6.0) - 64.0) / a;
327 if x >= t_rev {
328 ((a * x + 64.0).log2() - 6.0) / 14.0 * b_rev + c_rev
329 } else {
330 (x - t_rev) / s_rev
331 }
332 }
333 6 => {
334 let neg_graft = (0.097465473 - 0.12512219) / 1.9754798;
335 let pos_graft = (0.15277891 - 0.12512219) / 1.9754798;
336 if x < neg_graft {
337 -0.36726845 * ((-x * 14.98325 + 1.0).max(1e-10)).log10() + 0.12783901
338 } else if x <= pos_graft {
339 1.9754798 * x + 0.12512219
340 } else {
341 0.36726845 * (x * 14.98325 + 1.0).log10() + 0.12240537
342 }
343 }
344 7 => {
345 if x >= 0.000889 { 0.245281 * (5.555556 * x + 0.064829).log10() + 0.384316 }
346 else { 8.799461 * x + 0.092864 }
347 }
348 8 | 9 => {
349 if x < -0.05641088 { 0.0 }
350 else if x < 0.01 { 47.28711236 * (x + 0.05641088) * (x + 0.05641088) }
351 else { 0.08550479 * (x + 0.00964052).log2() + 0.69336945 }
352 }
353 10 => {
354 if x > 0.0078125 { (x.log2() + 9.72) / 17.52 }
355 else { 10.5402377416545 * x + 0.0729055341958355 }
356 }
357 11 => {
358 let m1 = 0.1593017578125;
359 let m2 = 78.84375;
360 let c1 = 0.8359375;
361 let c2 = 18.8515625;
362 let c3 = 18.6875;
363 let x_m1 = x.max(0.0).powf(m1);
364 (c1 + c2 * x_m1).max(0.0) / (1.0 + c3 * x_m1).max(1e-10).powf(m2)
365 }
366 12 => {
367 if x < 1.0 / 12.0 { (3.0 * x.max(0.0)).sqrt() }
368 else { 0.17883277 * (12.0 * x - 0.28466892).max(1e-10).ln() + 0.55991073 }
369 }
370 13 => {
371 if x <= 0.00262409 { x * 10.44426855 }
372 else { 0.07329248 * ((x + 0.0075).log2() + 7.0) }
373 }
374 _ => x,
375 }
376 };
377 [oetf_ch(r), oetf_ch(g), oetf_ch(b)]
378}
379
380fn inverse_oetf(r: f32, g: f32, b: f32, tf: u32) -> [f32; 3] {
381 let inv_ch = |y: f32| -> f32 {
382 match tf {
383 0 => y,
384 14 => y.max(0.0).powf(2.4),
385 1 => {
386 if y < 0.081 { y / 4.5 }
387 else { ((y + 0.099) / 1.099).powf(1.0 / 0.45) }
388 }
389 2 => {
390 let knee_val = (0.01 * 261.5 + 10.23) / 1023.0;
391 if y >= knee_val { ((10.0f32).powf((y - 0.037584) / 0.432699) - 1.0) / 10.0 }
392 else { (y * 1023.0 - 10.23) / 261.5 }
393 }
394 3 => {
395 if y < 0.181 { (y - 0.125) / 5.6 }
396 else { (10.0f32).powf((y - 0.598206) / 0.241514) - 0.00873 }
397 }
398 4 => {
399 let knee_val = 5.367655 * 0.010591 + 0.092809;
400 if y >= knee_val { ((10.0f32).powf((y - 0.385537) / 0.247190) - 0.052272) / 5.555556 }
401 else { (y - 0.092809) / 5.367655 }
402 }
403 5 => {
404 let a: f32 = (262144.0 - 16.0) / 117.45;
405 let b_rev: f32 = (1023.0 - 95.0) / 1023.0;
406 let c_rev: f32 = 95.0 / 1023.0;
407 let s_rev = (7.0 * 0.6931471805599453 * f32::exp2(7.0 - 14.0 * c_rev / b_rev)) / (a * b_rev);
408 let t_rev = (f32::exp2(14.0 * (-c_rev / b_rev) + 6.0) - 64.0) / a;
409 if y >= 0.0 { (f32::exp2(14.0 * ((y - c_rev) / b_rev) + 6.0) - 64.0) / a }
410 else { y * s_rev + t_rev }
411 }
412 6 => {
413 let neg_graft = (0.097465473 - 0.12512219) / 1.9754798;
414 let pos_graft = (0.15277891 - 0.12512219) / 1.9754798;
415 let knee_lo = 0.12512219 + neg_graft * 1.9754798;
416 let knee_hi = 0.12512219 + pos_graft * 1.9754798;
417 if y < knee_lo {
418 ((10.0f32).powf(-(y - 0.12783901) / 0.36726845) - 1.0) / (-14.98325)
419 } else if y <= knee_hi {
420 (y - 0.12512219) / 1.9754798
421 } else {
422 ((10.0f32).powf((y - 0.12240537) / 0.36726845) - 1.0) / 14.98325
423 }
424 }
425 7 => {
426 let knee_val = 8.799461 * 0.000889 + 0.092864;
427 if y >= knee_val { ((10.0f32).powf((y - 0.384316) / 0.245281) - 0.064829) / 5.555556 }
428 else { (y - 0.092864) / 8.799461 }
429 }
430 8 | 9 => {
431 if y <= 0.0 { -0.05641088 }
432 else {
433 let knee_val = 47.28711236 * (0.01 + 0.05641088) * (0.01 + 0.05641088);
434 if y < knee_val { (y / 47.28711236).sqrt() - 0.05641088 }
435 else { (2.0f32).powf((y - 0.69336945) / 0.08550479) - 0.00964052 }
436 }
437 }
438 10 => {
439 let cutoff = 10.5402377416545 * 0.0078125 + 0.0729055341958355;
440 if y > cutoff { (2.0f32).powf(y * 17.52 - 9.72) }
441 else { (y - 0.0729055341958355) / 10.5402377416545 }
442 }
443 11 => {
444 let m1 = 0.1593017578125;
445 let m2 = 78.84375;
446 let c1 = 0.8359375;
447 let c2 = 18.8515625;
448 let c3 = 18.6875;
449 let v = y.max(0.0);
450 let v_m2 = v.powf(1.0 / m2);
451 let num = (v_m2 - c1).max(0.0);
452 let den = c2 - c3 * v_m2;
453 if den > 0.0 { (num / den).powf(1.0 / m1) }
454 else { 0.0 }
455 }
456 12 => {
457 let knee_out: f32 = f32::sqrt(3.0 / 12.0);
458 if y <= knee_out { y * y / 3.0 }
459 else { ((y - 0.55991073) / 0.17883277).exp() + 0.28466892 / 12.0 }
460 }
461 13 => {
462 let cut_out = 0.00262409 * 10.44426855;
463 if y <= cut_out { y / 10.44426855 }
464 else { (2.0f32).powf(y / 0.07329248 - 7.0) - 0.0075 }
465 }
466 _ => y,
467 }
468 };
469 [inv_ch(r), inv_ch(g), inv_ch(b)]
470}
471
472fn srgb_oetf(r: f32, g: f32, b: f32) -> [f32; 3] {
473 let srgb_ch = |x: f32| -> f32 {
474 if x <= 0.0031308 { x * 12.92 }
475 else { 1.055 * x.max(0.0).powf(1.0 / 2.4) - 0.055 }
476 };
477 [srgb_ch(r), srgb_ch(g), srgb_ch(b)]
478}
479
480fn apply_tone_curve(r: f32, g: f32, b: f32, shadows: f32, highlights: f32) -> [f32; 3] {
481 let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
482 let shadow_weight = 1.0 - smoothstep(0.0, 0.35, luma);
483 let mut rt = r + shadows * shadow_weight;
484 let mut gt = g + shadows * shadow_weight;
485 let mut bt = b + shadows * shadow_weight;
486 let hi_weight = smoothstep(0.5, 1.0, luma);
487 rt = rt + highlights * hi_weight * rt;
488 gt = gt + highlights * hi_weight * gt;
489 bt = bt + highlights * hi_weight * bt;
490 [rt.max(0.0), gt.max(0.0), bt.max(0.0)]
491}
492
493fn xyz_to_rec709(x: f32, y: f32, z: f32) -> [f32; 3] {
494 [
495 3.2404542 * x + -1.9692660 * y + 0.0556434 * z,
496 -1.5371385 * x + 1.8760108 * y + -0.2040259 * z,
497 -0.4985314 * x + 0.0415560 * y + 1.0572252 * z,
498 ]
499}
500
501fn working_to_xyz(r: f32, g: f32, b: f32, cs: u32) -> [f32; 3] {
502 match cs {
503 0 => [ 0.6954522414 * r + 0.1406786965 * g + 0.1638690622 * b,
505 0.0447945634 * r + 0.8596711185 * g + 0.0955343182 * b,
506 -0.0055258826 * r + 0.0040252104 * g + 1.0015006723 * b,
507 ],
508 1 => [ 1.99650669 * r + -0.04380294 * g + 0.04729625 * b,
510 0.50573456 * r + 0.86522867 * g + -0.37096323 * b,
511 0.00612684 * r + -0.00089651 * g + 0.99476967 * b,
512 ],
513 2 => [ 0.688161 * r + 0.150181 * g + 0.161658 * b,
515 0.047434 * r + 0.807529 * g + 0.145037 * b,
516 -0.002103 * r + -0.004533 * g + 1.006636 * b,
517 ],
518 3 => [ 0.732690 * r + 0.143327 * g + 0.123983 * b,
520 0.044200 * r + 0.878486 * g + 0.077314 * b,
521 -0.001988 * r + -0.003142 * g + 1.005130 * b,
522 ],
523 5 => [ 0.8000 * r + 0.3130 * g + -0.1130 * b,
525 0.1682 * r + 0.9877 * g + -0.1559 * b,
526 0.0790 * r + -0.1155 * g + 1.0365 * b,
527 ],
528 6 | 7 => [ 0.4865709 * r + 0.2656677 * g + 0.1982242 * b,
530 0.2289746 * r + 0.6917385 * g + 0.0792869 * b,
531 0.0 * r + 0.0451136 * g + 1.0439444 * b,
532 ],
533 11 => [ 0.6369580 * r + 0.1446169 * g + 0.1688810 * b,
535 0.2627002 * r + 0.6779981 * g + 0.0593017 * b,
536 0.0 * r + 0.0280727 * g + 1.0609052 * b,
537 ],
538 _ => [r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)],
539 }
540}
541
542fn gamut_clip_to_srgb(r: f32, g: f32, b: f32, cs: u32) -> [f32; 3] {
543 if cs == 12 || cs == 15 { [r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)]
545 } else {
546 let xyz = working_to_xyz(r, g, b, cs);
547 let srgb = xyz_to_rec709(xyz[0], xyz[1], xyz[2]);
548 [srgb[0].clamp(0.0, 1.0), srgb[1].clamp(0.0, 1.0), srgb[2].clamp(0.0, 1.0)]
549 }
550}
551
552pub fn cpu_thumbnail(
557 bayer: &[u16],
558 params: &PreviewParams,
559) -> Result<(Vec<u8>, u32, u32)> {
560 if matches!(protocol(), TerminalProtocol::TextFallback) {
561 return Err(anyhow::anyhow!("Terminal does not support image display (sixel/kitty)"));
562 }
563 let out_w = params.width;
564 let out_h = params.height;
565 let raw_w = params.bayer_width;
566 let raw_h = params.bayer_height;
567 let black = params.black_level;
568 let range = (params.white_level - black).max(0.001);
569 let exp_gain = (2.0f32).powf(params.exposure);
570 let adjust = params.adjust_enabled != 0;
571 let phase = params.bayer_phase;
572
573 let mut rgba = vec![0u8; (out_w * out_h * 4) as usize];
574
575 for y in 0..out_h {
578 for x in 0..out_w {
579 let src_x = (x * raw_w) / out_w;
580 let src_y = (y * raw_h) / out_h;
581
582 let mut rgb = demosaic_bilinear(bayer, raw_w, raw_h, phase, src_x as i32, src_y as i32);
584
585 rgb[0] = (rgb[0] - black) / range;
587 rgb[1] = (rgb[1] - black) / range;
588 rgb[2] = (rgb[2] - black) / range;
589
590 rgb[0] *= exp_gain;
592 rgb[1] *= exp_gain;
593 rgb[2] *= exp_gain;
594
595 rgb[0] *= params.wb_r;
597 rgb[1] *= params.wb_g;
598 rgb[2] *= params.wb_b;
599
600 let (cr, cg, cb) = (rgb[0], rgb[1], rgb[2]);
605 rgb[0] = params.ccm_row0[0] * cr + params.ccm_row1[0] * cg + params.ccm_row2[0] * cb;
606 rgb[1] = params.ccm_row0[1] * cr + params.ccm_row1[1] * cg + params.ccm_row2[1] * cb;
607 rgb[2] = params.ccm_row0[2] * cr + params.ccm_row1[2] * cg + params.ccm_row2[2] * cb;
608
609 if adjust {
611 rgb = apply_tone_curve(rgb[0], rgb[1], rgb[2], params.shadows, params.highlights);
612 rgb[0] = ((rgb[0] - 0.18) * params.contrast + 0.18).max(0.0);
614 rgb[1] = ((rgb[1] - 0.18) * params.contrast + 0.18).max(0.0);
615 rgb[2] = ((rgb[2] - 0.18) * params.contrast + 0.18).max(0.0);
616 let luma = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
618 rgb[0] = luma + (rgb[0] - luma) * params.saturation;
619 rgb[1] = luma + (rgb[1] - luma) * params.saturation;
620 rgb[2] = luma + (rgb[2] - luma) * params.saturation;
621 }
622
623 let encoded = apply_oetf(rgb[0], rgb[1], rgb[2], params.transfer);
625
626 let linear_for_display = inverse_oetf(encoded[0], encoded[1], encoded[2], params.transfer);
628
629 let srgb_linear = gamut_clip_to_srgb(linear_for_display[0], linear_for_display[1], linear_for_display[2], params.color_space);
631
632 let display = srgb_oetf(srgb_linear[0], srgb_linear[1], srgb_linear[2]);
634
635 let idx = ((y * out_w + x) * 4) as usize;
637 rgba[idx] = (display[0].clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
638 rgba[idx + 1] = (display[1].clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
639 rgba[idx + 2] = (display[2].clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
640 rgba[idx + 3] = 255;
641 }
642 }
643
644 let encoded = encode_rgba_to_terminal(&rgba, out_w as usize, out_h as usize)?;
646
647 Ok((encoded, out_w, out_h))
648}