Skip to main content

csp_format/
render.rs

1//! Compositing the stored layer pixels into a canvas image (R10).
2//!
3//! The layers are drawn bottom to top (the first child of a folder is its
4//! bottom layer) with visibility, opacity, blending mode, clipping, layer
5//! masks and folders. Vector, gradient and correction layers, which CSP
6//! draws when it opens a file, are drawn from their settings; text and 3D
7//! layers without stored pixels are skipped and counted in the report.
8//!
9//! Rendering uses the mipmap level nearest above the wanted scale, so a
10//! render at the size of `CanvasPreview` decodes only that level. Colours
11//! are blended as premultiplied `f32` in the stored (sRGB) values. The
12//! mapping of `LayerComposite` values to modes is in [`BlendMode`]; its
13//! confidence is recorded in `docs/spec/`.
14//!
15//! Animated files are drawn at a frame ([`render_frame`], R17): only the
16//! cel an animation folder shows at that frame, keyframed layers moved by
17//! their transform and cameras applied to their folders. [`render`] draws
18//! the current frame as CSP's canvas shows it, without the camera (a
19//! keyframed layer's `LayerRenderMipmap` is not stored: CSP rebuilds it
20//! from `TimeLineOriginalMipmap`). Light-table reference layers are never
21//! drawn.
22
23use std::collections::BTreeMap;
24
25use crate::animation::{Affine, Animation, FrameState, LAYER_TYPE_LIGHT_TABLE};
26use crate::clip::ClipFile;
27use crate::correction::Correction;
28use crate::effect::{Border, LayerEffects, draw_border};
29use crate::error::Result;
30use crate::gradient::Gradient;
31use crate::image::{GrayImage, RgbaImage, luma};
32use crate::material::Placement;
33use crate::model::{Layer, LayerKind, LayerNode};
34use crate::offscreen::Offscreen;
35use crate::stroke::{
36    draw_balloons, draw_balloons_at_canvas_size, draw_layer, draw_layer_at_canvas_size, frame_mask, layer_vectors,
37};
38use crate::text::read_blocks;
39use crate::tone::Tone;
40
41/// Blending modes, numbered as in `LayerComposite`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
43#[non_exhaustive]
44pub enum BlendMode {
45    /// 通常 (0).
46    Normal,
47    /// 比較(暗) (1).
48    Darken,
49    /// 乗算 (2).
50    Multiply,
51    /// 焼き込みカラー (3).
52    ColorBurn,
53    /// 焼き込み(リニア) (4).
54    LinearBurn,
55    /// 減算 (5).
56    Subtract,
57    /// カラー比較(暗) (6).
58    DarkerColor,
59    /// 比較(明) (7).
60    Lighten,
61    /// スクリーン (8).
62    Screen,
63    /// 覆い焼きカラー (9).
64    ColorDodge,
65    /// 覆い焼き(発光) (10).
66    GlowDodge,
67    /// 加算 (11).
68    Add,
69    /// 加算(発光) (12).
70    AddGlow,
71    /// カラー比較(明) (13).
72    LighterColor,
73    /// オーバーレイ (14).
74    Overlay,
75    /// ソフトライト (15).
76    SoftLight,
77    /// ハードライト (16).
78    HardLight,
79    /// 差の絶対値 (21).
80    Difference,
81    /// 除外 (22).
82    Exclusion,
83    /// ビビッドライト (17).
84    VividLight,
85    /// リニアライト (18).
86    LinearLight,
87    /// ピンライト (19).
88    PinLight,
89    /// ハードミックス (20).
90    HardMix,
91    /// 色相 (23).
92    Hue,
93    /// 彩度 (24).
94    Saturation,
95    /// カラー (25).
96    Color,
97    /// 輝度 (26).
98    Brightness,
99    /// 除算 (36).
100    Divide,
101    /// Folders only: the children blend with what is below the folder.
102    PassThrough,
103}
104
105impl BlendMode {
106    /// The mode of a `LayerComposite` value (None for unknown values).
107    ///
108    /// Confirmed with a file made in CSP EX that has one layer per mode
109    /// (`docs/validation/V6-made-files.md`). The numbers do not follow the
110    /// order of CSP's mode list: darker / lighter colour, difference,
111    /// exclusion and divide were added later.
112    pub fn from_code(code: i64) -> Option<Self> {
113        use BlendMode::*;
114        Some(match code {
115            0 => Normal,
116            1 => Darken,
117            2 => Multiply,
118            3 => ColorBurn,
119            4 => LinearBurn,
120            5 => Subtract,
121            6 => DarkerColor,
122            7 => Lighten,
123            8 => Screen,
124            9 => ColorDodge,
125            10 => GlowDodge,
126            11 => Add,
127            12 => AddGlow,
128            13 => LighterColor,
129            14 => Overlay,
130            15 => SoftLight,
131            16 => HardLight,
132            17 => VividLight,
133            18 => LinearLight,
134            19 => PinLight,
135            20 => HardMix,
136            21 => Difference,
137            22 => Exclusion,
138            23 => Hue,
139            24 => Saturation,
140            25 => Color,
141            26 => Brightness,
142            30 => PassThrough,
143            36 => Divide,
144            _ => return None,
145        })
146    }
147
148    /// The `LayerComposite` value of the mode.
149    pub fn code(self) -> i64 {
150        (0..=36).find(|code| Self::from_code(*code) == Some(self)).unwrap_or(0)
151    }
152
153    /// 覆い焼き(発光) and 加算(発光).
154    fn is_glow(self) -> bool {
155        matches!(self, BlendMode::GlowDodge | BlendMode::AddGlow)
156    }
157
158    /// Separable blend of one channel (backdrop `b`, source `s`, both 0..1).
159    fn channel(self, b: f32, s: f32) -> f32 {
160        use BlendMode::*;
161        let burn = |b: f32, s: f32| {
162            if b >= 1.0 {
163                1.0
164            } else if s <= 0.0 {
165                0.0
166            } else {
167                1.0 - ((1.0 - b) / s).min(1.0)
168            }
169        };
170        let dodge = |b: f32, s: f32| {
171            if b <= 0.0 {
172                0.0
173            } else if s >= 1.0 {
174                1.0
175            } else {
176                (b / (1.0 - s)).min(1.0)
177            }
178        };
179        let hard = |b: f32, s: f32| if s <= 0.5 { b * 2.0 * s } else { b + (2.0 * s - 1.0) - b * (2.0 * s - 1.0) };
180        let vivid = |b: f32, s: f32| if s <= 0.5 { burn(b, 2.0 * s) } else { dodge(b, 2.0 * s - 1.0) };
181        match self {
182            Normal | PassThrough => s,
183            Darken => b.min(s),
184            Multiply => b * s,
185            ColorBurn => burn(b, s),
186            LinearBurn => (b + s - 1.0).max(0.0),
187            Subtract => (b - s).max(0.0),
188            Lighten => b.max(s),
189            Screen => b + s - b * s,
190            ColorDodge | GlowDodge => dodge(b, s),
191            Add | AddGlow => (b + s).min(1.0),
192            Overlay => hard(s, b),
193            SoftLight => {
194                if s <= 0.5 {
195                    b - (1.0 - 2.0 * s) * b * (1.0 - b)
196                } else {
197                    let d = if b <= 0.25 { ((16.0 * b - 12.0) * b + 4.0) * b } else { b.sqrt() };
198                    b + (2.0 * s - 1.0) * (d - b)
199                }
200            }
201            HardLight => hard(b, s),
202            Difference => (b - s).abs(),
203            Exclusion => b + s - 2.0 * b * s,
204            VividLight => vivid(b, s),
205            LinearLight => (b + 2.0 * s - 1.0).clamp(0.0, 1.0),
206            PinLight => {
207                if s <= 0.5 {
208                    b.min(2.0 * s)
209                } else {
210                    b.max(2.0 * s - 1.0)
211                }
212            }
213            HardMix => {
214                // In 8-bit units: b + s > 255, and at exactly 255 only when
215                // s < 128 (measured in CSP, docs/validation/V6-made-files.md).
216                let (b8, s8) = ((b * 255.0).round(), (s * 255.0).round());
217                if b8 + s8 > 255.0 || (b8 + s8 == 255.0 && s8 < 128.0) { 1.0 } else { 0.0 }
218            }
219            Divide => {
220                if s <= 0.0 {
221                    if b > 0.0 { 1.0 } else { 0.0 }
222                } else {
223                    (b / s).min(1.0)
224                }
225            }
226            DarkerColor | LighterColor | Hue | Saturation | Color | Brightness => s,
227        }
228    }
229
230    /// Blend a colour (non-premultiplied RGB).
231    fn blend(self, b: [f32; 3], s: [f32; 3]) -> [f32; 3] {
232        use BlendMode::*;
233        match self {
234            DarkerColor => {
235                if lum8(s) <= lum8(b) {
236                    s
237                } else {
238                    b
239                }
240            }
241            LighterColor => {
242                if lum8(s) >= lum8(b) {
243                    s
244                } else {
245                    b
246                }
247            }
248            Hue => set_lum(set_sat(s, sat(b)), lum(b)),
249            Saturation => set_lum(set_sat(b, sat(s)), lum(b)),
250            Color => set_lum(s, lum(b)),
251            Brightness => set_lum(b, lum(s)),
252            _ => [self.channel(b[0], s[0]), self.channel(b[1], s[1]), self.channel(b[2], s[2])],
253        }
254    }
255}
256
257/// The luminance darker / lighter colour compare: 0.3 R + 0.59 G + 0.11 B
258/// rounded to 8 bits, ties going to the source (exact on CSP's output).
259fn lum8(c: [f32; 3]) -> f32 {
260    (lum(c) * 255.0).round()
261}
262
263fn lum(c: [f32; 3]) -> f32 {
264    0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
265}
266
267fn clip_color(c: [f32; 3]) -> [f32; 3] {
268    let l = lum(c);
269    let n = c[0].min(c[1]).min(c[2]);
270    let x = c[0].max(c[1]).max(c[2]);
271    let mut out = c;
272    for v in &mut out {
273        if n < 0.0 {
274            *v = l + (*v - l) * l / (l - n);
275        }
276        if x > 1.0 {
277            *v = l + (*v - l) * (1.0 - l) / (x - l);
278        }
279    }
280    out
281}
282
283fn set_lum(c: [f32; 3], l: f32) -> [f32; 3] {
284    let d = l - lum(c);
285    clip_color([c[0] + d, c[1] + d, c[2] + d])
286}
287
288fn sat(c: [f32; 3]) -> f32 {
289    c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
290}
291
292fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
293    let max = c[0].max(c[1]).max(c[2]);
294    let min = c[0].min(c[1]).min(c[2]);
295    if max <= min {
296        return [0.0; 3];
297    }
298    c.map(|v| (v - min) * s / (max - min))
299}
300
301/// A premultiplied RGBA float image.
302#[derive(Debug, Clone)]
303struct Buffer {
304    width: usize,
305    height: usize,
306    data: Vec<[f32; 4]>,
307}
308
309impl Buffer {
310    fn new(width: usize, height: usize) -> Self {
311        Buffer { width, height, data: vec![[0.0; 4]; width * height] }
312    }
313
314    fn to_image(&self) -> RgbaImage {
315        let mut image = RgbaImage::new(self.width as u32, self.height as u32);
316        for (i, p) in self.data.iter().enumerate() {
317            let a = p[3].clamp(0.0, 1.0);
318            let un = |v: f32| if a > 0.0 { (v / a * 255.0).round().clamp(0.0, 255.0) as u8 } else { 0 };
319            image.pixels[i * 4..i * 4 + 4].copy_from_slice(&[un(p[0]), un(p[1]), un(p[2]), (a * 255.0).round() as u8]);
320        }
321        image
322    }
323
324    /// Composite `source` (premultiplied) onto self with a mode; `coverage`
325    /// scales the source alpha per pixel (opacity × mask × clipping base).
326    fn composite(&mut self, source: &Buffer, mode: BlendMode, coverage: impl Fn(usize) -> f32) {
327        self.composite_with(source, mode, coverage, false);
328    }
329
330    /// Like [`Buffer::composite`], but keep self's alpha: the source shows
331    /// only where self has pixels (clipping onto a base layer).
332    fn composite_atop(&mut self, source: &Buffer, mode: BlendMode, coverage: impl Fn(usize) -> f32) {
333        self.composite_with(source, mode, coverage, true);
334    }
335
336    fn composite_with(&mut self, source: &Buffer, mode: BlendMode, coverage: impl Fn(usize) -> f32, atop: bool) {
337        for i in 0..self.data.len() {
338            let s = source.data[i];
339            let k = coverage(i);
340            if k <= 0.0 || s[3] <= 0.0 {
341                continue;
342            }
343            let sa = s[3] * k;
344            let b = self.data[i];
345            let ba = b[3];
346            let sc = [s[0] / s[3], s[1] / s[3], s[2] / s[3]];
347            let bc = if ba > 0.0 { [b[0] / ba, b[1] / ba, b[2] / ba] } else { [0.0; 3] };
348            let oa = if atop { ba } else { sa + ba * (1.0 - sa) };
349            // Source where the backdrop is empty (not when clipping).
350            let alone = if atop { 0.0 } else { sa * (1.0 - ba) };
351            let mut out = [0.0; 4];
352            if mode.is_glow() {
353                // Glow modes blend the backdrop with the source colour times
354                // its alpha and are not then mixed by alpha (measured in CSP).
355                let mixed = mode.blend(bc, sc.map(|v| v * sa));
356                for c in 0..3 {
357                    out[c] = alone * sc[c] + ba * mixed[c];
358                }
359            } else {
360                let mixed = mode.blend(bc, sc);
361                for c in 0..3 {
362                    // W3C compositing: source over, with the blended colour where both exist.
363                    out[c] = alone * sc[c] + sa * ba * mixed[c] + (1.0 - sa) * b[c];
364                }
365            }
366            out[3] = oa;
367            self.data[i] = out;
368        }
369    }
370}
371
372/// Options for [`render`].
373#[derive(Debug, Clone, Copy)]
374pub struct RenderOptions {
375    /// Output scale (1.0 = canvas size).
376    pub scale: f64,
377    /// Output size; by default the canvas size × `scale` rounded down.
378    /// CSP's half-size levels round down at each step, so a quarter-size
379    /// image can be a pixel larger than `size × 0.25` rounded down.
380    pub size: Option<(u32, u32)>,
381}
382
383impl Default for RenderOptions {
384    fn default() -> Self {
385        RenderOptions { scale: 1.0, size: None }
386    }
387}
388
389/// What the renderer did, by layer count.
390#[derive(Debug, Clone, Default, serde::Serialize)]
391pub struct RenderReport {
392    /// Width of the image in pixels.
393    pub width: u32,
394    /// Height of the image in pixels.
395    pub height: u32,
396    /// Layers drawn.
397    pub drawn: usize,
398    /// Layers skipped because they or a folder above them are hidden.
399    pub hidden: usize,
400    /// Layers drawn without pixels of their own (no stored pixels), by kind.
401    pub without_pixels: BTreeMap<String, usize>,
402    /// `LayerComposite` values not in [`BlendMode`] (drawn as normal).
403    pub unknown_blend: BTreeMap<i64, usize>,
404    /// Modes used, by `LayerComposite` value.
405    pub blend_modes: BTreeMap<i64, usize>,
406    /// Layers clipped to the layer below.
407    pub clipped: usize,
408    /// Layers drawn through a mask.
409    pub masked: usize,
410    /// Layer effects drawn, by section name (`EffectEdge`, …).
411    pub effects: BTreeMap<String, usize>,
412    /// Switched-on effects not drawn yet (tone, watercolour edge, line extraction).
413    pub effects_skipped: BTreeMap<String, usize>,
414    /// Vector layers drawn from their vector data (see [`crate::stroke`]).
415    pub vector_layers: usize,
416    /// Text layers drawn from CSP's rendered text blocks.
417    pub text_layers: usize,
418    /// Image material layers drawn from their original image (see [`crate::material`]).
419    pub material_layers: usize,
420    /// Gradient layers drawn from `GradationFillInfo` (see [`crate::gradient`]),
421    /// and how many of them only approximately (the 知覚的 mix).
422    pub gradient_layers: usize,
423    /// Gradient layers drawn only approximately (see `gradient_layers`).
424    pub gradients_approximate: usize,
425    /// Tones with settings whose drawing was not measured.
426    pub tones_approximate: usize,
427    /// Correction layers applied (see [`crate::correction`]), and how many
428    /// of them only approximately.
429    pub correction_layers: usize,
430    /// Correction layers applied only approximately (see `correction_layers`).
431    pub corrections_approximate: usize,
432}
433
434struct Renderer<'a> {
435    file: &'a ClipFile,
436    scale: f64,
437    width: usize,
438    height: usize,
439    report: RenderReport,
440    frame: &'a FrameState,
441    /// Inside a 2D camera folder: output canvas position → the canvas
442    /// position shown there. Pixel layers are then sampled once through
443    /// the camera (and their keyframe transform).
444    camera: Option<Affine>,
445}
446
447/// Render the canvas. The image has the canvas size × `options.scale`.
448/// An animated file is drawn at its current frame (see the module notes).
449pub fn render(file: &ClipFile, options: RenderOptions) -> Result<(RgbaImage, RenderReport)> {
450    let mut state = match Animation::read(file) {
451        Ok(Some(animation)) => {
452            let frame = animation.current_timeline().map_or(0.0, |t| t.current_frame);
453            animation.frame_state(file, animation.current, frame).unwrap_or_default()
454        }
455        _ => FrameState::default(),
456    };
457    state.cameras.clear();
458    render_frame(file, &state, options)
459}
460
461/// Render the canvas as a frame's state changes it (see
462/// [`Animation::frame_state`]). Crop to [`Animation::frame`] for what an
463/// image-sequence export shows.
464pub fn render_frame(file: &ClipFile, frame: &FrameState, options: RenderOptions) -> Result<(RgbaImage, RenderReport)> {
465    let scale = options.scale.clamp(1e-3, 1.0);
466    let (width, height) = match options.size {
467        Some((width, height)) => (width.max(1) as usize, height.max(1) as usize),
468        None => (
469            ((file.canvas.width_px as f64 * scale).floor() as usize).max(1),
470            ((file.canvas.height_px as f64 * scale).floor() as usize).max(1),
471        ),
472    };
473    let mut renderer = Renderer { file, scale, width, height, report: RenderReport::default(), frame, camera: None };
474    let mut canvas = Buffer::new(width, height);
475    renderer.children(&file.layers.root, &mut canvas)?;
476    let mut report = renderer.report;
477    report.width = width as u32;
478    report.height = height as u32;
479    Ok((canvas.to_image(), report))
480}
481
482/// Render at the size of the stored `CanvasPreview`.
483pub fn render_like_preview(file: &ClipFile) -> Result<Option<(RgbaImage, RenderReport, RgbaImage)>> {
484    let Some(preview) = file.preview()? else { return Ok(None) };
485    let scale = preview.width as f64 / file.canvas.width_px.max(1) as f64;
486    let (mut image, report) = render(file, RenderOptions { scale, size: Some((preview.width, preview.height)) })?;
487    if (image.width, image.height) != (preview.width, preview.height) {
488        image = resample(&image, preview.width, preview.height);
489    }
490    Ok(Some((image, report, preview)))
491}
492
493fn resample(image: &RgbaImage, width: u32, height: u32) -> RgbaImage {
494    if image.width >= width && image.height >= height {
495        return image.resize_area(width, height);
496    }
497    // Nearest neighbour for the (one pixel) enlargements rounding causes.
498    let mut out = RgbaImage::new(width, height);
499    for y in 0..height {
500        for x in 0..width {
501            let sx = (x as u64 * image.width as u64 / width as u64) as u32;
502            let sy = (y as u64 * image.height as u64 / height as u64) as u32;
503            out.set_pixel(x, y, image.pixel(sx.min(image.width - 1), sy.min(image.height - 1)));
504        }
505    }
506    out
507}
508
509impl Renderer<'_> {
510    /// Draw the children of `node` (bottom first) onto `target`.
511    ///
512    /// A layer and the clipping layers above it form a group, as in CSP:
513    /// the clipping layers are drawn onto the base layer alone (keeping its
514    /// alpha), and the group is then drawn with the base layer's mode,
515    /// opacity and mask (measured in CSP, `docs/validation/V6-made-files.md`).
516    /// A pass-through folder or a correction layer as the base has no image
517    /// of its own; its clipping layers are drawn onto the backdrop, masked
518    /// by the base's alpha.
519    fn children(&mut self, node: &LayerNode, target: &mut Buffer) -> Result<()> {
520        let mut group: Option<Group> = None;
521        for child in &node.children {
522            let layer = &child.layer;
523            if !layer.clipping
524                && let Some(done) = group.take()
525            {
526                done.flush(target);
527            }
528            if !layer.visible
529                || layer.layer_type == LAYER_TYPE_LIGHT_TABLE
530                || self.frame.hidden.contains(&layer.main_id)
531            {
532                self.report.hidden += 1;
533                if !layer.clipping {
534                    group = Some(Group::Empty);
535                }
536                continue;
537            }
538            let backdrop = match (&group, layer.clipping) {
539                (Some(Group::Isolated { buffer, .. }), true) => buffer,
540                _ => &*target,
541            };
542            let Some(content) = self.content(child, backdrop)? else {
543                if !layer.clipping {
544                    group = Some(Group::Empty);
545                }
546                continue;
547            };
548            let opacity = match self.frame.transforms.get(&layer.main_id).and_then(|t| t.opacity) {
549                Some(opacity) => opacity as f64 / 256.0,
550                None => layer.opacity_fraction(),
551            };
552            let mut coverage = vec![opacity as f32; self.width * self.height];
553            if layer.mask_enabled
554                && !content.masked
555                && let Some(mask) = self.mask(layer)?
556            {
557                self.report.masked += 1;
558                for (c, m) in coverage.iter_mut().zip(mask) {
559                    *c *= m;
560                }
561            }
562            self.report.drawn += 1;
563            if !layer.clipping {
564                group = Some(match content.mode {
565                    Some(mode) => Group::Isolated { buffer: content.buffer, mode, coverage },
566                    None => {
567                        let alpha = content.buffer.data.iter().zip(&coverage).map(|(p, c)| p[3] * c).collect();
568                        mix(target, &content.buffer, &coverage);
569                        Group::Backdrop { alpha }
570                    }
571                });
572                // A frame folder's frame lines are drawn over its masked
573                // content (layers clipped to the folder are not supported).
574                if layer.kind == LayerKind::FrameFolder
575                    && let Some(lines) = self.frame_lines(layer)?
576                {
577                    if let Some(done) = group.take() {
578                        done.flush(target);
579                    }
580                    target.composite(&lines, BlendMode::Normal, |_| opacity as f32);
581                    group = Some(Group::Empty);
582                }
583                continue;
584            }
585            self.report.clipped += 1;
586            match &mut group {
587                Some(Group::Isolated { buffer, .. }) => match content.mode {
588                    Some(mode) => buffer.composite_atop(&content.buffer, mode, |i| coverage[i]),
589                    None => mix(buffer, &content.buffer, &coverage),
590                },
591                Some(Group::Backdrop { alpha }) => {
592                    for (c, a) in coverage.iter_mut().zip(alpha.iter()) {
593                        *c *= a;
594                    }
595                    match content.mode {
596                        Some(mode) => target.composite(&content.buffer, mode, |i| coverage[i]),
597                        None => mix(target, &content.buffer, &coverage),
598                    }
599                }
600                // No base (or a hidden or empty one): nothing shows.
601                Some(Group::Empty) | None => self.report.drawn -= 1,
602            }
603        }
604        if let Some(done) = group {
605            done.flush(target);
606        }
607        Ok(())
608    }
609
610    /// The layer's own image: pixels, a folder's composite, or None.
611    /// `mode` None means pass-through (the buffer already includes the backdrop).
612    fn content(&mut self, node: &LayerNode, backdrop: &Buffer) -> Result<Option<Content>> {
613        let layer = &node.layer;
614        *self.report.blend_modes.entry(layer.blend).or_default() += 1;
615        let mode = BlendMode::from_code(layer.blend).unwrap_or_else(|| {
616            *self.report.unknown_blend.entry(layer.blend).or_default() += 1;
617            BlendMode::Normal
618        });
619        if layer.kind.is_folder() {
620            if let Some(camera) = self.frame.cameras.get(&layer.main_id).copied()
621                && self.camera.is_none()
622            {
623                // A camera shows its folder's content through its frame.
624                // Each layer is sampled through the camera on its own, as
625                // CSP does (warping the composite blurs keyframed layers
626                // twice); with layer masks inside, the composite is warped.
627                let mode = if mode == BlendMode::PassThrough { BlendMode::Normal } else { mode };
628                let mut masks = false;
629                node.walk(0, &mut |n, _| masks |= n.layer.mask_enabled && n.layer.has_mask());
630                let mut buffer = Buffer::new(self.width, self.height);
631                if masks {
632                    self.children(node, &mut buffer)?;
633                    buffer = self.warp_buffer(&buffer, &camera);
634                } else {
635                    self.camera = Some(camera);
636                    let result = self.children(node, &mut buffer);
637                    self.camera = None;
638                    result?;
639                }
640                return Ok(Some(Content { buffer, mode: Some(mode), masked: false }));
641            }
642            // Pass-through (30) folders blend their children with what is
643            // below; any other mode makes the folder an isolated group.
644            if mode == BlendMode::PassThrough {
645                let mut buffer = backdrop.clone();
646                self.children(node, &mut buffer)?;
647                return Ok(Some(Content { buffer, mode: None, masked: false }));
648            }
649            let mut buffer = Buffer::new(self.width, self.height);
650            self.children(node, &mut buffer)?;
651            return Ok(Some(Content { buffer, mode: Some(mode), masked: false }));
652        }
653        if layer.kind == LayerKind::Paper {
654            // The paper's stored pixels are not its look (older files store
655            // opaque black); CSP draws the paper colour, white when unset.
656            let color =
657                layer.color("DrawColorMain").filter(|_| layer.row.truthy("DrawColorEnable")).unwrap_or([255; 3]);
658            let pixel = [color[0] as f32 / 255.0, color[1] as f32 / 255.0, color[2] as f32 / 255.0, 1.0];
659            let mut buffer = Buffer::new(self.width, self.height);
660            buffer.data.fill(pixel);
661            return Ok(Some(Content { buffer, mode: Some(mode), masked: false }));
662        }
663        if layer.kind == LayerKind::Correction
664            && let Some(correction) = layer.row.blob("FilterLayerInfo").and_then(|data| Correction::parse(data).ok())
665        {
666            // The corrected backdrop, mixed in by opacity and mask like a
667            // pass-through folder.
668            self.report.correction_layers += 1;
669            self.report.corrections_approximate += usize::from(correction.is_approximate());
670            let filter = correction.filter();
671            let mut buffer = backdrop.clone();
672            for pixel in &mut buffer.data {
673                let a = pixel[3];
674                if a <= 0.0 {
675                    continue;
676                }
677                let out = filter.apply([0, 1, 2].map(|c| pixel[c] / a * 255.0));
678                for c in 0..3 {
679                    pixel[c] = out[c] / 255.0 * a;
680                }
681            }
682            return Ok(Some(Content { buffer, mode: None, masked: false }));
683        }
684        // Gradient layers are drawn from their settings, as CSP does when it
685        // opens a file; the pixels some files store are a cache.
686        if layer.kind == LayerKind::Gradient
687            && let Some(mut gradient) = layer.row.blob("GradationFillInfo").and_then(|data| Gradient::parse(data).ok())
688        {
689            // Its points are relative to the layer's offset (a moved layer),
690            // as vector points are.
691            let (dx, dy) = (layer.offset.0 as f64, layer.offset.1 as f64);
692            gradient.start = (gradient.start.0 + dx, gradient.start.1 + dy);
693            gradient.end = (gradient.end.0 + dx, gradient.end.1 + dy);
694            self.report.gradient_layers += 1;
695            self.report.gradients_approximate += usize::from(gradient.is_approximate());
696            let opaque = gradient.fill && gradient.ramp.stops.first().is_some_and(|stop| stop.color[3] >= 1.0)
697                || gradient.ramp.stops.iter().all(|stop| stop.color[3] >= 1.0);
698            let canvas_pixels = self.file.canvas.width_px as u64 * self.file.canvas.height_px as u64;
699            let effects = self.effects(layer);
700            // A border shows only where the layer has holes. Effects need the
701            // full-size image (see `pixels`), which very large canvases
702            // cannot afford: those are drawn without them.
703            let shown = effects.tone.is_some() || effects.border.is_some() && (layer.mask_enabled || !opaque);
704            if shown && canvas_pixels <= EFFECT_PIXELS_MAX {
705                let image = gradient.draw(self.file.canvas.width_px, self.file.canvas.height_px, 1.0);
706                let (buffer, masked) = self.with_effects(layer, image, (0, 0), &effects)?;
707                return Ok(Some(Content { buffer, mode: Some(mode), masked }));
708            }
709            let image = gradient.draw(self.width as u32, self.height as u32, self.scale);
710            return Ok(Some(Content {
711                buffer: self.place(image, (0, 0), self.scale),
712                mode: Some(mode),
713                masked: false,
714            }));
715        }
716        if let Some(camera) = self.camera {
717            let transform = self.frame.transforms.get(&layer.main_id).map(|t| t.map);
718            let plain = layer.kind == LayerKind::Raster && !self.effects(layer).any() && layer_colors(layer).is_none();
719            if transform.is_some() || plain {
720                let map = transform.unwrap_or(Affine::IDENTITY);
721                if let Some(buffer) = self.keyframed(layer, &map, Some(&camera))? {
722                    return Ok(Some(Content { buffer, mode: Some(mode), masked: false }));
723                }
724            }
725            // Other layers are drawn as usual and then seen through the camera.
726            self.camera = None;
727            let content = self.content(node, backdrop);
728            self.camera = Some(camera);
729            return Ok(content?.map(|c| Content { buffer: self.warp_buffer(&c.buffer, &camera), ..c }));
730        }
731        if let Some(transform) = self.frame.transforms.get(&layer.main_id)
732            && let Some(buffer) = self.keyframed(layer, &transform.map, None)?
733        {
734            return Ok(Some(Content { buffer, mode: Some(mode), masked: false }));
735        }
736        let effects = self.effects(layer);
737        if let Some((buffer, masked)) = self.pixels(layer, &effects)? {
738            return Ok(Some(Content { buffer, mode: Some(mode), masked }));
739        }
740        // Layers drawn from other data than pixels. With effects they are
741        // drawn at full size (see `pixels`), unless the canvas is too large.
742        let canvas = (self.file.canvas.width_px, self.file.canvas.height_px);
743        let full = effects.any() && canvas.0 as u64 * canvas.1 as u64 <= EFFECT_PIXELS_MAX;
744        if layer.kind == LayerKind::Text {
745            let text = self.text(layer, full)?;
746            if text.is_some() {
747                self.report.text_layers += 1;
748            }
749            match text {
750                Some(TextImage::Full(image)) => {
751                    let (buffer, masked) = self.with_effects(layer, image, (0, 0), &effects)?;
752                    return Ok(Some(Content { buffer, mode: Some(mode), masked }));
753                }
754                Some(TextImage::Placed(buffer)) => {
755                    return Ok(Some(Content { buffer, mode: Some(mode), masked: false }));
756                }
757                None => {}
758            }
759        }
760        // CSP draws an image material into the layer at the canvas's size and
761        // makes the smaller views by halving that, so drawing it straight at
762        // the preview's size samples it differently (3.7–14 dB worse on the
763        // corpus files checked, V3).
764        if layer.kind == LayerKind::ImageMaterial
765            && !full
766            && let Some((image, origin)) = self.material_at_canvas_size(layer)?
767        {
768            self.report.material_layers += 1;
769            return Ok(Some(Content { buffer: self.place(image, origin, 1.0), mode: Some(mode), masked: false }));
770        }
771        // Vector layers likewise: CSP draws the lines at canvas size, so
772        // drawing them at the preview's size makes thin lines too faint
773        // and thick ones too sharp (V3 section 6).
774        if layer.kind == LayerKind::Vector
775            && !full
776            && self.scale < 1.0
777            && let Some((image, origin)) = draw_layer_at_canvas_size(self.file, layer)?
778        {
779            self.report.vector_layers += 1;
780            return Ok(Some(Content { buffer: self.place(image, origin, 1.0), mode: Some(mode), masked: false }));
781        }
782        let drawn = if layer.kind == LayerKind::ImageMaterial {
783            self.material(layer, full)?.inspect(|_| self.report.material_layers += 1)
784        } else if layer.kind == LayerKind::Vector {
785            let (w, h, scale) =
786                if full { (canvas.0, canvas.1, 1.0) } else { (self.width as u32, self.height as u32, self.scale) };
787            draw_layer(self.file, layer, w, h, scale)?.inspect(|_| self.report.vector_layers += 1)
788        } else {
789            None
790        };
791        if let Some(image) = drawn {
792            let (buffer, masked) = if full {
793                self.with_effects(layer, image, (0, 0), &effects)?
794            } else {
795                (self.place(image, (0, 0), self.scale), false)
796            };
797            return Ok(Some(Content { buffer, mode: Some(mode), masked }));
798        }
799        *self.report.without_pixels.entry(layer.kind.name().to_owned()).or_default() += 1;
800        Ok(None)
801    }
802
803    /// A text layer from the images CSP rendered its text blocks into
804    /// (attribute record 50, see [`crate::text`]): each is the size of the
805    /// block's bounding box and placed at its top-left (layer pixels). The
806    /// text itself is not laid out here. With `full`, the blocks are drawn
807    /// on a canvas-sized image for the effects; otherwise each is placed in
808    /// an output buffer.
809    fn text(&self, layer: &Layer, full: bool) -> Result<Option<TextImage>> {
810        let mut result: Option<TextImage> = None;
811        // The balloons under the text, drawn from the layer's vector objects.
812        let objects: Vec<_> = layer_vectors(self.file, layer)?.into_iter().flat_map(|data| data.objects).collect();
813        // Blocks name their balloon by its number among the objects that
814        // are not tails.
815        let balloons: Vec<_> = objects.iter().filter(|object| !object.is_balloon_tail()).collect();
816        if !objects.is_empty() && !full && self.scale < 1.0 {
817            if let Some((image, origin)) = draw_balloons_at_canvas_size(self.file, layer)? {
818                result = Some(TextImage::Placed(self.place(image, origin, 1.0)));
819            }
820        } else if !objects.is_empty() {
821            let (w, h, scale) = if full {
822                (self.file.canvas.width_px, self.file.canvas.height_px, 1.0)
823            } else {
824                (self.width as u32, self.height as u32, self.scale)
825            };
826            if let Some(image) = draw_balloons(self.file, layer, w, h, scale)? {
827                result = Some(if full {
828                    TextImage::Full(image)
829                } else {
830                    TextImage::Placed(self.place(image, (0, 0), self.scale))
831                });
832            }
833        }
834        let mut seen = Vec::new();
835        for block in read_blocks(&layer.row)? {
836            let (Some(id), Some(bbox)) = (block.attributes.render_offscreen(), block.attributes.bbox()) else {
837                continue;
838            };
839            if id == 0 || seen.contains(&id) {
840                continue;
841            }
842            seen.push(id);
843            let Some(offscreen) = self.file.offscreen(id as i64)? else { continue };
844            if offscreen.data.is_none() {
845                continue;
846            }
847            // A block in a balloon is placed from the centre of the balloon's
848            // bbox (V3 section 5).
849            let anchor = match block.attributes.balloon() {
850                Some(index) => match balloons.get(index as usize - 1) {
851                    Some(object) => {
852                        let b = object.bbox();
853                        ((b[0] as i64 + b[2] as i64).div_euclid(2), (b[1] as i64 + b[3] as i64).div_euclid(2))
854                    }
855                    None => continue,
856                },
857                None => (0, 0),
858            };
859            let origin = (layer.offset.0 + anchor.0 + bbox[0] as i64, layer.offset.1 + anchor.1 + bbox[1] as i64);
860            let image = offscreen.to_rgba()?;
861            match &mut result {
862                None if full => {
863                    let mut canvas = RgbaImage::new(self.file.canvas.width_px, self.file.canvas.height_px);
864                    canvas.draw_over(&image, origin.0, origin.1);
865                    result = Some(TextImage::Full(canvas));
866                }
867                None => result = Some(TextImage::Placed(self.place(image, origin, 1.0))),
868                Some(TextImage::Full(canvas)) => canvas.draw_over(&image, origin.0, origin.1),
869                Some(TextImage::Placed(buffer)) => {
870                    buffer.composite(&self.place(image, origin, 1.0), BlendMode::Normal, |_| 1.0)
871                }
872            }
873        }
874        Ok(result)
875    }
876
877    /// A frame folder's frame lines, drawn from its frames like a vector
878    /// layer when CSP has not stored them (`ComicFrameLineMipmap`).
879    fn frame_lines(&self, layer: &Layer) -> Result<Option<Buffer>> {
880        let stored = layer.row.int("ComicFrameLineMipmap");
881        if stored != 0 && self.level(stored, self.scale)?.is_some_and(|(offscreen, _)| offscreen.data.is_some()) {
882            return Ok(None);
883        }
884        if self.scale < 1.0 {
885            return Ok(
886                draw_layer_at_canvas_size(self.file, layer)?.map(|(image, origin)| self.place(image, origin, 1.0))
887            );
888        }
889        let image = draw_layer(self.file, layer, self.width as u32, self.height as u32, self.scale)?;
890        Ok(image.map(|image| self.place(image, (0, 0), self.scale)))
891    }
892
893    /// An image material drawn from its original image and placement, at
894    /// the output size, or the canvas's when `full` (see [`crate::material`]).
895    fn material(&self, layer: &Layer, full: bool) -> Result<Option<RgbaImage>> {
896        let original = layer.row.int("ResizableOriginalMipmap");
897        let Some(placement) = layer.row.blob("ResizableImageInfo").and_then(|data| Placement::parse(data).ok()) else {
898            return Ok(None);
899        };
900        if original == 0 {
901            return Ok(None);
902        }
903        let (width, height, scale) = if full {
904            (self.file.canvas.width_px, self.file.canvas.height_px, 1.0)
905        } else {
906            (self.width as u32, self.height as u32, self.scale)
907        };
908        let wanted = (scale * placement.magnification()).min(1.0);
909        let Some((offscreen, level_scale)) = self.level(original, wanted)? else { return Ok(None) };
910        if offscreen.data.is_none() {
911            return Ok(None);
912        }
913        let offset = (layer.offset.0 as f64, layer.offset.1 as f64);
914        let image = offscreen.to_rgba()?;
915        Ok(placement.draw(&image, level_scale, offset, width, height, scale))
916    }
917
918    /// An image material drawn at the canvas's size, only over the part it
919    /// covers (a whole canvas-sized buffer per material does not fit in
920    /// memory for large sheets). The part starts on a 256-pixel boundary so
921    /// that halving it keeps the grid of the canvas-sized image.
922    fn material_at_canvas_size(&self, layer: &Layer) -> Result<Option<(RgbaImage, (i64, i64))>> {
923        let original = layer.row.int("ResizableOriginalMipmap");
924        let Some(placement) = layer.row.blob("ResizableImageInfo").and_then(|data| Placement::parse(data).ok()) else {
925            return Ok(None);
926        };
927        if original == 0 {
928            return Ok(None);
929        }
930        let (width, height) = (self.file.canvas.width_px as i64, self.file.canvas.height_px as i64);
931        let offset = (layer.offset.0 as f64, layer.offset.1 as f64);
932        let (x0, y0, x1, y1) = placement.bounds(offset, 1.0);
933        let (x0, y0) = ((x0.clamp(0, width) / 256) * 256, (y0.clamp(0, height) / 256) * 256);
934        let (x1, y1) = (x1.clamp(0, width), y1.clamp(0, height));
935        if x1 <= x0 || y1 <= y0 {
936            return Ok(None);
937        }
938        let wanted = placement.magnification().min(1.0);
939        let Some((offscreen, level_scale)) = self.level(original, wanted)? else { return Ok(None) };
940        if offscreen.data.is_none() {
941            return Ok(None);
942        }
943        let region = (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32);
944        let image = offscreen.to_rgba()?;
945        Ok(placement.draw_region(&image, level_scale, offset, region, 1.0).map(|image| (image, (x0, y0))))
946    }
947
948    /// The mipmap level to read for the output scale: the smallest stored
949    /// level at or above it. CSP often stores only some levels (hidden
950    /// layers only the full size), so levels without data are passed over;
951    /// when none has data the full-size level is returned (its init colour
952    /// still applies). Returns the offscreen and the level's scale.
953    fn level(&self, mipmap: i64, scale: f64) -> Result<Option<(Offscreen, f64)>> {
954        let Some(mipmap) = self.file.mipmap(mipmap)? else { return Ok(None) };
955        let mut levels: Vec<_> = mipmap.levels.iter().filter(|level| level.scale / 100.0 + 1e-9 >= scale).collect();
956        levels.sort_by(|a, b| a.scale.total_cmp(&b.scale));
957        for level in levels {
958            if let Some(offscreen) = self.file.offscreen(level.offscreen)?
959                && offscreen.data.is_some()
960            {
961                return Ok(Some((offscreen, level.scale / 100.0)));
962            }
963        }
964        let Some(first) = mipmap.levels.first() else { return Ok(None) };
965        Ok(self.file.offscreen(first.offscreen)?.map(|offscreen| (offscreen, first.scale / 100.0)))
966    }
967
968    /// A keyframed layer: its untransformed pixels (`TimeLineOriginalMipmap`,
969    /// placed at the layer's pixel origin) moved by `map` (original canvas
970    /// position → canvas position), sampled bilinearly, and through a
971    /// camera (output position → canvas position) when given.
972    fn keyframed(&self, layer: &Layer, map: &Affine, camera: Option<&Affine>) -> Result<Option<Buffer>> {
973        let original = layer.row.int("TimeLineOriginalMipmap");
974        let keyframed = layer.row.truthy("TimeLineLayerKeyFrameEnabled") && original != 0;
975        let mipmap = if keyframed { original } else { layer.render_mipmap };
976        let Some(offscreen) = self.file.mipmap_base(mipmap)? else { return Ok(None) };
977        if offscreen.data.is_none() {
978            return Ok(None);
979        }
980        let Some(mut inverse) = map.inverse() else { return Ok(Some(Buffer::new(self.width, self.height))) };
981        if let Some(camera) = camera {
982            inverse = inverse.then_after(camera);
983        }
984        let image = offscreen.to_rgba()?;
985        let origin = layer.pixel_origin();
986        let (w, h) = (image.width as usize, image.height as usize);
987        let fetch = |x: usize, y: usize| {
988            let p = image.pixel(x as u32, y as u32);
989            let a = p[3] as f32 / 255.0;
990            [p[0] as f32 / 255.0 * a, p[1] as f32 / 255.0 * a, p[2] as f32 / 255.0 * a, a]
991        };
992        let mut buffer = Buffer::new(self.width, self.height);
993        for y in 0..self.height {
994            for x in 0..self.width {
995                let canvas = ((x as f64 + 0.5) / self.scale, (y as f64 + 0.5) / self.scale);
996                let (sx, sy) = inverse.apply(canvas);
997                let (sx, sy) = (sx - origin.0 as f64 - 0.5, sy - origin.1 as f64 - 0.5);
998                buffer.data[y * self.width + x] = bilinear(w, h, &fetch, sx, sy);
999            }
1000        }
1001        Ok(Some(buffer))
1002    }
1003
1004    /// `buffer` seen through a camera: each output position shows the
1005    /// buffer at `map(position)` (canvas coordinates), sampled bilinearly.
1006    fn warp_buffer(&self, buffer: &Buffer, map: &Affine) -> Buffer {
1007        let fetch = |x: usize, y: usize| buffer.data[y * buffer.width + x];
1008        let mut out = Buffer::new(self.width, self.height);
1009        for y in 0..self.height {
1010            for x in 0..self.width {
1011                let canvas = ((x as f64 + 0.5) / self.scale, (y as f64 + 0.5) / self.scale);
1012                let (sx, sy) = map.apply(canvas);
1013                out.data[y * self.width + x] =
1014                    bilinear(buffer.width, buffer.height, &fetch, sx * self.scale - 0.5, sy * self.scale - 0.5);
1015            }
1016        }
1017        out
1018    }
1019
1020    /// Place an image read at `level_scale` with its top-left at canvas
1021    /// `origin` into an output-sized buffer.
1022    fn place(&self, image: RgbaImage, origin: (i64, i64), level_scale: f64) -> Buffer {
1023        let factor = self.scale / level_scale;
1024        let halvings = exact_halvings(factor);
1025        // Output sizes are rounded down from the canvas size, so the scale
1026        // asked for is a little under the level's (826 / 1653 for a half-size
1027        // preview); within the tolerance of `exact_halvings` the level is
1028        // used as it is rather than resampled by a fraction of a pixel.
1029        let image = if (factor - 1.0).abs() < 0.005 {
1030            image
1031        } else if halvings > 0 {
1032            // CSP's mipmap levels: each halves the one above with a 2×2 box
1033            // (sizes rounded down). Following the same steps keeps the
1034            // pixel grid of the stored lower levels.
1035            let mut image = image;
1036            for _ in 0..halvings {
1037                image = image.halve();
1038            }
1039            image
1040        } else {
1041            let w = ((image.width as f64 * factor).round() as u32).max(1);
1042            let h = ((image.height as f64 * factor).round() as u32).max(1);
1043            image.resize_area(w, h)
1044        };
1045        let ox = (origin.0 as f64 * self.scale).round() as i64;
1046        let oy = (origin.1 as f64 * self.scale).round() as i64;
1047        let mut buffer = Buffer::new(self.width, self.height);
1048        for y in 0..image.height as i64 {
1049            let ty = oy + y;
1050            if ty < 0 || ty >= self.height as i64 {
1051                continue;
1052            }
1053            for x in 0..image.width as i64 {
1054                let tx = ox + x;
1055                if tx < 0 || tx >= self.width as i64 {
1056                    continue;
1057                }
1058                let p = image.pixel(x as u32, y as u32);
1059                let a = p[3] as f32 / 255.0;
1060                buffer.data[ty as usize * self.width + tx as usize] =
1061                    [p[0] as f32 / 255.0 * a, p[1] as f32 / 255.0 * a, p[2] as f32 / 255.0 * a, a];
1062            }
1063        }
1064        buffer
1065    }
1066
1067    /// The layer's stored pixels placed in an output buffer, and whether
1068    /// the mask was applied to them already.
1069    fn pixels(&mut self, layer: &Layer, effects: &Effects) -> Result<Option<(Buffer, bool)>> {
1070        if layer.render_mipmap == 0 {
1071            return Ok(None);
1072        }
1073        // Borders and tones are drawn at full size (their widths and screens
1074        // are in canvas pixels), then scaled with the layer.
1075        let scale = if effects.any() { 1.0 } else { self.scale };
1076        let Some((offscreen, level_scale)) = self.level(layer.render_mipmap, scale)? else { return Ok(None) };
1077        if offscreen.data.is_none() {
1078            return Ok(None);
1079        }
1080        if effects.any() {
1081            return Ok(Some(self.with_effects(layer, offscreen.to_rgba()?, layer.pixel_origin(), effects)?));
1082        }
1083        // Halve block by block when the wanted scale is a power of two
1084        // below the level (large canvases read for a preview).
1085        let halvings = exact_halvings(self.scale / level_scale);
1086        let (mut image, level_scale) = if halvings > 0 {
1087            (offscreen.to_rgba_halved(halvings)?, level_scale / 2f64.powi(halvings as i32))
1088        } else {
1089            (offscreen.to_rgba()?, level_scale)
1090        };
1091        if let Some(colors) = layer_colors(layer) {
1092            recolor(&mut image, colors);
1093        }
1094        Ok(Some((self.place(image, layer.pixel_origin(), level_scale), false)))
1095    }
1096
1097    /// Draw the effects of a full-size layer image whose top-left is at
1098    /// canvas `origin`: the tone, then the border. For a border the mask is
1099    /// applied first, so the border follows the masked shape (CSP draws
1100    /// the border of a masked fill layer around the mask's hole), and the
1101    /// result is not masked again.
1102    fn with_effects(
1103        &self,
1104        layer: &Layer,
1105        mut image: RgbaImage,
1106        origin: (i64, i64),
1107        effects: &Effects,
1108    ) -> Result<(Buffer, bool)> {
1109        if let Some(tone) = &effects.tone {
1110            image = tone.draw(&image, origin);
1111        }
1112        if let Some(colors) = layer_colors(layer) {
1113            recolor(&mut image, colors);
1114        }
1115        let Some((border, apply_opacity)) = &effects.border else {
1116            return Ok((self.place(image, origin, 1.0), false));
1117        };
1118        let masked = layer.mask_enabled && self.fold_mask(layer, &mut image, origin)?;
1119        let (bordered, margin) = draw_border(&image, border, *apply_opacity);
1120        let origin = (origin.0 - margin as i64, origin.1 - margin as i64);
1121        Ok((self.place(bordered, origin, 1.0), masked))
1122    }
1123
1124    /// Multiply the alpha of a full-size image at canvas `origin` by the
1125    /// layer's mask. Returns false when the layer has no mask.
1126    fn fold_mask(&self, layer: &Layer, image: &mut RgbaImage, origin: (i64, i64)) -> Result<bool> {
1127        if layer.mask_mipmap == 0 {
1128            return Ok(false);
1129        }
1130        let Some((offscreen, _)) = self.level(layer.mask_mipmap, 1.0)? else { return Ok(false) };
1131        let outside = offscreen.attribute.init_color().map(|init| (init.alpha & 0xff) as u8).unwrap_or(0);
1132        let mask = offscreen.to_alpha()?;
1133        let (mx, my) = layer.mask_origin();
1134        for y in 0..image.height {
1135            for x in 0..image.width {
1136                let (cx, cy) = (origin.0 + x as i64 - mx, origin.1 + y as i64 - my);
1137                let m = if cx >= 0 && cy >= 0 && (cx as u32) < mask.width && (cy as u32) < mask.height {
1138                    mask.pixels[cy as usize * mask.width as usize + cx as usize]
1139                } else {
1140                    outside
1141                };
1142                let i = (y * image.width + x) as usize * 4 + 3;
1143                image.pixels[i] = ((image.pixels[i] as u32 * m as u32 + 127) / 255) as u8;
1144            }
1145        }
1146        Ok(true)
1147    }
1148
1149    /// The layer's switched-on effects that are drawn (border and tone);
1150    /// the others are counted in the report.
1151    fn effects(&mut self, layer: &Layer) -> Effects {
1152        let mut drawn = Effects::default();
1153        if !layer.row.truthy("LayerEffectAttached") {
1154            return drawn;
1155        }
1156        let Some(effects) = layer.row.blob("LayerEffectInfo").and_then(|blob| LayerEffects::parse(blob).ok()) else {
1157            return drawn;
1158        };
1159        drawn.tone = effects.section("EffectTone").and_then(Tone::parse);
1160        drawn.border = effects
1161            .border()
1162            .filter(|border| border.enabled && border.width > 0.0)
1163            .map(|border| (border, effects.apply_opacity()));
1164        for name in effects.enabled() {
1165            let shown = match name {
1166                "EffectEdge" => drawn.border.is_some(),
1167                "EffectTone" => drawn.tone.is_some(),
1168                _ => false,
1169            };
1170            let counts = if shown { &mut self.report.effects } else { &mut self.report.effects_skipped };
1171            *counts.entry(name.to_owned()).or_default() += 1;
1172        }
1173        if drawn.tone.is_some_and(|tone| tone.is_approximate()) {
1174            self.report.tones_approximate += 1;
1175        }
1176        drawn
1177    }
1178
1179    /// The mask as coverage 0..1 per output pixel (outside the mask image:
1180    /// the mask's init value, usually hidden).
1181    fn mask(&self, layer: &Layer) -> Result<Option<Vec<f32>>> {
1182        if layer.mask_mipmap == 0 {
1183            return Ok(None);
1184        }
1185        let Some((offscreen, level_scale)) = self.level(layer.mask_mipmap, self.scale)? else { return Ok(None) };
1186        // CSP does not store a frame folder's mask: it is drawn from the frames.
1187        if layer.kind == LayerKind::FrameFolder
1188            && offscreen.data.is_none()
1189            && let Some(mask) = frame_mask(self.file, layer, self.width as u32, self.height as u32, self.scale)?
1190        {
1191            return Ok(Some(mask));
1192        }
1193        let outside = offscreen.attribute.init_color().map(|init| (init.alpha & 0xff) as f32 / 255.0).unwrap_or(0.0);
1194        let halvings = exact_halvings(self.scale / level_scale);
1195        let (image, level_scale) = if halvings > 0 {
1196            (offscreen.to_mask_rgba_halved(halvings)?, level_scale / 2f64.powi(halvings as i32))
1197        } else {
1198            (gray_to_rgba(&offscreen.to_alpha()?), level_scale)
1199        };
1200        // Inside the placed image alpha is 1 (fractional on resampled edges).
1201        let placed = self.place(image, layer.mask_origin(), level_scale);
1202        Ok(Some(placed.data.iter().map(|p| p[0] + outside * (1.0 - p[3])).collect()))
1203    }
1204}
1205
1206/// The layer colour (レイヤーカラー, `DrawColorEnable`): the main and sub
1207/// colours (white when the sub colour is unset). Not for the paper, whose
1208/// colour is drawn from the same columns.
1209fn layer_colors(layer: &Layer) -> Option<([u8; 3], [u8; 3])> {
1210    if layer.kind == LayerKind::Paper || !layer.row.truthy("DrawColorEnable") {
1211        return None;
1212    }
1213    Some((layer.color("DrawColorMain")?, layer.color("DrawColorSub").unwrap_or([255; 3])))
1214}
1215
1216/// Show dark parts in the main colour and light parts in the sub colour,
1217/// mixed by luminance (black is all main, white all sub); alpha is kept.
1218fn recolor(image: &mut RgbaImage, (main, sub): ([u8; 3], [u8; 3])) {
1219    for p in image.pixels.as_chunks_mut::<4>().0 {
1220        if p[3] == 0 {
1221            continue;
1222        }
1223        let light = luma(p[0], p[1], p[2]) as u32;
1224        for c in 0..3 {
1225            p[c] = ((main[c] as u32 * (255 - light) + sub[c] as u32 * light + 127) / 255) as u8;
1226        }
1227    }
1228}
1229
1230/// A text layer's blocks: on a canvas-sized image, or placed in an output
1231/// buffer.
1232enum TextImage {
1233    Full(RgbaImage),
1234    Placed(Buffer),
1235}
1236
1237/// The effects a layer is drawn with.
1238#[derive(Default)]
1239struct Effects {
1240    /// The border and whether it follows the layer's alpha.
1241    border: Option<(Border, bool)>,
1242    tone: Option<Tone>,
1243}
1244
1245impl Effects {
1246    fn any(&self) -> bool {
1247        self.border.is_some() || self.tone.is_some()
1248    }
1249}
1250
1251/// The largest canvas (pixels) on which a gradient layer's effects are
1252/// drawn (they need several full-size buffers).
1253const EFFECT_PIXELS_MAX: u64 = 40_000_000;
1254
1255/// `n` when `factor` is 1/2ⁿ (n ≥ 1), else 0.
1256fn exact_halvings(factor: f64) -> u32 {
1257    let halvings = (1.0 / factor).log2().round();
1258    if halvings >= 1.0 && ((1.0 / factor) / 2f64.powf(halvings) - 1.0).abs() < 0.005 { halvings as u32 } else { 0 }
1259}
1260
1261struct Content {
1262    buffer: Buffer,
1263    mode: Option<BlendMode>,
1264    /// The layer mask is already in the buffer (a border was drawn around
1265    /// the masked image).
1266    masked: bool,
1267}
1268
1269/// A base layer and the clipping layers drawn onto it so far.
1270enum Group {
1271    /// The base's own image, drawn with its mode and coverage at the end.
1272    Isolated { buffer: Buffer, mode: BlendMode, coverage: Vec<f32> },
1273    /// The base was mixed into the backdrop (pass-through or correction);
1274    /// clipping layers are masked by its alpha.
1275    Backdrop { alpha: Vec<f32> },
1276    /// The base is hidden or has no image.
1277    Empty,
1278}
1279
1280impl Group {
1281    fn flush(self, target: &mut Buffer) {
1282        if let Group::Isolated { buffer, mode, coverage } = self {
1283            target.composite(&buffer, mode, |i| coverage[i]);
1284        }
1285    }
1286}
1287
1288/// Mix `source` (which already includes the backdrop, as a pass-through
1289/// folder or a correction layer does) into `target` by `coverage`.
1290fn mix(target: &mut Buffer, source: &Buffer, coverage: &[f32]) {
1291    for (i, pixel) in target.data.iter_mut().enumerate() {
1292        let k = coverage[i];
1293        let s = source.data[i];
1294        for c in 0..4 {
1295            pixel[c] += (s[c] - pixel[c]) * k;
1296        }
1297    }
1298}
1299
1300/// Bilinear sample of premultiplied pixels at pixel-centre coordinates
1301/// (`x` = 0 is the centre of the first column); transparent outside.
1302fn bilinear(width: usize, height: usize, fetch: &impl Fn(usize, usize) -> [f32; 4], x: f64, y: f64) -> [f32; 4] {
1303    if !(x > -1.0 && y > -1.0 && x < width as f64 && y < height as f64) {
1304        return [0.0; 4];
1305    }
1306    let (x0, y0) = (x.floor(), y.floor());
1307    let (fx, fy) = ((x - x0) as f32, (y - y0) as f32);
1308    let (x0, y0) = (x0 as i64, y0 as i64);
1309    let mut out = [0.0f32; 4];
1310    for (dy, wy) in [(0, 1.0 - fy), (1, fy)] {
1311        for (dx, wx) in [(0, 1.0 - fx), (1, fx)] {
1312            let (sx, sy) = (x0 + dx, y0 + dy);
1313            let w = wx * wy;
1314            if w <= 0.0 || sx < 0 || sy < 0 || sx >= width as i64 || sy >= height as i64 {
1315                continue;
1316            }
1317            let p = fetch(sx as usize, sy as usize);
1318            for c in 0..4 {
1319                out[c] += p[c] * w;
1320            }
1321        }
1322    }
1323    out
1324}
1325
1326fn gray_to_rgba(gray: &GrayImage) -> RgbaImage {
1327    let mut image = RgbaImage::new(gray.width, gray.height);
1328    for (i, v) in gray.pixels.iter().enumerate() {
1329        image.pixels[i * 4..i * 4 + 4].copy_from_slice(&[*v, *v, *v, 255]);
1330    }
1331    image
1332}
1333
1334/// PSNR in dB of the RGB channels after compositing both images on white.
1335pub fn psnr(a: &RgbaImage, b: &RgbaImage) -> f64 {
1336    if (a.width, a.height) != (b.width, b.height) {
1337        return 0.0;
1338    }
1339    let flatten = |p: &[u8]| {
1340        let alpha = p[3] as f64 / 255.0;
1341        [0, 1, 2].map(|c| p[c] as f64 * alpha + 255.0 * (1.0 - alpha))
1342    };
1343    let mut sum = 0.0;
1344    for (pa, pb) in a.pixels.as_chunks::<4>().0.iter().zip(b.pixels.as_chunks::<4>().0.iter()) {
1345        let (x, y) = (flatten(pa), flatten(pb));
1346        for c in 0..3 {
1347            sum += (x[c] - y[c]).powi(2);
1348        }
1349    }
1350    let mse = sum / (a.pixels.len() / 4 * 3).max(1) as f64;
1351    if mse <= 1e-12 { 99.0 } else { 10.0 * (255.0f64 * 255.0 / mse).log10() }
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356    use super::*;
1357
1358    #[test]
1359    fn blend_modes_match_their_definitions() {
1360        assert_eq!(BlendMode::Multiply.channel(0.5, 0.5), 0.25);
1361        assert_eq!(BlendMode::Screen.channel(0.5, 0.5), 0.75);
1362        assert_eq!(BlendMode::Add.channel(0.7, 0.7), 1.0);
1363        assert_eq!(BlendMode::Overlay.channel(0.25, 1.0), 0.5);
1364        assert_eq!(BlendMode::Brightness.blend([1.0, 0.0, 0.0], [0.5, 0.5, 0.5]), set_lum([1.0, 0.0, 0.0], 0.5));
1365        assert_eq!(BlendMode::from_code(2), Some(BlendMode::Multiply));
1366        assert_eq!(BlendMode::from_code(99), None);
1367        assert_eq!(BlendMode::from_code(36), Some(BlendMode::Divide));
1368        assert_eq!(BlendMode::Divide.code(), 36);
1369    }
1370
1371    #[test]
1372    fn normal_over_opaque_backdrop() {
1373        let mut backdrop = Buffer::new(1, 1);
1374        backdrop.data[0] = [1.0, 1.0, 1.0, 1.0];
1375        let mut source = Buffer::new(1, 1);
1376        source.data[0] = [0.0, 0.0, 0.5, 0.5];
1377        backdrop.composite(&source, BlendMode::Normal, |_| 1.0);
1378        assert_eq!(backdrop.data[0], [0.5, 0.5, 1.0, 1.0]);
1379        let mut backdrop = Buffer::new(1, 1);
1380        backdrop.data[0] = [0.5, 0.5, 0.5, 1.0];
1381        source.data[0] = [0.5, 0.5, 0.5, 1.0];
1382        backdrop.composite(&source, BlendMode::Multiply, |_| 1.0);
1383        assert_eq!(backdrop.data[0], [0.25, 0.25, 0.25, 1.0]);
1384    }
1385
1386    #[test]
1387    fn psnr_of_equal_images_is_high() {
1388        let mut a = RgbaImage::new(2, 2);
1389        a.fill([10, 20, 30, 255]);
1390        assert_eq!(psnr(&a, &a), 99.0);
1391        let mut b = a.clone();
1392        b.set_pixel(0, 0, [11, 20, 30, 255]);
1393        assert!(psnr(&a, &b) > 50.0);
1394    }
1395
1396    /// A clipping layer is drawn onto its base layer alone, and the group
1397    /// then with the base's mode and opacity (measured in CSP: a screen
1398    /// layer clipped to a half-transparent multiply layer at 50%).
1399    #[test]
1400    fn clipping_layers_group_with_their_base() {
1401        let canvas = crate::NewCanvas { width: 4, height: 4, paper: Some([200, 200, 200]), ..Default::default() };
1402        let mut file = ClipFile::create(&canvas).unwrap();
1403        let mut base = RgbaImage::new(4, 4);
1404        base.fill([56, 199, 199, 140]);
1405        let base_id = file.add_raster_layer("base", &base, (0, 0)).unwrap();
1406        file.set_layer_blend(base_id, BlendMode::Multiply).unwrap();
1407        file.set_layer_opacity(base_id, 0.5).unwrap();
1408        let mut top = RgbaImage::new(4, 4);
1409        top.fill([151, 202, 160, 255]);
1410        let top_id = file.add_raster_layer("top", &top, (0, 0)).unwrap();
1411        file.set_layer_blend(top_id, BlendMode::Screen).unwrap();
1412        file.set_layer_clipping(top_id, true).unwrap();
1413        let (image, report) = render(&file, RenderOptions::default()).unwrap();
1414        assert_eq!(report.clipped, 1);
1415        // Expected: group colour = screen(base, top) (the top is opaque), alpha
1416        // = the base's; then multiplied onto the paper at 0.5 × 140/255.
1417        let (b, t, paper): ([f64; 3], [f64; 3], f64) = ([56.0, 199.0, 199.0], [151.0, 202.0, 160.0], 200.0);
1418        let a = 0.5 * 140.0 / 255.0;
1419        let expected: Vec<u8> = (0..3)
1420            .map(|c| {
1421                let group = 255.0 - (255.0 - b[c]) * (255.0 - t[c]) / 255.0;
1422                (paper * (1.0 - a) + paper * group / 255.0 * a).round() as u8
1423            })
1424            .collect();
1425        let got = image.pixel(1, 1);
1426        for c in 0..3 {
1427            assert!(got[c].abs_diff(expected[c]) <= 1, "{got:?} vs {expected:?}");
1428        }
1429    }
1430}