Различные способы отображения обложки для случаев с включенными и выключенными fancyapps
[openlib.git] / www / resources / php-epub-meta / epub.php
1 <?php
2 /**
3  * PHP EPub Meta library
4  *
5  * @author Andreas Gohr <andi@splitbrain.org>
6  * @author Sébastien Lucas <sebastien@slucas.fr>
7  */
8  
9 require_once(realpath( dirname( __FILE__ ) ) . '/tbszip.php');
10
11 define ("METADATA_FILE", "META-INF/container.xml");
12  
13 class EPub {
14     public $xml; //FIXME change to protected, later
15     public $toc;
16     protected $xpath;
17     protected $toc_xpath;
18     protected $file;
19     protected $meta;
20     protected $zip;
21     protected $coverpath='';
22     protected $namespaces;
23     protected $imagetoadd='';
24
25     /**
26      * Constructor
27      *
28      * @param string $file path to epub file to work on
29      * @throws Exception if metadata could not be loaded
30      */
31     public function __construct($file){
32         // open file
33         $this->file = $file;
34         $this->zip = new clsTbsZip();
35         if(!$this->zip->Open($this->file)){
36             throw new Exception('Failed to read epub file');
37         }
38
39         // read container data
40         if (!$this->zip->FileExists(METADATA_FILE)) {
41             throw new Exception ("Unable to find metadata.xml");
42         }
43         
44         $data = $this->zip->FileRead(METADATA_FILE);
45         if($data == false){
46             throw new Exception('Failed to access epub container data');
47         }
48         $xml = new DOMDocument();
49         $xml->registerNodeClass('DOMElement','EPubDOMElement');
50         $xml->loadXML($data);
51         $xpath = new EPubDOMXPath($xml);
52         $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]');
53         $this->meta = $nodes->item(0)->attr('full-path');
54
55         // load metadata
56         if (!$this->zip->FileExists($this->meta)) {
57             throw new Exception ("Unable to find " . $this->meta);
58         }
59         
60         $data = $this->zip->FileRead($this->meta);
61         if(!$data){
62             throw new Exception('Failed to access epub metadata');
63         }
64         $this->xml =  new DOMDocument();
65         $this->xml->registerNodeClass('DOMElement','EPubDOMElement');
66         $this->xml->loadXML($data);
67         $this->xml->formatOutput = true;
68         $this->xpath = new EPubDOMXPath($this->xml);
69     }
70     
71     public function initSpineComponent ()
72     {
73         $spine = $this->xpath->query('//opf:spine')->item(0);
74         $tocid = $spine->getAttribute('toc');
75         $tochref = $this->xpath->query("//opf:manifest/opf:item[@id='$tocid']")->item(0)->attr('href');
76         $tocpath = dirname($this->meta).'/'.$tochref; 
77         // read epub toc
78         if (!$this->zip->FileExists($tocpath)) {
79             throw new Exception ("Unable to find " . $tocpath);
80         }
81         
82         $data = $this->zip->FileRead($tocpath);
83         $this->toc =  new DOMDocument();
84         $this->toc->registerNodeClass('DOMElement','EPubDOMElement');
85         $this->toc->loadXML($data);
86         $this->toc_xpath = new EPubDOMXPath($this->toc);
87         $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); 
88         $this->toc_xpath->registerNamespace('x', $rootNamespace);
89     }
90
91     /**
92      * file name getter
93      */
94     public function file(){
95         return $this->file;
96     }
97     
98     /**
99      * Close the epub file
100      */
101     public function close (){
102         $this->zip->FileCancelModif($this->meta);
103         // TODO : Add cancelation of cover image
104         $this->zip->Close ();
105     }
106
107     /**
108      * Remove iTunes files
109      */
110     public function cleanITunesCrap () {
111         if ($this->zip->FileExists("iTunesMetadata.plist")) {
112             $this->zip->FileReplace ("iTunesMetadata.plist", false);
113         }
114         if ($this->zip->FileExists("iTunesArtwork")) {
115             $this->zip->FileReplace ("iTunesArtwork", false);
116         }
117     }
118
119     /**
120      * Writes back all meta data changes
121      */
122     public function save(){
123         $this->download ();
124         $this->zip->close();
125     }
126     
127     /**
128      * Get the updated epub
129      */
130     public function download($file=false){
131         $this->zip->FileReplace($this->meta,$this->xml->saveXML());
132         // add the cover image
133         if($this->imagetoadd){
134             $this->zip->FileReplace($this->coverpath,file_get_contents($this->imagetoadd));
135             $this->imagetoadd='';
136         }
137         if ($file) $this->zip->Flush(TBSZIP_DOWNLOAD, $file);
138     }
139
140     /**
141      * Get the components list as an array
142      */
143     public function components(){
144         $spine = array();
145         $nodes = $this->xpath->query('//opf:spine/opf:itemref');
146         foreach($nodes as $node){
147             $idref =  $node->getAttribute('idref');
148             $spine[] = $this->xpath->query("//opf:manifest/opf:item[@id='$idref']")->item(0)->getAttribute('href');
149         }
150         return $spine;
151     }
152     
153     /**
154      * Get the component content
155      */
156     public function component($comp) {
157         $path = dirname($this->meta).'/'.$comp;
158         if (!$this->zip->FileExists($path)) {
159             throw new Exception ("Unable to find " . $path);
160         }
161         
162         $data = $this->zip->FileRead($path);
163         $data = preg_replace ("/src=[\"']([\w\/\.]*?)[\"']/", "src='epubfs.php?comp=$1'", $data);
164         $data = preg_replace ("/href=[\"']([\w\/\.]*?)[\"']/", "href='epubfs.php?comp=$1'", $data);
165         return $data;
166     }
167     
168     /**
169      * Get the component content type
170      */
171     public function componentContentType($comp) {
172         return $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0)->getAttribute('media-type');
173     }
174
175     /**
176      * Get the Epub content (TOC) as an array
177      *
178      * For each chapter there is a "title" and a "src"
179      */
180     public function contents(){
181         $contents = array();
182         $nodes = $this->toc_xpath->query('//x:ncx/x:navMap/x:navPoint');
183         foreach($nodes as $node){
184             $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue;
185             $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src');
186             $contents[] =  array("title" => $title, "src" => $src);
187         }
188         return $contents;
189     }
190     
191
192     /**
193      * Get or set the book author(s)
194      *
195      * Authors should be given with a "file-as" and a real name. The file as
196      * is used for sorting in e-readers.
197      *
198      * Example:
199      *
200      * array(
201      *      'Pratchett, Terry'   => 'Terry Pratchett',
202      *      'Simpson, Jacqeline' => 'Jacqueline Simpson',
203      * )
204      *
205      * @params array $authors
206      */
207     public function Authors($authors=false){
208         // set new data
209         if($authors !== false){
210             // Author where given as a comma separated list
211             if(is_string($authors)){
212                 if($authors == ''){
213                     $authors = array();
214                 }else{
215                     $authors = explode(',',$authors);
216                     $authors = array_map('trim',$authors);
217                 }
218             }
219
220             // delete existing nodes
221             $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]');
222             foreach($nodes as $node) $node->delete();
223
224             // add new nodes
225             $parent = $this->xpath->query('//opf:metadata')->item(0);
226             foreach($authors as $as => $name){
227                 if(is_int($as)) $as = $name; //numeric array given
228                 $node = $parent->newChild('dc:creator',$name);
229                 $node->attr('opf:role', 'aut');
230                 $node->attr('opf:file-as', $as);
231             }
232
233             $this->reparse();
234         }
235
236         // read current data
237         $rolefix = false;
238         $authors = array();
239         $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]');
240         if($nodes->length == 0){
241             // no nodes where found, let's try again without role
242             $nodes = $this->xpath->query('//opf:metadata/dc:creator');
243             $rolefix = true;
244         }
245         foreach($nodes as $node){
246             $name = $node->nodeValue;
247             $as   = $node->attr('opf:file-as');
248             if(!$as){
249                 $as = $name;
250                 $node->attr('opf:file-as',$as);
251             }
252             if($rolefix){
253                 $node->attr('opf:role','aut');
254             }
255             $authors[$as] = $name;
256         }
257         return $authors;
258     }
259
260     /**
261      * Set or get the book title
262      *
263      * @param string $title
264      */
265     public function Title($title=false){
266         return $this->getset('dc:title',$title);
267     }
268
269     /**
270      * Set or get the book's language
271      *
272      * @param string $lang
273      */
274     public function Language($lang=false){
275         return $this->getset('dc:language',$lang);
276     }
277
278     /**
279      * Set or get the book' publisher info
280      *
281      * @param string $publisher
282      */
283     public function Publisher($publisher=false){
284         return $this->getset('dc:publisher',$publisher);
285     }
286
287     /**
288      * Set or get the book's copyright info
289      *
290      * @param string $rights
291      */
292     public function Copyright($rights=false){
293         return $this->getset('dc:rights',$rights);
294     }
295
296     /**
297      * Set or get the book's description
298      *
299      * @param string $description
300      */
301     public function Description($description=false){
302         return $this->getset('dc:description',$description);
303     }
304
305     /**
306      * Set or get the book's ISBN number
307      *
308      * @param string $isbn
309      */
310     public function ISBN($isbn=false){
311         return $this->getset('dc:identifier',$isbn,'opf:scheme','ISBN');
312     }
313
314     /**
315      * Set or get the Google Books ID
316      *
317      * @param string $google
318      */
319     public function Google($google=false){
320         return $this->getset('dc:identifier',$google,'opf:scheme','GOOGLE');
321     }
322
323     /**
324      * Set or get the Amazon ID of the book
325      *
326      * @param string $amazon
327      */
328     public function Amazon($amazon=false){
329         return $this->getset('dc:identifier',$amazon,'opf:scheme','AMAZON');
330     }
331     
332     /**
333      * Set or get the Calibre UUID of the book
334      *
335      * @param string $uuid
336      */
337     public function Calibre($uuid=false){
338         return $this->getset('dc:identifier',$uuid,'opf:scheme','calibre');
339     }
340
341     /**
342      * Set or get the Serie of the book
343      *
344      * @param string $serie
345      */
346     public function Serie($serie=false){
347         return $this->getset('opf:meta',$serie,'name','calibre:series','content');
348     }
349     
350     /**
351      * Set or get the Serie Index of the book
352      *
353      * @param string $serieIndex
354      */
355     public function SerieIndex($serieIndex=false){
356         return $this->getset('opf:meta',$serieIndex,'name','calibre:series_index','content');
357     }
358     
359     /**
360      * Set or get the book's subjects (aka. tags)
361      *
362      * Subject should be given as array, but a comma separated string will also
363      * be accepted.
364      *
365      * @param array $subjects
366      */
367     public function Subjects($subjects=false){
368         // setter
369         if($subjects !== false){
370             if(is_string($subjects)){
371                 if($subjects === ''){
372                     $subjects = array();
373                 }else{
374                     $subjects = explode(',',$subjects);
375                     $subjects = array_map('trim',$subjects);
376                 }
377             }
378
379             // delete previous
380             $nodes = $this->xpath->query('//opf:metadata/dc:subject');
381             foreach($nodes as $node){
382                 $node->delete();
383             }
384             // add new ones
385             $parent = $this->xpath->query('//opf:metadata')->item(0);
386             foreach($subjects as $subj){
387                 $node = $this->xml->createElement('dc:subject',htmlspecialchars($subj));
388                 $node = $parent->appendChild($node);
389             }
390
391             $this->reparse();
392         }
393
394         //getter
395         $subjects = array();
396         $nodes = $this->xpath->query('//opf:metadata/dc:subject');
397         foreach($nodes as $node){
398             $subjects[] =  $node->nodeValue;
399         }
400         return $subjects;
401     }
402
403     /**
404      * Read the cover data
405      *
406      * Returns an associative array with the following keys:
407      *
408      *   mime  - filetype (usually image/jpeg)
409      *   data  - the binary image data
410      *   found - the internal path, or false if no image is set in epub
411      *
412      * When no image is set in the epub file, the binary data for a transparent
413      * GIF pixel is returned.
414      *
415      * When adding a new image this function return no or old data because the
416      * image contents are not in the epub file, yet. The image will be added when
417      * the save() method is called.
418      *
419      * @param  string $path local filesystem path to a new cover image
420      * @param  string $mime mime type of the given file
421      * @return array
422      */
423     public function Cover($path=false, $mime=false){
424         // set cover
425         if($path !== false){
426             // remove current pointer
427             $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
428             foreach($nodes as $node) $node->delete();
429             // remove previous manifest entries if they where made by us
430             $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="php-epub-meta-cover"]');
431             foreach($nodes as $node) $node->delete();
432
433             if($path){
434                 // add pointer
435                 $parent = $this->xpath->query('//opf:metadata')->item(0);
436                 $node = $parent->newChild('opf:meta');
437                 $node->attr('opf:name','cover');
438                 $node->attr('opf:content','php-epub-meta-cover');
439
440                 // add manifest
441                 $parent = $this->xpath->query('//opf:manifest')->item(0);
442                 $node = $parent->newChild('opf:item');
443                 $node->attr('id','php-epub-meta-cover');
444                 $node->attr('opf:href','php-epub-meta-cover.img');
445                 $node->attr('opf:media-type',$mime);
446
447                 // remember path for save action
448                 $this->imagetoadd = $path;
449             }
450
451             $this->reparse();
452         }
453
454         // load cover
455         $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
456         if(!$nodes->length) return $this->no_cover();
457         $coverid = (String) $nodes->item(0)->attr('opf:content');
458         if(!$coverid) return $this->no_cover();
459
460         $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]');
461         if(!$nodes->length) return $this->no_cover();
462         $mime = $nodes->item(0)->attr('opf:media-type');
463         $path = $nodes->item(0)->attr('opf:href');
464         $path = dirname('/'.$this->meta).'/'.$path; // image path is relative to meta file
465         $path = ltrim($path,'/');
466
467         $zip = new ZipArchive();
468         if(!@$zip->open($this->file)){
469             throw new Exception('Failed to read epub file');
470         }
471         $data = $zip->getFromName($path);
472
473         return array(
474             'mime'  => $mime,
475             'data'  => $data,
476             'found' => $path
477         );
478     }
479     
480     public function getCoverItem () {
481         $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
482         if(!$nodes->length) return NULL;
483         
484         $coverid = (String) $nodes->item(0)->attr('opf:content');
485         if(!$coverid) return NULL;
486
487         $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]');
488         if(!$nodes->length) return NULL;
489
490         return $nodes->item(0);
491     }
492     
493     public function updateForKepub () {
494         $item = $this->getCoverItem ();
495         if (!is_null ($item)) {
496             $item->attr('opf:properties', 'cover-image');
497         }
498     }
499     
500     public function Cover2($path=false, $mime=false){
501         $hascover = true;
502         $item = $this->getCoverItem ();
503         if (is_null ($item)) {
504             $hascover = false;
505         } else {
506             $mime = $item->attr('opf:media-type');
507             $this->coverpath = $item->attr('opf:href');
508             $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file
509             $this->coverpath = ltrim($this->coverpath,'\\');
510             $this->coverpath = ltrim($this->coverpath,'/');
511         }
512         
513         // set cover
514         if($path !== false){
515             if (!$hascover) return; // TODO For now only update
516
517             if($path){
518                 $item->attr('opf:media-type',$mime);
519
520                 // remember path for save action
521                 $this->imagetoadd = $path;
522             }
523
524             $this->reparse();
525         }
526         
527         if (!$hascover) return $this->no_cover();
528     }
529
530     /**
531      * A simple getter/setter for simple meta attributes
532      *
533      * It should only be used for attributes that are expected to be unique
534      *
535      * @param string $item   XML node to set/get
536      * @param string $value  New node value
537      * @param string $att    Attribute name
538      * @param string $aval   Attribute value
539      * @param string $datt   Destination attribute
540      */
541     protected function getset($item,$value=false,$att=false,$aval=false,$datt=false){
542         // construct xpath
543         $xpath = '//opf:metadata/'.$item;
544         if($att){
545             $xpath .= "[@$att=\"$aval\"]";
546         }
547
548         // set value
549         if($value !== false){
550             $value = htmlspecialchars($value);
551             $nodes = $this->xpath->query($xpath);
552             if($nodes->length == 1 ){
553                 if($value === ''){
554                     // the user want's to empty this value -> delete the node
555                     $nodes->item(0)->delete();
556                 }else{
557                     // replace value
558                     if ($datt){
559                         $nodes->item(0)->attr ($datt, $value);
560                     }else{
561                         $nodes->item(0)->nodeValue = $value;
562                     }
563                 }
564             }else{
565                 // if there are multiple matching nodes for some reason delete
566                 // them. we'll replace them all with our own single one
567                 foreach($nodes as $n) $n->delete();
568                 // readd them
569                 if($value){
570                     $parent = $this->xpath->query('//opf:metadata')->item(0);
571
572                     $node = $parent->newChild ($item);
573                     if($att) $node->attr($att,$aval);
574                     if ($datt){
575                         $node->attr ($datt, $value);
576                     }else{
577                         $node->nodeValue = $value;
578                     }
579                 }
580             }
581
582             $this->reparse();
583         }
584
585         // get value
586         $nodes = $this->xpath->query($xpath);
587         if($nodes->length){
588             if ($datt){
589                 return $nodes->item(0)->attr ($datt);
590             }else{
591                 return $nodes->item(0)->nodeValue;
592             }
593         }else{
594             return '';
595         }
596     }
597
598     /**
599      * Return a not found response for Cover()
600      */
601     protected function no_cover(){
602         return array(
603             'data'  => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'),
604             'mime'  => 'image/gif',
605             'found' => false
606         );
607     }
608
609     /**
610      * Reparse the DOM tree
611      *
612      * I had to rely on this because otherwise xpath failed to find the newly
613      * added nodes
614      */
615     protected function reparse() {
616         $this->xml->loadXML($this->xml->saveXML());
617         $this->xpath = new EPubDOMXPath($this->xml);
618     }
619 }
620
621 class EPubDOMXPath extends DOMXPath {
622     public function __construct(DOMDocument $doc){
623         parent::__construct($doc);
624
625         if(is_a($doc->documentElement, 'EPubDOMElement')){
626             foreach($doc->documentElement->namespaces as $ns => $url){
627                 $this->registerNamespace($ns,$url);
628             }
629         }
630     }
631 }
632
633 class EPubDOMElement extends DOMElement {
634     public $namespaces = array(
635         'n'   => 'urn:oasis:names:tc:opendocument:xmlns:container',
636         'opf' => 'http://www.idpf.org/2007/opf',
637         'dc'  => 'http://purl.org/dc/elements/1.1/'
638     );
639
640
641     public function __construct($name, $value='', $namespaceURI=''){
642         list($ns,$name) = $this->splitns($name);
643         $value = htmlspecialchars($value);
644         if(!$namespaceURI && $ns){
645             $namespaceURI = $this->namespaces[$ns];
646         }
647         parent::__construct($name, $value, $namespaceURI);
648     }
649
650
651     /**
652      * Create and append a new child
653      *
654      * Works with our epub namespaces and omits default namespaces
655      */
656     public function newChild($name, $value=''){
657         list($ns,$local) = $this->splitns($name);
658         if($ns){
659             $nsuri = $this->namespaces[$ns];
660             if($this->isDefaultNamespace($nsuri)){
661                 $name  = $local;
662                 $nsuri = '';
663             }
664         }
665
666         // this doesn't call the construcor: $node = $this->ownerDocument->createElement($name,$value);
667         $node = new EPubDOMElement($name,$value,$nsuri);
668         return $this->appendChild($node);
669     }
670
671     /**
672      * Split given name in namespace prefix and local part
673      *
674      * @param  string $name
675      * @return array  (namespace, name)
676      */
677     public function splitns($name){
678         $list = explode(':',$name,2);
679         if(count($list) < 2) array_unshift($list,'');
680         return $list;
681     }
682
683     /**
684      * Simple EPub namespace aware attribute accessor
685      */
686     public function attr($attr,$value=null){
687         list($ns,$attr) = $this->splitns($attr);
688
689         $nsuri = '';
690         if($ns){
691             $nsuri = $this->namespaces[$ns];
692             if(!$this->namespaceURI){
693                 if($this->isDefaultNamespace($nsuri)){
694                     $nsuri = '';
695                 }
696             }elseif($this->namespaceURI == $nsuri){
697                  $nsuri = '';
698             }
699         }
700
701         if(!is_null($value)){
702             if($value === false){
703                 // delete if false was given
704                 if($nsuri){
705                     $this->removeAttributeNS($nsuri,$attr);
706                 }else{
707                     $this->removeAttribute($attr);
708                 }
709             }else{
710                 // modify if value was given
711                 if($nsuri){
712                     $this->setAttributeNS($nsuri,$attr,$value);
713                 }else{
714                     $this->setAttribute($attr,$value);
715                 }
716             }
717         }else{
718             // return value if none was given
719             if($nsuri){
720                 return $this->getAttributeNS($nsuri,$attr);
721             }else{
722                 return $this->getAttribute($attr);
723             }
724         }
725     }
726
727     /**
728      * Remove this node from the DOM
729      */
730     public function delete(){
731         $this->parentNode->removeChild($this);
732     }
733
734 }
735
736