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