Skip to main content

csp_format/
material.rs

1//! Image material layers (画像素材レイヤー, R10): CSP keeps the original
2//! image (`Layer.ResizableOriginalMipmap`) and its placement
3//! (`Layer.ResizableImageInfo`), and draws the transformed image when it
4//! opens the file; the render mipmap is often not stored.
5//!
6//! `ResizableImageInfo` (big endian, 184 bytes in the corpus):
7//!
8//! ```text
9//! +0   u32 120, 16, 4, 0, 1, 3, 2, 2 (meaning unknown)
10//! +32  u32 original width, height (pixels)
11//! +40  f64 scale x, scale y
12//! +56  f64 0 (rotation, unconfirmed)
13//! +64  f64 centre x, y (layer pixels)
14//! +80  f64 pivot x, y in the original (half its size)
15//! +96  u32 3, 1, 0, 1, 0, 1 (meaning unknown)
16//! +120 f64 × 8: where the original's corners go, (0, 0), (w, 0), (0, h),
17//!      (w, h), in layer pixels (add `LayerOffsetX/Y` for the canvas)
18//! ```
19//!
20//! The corners define the transform, so scaling, rotation, flips and free
21//! (perspective) transforms are all drawn the same way: each output pixel
22//! is mapped back into the original with the homography of the four
23//! corners and sampled bilinearly (the edge is hard: outside the image
24//! is transparent).
25
26use crate::binary::{be_f64_at, be_u32_at};
27use crate::error::{Result, bail};
28use crate::image::RgbaImage;
29
30/// The placement of an image material.
31#[derive(Debug, Clone, PartialEq, serde::Serialize)]
32pub struct Placement {
33    /// Width of the original image in pixels.
34    pub width: u32,
35    /// Height of the original image in pixels.
36    pub height: u32,
37    /// Scale (x, y) as stored; the corners are what is drawn.
38    pub scale: (f64, f64),
39    /// Centre in layer pixels.
40    pub center: (f64, f64),
41    /// Where (0, 0), (w, 0), (0, h) and (w, h) of the original go.
42    pub corners: [(f64, f64); 4],
43}
44
45const CORNERS_AT: usize = 120;
46
47impl Placement {
48    /// Parses `ResizableImageInfo`.
49    pub fn parse(data: &[u8]) -> Result<Self> {
50        if data.len() < CORNERS_AT + 64 {
51            bail!("ResizableImageInfo of {} bytes, expected at least 184", data.len());
52        }
53        let f = |at: usize| be_f64_at(data, at);
54        let corners = [0, 1, 2, 3].map(|i| (f(CORNERS_AT + 16 * i), f(CORNERS_AT + 16 * i + 8)));
55        let placement = Placement {
56            width: be_u32_at(data, 32),
57            height: be_u32_at(data, 36),
58            scale: (f(40), f(48)),
59            center: (f(64), f(72)),
60            corners,
61        };
62        if placement.width == 0
63            || placement.height == 0
64            || corners.iter().any(|(x, y)| !x.is_finite() || !y.is_finite())
65        {
66            bail!("ResizableImageInfo without a usable size or corners");
67        }
68        Ok(placement)
69    }
70
71    /// The homography mapping canvas points back into the original image
72    /// (after moving the corners by `offset`), or None when the corners are
73    /// degenerate.
74    fn inverse(&self, offset: (f64, f64)) -> Option<[f64; 9]> {
75        let (w, h) = (self.width as f64, self.height as f64);
76        let source = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)];
77        let target = self.corners.map(|(x, y)| (x + offset.0, y + offset.1));
78        homography(target, source)
79    }
80
81    /// Draw `original` (the original image read at `level_scale` of its
82    /// full size) placed on a `width` × `height` output at `scale` of the
83    /// canvas, with the corners moved by the layer's `offset`.
84    pub fn draw(
85        &self,
86        original: &RgbaImage,
87        level_scale: f64,
88        offset: (f64, f64),
89        width: u32,
90        height: u32,
91        scale: f64,
92    ) -> Option<RgbaImage> {
93        self.draw_region(original, level_scale, offset, (0, 0, width, height), scale)
94    }
95
96    /// The output pixels the placed image can cover, `(x0, y0, x1, y1)`, at
97    /// `scale` of the canvas (not clipped to any output).
98    pub fn bounds(&self, offset: (f64, f64), scale: f64) -> (i64, i64, i64, i64) {
99        let xs = self.corners.map(|c| (c.0 + offset.0) * scale);
100        let ys = self.corners.map(|c| (c.1 + offset.1) * scale);
101        let min = |v: [f64; 4]| v.iter().copied().fold(f64::INFINITY, f64::min).floor() as i64;
102        let max = |v: [f64; 4]| v.iter().copied().fold(f64::NEG_INFINITY, f64::max).ceil() as i64;
103        (min(xs), min(ys), max(xs), max(ys))
104    }
105
106    /// [`Placement::draw`] for the part `(left, top, width, height)` of the
107    /// output only: the result is `width` × `height` with its top-left at
108    /// output pixel (`left`, `top`).
109    pub fn draw_region(
110        &self,
111        original: &RgbaImage,
112        level_scale: f64,
113        offset: (f64, f64),
114        region: (u32, u32, u32, u32),
115        scale: f64,
116    ) -> Option<RgbaImage> {
117        let (left, top, width, height) = region;
118        let m = self.inverse(offset)?;
119        let premultiplied: Vec<[f32; 4]> = original
120            .pixels
121            .as_chunks::<4>()
122            .0
123            .iter()
124            .map(|p| {
125                let a = p[3] as f32 / 255.0;
126                [p[0] as f32 * a, p[1] as f32 * a, p[2] as f32 * a, p[3] as f32]
127            })
128            .collect();
129        let (ow, oh) = (original.width as i64, original.height as i64);
130        // Inside the image, samples near its edge repeat the edge pixels.
131        let texel = |x: i64, y: i64| premultiplied[(y.clamp(0, oh - 1) * ow + x.clamp(0, ow - 1)) as usize];
132        // Only the outputs inside the corners' bounding box can be covered.
133        let (bx0, by0, bx1, by1) = self.bounds(offset, scale);
134        let clip = |value: i64, start: u32, size: u32| (value - start as i64).clamp(0, size as i64) as u32;
135        let (x0, x1) = (clip(bx0, left, width), clip(bx1, left, width));
136        let (y0, y1) = (clip(by0, top, height), clip(by1, top, height));
137        let mut out = RgbaImage::new(width, height);
138        for y in y0..y1 {
139            for x in x0..x1 {
140                let cx = ((x + left) as f64 + 0.5) / scale;
141                let cy = ((y + top) as f64 + 0.5) / scale;
142                let d = m[6] * cx + m[7] * cy + m[8];
143                if d.abs() < 1e-12 {
144                    continue;
145                }
146                // Original pixel coordinates, then the level's.
147                let sx = (m[0] * cx + m[1] * cy + m[2]) / d * level_scale - 0.5;
148                let sy = (m[3] * cx + m[4] * cy + m[5]) / d * level_scale - 0.5;
149                if sx < -0.5 || sy < -0.5 || sx >= ow as f64 - 0.5 || sy >= oh as f64 - 0.5 {
150                    continue;
151                }
152                let (fx, fy) = (sx.floor(), sy.floor());
153                let (tx, ty) = ((sx - fx) as f32, (sy - fy) as f32);
154                let (ix, iy) = (fx as i64, fy as i64);
155                let (a, b, c, e) = (texel(ix, iy), texel(ix + 1, iy), texel(ix, iy + 1), texel(ix + 1, iy + 1));
156                let mut p = [0f32; 4];
157                for k in 0..4 {
158                    let top = a[k] + (b[k] - a[k]) * tx;
159                    let bottom = c[k] + (e[k] - c[k]) * tx;
160                    p[k] = top + (bottom - top) * ty;
161                }
162                if p[3] <= 0.0 {
163                    continue;
164                }
165                let alpha = p[3] / 255.0;
166                let at = (y as usize * width as usize + x as usize) * 4;
167                let straight = [p[0] / alpha, p[1] / alpha, p[2] / alpha, p[3]];
168                for (target, value) in out.pixels[at..at + 4].iter_mut().zip(straight) {
169                    *target = value.round().clamp(0.0, 255.0) as u8;
170                }
171            }
172        }
173        Some(out)
174    }
175
176    /// How much the original is scaled on the canvas (the larger side).
177    pub fn magnification(&self) -> f64 {
178        let length = |a: (f64, f64), b: (f64, f64)| ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt();
179        let x = length(self.corners[0], self.corners[1]) / self.width as f64;
180        let y = length(self.corners[0], self.corners[2]) / self.height as f64;
181        x.max(y)
182    }
183}
184
185/// The 3 × 3 homography (row major, last entry 1) taking each `from` point
186/// to the matching `to` point.
187fn homography(from: [(f64, f64); 4], to: [(f64, f64); 4]) -> Option<[f64; 9]> {
188    let mut a = [[0f64; 9]; 8];
189    for (i, ((x, y), (u, v))) in from.iter().zip(to.iter()).enumerate() {
190        a[2 * i] = [*x, *y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, *u];
191        a[2 * i + 1] = [0.0, 0.0, 0.0, *x, *y, 1.0, -v * x, -v * y, *v];
192    }
193    // Gaussian elimination with partial pivoting.
194    for col in 0..8 {
195        let pivot = (col..8).max_by(|&r, &s| a[r][col].abs().total_cmp(&a[s][col].abs()))?;
196        if a[pivot][col].abs() < 1e-12 {
197            return None;
198        }
199        a.swap(col, pivot);
200        let pivot_row = a[col];
201        for (index, row) in a.iter_mut().enumerate() {
202            if index != col {
203                let factor = row[col] / pivot_row[col];
204                for (value, pivot_value) in row.iter_mut().zip(pivot_row).skip(col) {
205                    *value -= factor * pivot_value;
206                }
207            }
208        }
209    }
210    let mut h = [0f64; 9];
211    for (i, (value, row)) in h.iter_mut().zip(&a).enumerate() {
212        *value = row[8] / row[i];
213    }
214    h[8] = 1.0;
215    Some(h)
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    fn blob(width: u32, height: u32, corners: [(f64, f64); 4]) -> Vec<u8> {
223        let mut data = vec![0u8; 184];
224        data[32..36].copy_from_slice(&width.to_be_bytes());
225        data[36..40].copy_from_slice(&height.to_be_bytes());
226        for (i, (x, y)) in corners.iter().enumerate() {
227            data[120 + 16 * i..128 + 16 * i].copy_from_slice(&x.to_be_bytes());
228            data[128 + 16 * i..136 + 16 * i].copy_from_slice(&y.to_be_bytes());
229        }
230        data
231    }
232
233    #[test]
234    fn scaled_and_moved_image_lands_on_its_corners() {
235        let mut original = RgbaImage::new(4, 2);
236        original.fill([200, 10, 20, 255]);
237        // Twice the size, top-left at (10, 6) in layer pixels, layer moved by (2, 1).
238        let placement = Placement::parse(&blob(4, 2, [(10.0, 6.0), (18.0, 6.0), (10.0, 10.0), (18.0, 10.0)])).unwrap();
239        assert!((placement.magnification() - 2.0).abs() < 1e-9);
240        let image = placement.draw(&original, 1.0, (2.0, 1.0), 32, 16, 1.0).unwrap();
241        assert_eq!(image.pixel(13, 9), [200, 10, 20, 255]);
242        assert_eq!(image.pixel(11, 6)[3], 0);
243        assert_eq!(image.pixel(20, 9)[3], 0);
244        assert_eq!(image.pixel(19, 10), [200, 10, 20, 255]);
245    }
246
247    #[test]
248    fn flipped_corners_mirror_the_image() {
249        let mut original = RgbaImage::new(2, 1);
250        original.set_pixel(0, 0, [255, 0, 0, 255]);
251        original.set_pixel(1, 0, [0, 0, 255, 255]);
252        let placement = Placement::parse(&blob(2, 1, [(20.0, 0.0), (0.0, 0.0), (20.0, 10.0), (0.0, 10.0)])).unwrap();
253        let image = placement.draw(&original, 1.0, (0.0, 0.0), 20, 10, 1.0).unwrap();
254        assert_eq!(image.pixel(2, 5), [0, 0, 255, 255]);
255        assert_eq!(image.pixel(17, 5), [255, 0, 0, 255]);
256    }
257
258    #[test]
259    fn a_region_is_the_same_part_of_the_whole() {
260        let mut original = RgbaImage::new(3, 3);
261        for (i, pixel) in original.pixels.chunks_mut(4).enumerate() {
262            pixel.copy_from_slice(&[i as u8 * 20, 100, 200 - i as u8 * 10, 255]);
263        }
264        let placement = Placement::parse(&blob(3, 3, [(3.5, 2.0), (15.0, 4.0), (2.0, 13.0), (14.0, 16.5)])).unwrap();
265        let whole = placement.draw(&original, 1.0, (1.0, 1.0), 24, 20, 1.0).unwrap();
266        let part = placement.draw_region(&original, 1.0, (1.0, 1.0), (5, 4, 8, 9), 1.0).unwrap();
267        for y in 0..9 {
268            for x in 0..8 {
269                assert_eq!(part.pixel(x, y), whole.pixel(x + 5, y + 4));
270            }
271        }
272        assert_eq!(placement.bounds((1.0, 1.0), 1.0), (3, 3, 16, 18));
273    }
274}