Skip to main content

pic_scale/
scaler.rs

1/*
2 * Copyright (c) Radzivon Bartoshyk. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification,
5 * are permitted provided that the following conditions are met:
6 *
7 * 1.  Redistributions of source code must retain the above copyright notice, this
8 * list of conditions and the following disclaimer.
9 *
10 * 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 *
14 * 3.  Neither the name of the copyright holder nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29#![forbid(unsafe_code)]
30use crate::convolution::{ConvolutionOptions, HorizontalFilterPass, VerticalConvolutionPass};
31use crate::factory::{Ar30ByteOrder, Rgb30};
32use crate::image_size::ImageSize;
33use crate::image_store::{CheckStoreDensity, ImageStore, ImageStoreMut};
34use crate::math::WeightsGenerator;
35use crate::plan::{
36    AlphaPlanner, Ar30Destructuring, Ar30DestructuringImpl, Ar30Plan, BothAxesConvolvePlan,
37    DefaultPlanner, HorizontalConvolvePlan, NoopPlan, ResampleNearestPlan, Resampling,
38    TrampolineFiltering, VerticalConvolvePlan,
39};
40use crate::threading_policy::ThreadingPolicy;
41use crate::validation::PicScaleError;
42use crate::{
43    CbCr8ImageStore, CbCr16ImageStore, CbCrF32ImageStore, Planar8ImageStore, Planar16ImageStore,
44    PlanarF32ImageStore, ResamplingFunction, ResamplingPlan, Rgb8ImageStore, Rgb16ImageStore,
45    RgbF32ImageStore, Rgba8ImageStore, Rgba16ImageStore, RgbaF32ImageStore,
46};
47use std::fmt::Debug;
48use std::marker::PhantomData;
49use std::sync::Arc;
50
51#[derive(Debug, Copy, Clone)]
52/// Represents base scaling structure
53pub struct Scaler {
54    pub(crate) function: ResamplingFunction,
55    pub(crate) threading_policy: ThreadingPolicy,
56    pub workload_strategy: WorkloadStrategy,
57    pub(crate) multi_step_upscaling: bool,
58    pub(crate) supersampling: bool,
59}
60
61/// Defines execution hint about preferred strategy
62#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)]
63pub enum WorkloadStrategy {
64    /// Prefers quality to speed
65    PreferQuality,
66    /// Prefers speed to quality
67    #[default]
68    PreferSpeed,
69}
70
71impl Scaler {
72    /// Creates new [Scaler] instance with corresponding filter
73    ///
74    /// Creates default [crate::Scaler] with corresponding filter and default [ThreadingPolicy::Single]
75    ///
76    pub fn new(filter: ResamplingFunction) -> Self {
77        Scaler {
78            function: filter,
79            threading_policy: ThreadingPolicy::Single,
80            workload_strategy: WorkloadStrategy::default(),
81            multi_step_upscaling: false,
82            supersampling: false,
83        }
84    }
85
86    /// Sets preferred workload strategy
87    ///
88    /// This is hint only, it may change something, or may be not.
89    pub fn set_workload_strategy(&mut self, workload_strategy: WorkloadStrategy) -> Self {
90        self.workload_strategy = workload_strategy;
91        *self
92    }
93
94    /// Enables multistep upscaling for large magnification ratios.
95    ///
96    /// When upscaling by a large factor (e.g. 10× or more), a single-pass filter
97    /// does not have enough source samples to interpolate smoothly — the kernel
98    /// spans so few real pixels that the result looks blocky or rings heavily.
99    /// Multistep upscaling breaks the operation into a chain of smaller steps,
100    /// each within the filter's optimal range, so every pass has enough source
101    /// data to produce a smooth result.
102    ///
103    /// The number of steps and the intermediate sizes are chosen automatically
104    /// based on the resampling function's support width.
105    ///
106    /// This has no effect on downscaling or on [`ResamplingFunction::Nearest`],
107    /// which are always single-pass. For modest upscale ratios already within
108    /// the filter's safe range the plan degenerates to a single step with no
109    /// overhead.
110    pub fn set_multi_step_upsampling(&mut self, value: bool) -> Self {
111        self.multi_step_upscaling = value;
112        *self
113    }
114
115    /// Enables a cheap pre-filter pass before large downscales to improve
116    /// quality and performance.
117    ///
118    /// When downscaling by a large ratio (≥ 3×) the quality filter must
119    /// average a very large number of source pixels per output pixel, which
120    /// is slow and can produce aliasing. With supersampling enabled, a fast
121    /// pre-filter first reduces the image to approximately twice the target
122    /// size, then the quality filter performs a final clean 2× reduction.
123    /// This keeps the quality filter in its optimal range while the cheap
124    /// pre-filter handles the heavy lifting.
125    ///
126    /// The pre-filter is chosen automatically based on the downscale ratio:
127    /// - **≥ 4×**: [`ResamplingFunction::Nearest`] — fastest, no blending.
128    /// - **3–4×**: [`ResamplingFunction::Box`] (area average) — slightly
129    ///   higher quality than nearest for non-integer ratios.
130    ///
131    /// Has no effect on upscaling or on [`ResamplingFunction::Nearest`],
132    /// which is always single-pass.
133    pub fn set_supersampling(&mut self, value: bool) -> Self {
134        self.supersampling = value;
135        *self
136    }
137}
138
139impl Scaler {
140    pub(crate) fn build_single_step_plan<
141        T: Clone + Copy + Debug + Send + Sync + Default + WeightsGenerator<W> + 'static,
142        W,
143        const N: usize,
144    >(
145        &self,
146        source_size: ImageSize,
147        destination_size: ImageSize,
148        bit_depth: usize,
149    ) -> Result<Arc<Resampling<T, N>>, PicScaleError>
150    where
151        for<'a> ImageStore<'a, T, N>:
152            VerticalConvolutionPass<T, W, N> + HorizontalFilterPass<T, W, N>,
153        for<'a> ImageStoreMut<'a, T, N>: CheckStoreDensity,
154    {
155        if self.function == ResamplingFunction::Nearest {
156            return Ok(Arc::new(ResampleNearestPlan {
157                source_size,
158                target_size: destination_size,
159                threading_policy: self.threading_policy,
160                _phantom_data: PhantomData,
161            }));
162        }
163
164        let should_do_horizontal = source_size.width != destination_size.width;
165        let should_do_vertical = source_size.height != destination_size.height;
166
167        let options = ConvolutionOptions {
168            workload_strategy: self.workload_strategy,
169            bit_depth,
170            src_size: source_size,
171            dst_size: destination_size,
172        };
173
174        let vertical_plan = if should_do_vertical {
175            let vertical_filters =
176                T::make_weights(self.function, source_size.height, destination_size.height)?;
177            Some(ImageStore::<T, N>::vertical_plan(
178                vertical_filters,
179                self.threading_policy,
180                options,
181            ))
182        } else {
183            None
184        };
185
186        let horizontal_plan = if should_do_horizontal {
187            let horizontal_filters =
188                T::make_weights(self.function, source_size.width, destination_size.width)?;
189            Some(ImageStore::<T, N>::horizontal_plan(
190                horizontal_filters,
191                self.threading_policy,
192                options,
193            ))
194        } else {
195            None
196        };
197
198        match (should_do_vertical, should_do_horizontal) {
199            (true, true) => {
200                let v = vertical_plan.expect("Should have a vertical filter");
201                let h = horizontal_plan.expect("Should have a horizontal filter");
202                let trampoline = Arc::new(TrampolineFiltering {
203                    horizontal_filter: h.clone(),
204                    vertical_filter: v.clone(),
205                    source_size,
206                    target_size: destination_size,
207                });
208                Ok(Arc::new(BothAxesConvolvePlan {
209                    source_size,
210                    target_size: destination_size,
211                    horizontal_filter: h,
212                    vertical_filter: v,
213                    trampoline_filter: trampoline,
214                    threading_policy: self.threading_policy,
215                }))
216            }
217            (true, false) => Ok(Arc::new(VerticalConvolvePlan {
218                source_size,
219                target_size: destination_size,
220                vertical_filter: vertical_plan.expect("Should have a vertical filter"),
221            })),
222            (false, true) => Ok(Arc::new(HorizontalConvolvePlan {
223                source_size,
224                target_size: destination_size,
225                horizontal_filter: horizontal_plan.expect("Should have a horizontal filter"),
226            })),
227            (false, false) => Ok(Arc::new(NoopPlan {
228                source_size,
229                target_size: destination_size,
230                _phantom: PhantomData,
231            })),
232        }
233    }
234
235    /// Creates a resampling plan for a single-channel (planar/grayscale) `u8` image.
236    ///
237    /// The returned [`Arc<Resampling<u8, 1>>`] can be executed repeatedly against images
238    /// of `source_size` to produce output of `target_size` without recomputing filter weights.
239    ///
240    /// # Arguments
241    ///
242    /// - `source_size` — Dimensions of the input image.
243    /// - `target_size` — Desired dimensions of the output image.
244    /// # Example
245    ///
246    /// ```rust,no_run,ignore
247    /// let plan = scaler.plan_planar_resampling(source_size, target_size)?;
248    /// plan.resample(&store, &mut target_store)?;
249    /// ```
250    pub fn plan_planar_resampling(
251        &self,
252        source_size: ImageSize,
253        target_size: ImageSize,
254    ) -> Result<Arc<Resampling<u8, 1>>, PicScaleError> {
255        DefaultPlanner::plan_generic_resize(self, source_size, target_size, 8)
256    }
257
258    /// Creates a resampling plan for a two-channel grayscale + alpha (`GA`) `u8` image.
259    ///
260    /// When `premultiply_alpha` is `true` the alpha channel is pre-multiplied into the gray
261    /// channel before resampling and un-multiplied afterward.
262    ///
263    /// # Arguments
264    ///
265    /// - `source_size` — Dimensions of the input image.
266    /// - `target_size` — Desired dimensions of the output image.
267    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
268    ///
269    /// # Example
270    ///
271    /// ```rust,no_run,ignore
272    /// // Resample with alpha-aware filtering to avoid dark fringing
273    /// let plan = scaler.plan_gray_alpha_resampling(source_size, target_size, true)?;
274    /// plan.resample(&store, &mut target_store)?;
275    /// ```
276    pub fn plan_gray_alpha_resampling(
277        &self,
278        source_size: ImageSize,
279        target_size: ImageSize,
280        premultiply_alpha: bool,
281    ) -> Result<Arc<Resampling<u8, 2>>, PicScaleError> {
282        if premultiply_alpha {
283            AlphaPlanner::plan_generic_resize_with_alpha(
284                self,
285                source_size,
286                target_size,
287                8,
288                premultiply_alpha,
289            )
290        } else {
291            DefaultPlanner::plan_generic_resize(self, source_size, target_size, 8)
292        }
293    }
294
295    /// Creates a resampling plan for a two-channel chroma (`CbCr`) `u8` image.
296    ///
297    /// Intended for the chroma planes of YCbCr images (e.g. the `Cb`/`Cr` planes in
298    /// 4:2:0 or 4:2:2 video), where both channels are treated as independent signals
299    /// with no alpha relationship. For the luma plane use [`plan_planar_resampling`].
300    ///
301    /// # Arguments
302    ///
303    /// - `source_size` — Dimensions of the input chroma plane.
304    /// - `target_size` — Desired dimensions of the output chroma plane.
305    ///
306    /// # Example
307    ///
308    /// ```rust,no_run,ignore
309    /// let plan = scaler.plan_cbcr_resampling(source_size, target_size)?;
310    /// plan.resample(&cbcr_store, &mut target_cbcr_store)?;
311    /// ```
312    pub fn plan_cbcr_resampling(
313        &self,
314        source_size: ImageSize,
315        target_size: ImageSize,
316    ) -> Result<Arc<Resampling<u8, 2>>, PicScaleError> {
317        DefaultPlanner::plan_generic_resize(self, source_size, target_size, 8)
318    }
319
320    /// Creates a resampling plan for a three-channel RGB `u8` image.
321    ///
322    /// The returned [`Arc<Resampling<u8, 3>>`] encodes all filter weights for scaling
323    /// from `source_size` to `target_size` and can be reused across many frames without
324    /// recomputation.
325    ///
326    /// # Arguments
327    ///
328    /// - `source_size` — Dimensions of the input image.
329    /// - `target_size` — Desired dimensions of the output image.
330    ///
331    /// # Example
332    ///
333    /// ```rust,no_run,ignore
334    /// let plan = scaler.plan_rgb_resampling(source_size, target_size)?;
335    /// plan.resample(&store, &mut target_store)?;
336    /// ```
337    pub fn plan_rgb_resampling(
338        &self,
339        source_size: ImageSize,
340        target_size: ImageSize,
341    ) -> Result<Arc<Resampling<u8, 3>>, PicScaleError> {
342        DefaultPlanner::plan_generic_resize(self, source_size, target_size, 8)
343    }
344
345    /// Creates a resampling plan for a four-channel RGBA `u8` image.
346    ///
347    /// When `premultiply_alpha` is `true` the RGB channels are pre-multiplied by alpha
348    /// before resampling and un-multiplied afterward.
349    ///
350    /// # Arguments
351    ///
352    /// - `source_size` — Dimensions of the input image.
353    /// - `target_size` — Desired dimensions of the output image.
354    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
355    ///
356    /// # Example
357    ///
358    /// ```rust,no_run,ignore
359    /// // Resample a sprite sheet with correct alpha blending
360    /// let plan = scaler.plan_rgba_resampling(source_size, target_size, true)?;
361    /// plan.resample(&store, &mut target_store)?;
362    /// ```
363    pub fn plan_rgba_resampling(
364        &self,
365        source_size: ImageSize,
366        target_size: ImageSize,
367        premultiply_alpha: bool,
368    ) -> Result<Arc<Resampling<u8, 4>>, PicScaleError> {
369        if premultiply_alpha {
370            AlphaPlanner::plan_generic_resize_with_alpha(
371                self,
372                source_size,
373                target_size,
374                8,
375                premultiply_alpha,
376            )
377        } else {
378            DefaultPlanner::plan_generic_resize(self, source_size, target_size, 8)
379        }
380    }
381
382    /// Creates a resampling plan for a single-channel (planar/grayscale) `u16` image.
383    ///
384    /// The 16-bit variant of [`plan_planar_resampling`], suitable for high-bit-depth
385    /// grayscale content such as HDR images or luma planes from 10/12-bit video.
386    ///
387    /// # Arguments
388    ///
389    /// - `source_size` — Dimensions of the input image.
390    /// - `target_size` — Desired dimensions of the output image.
391    /// - `bit_depth` — Effective bit depth of the pixel data (e.g. `10`, `12`, or `16`).
392    ///   Must not exceed `16`.
393    ///
394    /// # Example
395    ///
396    /// ```rust,no_run,ignore
397    /// let plan = scaler.plan_planar_resampling16(source_size, target_size, 12)?;
398    /// plan.resample(&store, &mut target_store)?;
399    /// ```
400    pub fn plan_planar_resampling16(
401        &self,
402        source_size: ImageSize,
403        target_size: ImageSize,
404        bit_depth: usize,
405    ) -> Result<Arc<Resampling<u16, 1>>, PicScaleError> {
406        DefaultPlanner::plan_generic_resize(self, source_size, target_size, bit_depth)
407    }
408
409    /// Creates a resampling plan for a single-channel (planar/grayscale) `i16` image.
410    ///
411    /// The 16-bit variant of [`plan_planar_resampling`], suitable for high-bit-depth
412    /// grayscale content such as HDR images or luma planes from 10/12-bit video.
413    ///
414    /// # Arguments
415    ///
416    /// - `source_size` — Dimensions of the input image.
417    /// - `target_size` — Desired dimensions of the output image.
418    /// - `bit_depth` — Effective bit depth of the pixel data (e.g. `10`, `12`, or `16`).
419    ///   Must not exceed `16`.
420    ///
421    /// # Example
422    ///
423    /// ```rust,no_run,ignore
424    /// let plan = scaler.plan_planar_resampling_s16(source_size, target_size, 12)?;
425    /// plan.resample(&store, &mut target_store)?;
426    /// ```
427    pub fn plan_planar_resampling_s16(
428        &self,
429        source_size: ImageSize,
430        target_size: ImageSize,
431        bit_depth: usize,
432    ) -> Result<Arc<Resampling<i16, 1>>, PicScaleError> {
433        DefaultPlanner::plan_generic_resize(self, source_size, target_size, bit_depth)
434    }
435
436    /// Creates a resampling plan for a two-channel chroma (`CbCr`) `u16` image.
437    ///
438    /// The 16-bit variant of [`plan_cbcr_resampling`], intended for high-bit-depth chroma
439    /// planes of YCbCr content (e.g. 10-bit 4:2:0 or 4:2:2 video). Both channels are
440    /// treated as independent signals with no alpha relationship.
441    ///
442    /// # Arguments
443    ///
444    /// - `source_size` — Dimensions of the input chroma plane.
445    /// - `target_size` — Desired dimensions of the output chroma plane.
446    /// - `bit_depth` — Effective bit depth of the pixel data (e.g. `10`, `12`, or `16`).
447    ///   Must not exceed `16`.
448    ///
449    /// # Example
450    ///
451    /// ```rust,no_run,ignore
452    /// let plan = scaler.plan_cbcr_resampling16(source_size, target_size, 10)?;
453    /// plan.resample(&cbcr_store, &mut target_cbcr_store)?;
454    /// ```
455    pub fn plan_cbcr_resampling16(
456        &self,
457        source_size: ImageSize,
458        target_size: ImageSize,
459        bit_depth: usize,
460    ) -> Result<Arc<Resampling<u16, 2>>, PicScaleError> {
461        DefaultPlanner::plan_generic_resize(self, source_size, target_size, bit_depth)
462    }
463
464    /// Creates a resampling plan for a two-channel grayscale + alpha (`GA`) `u16` image.
465    ///
466    /// The 16-bit variant of [`plan_gray_alpha_resampling`]. When `premultiply_alpha` is
467    /// `true` the gray channel is pre-multiplied by alpha before resampling and
468    /// un-multiplied afterward.
469    ///
470    /// # Arguments
471    ///
472    /// - `source_size` — Dimensions of the input image.
473    /// - `target_size` — Desired dimensions of the output image.
474    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
475    /// - `bit_depth` — Effective bit depth of the pixel data (e.g. `10`, `12`, or `16`).
476    ///   Must not exceed `16`.
477    ///
478    /// # Example
479    ///
480    /// ```rust,no_run,ignore
481    /// let plan = scaler.plan_gray_alpha_resampling16(source_size, target_size, true, 16)?;
482    /// plan.resample(&store, &mut target_store)?;
483    /// ```
484    pub fn plan_gray_alpha_resampling16(
485        &self,
486        source_size: ImageSize,
487        target_size: ImageSize,
488        premultiply_alpha: bool,
489        bit_depth: usize,
490    ) -> Result<Arc<Resampling<u16, 2>>, PicScaleError> {
491        if premultiply_alpha {
492            AlphaPlanner::plan_generic_resize_with_alpha(
493                self,
494                source_size,
495                target_size,
496                bit_depth,
497                premultiply_alpha,
498            )
499        } else {
500            self.plan_cbcr_resampling16(source_size, target_size, bit_depth)
501        }
502    }
503
504    /// Creates a resampling plan for a three-channel RGB `u16` image.
505    ///
506    /// The 16-bit variant of [`plan_rgb_resampling`], suitable for high-bit-depth color
507    /// images such as 10/12-bit HDR or wide-gamut content. All three channels are
508    /// resampled independently with no alpha relationship.
509    ///
510    /// # Arguments
511    ///
512    /// - `source_size` — Dimensions of the input image.
513    /// - `target_size` — Desired dimensions of the output image.
514    /// - `bit_depth` — Effective bit depth of the pixel data (e.g. `10`, `12`, or `16`).
515    ///   Must not exceed `16`.
516    ///
517    /// # Example
518    ///
519    /// ```rust,no_run,ignore
520    /// let plan = scaler.plan_rgb_resampling16(source_size, target_size, 12)?;
521    /// plan.resample(&store, &mut target_store)?;
522    /// ```
523    pub fn plan_rgb_resampling16(
524        &self,
525        source_size: ImageSize,
526        target_size: ImageSize,
527        bit_depth: usize,
528    ) -> Result<Arc<Resampling<u16, 3>>, PicScaleError> {
529        DefaultPlanner::plan_generic_resize(self, source_size, target_size, bit_depth)
530    }
531
532    /// Creates a resampling plan for a four-channel RGBA `u16` image.
533    ///
534    /// The 16-bit variant of [`plan_rgba_resampling`]. When `premultiply_alpha` is `true`
535    /// the RGB channels are pre-multiplied by alpha before resampling and un-multiplied
536    /// afterward.
537    ///
538    /// # Arguments
539    ///
540    /// - `source_size` — Dimensions of the input image.
541    /// - `target_size` — Desired dimensions of the output image.
542    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
543    /// - `bit_depth` — Effective bit depth of the pixel data (e.g. `10`, `12`, or `16`).
544    ///   Must not exceed `16`.
545    ///
546    /// # Example
547    ///
548    /// ```rust,no_run,ignore
549    /// let plan = scaler.plan_rgba_resampling16(source_size, target_size, true, 10)?;
550    /// plan.resample(&store, &mut target_store)?;
551    /// ```
552    pub fn plan_rgba_resampling16(
553        &self,
554        source_size: ImageSize,
555        target_size: ImageSize,
556        premultiply_alpha: bool,
557        bit_depth: usize,
558    ) -> Result<Arc<Resampling<u16, 4>>, PicScaleError> {
559        if premultiply_alpha {
560            AlphaPlanner::plan_generic_resize_with_alpha(
561                self,
562                source_size,
563                target_size,
564                bit_depth,
565                premultiply_alpha,
566            )
567        } else {
568            DefaultPlanner::plan_generic_resize(self, source_size, target_size, bit_depth)
569        }
570    }
571
572    /// Creates a resampling plan for a single-channel (planar/grayscale) `f32` image.
573    ///
574    /// The `f32` variant of [`plan_planar_resampling`], suitable for HDR or linear-light
575    /// grayscale content where full floating-point precision is required.
576    ///
577    /// The internal accumulator precision is selected automatically based on the scaler's
578    /// [`WorkloadStrategy`]:
579    /// - [`PreferQuality`](WorkloadStrategy::PreferQuality) — accumulates in `f64` for
580    ///   maximum numerical accuracy.
581    /// - [`PreferSpeed`](WorkloadStrategy::PreferSpeed) — accumulates in `f32` for
582    ///   faster throughput at a small precision cost.
583    ///
584    /// # Arguments
585    ///
586    /// - `source_size` — Dimensions of the input image.
587    /// - `target_size` — Desired dimensions of the output image.
588    ///
589    /// # Example
590    ///
591    /// ```rust,no_run,ignore
592    /// let plan = scaler.plan_planar_resampling_f32(source_size, target_size)?;
593    /// plan.resample(&store, &mut target_store)?;
594    /// ```
595    pub fn plan_planar_resampling_f32(
596        &self,
597        source_size: ImageSize,
598        target_size: ImageSize,
599    ) -> Result<Arc<Resampling<f32, 1>>, PicScaleError> {
600        match self.workload_strategy {
601            WorkloadStrategy::PreferQuality => DefaultPlanner::plan_generic_resize::<f32, f64, 1>(
602                self,
603                source_size,
604                target_size,
605                8,
606            ),
607            WorkloadStrategy::PreferSpeed => DefaultPlanner::plan_generic_resize::<f32, f32, 1>(
608                self,
609                source_size,
610                target_size,
611                8,
612            ),
613        }
614    }
615
616    /// Creates a resampling plan for a two-channel chroma (`CbCr`) `f32` image.
617    ///
618    /// The `f32` variant of [`plan_cbcr_resampling`], intended for floating-point chroma
619    /// planes of YCbCr content. Both channels are treated as independent signals with no
620    /// alpha relationship.
621    ///
622    /// The internal accumulator precision is selected automatically based on the scaler's
623    /// [`WorkloadStrategy`]:
624    /// - [`PreferQuality`](WorkloadStrategy::PreferQuality) — accumulates in `f64` for
625    ///   maximum numerical accuracy.
626    /// - [`PreferSpeed`](WorkloadStrategy::PreferSpeed) — accumulates in `f32` for
627    ///   faster throughput at a small precision cost.
628    ///
629    /// # Arguments
630    ///
631    /// - `source_size` — Dimensions of the input chroma plane.
632    /// - `target_size` — Desired dimensions of the output chroma plane.
633    ///
634    /// # Example
635    ///
636    /// ```rust,no_run,ignore
637    /// let plan = scaler.plan_cbcr_resampling_f32(source_size, target_size)?;
638    /// plan.resample(&cbcr_store, &mut target_cbcr_store)?;
639    /// ```
640    pub fn plan_cbcr_resampling_f32(
641        &self,
642        source_size: ImageSize,
643        target_size: ImageSize,
644    ) -> Result<Arc<dyn ResamplingPlan<f32, 2> + Send + Sync>, PicScaleError> {
645        match self.workload_strategy {
646            WorkloadStrategy::PreferQuality => DefaultPlanner::plan_generic_resize::<f32, f64, 2>(
647                self,
648                source_size,
649                target_size,
650                8,
651            ),
652            WorkloadStrategy::PreferSpeed => DefaultPlanner::plan_generic_resize::<f32, f32, 2>(
653                self,
654                source_size,
655                target_size,
656                8,
657            ),
658        }
659    }
660
661    /// Creates a resampling plan for a two-channel grayscale + alpha (`GA`) `f32` image.
662    ///
663    /// The `f32` variant of [`plan_gray_alpha_resampling`]. When `premultiply_alpha` is
664    /// `true` the gray channel is pre-multiplied by alpha before resampling and
665    /// un-multiplied afterward, preventing dark fringing around transparent edges.
666    /// Set it to `false` if the image uses straight alpha or the channels should be
667    /// filtered independently.
668    ///
669    /// The internal accumulator precision is selected automatically based on the scaler's
670    /// [`WorkloadStrategy`]:
671    /// - [`PreferQuality`](WorkloadStrategy::PreferQuality) — accumulates in `f64` for
672    ///   maximum numerical accuracy.
673    /// - [`PreferSpeed`](WorkloadStrategy::PreferSpeed) — accumulates in `f32` for
674    ///   faster throughput at a small precision cost.
675    ///
676    /// # Arguments
677    ///
678    /// - `source_size` — Dimensions of the input image.
679    /// - `target_size` — Desired dimensions of the output image.
680    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
681    ///
682    /// # Example
683    ///
684    /// ```rust,no_run,ignore
685    /// let plan = scaler.plan_gray_alpha_resampling_f32(source_size, target_size, true)?;
686    /// plan.resample(&store, &mut target_store)?;
687    /// ```
688    pub fn plan_gray_alpha_resampling_f32(
689        &self,
690        source_size: ImageSize,
691        target_size: ImageSize,
692        premultiply_alpha: bool,
693    ) -> Result<Arc<Resampling<f32, 2>>, PicScaleError> {
694        if premultiply_alpha {
695            match self.workload_strategy {
696                WorkloadStrategy::PreferQuality => {
697                    AlphaPlanner::plan_generic_resize_with_alpha::<f32, f64, 2>(
698                        self,
699                        source_size,
700                        target_size,
701                        8,
702                        premultiply_alpha,
703                    )
704                }
705                WorkloadStrategy::PreferSpeed => {
706                    AlphaPlanner::plan_generic_resize_with_alpha::<f32, f32, 2>(
707                        self,
708                        source_size,
709                        target_size,
710                        8,
711                        premultiply_alpha,
712                    )
713                }
714            }
715        } else {
716            self.plan_cbcr_resampling_f32(source_size, target_size)
717        }
718    }
719
720    /// Creates a resampling plan for a three-channel RGB `f32` image.
721    ///
722    /// The `f32` variant of [`plan_rgb_resampling`], suitable for HDR or linear-light
723    /// color images where full floating-point precision is required. All three channels
724    /// are resampled independently with no alpha relationship.
725    ///
726    /// The internal accumulator precision is selected automatically based on the scaler's
727    /// [`WorkloadStrategy`]:
728    /// - [`PreferQuality`](WorkloadStrategy::PreferQuality) — accumulates in `f64` for
729    ///   maximum numerical accuracy.
730    /// - [`PreferSpeed`](WorkloadStrategy::PreferSpeed) — accumulates in `f32` for
731    ///   faster throughput at a small precision cost.
732    ///
733    /// # Arguments
734    ///
735    /// - `source_size` — Dimensions of the input image.
736    /// - `target_size` — Desired dimensions of the output image.
737    ///
738    /// # Example
739    ///
740    /// ```rust,no_run,ignore
741    /// let plan = scaler.plan_rgb_resampling_f32(source_size, target_size)?;
742    /// plan.resample(&store, &mut target_store)?;
743    /// ```
744    pub fn plan_rgb_resampling_f32(
745        &self,
746        source_size: ImageSize,
747        target_size: ImageSize,
748    ) -> Result<Arc<Resampling<f32, 3>>, PicScaleError> {
749        match self.workload_strategy {
750            WorkloadStrategy::PreferQuality => DefaultPlanner::plan_generic_resize::<f32, f64, 3>(
751                self,
752                source_size,
753                target_size,
754                8,
755            ),
756            WorkloadStrategy::PreferSpeed => DefaultPlanner::plan_generic_resize::<f32, f32, 3>(
757                self,
758                source_size,
759                target_size,
760                8,
761            ),
762        }
763    }
764
765    /// Creates a resampling plan for a four-channel RGBA `f32` image.
766    ///
767    /// The `f32` variant of [`plan_rgba_resampling`]. When `premultiply_alpha` is `true`
768    /// the RGB channels are pre-multiplied by alpha before resampling and un-multiplied
769    /// afterward, preventing dark halos around semi-transparent edges. Set it to `false`
770    /// if the image uses straight alpha or the channels should be filtered independently.
771    ///
772    /// The internal accumulator precision is selected automatically based on the scaler's
773    /// [`WorkloadStrategy`]:
774    /// - [`PreferQuality`](WorkloadStrategy::PreferQuality) — accumulates in `f64` for
775    ///   maximum numerical accuracy.
776    /// - [`PreferSpeed`](WorkloadStrategy::PreferSpeed) — accumulates in `f32` for
777    ///   faster throughput at a small precision cost.
778    ///
779    /// # Arguments
780    ///
781    /// - `source_size` — Dimensions of the input image.
782    /// - `target_size` — Desired dimensions of the output image.
783    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
784    ///
785    /// # Example
786    ///
787    /// ```rust,no_run,ignore
788    /// let plan = scaler.plan_rgba_resampling_f32(source_size, target_size, true)?;
789    /// plan.resample(&store, &mut target_store)?;
790    /// ```
791    pub fn plan_rgba_resampling_f32(
792        &self,
793        source_size: ImageSize,
794        target_size: ImageSize,
795        premultiply_alpha: bool,
796    ) -> Result<Arc<Resampling<f32, 4>>, PicScaleError> {
797        if premultiply_alpha {
798            match self.workload_strategy {
799                WorkloadStrategy::PreferQuality => {
800                    AlphaPlanner::plan_generic_resize_with_alpha::<f32, f64, 4>(
801                        self,
802                        source_size,
803                        target_size,
804                        8,
805                        premultiply_alpha,
806                    )
807                }
808                WorkloadStrategy::PreferSpeed => {
809                    AlphaPlanner::plan_generic_resize_with_alpha::<f32, f32, 4>(
810                        self,
811                        source_size,
812                        target_size,
813                        8,
814                        premultiply_alpha,
815                    )
816                }
817            }
818        } else {
819            match self.workload_strategy {
820                WorkloadStrategy::PreferQuality => {
821                    DefaultPlanner::plan_generic_resize::<f32, f64, 4>(
822                        self,
823                        source_size,
824                        target_size,
825                        8,
826                    )
827                }
828                WorkloadStrategy::PreferSpeed => {
829                    DefaultPlanner::plan_generic_resize::<f32, f32, 4>(
830                        self,
831                        source_size,
832                        target_size,
833                        8,
834                    )
835                }
836            }
837        }
838    }
839
840    pub fn set_threading_policy(&mut self, threading_policy: ThreadingPolicy) -> Self {
841        self.threading_policy = threading_policy;
842        *self
843    }
844}
845
846impl Scaler {
847    pub(crate) fn plan_resize_ar30<const AR30_ORDER: usize>(
848        &self,
849        ar30_type: Rgb30,
850        source_size: ImageSize,
851        destination_size: ImageSize,
852    ) -> Result<Arc<Resampling<u8, 4>>, PicScaleError> {
853        if self.function == ResamplingFunction::Nearest {
854            return Ok(Arc::new(ResampleNearestPlan {
855                source_size,
856                target_size: destination_size,
857                threading_policy: self.threading_policy,
858                _phantom_data: PhantomData,
859            }));
860        }
861        let inner_plan = self.plan_rgb_resampling16(source_size, destination_size, 10)?;
862        let mut _decomposer: Arc<dyn Ar30Destructuring + Send + Sync> =
863            Arc::new(Ar30DestructuringImpl::<AR30_ORDER> { rgb30: ar30_type });
864        #[cfg(all(target_arch = "x86_64", feature = "avx"))]
865        {
866            if std::arch::is_x86_feature_detected!("avx2") {
867                use crate::avx2::{
868                    avx_column_handler_fixed_point_ar30, avx_convolve_horizontal_rgba_rows_4_ar30,
869                    avx_convolve_horizontal_rgba_rows_ar30,
870                };
871                use crate::plan::{HorizontalFiltering, VerticalFiltering};
872                let should_do_horizontal = source_size.width != destination_size.width;
873                let should_do_vertical = source_size.height != destination_size.height;
874
875                let vertical_plan = if should_do_vertical {
876                    let vertical_filters = u8::make_weights(
877                        self.function,
878                        source_size.height,
879                        destination_size.height,
880                    )?;
881                    Some(Arc::new(VerticalFiltering {
882                        filter_row: match ar30_type {
883                            Rgb30::Ar30 => {
884                                avx_column_handler_fixed_point_ar30::<
885                                    { Rgb30::Ar30 as usize },
886                                    AR30_ORDER,
887                                >
888                            }
889                            Rgb30::Ra30 => {
890                                avx_column_handler_fixed_point_ar30::<
891                                    { Rgb30::Ra30 as usize },
892                                    AR30_ORDER,
893                                >
894                            }
895                        },
896                        filter_weights: vertical_filters
897                            .numerical_approximation_i16::<{ crate::support::PRECISION }>(0),
898                        threading_policy: self.threading_policy,
899                    }))
900                } else {
901                    None
902                };
903
904                let horizontal_plan = if should_do_horizontal {
905                    let horizontal_filters =
906                        u8::make_weights(self.function, source_size.width, destination_size.width)?;
907                    Some(Arc::new(HorizontalFiltering {
908                        filter_row: match ar30_type {
909                            Rgb30::Ar30 => {
910                                avx_convolve_horizontal_rgba_rows_ar30::<
911                                    { Rgb30::Ar30 as usize },
912                                    AR30_ORDER,
913                                >
914                            }
915                            Rgb30::Ra30 => {
916                                avx_convolve_horizontal_rgba_rows_ar30::<
917                                    { Rgb30::Ra30 as usize },
918                                    AR30_ORDER,
919                                >
920                            }
921                        },
922                        filter_4_rows: Some(match ar30_type {
923                            Rgb30::Ar30 => {
924                                avx_convolve_horizontal_rgba_rows_4_ar30::<
925                                    { Rgb30::Ar30 as usize },
926                                    AR30_ORDER,
927                                >
928                            }
929                            Rgb30::Ra30 => {
930                                avx_convolve_horizontal_rgba_rows_4_ar30::<
931                                    { Rgb30::Ra30 as usize },
932                                    AR30_ORDER,
933                                >
934                            }
935                        }),
936                        threading_policy: self.threading_policy,
937                        filter_weights: horizontal_filters
938                            .numerical_approximation_i16::<{ crate::support::PRECISION }>(0),
939                    }))
940                } else {
941                    None
942                };
943
944                return Ok(match (should_do_vertical, should_do_horizontal) {
945                    (true, true) => {
946                        let v = vertical_plan.expect("Should have vertical plan");
947                        let h = horizontal_plan.expect("Should have horizontal plan");
948                        let trampoline = Arc::new(TrampolineFiltering {
949                            horizontal_filter: h.clone(),
950                            vertical_filter: v.clone(),
951                            source_size,
952                            target_size: destination_size,
953                        });
954                        Arc::new(BothAxesConvolvePlan {
955                            source_size,
956                            target_size: destination_size,
957                            horizontal_filter: h,
958                            vertical_filter: v,
959                            trampoline_filter: trampoline,
960                            threading_policy: self.threading_policy,
961                        })
962                    }
963                    (true, false) => Arc::new(VerticalConvolvePlan {
964                        source_size,
965                        target_size: destination_size,
966                        vertical_filter: vertical_plan.expect("Should have vertical plan"),
967                    }),
968                    (false, true) => Arc::new(HorizontalConvolvePlan {
969                        source_size,
970                        target_size: destination_size,
971                        horizontal_filter: horizontal_plan.expect("Should have horizontal plan"),
972                    }),
973                    (false, false) => Arc::new(NoopPlan {
974                        source_size,
975                        target_size: destination_size,
976                        _phantom: PhantomData,
977                    }),
978                });
979            }
980        }
981        Ok(Arc::new(Ar30Plan {
982            source_size,
983            target_size: destination_size,
984            inner_filter: inner_plan,
985            decomposer: _decomposer,
986        }))
987    }
988
989    /// Creates a resampling plan for an AR30 (`RGBA2101010`) packed 10-bit image.
990    ///
991    /// AR30 stores each pixel as a 32-bit word with 10 bits per RGB channel and a
992    /// 2-bit alpha.
993    ///
994    /// The `order` argument controls the byte layout of the packed word:
995    /// - [`Ar30ByteOrder::Host`] — native endianness of the current platform.
996    /// - [`Ar30ByteOrder::Network`] — big-endian (network) byte order.
997    ///
998    /// # Arguments
999    ///
1000    /// - `source_size` — Dimensions of the input image.
1001    /// - `target_size` — Desired dimensions of the output image.
1002    /// - `order` — Byte order of the packed AR30 words.
1003    ///
1004    /// # Example
1005    ///
1006    /// ```rust,no_run,ignore
1007    /// let plan = scaler.plan_ar30_resampling(source_size, target_size, Ar30ByteOrder::Host)?;
1008    /// plan.resample(&store, &mut target_store)?;
1009    /// ```
1010    pub fn plan_ar30_resampling(
1011        &self,
1012        source_size: ImageSize,
1013        target_size: ImageSize,
1014        order: Ar30ByteOrder,
1015    ) -> Result<Arc<Resampling<u8, 4>>, PicScaleError> {
1016        match order {
1017            Ar30ByteOrder::Host => self.plan_resize_ar30::<{ Ar30ByteOrder::Host as usize }>(
1018                Rgb30::Ar30,
1019                source_size,
1020                target_size,
1021            ),
1022            Ar30ByteOrder::Network => self.plan_resize_ar30::<{ Ar30ByteOrder::Network as usize }>(
1023                Rgb30::Ar30,
1024                source_size,
1025                target_size,
1026            ),
1027        }
1028    }
1029
1030    /// Creates a resampling plan for an RA30 (`RGBA1010102`) packed 10-bit image.
1031    ///
1032    /// RA30 stores each pixel as a 32-bit word with 10 bits per RGB channel and a
1033    /// 2-bit alpha in the least-significant position.
1034    ///
1035    /// The `order` argument controls the byte layout of the packed word:
1036    /// - [`Ar30ByteOrder::Host`] — native endianness of the current platform.
1037    /// - [`Ar30ByteOrder::Network`] — big-endian (network) byte order.
1038    ///
1039    /// # Arguments
1040    ///
1041    /// - `source_size` — Dimensions of the input image.
1042    /// - `target_size` — Desired dimensions of the output image.
1043    /// - `order` — Byte order of the packed RA30 words.
1044    ///
1045    /// # Example
1046    ///
1047    /// ```rust,no_run,ignore
1048    /// let plan = scaler.resize_ra30(source_size, target_size, Ar30ByteOrder::Host)?;
1049    /// plan.resample(&store, &mut target_store)?;
1050    /// ```
1051    pub fn plan_ra30_resampling(
1052        &self,
1053        source_size: ImageSize,
1054        target_size: ImageSize,
1055        order: Ar30ByteOrder,
1056    ) -> Result<Arc<Resampling<u8, 4>>, PicScaleError> {
1057        match order {
1058            Ar30ByteOrder::Host => self.plan_resize_ar30::<{ Ar30ByteOrder::Host as usize }>(
1059                Rgb30::Ra30,
1060                source_size,
1061                target_size,
1062            ),
1063            Ar30ByteOrder::Network => self.plan_resize_ar30::<{ Ar30ByteOrder::Network as usize }>(
1064                Rgb30::Ra30,
1065                source_size,
1066                target_size,
1067            ),
1068        }
1069    }
1070}
1071
1072/// Declares default scaling options
1073#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Default)]
1074pub struct ScalingOptions {
1075    pub resampling_function: ResamplingFunction,
1076    pub premultiply_alpha: bool,
1077    pub threading_policy: ThreadingPolicy,
1078}
1079
1080/// Generic trait for [ImageStore] to implement abstract scaling.
1081pub trait ImageStoreScaling<'b, T, const N: usize>
1082where
1083    T: Clone + Copy + Debug,
1084{
1085    fn scale(
1086        &self,
1087        store: &mut ImageStoreMut<'b, T, N>,
1088        options: ScalingOptions,
1089    ) -> Result<(), PicScaleError>;
1090}
1091
1092macro_rules! def_image_scaling_alpha {
1093    ($clazz: ident, $fx_type: ident, $cn: expr) => {
1094        impl<'b> ImageStoreScaling<'b, $fx_type, $cn> for $clazz<'b> {
1095            fn scale(
1096                &self,
1097                store: &mut ImageStoreMut<'b, $fx_type, $cn>,
1098                options: ScalingOptions,
1099            ) -> Result<(), PicScaleError> {
1100                let scaler = Scaler::new(options.resampling_function)
1101                    .set_threading_policy(options.threading_policy);
1102                let plan = AlphaPlanner::plan_generic_resize_with_alpha::<$fx_type, f32, $cn>(
1103                    &scaler,
1104                    self.size(),
1105                    store.size(),
1106                    store.bit_depth,
1107                    options.premultiply_alpha,
1108                )?;
1109                plan.resample(self, store)
1110            }
1111        }
1112    };
1113}
1114
1115macro_rules! def_image_scaling {
1116    ($clazz: ident, $fx_type: ident, $cn: expr) => {
1117        impl<'b> ImageStoreScaling<'b, $fx_type, $cn> for $clazz<'b> {
1118            fn scale(
1119                &self,
1120                store: &mut ImageStoreMut<'b, $fx_type, $cn>,
1121                options: ScalingOptions,
1122            ) -> Result<(), PicScaleError> {
1123                let scaler = Scaler::new(options.resampling_function)
1124                    .set_threading_policy(options.threading_policy);
1125                let plan = DefaultPlanner::plan_generic_resize::<$fx_type, f32, $cn>(
1126                    &scaler,
1127                    self.size(),
1128                    store.size(),
1129                    store.bit_depth,
1130                )?;
1131                plan.resample(self, store)
1132            }
1133        }
1134    };
1135}
1136
1137def_image_scaling_alpha!(Rgba8ImageStore, u8, 4);
1138def_image_scaling!(Rgb8ImageStore, u8, 3);
1139def_image_scaling!(CbCr8ImageStore, u8, 2);
1140def_image_scaling!(Planar8ImageStore, u8, 1);
1141def_image_scaling!(Planar16ImageStore, u16, 1);
1142def_image_scaling!(CbCr16ImageStore, u16, 2);
1143def_image_scaling!(Rgb16ImageStore, u16, 3);
1144def_image_scaling_alpha!(Rgba16ImageStore, u16, 4);
1145def_image_scaling!(PlanarF32ImageStore, f32, 1);
1146def_image_scaling!(CbCrF32ImageStore, f32, 2);
1147def_image_scaling!(RgbF32ImageStore, f32, 3);
1148def_image_scaling_alpha!(RgbaF32ImageStore, f32, 4);
1149
1150#[cfg(test)]
1151mod tests {
1152    use super::*;
1153
1154    macro_rules! check_rgba8 {
1155        ($dst: expr, $image_width: expr, $max: expr) => {
1156            {
1157                for (y, row) in $dst.chunks_exact($image_width * 4).enumerate() {
1158                    for (i, dst) in row.chunks_exact(4).enumerate() {
1159                        let diff0 = (dst[0] as i32 - 124).abs();
1160                        let diff1 = (dst[1] as i32 - 41).abs();
1161                        let diff2 = (dst[2] as i32 - 99).abs();
1162                        let diff3 = (dst[3] as i32 - 77).abs();
1163                        assert!(
1164                            diff0 < $max,
1165                            "Diff for channel 0 is expected < {}, but it was {diff0}, at (y: {y}, x: {i})",
1166                            $max
1167                        );
1168                        assert!(
1169                            diff1 < $max,
1170                            "Diff for channel 1 is expected < {}, but it was {diff1}, at (y: {y}, x: {i})",
1171                            $max
1172                        );
1173                        assert!(
1174                            diff2 < $max,
1175                            "Diff for channel 2 is expected < {}, but it was {diff2}, at (y: {y}, x: {i})",
1176                            $max
1177                        );
1178                        assert!(
1179                            diff3 < $max,
1180                            "Diff for channel 3 is expected < {}, but it was {diff3}, at (y: {y}, x: {i})",
1181                            $max
1182                        );
1183                    }
1184                }
1185            }
1186        };
1187    }
1188
1189    macro_rules! check_rgb16 {
1190        ($dst: expr, $image_width: expr, $max: expr) => {
1191            {
1192                for (y, row) in $dst.chunks_exact($image_width * 3).enumerate() {
1193                    for (i, dst) in row.chunks_exact(3).enumerate() {
1194                        let diff0 = (dst[0] as i32 - 124).abs();
1195                        let diff1 = (dst[1] as i32 - 41).abs();
1196                        let diff2 = (dst[2] as i32 - 99).abs();
1197                        assert!(
1198                            diff0 < $max,
1199                            "Diff for channel 0 is expected < {}, but it was {diff0}, at (y: {y}, x: {i})",
1200                            $max
1201                        );
1202                        assert!(
1203                            diff1 < $max,
1204                            "Diff for channel 1 is expected < {}, but it was {diff1}, at (y: {y}, x: {i})",
1205                            $max
1206                        );
1207                        assert!(
1208                            diff2 < $max,
1209                            "Diff for channel 2 is expected < {}, but it was {diff2}, at (y: {y}, x: {i})",
1210                            $max
1211                        );
1212                    }
1213                }
1214            }
1215        };
1216    }
1217
1218    #[test]
1219    fn check_rgba8_resizing_vertical() {
1220        let image_width = 255;
1221        let image_height = 512;
1222        const CN: usize = 4;
1223        let mut image = vec![0u8; image_height * image_width * CN];
1224        for dst in image.chunks_exact_mut(4) {
1225            dst[0] = 124;
1226            dst[1] = 41;
1227            dst[2] = 99;
1228            dst[3] = 77;
1229        }
1230        let scaler =
1231            Scaler::new(ResamplingFunction::Bilinear).set_threading_policy(ThreadingPolicy::Single);
1232        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1233        let mut target_store = ImageStoreMut::alloc(image_width, image_height / 2);
1234        let planned = scaler
1235            .plan_rgba_resampling(src_store.size(), target_store.size(), false)
1236            .unwrap();
1237        planned.resample(&src_store, &mut target_store).unwrap();
1238        let target_data = target_store.buffer.borrow();
1239        check_rgba8!(target_data, image_width, 34);
1240    }
1241
1242    #[test]
1243    fn check_rgba8_resizing_both() {
1244        let image_width = 255;
1245        let image_height = 512;
1246        const CN: usize = 4;
1247        let mut image = vec![0u8; image_height * image_width * CN];
1248        for dst in image.chunks_exact_mut(4) {
1249            dst[0] = 124;
1250            dst[1] = 41;
1251            dst[2] = 99;
1252            dst[3] = 77;
1253        }
1254        image[3] = 78;
1255        let mut scaler = Scaler::new(ResamplingFunction::Bilinear);
1256        scaler.set_threading_policy(ThreadingPolicy::Single);
1257        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1258        let mut target_store = ImageStoreMut::alloc(image_width / 2, image_height / 2);
1259        let planned = scaler
1260            .plan_rgba_resampling(src_store.size(), target_store.size(), false)
1261            .unwrap();
1262        planned.resample(&src_store, &mut target_store).unwrap();
1263        let target_data = target_store.buffer.borrow();
1264        check_rgba8!(target_data, image_width, 34);
1265    }
1266
1267    #[test]
1268    fn check_rgba8_resizing_alpha() {
1269        let image_width = 255;
1270        let image_height = 512;
1271        const CN: usize = 4;
1272        let mut image = vec![0u8; image_height * image_width * CN];
1273        for dst in image.chunks_exact_mut(4) {
1274            dst[0] = 124;
1275            dst[1] = 41;
1276            dst[2] = 99;
1277            dst[3] = 77;
1278        }
1279        image[3] = 78;
1280        let scaler =
1281            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1282        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1283        let mut target_store = ImageStoreMut::alloc(image_width / 2, image_height / 2);
1284        let planned = scaler
1285            .plan_rgba_resampling(src_store.size(), target_store.size(), true)
1286            .unwrap();
1287        planned.resample(&src_store, &mut target_store).unwrap();
1288        let target_data = target_store.buffer.borrow();
1289        check_rgba8!(target_data, image_width, 160);
1290    }
1291
1292    #[test]
1293    fn check_rgb8_resizing_vertical() {
1294        let image_width = 255;
1295        let image_height = 512;
1296        const CN: usize = 3;
1297        let mut image = vec![0u8; image_height * image_width * CN];
1298        for dst in image.chunks_exact_mut(3) {
1299            dst[0] = 124;
1300            dst[1] = 41;
1301            dst[2] = 99;
1302        }
1303        let mut scaler = Scaler::new(ResamplingFunction::Bilinear);
1304        scaler.set_threading_policy(ThreadingPolicy::Single);
1305        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1306        let mut target_store = ImageStoreMut::alloc(image_width, image_height / 2);
1307        let planned = scaler
1308            .plan_rgb_resampling(src_store.size(), target_store.size())
1309            .unwrap();
1310        planned.resample(&src_store, &mut target_store).unwrap();
1311        let target_data = target_store.buffer.borrow();
1312
1313        check_rgb16!(target_data, image_width, 85);
1314    }
1315
1316    #[test]
1317    fn check_rgb8_resizing_vertical_threading() {
1318        let image_width = 255;
1319        let image_height = 512;
1320        const CN: usize = 3;
1321        let mut image = vec![0u8; image_height * image_width * CN];
1322        for dst in image.chunks_exact_mut(3) {
1323            dst[0] = 124;
1324            dst[1] = 41;
1325            dst[2] = 99;
1326        }
1327        let mut scaler = Scaler::new(ResamplingFunction::Bilinear);
1328        scaler.set_threading_policy(ThreadingPolicy::Adaptive);
1329        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1330        let mut target_store = ImageStoreMut::alloc(image_width, image_height / 2);
1331        let planned = scaler
1332            .plan_rgb_resampling(src_store.size(), target_store.size())
1333            .unwrap();
1334        planned.resample(&src_store, &mut target_store).unwrap();
1335        let target_data = target_store.buffer.borrow();
1336
1337        check_rgb16!(target_data, image_width, 85);
1338    }
1339
1340    #[test]
1341    fn check_rgba10_resizing_vertical() {
1342        let image_width = 8;
1343        let image_height = 8;
1344        const CN: usize = 4;
1345        let mut image = vec![0u16; image_height * image_width * CN];
1346        for dst in image.chunks_exact_mut(4) {
1347            dst[0] = 124;
1348            dst[1] = 41;
1349            dst[2] = 99;
1350            dst[3] = 77;
1351        }
1352        image[3] = 78;
1353        let scaler =
1354            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1355        let mut src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1356        src_store.bit_depth = 10;
1357        let mut target_store = ImageStoreMut::alloc_with_depth(image_width, image_height / 2, 10);
1358        let planned = scaler
1359            .plan_rgba_resampling16(src_store.size(), target_store.size(), true, 10)
1360            .unwrap();
1361        planned.resample(&src_store, &mut target_store).unwrap();
1362        let target_data = target_store.buffer.borrow();
1363
1364        check_rgba8!(target_data, image_width, 60);
1365    }
1366
1367    #[test]
1368    fn check_rgb10_resizing_vertical() {
1369        let image_width = 8;
1370        let image_height = 4;
1371        const CN: usize = 3;
1372        let mut image = vec![0; image_height * image_width * CN];
1373        for dst in image.chunks_exact_mut(3) {
1374            dst[0] = 124;
1375            dst[1] = 41;
1376            dst[2] = 99;
1377        }
1378        let scaler =
1379            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1380        let mut src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1381        src_store.bit_depth = 10;
1382        let mut target_store = ImageStoreMut::alloc_with_depth(image_width, image_height / 2, 10);
1383        let planned = scaler
1384            .plan_rgb_resampling16(src_store.size(), target_store.size(), 10)
1385            .unwrap();
1386        planned.resample(&src_store, &mut target_store).unwrap();
1387        let target_data = target_store.buffer.borrow();
1388
1389        check_rgb16!(target_data, image_width, 85);
1390    }
1391
1392    #[test]
1393    fn check_rgb10_resizing_vertical_adaptive() {
1394        let image_width = 8;
1395        let image_height = 4;
1396        const CN: usize = 3;
1397        let mut image = vec![0; image_height * image_width * CN];
1398        for dst in image.chunks_exact_mut(3) {
1399            dst[0] = 124;
1400            dst[1] = 41;
1401            dst[2] = 99;
1402        }
1403        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
1404        scaler.set_threading_policy(ThreadingPolicy::Adaptive);
1405        let mut src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1406        src_store.bit_depth = 10;
1407        let mut target_store = ImageStoreMut::alloc_with_depth(image_width, image_height / 2, 10);
1408        let planned = scaler
1409            .plan_rgb_resampling16(src_store.size(), target_store.size(), 10)
1410            .unwrap();
1411        planned.resample(&src_store, &mut target_store).unwrap();
1412        let target_data = target_store.buffer.borrow();
1413
1414        check_rgb16!(target_data, image_width, 85);
1415    }
1416
1417    #[test]
1418    fn check_rgb16_resizing_vertical() {
1419        let image_width = 8;
1420        let image_height = 8;
1421        const CN: usize = 3;
1422        let mut image = vec![164; image_height * image_width * CN];
1423        for dst in image.chunks_exact_mut(3) {
1424            dst[0] = 124;
1425            dst[1] = 41;
1426            dst[2] = 99;
1427        }
1428        let scaler =
1429            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1430        let mut src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1431        src_store.bit_depth = 10;
1432        let mut target_store = ImageStoreMut::alloc_with_depth(image_width, image_height / 2, 16);
1433        let planned = scaler
1434            .plan_rgb_resampling16(src_store.size(), target_store.size(), 16)
1435            .unwrap();
1436        planned.resample(&src_store, &mut target_store).unwrap();
1437        let target_data = target_store.buffer.borrow();
1438
1439        check_rgb16!(target_data, image_width, 100);
1440    }
1441
1442    #[test]
1443    fn check_rgba16_resizing_vertical() {
1444        let image_width = 8;
1445        let image_height = 8;
1446        const CN: usize = 4;
1447        let mut image = vec![0u16; image_height * image_width * CN];
1448        for dst in image.chunks_exact_mut(4) {
1449            dst[0] = 124;
1450            dst[1] = 41;
1451            dst[2] = 99;
1452            dst[3] = 255;
1453        }
1454        let scaler =
1455            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1456        let mut src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1457        src_store.bit_depth = 10;
1458        let mut target_store = ImageStoreMut::alloc_with_depth(image_width, image_height / 2, 16);
1459        let planned = scaler
1460            .plan_rgba_resampling16(src_store.size(), target_store.size(), false, 16)
1461            .unwrap();
1462        planned.resample(&src_store, &mut target_store).unwrap();
1463        let target_data = target_store.buffer.borrow();
1464
1465        check_rgba8!(target_data, image_width, 180);
1466    }
1467
1468    #[test]
1469    fn check_rgba16_resizing_vertical_threading() {
1470        let image_width = 8;
1471        let image_height = 8;
1472        const CN: usize = 4;
1473        let mut image = vec![0u16; image_height * image_width * CN];
1474        for dst in image.chunks_exact_mut(4) {
1475            dst[0] = 124;
1476            dst[1] = 41;
1477            dst[2] = 99;
1478            dst[3] = 255;
1479        }
1480        let scaler = Scaler::new(ResamplingFunction::Lanczos3)
1481            .set_threading_policy(ThreadingPolicy::Adaptive);
1482        let mut src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1483        src_store.bit_depth = 10;
1484        let mut target_store = ImageStoreMut::alloc_with_depth(image_width, image_height / 2, 16);
1485        let planned = scaler
1486            .plan_rgba_resampling16(src_store.size(), target_store.size(), false, 16)
1487            .unwrap();
1488        planned.resample(&src_store, &mut target_store).unwrap();
1489        let target_data = target_store.buffer.borrow();
1490
1491        check_rgba8!(target_data, image_width, 180);
1492    }
1493
1494    #[test]
1495    fn check_rgba8_nearest_vertical() {
1496        let image_width = 255;
1497        let image_height = 512;
1498        const CN: usize = 4;
1499        let mut image = vec![0u8; image_height * image_width * CN];
1500        for dst in image.chunks_exact_mut(4) {
1501            dst[0] = 124;
1502            dst[1] = 41;
1503            dst[2] = 99;
1504            dst[3] = 77;
1505        }
1506        let mut scaler = Scaler::new(ResamplingFunction::Nearest);
1507        scaler.set_threading_policy(ThreadingPolicy::Single);
1508        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1509        let mut target_store = ImageStoreMut::alloc(image_width, image_height / 2);
1510        let planned = scaler
1511            .plan_rgba_resampling(src_store.size(), target_store.size(), false)
1512            .unwrap();
1513        planned.resample(&src_store, &mut target_store).unwrap();
1514        let target_data = target_store.buffer.borrow();
1515
1516        check_rgba8!(target_data, image_width, 80);
1517    }
1518
1519    #[test]
1520    fn check_rgba8_nearest_vertical_threading() {
1521        let image_width = 255;
1522        let image_height = 512;
1523        const CN: usize = 4;
1524        let mut image = vec![0u8; image_height * image_width * CN];
1525        for dst in image.chunks_exact_mut(4) {
1526            dst[0] = 124;
1527            dst[1] = 41;
1528            dst[2] = 99;
1529            dst[3] = 77;
1530        }
1531        let scaler = Scaler::new(ResamplingFunction::Nearest)
1532            .set_threading_policy(ThreadingPolicy::Adaptive);
1533        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
1534        let mut target_store = ImageStoreMut::alloc(image_width, image_height / 2);
1535        let planned = scaler
1536            .plan_rgba_resampling(src_store.size(), target_store.size(), false)
1537            .unwrap();
1538        planned.resample(&src_store, &mut target_store).unwrap();
1539        let target_data = target_store.buffer.borrow();
1540
1541        check_rgba8!(target_data, image_width, 80);
1542    }
1543
1544    #[test]
1545    fn check_plane_s16_10bit_resizing_horizontal() {
1546        let image_width = 8;
1547        let image_height = 1;
1548        const CN: usize = 1;
1549        let mut image = vec![0i16; image_height * image_width * CN];
1550        for (i, px) in image.iter_mut().enumerate() {
1551            *px = (100 + i as i16 * 10).min(511);
1552        }
1553        image[0] = -200;
1554
1555        let scaler =
1556            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1557
1558        let src_store =
1559            ImageStore::<i16, CN>::from_slice(&image, image_width, image_height).unwrap();
1560        let mut target_store =
1561            ImageStoreMut::<i16, CN>::alloc_with_depth(image_width / 2, image_height, 10);
1562
1563        let planned = scaler
1564            .plan_planar_resampling_s16(src_store.size(), target_store.size(), 10)
1565            .unwrap();
1566        planned.resample(&src_store, &mut target_store).unwrap();
1567
1568        let target_data = target_store.buffer.borrow();
1569        // All output pixels must stay within signed 10-bit bounds [-512, 511]
1570        for &px in target_data.iter() {
1571            assert!(
1572                px >= -512 && px <= 511,
1573                "pixel {px} out of 10-bit signed range"
1574            );
1575        }
1576    }
1577
1578    #[test]
1579    fn check_plane_s16_10bit_resizing_vertical() {
1580        let image_width = 8;
1581        let image_height = 8;
1582        const CN: usize = 1;
1583        let mut image = vec![0i16; image_height * image_width * CN];
1584        for px in image.iter_mut() {
1585            *px = 124;
1586        }
1587        image[0] = -200;
1588
1589        let scaler =
1590            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1591
1592        let src_store =
1593            ImageStore::<i16, CN>::from_slice(&image, image_width, image_height).unwrap();
1594        let mut target_store =
1595            ImageStoreMut::<i16, CN>::alloc_with_depth(image_width, image_height / 2, 10);
1596
1597        let planned = scaler
1598            .plan_planar_resampling_s16(src_store.size(), target_store.size(), 10)
1599            .unwrap();
1600        planned.resample(&src_store, &mut target_store).unwrap();
1601
1602        let target_data = target_store.buffer.borrow();
1603        for &px in target_data.iter() {
1604            assert!(
1605                px >= -512 && px <= 511,
1606                "pixel {px} out of 10-bit signed range"
1607            );
1608        }
1609        for &px in target_data.iter().skip(1) {
1610            assert!(
1611                (px - 124).abs() < 30,
1612                "flat region drifted: got {px}, expected ~124"
1613            );
1614        }
1615    }
1616
1617    #[test]
1618    fn check_plane_s16_16bit_resizing_horizontal() {
1619        let image_width = 8;
1620        let image_height = 1;
1621        const CN: usize = 1;
1622        let mut image = vec![0i16; image_height * image_width * CN];
1623        for (i, px) in image.iter_mut().enumerate() {
1624            *px = (1000 + i as i16 * 500).min(i16::MAX);
1625        }
1626        image[0] = i16::MIN;
1627
1628        let scaler =
1629            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1630
1631        let src_store =
1632            ImageStore::<i16, CN>::from_slice(&image, image_width, image_height).unwrap();
1633        let mut target_store =
1634            ImageStoreMut::<i16, CN>::alloc_with_depth(image_width / 2, image_height, 16);
1635
1636        let planned = scaler
1637            .plan_planar_resampling_s16(src_store.size(), target_store.size(), 16)
1638            .unwrap();
1639        planned.resample(&src_store, &mut target_store).unwrap();
1640
1641        let target_data = target_store.buffer.borrow();
1642        for &px in target_data.iter() {
1643            assert!(
1644                px >= i16::MIN && px <= i16::MAX,
1645                "pixel {px} out of 16-bit signed range"
1646            );
1647        }
1648    }
1649
1650    #[test]
1651    fn check_plane_s16_16bit_resizing_vertical() {
1652        let image_width = 8;
1653        let image_height = 8;
1654        const CN: usize = 1;
1655        let mut image = vec![0i16; image_height * image_width * CN];
1656        for px in image.iter_mut() {
1657            *px = 5000;
1658        }
1659
1660        let scaler =
1661            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1662
1663        let src_store =
1664            ImageStore::<i16, CN>::from_slice(&image, image_width, image_height).unwrap();
1665        let mut target_store =
1666            ImageStoreMut::<i16, CN>::alloc_with_depth(image_width, image_height / 2, 16);
1667
1668        let planned = scaler
1669            .plan_planar_resampling_s16(src_store.size(), target_store.size(), 16)
1670            .unwrap();
1671        planned.resample(&src_store, &mut target_store).unwrap();
1672
1673        let target_data = target_store.buffer.borrow();
1674        for &px in target_data.iter() {
1675            assert!(
1676                px >= i16::MIN && px <= i16::MAX,
1677                "pixel {px} out of 16-bit signed range"
1678            );
1679        }
1680        // Flat region check — skip pixel influenced by the outlier at [0]
1681        for &px in target_data.iter().skip(1) {
1682            assert!(
1683                (px as i32 - 5000).abs() < 500,
1684                "flat region drifted: got {px}, expected ~5000"
1685            );
1686        }
1687    }
1688
1689    #[test]
1690    fn check_plane_s16_10bit_both_axes() {
1691        let image_width = 8;
1692        let image_height = 8;
1693        const CN: usize = 1;
1694        let mut image = vec![0i16; image_height * image_width * CN];
1695        for px in image.iter_mut() {
1696            *px = 200;
1697        }
1698        image[0] = -300;
1699
1700        let scaler =
1701            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1702
1703        let src_store =
1704            ImageStore::<i16, CN>::from_slice(&image, image_width, image_height).unwrap();
1705        let mut target_store =
1706            ImageStoreMut::<i16, CN>::alloc_with_depth(image_width / 2, image_height / 2, 10);
1707
1708        let planned = scaler
1709            .plan_planar_resampling_s16(src_store.size(), target_store.size(), 10)
1710            .unwrap();
1711        planned.resample(&src_store, &mut target_store).unwrap();
1712
1713        let target_data = target_store.buffer.borrow();
1714        for &px in target_data.iter() {
1715            assert!(
1716                px >= -512 && px <= 511,
1717                "pixel {px} out of 10-bit signed range"
1718            );
1719        }
1720    }
1721
1722    #[test]
1723    fn check_plane_s16_16bit_both_axes() {
1724        let image_width = 8;
1725        let image_height = 8;
1726        const CN: usize = 1;
1727        let mut image = vec![0i16; image_height * image_width * CN];
1728        for px in image.iter_mut() {
1729            *px = 10000;
1730        }
1731        image[0] = i16::MIN;
1732
1733        let scaler =
1734            Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
1735
1736        let src_store =
1737            ImageStore::<i16, CN>::from_slice(&image, image_width, image_height).unwrap();
1738        let mut target_store =
1739            ImageStoreMut::<i16, CN>::alloc_with_depth(image_width / 2, image_height / 2, 16);
1740
1741        let planned = scaler
1742            .plan_planar_resampling_s16(src_store.size(), target_store.size(), 16)
1743            .unwrap();
1744        planned.resample(&src_store, &mut target_store).unwrap();
1745
1746        let target_data = target_store.buffer.borrow();
1747        for &px in target_data.iter() {
1748            assert!(
1749                px >= i16::MIN && px <= i16::MAX,
1750                "pixel {px} out of 16-bit signed range"
1751            );
1752        }
1753    }
1754}