1use crate::{ImageDiff, ImageDiffReporter, image_format};
2use askama::Template;
3use image::{ImageError, ImageFormat, RgbaImage};
4use semdiff_core::fs::FileLeaf;
5use semdiff_core::{DetailReporter, MayUnsupported};
6use semdiff_output::html::{HtmlReport, HtmlReportError};
7use thiserror::Error;
8
9const COMPARES_NAME: &str = "image";
10
11#[derive(Debug, Error)]
12pub enum ImageDiffReportError {
13 #[error("html report error: {0}")]
14 HtmlReport(#[from] HtmlReportError),
15 #[error("image decode error: {0}")]
16 ImageDecode(#[from] ImageError),
17}
18
19#[derive(Template)]
20#[template(path = "image_preview.html")]
21struct ImagePreviewTemplate<'a> {
22 body: ImagePreviewBody<'a>,
23}
24
25#[derive(Clone)]
26struct ImagePreviewImage<'a> {
27 src: &'a str,
28 label: &'a str,
29}
30
31enum ImagePreviewBody<'a> {
32 Modified { image: ImagePreviewImage<'a> },
33 Single { image: ImagePreviewImage<'a> },
34}
35
36#[derive(Template)]
37#[template(path = "image_detail.html")]
38struct ImageDetailTemplate<'a> {
39 detail: ImageDetailBody<'a>,
40}
41
42#[derive(Clone)]
43struct ImageDetailImage<'a> {
44 uri: &'a str,
45 width: u32,
46 height: u32,
47}
48
49enum ImageDetailBody<'a> {
50 Diff {
51 expected: ImageDetailImage<'a>,
52 actual: ImageDetailImage<'a>,
53 diff: ImageDetailImage<'a>,
54 },
55 Single {
56 label: &'a str,
57 image: ImageDetailImage<'a>,
58 },
59}
60
61impl DetailReporter<ImageDiff, FileLeaf, HtmlReport> for ImageDiffReporter {
62 type Error = ImageDiffReportError;
63
64 fn report_unchanged(
65 &self,
66 name: &str,
67 diff: &ImageDiff,
68 reporter: &HtmlReport,
69 ) -> Result<MayUnsupported<()>, Self::Error> {
70 let detail_image = write_image(reporter, name, "same", &diff.expected().data)?;
71 let preview_html = ImagePreviewTemplate {
72 body: ImagePreviewBody::Single {
73 image: ImagePreviewImage {
74 src: &reporter.detail_asset_path(&detail_image),
75 label: "same",
76 },
77 },
78 };
79 let detail_html = ImageDetailTemplate {
80 detail: ImageDetailBody::Single {
81 label: "same",
82 image: ImageDetailImage {
83 uri: &detail_image,
84 width: diff.expected().width,
85 height: diff.expected.height,
86 },
87 },
88 };
89 reporter.record_unchanged(name, COMPARES_NAME, preview_html, detail_html)?;
90 Ok(MayUnsupported::Ok(()))
91 }
92
93 fn report_modified(
94 &self,
95 name: &str,
96 diff: &ImageDiff,
97 reporter: &HtmlReport,
98 ) -> Result<MayUnsupported<()>, Self::Error> {
99 let expected_image = write_image(reporter, name, "expected", &diff.expected().data)?;
100 let actual_image = write_image(reporter, name, "actual", &diff.actual().data)?;
101 let diff_image = diff.diff_image();
102 let diff_image_file_name = write_image(reporter, name, "diff", diff_image)?;
103 let diff_image = ImageDetailImage {
104 uri: &diff_image_file_name,
105 width: diff.diff_image.width(),
106 height: diff.diff_image.height(),
107 };
108 let preview_image = ImagePreviewImage {
109 src: &reporter.detail_asset_path(diff_image.uri),
110 label: "diff",
111 };
112 let preview_html = ImagePreviewTemplate {
113 body: ImagePreviewBody::Modified { image: preview_image },
114 };
115 let detail_html = ImageDetailTemplate {
116 detail: ImageDetailBody::Diff {
117 expected: ImageDetailImage {
118 uri: &expected_image,
119 width: diff.expected().width,
120 height: diff.expected().height,
121 },
122 actual: ImageDetailImage {
123 uri: &actual_image,
124 width: diff.actual().width,
125 height: diff.actual().height,
126 },
127 diff: diff_image,
128 },
129 };
130 reporter.record_modified(name, COMPARES_NAME, preview_html, detail_html)?;
131 Ok(MayUnsupported::Ok(()))
132 }
133
134 fn report_added(
135 &self,
136 name: &str,
137 data: &FileLeaf,
138 reporter: &HtmlReport,
139 ) -> Result<MayUnsupported<()>, Self::Error> {
140 let Some(format) = image_format(&data.kind) else {
141 return Ok(MayUnsupported::Unsupported);
142 };
143 let image = image::load_from_memory_with_format(&data.content, format)?.into_rgba8();
144 let width = image.width();
145 let height = image.height();
146 let image_path = write_image(reporter, name, "added", &image)?;
147 let preview_html = ImagePreviewTemplate {
148 body: ImagePreviewBody::Single {
149 image: ImagePreviewImage {
150 src: &reporter.detail_asset_path(&image_path),
151 label: "added",
152 },
153 },
154 };
155 let detail_html = ImageDetailTemplate {
156 detail: ImageDetailBody::Single {
157 label: "added",
158 image: ImageDetailImage {
159 uri: &image_path,
160 width,
161 height,
162 },
163 },
164 };
165 reporter.record_added(name, COMPARES_NAME, preview_html, detail_html)?;
166 Ok(MayUnsupported::Ok(()))
167 }
168
169 fn report_deleted(
170 &self,
171 name: &str,
172 data: &FileLeaf,
173 reporter: &HtmlReport,
174 ) -> Result<MayUnsupported<()>, Self::Error> {
175 let Some(format) = image_format(&data.kind) else {
176 return Ok(MayUnsupported::Unsupported);
177 };
178 let image = image::load_from_memory_with_format(&data.content, format)?.into_rgba8();
179 let width = image.width();
180 let height = image.height();
181 let image_path = write_image(reporter, name, "deleted", &image)?;
182 let preview_html = ImagePreviewTemplate {
183 body: ImagePreviewBody::Single {
184 image: ImagePreviewImage {
185 src: &reporter.detail_asset_path(&image_path),
186 label: "deleted",
187 },
188 },
189 };
190 let detail_html = ImageDetailTemplate {
191 detail: ImageDetailBody::Single {
192 label: "deleted",
193 image: ImageDetailImage {
194 uri: &image_path,
195 width,
196 height,
197 },
198 },
199 };
200 reporter.record_deleted(name, COMPARES_NAME, preview_html, detail_html)?;
201 Ok(MayUnsupported::Ok(()))
202 }
203}
204
205fn write_image(reporter: &HtmlReport, name: &str, label: &str, image: &RgbaImage) -> Result<String, HtmlReportError> {
206 reporter.write_detail_asset(name, label, "png", |w| match image.write_to(w, ImageFormat::Png) {
207 Ok(()) => Ok(()),
208 Err(ImageError::IoError(err)) => Err(err.into()),
209 Err(err) => panic!("Unexpected error writing diff image: {}", err),
210 })
211}