epubCreator "Pride and Prejudice" "Jane Austen" p_and_p.xhtml cover.jpg img1.jpg img2.jpgAn epub file is essentially a zip file with some metadata files and one or more xhtml files with the book's content.
ak - 2014-03-14 23:42:26Tcllib contains a package "zipfile::encode (doc)" that can obviate the need for 'exec zip'. It requires Trf and zlibtcl though. Note that while Tcl 8.6 provides zip functions in-core, the Tcllib package currently makes no use of that.KPV "zipfile::encode (doc)" won't work because of epub's weird requirement that the first file has to be uncompressed.
clif flynt - 2014-07-14 I modified and extended Keith's code a bit. After some tweaking, I've got it passing the epubcheck validator, accepting multiple files and a couple other tweaks.Check the comments for the new, expanded command line.
KPV 2018-08-31 -- Inspired by Clif Flynt's changes, I added a bunch more features, including automatically creating a cover image and a TOC. But it's because of ao3ToEpub that I finally got around to updating this page.
##+########################################################################## # # epubCreator.tsh -- command line tool to create an epub version 3.0 file # from text or xhmtml files, an optional cover image, style sheets and images. # # The EPUB Contents Document 3.0.1 spec is at # http://www.idpf.org/epub/301/spec/epub-contentdocs.html # A good description of how an epub (version 2.0) file is organinized is at # http://gbenthien.net/Kindle%20and%20EPUB/epub.php # # by Keith Vetter 2014-03-14 # Clif Flynt, 2014-04-01 # Support for multiple text/html files (multiple chapters) # Support for additional .css file # Support for filename.epub different from "book title.epub" # Support for toc.ncx as well as nav.xhtml # http://www.idpf.org/epub/301/spec/epub-contentdocs.html#sec-xhtml-nav # [NCX is part of Epub 2.0 but inserted for backwards compatibility] # Expanded command line processing # Keith Vetter 2015-12-03 # extract title, author, stylesheets and images from html data files # insert a TOC after cover image # create cover image if none given, requires ImageMagick or Tk # cleaned up few bugs # support multiple CSS files # package require fileutil package require base64 package require textutil set version "0.5" array set E { data {} title {*} author {*} cover {*} images {*} css {*} html {*} output {*} toc 1 verbose 1 tk 0 zip {*} } set usage {usage: epubCreator -data file1.txt file2.xhtml file3.xhtml... epubCreator -data file1.txt file2.xhtml file3.xhtml... -title 'Book Title' -author 'last, first' -cover Cover.jpg -images <additional Images> -css stylesheet.css -toc (0/1) -html (0/1) -verbose (0/1) -tk (0/1) -output BookName.epub -data (required) List of data files to include in the text -title Title for book default: extracts title from <title>...</title> -author Name of author as last, first default: extracts author from <meta name='author'.../> -cover An image file for the cover, use "" for no cover default: a cover image will be created using ImageMagick -images Additional images that might be reference by text default: extracts image tags from all the source files -css An optional css file if you want special formatting default: extracts stylesheets referenced in all the source files -toc 1 include a TOC after the cover page, 0 omit TOC default: 1 include TOC -html 1 if data already HTML, 0 if text default: examines each source files for its format -verbose 1 for more verbose messages -tk Make cover image: 0 use ImageMagick, 1 use Tk if no ImageMagick default: 0 use ImageMagick -output The name for the .epub file, use "" for no output default: uses basename of the first source file By default, epubCreator will examine the source files for title, author, css and images. It will create a cover image, a cover page and table of contents for you. You can disable any of these features by specifying an empty value for the appropriate flag. } array set media_types {"" "" .png image/png .gif image/gif .jpg image/jpeg .jpeg image/jpeg .svg image/svg+xml .css text/css} proc Usage {emsg} { puts stderr $emsg$::usage if {$::tcl_interactive} {error ""} exit 0 } proc INFO {msg} {if {$::E(verbose)} {puts "I: $msg"}} proc WARN {msg} {puts stderr "W: $msg" ; flush stderr} proc ERROR {msg} {puts stderr "E: $msg" ; exit 1 } proc INFO_LIST {who values} { set msg "found [Plural [llength $values] $who]" if {$values ne {}} { append msg ": [join $values {, }]" } INFO $msg } proc ParseArgs {} { global E argv if {"-help" in $argv || "--help" in $argv} { Usage "" } if {[string index [lindex $argv 0] 0] ne "-"} { Usage "Error: bad option [lindex $argv 0]\n\n" } foreach arg $argv { if {([string first "-" $arg] == 0)} { set index [string range $arg 1 end] if {![info exists E($index)]} { Usage "Error: unknown option '$arg'\n\n" } set E($index) {} } else { if {[llength $E($index)] == 0} { set E($index) $arg } else { lappend E($index) $arg } } } if {[llength $E(data)] == 0} { Usage "Error: no input files specified\n\n" } # Allow -verbose, -tk and -toc to be flags without values foreach idx {verbose tk toc} { if {$E($idx) eq ""} { set E($idx) 1 } } INFO "creating epub from [Plural [llength $E(data)] {data file}]" } proc Init {} { global E set guid [guid] if {$E(zip) eq "*" || $E(zip) eq ""} { set E(output,tempdir) [file join [::fileutil::tempdir] "epubCreator_$guid"] } else { set E(output,tempdir) $E(zip) } INFO "tempdir $E(output,tempdir)" ExtractMetadata if {$E(title) eq "*"} { set E(title) "My Ebook" INFO "no title information found, using $E(title)" } if {$E(author) eq "*"} { set E(author) "epubCreator" set E(author,pretty) $E(author) INFO "no author information found, using $E(author)" } else { set E(author,pretty) $E(author) set rest [lassign [split $E(author) ","] last first] if {$rest eq "" && $first ne ""} { set E(author,pretty) "[string trim $first] [string trim $last]" INFO "author pretty name: $E(author,pretty)" } } if {$E(output) eq "*"} { set E(output,final) [file normalize "[file rootname [lindex $E(data) 0]].epub"] } elseif {$E(output) eq ""} { set E(output,final) "" } else { set E(output,final) [file normalize "[file rootname $E(output)].epub"] } set E(epub) EPUB set E(epub,tempdir) [file join $E(output,tempdir) $E(epub)] set E(opf,name) [file join $E(epub) package.opf] set E(opf,tempname) [file join $E(output,tempdir) $E(opf,name)] set E(nav,tempname) [file join $E(epub,tempdir) "nav.xhtml"] set E(ncx,tempname) [file join $E(epub,tempdir) "toc.ncx"] set E(mimetype) mimetype set E(mimetype,tempname) [file join $E(output,tempdir) $E(mimetype)] set E(meta-inf) META-INF set E(meta-inf,tempdir) [file join $E(output,tempdir) $E(meta-inf)] set E(meta-inf,tempname) [file join $E(meta-inf,tempdir) container.xml] set E(date) [clock format [clock seconds] -gmt 1 -format "%Y-%m-%dT%TZ"] set E(guid) "ebook:$guid" if {$E(cover) eq "*" && ! [::BlankCover::CanMakeCoverImage]} { INFO "skipping making cover image, requires ImageMagick" set E(cover) "" } if {$E(cover) eq "*"} { set E(cover,source) [file join $E(epub,tempdir) "_created_cover.jpg"] } else { set E(cover,source) $E(cover) } set E(cover,name) [file tail $E(cover,source)] set E(cover,media_type) $::media_types([file extension $E(cover,source)]) set E(manifest,stylesheets) " <!-- stylesheet.css items -->" set E(manifest,images) " <!-- image items -->" set E(css,link) " <!-- link to stylesheet.css -->" file delete -force $E(output,tempdir) file mkdir $E(output,tempdir) file mkdir $E(meta-inf,tempdir) file mkdir $E(epub,tempdir) file mkdir [file dirname $E(output,final)] return } proc MakeEpubFiles {} { global E MakeOPF_Stylesheets MakeOPF_Images set ncxs "" set navs "" set E(manifest,sources) {} set E(opf,spine_items) "" set play_order -1 if {$E(cover,source) ne ""} { if {$E(cover) eq "*"} { ::BlankCover::MakeCoverImage $E(title) $E(author,pretty) $E(cover,source) } else { INFO "adding cover image: [file tail $E(cover,source)]" file copy $E(cover,source) $E(epub,tempdir) } incr play_order set html_name [MakeCoverPage] set navlabel "Cover Page" append navs [subst $::NAV_XHTML1] append ncxs [subst $::CONTENT_NCX1] } else { INFO "skipping cover page" } # Add our table of contents (nav.xhtml) unless user asks not to or if # there's only 1 source file if {$E(toc) == 2 || ($E(toc) && [llength $E(data)] > 1)} { INFO "adding TOC" incr play_order set html_name [file tail $E(nav,tempname)] set navlabel "Table of Contents" append navs [subst $::NAV_XHTML1] append ncxs [subst $::CONTENT_NCX1] } else { INFO "skipping TOC" } # Add all our source files for {set idx 0} {$idx < [llength $E(data)]} {incr idx} { # 1. add item into manifest # 2. add item into spine # 3. extract title for nav and toc # 4. add item into nav.xhtml # 5. add item into toc.ncx # 6. copy file to $E(epub,tempdir) to be zipped up # a. possibly convert to xhtml set data_file [lindex $E(data) $idx] INFO "processing $data_file" set html_name "[file tail [file rootname $data_file]].xhtml" set manifest_id "id_file_$idx" append E(manifest,sources) \ " <item id='$manifest_id' href='$html_name' media-type='application/xhtml+xml'/>\n" append E(opf,spine_items) " <itemref idref='$manifest_id'/>\n" set navlabel [GuessChapterTitles $data_file [expr {$idx + 1}]] incr play_order append navs [subst $::NAV_XHTML1] append ncxs [subst $::CONTENT_NCX1] set tempname [file join $E(epub,tempdir) $html_name] CopyTextFile $data_file $tempname } WriteAllData $E(mimetype,tempname) "application/epub+zip" WriteAllData $E(meta-inf,tempname) [subst $::CONTAINER_XML] WriteAllData $E(opf,tempname) [MakeOPF] WriteAllData $E(nav,tempname) "$::NAV_XHTML0\n$navs$::NAV_XHTML2" WriteAllData $E(ncx,tempname) "[subst $::CONTENT_NCX0]\n$ncxs\n$::CONTENT_NCX2" } ##+########################################################################## # # TextToHtml -- Converts text files to html by adding correct header and footer # proc TextToHtml {src} { global E set data [ReadAllData $src] if {! [IsHtmlData $data]} { INFO "converting $src to html" set data [string map {& & < < > > \x22 " ' '} $data] ; list regsub -all -line {^$} $data {</p><p>} data set data "<p>$data</p>" set data [MakeHtmlPage $data $E(title)] } else { set data [FixHtml $data] if {! [HasHtmlHeader $data]} { INFO "adding header" set data [MakeHtmlPage $data $E(title)] } } return $data } proc IsHtmlData {data} { if {$::E(html) ne "*"} { return $::E(html) } if {[string first "<html" $data] > -1} { return 1 } if {[string first "<p" $data] > -1} { return 1 } return 0 } proc HasHtmlHeader {data} { if {[string first "<html" $data] > -1} { return 1 } return 0 } proc FixHtml {data} { # Found some pages had "<br >" without closing slash return [regsub -all {<br *>} $data {<br/>}] } proc MakeHtmlPage {body title} { global E set html "[subst $::HTML_TEMPLATE]" return $html } proc Plural {num word} { if {$num != 1} {append word "s"} return "$num $word" } proc MakeCoverPage {} { global E set html_name "cover.xhtml" set tempname [file join $::E(epub,tempdir) $html_name] set fout [open $tempname w] puts $fout [MakeHtmlPage "<img src=\"$E(cover,name)\"/>" $E(title)] close $fout return $html_name } proc MakeOPF {} { global E set opf [subst $::PACKAGE_OPF] if {$E(cover,source) eq ""} { INFO "removing cover page from opf" regsub -all -line {^.*id_cover.*$} $opf "<!-- \& -->" opf } if {! $E(toc)} { INFO "removing TOC from spine" regsub -all -line {^.*<itemref idref=.id_navpage.*$} $opf "<!-- \& -->" opf } return $opf } proc MakeOPF_Images {} { global E if {[llength $E(images)] == 0} return set E(manifest,images) "" for {set i 0} {$i < [llength $E(images)]} {incr i} { set fname [lindex $E(images) $i] file copy $fname $E(epub,tempdir) set tailname [file tail $fname] set media $::media_types([file extension $fname]) set id "id_image_$i" append E(manifest,images) \ " <item href='$tailname' id='$id' media-type='$media'/>\n" INFO "adding image $tailname" } } proc MakeOPF_Stylesheets {} { global E if {[llength $E(css)] == 0} return set E(manifest,stylesheets) "" set E(css,link) "" for {set i 0} {$i < [llength $E(css)]} {incr i} { set fname [lindex $E(css) $i] file copy $fname $E(epub,tempdir) set tailname [file tail $fname] set id "id_css_$i" set media "text/css" append E(manifest,stylesheets) \ " <item href='$tailname' id='$id' media-type='$media'/>\n" append E(css,link) " <link href='$tailname' type='$media' rel='stylesheet'/>\n" INFO "adding stylesheet $tailname" } } ##+########################################################################## # # ZipEpub -- zips up all the files in E(output,tempdir) making sure that # mimetype is first and uncompressed, followed by everything else. # # ::zipfile::encode v0.3 doesn't work--no way to ensure mimetype is # first and uncompressed. # proc ZipEpub {} { global E if {$E(output) eq ""} { INFO "skipping zipping" return } INFO "zipping $E(output,final)" set old_pwd [pwd] cd $E(output,tempdir) catch {file delete $E(output,final)} catch {package require zipfile::encode 0.4} ;# Not yet released if {[info commands ::zipfile::encode] ne ""} { set zip [::zipfile::encode epubCreator_zipper] $zip comment: "Created with epubCreator on $E(date)" INFO " zip file: $E(mimetype) nocompress=true" $zip file: $E(mimetype) 0 $E(mimetype) 1 INFO " zip file: $E(meta-inf)/* $E(epub)/*" foreach fname [glob $E(meta-inf)/* $E(epub)/*] { $zip file: $fname 0 $fname } $zip write $E(output,final) } else { INFO " zip -0X $E(output,final) $E(mimetype)" exec zip -0X $E(output,final) $E(mimetype) INFO " zip -rX $E(output,final) $E(meta-inf)/ $E(epub)/" exec zip -rX $E(output,final) $E(meta-inf)/ $E(epub)/ } cd $old_pwd } proc CopyTextFile {src dest} { WriteAllData $dest [TextToHtml $src] } proc WriteAllData {fname data} { INFO "copying [file tail $fname]" set fout [open $fname w]; puts -nonewline $fout $data; close $fout; } proc ReadAllData {fname} { if {! [file exists $fname]} { ERROR "file $fname does not exists" } set fin [open $fname r] set data [read $fin] ; list close $fin return $data } proc Cleanup {} { global E if {$E(output) eq ""} { INFO "skipping cleanup" return } INFO "cleanup $E(output,tempdir)" file delete -force -- $E(output,tempdir) } ##+########################################################################## # # Searches data file for title, author and links to images and stylesheets # proc ExtractMetadata {} { global E if {$E(html) == 0} return if {$E(title) ne "*" && $E(author) ne "*" && $E(css) ne "*" && $E(images) ne "*"} return set all(stylesheet) {} set all(image) {} foreach data_name $E(data) { set html [ReadAllData $data_name] ; list if {! [IsHtmlData $html]} continue if {$E(title) eq "*"} { set n [regexp {<title>(.*?)</title>} $html . title] if {$n} { set E(title) $title INFO "found title: $E(title)" } } if {$E(author) eq "*"} { # <meta name="author" content="Keith Vetter"/> foreach meta [regexp -all -inline -indices {<meta [^>]*name=.author[^>]*>} $html] { set author [ExtractAttributeForTag [string range $html {*}$meta] meta content] if {$author ne ""} { set E(author) [lindex $author 0] INFO "found author: $E(author)" break } } } # Pick up css and images set dirname [file dirname $data_name] foreach {who tag attr} {stylesheet link href image img src} { set all_values {} foreach tag [regexp -all -inline "<${tag}\\M.*?>" $html] { set n [regexp " $attr=(\[\"'])(.*?)\\1" $tag a b value] if {$n && $value ni $all_values} { lappend all_values $value } } foreach path $all_values { set actual [FindResourceFile $who $dirname $path] if {$actual ne "" && $actual ni $all($who)} { lappend all($who) $actual } } } } if {$E(css) eq "*"} { set E(css) $all(stylesheet) INFO_LIST stylesheet $E(css) } if {$E(images) eq "*"} { set E(images) $all(image) INFO_LIST image $E(images) } } ##+########################################################################## # # Insures path exists, either as absolute path or directly in dirname # proc FindResourceFile {type dirname path} { if {[file pathtype $path] eq "relative" && [llength [file split $path]] > 1} { WARN "skipping $type: directory not allowed in path: $path" return "" } set full [file join $dirname $path] if {[file exists $full]} { return $full } WARN "skipping $type: cannot locate file: $path" return "" } ##+########################################################################## # # Returns the attr value for each instance of <tag> in html # proc ExtractAttributeForTag {html tag attr} { set all {} foreach tag [regexp -all -inline "<${tag}\\M.*?>" $html] { set n [regexp " $attr=(\[\"'])(.*?)\\1" $tag a b value] if {$n && $value ni $all} { lappend all $value } } return $all } ##+########################################################################## # # Tries to extract the <title>...</title> text to use # as chapter title # proc GuessChapterTitles {fname chapter} { set data [ReadAllData $fname] set navlabel "Chapter $chapter" set n [regexp {<title>(.*?)</title>} $data . navlabel] if {! $n} { regexp {<h3[^>]+?title=['"](.*?)["']} $data . navlabel } INFO "chapter $chapter title: => $navlabel" return $navlabel } ##+########################################################################## # # guid -- like uuid::uuid generate but that functions displays a warning on OSX # proc guid { } { if {![info exists ::GuiD__SeEd__VaR]} {set ::GuiD__SeEd__VaR 0} if {![info exists ::GuiD__MaChInFo__VaR]} { set ::GuiD__MaChInFo__VaR $::tcl_platform(user)[info hostname]$::tcl_platform(machine)$::tcl_platform(os) } set MachInfo [expr {rand()}]$::GuiD__SeEd__VaR$::GuiD__MaChInFo__VaR binary scan $MachInfo h* MachInfo_Hex set CmdCntAndSeq [string range "[info cmdcount]$::GuiD__SeEd__VaR$::GuiD__SeEd__VaR" 0 8] binary scan [expr {rand()}] h* Rand_Hex set guid [format %2.2x [clock seconds]] # Pick though clock clicks for a good sequence. append guid -[string range [format %2.2x [clock clicks]] 0 3] \ -[string range [format %2.2x $CmdCntAndSeq] 0 3] \ -[string range $Rand_Hex 3 6] \ -[string range $MachInfo_Hex 0 11] incr ::GuiD__SeEd__VaR return [string toupper $guid] } # # Makes a cover image # namespace eval ::BlankCover { variable blank_cover_tile { /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABAAEADASIA AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3 ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3 uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi2xgE jIHON3OP8/yo6r1XbnGM8etKQQ27IJU9D1xik8zcMP8Ad7kt+h44rzj0BTv9eOON3akGMlc5PoeK aApG4EA9cEZ/Wg7QoJU9PmwQB/OmIdnBzvy3Rffnpn1o24YqPlOfb0pOn1bg8DP/ANel3Zx13diT xkdv8/0oATJLbDj5ufmX6/T0zQAQmBlk6cnHHcH/AD+VKwyGz97sev8AkUg+QsR0/un9KAFHylct n+Erx1z0/nSjKhMgjjGfX0P+fWmA4UFhtYdh049qdtYkjI+ZuRigBeA/zZLZ7df85pM7SoG4svqP wpvDBApP17A9f8/hSjnqoI/3u/8A+ugALMRgSAEtyCMHPT+Qo+Zh/ex7f05oIThiCgHfOMUrgKMg 5IyeB/n1/WgAUEA7Ccngg8YpCzFQyx/N6/h+ff0pMlS4xtwMkfT3pzKQSoIBAycnvQAhAIITvwRg YH+eaXcQD949+RyPr+vNNIV9qkDdkAkj/GlbeDsz1/CgAwCADkZbqeo4PT9KcRkMVLYP94f4U0sF 3Y2gd8c/h/npRhQ+VyT1BxmgA6EfJyD/AHcZ4/8A1UctlQHJwQRnH+e9OAJ+8/fjAz+tMVTjrweD u7n/ADmgB4Zy2SMlh03Y/KkAxtGWAxjhenp19qAwDbhkY7fl2pGI2NhFI/u46D/P86AH9FA3de56 nnimHdtXOQD684P50oXPzdGJ7etKV5+9nBzwhoGxozxhsdOo9KRwME4KnHTIOfWnKwO4A8f71Ivz YGMhuQB9D3oEA2hf4lOecfKBz7UAbWYZbvzmjJCllOG9ex+lAGGUIARjHTjFMAO0gk7d3B4PI55F BbnCk5xzlqXjnKk7eq+n/wBagEbfRlPYc4/H8aQH/9k=} proc CanMakeCoverImage {} { if {$::E(tk) > 1} { set ::auto_execs(convert) "" } ;# Hidden way to force Tk if {[auto_execok convert] ne "" && [auto_execok montage] ne ""} { return 1 } if {$::E(tk) == 0} { return 0 } foreach pkg {Tk Img} { set n [catch [list package require $pkg] emsg] if {$n} { WARN "cannot load $pkg: $emsg" return 0 } wm withdraw . } return 1 } proc MakeCoverImage {title author output_image} { if {[auto_execok convert] ne ""} { INFO "creating cover image using ImageMagick" MakeBlankCover $output_image WriteOntoBlankCover $title $author $output_image } else { MakeCoverImage_Tk $title $author $output_image } } proc MakeBlankCover {output_image} { set fout [open $output_image wb] puts -nonewline $fout [::base64::decode $::BlankCover::blank_cover_tile] close $fout # Tile our blank_cover_tile INFO " montage -mode concatenate -tile 8x12 \$img*96 \$img" exec montage -mode concatenate -tile 8x12 \ {*}[lrepeat [expr {8 * 12}] $output_image] $output_image ;# Add black border around page INFO [sjoin " convert \$img -fill none -stroke black -strokewidth 10 " \ "-draw {rectangle 20 20 492 748} \$img"] exec convert $output_image -fill none -stroke black -strokewidth 10 \ -draw {rectangle 20 20 492 748} $output_image } proc WriteOntoBlankCover {title author output_image} { set font [WhichImageMagickFont] INFO " using ImageMagick font '$font'" if {$font ne ""} { set font "-font $font" } set title [::textutil::adjust $title -length 18 -strictlength true] set author [::textutil::adjust $author -length 18 -strictlength true] set txt "$title\n\nby\n$author" set cmd [list convert $output_image -fill black -stroke black {*}$font] lappend cmd -pointsize 64 -gravity north -annotate +0+100 $txt $output_image INFO [sjoin " convert \$img -fill black -stroke black $font -pointsize 64 " \ "-gravity north -annotate +0+100 \$title \$img"] exec {*}$cmd } proc WhichImageMagickFont {} { # ImageMagick doesn't seem to have consistent font names across systems # so we list all available fonts and search for a Times Roman font. set fin [open "|convert -list font" r] set all [read $fin] ; list catch {close $fin} ;# convert exits with non-zero status set times(all) {} set times(good) {} foreach {. font} [regexp -inline -all -line {^.*Font: (.*Times.*)$} $all] { set font_ [string map {- ""} $font] if {$font_ eq "Times"} {return $font} if {$font_ eq "TimesRoman"} { return $font } if {$font_ eq "TimesNewRoman"} { return $font } lappend times(all) $font if {[string match -nocase "*italic" $font]} continue if {[string match -nocase "*I" $font]} continue if {[string match -nocase "*oblique" $font]} continue if {[string match -nocase "*O" $font]} continue lappend times(good) $font } if {$times(good) ne {}} { return [lindex $times(good) 0] } return [lindex $times(all) 0] } proc MakeCoverImage_Tk {title author output_image} { if {[package version Img] eq ""} { ERROR "requires Img package" } INFO "creating cover image using Tk" foreach img [image names] { if {[string match "::cover::*" $img]} { image delete $img } } image create photo ::cover::tile -data [::base64::decode $::BlankCover::blank_cover_tile] image create photo ::cover::blank_cover -width 512 -height 768 ::cover::blank_cover copy ::cover::tile -to 0 0 512 768 set font {Times 40 bold} set title [::textutil::adjust $title -length 18 -strictlength true] set author [::textutil::adjust $author -length 18 -strictlength true] set txt "$title\n\nby\n$author" destroy .c wm deiconify . wm geom . -10000-10000 pack [canvas .c -width 512 -height 768 -bd 0 -highlightthickness 0] .c create image 0 0 -anchor nw -image ::cover::blank_cover .c create rect 20 20 492 748 -fill {} -outline black -width 10 # .c create text 256 50 -font $font -tag a -anchor n -justify center -text $txt set y 50 foreach line [split [string trim $txt] \n] { .c create text 256 $y -font $font -tag b -anchor n -justify center -text $line incr y 50 } ;# Now copy canvas into an image and save it raise . update image create photo ::cover::cover -data .c ::cover::cover write $output_image -format jpeg wm withdraw . destroy .c foreach img [image names] { if {[string match "::cover::*" $img]} { image delete $img } } } } proc sjoin {args} { return [join $args ""] } # # Various XHTML templates # HTML_TEMPLATE -- convert text into xhtml, also used by cover page # CONTAINER_XML -- for META-INF/container.xml # PACKAGE_OPF -- for the EPUB/package.opf file # NAV_XHTML# -- for the nav.xhtml navigation document # CONTENT_NCX# -- for the EPub version 2.0 toc.ncx navigation document # set HTML_TEMPLATE {<?xml version="1.0"?> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:epub="http://www.idpf.org/2007/ops"> <head> <title>$title</title> $::E(css,link) </head> <body> $body </body> </html> } set CONTAINER_XML {<?xml version="1.0"?> <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> <rootfiles> <rootfile media-type="application/oebps-package+xml" full-path="$E(opf,name)" /> </rootfiles> </container> } set PACKAGE_OPF {<?xml version="1.0" encoding="UTF-8"?> <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uuid"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <dc:title>$E(title)</dc:title> <dc:creator>$E(author)</dc:creator> <dc:identifier id="uuid">$E(guid)</dc:identifier> <dc:language>en</dc:language> <meta property="dcterms:modified">$E(date)</meta> <meta name="cover" content="id_cover_image"/> </metadata> <manifest> <item id="id_cover_image" href="$E(cover,name)" media-type="$E(cover,media_type)"/> <item id="id_coverpage" href="cover.xhtml" media-type="application/xhtml+xml"/> <item id="id_navpage" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/> <item id="toc" href="toc.ncx" media-type="application/x-dtbncx+xml" /> $::E(manifest,sources) $::E(manifest,stylesheets) $::E(manifest,images) </manifest> <spine toc="toc"> <itemref idref="id_coverpage"/> <itemref idref="id_navpage"/> $::E(opf,spine_items) </spine> </package> } # EPUB 3.0 section 2.2 EPUB Navigation Document # see http://www.idpf.org/epub/301/spec/epub-contentdocs.html#sec-xhtml-nav set NAV_XHTML0 {<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:epub="http://www.idpf.org/2007/ops"> <head> <title>Table of Contents</title> </head> <body> <nav epub:type="toc" id="toc"> <h1>Table of Contents</h1> <ol>} set NAV_XHTML1 { <li><a href="$html_name">$navlabel</a></li> } set NAV_XHTML2 { </ol> </nav> </body> </html> } # NCX format # see: http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4.1.2 # also: http://gbenthien.net/Kindle%20and%20EPUB/ncx.php set CONTENT_NCX0 {<?xml version="1.0" encoding="UTF-8"?> <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="en"> <head> <meta name="dtb:uid" content="$::E(guid)"/> <meta name="dtb:depth" content="1"/> <meta name="dtb:totalPageCount" content="0"/> <meta name="dtb:maxPageNumber" content="0"/> </head> <docTitle> <text>$E(title)</text> </docTitle> <docAuthor> <text>$E(author)</text> </docAuthor> <navMap>} set CONTENT_NCX1 { <navPoint id="navpoint-$play_order" playOrder="$play_order"> <navLabel> <text>$navlabel</text> </navLabel> <content src="$html_name"/> </navPoint>} set CONTENT_NCX2 {</navMap> </ncx> } proc Main {} { global E set E(when) [clock milliseconds] ParseArgs Init MakeEpubFiles ZipEpub Cleanup set done "created $E(output,final)" if {$E(output) eq ""} { set done "epub in $E(output,tempdir)" } INFO $done INFO "elapsed time: [expr {[clock milliseconds] - $E(when)}]ms" INFO "to upload to Google books, goto https://play.google.com/books/uploads" if {! $E(verbose)} { puts $done } } puts "\nepubCreator v$version\nby Keith Vetter & Clif Flynt\n" if {$tcl_interactive} { set argv {-data _data/epub_1_1.html -author "Keith Vetter" -output ~/FBooks/me.epub -verbose 1} set argv {-data "/tmp/foo_13569879.html" -verbose 1 -output "~/FBooks/me.epub" -title "Another Innocent Bystander" -author "Rose_Milburn"} return } if {"-data" ni $argv || [llength $argv] < 2} { Usage "" } Main exit return