Skip to main content

fastpack_core/imaging/
scale.rs

1use anyhow::Result;
2use image::{DynamicImage, GenericImageView, imageops::FilterType};
3
4use crate::types::{
5    config::ScaleMode,
6    rect::{Point, Size, SourceRect},
7    sprite::{NinePatch, Sprite},
8};
9
10use super::pixel_art;
11
12/// Produce a copy of `sprite` scaled by `factor` using the given resampling mode.
13///
14/// `factor < 1.0` shrinks; `factor > 1.0` enlarges. When `factor` is exactly
15/// 1.0 the sprite is returned as-is (cloned, no resampling).
16///
17/// Pixel art modes (`Scale2x`, `Scale3x`, `Hq2x`, `Eagle`) first apply their
18/// integer upscaler (2× or 3×) and then resize to the exact target dimensions
19/// with nearest-neighbour when the factor does not align exactly with the
20/// algorithm's native multiplier.
21pub fn scale_sprite(sprite: &Sprite, factor: f32, mode: ScaleMode) -> Result<Sprite> {
22    if (factor - 1.0).abs() < f32::EPSILON {
23        return Ok(sprite.clone());
24    }
25
26    let (src_w, src_h) = sprite.image.dimensions();
27    let target_w = ((src_w as f32 * factor).round() as u32).max(1);
28    let target_h = ((src_h as f32 * factor).round() as u32).max(1);
29
30    let scaled_image = match mode {
31        ScaleMode::Smooth => sprite
32            .image
33            .resize_exact(target_w, target_h, FilterType::Lanczos3),
34        ScaleMode::Fast => sprite
35            .image
36            .resize_exact(target_w, target_h, FilterType::Nearest),
37        ScaleMode::Scale2x => {
38            let up = pixel_art::scale2x(&sprite.image.to_rgba8());
39            resize_to(DynamicImage::ImageRgba8(up), target_w, target_h)
40        }
41        ScaleMode::Scale3x => {
42            let up = pixel_art::scale3x(&sprite.image.to_rgba8());
43            resize_to(DynamicImage::ImageRgba8(up), target_w, target_h)
44        }
45        ScaleMode::Hq2x => {
46            let up = pixel_art::hq2x(&sprite.image.to_rgba8());
47            resize_to(DynamicImage::ImageRgba8(up), target_w, target_h)
48        }
49        ScaleMode::Eagle => {
50            let up = pixel_art::eagle2x(&sprite.image.to_rgba8());
51            resize_to(DynamicImage::ImageRgba8(up), target_w, target_h)
52        }
53    };
54
55    let original_size = Size {
56        w: ((sprite.original_size.w as f32 * factor).round() as u32).max(1),
57        h: ((sprite.original_size.h as f32 * factor).round() as u32).max(1),
58    };
59
60    let trim_rect = sprite.trim_rect.as_ref().map(|tr| SourceRect {
61        x: (tr.x as f32 * factor).round() as i32,
62        y: (tr.y as f32 * factor).round() as i32,
63        w: ((tr.w as f32 * factor).round() as u32).max(1),
64        h: ((tr.h as f32 * factor).round() as u32).max(1),
65    });
66
67    let nine_patch = sprite.nine_patch.map(|np| NinePatch {
68        top: (np.top as f32 * factor).round() as u32,
69        right: (np.right as f32 * factor).round() as u32,
70        bottom: (np.bottom as f32 * factor).round() as u32,
71        left: (np.left as f32 * factor).round() as u32,
72    });
73
74    let polygon = sprite.polygon.as_ref().map(|pts| {
75        pts.iter()
76            .map(|p| Point {
77                x: p.x * factor,
78                y: p.y * factor,
79            })
80            .collect()
81    });
82
83    Ok(Sprite {
84        id: sprite.id.clone(),
85        source_path: sprite.source_path.clone(),
86        image: scaled_image,
87        trim_rect,
88        original_size,
89        polygon,
90        nine_patch,
91        pivot: sprite.pivot,
92        content_hash: sprite.content_hash,
93        extrude: (sprite.extrude as f32 * factor).round() as u32,
94        alias_of: sprite.alias_of.clone(),
95    })
96}
97
98/// Resize `img` to `(w, h)` using nearest-neighbour, or return it unchanged
99/// when the dimensions already match.
100fn resize_to(img: DynamicImage, w: u32, h: u32) -> DynamicImage {
101    if img.width() == w && img.height() == h {
102        img
103    } else {
104        img.resize_exact(w, h, FilterType::Nearest)
105    }
106}