unit uEpub;
interface
uses
//jpeg und XmlDoc wird benötigt, für die Unzip-Funktion hier auch KaZip
Windows, Messages, ...., jpeg, KaZip, XmlDoc;
//Strukturierter Datentyp erleichtert es die Übersicht zu behalten
type contentRec =
record
title :
String;
creator :
String;
publisher :
String;
subject :
String;
description :
String;
metadata :
String;
isbn :
String;
uuid :
String;
asin :
String;
mname :
String;
//für <meta name= cover
mcover :
String;
//für <meta name= cover
mbookid :
String;
opfFile :
String;
opfDir :
String;
opfContent :
String;
coverpath :
String;
ms : TMemoryStream;
html :
Array of Array of string;
end;
type
TEpub =
class
procedure ParseEpub(epubfile:
string;
var cr: contentRec);
private
{ Private declarations }
function ExtractOpfDir(s:
string):
string;
function ExtractHtmlTags(s:
string):
string;
public
{ Public declarations }
end;
var
Epub: TEpub;
implementation
//Beispiel: Unit1 ist Unit meiner Applikation
uses Unit1;
//epubfile ist der Pfad zu meiner *.epub z.B. 'Strobel, Arno - Das Rachespiel.epub'
procedure TEpub.ParseEpub(epubfile:
string;
var cr: contentRec);
var
posStart, posdc, posEnd, pos1, pos2, posfullpath, posidref, poshref, posid, postitle, postype, posmedia: integer;
s, sub1, xdir:
string;
shref, stitle, sid, smedia, sidref, stype:
string;
KaZip1: TKaZip;
index, z, h: integer;
arrTemp:
Array of Array of string;
XmlDoc: TXMLDocument;
this: boolean;
begin
//Zip-Komponente zur Laufzeit implementieren
KaZip1 := TKaZip.Create(
nil);
{****************************************************************************}
{* Container.xml *}
{****************************************************************************}
// Container.xml enthält den Pfad zur wichtigen Datei content.opf
try
KaZip1.Open(epubfile);
index := KAZip1.Entries.IndexOf('
META-INF\Container.xml');
if index < 0
then exit;
KAZip1.ExtractToStream(KAZip1.Entries.Items[
Index], cr.ms);
//Alternative: entpacken statt einlesen: KAZip1.ExtractToFile(KAZip1.Entries.Items[index], 'D:\Ziel\Container.xml');
KaZip1.Active := true;
cr.ms.seek(0,sofromBeginning);
//Die Datei Container.xml ist jetzt im MemoryStream cr.ms und kann per Xml-Parser dursucht werden
XmlDoc := TXMLDocument.Create(Application);
try
XmlDoc.Active := True;
XmlDoc.LoadFromStream(cr.ms);
//Suche: <rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
cr.opfFile := XmlDoc.DocumentElement.ChildNodes['
rootfiles'].ChildNodes['
rootfile'].AttributeNodes['
full-path'].Text;
cr.opfDir := ExtractOpfDir(cr.opfFile);
//wird später noch als relativer Pfad benötigt!
XMLDoc.Active := False;
finally
XmlDoc :=
nil;
end;
cr.ms.Clear;
finally
end;
{****************************************************************************}
{* content.opf *}
{****************************************************************************}
//Ab hier geht es nur noch um Inhalte der Datei content.opf
//Achtung: Pfadangaben sind immer relativ zum Pfad der content.opf !!
try
index := KAZip1.Entries.IndexOf(cr.opfFile);
//auch hier habe ich die Datei content.opf in einen Stream cr.ms eingelesen.
//Alternativ ist das epub-Archiv vielleicht aber auch schon entpackt
KAZip1.ExtractToStream(KAZip1.Entries.Items[
Index], cr.ms);
if index < 0
then exit;
KaZip1.Active := true;
cr.ms.seek(0,sofromBeginning);
//so kann man einen Stream in eine string-Variable übergeben. s bzw cr.opfContent wird aber nur zum testen benötigt
SetString(s, PAnsiChar(cr.ms.Memory), cr.ms.Size);
{set string to get memory}
cr.opfContent := s;
//hier wird der Stream in den Xml-Parser übergeben
XmlDoc := TXMLDocument.Create(Application);
XmlDoc.Active := True;
XmlDoc.LoadFromStream(cr.ms);
cr.ms.Clear;
finally
end;
{****************************************************************************}
{* <Metadata> durchsuchen *}
{****************************************************************************}
//Test: Attribut auslesen: ShowMessage(XmlDoc.DocumentElement.ChildNodes['metadata'].ChildNodes['meta'].Attributes['content']);
//Test: funktioniert nicht wegen dem Doppelpunkt?: ShowMessage(XmlDoc.DocumentElement.ChildNodes['metadata'].ChildNodes['dc:title'].Text);
//Test: funktioniert mit Integer (als Iterations-Schleife benutzbar): ShowMessage(XmlDoc.DocumentElement.ChildNodes['metadata'].ChildNodes[8].Text);
With XmlDoc.DocumentElement.ChildNodes['
metadata']
do
begin
for h := 0
to ChildNodes.Count - 1
do
begin
if ChildNodes[h].NodeName = '
dc:title'
then cr.title := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:creator'
then cr.creator := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:publisher'
then cr.publisher := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:description'
then cr.description := ExtractHtmlTags(ChildNodes[h].Text);
if ChildNodes[h].NodeName = '
dc:subject'
then cr.subject := ChildNodes[h].Text;
//Weitere Möglichkeiten dc:contributor dc:source dc:relation dc:coverage dc:language dc:rights
if ChildNodes[h].NodeName = '
meta'
then
if (ChildNodes[h].Attributes['
name']<>null)
and (ChildNodes[h].Attributes['
name'] = '
cover')
then cr.mcover := ChildNodes[h].Attributes['
content'];
//Weitere Möglichkeiten für <meta name
//außer cover auch:
//igil version, generator, calibre:series_index, calibre:user_metadata:#gelesen,
//calibre:timestamp, calibre:user_categories, calibre:title_sort, calibre:rating,
//calibre:author_link_map, calibre:user_metadata:#formats
if ChildNodes[h].NodeName = '
dc:identifier'
then
if (ChildNodes[h].Attributes['
opf:scheme']<>null)
and (ChildNodes[h].Attributes['
opf:scheme'] = '
uuid')
then cr.uuid := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:identifier'
then
if (ChildNodes[h].Attributes['
opf:scheme']<>null)
and (ChildNodes[h].Attributes['
opf:scheme'] = '
isbn')
then cr.isbn := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:identifier'
then
if (ChildNodes[h].Attributes['
opf:scheme']<>null)
and (ChildNodes[h].Attributes['
opf:scheme'] = '
asin')
then cr.asin := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:identifier'
then
if (ChildNodes[h].Attributes['
opf:scheme']<>null)
and (LowerCase(ChildNodes[h].Attributes['
opf:scheme']) = '
mobi-asin')
then cr.asin := ChildNodes[h].Text;
if ChildNodes[h].NodeName = '
dc:identifier'
then
if (ChildNodes[h].Attributes['
id']<>null)
and (LowerCase(ChildNodes[h].Attributes['
id']) = '
bookid')
then cr.mbookid := ChildNodes[h].Text;
end;
end;
{****************************************************************************}
{* <Manifest> durchsuchen *}
{****************************************************************************}
for h := 0
to XmlDoc.DocumentElement.ChildNodes['
manifest'].ChildNodes.Count - 1
do
begin
shref := XmlDoc.DocumentElement.ChildNodes['
manifest'].ChildNodes[h].AttributeNodes['
href'].Text;
sid := XmlDoc.DocumentElement.ChildNodes['
manifest'].ChildNodes[h].AttributeNodes['
id'].Text;
smedia := XmlDoc.DocumentElement.ChildNodes['
manifest'].ChildNodes[h].AttributeNodes['
media-type'].Text;
//*** Cover steht in manifest entweder unter id=cover
//*** oder in Metadata <meta name=cover content=xyz.jpg
//Nicht berücksichtigt: das Cover kann auch in einer extra html oder xhtlm-Datei referenziert werden
if (pos('
cover', sid) > 0)
and (smedia = '
image/jpeg')
then cr.coverpath := shref;
if cr.coverpath = '
'
then if (pos(cr.mcover, sid) > 0)
and (smedia = '
image/jpeg')
then cr.coverpath := shref;
//temporäres dreidimensionales Array mit allen "html" bzw. "htm" bzw. "xhtml"-Dateiangaben füllen.
//temporär deshalb, weil leider die Reihenfolge der Buchseiten später in spine noch sortiert werden muss.
//Das sortierte Array ist cr.html und wird später gefüllt:
if pos('
htm', smedia)>0
then
begin
SetLength(arrTemp, h+1, 3);
arrTemp[h, 0] := sid; arrTemp[h, 1] := shref; arrTemp[h, 2] := smedia;
//ShowMessage(shref + ' ' + sid + ' ' + smedia);
end;
end;
{****************************************************************************}
{* <Spine> durchsuchen *}
{****************************************************************************}
//Spine enthält die Reihenfolge der Buchseiten
//<spine toc="ncx"><itemref idref="cover"/><itemref idref="haupttitel"/><itemref idref="chapter1"/></spine>
for h := 0
to XmlDoc.DocumentElement.ChildNodes['
spine'].ChildNodes.Count - 1
do
begin
sidref := XmlDoc.DocumentElement.ChildNodes['
spine'].ChildNodes[h].AttributeNodes['
idref'].Text;
//Inhaltsverzeichnis sidref eintragen.
//Die Array-Länge wird mit SetLength angepasst und ergibt sich durch die Variable h in der Iteration (Schleife for h=0 to ...)
SetLength(cr.html, h+1, 3);
cr.html[h,0] := sidref;
end;
//Test: ShowMessage(IntToStr(high(cr.html)) + ' Einträge in spine gefunden');
//die erste Dimesion des Arrays cr.html[x,0] ist bereits in der korrekten Reihenfolge gefüllt,
//die anderen Werte werden aus dem temporären Array arrTemp kopiert
for h := low(cr.html)
to high(cr.html)
do
begin
//Test: ShowMessage(cr.html[h,0]);
for z := low(arrTemp)
to high(arrTemp)
do
if cr.html[h,0] = arrTemp[z,0]
then
begin
cr.html[h,1] := arrTemp[z,1]; cr.html[h,2] := arrTemp[z,2];
end;
end;
arrTemp :=
nil;
//Test: for h := low(cr.html) to high(cr.html) do ShowMessage('Sortiert: ' + #13 + cr.html[h,0] + #13 + cr.html[h,1] + #13 + cr.html[h,2]);
{****************************************************************************}
{* <Guide><reference> durchsuchen *}
{****************************************************************************}
// so könnte auch der Abschnitt Guide der Datei content.opf durchsucht werden. Wird aktuell aber nicht benötigt
for z := 0
to XmlDoc.DocumentElement.ChildNodes['
guide'].ChildNodes.Count - 1
do
begin
shref := XmlDoc.DocumentElement.ChildNodes['
guide'].ChildNodes['
reference'].AttributeNodes['
href'].Text;
stitle := XmlDoc.DocumentElement.ChildNodes['
guide'].ChildNodes['
reference'].AttributeNodes['
title'].Text;
stype := XmlDoc.DocumentElement.ChildNodes['
guide'].ChildNodes['
reference'].AttributeNodes['
type'].Text;
//ShowMessage(shref + ' ' + stitle + ' ' + stype);
end;
//XmlDoc mit dem Inhalt der Datei content.opf freigeben
XMLDoc.Active := False;
XmlDoc :=
nil;
{****************************************************************************}
{* Titelbild suchen *}
{****************************************************************************}
// hier habe ich versucht, das Titelbild zu suchen und in einen Stream einzulesen.
// Das ist leider unübersichtlich, da viele verschiedene Pfad-Varianten berücksichtigt werden müssen.
// Leider fehlt die 3. Variante mit eigener html-Datei als Bildreferenz
// 1. Cover als Bilddatei in Metadaten: <meta name="cover" content="xyzcover.jpg"/>
// 2. Cover als Bilddatei in Manifest: <item href="cover.jpeg" id="cover" media-type="image/jpeg"/>
// oder so <item href="images/cover.jpg" id="cover-image" media-type="image/jpeg"/> }
// 3. Cover in eigener html-Datei nicht fertig: <body><img src="buch.jpg" alt="Mein Buchtitel"/>
if cr.coverpath <> '
'
then
begin
try
//coverpath kann in Unterverzeichnis opfDir liegen. opfDir kann leer bleiben, das stört nicht
// aber: ../cover.jpg bedeutet, dass opfDir ignoriert werden muss.
// ../ muss dann aber auch noch abgeschnitten werden
if cr.opfDir <> '
'
then xdir := cr.opfDir + cr.coverpath;
//1.
if pos('
../', cr.coverpath) > 0
then xdir := RightStr(cr.coverpath, Length(cr.coverpath)-3);
//2. Ausnahme von 1.
if cr.opfDir = '
'
then xdir := cr.coverpath;
//3. noch unvollständig
index := KAZip1.Entries.IndexOf(xdir);
if (
index > 0)
then
begin
KAZip1.ExtractToStream(KAZip1.Entries.Items[
index], cr.ms);
cr.ms.seek(0,sofromBeginning);
end;
finally
end;
end;
{****************************************************************************}
//Zip-Komponente freigeben
KaZip1.Free;
end;
function TEpub.ExtractOpfDir(s:
string):
string;
var
pos1: integer;
begin
pos1 := 0;
//letzten Slash suchen:
repeat pos1 := posex('
/', s, pos1+1)
until posex('
/', s, pos1+1) = 0;
if pos1 = 0
then result := '
';
//kein Slash = kein Verzeichnis
if pos1 > 0
then result := Copy(s,1, pos1);
//Verzeichnis mit abschließendem Slash
end;
// für cr.description benutzt, um die Inhaltsangabe der Bücher in <dc:description> als reinen string zu erhalten
function TEpub.ExtractHtmlTags(s:
string):
string;
begin
s := (StringReplace(s, '
"' , '
"', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
&' , '
&', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<' , '
<', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
>' , '
>', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<br>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<p>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
</p>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<div>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
</div>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
–' , '
-', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<i' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
/i' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<h1>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
</h1>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<h2>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
</h2>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<h3>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
</h3>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<em>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
</em>' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
‹' , '
‹', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
›' , '
›', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
›' , '
›', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
{' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
}' , '
', [rfReplaceAll, rfIgnoreCase]));
s := (StringReplace(s, '
<p class="description">' , '
', [rfReplaceAll, rfIgnoreCase]));
Result := s;
end;
end.