Epub-Generator
Bisher habe ich meine Ebooks mit pandoc erzeugt. Funktioniert, aber das Tool optimiert meine freie Gestaltung weg. Darum habe ich mit Tcl/Tk das Kommandozeilen-Tool epubgen.tcl gemacht, das aus Standard-HTML ein Epub erzeugt. Epub ist im Grund nur eine Zip-Datei, deren Aufbau bei Wikipedia nachgeschlagen werden kann.
Der Buch-Autor wird im Kopfbereich des HTML mit <meta name="author" content="Nachname, Vorname" />
notiert.
Das HTML wird anhand der Haupt-Überschriften <h1>
...</h1>
in Epub-Kapitel zerteilt. Um auch bei Ebene Unter-Überschriften <h2>
...</h2>
zu unterteilen dient die Angabe <meta name="splitlevel" content="2" />
. Reicht je nach Bedarf bis Ebene 6.
Um an beliebiger Stelle zu unterteilen, werden – zwischen den Block-Elementen <p>
...</p>
die HTML-Kommentare <!--split-->
eingefügt.
Anwendung
tclsh epubgen.tcl Beispiel.htm
produziert Beispiel.epub
Randbedingungen
Das Kommandozeilentool zip muss im Suchpfad erreichbar sein.
CSS-Angaben müssen im Kopfbereich des HTML in <style>
...</style>
stehen.
Referenzierte Bilder (etwa <img src="hi.gif" />
) müssen im gleichen Verzeichnis existieren, ebenso fonts (im style-Bereich etwa mit url(garamond.otf)
referenziert).
Falls zu einer Datei Beispiel.htm eine gleichnamige Grafik Beispiel.jpg existiert (Dateiendung immer .jpg
), so wird diese als Titelbild ins Epub eingearbeitet. Dazu muss das ImageMagick-Tool convert im Suchpfad erreichbar sein.
Zur Sicherheit kann das produzierte Epub mit dem Kommandozeilentool epubcheck überprüft werden.
Nachtrag 24.4.17 – Fehler entfernt (Inhaltsverzeichnis mit mehreren Ebenen war fehlerhaft verschachtelt), Code aktualisiert.
Nachtrag 2 – mit diesem Tool habe ich Karl Marxʼ Kapital von HTML nach Epub gebracht. Für meinen persönlichen Gebrauch die Struktur auf Vordermann gebracht und lesefreundlich formatiert (Schriftschnitt Garamond, open source). Fußnoten entfernt, Vorworte drin gelassen. Das Werk ist gemeinfrei, darum darf ich es voller Freude öffentlich verfügbar machen!
#!/usr/bin/tclsh lassign $argv html namespace path ::tcl::mathop proc echo args {puts $args} if {[string tolower [file extension $html]] ni {.htm .html .xhtml}} then { return -code error [list $html mus be a HTML file!] } if {![file exists $html]} then { return -code error [list HTML file $html does not exist!] } set epub [file root $html].epub file delete -force $epub apply {{html args} { foreach ext $args { if {[file exists [file root $html]$ext]} then { exec convert -resize x900 [file root $html]$ext cover.jpg break } } }} $html .jpg .jpeg .png .gif .tif .tiff .JPG .JPEG .PNG .GIF .TIF .TIFF proc strcat args { append result {*}$args } proc fileToString file { set chan [open $file r] set result [read $chan] close $chan set result } proc uuid {} { for {set i 0} {$i < 32} {incr i} { append result [format %x [expr {int(rand()*16)}]] if {[incr x] in {8 12 16 20}} then { append result - } } set result } set urnUuid [uuid] # set urnUuid [fileToString /proc/sys/kernel/random/uuid] proc stringToFile {str file} { set chan [open $file w] puts -nonewline $chan $str close $chan } proc coverGiven? {{img cover.jpg}} { file exists $img } proc coverImage {} { return -level 0 cover.jpg } proc element {name {atts {}} args} { strcat <$name \ {*}[lmap {att val} $atts { subst {\n $att="$val"} }]\ {*}[if {[llength $args] == 0} then { list " " /> } else { list > {*}[lmap el $args { string map [list \n "\n "] \n[string trim $el] }] \n</$name> }] } proc xmlDoc el { subst {<?xml version="1.0" encoding="UTF-8"?>\n$el} } proc fileToMediaType file { dict get { .gif image/gif .png image/png .jpg image/jpeg .jpeg image/jpeg .woff application/font-woff .woff2 application/font-woff2 .ttf application/x-font-truetype .svg image/svg+xml .otf application/x-font-opentype } [string tolower [file extension $file]] } proc htmlDoc el { join [list\ {<?xml version="1.0" encoding="UTF-8"?>}\ {<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"}\ { "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">}\ $el] \n } proc coverDoc {{img cover.jpg}} { htmlDoc\ [element html {xmlns http://www.w3.org/1999/xhtml}\ [element head {}\ [element title {} Cover]\ [element style {type text/css} { img { max-width: 100%; max-height: 100%; } body { padding: 0px; margin: 0px; } div { display: table; width: 100%; height: 100%; } div div { display: table-cell; text-align: center; vertical-align: middle; } }]]\ [element body {}\ [element div {}\ [element div {}\ [element img\ [list src $img alt "cover image"]]]]]] } # ==== proc tocListToNest {_toc {level 1}} { upvar $_toc toc set result {} while {[llength $toc] > 0} { if {[lindex $toc 1] > $level} then { lset result end [concat [lindex $result end] [tocListToNest toc [+ $level 1]]] } elseif {[lindex $toc 1] < $level} then break else { set toc [lassign $toc label - target] lappend result [list $label $target] } } set result } proc tocElementToMarkup el { upvar navPointID navPointID upvar navPlayOrder navPlayOrder set data [lassign $el label file] element navPoint [list\ id navPoint-[incr navPointID]\ playOrder [incr navPlayOrder]]\ [element navLabel {}\ [element text {} $label]]\ [element content [list src $file]]\ {*}[lmap subEl $data { tocElementToMarkup $subEl }] } proc navMap toc { set navPointID 0 set navPlayOrder 0 set nestedToc [tocListToNest toc] element navMap {} {*}[lmap el $nestedToc { tocElementToMarkup $el }] } # === proc zip args { exec zip {*}$args } stringToFile application/epub+zip mimetype zip -m0Xq $epub mimetype file mkdir META-INF stringToFile\ [xmlDoc\ [element container { xmlns urn:oasis:names:tc:opendocument:xmlns:container version 1.0 } [element rootfiles { } [element rootfile { full-path content.opf media-type application/oebps-package+xml }]]]] META-INF/container.xml zip -mr9X $epub META-INF/ proc srcPart {src element} { # return element content of XML source, # e.g. range "head" or "body" of HTML document. set idx0 [string first <$element $src] if {$idx0 >= 0} then { set idx1 [string first > $src $idx0] if {$idx1 >= 0} then { incr idx1 set idx2 [string first </$element $src $idx1] if {$idx2 >= 0} then { incr idx2 -1 string range $src $idx1 $idx2 } } } } proc splitByComment src { # return splices of document delimited by string "<!--split-->" set idx [string first <!--split--> $src] if {$idx < 0} then { list $src } else { list [string range $src 0 $idx-1]\ {*}[splitByComment [string range $src $idx+12 end]] } } proc withoutComments src { regsub -all {<!--(?!split-->).*?-->} $src "" } set source [fileToString $html] set headSrc [srcPart $source head] set bodySrc [withoutComments [srcPart $source body]] set cssText [srcPart $headSrc style] stringToFile $cssText style.css zip -m9 $epub style.css set title [string trim [srcPart $headSrc title]] set metaSrc [regexp -inline -all {<meta [^>]*>} $headSrc] set splitlevel 1 set author unknown foreach el $metaSrc { if {[string first {name="author"} $el] >= 0} then { regexp {content="\s*([^"]+)\s*"} $el - author } elseif {[string first {name="splitlevel"} $el] >= 0} then { regexp {content="\s*([^"]+)\s*"} $el - splitlevel } } set sections\ [lmap el\ [regexp -inline -all -indices "<h\[1-$splitlevel\]" $bodySrc] { lindex $el 0 }] set parts [lmap i0 [concat 0 $sections] i1 [concat $sections end] { string trim [string range $bodySrc $i0 $i1-1] }] # vgh-quakenbrueck@vgh.de # tobias.bleischwitz@vgh.de set count 0 set navCount 0 set toc "" set metaData "" set manifestData "" set spineData "" set guideData "" append metaData\ \n [element dc:identifier {id epub-id-1} urn:uuid:$urnUuid]\ \n [element dc:title {id epub-title-1} $title]\ \n [element dc:language {} de-DE]\ \n [element dc:creator [list opf:role aut opf:file-as $author]\ [join [lreverse [lmap part [split $author ,] { string trim $part }]]]] if {[coverGiven?]} then { stringToFile [coverDoc] cover.htm zip -m $epub cover.htm cover.jpg append manifestData\ \n [element item {id cover href cover.htm media-type application/xhtml+xml}]\ \n [element item {id cover-image href cover.jpg media-type image/jpeg}] append spineData \n [element itemref {idref cover linear no}] append guideData \n [element reference {type cover title Cover href cover.htm}] append metaData \n [element meta {name cover content cover-image}] } # collect resource files (images, fonts) set imageSrc [regexp -inline -all {<img\s[^>]*?/\s*>} $bodySrc] set externalFiles [lmap src $imageSrc { regexp {<.*src="(.*?)".*?>} $src - file return -level 0 $file }] # append css ressource file names to resource list set cssFileSrc [regexp -inline -all {url\(.+?\)} $cssText] lappend externalFiles {*}[lmap src $cssFileSrc { string map [list ./ "" \" "" ' ""] [string range $src 4 end-1] }] append manifestData\ \n [element item {id ncx href toc.ncx media-type application/x-dtbncx+xml}]\ \n [element item {id style href style.css media-type text/css}]\ {*}[if {[llength $imageSrc] > 0} then { list \n }]\ [join\ [lmap file [lsort -unique $externalFiles] { zip -r9 $epub $file element item [subst { id [file rootname [file tail $file]] href $file media-type [fileToMediaType $file] }] }] \n] set chapterTitle $title foreach el $parts { foreach splitEl [splitByComment $el] { if {$splitEl ne ""} then { # set splitEl [withoutComments $splitEl] set fileID part[format %04d [incr count]] set fileName $fileID.xhtml append manifestData \n\ [element item [subst {id $fileID href $fileName media-type application/xhtml+xml}]] append spineData \n [element itemref [subst {idref $fileID}]] if {[regexp\ {<h([1-6])[^>]*>\s*([^<]*?)\s*</h\1>}\ $splitEl - headLevel chapterTitle]} then { lappend toc $chapterTitle $headLevel $fileName } stringToFile\ [htmlDoc\ [element html {lang de xml:lang de xmlns http://www.w3.org/1999/xhtml}\ [element head {}\ [element meta {http-equiv Content-Type content "text/html; charset=utf-8"}]\ [element meta {http-equiv Content-Style-Type content text/css}]\ [element meta {name DC.language content de}]\ [element title {} $chapterTitle]\ [element link {type text/css rel stylesheet href style.css}]]\ [element body {} $splitEl]]]\ $fileName zip -m $epub $fileName } } } set packageData [element package {version 2.0 xmlns http://www.idpf.org/2007/opf unique-identifier epub-id-1}\ [element metadata {xmlns:dc http://purl.org/dc/elements/1.1/ xmlns:opf http://www.idpf.org/2007/opf}\ $metaData]\ [element manifest {} $manifestData]\ [element spine {toc ncx} $spineData]\ [element guide {} $guideData] ] stringToFile [xmlDoc $packageData] content.opf zip -m $epub content.opf set ncxData\ [element ncx {version 2005-1 xmlns http://www.daisy.org/z3986/2005/ncx/}\ [element head {}\ [element meta [subst {name dtb:uid content urn:uuid:$urnUuid}]]\ [element meta {name dtb:depth content 1}]\ [element meta {name dtb:totalPageCount content 0}]\ [element meta {name dtb:maxPageNumber content 0}]\ [element meta {name cover content cover-image}]]\ [element docTitle {}\ [element text {} $title]]\ [navMap $toc]] stringToFile [xmlDoc $ncxData] toc.ncx zip -m $epub toc.ncx
Läuft unter Linux, sollte unter Windows ebenfalls funktionieren. Viel Spaß beim Ausprobieren!
Montag, den 3. April 2017, um 13 Uhr 24
<< | Heimatseite | Verzeichnis | Stichworte | Autor | >>