1use crate::content::NodeId;
31use crate::fonts::FontRefEntry;
32use crate::images::Asset;
33use crate::pages::Page;
34use crate::{LayoutOutput, Warning};
35
36pub const VERSION: u16 = 19;
39
40#[derive(Debug, thiserror::Error)]
42pub enum WireError {
43 #[error("wire version {found}, expected {VERSION}")]
46 Version {
47 found: u16,
49 },
50 #[error("the wire could not be read: {0}")]
52 Malformed(#[from] postcard::Error),
53}
54
55#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
63pub struct Reply {
64 pub pages: Vec<Page>,
66 pub first: usize,
68 pub book_pages: usize,
71 pub fonts: Vec<FontRefEntry>,
73 pub assets: Vec<Asset>,
75 pub warnings: Vec<Warning>,
77}
78
79pub fn encode(output: &LayoutOutput) -> Result<Vec<u8>, WireError> {
81 encode_range(output, 0, output.pages.len())
82}
83
84pub fn encode_range(
93 output: &LayoutOutput,
94 first: usize,
95 count: usize,
96) -> Result<Vec<u8>, WireError> {
97 let book_pages = output.pages.len();
98 let first = first.min(book_pages);
99 let end = first.saturating_add(count).min(book_pages);
100 let slice = &output.pages[first..end];
101 Ok(postcard::to_stdvec(&(
102 VERSION,
103 first as u32,
104 book_pages as u32,
105 &output.fonts,
106 &output.assets,
107 &output.warnings,
108 slice,
109 ))?)
110}
111
112pub fn decode(bytes: &[u8]) -> Result<Reply, WireError> {
114 let (found, rest) = postcard::take_from_bytes::<u16>(bytes)?;
115 if found != VERSION {
116 return Err(WireError::Version { found });
117 }
118 let (first, book_pages, fonts, assets, warnings, pages): (
119 u32,
120 u32,
121 Vec<FontRefEntry>,
122 Vec<Asset>,
123 Vec<Warning>,
124 Vec<Page>,
125 ) = postcard::from_bytes(rest)?;
126 Ok(Reply {
127 pages,
128 first: first as usize,
129 book_pages: book_pages as usize,
130 fonts,
131 assets,
132 warnings,
133 })
134}
135
136#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
141pub struct Epub {
142 pub warnings: Vec<Warning>,
145 pub bytes: Vec<u8>,
147}
148
149pub fn encode_epub(bytes: &[u8], warnings: &[Warning]) -> Result<Vec<u8>, WireError> {
151 Ok(postcard::to_stdvec(&(VERSION, warnings, bytes))?)
152}
153
154pub fn decode_epub(bytes: &[u8]) -> Result<Epub, WireError> {
157 let (found, rest) = postcard::take_from_bytes::<u16>(bytes)?;
158 if found != VERSION {
159 return Err(WireError::Version { found });
160 }
161 Ok(postcard::from_bytes(rest)?)
162}
163
164#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
167pub struct EpubFiles {
168 pub warnings: Vec<Warning>,
170 pub spine: Vec<EpubSpineEntry>,
172 pub files: Vec<EpubFile>,
174}
175
176#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
178pub struct EpubSpineEntry {
179 pub path: String,
181 pub section: Option<NodeId>,
184}
185
186#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
188pub struct EpubFile {
189 pub path: String,
191 pub media_type: String,
193 pub bytes: Vec<u8>,
195}
196
197pub fn encode_epub_files(files: &EpubFiles) -> Result<Vec<u8>, WireError> {
199 Ok(postcard::to_stdvec(&(VERSION, files))?)
200}
201
202pub fn decode_epub_files(bytes: &[u8]) -> Result<EpubFiles, WireError> {
205 let (found, rest) = postcard::take_from_bytes::<u16>(bytes)?;
206 if found != VERSION {
207 return Err(WireError::Version { found });
208 }
209 Ok(postcard::from_bytes(rest)?)
210}
211
212pub fn version(bytes: &[u8]) -> Result<u16, WireError> {
214 Ok(postcard::take_from_bytes::<u16>(bytes)?.0)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::Warning;
221 use crate::content::{NodeId, PseudoElement, SourceRange};
222 use crate::fonts::{AxisSetting, FaceAttributes, FeatureSetting, Features, FontRefEntry};
223 use crate::images::{Asset, Intrinsic};
224 use crate::pages::{Corners, DrawItem, Glyph, Link, LinkTo, Page, PageBox, Radius, Side};
225 use crate::style::{Color, Edges};
226
227 fn output() -> LayoutOutput {
228 LayoutOutput {
229 pages: vec![Page {
230 number: 1,
231 side: Side::Recto,
232 width: 396.0,
233 height: 612.0,
234 sections: vec![NodeId::UNASSIGNED],
235 items: vec![
236 DrawItem::Text {
237 x: 72.0,
238 y: 96.5,
239 font_id: 0,
240 size: 11.0,
241 width: 18.5,
242 text: "FI ❦".into(),
243 source: "fi ❦".into(),
244 source_map: vec![0, 1, 2, 3, 4, 5, 6],
245 origin: Some(SourceRange {
246 node: NodeId::UNASSIGNED,
247 range: 3..9,
248 }),
249 pseudo_element: Some(NodeId::new(7).pseudo(PseudoElement::FirstLine)),
250 features: Features::new(true, vec![FeatureSetting::new(*b"onum", 1)]),
251 color: Color::rgb(180, 30, 30),
252 glyphs: vec![Glyph {
253 id: 42,
254 x: 72.0,
255 range: 0..2,
256 }],
257 layer: 10,
258 },
259 DrawItem::Rect {
260 x: 0.0,
261 y: 0.0,
262 w: 396.0,
263 h: 0.5,
264 color: Color::BLACK,
265 layer: DrawItem::PAGE_BACKGROUND,
266 },
267 DrawItem::Image {
268 x: 1.0,
269 y: 2.0,
270 w: 3.0,
271 h: 4.0,
272 asset: 7,
273 alpha: 128,
274 layer: -1,
275 },
276 DrawItem::Rounded {
277 x: 5.0,
278 y: 6.0,
279 w: 70.0,
280 h: 20.0,
281 radii: Corners {
282 top_left: Radius { x: 3.0, y: 3.0 },
283 ..Corners::SQUARE
284 },
285 ring: Edges {
286 left: 2.0,
287 ..Edges::all(0.0)
288 },
289 color: Color::rgba(51, 102, 153, 128),
290 layer: 0,
291 },
292 ],
293 links: vec![
294 Link {
295 areas: vec![
296 PageBox {
297 page: 0,
298 x: 72.0,
299 y: 88.0,
300 width: 40.0,
301 height: 12.0,
302 },
303 PageBox {
304 page: 0,
305 x: 72.0,
306 y: 102.0,
307 width: 20.0,
308 height: 12.0,
309 },
310 ],
311 to: LinkTo::Place {
312 node: NodeId::new(12),
313 place: PageBox {
314 page: 4,
315 x: 72.0,
316 y: 72.0,
317 width: 252.0,
318 height: 30.0,
319 },
320 },
321 },
322 Link {
323 areas: vec![],
324 to: LinkTo::Uri("https://example.com".into()),
325 },
326 ],
327 }],
328 fonts: vec![FontRefEntry {
329 family: "eb garamond".into(),
330 name: "EB Garamond Regular".into(),
331 style: "Regular".into(),
332 attributes: FaceAttributes::REGULAR,
333 variations: vec![AxisSetting {
334 tag: *b"wght",
335 value: 400.0,
336 }],
337 }],
338 assets: vec![Asset {
339 url: "plate.jpg".into(),
340 intrinsic: Intrinsic {
341 width: 480,
342 height: 320,
343 dpi_x: 300.0,
344 dpi_y: 300.0,
345 },
346 }],
347 warnings: vec![Warning {
348 message: "a table became prose".into(),
349 origin: Some("ch01.md:12:1".into()),
350 }],
351 navigation: Default::default(),
352 }
353 }
354
355 fn as_output(reply: Reply) -> LayoutOutput {
359 LayoutOutput {
360 pages: reply.pages,
361 fonts: reply.fonts,
362 assets: reply.assets,
363 warnings: reply.warnings,
364 navigation: Default::default(),
365 }
366 }
367
368 #[test]
371 fn the_wire_round_trips() {
372 let bytes = encode(&output()).unwrap();
373 let read = decode(&bytes).unwrap();
374 assert_eq!(read.pages, output().pages);
375 assert_eq!(read.first, 0, "a whole-book reply starts at page 0");
376 assert_eq!(
377 read.book_pages,
378 output().pages.len(),
379 "a whole-book reply's total is its own page count"
380 );
381 assert_eq!(read.fonts, output().fonts);
382 assert_eq!(read.assets, output().assets);
383 assert_eq!(read.warnings, output().warnings);
384 assert_eq!(encode(&as_output(read)).unwrap(), bytes);
385 }
386
387 #[test]
390 fn the_version_leads_the_bytes() {
391 let bytes = encode(&output()).unwrap();
392 assert_eq!(version(&bytes).unwrap(), VERSION);
393 assert_eq!(bytes[0], VERSION as u8);
394 }
395
396 #[test]
399 fn an_unknown_version_is_refused() {
400 let mut bytes = encode(&output()).unwrap();
401 bytes[0] = VERSION as u8 + 1;
402 assert!(matches!(decode(&bytes), Err(WireError::Version { .. })));
403 }
404
405 #[test]
408 fn a_range_carries_only_its_own_slice_with_the_full_tables() {
409 let mut book = output();
410 book.pages.push(Page {
411 number: 2,
412 side: Side::Verso,
413 width: 396.0,
414 height: 612.0,
415 sections: vec![],
416 items: vec![],
417 links: vec![],
418 });
419 let bytes = encode_range(&book, 1, 1).unwrap();
420 let read = decode(&bytes).unwrap();
421 assert_eq!(read.pages, book.pages[1..2]);
422 assert_eq!(read.first, 1);
423 assert_eq!(read.book_pages, 2);
424 assert_eq!(read.fonts, book.fonts, "the font table is not sliced");
425 assert_eq!(read.assets, book.assets, "the asset table is not sliced");
426 assert_eq!(read.warnings, book.warnings, "the warnings are not sliced");
427 }
428
429 #[test]
433 fn a_range_past_the_end_clamps_rather_than_panics() {
434 let book = output();
435 let bytes = encode_range(&book, 5, 3).unwrap();
436 let read = decode(&bytes).unwrap();
437 assert!(read.pages.is_empty());
438 assert_eq!(read.first, 1, "first clamps to the book's own length");
439 assert_eq!(read.book_pages, 1);
440 }
441
442 #[test]
445 fn an_epub_s_files_round_trip() {
446 let files = EpubFiles {
447 warnings: output().warnings,
448 spine: vec![
449 EpubSpineEntry {
450 path: "EPUB/section-001.xhtml".into(),
451 section: Some(NodeId::new(7)),
452 },
453 EpubSpineEntry {
454 path: "EPUB/section-002.xhtml".into(),
455 section: None,
456 },
457 ],
458 files: vec![
459 EpubFile {
460 path: "mimetype".into(),
461 media_type: "text/plain".into(),
462 bytes: b"application/epub+zip".to_vec(),
463 },
464 EpubFile {
465 path: "EPUB/section-001.xhtml".into(),
466 media_type: "application/xhtml+xml".into(),
467 bytes: b"<html/>".to_vec(),
468 },
469 ],
470 };
471 let bytes = encode_epub_files(&files).unwrap();
472 assert_eq!(version(&bytes).unwrap(), VERSION);
473 assert_eq!(decode_epub_files(&bytes).unwrap(), files);
474
475 let mut stale = bytes;
476 stale[0] = VERSION as u8 + 1;
477 assert!(matches!(
478 decode_epub_files(&stale),
479 Err(WireError::Version { .. })
480 ));
481 }
482
483 #[test]
486 fn an_epub_round_trips_with_its_warnings() {
487 let file = b"PK\x03\x04mimetypeapplication/epub+zip".to_vec();
488 let warnings = output().warnings;
489 let bytes = encode_epub(&file, &warnings).unwrap();
490 assert_eq!(version(&bytes).unwrap(), VERSION);
491 assert_eq!(
492 decode_epub(&bytes).unwrap(),
493 Epub {
494 warnings,
495 bytes: file
496 }
497 );
498
499 let mut stale = bytes;
500 stale[0] = VERSION as u8 + 1;
501 assert!(matches!(
502 decode_epub(&stale),
503 Err(WireError::Version { .. })
504 ));
505 }
506}