use crate::bitmap::Bitmap;
use crate::djvu_document::DjVuBookmark;
use crate::fgbz_encode::{FgbzColor, encode_fgbz};
use crate::iff::{Chunk, ChunkId};
use crate::jb2_encode::encode_jb2;
use crate::navm_encode::encode_navm;
use crate::smmr::encode_smmr;
use crate::text::TextLayer;
use crate::text_encode::encode_text_layer;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncodeError {
BookmarkChildrenOverflow {
found: usize,
},
BookmarkCountOverflow {
found: usize,
},
PaletteOverflow {
found: usize,
},
IndexCountOverflow {
found: usize,
},
}
impl core::fmt::Display for EncodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
EncodeError::BookmarkChildrenOverflow { found } => write!(
f,
"NAVM bookmark node has {found} children, exceeds wire-format limit 255"
),
EncodeError::BookmarkCountOverflow { found } => write!(
f,
"NAVM bookmark tree has {found} nodes, exceeds wire-format limit 65535"
),
EncodeError::PaletteOverflow { found } => write!(
f,
"FGbz palette size {found} exceeds wire-format limit 65535"
),
EncodeError::IndexCountOverflow { found } => write!(
f,
"FGbz index count {found} exceeds wire-format limit 2^24 - 1"
),
}
}
}
impl std::error::Error for EncodeError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedChunk {
pub id: ChunkId,
pub payload: Vec<u8>,
}
impl EncodedChunk {
pub fn into_leaf(self) -> Chunk {
Chunk::Leaf {
id: self.id,
data: self.payload,
}
}
}
pub trait ChunkEncoder {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError>;
}
pub fn encode_info(width: u16, height: u16, dpi: u16) -> Vec<u8> {
let mut b = vec![0u8; 10];
b[0..2].copy_from_slice(&width.to_be_bytes());
b[2..4].copy_from_slice(&height.to_be_bytes());
b[4] = 0x18; b[5] = 0x00; b[6..8].copy_from_slice(&dpi.to_le_bytes()); b[8] = 22; b[9] = 0x00; b
}
pub struct InfoChunk {
pub width: u16,
pub height: u16,
pub dpi: u16,
}
impl ChunkEncoder for InfoChunk {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
Ok(EncodedChunk {
id: *b"INFO",
payload: encode_info(self.width, self.height, self.dpi),
})
}
}
pub struct NavmChunk<'a>(pub &'a [DjVuBookmark]);
impl ChunkEncoder for NavmChunk<'_> {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
Ok(EncodedChunk {
id: *b"NAVM",
payload: encode_navm(self.0)?,
})
}
}
pub struct FgbzChunk<'a> {
pub palette: &'a [FgbzColor],
pub indices: Option<&'a [i16]>,
}
impl ChunkEncoder for FgbzChunk<'_> {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
Ok(EncodedChunk {
id: *b"FGbz",
payload: encode_fgbz(self.palette, self.indices)?,
})
}
}
pub struct TextChunk<'a> {
pub layer: &'a TextLayer,
pub page_height: u32,
}
impl ChunkEncoder for TextChunk<'_> {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
Ok(EncodedChunk {
id: *b"TXTa",
payload: encode_text_layer(self.layer, self.page_height),
})
}
}
pub struct SmmrChunk<'a>(pub &'a Bitmap);
impl ChunkEncoder for SmmrChunk<'_> {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
Ok(EncodedChunk {
id: *b"Smmr",
payload: encode_smmr(self.0),
})
}
}
pub struct Jb2Chunk<'a>(pub &'a Bitmap);
impl ChunkEncoder for Jb2Chunk<'_> {
fn encode_chunk(&self) -> Result<EncodedChunk, EncodeError> {
Ok(EncodedChunk {
id: *b"Sjbz",
payload: encode_jb2(self.0),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bm(title: &str, children: Vec<DjVuBookmark>) -> DjVuBookmark {
DjVuBookmark {
title: title.to_string(),
url: "#page=1".to_string(),
children,
}
}
#[test]
fn encode_error_display_messages() {
assert!(
EncodeError::BookmarkChildrenOverflow { found: 256 }
.to_string()
.contains("256")
);
assert!(
EncodeError::BookmarkCountOverflow { found: 65536 }
.to_string()
.contains("65536")
);
assert!(
EncodeError::PaletteOverflow { found: 70000 }
.to_string()
.contains("70000")
);
assert!(
EncodeError::IndexCountOverflow { found: 1 << 24 }
.to_string()
.contains("16777216")
);
}
#[test]
fn text_chunk_yields_txta_id() {
use crate::text::{Rect, TextLayer, TextZone, TextZoneKind};
let layer = TextLayer {
text: "hi".into(),
zones: vec![TextZone {
kind: TextZoneKind::Page,
rect: Rect {
x: 0,
y: 0,
width: 100,
height: 200,
},
text: "hi".into(),
children: vec![],
}],
};
let chunk = TextChunk {
layer: &layer,
page_height: 200,
}
.encode_chunk()
.unwrap();
assert_eq!(&chunk.id, b"TXTa");
assert!(!chunk.payload.is_empty());
}
#[test]
fn navm_chunk_yields_navm_id_and_bridges_to_leaf() {
let bookmarks = vec![bm("Chapter 1", vec![])];
let chunk = NavmChunk(&bookmarks).encode_chunk().unwrap();
assert_eq!(&chunk.id, b"NAVM");
assert!(!chunk.payload.is_empty());
match chunk.clone().into_leaf() {
Chunk::Leaf { id, data } => {
assert_eq!(&id, b"NAVM");
assert_eq!(data, chunk.payload);
}
other => panic!("expected leaf, got {other:?}"),
}
}
#[test]
fn navm_rejects_more_than_255_children() {
let children: Vec<DjVuBookmark> = (0..256).map(|_| bm("child", vec![])).collect();
let root = vec![bm("root", children)];
let err = NavmChunk(&root).encode_chunk().unwrap_err();
assert_eq!(err, EncodeError::BookmarkChildrenOverflow { found: 256 });
}
#[test]
fn navm_rejects_more_than_65535_nodes() {
let bookmarks: Vec<DjVuBookmark> = (0..(u16::MAX as usize) + 1)
.map(|_| bm("x", vec![]))
.collect();
let err = NavmChunk(&bookmarks).encode_chunk().unwrap_err();
assert_eq!(err, EncodeError::BookmarkCountOverflow { found: 65536 });
}
#[test]
fn fgbz_chunk_yields_fgbz_id() {
let palette = [FgbzColor { r: 1, g: 2, b: 3 }];
let chunk = FgbzChunk {
palette: &palette,
indices: None,
}
.encode_chunk()
.unwrap();
assert_eq!(&chunk.id, b"FGbz");
}
#[test]
fn fgbz_rejects_oversized_palette() {
let palette = vec![FgbzColor::default(); (u16::MAX as usize) + 1];
let err = FgbzChunk {
palette: &palette,
indices: None,
}
.encode_chunk()
.unwrap_err();
assert_eq!(err, EncodeError::PaletteOverflow { found: 65536 });
}
#[test]
fn smmr_and_jb2_yield_their_ids() {
let mut mask = Bitmap::new(4, 4);
mask.set_black(1, 1);
assert_eq!(&SmmrChunk(&mask).encode_chunk().unwrap().id, b"Smmr");
assert_eq!(&Jb2Chunk(&mask).encode_chunk().unwrap().id, b"Sjbz");
}
#[test]
fn encode_info_has_canonical_layout() {
let info = encode_info(181, 240, 100);
assert_eq!(
info,
vec![
0x00, 0xB5, 0x00, 0xF0, 0x18, 0x00, 0x64, 0x00, 22, 0x00, ]
);
}
#[test]
fn info_chunk_yields_info_id_and_matches_free_fn() {
let chunk = InfoChunk {
width: 640,
height: 480,
dpi: 300,
}
.encode_chunk()
.unwrap();
assert_eq!(&chunk.id, b"INFO");
assert_eq!(chunk.payload, encode_info(640, 480, 300));
assert_eq!(&chunk.payload[6..8], &[0x2C, 0x01]);
}
}