1use honzo_core::HonzoError;
6use image::{codecs::jpeg::JpegEncoder, DynamicImage, GenericImageView};
7use lexepub::core::chapter::{AstNode, ParsedChapter};
8use lexepub::LexEpub;
9use std::collections::HashMap;
10
11pub const IMG_TAG: [u8; 4] = *b"IMG_";
12
13const MAX_IMAGE_DIM_PX: u32 = 20_000;
14
15fn check_dims(width: u32, height: u32) -> Result<(), HonzoError> {
16 if width == 0 || height == 0 {
17 return Err(HonzoError::Truncated);
18 }
19 if width > MAX_IMAGE_DIM_PX || height > MAX_IMAGE_DIM_PX {
20 return Err(HonzoError::Truncated);
21 }
22 Ok(())
23}
24
25pub fn load_image(bytes: &[u8]) -> Result<DynamicImage, HonzoError> {
27 let img = image::load_from_memory(bytes).map_err(|_| HonzoError::Truncated)?;
28 let (w, h) = img.dimensions();
29 check_dims(w, h)?;
30 Ok(img)
31}
32
33pub fn validate_img(bytes: &[u8]) -> Result<&[u8], HonzoError> {
36 load_image(bytes)?;
37 Ok(bytes)
38}
39
40pub fn guess_mime(bytes: &[u8]) -> Option<&'static str> {
42 match image::guess_format(bytes).ok()? {
43 image::ImageFormat::Png => Some("image/png"),
44 image::ImageFormat::Jpeg => Some("image/jpeg"),
45 image::ImageFormat::Gif => Some("image/gif"),
46 image::ImageFormat::Bmp => Some("image/bmp"),
47 image::ImageFormat::Tiff => Some("image/tiff"),
48 image::ImageFormat::WebP => Some("image/webp"),
49 image::ImageFormat::Ico => Some("image/x-icon"),
50 image::ImageFormat::Pnm => Some("image/x-portable-anymap"),
51 _ => None,
52 }
53}
54
55pub fn encode_jpeg(img: &DynamicImage, quality: u8) -> Result<Vec<u8>, HonzoError> {
57 let rgb = img.to_rgb8();
58 let (w, h) = rgb.dimensions();
59 let mut out = Vec::new();
60 let mut encoder = JpegEncoder::new_with_quality(&mut out, quality);
61 encoder
62 .encode(&rgb, w, h, image::ExtendedColorType::Rgb8)
63 .map_err(|_| HonzoError::Truncated)?;
64 Ok(out)
65}
66
67pub fn collect_img_alts_from_parsed(parsed: &[ParsedChapter]) -> HashMap<String, String> {
71 let mut map: HashMap<String, String> = HashMap::new();
72
73 fn walk(node: &AstNode, map: &mut HashMap<String, String>) {
74 if let AstNode::Element {
75 tag,
76 attrs,
77 children,
78 ..
79 } = node
80 {
81 if tag.eq_ignore_ascii_case("img") {
82 if let Some(src) = attrs.get("src").or_else(|| attrs.get("href")) {
83 let alt = attrs.get("alt").cloned().unwrap_or_default();
84 map.entry(src.clone()).or_insert(alt);
85 }
86 }
87 for c in children {
88 walk(c, map);
89 }
90 }
91 }
92
93 for p in parsed.iter() {
94 if let Some(ast) = &p.ast {
95 walk(ast, &mut map);
96 }
97 }
98
99 map
100}
101
102pub async fn collect_and_resolve_img_alts_async(
107 parsed: &[ParsedChapter],
108 epub: &mut LexEpub,
109) -> HashMap<String, String> {
110 let raw_map = collect_img_alts_from_parsed(parsed);
111 let mut resolved: HashMap<String, String> = HashMap::new();
112
113 for (raw_href, alt) in raw_map.into_iter() {
114 let mut final_key = raw_href.clone();
115 for ci in 0..parsed.len() {
116 match epub.resolve_chapter_resource_path(ci, &raw_href).await {
117 Ok(p) => {
118 final_key = p;
119 break;
120 }
121 Err(_) => continue,
122 }
123 }
124 resolved.entry(final_key.clone()).or_insert(alt.clone());
127 resolved.entry(raw_href).or_insert(alt);
128 }
129
130 resolved
131}