Skip to main content

semdiff_differ_image/
report_html.rs

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}