|
Registriert seit: 8. Aug 2003 124 Beiträge Delphi 7 Professional |
#2
Delphi-Projekt Epub
Um ein Ebook im epub-Format anzuzeigen, muss das File also zuerst entpackt werden. Ich benutze hierfür die kostenlose Komponente kazip20. Diese Komponente erlaubt es sogar, einzelne Dateien in einem Stream zu öffnen, ohne das gesamte Archiv zu entpacken Jede andere Zip-Komponente funktioniert aber sicher ähnlich Download einiger freien Zip-Komponenten: ![]() Für das Epub habe ich eine eigene Klasse TEpub angelegt: ![]()
Delphi-Quellcode:
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. Die Klasse kann einfach zu einem Delphi-Projekt hinzugefügt werden Hier eine Beispiel-Anwendung: In meiner Anwendung verwalte ich eine Liste aller meiner Epub-Bücher. Als Liste benutzte ich ein AdoDataSet. Das kann als Verbindung zu einer Access-Datei dienen, aber auch ungebunden benutzt werden Beim Scrollen innerhalb der Bücher soll das Titelbild und die Metadaten angezeigt werden
Delphi-Quellcode:
procedure TForm1.ADODataSet1AfterScroll(DataSet: TDataSet);
var cr: contentRec; //Datentyp aus Epub.pas jpeg: TJPEGImage; //uses Jpeg begin //Cover in MemoryStream cr.ms laden {Datentyp aus Epub.pas} Image1.Picture.Assign(nil); //altes Bild entfernen filename := AdoDataSet1.FieldByName('Dateiname').AsString; if ExtractFileExt(filename) <> '.epub' then exit; jpeg := TJPEGImage.Create; //Wichtig: vor Aufruf von Epub.ParseEpub(filename, cr) immer initialisieren cr.ms := TMemoryStream.Create; try Epub.ParseEpub(filename, cr); //ein Bild wurde gefunden if (cr.coverpath <> '') and (cr.ms.Size>0) then begin if (Image1.Picture.Graphic = nil) or (Image1.Picture.Graphic.Empty) then begin jpeg.LoadFromStream(cr.ms); // falls epub entpackt wurde auch LoadFromFile() möglich Image1.Picture.Assign(jpeg); end; end; //Metadaten in einem TMemo anzeigen With Memo1.Lines do begin Clear; Add(cr.title); Add(cr.subject); Add(cr.description); Add(cr.creator); Memo1.Perform(WM_VSCROLL, SB_TOP, 0); //nach unten scrollen end; finally jpeg.free; end; end;
Ralf Stehle
ralfstehle@yahoo.de |
![]() |
Ansicht |
![]() |
![]() |
![]() |
ForumregelnEs ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.
BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus. Trackbacks are an
Pingbacks are an
Refbacks are aus
|
|
Nützliche Links |
Heutige Beiträge |
Sitemap |
Suchen |
Code-Library |
Wer ist online |
Alle Foren als gelesen markieren |
Gehe zu... |
LinkBack |
![]() |
![]() |