1
0
mirror of https://github.com/lise-henry/crowbook synced 2024-09-30 09:01:24 +02:00
crowbook/src/lib/epub.rs
2016-02-29 01:47:49 +01:00

375 lines
15 KiB
Rust

// Copyright (C) 2016 Élisabeth HENRY.
//
// This file is part of Crowbook.
//
// Crowbook is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
//
// Caribon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received ba copy of the GNU Lesser General Public License
// along with Crowbook. If not, see <http://www.gnu.org/licenses/>.
use error::{Error,Result};
use token::Token;
use html::HtmlRenderer;
use book::Book;
use number::Number;
use zipper::Zipper;
use templates::epub::*;
use templates::epub3;
use mustache;
use chrono;
use uuid;
use std::io::{Read,Write};
use std::path::Path;
use std::fs::File;
use std::borrow::Cow;
/// Renderer for Epub
///
/// Uses part of the HTML renderer
pub struct EpubRenderer<'a> {
book: &'a Book,
toc: Vec<String>,
html: HtmlRenderer<'a>,
}
impl<'a> EpubRenderer<'a> {
/// Creates a new Epub renderer
pub fn new(book: &'a Book) -> EpubRenderer<'a> {
let mut html = HtmlRenderer::new(book);
html.toc.numbered(true);
html.handler.set_images_mapping(true);
EpubRenderer {
book: book,
html: html,
toc: vec!(),
}
}
/// Render a book
pub fn render_book(&mut self) -> Result<String> {
for (i, filename) in self.book.filenames.iter().enumerate() {
self.html.handler.add_link(filename.clone(), filenamer(i));
}
let mut zipper = try!(Zipper::new(&self.book.options.get_path("temp_dir").unwrap()));
// Write mimetype
try!(zipper.write("mimetype", b"application/epub+zip", true));
// Write chapters
for (i, &(n, ref v)) in self.book.chapters.iter().enumerate() {
self.html.filename = filenamer(i);
self.html.current_hide = false;
let book_numbering = self.book.options.get_i32("numbering").unwrap();
match n {
Number::Unnumbered => self.html.current_numbering = 0,
Number::Default => self.html.current_numbering = book_numbering,
Number::Specified(n) => {
self.html.current_numbering = book_numbering;
self.html.current_chapter[0] = n - 1;
},
Number::Hidden => {
self.html.current_numbering = 0;
self.html.current_hide = true;
}
}
let chapter = try!(self.render_chapter(v));
try!(zipper.write(&filenamer(i), &chapter.as_bytes(), true));
}
// Write CSS file
try!(zipper.write("stylesheet.css",
&try!(self.book.get_template("epub.css")).as_bytes(), true));
// Write titlepage
try!(zipper.write("title_page.xhtml", &try!(self.render_titlepage()).as_bytes(), true));
// Write file for ibook (why?)
try!(zipper.write("META-INF/com.apple.ibooks.display-options.xml", IBOOK.as_bytes(), true));
// Write container.xml
try!(zipper.write("META-INF/container.xml", CONTAINER.as_bytes(), true));
// Write nav.xhtml
try!(zipper.write("nav.xhtml", &try!(self.render_nav()).as_bytes(), true));
// Write content.opf
try!(zipper.write("content.opf", &try!(self.render_opf()).as_bytes(), true));
// Write toc.ncx
try!(zipper.write("toc.ncx", &try!(self.render_toc()).as_bytes(), true));
// Write cover.xhtml (if needs be)
if self.book.options.get_path("cover").is_ok() {
try!(zipper.write("cover.xhtml", &try!(self.render_cover()).as_bytes(), true));
}
// Write all images (including cover)
for (source, dest) in self.html.handler.images_mapping() {
let mut f = try!(File::open(self.book.root.join(source)).map_err(|_| Error::FileNotFound(source.to_owned())));
let mut content = vec!();
try!(f.read_to_end(&mut content).map_err(|_| Error::Render("error while reading image file")));
try!(zipper.write(dest, &content, true));
}
if let Ok(epub_file) = self.book.options.get_path("output.epub") {
let res = try!(zipper.generate_epub(self.book.options.get_str("zip.command").unwrap(), &epub_file));
Ok(res)
} else {
Err(Error::Render("no output epub file specified in book config"))
}
}
/// Render the titlepgae
fn render_titlepage(&self) -> Result<String> {
let template = mustache::compile_str(if self.book.options.get_i32("epub.version").unwrap() == 3 {epub3::TITLE} else {TITLE});
let data = self.book.get_mapbuilder("none")
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated HTML in titlepage was not utf-8 valid")),
Ok(res) => Ok(res)
}
}
/// Render toc.ncx
fn render_toc(&self) -> Result<String> {
let mut nav_points = String::new();
for (n, ref title) in self.toc.iter().enumerate() {
let filename = filenamer(n);
let id = format!("navPoint-{}", n + 1);
nav_points.push_str(&format!(
" <navPoint id=\"{}\">
<navLabel>
<text>{}</text>
</navLabel>
<content src = \"{}\" />
</navPoint>\n", id, title, filename));
}
let template = mustache::compile_str(TOC);
let data = self.book.get_mapbuilder("none")
.insert_str("nav_points", nav_points)
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated HTML in toc.ncx was not valid utf-8")),
Ok(res) => Ok(res)
}
}
/// Render content.opf
fn render_opf(&mut self) -> Result<String> {
// Optional metadata
let mut cover_xhtml = String::new();
let mut optional = String::new();
if let Ok(s) = self.book.options.get_str("description") {
optional.push_str(&format!("<dc:description>{}</dc:description>\n", s));
}
if let Ok(s) = self.book.options.get_str("subject") {
optional.push_str(&format!("<dc:subject>{}</dc:subject>\n", s));
}
if let Ok(ref s) = self.book.options.get_path("cover") {
optional.push_str(&format!("<meta name = \"cover\" content = \"{}\" />\n",
self.html.handler.map_image(Cow::Borrowed(s))));
cover_xhtml.push_str(&format!("<reference type=\"cover\" title=\"Cover\" href=\"cover.xhtml\" />"));
}
// date
let date = chrono::UTC::now().format("%Y-%m-%dT%H:%M:%SZ");
// uuid
let uuid = uuid::Uuid::new_v4().to_urn_string();
let mut items = String::new();
let mut itemrefs = String::new();
let mut coverref = String::new();
if self.book.options.get("cover").is_ok() {
items.push_str("<item id = \"cover_xhtml\" href = \"cover.xhtml\" media-type = \"application/xhtml+xml\" />\n");
coverref.push_str("<itemref idref = \"cover_xhtml\" />");
}
for n in 0..self.toc.len() {
let filename = filenamer(n);
items.push_str(&format!("<item id = \"{}\" href = \"{}\" media-type=\"application/xhtml+xml\" />\n",
to_id(&filename),
filename));
itemrefs.push_str(&format!("<itemref idref=\"{}\" />\n", to_id(&filename)));
}
// oh we must put cover in the manifest too
if let Ok(ref cover) = self.book.options.get_path("cover") {
let format = self.get_format(cover);
let s= self.html.handler.map_image(Cow::Borrowed(cover));
items.push_str(&format!("<item {} media-type = \"image/{}\" id =\"{}\" href = \"{}\" />\n",
if self.book.options.get_i32("epub.version").unwrap() == 3 { "properties=\"cover-image\"" } else { "" },
format,
to_id(s.as_ref()),
s));
}
// and the other images
for image in self.html.handler.images_mapping().values() {
let format = self.get_format(image);
items.push_str(&format!("<item media-type = \"image/{}\" id = \"{}\" href = \"{}\" />\n",
format, to_id(image), image));
}
let template = mustache::compile_str(if self.book.options.get_i32("epub.version").unwrap() == 3 {epub3::OPF} else {OPF});
let data = self.book.get_mapbuilder("none")
.insert_str("optional", optional)
.insert_str("items", items)
.insert_str("itemrefs", itemrefs)
.insert_str("date", date)
.insert_str("uuid", uuid)
.insert_str("cover_xhtml", cover_xhtml)
.insert_str("coverref", coverref)
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated HTML in content.opf was not valid utf-8")),
Ok(res) => Ok(res)
}
}
/// Render cover.xhtml
fn render_cover(&mut self) -> Result<String> {
if let Ok(cover) = self.book.options.get_path("cover") {
let template = mustache::compile_str(if self.book.options.get_i32("epub.version").unwrap() == 3 {epub3::COVER} else {COVER});
let data = self.book.get_mapbuilder("none")
.insert_str("cover", self.html.handler.map_image(Cow::Owned(cover)).into_owned())
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated HTML for cover.xhtml was not utf-8 valid")),
Ok(res) => Ok(res)
}
} else {
panic!("Why is this method called if cover is None???");
}
}
/// Render nav.xhtml
fn render_nav(&self) -> Result<String> {
let content = self.html.toc.render();
let template = mustache::compile_str(if self.book.options.get_i32("epub.version").unwrap() == 3 {epub3::NAV} else {NAV});
let data = self.book.get_mapbuilder("none")
.insert_str("content", content)
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated HTML in nav.xhtml was not utf-8 valid")),
Ok(res) => Ok(res)
}
}
/// Render a chapter
pub fn render_chapter(&mut self, v: &[Token]) -> Result<String> {
let mut content = String::new();
let mut title = String::new();
for token in v {
content.push_str(&self.parse_token(&token, &mut title));
self.html.render_side_notes(&mut content);
}
self.html.render_end_notes(&mut content);
if title.is_empty() {
if self.html.current_numbering >= 1 {
let number = self.html.current_chapter[0] + 1;
title = try!(self.book.get_header(number, ""));
} else {
return Err(Error::Render("chapter without h1 tag is not OK if numbering is off"));
}
}
self.toc.push(title.clone());
let template = mustache::compile_str(try!(self.book.get_template("epub.template")).as_ref());
let data = self.book.get_mapbuilder("none")
.insert_str("content", content)
.insert_str("chapter_title", title)
.build();
let mut res:Vec<u8> = vec!();
template.render_data(&mut res, &data);
match String::from_utf8(res) {
Err(_) => Err(Error::Render("generated HTML was not utf-8 valid")),
Ok(res) => Ok(res)
}
}
fn parse_token(&mut self, token: &Token, title: &mut String) -> String {
match *token {
Token::Header(n, ref vec) => {
if n == 1 {
if self.html.current_hide || self.html.current_numbering == 0 {
if title.is_empty() {
*title = self.html.render_vec(vec);
} else {
self.book.logger.warning("EPUB: detected two chapter titles inside the same markdown file...");
self.book.logger.warning("EPUB: ...in a file where chapter titles are not even rendered.");
}
} else {
let res = self.book.get_header(self.html.current_chapter[0] + 1, &self.html.render_vec(vec));
let s = res.unwrap();
if title.is_empty() {
*title = s;
} else {
self.book.logger.warning("EPUB: detected two chapters inside the same markdown file.");
self.book.logger.warning(format!("EPUB: conflict between: {} and {}", title, s));
}
}
}
self.html.parse_token(token)
},
_ => self.html.parse_token(token)
}
}
// Get the format of an image file, based on its extension
fn get_format(&self, s: &str) -> &'static str {
let format = if let Some(ext) = Path::new(s).extension() {
match ext.to_string_lossy().as_ref() {
"png" => Some("png"),
"jpg" | "jpeg" => Some("jpeg"),
"gif" => Some("gif"),
_ => None,
}
} else {
None
};
match format {
Some(s) => s,
None => {
self.book.logger.warning(format!("EPUB: could not guess the format of {} based on extension. Assuming png.", s));
"png"
}
}
}
}
// generate an id compatible string, replacing / and . by _
fn to_id(s: &str) -> String {
s.replace(".", "_").replace("/", "_")
}
/// Generate a file name given an int
fn filenamer(i: usize) -> String {
format!("chapter_{:03}.xhtml", i)
}