1<?php
2/**
3 * Embed an image gallery
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 * @author     Joe Lapp <joe.lapp@pobox.com>
8 * @author     Dave Doyle <davedoyle.canadalawbook.ca>
9 */
10
11if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
13require_once(DOKU_PLUGIN.'syntax.php');
14require_once(DOKU_INC.'inc/search.php');
15require_once(DOKU_INC.'inc/JpegMeta.php');
16
17class syntax_plugin_gallery extends DokuWiki_Syntax_Plugin {
18
19    /**
20     * What kind of syntax are we?
21     */
22    function getType(){
23        return 'substition';
24    }
25
26    /**
27     * What about paragraphs?
28     */
29    function getPType(){
30        return 'block';
31    }
32
33    /**
34     * Where to sort in?
35     */
36    function getSort(){
37        return 301;
38    }
39
40
41    /**
42     * Connect pattern to lexer
43     */
44    function connectTo($mode) {
45        $this->Lexer->addSpecialPattern('\{\{gallery>[^}]*\}\}',$mode,'plugin_gallery');
46    }
47
48    /**
49     * Handle the match
50     */
51    function handle($match, $state, $pos, Doku_Handler $handler){
52        global $ID;
53        $match = substr($match,10,-2); //strip markup from start and end
54
55        $data = array();
56
57        $data['galid'] = substr(md5($match),0,4);
58
59        // alignment
60        $data['align'] = 0;
61        if(substr($match,0,1) == ' ') $data['align'] += 1;
62        if(substr($match,-1,1) == ' ') $data['align'] += 2;
63
64        // extract params
65        list($ns,$params) = explode('?',$match,2);
66        $ns = trim($ns);
67
68        // namespace (including resolving relatives)
69        if(!preg_match('/^https?:\/\//i',$ns)){
70            $data['ns'] = resolve_id(getNS($ID),$ns);
71        }else{
72            $data['ns'] =  $ns;
73        }
74
75        // set the defaults
76        $data['tw']       = $this->getConf('thumbnail_width');
77        $data['th']       = $this->getConf('thumbnail_height');
78        $data['iw']       = $this->getConf('image_width');
79        $data['ih']       = $this->getConf('image_height');
80        $data['cols']     = $this->getConf('cols');
81        $data['filter']   = '';
82        $data['lightbox'] = false;
83        $data['direct']   = false;
84        $data['showname'] = false;
85        $data['showtitle'] = false;
86        $data['reverse']  = false;
87        $data['random']   = false;
88        $data['cache']    = true;
89        $data['crop']     = false;
90        $data['recursive']= true;
91        $data['sort']     = $this->getConf('sort');
92        $data['limit']    = 0;
93        $data['offset']   = 0;
94        $data['paginate'] = 0;
95
96        // parse additional options
97        $params = $this->getConf('options').','.$params;
98        $params = preg_replace('/[,&\?]+/',' ',$params);
99        $params = explode(' ',$params);
100        foreach($params as $param){
101            if($param === '') continue;
102            if($param == 'titlesort'){
103                $data['sort'] = 'title';
104            }elseif($param == 'datesort'){
105                $data['sort'] = 'date';
106            }elseif($param == 'modsort'){
107                $data['sort'] = 'mod';
108            }elseif(preg_match('/^=(\d+)$/',$param,$match)){
109                $data['limit'] = $match[1];
110            }elseif(preg_match('/^\+(\d+)$/',$param,$match)){
111                $data['offset'] = $match[1];
112            }elseif(is_numeric($param)){
113                $data['cols'] = (int) $param;
114            }elseif(preg_match('/^~(\d+)$/',$param,$match)){
115                $data['paginate'] = $match[1];
116            }elseif(preg_match('/^(\d+)([xX])(\d+)$/',$param,$match)){
117                if($match[2] == 'X'){
118                    $data['iw'] = $match[1];
119                    $data['ih'] = $match[3];
120                }else{
121                    $data['tw'] = $match[1];
122                    $data['th'] = $match[3];
123                }
124            }elseif(strpos($param,'*') !== false){
125                $param = preg_quote($param,'/');
126                $param = '/^'.str_replace('\\*','.*?',$param).'$/';
127                $data['filter'] = $param;
128            }else{
129                if(substr($param,0,2) == 'no'){
130                    $data[substr($param,2)] = false;
131                }else{
132                    $data[$param] = true;
133                }
134            }
135        }
136
137        // implicit direct linking?
138        if($data['lightbox']) $data['direct']   = true;
139
140
141        return $data;
142    }
143
144    /**
145     * Create output
146     */
147    function render($mode, Doku_Renderer $R, $data){
148        global $ID;
149        if($mode == 'xhtml'){
150            $R->info['cache'] &= $data['cache'];
151            $R->doc .= $this->_gallery($data);
152            return true;
153        }elseif($mode == 'metadata'){
154            $rel = p_get_metadata($ID,'relation',METADATA_RENDER_USING_CACHE);
155            $img = $rel['firstimage'];
156            if(empty($img)){
157                $files = $this->_findimages($data);
158                if(count($files)) $R->internalmedia($files[0]['id']);
159            }
160            return true;
161        }
162        return false;
163    }
164
165    /**
166     * Loads images from a MediaRSS or ATOM feed
167     */
168    function _loadRSS($url){
169        require_once(DOKU_INC.'inc/FeedParser.php');
170        $feed = new FeedParser();
171        $feed->set_feed_url($url);
172        $feed->init();
173        $files = array();
174
175        // base url to use for broken feeds with non-absolute links
176        $main = parse_url($url);
177        $host = $main['scheme'].'://'.
178                $main['host'].
179                (($main['port'])?':'.$main['port']:'');
180        $path = dirname($main['path']).'/';
181
182        foreach($feed->get_items() as $item){
183            if ($enclosure = $item->get_enclosure()){
184                // skip non-image enclosures
185                if($enclosure->get_type()){
186                    if(substr($enclosure->get_type(),0,5) != 'image') continue;
187                }else{
188                    if(!preg_match('/\.(jpe?g|png|gif)(\?|$)/i',
189                       $enclosure->get_link())) continue;
190                }
191
192                // non absolute links
193                $ilink = $enclosure->get_link();
194                if(!preg_match('/^https?:\/\//i',$ilink)){
195                    if($ilink[0] == '/'){
196                        $ilink = $host.$ilink;
197                    }else{
198                        $ilink = $host.$path.$ilink;
199                    }
200                }
201                $link = $item->link;
202                if(!preg_match('/^https?:\/\//i',$link)){
203                    if($link[0] == '/'){
204                        $link = $host.$link;
205                    }else{
206                        $link = $host.$path.$link;
207                    }
208                }
209
210                $files[] = array(
211                    'id'     => $ilink,
212                    'isimg'  => true,
213                    'file'   => basename($ilink),
214                    // decode to avoid later double encoding
215                    'title'  => htmlspecialchars_decode($enclosure->get_title(),ENT_COMPAT),
216                    'desc'   => strip_tags(htmlspecialchars_decode($enclosure->get_description(),ENT_COMPAT)),
217                    'width'  => $enclosure->get_width(),
218                    'height' => $enclosure->get_height(),
219                    'mtime'  => $item->get_date('U'),
220                    'ctime'  => $item->get_date('U'),
221                    'detail' => $link,
222                );
223            }
224        }
225        return $files;
226    }
227
228    /**
229     * Gather all photos matching the given criteria
230     */
231    function _findimages(&$data){
232        global $conf;
233        $files = array();
234
235        // http URLs are supposed to be media RSS feeds
236        if(preg_match('/^https?:\/\//i',$data['ns'])){
237            $files = $this->_loadRSS($data['ns']);
238            $data['_single'] = false;
239        }else{
240            $dir = utf8_encodeFN(str_replace(':','/',$data['ns']));
241            // all possible images for the given namespace (or a single image)
242            if(is_file($conf['mediadir'].'/'.$dir)){
243                require_once(DOKU_INC.'inc/JpegMeta.php');
244                $files[] = array(
245                    'id'    => $data['ns'],
246                    'isimg' => preg_match('/\.(jpe?g|gif|png)$/',$dir),
247                    'file'  => basename($dir),
248                    'mtime' => filemtime($conf['mediadir'].'/'.$dir),
249                    'meta'  => new JpegMeta($conf['mediadir'].'/'.$dir)
250                );
251                $data['_single'] = true;
252            }else{
253                $depth = $data['recursive'] ? 0 : 1;
254                search($files,
255                       $conf['mediadir'],
256                       'search_media',
257                       array('depth'=>$depth),
258                       $dir);
259                $data['_single'] = false;
260            }
261        }
262
263        // done, yet?
264        $len = count($files);
265        if(!$len) return $files;
266        if($data['single']) return $files;
267
268        // filter images
269        for($i=0; $i<$len; $i++){
270            if(!$files[$i]['isimg']){
271                unset($files[$i]); // this is faster, because RE was done before
272            }elseif($data['filter']){
273                if(!preg_match($data['filter'],noNS($files[$i]['id']))) unset($files[$i]);
274            }
275        }
276        if($len<1) return $files;
277
278        // random?
279        if($data['random']){
280            shuffle($files);
281        }else{
282            // sort?
283            if($data['sort'] == 'date'){
284                usort($files,array($this,'_datesort'));
285            }elseif($data['sort'] == 'mod'){
286                usort($files,array($this,'_modsort'));
287            }elseif($data['sort'] == 'title'){
288                usort($files,array($this,'_titlesort'));
289            }
290
291            // reverse?
292            if($data['reverse']) $files = array_reverse($files);
293        }
294
295        // limits and offsets?
296        if($data['offset']) $files = array_slice($files,$data['offset']);
297        if($data['limit']) $files = array_slice($files,0,$data['limit']);
298
299        return $files;
300    }
301
302    /**
303     * usort callback to sort by file lastmodified time
304     */
305    function _modsort($a,$b){
306        if($a['mtime'] < $b['mtime']) return -1;
307        if($a['mtime'] > $b['mtime']) return 1;
308        return strcmp($a['file'],$b['file']);
309    }
310
311    /**
312     * usort callback to sort by EXIF date
313     */
314    function _datesort($a,$b){
315        $da = $this->_meta($a,'cdate');
316        $db = $this->_meta($b,'cdate');
317        if($da < $db) return -1;
318        if($da > $db) return 1;
319        return strcmp($a['file'],$b['file']);
320    }
321
322    /**
323     * usort callback to sort by EXIF title
324     */
325    function _titlesort($a,$b){
326        $ta = $this->_meta($a,'title');
327        $tb = $this->_meta($b,'title');
328        return strcmp($ta,$tb);
329    }
330
331
332    /**
333     * Does the gallery formatting
334     */
335    function _gallery($data){
336        global $conf;
337        global $lang;
338        $ret = '';
339
340        $files = $this->_findimages($data);
341
342        //anything found?
343        if(!count($files)){
344            $ret .= '<div class="nothing">'.$this->getLang('nothingfound').'</div>';
345            return $ret;
346        }
347
348        // prepare alignment
349        $align = '';
350        $xalign = '';
351        if($data['align'] == 1){
352            $align  = ' gallery_right';
353            $xalign = ' align="right"';
354        }
355        if($data['align'] == 2){
356            $align  = ' gallery_left';
357            $xalign = ' align="left"';
358        }
359        if($data['align'] == 3){
360            $align  = ' gallery_center';
361            $xalign = ' align="center"';
362        }
363        if(!$data['_single']){
364            if(!$align) $align = ' gallery_center'; // center galleries on default
365            if(!$xalign) $xalign = ' align="center"';
366        }
367
368        $page = 0;
369
370        // build gallery
371        if($data['_single']){
372            $ret .= $this->_image($files[0],$data);
373            $ret .= $this->_showname($files[0],$data);
374            $ret .= $this->_showtitle($files[0],$data);
375        }elseif($data['cols'] > 0){ // format as table
376            $close_pg = false;
377
378            $i = 0;
379            foreach($files as $img){
380
381                // new page?
382                if($data['paginate'] && ($i % $data['paginate'] == 0)){
383                     $ret .= '<div class="gallery_page gallery__'.$data['galid'].'" id="gallery__'.$data['galid'].'_'.(++$page).'">';
384                     $close_pg = true;
385                }
386
387                // new table?
388                if($i == 0 || ($data['paginate'] && ($i % $data['paginate'] == 0))){
389                    $ret .= '<table>';
390
391                }
392
393                // new row?
394                if($i % $data['cols'] == 0){
395                    $ret .= '<tr>';
396                }
397
398                // an image cell
399                $ret .= '<td>';
400                $ret .= $this->_image($img,$data);
401                $ret .= $this->_showname($img,$data);
402                $ret .= $this->_showtitle($img,$data);
403                $ret .= '</td>';
404                $i++;
405
406                // done with this row? close it
407                $close_tr = true;
408                if($i % $data['cols'] == 0){
409                    $ret .= '</tr>';
410                    $close_tr = false;
411                }
412
413                // close current page and table
414                if($data['paginate'] && ($i % $data['paginate'] == 0)){
415                    if ($close_tr){
416                        // add remaining empty cells
417                        while($i % $data['cols']){
418                            $ret .= '<td></td>';
419                            $i++;
420                        }
421                        $ret .= '</tr>';
422                    }
423                    $ret .= '</table>';
424                    $ret .= '</div>';
425                    $close_pg = false;
426                    $i = 0; // reset counter to ensure next page is properly started
427                }
428
429            } // foreach
430
431            if ($close_tr){
432                // add remaining empty cells
433                while($i % $data['cols']){
434                    $ret .= '<td></td>';
435                    $i++;
436                }
437                $ret .= '</tr>';
438            }
439
440            if(!$data['paginate']){
441                $ret .= '</table>';
442            }elseif ($close_pg){
443                $ret .= '</table>';
444                $ret .= '</div>';
445            }
446        }else{ // format as div sequence
447            $i = 0;
448            $close_pg = false;
449            foreach($files as $img){
450
451                if($data['paginate'] && ($i % $data['paginate'] == 0)){
452                     $ret .= '<div class="gallery_page gallery__'.$data['galid'].'" id="gallery__'.$data['galid'].'_'.(++$page).'">';
453                     $close_pg = true;
454                }
455
456                $ret .= '<div>';
457                $ret .= $this->_image($img,$data);
458                $ret .= $this->_showname($img,$data);
459                $ret .= $this->_showtitle($img,$data);
460                $ret .= '</div> ';
461
462                $i++;
463
464                if($data['paginate'] && ($i % $data['paginate'] == 0)){
465                    $ret .= '</div>';
466                    $close_pg = false;
467                }
468            }
469
470            if($close_pg) $ret .= '</div>';
471
472            $ret .= '<br style="clear:both" />';
473        }
474
475        // pagination links
476        $pgret = '';
477        if($page){
478            $pgret .= '<div class="gallery_pages"><span>'.$this->getLang('pages').' </span>';
479            for($j=1; $j<=$page; $j++){
480                $pgret .= '<a href="#gallery__'.$data['galid'].'_'.$j.'" class="gallery_pgsel button">'.$j.'</a> ';
481            }
482            $pgret .= '</div>';
483        }
484
485        return '<div class="gallery'.$align.'"'.$xalign.'>'.$pgret.$ret.'<div class="clearer"></div></div>';
486    }
487
488    /**
489     * Defines how a thumbnail should look like
490     */
491    function _image(&$img,$data){
492        global $ID;
493
494        // calculate thumbnail size
495        if(!$data['crop']){
496            $w = (int) $this->_meta($img,'width');
497            $h = (int) $this->_meta($img,'height');
498            if($w && $h){
499                $dim = array();
500                if($w > $data['tw'] || $h > $data['th']){
501                    $ratio = $this->_ratio($img,$data['tw'],$data['th']);
502                    $w = floor($w * $ratio);
503                    $h = floor($h * $ratio);
504                    $dim = array('w'=>$w,'h'=>$h);
505                }
506            }else{
507                $data['crop'] = true; // no size info -> always crop
508            }
509        }
510        if($data['crop']){
511            $w = $data['tw'];
512            $h = $data['th'];
513            $dim = array('w'=>$w,'h'=>$h);
514        }
515
516        //prepare img attributes
517        $i             = array();
518        $i['width']    = $w;
519        $i['height']   = $h;
520        $i['border']   = 0;
521        $i['alt']      = $this->_meta($img,'title');
522        $i['class']    = 'tn';
523        $iatt = buildAttributes($i);
524        $src  = ml($img['id'],$dim);
525
526        // prepare lightbox dimensions
527        $w_lightbox = (int) $this->_meta($img,'width');
528        $h_lightbox = (int) $this->_meta($img,'height');
529        $dim_lightbox = array();
530        if($w_lightbox > $data['iw'] || $h_lightbox > $data['ih']){
531            $ratio = $this->_ratio($img,$data['iw'],$data['ih']);
532            $w_lightbox = floor($w_lightbox * $ratio);
533            $h_lightbox = floor($h_lightbox * $ratio);
534            $dim_lightbox = array('w'=>$w_lightbox,'h'=>$h_lightbox);
535        }
536
537        //prepare link attributes
538        $a           = array();
539        $a['title']  = $this->_meta($img,'title');
540        $a['data-caption'] = trim(str_replace("\n",' ',$this->_meta($img,'desc')));
541        if(!$a['data-caption']) unset($a['data-caption']);
542        if($data['lightbox']){
543            $href   = ml($img['id'],$dim_lightbox);
544            $a['class'] = "lightbox JSnocheck";
545            $a['rel']   = 'lightbox[gal-'.substr(md5($ID),4).']'; //unique ID for the gallery
546        }elseif($img['detail'] && !$data['direct']){
547            $href   = $img['detail'];
548        }else{
549            $href   = ml($img['id'],array('id'=>$ID),$data['direct']);
550        }
551        $aatt = buildAttributes($a);
552
553        // prepare output
554        $ret  = '';
555        $ret .= '<a href="'.$href.'" '.$aatt.'>';
556        $ret .= '<img src="'.$src.'" '.$iatt.' />';
557        $ret .= '</a>';
558        return $ret;
559    }
560
561
562    /**
563     * Defines how a filename + link should look
564     */
565    function _showname($img,$data){
566        global $ID;
567
568        if(!$data['showname'] ) { return ''; }
569
570        //prepare link
571        $lnk = ml($img['id'],array('id'=>$ID),false);
572
573        // prepare output
574        $ret  = '';
575        $ret .= '<br /><a href="'.$lnk.'">';
576        $ret .= hsc($img['file']);
577        $ret .= '</a>';
578        return $ret;
579    }
580
581    /**
582     * Defines how title + link should look
583     */
584    function _showtitle($img,$data){
585        global $ID;
586
587        if(!$data['showtitle'] ) { return ''; }
588
589        //prepare link
590        $lnk = ml($img['id'],array('id'=>$ID),false);
591
592        // prepare output
593        $ret  = '';
594        $ret .= '<br /><a href="'.$lnk.'">';
595        $ret .= hsc($this->_meta($img,'title'));
596        $ret .= '</a>';
597        return $ret;
598    }
599
600    /**
601     * Return the metadata of an item
602     *
603     * Automatically checks if a JPEGMeta object is available or if all data is
604     * supplied in array
605     */
606    function _meta(&$img,$opt){
607        if($img['meta']){
608            // map JPEGMeta calls to opt names
609
610            switch($opt){
611                case 'title':
612                    return $img['meta']->getField('Simple.Title');
613                case 'desc':
614                    return $img['meta']->getField('Iptc.Caption');
615                case 'cdate':
616                    return $img['meta']->getField('Date.EarliestTime');
617                case 'width':
618                    return $img['meta']->getField('File.Width');
619                case 'height':
620                    return $img['meta']->getField('File.Height');
621
622
623                default:
624                    return '';
625            }
626
627        }else{
628            // just return the array field
629            return $img[$opt];
630        }
631    }
632
633    /**
634     * Calculates the multiplier needed to resize the image to the given
635     * dimensions
636     *
637     * @author Andreas Gohr <andi@splitbrain.org>
638     */
639    function _ratio(&$img,$maxwidth,$maxheight=0){
640        if(!$maxheight) $maxheight = $maxwidth;
641
642        $w = $this->_meta($img,'width');
643        $h = $this->_meta($img,'height');
644
645        $ratio = 1;
646        if($w >= $h){
647            if($w >= $maxwidth){
648                $ratio = $maxwidth/$w;
649            }elseif($h > $maxheight){
650                $ratio = $maxheight/$h;
651            }
652        }else{
653            if($h >= $maxheight){
654                $ratio = $maxheight/$h;
655            }elseif($w > $maxwidth){
656                $ratio = $maxwidth/$w;
657            }
658        }
659        return $ratio;
660    }
661
662}
663
664//Setup VIM: ex: et ts=4 enc=utf-8 :
665