|
Antwort |
Registriert seit: 8. Aug 2003
Kurze Übersicht über das Thema Epub-Format
Bevor es weiter unten mit dem Delphi-Projekt losgeht, möchte ich eine kurze Übersicht über das Epub-Format geben. Das Delphi-Projekt wurde mit der Delphi-Version 7 unter Windows XP und Window 8 erstellt: In jedem Archiv sind die Unterverzeichnisse META-INF und OPS enthalten META-INF enthält die Datei container.xml OPS enthält alle Buchseiten im html oder xhtml-Format Die Datei container.xml enthält eine html-Referenz auf die Datei content.opf In content.opf sind sämtliche Teile des Ebook im xml-Format aufgelistet. Daher ist diese Datei die zentrale Information über das Ebook Meistens, aber nicht immer liegt die Datei content.opf im Verzeichnis OPS Daneben gibt es für die Navigation auch eine Datei toc.ncx. Die Reihenfolge der Seiten ist aber auch bereits in content.opf enthalten Alle Inhalte liegen im Unicode-Dateiformat vor. Weitere Informationen über das Epub-Format unter: http://de.wikipedia.org/wiki/EPUB http://www.iks.hs-merseburg.de/~mein...S2_Meinike.pdf https://hpv.leo.uberspace.de/wordpress/epub-cover/ http://www.jedisaber.com/eBooks/Tutorial.shtml http://www.hxa.name/articles/content...7241_2007.html
Ralf Stehle
ralfstehle@yahoo.de |
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: http://www.delphipages.com/forum/showthread.php?t=27536 Für das Epub habe ich eine eigene Klasse TEpub angelegt: http://www.delphipraxis.net/92358-ei...klarieren.html
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; |
Zitat |
Delphi 7 Professional |
#3
Epub-Viewer mit TWebbrowser erstellen
Da ein Epub eine Archivdatei im Zipformat mit html oder xhtml-Seiten ist, kann der TWebbrowser für die Darstellung von Epubs benutzt werden. Das Epub wird dabei vollständig mit allen Seiten und nicht nur seitenweise in den Webbrowser geladen. Es gibt eine Seitenanzeige und man kann durch Seiten und Kapitel navigieren. Die Schriftgröße kann verändert werden Probleme mit dem TWebbrowser in Delphi7 mit dem Unicode-Format wurden berücksichtigt Das Fehlen eines Scroll-Events wurde einfach durch einen Timer gelöst, der ständig die Seitenzahlen neu berechnet Im Gegensatz zum 2. Teil habe ich statt Kazip20 jetzt die Abbrevia-Zip-Komponente benutzt. Leider musste ich feststellen, das KaZip nicht alle Epubs entpacken kann
Delphi-Quellcode:
unit uViewer;
interface uses Windows ......., ActiveX, mshtml, ShellApi, SHDocVw, AbUnzper, AbArcTyp; type TViewer = class(TForm) PanelTop: TPanel; ShowEpub: TTimer; ...... private { Private-Deklarationen } function IsInteger(str: string): Boolean; function deldir(dir: string): Boolean; function WB_GetScrollPosition(WB: TWebBrowser; var ScrollPos: TPoint): Boolean; public { Public-Deklarationen } end; var Viewer: TViewer; implementation uses uEpub; // die Unit uEpub aus Teil 2 wird benötigt {$R *.dfm} var //Variablen für die Steuerung im Buch PageNr, KapitelNr: integer; MaxPage, ThisPage, MaxLines, VisibleLines, FirstVisibleLine, LastVisibleLine: integer; //Variable für das Vergrößern und Verkleinern der Schrift wbZoom: OleVariant; tempPath: string; //Strukturierter Datentyp aus uEpub cr: contentRec; procedure TViewer.FormCreate(Sender: TObject); begin wbZoom := 100; PageNr := 1; KapitelNr := 1; tempPath := SysUtils.GetEnvironmentVariable('temp') + '\unzip\'; //hierhin wird das Epub entzippt end; //Erst wird die Applicatin mit dem Webbrowser angezeigt. //Der Timer verzögert das Laben des Epub, sonst wird die Application erst danach sichtbar procedure TViewer.ShowEpubTimer(Sender: TObject); var UnZip: TAbUnZipper; // diesesmal benutze ich die Abbrevia-Komponente s: string; i, j, k, index, posEnd: integer; WebDoc: HTMLDocument; WebBody: HTMLBody; Range: IHTMLTxtRange; begin ShowEpub.Interval := 0; // sonst macht der Timer eine Endlosschleife //Das Epub wird in das temporäre Verzeichnis entpackt UnZip:= TAbUnZipper.create(nil); //uses AbUnzper UnZip.FileName:= 'MeinNeuesEbook.epub'; UnZip.BaseDirectory:= ExtractFilePath(tempPath); UnZip.ExtractOptions := [eoCreateDirs, eoRestorePath]; //uses AbArcTyp UnZip.ExtractFiles('*.*'); UnZip.OpenArchive(Form1.filename); //für ExtractToStream weiter unten cr.ms := TMemoryStream.Create; //vor Aufruf von Epub.ParseEpub(filename, cr) initialisieren Epub.ParseEpub('MeinNeuesEbook.epub', cr); //Erste Seite per Navigate anzeigen. Im cr.html-Array aus uEpub stehen alle html-Seiten des Epub zur Verfügung WB.Navigate(tempPath + cr.opfDir + cr.html[1, 1]); Application.ProcessMessages; //statt WB.Navigate werden die restlichen Seiten so angefügt. //so hat man das komplette Ebook im Webbrowser als eine fortlaufende Seite geladen for i := low(cr.html) to high(cr.html) do begin cr.ms.Clear; UnZip.ExtractToStream(cr.opfDir + cr.html[i,1], cr.ms); cr.ms.seek(0,sofromBeginning); SetString(s, PAnsiChar(cr.ms.Memory), cr.ms.Size); {set string to get memory} //Utf8Decode führt häufig zu einem leeren String, wenn am Schluss sogenannte "invalide byte sequence" vorhanden sind //daher Ende der html-Datei bestimmen und string dahinter abschneiden posEnd := pos('</html>', s) + Length('</html>'); s := Utf8Decode(Copy(s, 1, posEnd)) + '<p> </p>'; //Warten bis eine Seite fertig geladen wurde... while (WB.ReadyState < READYSTATE_INTERACTIVE) do Application.ProcessMessages; if Assigned(WB.Document) then begin //http://www.swissdelphicenter.ch/torry/showcode.php?id=2148 Range := ((WB.Document as IHTMLDocument2).body as IHTMLBodyElement).createTextRange; Range.collapse(False); Range.pasteHTML(s); //hier füge ich bei jeder neuen html-Seite ein unsichtbaren Text ekapitel1 bis x ein //damit kann ich später durch die Kapitel navigieren Range.pasteHTML('<span style="color:white">ekapitel' + IntToStr(i) + '</span><span style="color:black"> </span>'); end; Application.ProcessMessages; end; UnZip.Free; BerechneSeiten.Interval := 500; //Seitenzähler aktivieren, ohne Verzögerung Errormeldung end; procedure TViewer.WBDocumentComplete(Sender: TObject; const pDisp: IDispatch; var URL: OleVariant); var RefreshLevel: OleVariant; begin //in Delphi7 gibt es ein Problem mit Unicode. Diese Zeilen lösen das Problem leider nur //bei Seiten die mit Webbrowser.Navigate geladen wurden. if Assigned(WB) then try IHTMLDocument2(WB.Document).Set_CharSet('utf-8'); //'utf-8' iso-8859-1 oder 'iso-8859-2' RefreshLevel := 7; WB.Refresh2(RefreshLevel); except end; end; //So kann der Text verkleinert werden procedure TViewer.BitBtnFontDownsizeClick(Sender: TObject); begin wbZoom:= wbZoom - 10; WB.ExecWB(63, OLECMDEXECOPT_PROMPTUSER, wbZoom); end; //und so vergößert werden procedure TViewer.BitBtnFontUpsizeClick(Sender: TObject); begin wbZoom:= wbZoom + 10; WB.ExecWB(63, OLECMDEXECOPT_PROMPTUSER, wbZoom); end; //Ab hier geht es nur noch um das Navigieren im Ebook procedure TViewer.BitBtnForwardsClick(Sender: TObject); var i: integer; anchor: OleVariant; begin //Im Text scrollen, falls die Seite nicht verlassen wurde if pos(cr.opfDir + cr.html[PageNr, 1], WB.LocationURL)>0 then WB.OleObject.Document.ParentWindow.ScrollBy(0, WB.Height) //funktioniert nicht immer: OleVariant(WB.Document as IHTMLDocument2).Body.ScrollTop := OleVariant(WB.Document as IHTMLDocument2).Body.ScrollTop + WB.Height; else WB.GoForward; //falls Seite über Link verlassen wurde, muss normale Browser-Navigation aktiviert werden end; procedure TViewer.BitBtnBackwardsClick(Sender: TObject); begin //ist die Url noch diesselbe wie das Epub? Dann Scrollen, sonst Navigieren mit WB.GoBack if pos(cr.opfDir + cr.html[PageNr, 1], WB.LocationURL) > 0 then WB.OleObject.Document.ParentWindow.ScrollBy(0, -WB.Height) //Variante funktioniert nicht immer: OleVariant(WB.Document as IHTMLDocument2).Body.ScrollTop := OleVariant(WB.Document as IHTMLDocument2).Body.ScrollTop + WB.Height; else WB.GoBack; //falls Seite über Link verlassen wurde, muss normale Browser-Navigation aktiviert werden end; //hier kann in einem TEdit mit Namen "edThisPage" direkt zu einer Seite gesprungen werden procedure TViewer.edThisPageKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin BerechneSeiten.Interval := 0; if Key <> VK_RETURN then exit; if not IsInteger(edThisPage.Text) then exit; WB.OleObject.Document.ParentWindow.ScrollBy(0, StrToInt(edThisPage.Text) * WB.Height); //OleVariant(WB.Document as IHTMLDocument2).Body.ScrollTop := OleVariant(WB.Document as IHTMLDocument2).Body.ScrollTop + WB.Height; BerechneSeiten.Interval := 500; end; procedure TViewer.edThisPageEnter(Sender: TObject); begin //Der Timer muss unterbrochen werden, sonst kann ich in diesem TEdit edThisPage nichts eingeben BerechneSeiten.Interval := 0; end; procedure TViewer.edThisPageExit(Sender: TObject); begin //Bei Verlassen den Timer für die Seitenberechnung wieder starten BerechneSeiten.Interval := 500; end; //Die Kapitelsteuerung. Einfach den unsichtbaren weißen Text ekapitelx suchen //falls vorher die Scrollbalken benutzt wurden, sorgt die Schleife dafür, dass //eine ekapitel-Nr unterhab der aktuellen Seite i angesteuert wird procedure TViewer.BtnKapitelPreviousClick(Sender: TObject); var Range: IHTMLTxtRange; i: integer; begin if WB.Busy then exit; i := thisPage; WB.Hide; //Anzeige wird solange unterbrochen repeat if KapitelNr > 2 then Dec(KapitelNr); Range := ((WB.Document as IHTMLDocument2).body as IHTMLBodyElement).createTextRange; if Range.findText('ekapitel' + IntToStr(KapitelNr), 1, 0) then Range.ScrollIntoView(True); BerechneSeitenTimer(Self); until (i >= thisPage); WB.Show; end; //Dasselbe rückwärts procedure TViewer.BtnKapitelNextClick(Sender: TObject); var Range: IHTMLTxtRange; i: integer; begin if WB.Busy then exit; i := thisPage; WB.Hide; repeat if KapitelNr < High(cr.html) then Inc(KapitelNr); Range := ((WB.Document as IHTMLDocument2).body as IHTMLBodyElement).createTextRange; if Range.findText('ekapitel' + IntToStr(KapitelNr), 1, 0) then begin Range.ScrollIntoView(True); WB.OleObject.Document.ParentWindow.ScrollBy(0, WB.Height - 50); {Range.select} end; BerechneSeitenTimer(Self); until (i <= thisPage); WB.Show; end; //Da der TWebbrowser kein Scrollevent hat, wird einfach regelmäßig die Seitenzahl neu berechnet procedure TViewer.BerechneSeitenTimer(Sender: TObject); var px: TPoint; begin PanelHide.Visible := false; WB_GetScrollPosition(WB, px); FirstVisibleLine := px.y; VisibleLines := WB.Height; MaxLines := ((WB.Document as IHTMLDocument2).body as IHTMLElement2).ScrollHeight; ThisPage := ((FirstVisibleLine+1) div VisibleLines) + 1; //+1 sonst 0 möglich MaxPage := MaxLines div VisibleLines; edThisPage.Text := IntToStr(ThisPage); lbSeite.Caption := 'Seite ' + IntToStr(ThisPage) + ' von ' + IntToStr(MaxPage); //'Seiten: ' + IntToStr(MaxPage); //lbSeite.Caption := end; //temporäre Dateien wieder löschen procedure TViewer.FormClose(Sender: TObject; var Action: TCloseAction); begin WB.Navigate('about:blank'); //Webbrowser geleert. Jetzt temporäre Daten löschen. Achtung: Verzeichnis ohne Backslash! = ExcludeTrailingPathDelimiter(tempPath) While (WB.ReadyState < READYSTATE_INTERACTIVE) do Application.ProcessMessages; if Assigned(WB.Document) then if deldir(ExcludeTrailingPathDelimiter(tempPath)) = false then ShowMessage('Temporäre Dateien in ' + tempPath + ' konnten nicht entfernt werden.'+#13+'Bitte manuell löschen'); end; {***********************************************************************************************} {* Webbrowser Scrollbar X,Y Position ermitteln: *} {* Quelle: http://www.delphipraxis.net/110089-twebbrowser-scrollposition-ermitteln.html *} {***********************************************************************************************} function TViewer.WB_GetScrollPosition(WB: TWebBrowser; var ScrollPos: TPoint): Boolean; // Scrollbar X,Y Position der ListView ermitteln function WB_GetLVScrollPosition(WB: TWebBrowser; var ScrollPos: TPoint): Boolean; var lpsi: TScrollInfo; WND, WndLV: HWND; begin Result := False; {SysListView32 Child vom TWebBrowser suchen} WndLV := 0; Wnd := GetNextWindow(WB.Handle, GW_CHILD); while (WndLV = 0) and (WND <> 0) do begin WndLV := FindWindowEx(Wnd, 0, 'SysListView32', nil); Wnd := GetNextWindow(Wnd, GW_CHILD) end; if WndLV <> 0 then {SysListView32 gefunden} begin {TScrollInfo initialisieren} FillChar(lpsi, SizeOf(lpsi), 0); with lpsi do begin cbSize := SizeOf(lpsi); fMask := SIF_POS; end; {ScrollInfos der vertikalen ScrollBar ermitteln} if GetScrollInfo(WndLV, SB_VERT, lpsi) then begin ScrollPos.Y := lpsi.nPos; {ScrollInfos der horizontalen ScrollBar ermitteln} if GetScrollInfo(WndLV, SB_HORZ, lpsi) then begin ScrollPos.X := lpsi.nPos; Result := True; end; end; end; end; // Scrollbar X,Y Position des HTML Documents ermitteln function WB_GetDOCScrollPosition(WB: TWebBrowser; var ScrollPos: TPoint): Boolean; var IDoc: IHTMLDocument2; IDoc3: IHTMLDocument3; IElement: IHTMLElement; begin ScrollPos := Point(-1, -1); Result := False; if Assigned(WB.Document) and (Succeeded(WB.Document.QueryInterface(IHTMLDocument2, IDoc))) then begin IDoc := WB.Document as IHTMLDocument2; if Assigned(IDoc) and Assigned((IHTMLDocument2(IDoc).Body)) then begin if (IDoc.QueryInterface(IHTMLDocument3, IDoc3) = S_OK) then if Assigned(IDoc3) then IElement := IDoc3.get_documentElement; if (Assigned(IElement)) and (Variant(IDoc).DocumentElement.scrollTop = 0) then ScrollPos.Y := IHTMLDocument2(IDoc).Body.getAttribute('ScrollTop', 0) else ScrollPos.Y := Variant(IDoc).DocumentElement.scrollTop; if Assigned(IElement) and (Variant(IDoc).DocumentElement.scrollLeft = 0) then ScrollPos.X := IHTMLDocument2(IDoc).Body.getAttribute('ScrollLeft', 0) else ScrollPos.X := Variant(IDoc).DocumentElement.scrollLeft end; Result := (ScrollPos.X <> -1) and (ScrollPos.Y <> -1) end; end; begin Result := WB_GetDOCScrollPosition(WB, ScrollPos); if not Result then Result := WB_GetLVScrollPosition(WB, ScrollPos); end; {***********************************************************************************************} function TViewer.IsInteger(str : String): Boolean; var i: integer; begin Result:=true; try i := StrToInt(str); except Result:=false; end; end; // mit deldir wird das temporäre entpackte Epub bei Beenden der Application wieder gelöscht function TViewer.deldir(dir: string): Boolean; var fos: TSHFileOpStruct; begin ZeroMemory(@fos, SizeOf(fos)); with fos do begin wFunc := FO_DELETE; fFlags := FOF_SILENT or FOF_NOCONFIRMATION; pFrom := PChar(dir + #0); end; Result := (0 = ShFileOperation(fos)); end; end. |
Zitat |
Delphi 6 Enterprise |
#4
Schau dir doch auch mal das FB2 Format an. Das ist glaub ich auch XML basiert. Ggf. kannste das in einem Abwasch mit behandeln und schon kann dein Reader ein Format mehr...
Ralph
|
Zitat |
Perlsau
|
#5
Hallo Ralf, hab deinen Code mal etwas näher unter die Lupe genommen, weil ich eine Anwendung entwickle, die Epubs in einer Datenbank verwalten soll. Dabei ist mir folgende Ungereimtheit aufgefallen:
Delphi-Quellcode:
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; // Test von index erst nach der Verwendung??? 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; Des weiteren fand ich einen vollkommen unverständlichen Vergleich mit null im Abschnitt Metadata durchsuchen:
Delphi-Quellcode:
Wozu hier ein Vergleich mit null, der zudem einen Compilerfehler erzeugt (was soll das sein: null?), nötig sein sollte, wo doch anschließend sowieso getestet wird, ob der Attribut-Name mit dem String 'cover' identisch ist, kann ich nicht nachvollziehen. Wenn dort 'cover" drinsteht, kann es ja nicht leer sein, oder?
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']; Im Abschnitt Guide reference durchsuchen kann ich nicht nachvollziehen, wieso mehrmals dieselben Childnodes abgerufen werden:
Delphi-Quellcode:
Die Zähl-Variable z wird nirgendwo verwendet, um auf den nächsten Reference-Node zuzugreifen. Soweit ich das anhand meiner hier verfügbaren Epubs beurteilen kann, besitzt guide stets ein odere mehrere Child-Nodes namens reference, die jeweils drei Attribute enthalten. Müßte es dann nicht so geschrieben werden:
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; end;
Delphi-Quellcode:
Schleierhaft ist mir auch, was mit den Strings, die den lokalen Variablen shref, stitle und stype zugewiesen wurden, geschehen soll. In deinem Code werden diese Variablen bis zum Ende der Procedure nicht mehr verwendet.
for z := 0 to XmlDoc.DocumentElement.ChildNodes['guide'].ChildNodes.Count - 1 do
if XmlDoc.DocumentElement.ChildNodes['guide'].ChildNodes[z].name = 'reference' Then begin shref := XmlDoc.DocumentElement.ChildNodes['guide'].ChildNodes[z].AttributeNodes['href'].Text; stitle := XmlDoc.DocumentElement.ChildNodes['guide'].ChildNodes[z].AttributeNodes['title'].Text; stype := XmlDoc.DocumentElement.ChildNodes['guide'].ChildNodes[z].AttributeNodes['type'].Text; end; Ach ja, ich verwende selbstverständlich Abbrevia statt KaZip und bin gerade dabei, deine ellenlangen (und damit unübersichtlichen) Methoden in kleinere Häppchen auftzuteilen. Ansonsten: gute Arbeit (soweit ich das bislang zu beurteilen vermag) oder besser: Vorarbeit Geändert von Perlsau ( 1. Aug 2015 um 02:07 Uhr) Grund: weitere Fehler gefunden ... |
Zitat |
Delphi 7 Professional |
#6
Danke für Deine Anregungen.
Ich bin absoluter Hobby-Programmierer, das Durchsuchen der Epubs war für mich eigentlich eine Nummer zu groß. Durch meinen Beitrag hoffte ich auf andere dafür zu interessieren. Leider habe ich mich aus Zeitmangel schon länger nicht mehr mit dem Projekt beschäftigt und weiß nicht mehr, warum ich die zitierten Codezeilen so programmiert habe. |
Zitat |
Perlsau
|
#7
Okay, das ist eine ehrliche Antwort, die keines weiteren Kommentars bedarf ... ist ja auch schon bald 20 Monate her, daß du dich damit befaßt hast
Doch offenbar kennst du dich ein wenig mit XML-Bearbeitung aus. Falls ich mich hierin geirrt habe, sind natürlich auch andere User dazu aufgerufen, zur Lösung beizutragen ... Ich erhalte nämlich an der Stelle, wo ich die container.xml durchsuchen will, eine Zugriffsverletzung: TextDataBase.exe ist eine Exception der Klasse EInvalidPointer mit der Meldung 'Ungültige Zeigeroperation' aufgetreten. ... und zwar an dieser Stelle: cr.opfFile := XmlDoc.DocumentElement.ChildNodes['rootfiles'].ChildNodes['rootfile'].AttributeNodes['full-path'].Text; Bei mir sieht das natürlich ein wenig anders aus, ist aber im Grunde dasselbe:
Delphi-Quellcode:
Die Methode ist Teil einer Klasse, die bereits im Constructor alle benötigten Komponenten erzeugt (und selbstverständlich im Destructor wieder freigibt):
Function TEpubs.GetMetaData : Boolean;
begin Inhalt.MemS.Seek(0,soFromBeginning); If Inhalt.MemS.Size = 0 Then Begin Result := False; GLD.Fehlertext := 'Stream für Container.xml ist leer - in "' + Inhalt.EpubDatei + '"'; End ELse Try XmlDok.LoadFromStream(Inhalt.MemS); Inhalt.OpfFile := XmlDok.DocumentElement.ChildNodes['rootfiles'].ChildNodes['rootfile'].AttributeNodes['full-path'].Text; Inhalt.OpfDir := ExtractOpfDir(Inhalt.OpfFile); Result := True; Except On e:exception Do Begin Result := False; GLD.Fehlertext := 'Fehler beim Analysieren der Container.xml in "' + Inhalt.EpubDatei + '": ' + e.Message; End; End; end;
Delphi-Quellcode:
Wieso löst der Zugriff auf die Childnodes von container.xml eine ungültige Zeigeroperation aus? Bin leider kein XML-Experte, was ich wohl ändern sollte ...
Constructor TEpubs.Create;
begin inherited; UnZip := TAbUnZipper.Create(Nil); XmlDok := TXMLDocument.Create(nil); XmlDok.DOMVendor := DOMVendors.Vendors[0]; Inhalt.MemS := TMemoryStream.Create; LogList := TStringList.Create; If System.SysUtils.FileExists(GLD.URec.LogDatei) Then LogList.LoadFromFile(GLD.URec.LogDatei); end; Die container.xml sieht ja bei allen Epubs ziemlich gleich aus, manchmal feht der Encoding-Hinweis, aber sonst:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
- <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> - <rootfiles> <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml" /> </rootfiles> </container> |
Zitat |
Perlsau
|
#8
Das Problem mit dem ungültigen Zeiger beim Zugriff auf einen Knoten der XML-Struktur liegt offenbar darin, daß ich TXmlDocument keinen Owner zuweise, sondern nil verwende. Die – vorläufige – Lösung besteht darin, die ganze Klasse mit Owner zu erzeugen und diesen Owner dann beim Erzeugen der TXmlDocument-Instanz einzusetzen. Ob das so sein muß oder auch anders geht, weiß ich nicht und erhoffe mir eine Antwort in diesem Thread, den ich zu dieser speziellen XML-Thematik erstellt habe.
|
Zitat |
Ansicht |
Linear-Darstellung |
Zur Hybrid-Darstellung wechseln |
Zur Baum-Darstellung wechseln |
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 |
LinkBack URL |
About LinkBacks |