1use crate::bitmap::Bitmap;
21use crate::djvu_document::DjVuBookmark;
22use crate::fgbz_encode::{FgbzColor, encode_fgbz};
23use crate::iff::{Chunk, ChunkId};
24use crate::jb2_encode::encode_jb2;
25use crate::navm_encode::encode_navm;
26use crate::smmr::encode_smmr;
27use crate::text::TextLayer;
28use crate::text_encode::encode_text_layer;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum EncodeError {
38 BookmarkChildrenOverflow {
41 found: usize,
43 },
44 BookmarkCountOverflow {
47 found: usize,
49 },
50 PaletteOverflow {
53 found: usize,
55 },
56 IndexCountOverflow {
59 found: usize,
61 },
62}
63
64impl core::fmt::Display for EncodeError {
65 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66 match self {
67 EncodeError::BookmarkChildrenOverflow { found } => write!(
68 f,
69 "NAVM bookmark node has {found} children, exceeds wire-format limit 255"
70 ),
71 EncodeError::BookmarkCountOverflow { found } => write!(
72 f,
73 "NAVM bookmark tree has {found} nodes, exceeds wire-format limit 65535"
74 ),
75 EncodeError::PaletteOverflow { found } => write!(
76 f,
77 "FGbz palette size {found} exceeds wire-format limit 65535"
78 ),
79 EncodeError::IndexCountOverflow { found } => write!(
80 f,
81 "FGbz index count {found} exceeds wire-format limit 2^24 - 1"
82 ),
83 }
84 }
85}
86
87impl std::error::Error for EncodeError {}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct EncodedChunk {
95 pub id: ChunkId,
97 pub payload: Vec<u8>,
99}
100
101impl EncodedChunk {
102 pub fn into_leaf(self) -> Chunk {
107 Chunk::Leaf {
108 id: self.id,
109 data: self.payload,
110 }
111 }
112}
113
114pub trait ChunkEncoder {
119 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError>;
121}
122
123pub fn encode_info(width: u16, height: u16, dpi: u16) -> Vec<u8> {
134 let mut b = vec![0u8; 10];
135 b[0..2].copy_from_slice(&width.to_be_bytes());
136 b[2..4].copy_from_slice(&height.to_be_bytes());
137 b[4] = 0x18; b[5] = 0x00; b[6..8].copy_from_slice(&dpi.to_le_bytes()); b[8] = 22; b[9] = 0x00; b
143}
144
145pub struct InfoChunk {
147 pub width: u16,
149 pub height: u16,
151 pub dpi: u16,
153}
154
155impl ChunkEncoder for InfoChunk {
156 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
157 Ok(EncodedChunk {
158 id: *b"INFO",
159 payload: encode_info(self.width, self.height, self.dpi),
160 })
161 }
162}
163
164pub struct NavmChunk<'a>(pub &'a [DjVuBookmark]);
166
167impl ChunkEncoder for NavmChunk<'_> {
168 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
169 Ok(EncodedChunk {
170 id: *b"NAVM",
171 payload: encode_navm(self.0)?,
172 })
173 }
174}
175
176pub struct FgbzChunk<'a> {
178 pub palette: &'a [FgbzColor],
180 pub indices: Option<&'a [i16]>,
182}
183
184impl ChunkEncoder for FgbzChunk<'_> {
185 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
186 Ok(EncodedChunk {
187 id: *b"FGbz",
188 payload: encode_fgbz(self.palette, self.indices)?,
189 })
190 }
191}
192
193pub struct TextChunk<'a> {
199 pub layer: &'a TextLayer,
201 pub page_height: u32,
203}
204
205impl ChunkEncoder for TextChunk<'_> {
206 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
207 Ok(EncodedChunk {
208 id: *b"TXTa",
209 payload: encode_text_layer(self.layer, self.page_height),
210 })
211 }
212}
213
214pub struct SmmrChunk<'a>(pub &'a Bitmap);
216
217impl ChunkEncoder for SmmrChunk<'_> {
218 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
219 Ok(EncodedChunk {
220 id: *b"Smmr",
221 payload: encode_smmr(self.0),
222 })
223 }
224}
225
226pub struct Jb2Chunk<'a>(pub &'a Bitmap);
228
229impl ChunkEncoder for Jb2Chunk<'_> {
230 fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
231 Ok(EncodedChunk {
232 id: *b"Sjbz",
233 payload: encode_jb2(self.0),
234 })
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn bm(title: &str, children: Vec<DjVuBookmark>) -> DjVuBookmark {
243 DjVuBookmark {
244 title: title.to_string(),
245 url: "#page=1".to_string(),
246 children,
247 }
248 }
249
250 #[test]
251 fn encode_error_display_messages() {
252 assert!(
253 EncodeError::BookmarkChildrenOverflow { found: 256 }
254 .to_string()
255 .contains("256")
256 );
257 assert!(
258 EncodeError::BookmarkCountOverflow { found: 65536 }
259 .to_string()
260 .contains("65536")
261 );
262 assert!(
263 EncodeError::PaletteOverflow { found: 70000 }
264 .to_string()
265 .contains("70000")
266 );
267 assert!(
268 EncodeError::IndexCountOverflow { found: 1 << 24 }
269 .to_string()
270 .contains("16777216")
271 );
272 }
273
274 #[test]
275 fn text_chunk_yields_txta_id() {
276 use crate::text::{Rect, TextLayer, TextZone, TextZoneKind};
277 let layer = TextLayer {
278 text: "hi".into(),
279 zones: vec![TextZone {
280 kind: TextZoneKind::Page,
281 rect: Rect {
282 x: 0,
283 y: 0,
284 width: 100,
285 height: 200,
286 },
287 text: "hi".into(),
288 children: vec![],
289 }],
290 };
291 let chunk = TextChunk {
292 layer: &layer,
293 page_height: 200,
294 }
295 .encode_chunk()
296 .unwrap();
297 assert_eq!(&chunk.id, b"TXTa");
298 assert!(!chunk.payload.is_empty());
299 }
300
301 #[test]
302 fn navm_chunk_yields_navm_id_and_bridges_to_leaf() {
303 let bookmarks = vec![bm("Chapter 1", vec![])];
304 let chunk = NavmChunk(&bookmarks).encode_chunk().unwrap();
305 assert_eq!(&chunk.id, b"NAVM");
306 assert!(!chunk.payload.is_empty());
307
308 match chunk.clone().into_leaf() {
309 Chunk::Leaf { id, data } => {
310 assert_eq!(&id, b"NAVM");
311 assert_eq!(data, chunk.payload);
312 }
313 other => panic!("expected leaf, got {other:?}"),
314 }
315 }
316
317 #[test]
318 fn navm_rejects_more_than_255_children() {
319 let children: Vec<DjVuBookmark> = (0..256).map(|_| bm("child", vec![])).collect();
320 let root = vec![bm("root", children)];
321 let err = NavmChunk(&root).encode_chunk().unwrap_err();
322 assert_eq!(err, EncodeError::BookmarkChildrenOverflow { found: 256 });
323 }
324
325 #[test]
326 fn navm_rejects_more_than_65535_nodes() {
327 let bookmarks: Vec<DjVuBookmark> = (0..(u16::MAX as usize) + 1)
331 .map(|_| bm("x", vec![]))
332 .collect();
333 let err = NavmChunk(&bookmarks).encode_chunk().unwrap_err();
334 assert_eq!(err, EncodeError::BookmarkCountOverflow { found: 65536 });
335 }
336
337 #[test]
338 fn fgbz_chunk_yields_fgbz_id() {
339 let palette = [FgbzColor { r: 1, g: 2, b: 3 }];
340 let chunk = FgbzChunk {
341 palette: &palette,
342 indices: None,
343 }
344 .encode_chunk()
345 .unwrap();
346 assert_eq!(&chunk.id, b"FGbz");
347 }
348
349 #[test]
350 fn fgbz_rejects_oversized_palette() {
351 let palette = vec![FgbzColor::default(); (u16::MAX as usize) + 1];
353 let err = FgbzChunk {
354 palette: &palette,
355 indices: None,
356 }
357 .encode_chunk()
358 .unwrap_err();
359 assert_eq!(err, EncodeError::PaletteOverflow { found: 65536 });
360 }
361
362 #[test]
363 fn smmr_and_jb2_yield_their_ids() {
364 let mut mask = Bitmap::new(4, 4);
365 mask.set_black(1, 1);
366 assert_eq!(&SmmrChunk(&mask).encode_chunk().unwrap().id, b"Smmr");
367 assert_eq!(&Jb2Chunk(&mask).encode_chunk().unwrap().id, b"Sjbz");
368 }
369
370 #[test]
371 fn encode_info_has_canonical_layout() {
372 let info = encode_info(181, 240, 100);
375 assert_eq!(
376 info,
377 vec![
378 0x00, 0xB5, 0x00, 0xF0, 0x18, 0x00, 0x64, 0x00, 22, 0x00, ]
385 );
386 }
387
388 #[test]
389 fn info_chunk_yields_info_id_and_matches_free_fn() {
390 let chunk = InfoChunk {
391 width: 640,
392 height: 480,
393 dpi: 300,
394 }
395 .encode_chunk()
396 .unwrap();
397 assert_eq!(&chunk.id, b"INFO");
398 assert_eq!(chunk.payload, encode_info(640, 480, 300));
399 assert_eq!(&chunk.payload[6..8], &[0x2C, 0x01]);
402 }
403}