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 | >>