1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
43#[non_exhaustive]
44pub enum BlendMode {
45 Normal,
47 Darken,
49 Multiply,
51 ColorBurn,
53 LinearBurn,
55 Subtract,
57 DarkerColor,
59 Lighten,
61 Screen,
63 ColorDodge,
65 GlowDodge,
67 Add,
69 AddGlow,
71 LighterColor,
73 Overlay,
75 SoftLight,
77 HardLight,
79 Difference,
81 Exclusion,
83 VividLight,
85 LinearLight,
87 PinLight,
89 HardMix,
91 Hue,
93 Saturation,
95 Color,
97 Brightness,
99 Divide,
101 PassThrough,
103}
104
105impl BlendMode {
106 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 pub fn code(self) -> i64 {
150 (0..=36).find(|code| Self::from_code(*code) == Some(self)).unwrap_or(0)
151 }
152
153 fn is_glow(self) -> bool {
155 matches!(self, BlendMode::GlowDodge | BlendMode::AddGlow)
156 }
157
158 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 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 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
257fn 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#[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 fn composite(&mut self, source: &Buffer, mode: BlendMode, coverage: impl Fn(usize) -> f32) {
327 self.composite_with(source, mode, coverage, false);
328 }
329
330 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 let alone = if atop { 0.0 } else { sa * (1.0 - ba) };
351 let mut out = [0.0; 4];
352 if mode.is_glow() {
353 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 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#[derive(Debug, Clone, Copy)]
374pub struct RenderOptions {
375 pub scale: f64,
377 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#[derive(Debug, Clone, Default, serde::Serialize)]
391pub struct RenderReport {
392 pub width: u32,
394 pub height: u32,
396 pub drawn: usize,
398 pub hidden: usize,
400 pub without_pixels: BTreeMap<String, usize>,
402 pub unknown_blend: BTreeMap<i64, usize>,
404 pub blend_modes: BTreeMap<i64, usize>,
406 pub clipped: usize,
408 pub masked: usize,
410 pub effects: BTreeMap<String, usize>,
412 pub effects_skipped: BTreeMap<String, usize>,
414 pub vector_layers: usize,
416 pub text_layers: usize,
418 pub material_layers: usize,
420 pub gradient_layers: usize,
423 pub gradients_approximate: usize,
425 pub tones_approximate: usize,
427 pub correction_layers: usize,
430 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 camera: Option<Affine>,
445}
446
447pub 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
461pub 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
482pub 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 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 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 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 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 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 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 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 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 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 if layer.kind == LayerKind::Gradient
687 && let Some(mut gradient) = layer.row.blob("GradationFillInfo").and_then(|data| Gradient::parse(data).ok())
688 {
689 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 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 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 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 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 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 fn text(&self, layer: &Layer, full: bool) -> Result<Option<TextImage>> {
810 let mut result: Option<TextImage> = None;
811 let objects: Vec<_> = layer_vectors(self.file, layer)?.into_iter().flat_map(|data| data.objects).collect();
813 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 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 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 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 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 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 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 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 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 let image = if (factor - 1.0).abs() < 0.005 {
1030 image
1031 } else if halvings > 0 {
1032 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 fn pixels(&mut self, layer: &Layer, effects: &Effects) -> Result<Option<(Buffer, bool)>> {
1070 if layer.render_mipmap == 0 {
1071 return Ok(None);
1072 }
1073 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 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 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 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 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 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 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 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
1206fn 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
1216fn 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
1230enum TextImage {
1233 Full(RgbaImage),
1234 Placed(Buffer),
1235}
1236
1237#[derive(Default)]
1239struct Effects {
1240 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
1251const EFFECT_PIXELS_MAX: u64 = 40_000_000;
1254
1255fn 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 masked: bool,
1267}
1268
1269enum Group {
1271 Isolated { buffer: Buffer, mode: BlendMode, coverage: Vec<f32> },
1273 Backdrop { alpha: Vec<f32> },
1276 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
1288fn 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
1300fn 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
1334pub 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 #[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 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}