Skip to main content

flashraw_codecs/
dng_opcodes.rs

1//! DNG opcode lists (OpcodeList1/2/3, DNG spec 1.3+ chapter 7).
2//!
3//! Opcode lists carry processing steps the camera maker specifies: bad-pixel repair,
4//! lens-shading gain maps (ubiquitous in smartphone DNGs), linearisation tables and
5//! polynomials, per-row/column offsets and scales, and geometric corrections.
6//!
7//! FlashRaw parses all lists and applies the pixel-value opcodes to f32 planes (the
8//! high-level API does so by default, each list in its own domain; lists of tables and
9//! bad-pixel repairs on stored integers run on the u16 samples directly, with the same
10//! result); geometric opcodes
11//! (WarpRectilinear, WarpFisheye, TrimBounds) are parsed and reported but not applied.
12
13use flashraw_core::bytes::Cursor;
14use flashraw_core::{ByteOrder, Error, Result, par};
15
16/// Area/plane selector shared by most pixel opcodes.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Region {
19    pub top: u32,
20    pub left: u32,
21    pub bottom: u32,
22    pub right: u32,
23    pub plane: u32,
24    pub planes: u32,
25    pub row_pitch: u32,
26    pub col_pitch: u32,
27}
28
29/// A parsed opcode.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Opcode {
32    /// Lens distortion / lateral CA model (DNG 1.3 ยง6.2): per plane radial `kr[0..4]`
33    /// and tangential `kt[0..2]` coefficients around the optical centre `(cx, cy)`
34    /// (relative image coordinates). Reported, never applied.
35    WarpRectilinear {
36        planes: Vec<([f64; 4], [f64; 2])>,
37        cx: f64,
38        cy: f64,
39    },
40    WarpFisheye,
41    /// Radial gain `1 + k0 r^2 + k1 r^4 + ... + k4 r^10` around `(cx, cy)` (relative
42    /// image coordinates), `r` normalised to 1 at the farthest image corner.
43    FixVignetteRadial {
44        k: [f64; 5],
45        cx: f64,
46        cy: f64,
47    },
48    FixBadPixelsConstant {
49        constant: u32,
50        bayer_phase: u32,
51    },
52    FixBadPixelsList {
53        bayer_phase: u32,
54        points: Vec<(u32, u32)>,
55        rects: Vec<(u32, u32, u32, u32)>,
56    },
57    TrimBounds {
58        top: u32,
59        left: u32,
60        bottom: u32,
61        right: u32,
62    },
63    MapTable {
64        region: Region,
65        table: Vec<u16>,
66    },
67    MapPolynomial {
68        region: Region,
69        coeffs: Vec<f64>,
70    },
71    GainMap {
72        region: Region,
73        points_v: u32,
74        points_h: u32,
75        spacing_v: f64,
76        spacing_h: f64,
77        origin_v: f64,
78        origin_h: f64,
79        map_planes: u32,
80        gains: Vec<f32>,
81    },
82    DeltaPerRow {
83        region: Region,
84        deltas: Vec<f32>,
85    },
86    DeltaPerColumn {
87        region: Region,
88        deltas: Vec<f32>,
89    },
90    ScalePerRow {
91        region: Region,
92        scales: Vec<f32>,
93    },
94    ScalePerColumn {
95        region: Region,
96        scales: Vec<f32>,
97    },
98    /// Unknown or unsupported opcode (id, flags); `optional` ones may be skipped.
99    Other {
100        id: u32,
101        optional: bool,
102    },
103}
104
105impl Opcode {
106    pub fn name(&self) -> &'static str {
107        match self {
108            Opcode::WarpRectilinear { .. } => "WarpRectilinear",
109            Opcode::WarpFisheye => "WarpFisheye",
110            Opcode::FixVignetteRadial { .. } => "FixVignetteRadial",
111            Opcode::FixBadPixelsConstant { .. } => "FixBadPixelsConstant",
112            Opcode::FixBadPixelsList { .. } => "FixBadPixelsList",
113            Opcode::TrimBounds { .. } => "TrimBounds",
114            Opcode::MapTable { .. } => "MapTable",
115            Opcode::MapPolynomial { .. } => "MapPolynomial",
116            Opcode::GainMap { .. } => "GainMap",
117            Opcode::DeltaPerRow { .. } => "DeltaPerRow",
118            Opcode::DeltaPerColumn { .. } => "DeltaPerColumn",
119            Opcode::ScalePerRow { .. } => "ScalePerRow",
120            Opcode::ScalePerColumn { .. } => "ScalePerColumn",
121            Opcode::Other { .. } => "Other",
122        }
123    }
124    /// Whether [`apply`] computes each sample of this opcode from that sample and its
125    /// coordinates alone (no neighbours): such opcodes give the same result applied to
126    /// any window of rows (e.g. band by band) as applied to the whole image. True for
127    /// everything except the bad-pixel repairs.
128    pub fn is_row_local(&self) -> bool {
129        !matches!(self, Opcode::FixBadPixelsConstant { .. } | Opcode::FixBadPixelsList { .. })
130    }
131    /// Whether [`apply`] implements this opcode.
132    pub fn is_pixel_opcode(&self) -> bool {
133        !matches!(
134            self,
135            Opcode::WarpRectilinear { .. } | Opcode::WarpFisheye | Opcode::TrimBounds { .. } | Opcode::Other { .. }
136        )
137    }
138}
139
140const MAX_ELEMENTS: usize = 1 << 24;
141
142fn region(c: &mut Cursor) -> Result<Region> {
143    Ok(Region {
144        top: c.u32()?,
145        left: c.u32()?,
146        bottom: c.u32()?,
147        right: c.u32()?,
148        plane: c.u32()?,
149        planes: c.u32()?,
150        row_pitch: c.u32()?.max(1),
151        col_pitch: c.u32()?.max(1),
152    })
153}
154
155fn count(c: &mut Cursor, elem: usize) -> Result<usize> {
156    let n = c.u32()? as usize;
157    if n > MAX_ELEMENTS || n.saturating_mul(elem) > c.remaining() {
158        return Err(Error::corrupt(format!("invalid DNG opcode with {n} elements")));
159    }
160    Ok(n)
161}
162
163fn warp_rectilinear(b: &mut Cursor) -> Result<Opcode> {
164    let n = b.u32()? as usize;
165    if n == 0 || n > 4 || (n * 6 + 2) * 8 > b.remaining() {
166        return Err(Error::corrupt("invalid WarpRectilinear plane count"));
167    }
168    let mut f = || b.u64().map(f64::from_bits);
169    let mut planes = Vec::with_capacity(n);
170    for _ in 0..n {
171        planes.push(([f()?, f()?, f()?, f()?], [f()?, f()?]));
172    }
173    let (cx, cy) = (f()?, f()?);
174    Ok(Opcode::WarpRectilinear { planes, cx, cy })
175}
176
177/// One opcode of a list with its header fields.
178#[derive(Debug, Clone, PartialEq)]
179pub struct OpcodeEntry {
180    /// DNG opcode id (1 = WarpRectilinear ... 13 = ScalePerColumn).
181    pub id: u32,
182    /// Flag bit 0: the opcode may be skipped by readers that do not understand it.
183    pub optional: bool,
184    /// Flag bit 1: may be skipped when rendering previews.
185    pub preview_only: bool,
186    pub op: Opcode,
187}
188
189/// Parse an opcode list (always big-endian, per the DNG spec).
190pub fn parse(data: &[u8]) -> Result<Vec<Opcode>> {
191    Ok(parse_entries(data)?.into_iter().map(|e| e.op).collect())
192}
193
194/// [`parse`] keeping each opcode's id and flags.
195pub fn parse_entries(data: &[u8]) -> Result<Vec<OpcodeEntry>> {
196    let mut c = Cursor::new(data, ByteOrder::Big);
197    let n = c.u32()? as usize;
198    if n > 4096 {
199        return Err(Error::corrupt(format!("invalid DNG opcode list with {n} opcodes")));
200    }
201    let mut out = Vec::with_capacity(n);
202    for _ in 0..n {
203        let id = c.u32()?;
204        let _version = c.u32()?;
205        let flags = c.u32()?;
206        let len = c.u32()? as usize;
207        let body = c.bytes(len)?;
208        let mut b = Cursor::new(body, ByteOrder::Big);
209        let op = match id {
210            // Never applied, so a malformed body must not cost the rest of the list: report
211            // it without parameters instead.
212            1 => warp_rectilinear(&mut b).unwrap_or(Opcode::WarpRectilinear { planes: Vec::new(), cx: 0.5, cy: 0.5 }),
213            2 => Opcode::WarpFisheye,
214            3 => {
215                let mut v = [0f64; 7];
216                for x in &mut v {
217                    *x = f64::from_bits(b.u64()?);
218                }
219                Opcode::FixVignetteRadial { k: [v[0], v[1], v[2], v[3], v[4]], cx: v[5], cy: v[6] }
220            }
221            4 => Opcode::FixBadPixelsConstant { constant: b.u32()?, bayer_phase: b.u32()? },
222            5 => {
223                let bayer_phase = b.u32()?;
224                let np = b.u32()? as usize;
225                let nr = b.u32()? as usize;
226                if np.saturating_mul(8).saturating_add(nr.saturating_mul(16)) > b.remaining() {
227                    return Err(Error::corrupt("FixBadPixelsList counts exceed opcode size"));
228                }
229                let points = (0..np).map(|_| Ok((b.u32()?, b.u32()?))).collect::<Result<Vec<_>>>()?;
230                let rects =
231                    (0..nr).map(|_| Ok((b.u32()?, b.u32()?, b.u32()?, b.u32()?))).collect::<Result<Vec<_>>>()?;
232                Opcode::FixBadPixelsList { bayer_phase, points, rects }
233            }
234            6 => Opcode::TrimBounds { top: b.u32()?, left: b.u32()?, bottom: b.u32()?, right: b.u32()? },
235            7 => {
236                let region = region(&mut b)?;
237                let n = count(&mut b, 2)?;
238                let table = (0..n).map(|_| b.u16()).collect::<Result<Vec<_>>>()?;
239                Opcode::MapTable { region, table }
240            }
241            8 => {
242                let region = region(&mut b)?;
243                let n = b.u32()? as usize;
244                if n > 8 || (n + 1) * 8 > b.remaining() {
245                    return Err(Error::corrupt("invalid MapPolynomial degree"));
246                }
247                let coeffs = (0..=n).map(|_| Ok(f64::from_bits(b.u64()?))).collect::<Result<Vec<_>>>()?;
248                Opcode::MapPolynomial { region, coeffs }
249            }
250            9 => {
251                let region = region(&mut b)?;
252                let points_v = b.u32()?;
253                let points_h = b.u32()?;
254                let spacing_v = f64::from_bits(b.u64()?);
255                let spacing_h = f64::from_bits(b.u64()?);
256                let origin_v = f64::from_bits(b.u64()?);
257                let origin_h = f64::from_bits(b.u64()?);
258                let map_planes = b.u32()?;
259                let total =
260                    (points_v as usize).saturating_mul(points_h as usize).saturating_mul(map_planes.max(1) as usize);
261                if total == 0 || total > MAX_ELEMENTS || total * 4 > b.remaining() {
262                    return Err(Error::corrupt("invalid GainMap dimensions"));
263                }
264                let gains = (0..total).map(|_| Ok(f32::from_bits(b.u32()?))).collect::<Result<Vec<_>>>()?;
265                Opcode::GainMap {
266                    region,
267                    points_v,
268                    points_h,
269                    spacing_v,
270                    spacing_h,
271                    origin_v,
272                    origin_h,
273                    map_planes: map_planes.max(1),
274                    gains,
275                }
276            }
277            10..=13 => {
278                let region = region(&mut b)?;
279                let n = count(&mut b, 4)?;
280                let v = (0..n).map(|_| Ok(f32::from_bits(b.u32()?))).collect::<Result<Vec<_>>>()?;
281                match id {
282                    10 => Opcode::DeltaPerRow { region, deltas: v },
283                    11 => Opcode::DeltaPerColumn { region, deltas: v },
284                    12 => Opcode::ScalePerRow { region, scales: v },
285                    _ => Opcode::ScalePerColumn { region, scales: v },
286                }
287            }
288            _ => Opcode::Other { id, optional: flags & 1 != 0 },
289        };
290        out.push(OpcodeEntry { id, optional: flags & 1 != 0, preview_only: flags & 2 != 0, op });
291    }
292    Ok(out)
293}
294
295/// Sample types the opcodes run on: f32 for every opcode, u16 (stored integer values)
296/// for the lists [`apply_u16`] accepts.
297pub trait Sample: Copy + Send + Sync + 'static {
298    fn to_f32(self) -> f32;
299    /// A repaired value (mean of neighbours) as stored.
300    fn from_mean(v: f32) -> Self;
301}
302
303impl Sample for f32 {
304    fn to_f32(self) -> f32 {
305        self
306    }
307    fn from_mean(v: f32) -> f32 {
308        v
309    }
310}
311
312impl Sample for u16 {
313    fn to_f32(self) -> f32 {
314        self as f32
315    }
316    /// Rounded as stored values are after an f32 pass.
317    fn from_mean(v: f32) -> u16 {
318        v.round().clamp(0.0, 65535.0) as u16
319    }
320}
321
322/// A mutable image the opcodes operate on: a `width x height` window (rows
323/// `stride` pixels apart, `planes` interleaved samples per pixel) placed at
324/// `(x0, y0)` in the coordinate system of the whole image the list describes, which is
325/// `full_width x full_height` (GainMap positions are relative to it).
326pub struct PlaneImage<'a, T = f32> {
327    pub data: &'a mut [T],
328    pub width: usize,
329    pub height: usize,
330    pub planes: usize,
331    pub stride: usize,
332    pub x0: usize,
333    pub y0: usize,
334    pub full_width: usize,
335    pub full_height: usize,
336}
337
338impl<'a, T: Sample> PlaneImage<'a, T> {
339    /// A whole, tightly packed image.
340    pub fn new(data: &'a mut [T], width: usize, height: usize, planes: usize) -> Self {
341        PlaneImage { data, width, height, planes, stride: width, x0: 0, y0: 0, full_width: width, full_height: height }
342    }
343
344    /// Run `f(y, row)` for every row `y` (whole-image coordinates) of the window that
345    /// `r` selects; `row` is the window row (first pixel at `x0`).
346    fn rows(&mut self, r: &Region, parallel: bool, f: impl Fn(usize, &mut [T]) + Sync + Send) {
347        let (w, h, p, stride) = (self.width, self.height, self.planes, self.stride);
348        let y0 = self.y0;
349        let top = r.top as usize;
350        let bottom = (r.bottom as usize).min(y0 + h);
351        if bottom <= top.max(y0) || w == 0 || stride < w {
352            return;
353        }
354        let rl = stride * p;
355        let n = self.data.len().min(h * rl);
356        par::for_each_row_chunk(&mut self.data[..n], rl, 16, parallel, |first, chunk| {
357            for (i, row) in chunk.chunks_mut(rl).enumerate() {
358                let y = y0 + first + i;
359                if y >= top && y < bottom && (y - top) % r.row_pitch as usize == 0 {
360                    let len = (w * p).min(row.len());
361                    f(y, &mut row[..len]);
362                }
363            }
364        });
365    }
366
367    /// Columns `x` (whole-image coordinates) of the window that `r` selects, with the
368    /// sample offset of plane `r.plane` inside a window row.
369    fn columns(&self, r: &Region) -> impl Iterator<Item = (usize, usize)> + Clone + '_ {
370        let left = r.left as usize;
371        let start = left.max(self.x0);
372        // First selected column at or after the window start.
373        let start = left + (start - left).div_ceil(r.col_pitch as usize) * r.col_pitch as usize;
374        let right = (r.right as usize).min(self.x0 + self.width);
375        let (x0, p, plane) = (self.x0, self.planes, r.plane as usize);
376        (start..right.max(start)).step_by(r.col_pitch as usize).map(move |x| (x, (x - x0) * p + plane))
377    }
378
379    /// Number of selected planes that exist.
380    fn plane_count(&self, r: &Region) -> usize {
381        (r.plane as usize).saturating_add(r.planes as usize).min(self.planes).saturating_sub(r.plane as usize)
382    }
383
384    /// Apply `f(x, y, plane_index_within_region, sample)` to every selected sample.
385    fn for_region(&mut self, r: &Region, parallel: bool, f: impl Fn(usize, usize, usize, &mut T) + Sync + Send) {
386        let cols: Vec<(usize, usize)> = self.columns(r).collect();
387        let np = self.plane_count(r);
388        if cols.is_empty() || np == 0 {
389            return;
390        }
391        self.rows(r, parallel, |y, row| {
392            for &(x, off) in &cols {
393                for k in 0..np {
394                    if let Some(v) = row.get_mut(off + k) {
395                        f(x, y, k, v);
396                    }
397                }
398            }
399        });
400    }
401}
402
403impl PlaneImage<'_, f32> {
404    /// Run `f(y, row)` for every row of the window (`y` in whole-image coordinates).
405    fn each_row(&mut self, parallel: bool, f: &RowFn<'_>) {
406        let (w, h, p, stride, y0) = (self.width, self.height, self.planes, self.stride, self.y0);
407        if w == 0 || stride < w {
408            return;
409        }
410        let rl = stride * p;
411        let n = self.data.len().min(h * rl);
412        par::for_each_row_chunk(&mut self.data[..n], rl, 16, parallel, |first, chunk| {
413            for (i, row) in chunk.chunks_mut(rl).enumerate() {
414                let len = (w * p).min(row.len());
415                f(y0 + first + i, &mut row[..len]);
416            }
417        });
418    }
419}
420
421/// A row-local opcode prepared for a window: `f(y, row)` processes window row `y`
422/// (whole-image coordinates; `row` holds the window's `width * planes` samples).
423type RowFn<'a> = Box<dyn Fn(usize, &mut [f32]) + Send + Sync + 'a>;
424
425/// Columns of a window's row: placement of the window in the whole image.
426#[derive(Debug, Clone, Copy)]
427struct RowGeometry {
428    width: usize,
429    planes: usize,
430    x0: usize,
431    full_width: usize,
432    full_height: usize,
433}
434
435impl RowGeometry {
436    /// Columns `x` (whole-image coordinates) that `r` selects, with the sample offset of
437    /// plane `r.plane` inside a window row.
438    fn columns(&self, r: &Region) -> Vec<(usize, usize)> {
439        let left = r.left as usize;
440        let start = left.max(self.x0);
441        // First selected column at or after the window start.
442        let start = left + (start - left).div_ceil(r.col_pitch as usize) * r.col_pitch as usize;
443        let right = (r.right as usize).min(self.x0 + self.width);
444        let (x0, p, plane) = (self.x0, self.planes, r.plane as usize);
445        (start..right.max(start)).step_by(r.col_pitch as usize).map(move |x| (x, (x - x0) * p + plane)).collect()
446    }
447
448    /// Number of selected planes that exist.
449    fn plane_count(&self, r: &Region) -> usize {
450        (r.plane as usize).saturating_add(r.planes as usize).min(self.planes).saturating_sub(r.plane as usize)
451    }
452}
453
454/// Whether row `y` is selected by `r` (rows `top..bottom`, every `row_pitch`).
455#[inline]
456fn row_selected(r: &Region, y: usize) -> bool {
457    let top = r.top as usize;
458    y >= top && y < r.bottom as usize && (y - top) % r.row_pitch as usize == 0
459}
460
461/// `f(x, y, plane_index_within_region, sample)` for every sample of a row that `r` selects.
462fn region_fn<'a>(
463    g: &RowGeometry,
464    r: &'a Region,
465    f: impl Fn(usize, usize, usize, &mut f32) + Send + Sync + 'a,
466) -> Option<RowFn<'a>> {
467    let cols = g.columns(r);
468    let np = g.plane_count(r);
469    if cols.is_empty() || np == 0 {
470        return None;
471    }
472    Some(Box::new(move |y, row| {
473        if !row_selected(r, y) {
474            return;
475        }
476        for &(x, off) in &cols {
477            for k in 0..np {
478                if let Some(v) = row.get_mut(off + k) {
479                    f(x, y, k, v);
480                }
481            }
482        }
483    }))
484}
485
486/// Prepare a row-local pixel opcode for windows of geometry `g`: `Some(Some(f))`, or
487/// `Some(None)` when it changes nothing there; `None` for the opcodes that are not
488/// row-local or not applied (bad-pixel repairs, geometric and unknown ones).
489fn row_op<'a>(op: &'a Opcode, g: &RowGeometry, value_scale: f32) -> Option<Option<RowFn<'a>>> {
490    Some(match op {
491        Opcode::MapTable { region, table } if !table.is_empty() => {
492            let last = (table.len() - 1) as f32;
493            region_fn(g, region, move |_, _, _, v| {
494                let i = (*v * value_scale).round().clamp(0.0, last) as usize;
495                *v = table[i] as f32 / value_scale;
496            })
497        }
498        Opcode::MapTable { .. } => None,
499        Opcode::MapPolynomial { region, coeffs } => region_fn(g, region, move |_, _, _, v| {
500            // Polynomials map values normalised to [0, 1].
501            let x = (*v * value_scale / 65535.0) as f64;
502            let mut acc = 0f64;
503            for c in coeffs.iter().rev() {
504                acc = acc * x + c;
505            }
506            *v = (acc * 65535.0 / value_scale as f64) as f32;
507        }),
508        Opcode::DeltaPerRow { region, deltas } => region_fn(g, region, move |_, y, _, v| {
509            let i = (y - region.top as usize) / region.row_pitch as usize;
510            *v += deltas.get(i).copied().unwrap_or(0.0) * 65535.0 / value_scale;
511        }),
512        Opcode::DeltaPerColumn { region, deltas } => region_fn(g, region, move |x, _, _, v| {
513            let i = (x - region.left as usize) / region.col_pitch as usize;
514            *v += deltas.get(i).copied().unwrap_or(0.0) * 65535.0 / value_scale;
515        }),
516        Opcode::ScalePerRow { region, scales } => region_fn(g, region, move |_, y, _, v| {
517            let i = (y - region.top as usize) / region.row_pitch as usize;
518            *v *= scales.get(i).copied().unwrap_or(1.0);
519        }),
520        Opcode::ScalePerColumn { region, scales } => region_fn(g, region, move |x, _, _, v| {
521            let i = (x - region.left as usize) / region.col_pitch as usize;
522            *v *= scales.get(i).copied().unwrap_or(1.0);
523        }),
524        Opcode::GainMap { region, points_v, points_h, spacing_v, spacing_h, origin_v, origin_h, map_planes, gains } => {
525            let (pv, ph, mp) = (*points_v as usize, *points_h as usize, *map_planes as usize);
526            if pv == 0 || ph == 0 || mp == 0 || gains.len() < pv * ph * mp {
527                return Some(None);
528            }
529            let (fw, fh) = (g.full_width.max(1) as f64, g.full_height.max(1) as f64);
530            // Map positions are relative to the whole image (0..1). Horizontal
531            // interpolation weights are per column; vertical ones per row.
532            let pos = |rel: f64, origin: f64, spacing: f64, n: usize| -> (usize, usize, f32) {
533                let f = if spacing > 0.0 { ((rel - origin) / spacing).clamp(0.0, (n - 1) as f64) } else { 0.0 };
534                let i0 = (f.floor() as usize).min(n - 1);
535                (i0, (i0 + 1).min(n - 1), (f - i0 as f64) as f32)
536            };
537            let cols: Vec<(usize, usize, usize, f32)> = g
538                .columns(region)
539                .into_iter()
540                .map(|(x, off)| {
541                    let (h0, h1, t) = pos(x as f64 / fw, *origin_h, *spacing_h, ph);
542                    (off, h0, h1, t)
543                })
544                .collect();
545            let np = g.plane_count(region);
546            if cols.is_empty() || np == 0 {
547                return Some(None);
548            }
549            let (origin_v, spacing_v) = (*origin_v, *spacing_v);
550            Some(Box::new(move |y, row| {
551                if !row_selected(region, y) {
552                    return;
553                }
554                let (v0, v1, tv) = pos(y as f64 / fh, origin_v, spacing_v, pv);
555                let mut line = vec![0f32; ph];
556                for k in 0..np {
557                    let m = k.min(mp - 1);
558                    for (hi, l) in line.iter_mut().enumerate() {
559                        let a = gains[(v0 * ph + hi) * mp + m];
560                        let b = gains[(v1 * ph + hi) * mp + m];
561                        *l = a + (b - a) * tv;
562                    }
563                    for &(off, h0, h1, th) in &cols {
564                        if let Some(v) = row.get_mut(off + k) {
565                            *v *= line[h0] + (line[h1] - line[h0]) * th;
566                        }
567                    }
568                }
569            }))
570        }
571        Opcode::FixVignetteRadial { k, cx, cy } => {
572            let (fw, fh) = (g.full_width as f64, g.full_height as f64);
573            let (cx, cy) = (cx * fw, cy * fh);
574            // Squared distance to the farthest corner normalises r to 1 there.
575            let m2 = [(0.0, 0.0), (fw, 0.0), (0.0, fh), (fw, fh)]
576                .iter()
577                .map(|&(x, y): &(f64, f64)| (x - cx).powi(2) + (y - cy).powi(2))
578                .fold(0.0, f64::max);
579            if m2 <= 0.0 || !m2.is_finite() || g.planes == 0 {
580                return Some(None);
581            }
582            let (np, x0, full_h) = (g.planes, g.x0, g.full_height);
583            Some(Box::new(move |y, row| {
584                if y >= full_h {
585                    return;
586                }
587                let dy2 = (y as f64 - cy).powi(2);
588                for (i, px) in row.chunks_exact_mut(np).enumerate() {
589                    let r2 = (((x0 + i) as f64 - cx).powi(2) + dy2) / m2;
590                    let g = 1.0 + r2 * (k[0] + r2 * (k[1] + r2 * (k[2] + r2 * (k[3] + r2 * k[4]))));
591                    for v in px {
592                        *v *= g as f32;
593                    }
594                }
595            }))
596        }
597        _ => return None,
598    })
599}
600
601/// A list of row-local opcodes prepared for one window geometry, applied a row at a time
602/// (e.g. by a streaming pipeline that produces the rows band by band). For every row this
603/// gives exactly what [`apply`] gives on the whole window.
604pub struct RowOps<'a> {
605    fns: Vec<RowFn<'a>>,
606}
607
608impl<'a> RowOps<'a> {
609    /// Prepare `ops` for windows `width` pixels wide of `planes` samples per pixel, whose
610    /// first column is column `x0` of a `full_width x full_height` image (`value_scale` as
611    /// for [`apply`]). `None` when a pixel opcode is not row-local
612    /// ([`Opcode::is_row_local`]); geometric and unknown opcodes are left out, as by
613    /// [`apply`].
614    pub fn new(
615        ops: &'a [Opcode],
616        width: usize,
617        planes: usize,
618        x0: usize,
619        full_width: usize,
620        full_height: usize,
621        value_scale: f32,
622    ) -> Option<Self> {
623        let g = RowGeometry { width, planes, x0, full_width, full_height };
624        let mut fns = Vec::new();
625        for op in ops.iter().filter(|o| o.is_pixel_opcode()) {
626            if let Some(f) = row_op(op, &g, value_scale)? {
627                fns.push(f);
628            }
629        }
630        Some(RowOps { fns })
631    }
632
633    /// Whether no opcode changes anything.
634    pub fn is_empty(&self) -> bool {
635        self.fns.is_empty()
636    }
637
638    /// Apply the opcodes, in order, to window row `y` (whole-image coordinates; `row`:
639    /// `width * planes` samples).
640    pub fn apply(&self, y: usize, row: &mut [f32]) {
641        for f in &self.fns {
642            f(y, row);
643        }
644    }
645}
646
647/// Apply pixel opcodes to `img`, whose samples are in the units the list expects
648/// (OpcodeList1: stored values; OpcodeList2: linear reference values in `[0, 1]`;
649/// OpcodeList3: demosaiced values). `cfa_period` is the CFA repeat (2 for Bayer) used by
650/// the bad-pixel opcodes; `value_scale` maps sample values to the 16-bit table/constant
651/// domain (1 for stored values, 65535 for normalised ones). Returns the names of
652/// opcodes that were not applied (geometric and unknown ones).
653pub fn apply(
654    ops: &[Opcode],
655    img: &mut PlaneImage,
656    cfa_period: usize,
657    value_scale: f32,
658    parallel: bool,
659) -> Vec<&'static str> {
660    let mut skipped = Vec::new();
661    let g = RowGeometry {
662        width: img.width,
663        planes: img.planes,
664        x0: img.x0,
665        full_width: img.full_width,
666        full_height: img.full_height,
667    };
668    for op in ops {
669        if let Some(f) = row_op(op, &g, value_scale) {
670            if let Some(f) = f {
671                img.each_row(parallel, &f);
672            }
673            continue;
674        }
675        match op {
676            Opcode::FixBadPixelsConstant { constant, bayer_phase } => {
677                let c = *constant as f32 / value_scale;
678                fix_bad_pixels(img, cfa_period, *bayer_phase, |v: f32| v == c, &[], &[], parallel);
679            }
680            Opcode::FixBadPixelsList { bayer_phase, points, rects } => {
681                fix_bad_pixels(img, cfa_period, *bayer_phase, |_| false, points, rects, parallel);
682            }
683            other => skipped.push(other.name()),
684        }
685    }
686    skipped
687}
688
689/// Whether [`apply_u16`] gives exactly what [`apply`] gives on the same values converted
690/// to f32 (value scale 1) and rounded back: tables look up rounded values and repairs
691/// produce means that only a final rounding (or a table) reads afterwards.
692pub fn u16_exact(ops: &[Opcode]) -> bool {
693    let mut repaired = false;
694    for op in ops {
695        match op {
696            Opcode::MapTable { .. } => {}
697            _ if !op.is_pixel_opcode() => {}
698            Opcode::FixBadPixelsConstant { .. } | Opcode::FixBadPixelsList { .. } if !repaired => repaired = true,
699            _ => return false,
700        }
701    }
702    true
703}
704
705/// [`apply`] on stored integer samples (value scale 1), for lists made of tables and
706/// bad-pixel repairs: no f32 copy of the image is needed and the result is identical
707/// to the f32 pass rounded back to u16. Returns `None` (leaving `img` untouched) for
708/// lists it does not cover.
709pub fn apply_u16(
710    ops: &[Opcode],
711    img: &mut PlaneImage<u16>,
712    cfa_period: usize,
713    parallel: bool,
714) -> Option<Vec<&'static str>> {
715    if !u16_exact(ops) {
716        return None;
717    }
718    let mut skipped = Vec::new();
719    for op in ops {
720        match op {
721            Opcode::MapTable { region, table } if !table.is_empty() => {
722                let last = table.len() - 1;
723                img.for_region(region, parallel, |_, _, _, v| *v = table[(*v as usize).min(last)]);
724            }
725            Opcode::FixBadPixelsConstant { constant, bayer_phase } => {
726                let c = *constant;
727                fix_bad_pixels(img, cfa_period, *bayer_phase, |v: u16| v as u32 == c, &[], &[], parallel);
728            }
729            Opcode::FixBadPixelsList { bayer_phase, points, rects } => {
730                fix_bad_pixels(img, cfa_period, *bayer_phase, |_| false, points, rects, parallel);
731            }
732            op if !op.is_pixel_opcode() => skipped.push(op.name()),
733            _ => {}
734        }
735    }
736    Some(skipped)
737}
738
739/// Indices `x < w` of `row` (`p` samples per pixel) whose first sample is bad. Rows are
740/// tested in blocks first: bad values are rare.
741fn bad_in_row<T: Sample>(row: &[T], w: usize, p: usize, is_bad: &impl Fn(T) -> bool) -> impl Iterator<Item = usize> {
742    const BLOCK: usize = 64;
743    let mut out = Vec::new();
744    for b in (0..w).step_by(BLOCK) {
745        let e = (b + BLOCK).min(w);
746        let hit = if p == 1 {
747            row[b..e].iter().fold(false, |a, &v| a | is_bad(v))
748        } else {
749            (b..e).fold(false, |a, x| a | is_bad(row[x * p]))
750        };
751        if hit {
752            out.extend((b..e).filter(|&x| is_bad(row[x * p])));
753        }
754    }
755    out.into_iter()
756}
757
758/// Replace bad pixels by the mean of the nearest same-colour neighbours (distance =
759/// CFA period) that are not themselves bad. Coordinates are whole-image ones.
760fn fix_bad_pixels<T: Sample>(
761    img: &mut PlaneImage<T>,
762    period: usize,
763    _phase: u32,
764    is_bad_value: impl Fn(T) -> bool + Sync,
765    points: &[(u32, u32)],
766    rects: &[(u32, u32, u32, u32)],
767    parallel: bool,
768) {
769    let (w, h, p, stride) = (img.width, img.height, img.planes, img.stride);
770    let (x0, y0) = (img.x0, img.y0);
771    if w == 0 || h == 0 || stride < w || img.data.len() < ((h - 1) * stride + w) * p {
772        return;
773    }
774    let period = period.max(1);
775    let at = |x: usize, y: usize| (y * stride + x) * p;
776    // Bad pixels are sparse: collect their window indices (y * w + x), sorted.
777    let data = &*img.data;
778    let per_row: Vec<Vec<usize>> = par::map((0..h).collect(), parallel, |y| {
779        let row = &data[at(0, y)..at(w - 1, y) + 1];
780        bad_in_row(row, w, p, &is_bad_value).map(|x| y * w + x).collect()
781    });
782    let mut bad: Vec<usize> = per_row.into_iter().flatten().collect();
783    let mut mark = |row: usize, col: usize| {
784        if row >= y0 && col >= x0 && row - y0 < h && col - x0 < w {
785            bad.push((row - y0) * w + col - x0);
786        }
787    };
788    for &(row, col) in points {
789        mark(row as usize, col as usize);
790    }
791    for &(t, l, bm, r) in rects {
792        for y in (t as usize).max(y0)..(bm as usize).min(y0 + h) {
793            for x in (l as usize).max(x0)..(r as usize).min(x0 + w) {
794                mark(y, x);
795            }
796        }
797    }
798    if bad.is_empty() {
799        return;
800    }
801    bad.sort_unstable();
802    bad.dedup();
803    let is_bad = |i: usize| bad.binary_search(&i).is_ok();
804    let mut fixes = Vec::new();
805    for &i in &bad {
806        let (x, y) = (i % w, i / w);
807        for pl in 0..p {
808            let (mut s, mut n) = (0f32, 0f32);
809            for (dx, dy) in [(-1isize, 0isize), (1, 0), (0, -1), (0, 1), (-1, -1), (1, 1), (-1, 1), (1, -1)] {
810                let xx = x as isize + dx * period as isize;
811                let yy = y as isize + dy * period as isize;
812                if xx >= 0
813                    && yy >= 0
814                    && (xx as usize) < w
815                    && (yy as usize) < h
816                    && !is_bad(yy as usize * w + xx as usize)
817                {
818                    s += img.data[at(xx as usize, yy as usize) + pl].to_f32();
819                    n += 1.0;
820                }
821            }
822            if n > 0.0 {
823                fixes.push((at(x, y) + pl, s / n));
824            }
825        }
826    }
827    for (i, v) in fixes {
828        img.data[i] = T::from_mean(v);
829    }
830}
831
832#[cfg(test)]
833mod tests {
834    use super::*;
835
836    fn op(id: u32, body: &[u8]) -> Vec<u8> {
837        let mut v = id.to_be_bytes().to_vec();
838        v.extend(0x01030000u32.to_be_bytes());
839        v.extend(0u32.to_be_bytes());
840        v.extend((body.len() as u32).to_be_bytes());
841        v.extend(body);
842        v
843    }
844    fn region_bytes(t: u32, l: u32, b: u32, r: u32) -> Vec<u8> {
845        [t, l, b, r, 0, 1, 1, 1].iter().flat_map(|v| v.to_be_bytes()).collect()
846    }
847
848    #[test]
849    fn gain_map_and_scale() {
850        let mut body = region_bytes(0, 0, 2, 2);
851        body.extend(2u32.to_be_bytes());
852        body.extend(2u32.to_be_bytes());
853        for f in [1.0f64, 1.0, 0.0, 0.0] {
854            body.extend(f.to_bits().to_be_bytes());
855        }
856        body.extend(1u32.to_be_bytes());
857        for g in [1.0f32, 2.0, 3.0, 4.0] {
858            body.extend(g.to_bits().to_be_bytes());
859        }
860        let mut list = 2u32.to_be_bytes().to_vec();
861        list.extend(op(9, &body));
862        let mut sb = region_bytes(0, 0, 2, 2);
863        sb.extend(2u32.to_be_bytes());
864        sb.extend(1.0f32.to_bits().to_be_bytes());
865        sb.extend(0.5f32.to_bits().to_be_bytes());
866        list.extend(op(12, &sb));
867        let ops = parse(&list).unwrap();
868        assert_eq!(ops.len(), 2);
869        let mut data = vec![1.0f32; 4];
870        let mut img = PlaneImage::new(&mut data, 2, 2, 1);
871        assert!(apply(&ops, &mut img, 2, 65535.0, false).is_empty());
872        // gain at (0,0)=1, (0,1)=1.5 (half way to 2), row 1 scaled by 0.5 after gain.
873        assert_eq!(data[0], 1.0);
874        assert!((data[1] - 1.5).abs() < 1e-6);
875    }
876
877    #[test]
878    fn malformed_lists_error() {
879        assert!(parse(&[0, 0, 0, 1]).is_err());
880        let mut list = 1u32.to_be_bytes().to_vec();
881        list.extend(op(9, &[0; 20]));
882        assert!(parse(&list).is_err());
883        let mut list = 1u32.to_be_bytes().to_vec();
884        let mut body = region_bytes(0, 0, 1, 1);
885        body.extend(u32::MAX.to_be_bytes());
886        list.extend(op(10, &body));
887        assert!(parse(&list).is_err());
888    }
889
890    #[test]
891    fn bad_pixel_list() {
892        let mut data: Vec<f32> = (0..36).map(|i| i as f32).collect();
893        data[2 * 6 + 2] = 1000.0;
894        let ops = vec![Opcode::FixBadPixelsList { bayer_phase: 0, points: vec![(2, 2)], rects: vec![] }];
895        let mut img = PlaneImage::new(&mut data, 6, 6, 1);
896        apply(&ops, &mut img, 2, 1.0, false);
897        assert!(data[14] < 100.0);
898    }
899
900    fn gain_map_op(region: Region) -> Opcode {
901        Opcode::GainMap {
902            region,
903            points_v: 3,
904            points_h: 4,
905            spacing_v: 0.5,
906            spacing_h: 1.0 / 3.0,
907            origin_v: 0.0,
908            origin_h: 0.0,
909            map_planes: 1,
910            gains: (0..12).map(|i| 1.0 + i as f32 * 0.1).collect(),
911        }
912    }
913
914    #[test]
915    fn windows_match_the_whole_image() {
916        // Pitched (one Bayer plane) gain map, row deltas, bad pixels and a vignette: applied to a
917        // window of a larger buffer they must give exactly the whole-image result there.
918        let r = Region { top: 1, left: 0, bottom: 20, right: 24, plane: 0, planes: 1, row_pitch: 2, col_pitch: 2 };
919        let ops = vec![
920            gain_map_op(r.clone()),
921            Opcode::DeltaPerRow { region: r.clone(), deltas: (0..10).map(|i| i as f32 * 1e-5).collect() },
922            Opcode::FixVignetteRadial { k: [0.3, -0.1, 0.0, 0.0, 0.0], cx: 0.4, cy: 0.6 },
923            Opcode::FixBadPixelsList { bayer_phase: 0, points: vec![(9, 11)], rects: vec![] },
924        ];
925        let (w, h) = (24usize, 20usize);
926        let base: Vec<f32> = (0..w * h).map(|i| 100.0 + (i * 7 % 13) as f32).collect();
927        let mut whole = base.clone();
928        apply(&ops, &mut PlaneImage::new(&mut whole, w, h, 1), 2, 1.0, false);
929        // Window (5, 4) .. (21, 18) inside a buffer with a 3-pixel border.
930        let (wx, wy, ww, wh) = (5usize, 4usize, 16usize, 14usize);
931        let stride = ww + 3;
932        let mut buf = vec![0f32; stride * (wh + 1)];
933        for y in 0..wh {
934            for x in 0..ww {
935                buf[(y + 1) * stride + x + 2] = base[(y + wy) * w + x + wx];
936            }
937        }
938        let mut pi = PlaneImage {
939            data: &mut buf[stride + 2..],
940            width: ww,
941            height: wh,
942            planes: 1,
943            stride,
944            x0: wx,
945            y0: wy,
946            full_width: w,
947            full_height: h,
948        };
949        apply(&ops, &mut pi, 2, 1.0, true);
950        for y in 0..wh {
951            for x in 0..ww {
952                let (a, b) = (buf[(y + 1) * stride + x + 2], whole[(y + wy) * w + x + wx]);
953                assert!((a - b).abs() < 1e-3, "({x},{y}): {a} vs {b}");
954            }
955        }
956        // Pixels outside the gain map's plane pitch are untouched by it.
957        assert_eq!(whole[0], base[0] * vignette_gain(0.0, 0.0, w, h));
958    }
959
960    fn vignette_gain(x: f64, y: f64, w: usize, h: usize) -> f32 {
961        let (cx, cy) = (0.4 * w as f64, 0.6 * h as f64);
962        let m2 = [(0.0, 0.0), (w as f64, 0.0), (0.0, h as f64), (w as f64, h as f64)]
963            .iter()
964            .map(|&(a, b): &(f64, f64)| (a - cx).powi(2) + (b - cy).powi(2))
965            .fold(0.0, f64::max);
966        let r2 = ((x - cx).powi(2) + (y - cy).powi(2)) / m2;
967        (1.0 + 0.3 * r2 - 0.1 * r2 * r2) as f32
968    }
969
970    #[test]
971    fn u16_path_matches_f32_path() {
972        let (w, h) = (150usize, 23usize);
973        let table: Vec<u16> = (0..3000u32).map(|i| (i * 7 / 3 + 5) as u16).collect();
974        let pitched =
975            Region { top: 1, left: 3, bottom: 20, right: 140, plane: 0, planes: 1, row_pitch: 2, col_pitch: 2 };
976        let map = |region: Region| Opcode::MapTable { region, table: table.clone() };
977        let all =
978            |planes| Region { top: 0, left: 0, bottom: 100, right: 1000, plane: 0, planes, row_pitch: 1, col_pitch: 1 };
979        let constant = Opcode::FixBadPixelsConstant { constant: 0, bayer_phase: 1 };
980        let list =
981            Opcode::FixBadPixelsList { bayer_phase: 0, points: vec![(4, 7), (0, 0)], rects: vec![(10, 60, 13, 66)] };
982        let warp = Opcode::WarpRectilinear { planes: vec![], cx: 0.5, cy: 0.5 };
983        let lists: Vec<Vec<Opcode>> = vec![
984            vec![constant.clone()],
985            vec![list.clone()],
986            vec![map(pitched.clone()), map(all(1)), constant.clone(), map(pitched.clone())],
987            vec![warp.clone(), list.clone(), map(all(3))],
988        ];
989        for planes in [1usize, 3] {
990            for ops in &lists {
991                assert!(u16_exact(ops));
992                let mut x = 99u32;
993                let base: Vec<u16> = (0..w * h * planes)
994                    .map(|_| {
995                        x = x.wrapping_mul(1664525).wrapping_add(1013904223);
996                        // Some zeros (bad for the constant), values past the table's end.
997                        if x >> 27 == 0 { 0 } else { (x >> 20) as u16 % 3500 }
998                    })
999                    .collect();
1000                let mut f: Vec<f32> = base.iter().map(|&v| v as f32).collect();
1001                let skipped_f = apply(ops, &mut PlaneImage::new(&mut f, w, h, planes), 2, 1.0, true);
1002                let want: Vec<u16> = f.iter().map(|v| v.round().clamp(0.0, 65535.0) as u16).collect();
1003                let mut got = base.clone();
1004                let skipped = apply_u16(ops, &mut PlaneImage::new(&mut got, w, h, planes), 2, true).unwrap();
1005                assert_eq!(skipped, skipped_f);
1006                assert_eq!(got, want, "planes {planes} ops {ops:?}");
1007                assert_ne!(got, base);
1008            }
1009        }
1010        // Repairs read by a later repair (or by a non-table opcode) need the f32 pass.
1011        assert!(!u16_exact(&[constant.clone(), list]));
1012        assert!(!u16_exact(&[constant, Opcode::ScalePerRow { region: all(1), scales: vec![1.0] }]));
1013    }
1014
1015    #[test]
1016    fn vignette_parse_and_gain() {
1017        let mut body = Vec::new();
1018        for f in [0.5f64, 0.0, 0.0, 0.0, 0.0, 0.5, 0.5] {
1019            body.extend(f.to_bits().to_be_bytes());
1020        }
1021        let mut list = 1u32.to_be_bytes().to_vec();
1022        list.extend(op(3, &body));
1023        let ops = parse(&list).unwrap();
1024        assert!(ops[0].is_pixel_opcode());
1025        let mut data = vec![1.0f32; 3 * 4 * 4];
1026        apply(&ops, &mut PlaneImage::new(&mut data, 4, 4, 3), 1, 65535.0, false);
1027        // Corner (0,0): r = 1 -> gain 1.5 on every plane; centre (2,2): gain 1.
1028        assert!(data[..3].iter().all(|&v| (v - 1.5).abs() < 1e-6));
1029        assert!(data[(2 * 4 + 2) * 3..][..3].iter().all(|&v| (v - 1.0).abs() < 1e-6));
1030    }
1031}