Einzelnen Beitrag anzeigen

Ralf Stehle

Registriert seit: 8. Aug 2003
124 Beiträge
 
Delphi 7 Professional
 
#2

Teil 2: Delphi-Projekt zum Anzeigen eines Ebooks im Epub-Format

  Alt 4. Feb 2014, 19:44
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:descriptionthen 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 = 'metathen
       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:identifierthen
       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:identifierthen
       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:identifierthen
       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:identifierthen
       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:identifierthen
       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, '&quot;' , '"', [rfReplaceAll, rfIgnoreCase]));
  s := (StringReplace(s, '&amp;'  , '&', [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) <> '.epubthen 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
  Mit Zitat antworten Zitat