issue84, don't try to display not existing pictures
[phpfspot.git] / phpfspot.class.php
1 <?php
2
3 /***************************************************************************
4  *
5  * Copyright (c) by Andreas Unterkircher, unki@netshadow.at
6  * All rights reserved
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU General Public License as published by
10  *  the Free Software Foundation; either version 2 of the License, or
11  *  any later version.
12  *
13  *  This program is distributed in the hope that it will be useful,
14  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *  GNU General Public License for more details.
17  *
18  *  You should have received a copy of the GNU General Public License
19  *  along with this program; if not, write to the Free Software
20  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21  *
22  ***************************************************************************/
23
24 require_once "phpfspot_cfg.php";
25 require_once "phpfspot_db.php";
26
27 class PHPFSPOT {
28
29    var $cfg;
30    var $db;
31    var $cfg_db;
32    var $tmpl;
33    var $tags;
34    var $avail_tags;
35    private $dbver;
36
37    /**
38     * class constructor
39     *
40     * this function will be called on class construct
41     * and will check requirements, loads configuration,
42     * open databases and start the user session
43     */
44    public function __construct()
45    {
46       $this->cfg = new PHPFSPOT_CFG;
47
48       /* set application name and version information */
49       $this->cfg->product = "phpfspot";
50       $this->cfg->version = "1.2";
51
52       /* Check necessary requirements */
53       if(!$this->checkRequirements()) {
54          exit(1);
55       }
56
57       $this->db  = new PHPFSPOT_DB($this, $this->cfg->fspot_db);
58       if(!is_writeable($this->cfg->fspot_db)) {
59          print $this->cfg->fspot_db ." is not writeable for user ". $this->getuid() ."\n";
60          exit(1);
61       }
62
63       $this->dbver = $this->getFspotDBVersion();
64
65       if(!is_writeable(dirname($this->cfg->phpfspot_db))) {
66          print dirname($this->cfg->phpfspot_db) .": directory is not writeable for user ". $this->getuid() ."\n";
67          exit(1);
68       }
69
70       if(!is_writeable($this->cfg->base_path ."/templates_c")) {
71          print $this->cfg->base_path ."/templates_c: directory is not writeable for user ". $this->getuid() ."\n";
72          exit(1);
73       }
74
75       $this->cfg_db = new PHPFSPOT_DB($this, $this->cfg->phpfspot_db);
76       if(!is_writeable($this->cfg->phpfspot_db)) {
77          print $this->cfg->phpfspot_db ." is not writeable for user ". $this->getuid() ."\n";
78          exit(1);
79       }
80
81       $this->check_config_table();
82
83       /* include Smarty template engine */
84       if(!$this->check_readable($this->cfg->smarty_path .'/libs/Smarty.class.php')) {
85          exit(1);
86       }
87       require $this->cfg->smarty_path .'/libs/Smarty.class.php';
88       /* overload Smarty class if our own template handler */
89       require_once "phpfspot_tmpl.php";
90       $this->tmpl = new PHPFSPOT_TMPL($this);
91
92       /* check if all necessary indices exist */
93       $this->checkDbIndices();
94
95       /* if session is not yet started, do it now */
96       if(session_id() == "")
97          session_start();
98
99       if(!isset($_SESSION['tag_condition']))
100          $_SESSION['tag_condition'] = 'or';
101
102       if(!isset($_SESSION['sort_order']))
103          $_SESSION['sort_order'] = 'date_asc';
104
105       if(!isset($_SESSION['searchfor']))
106          $_SESSION['searchfor'] = '';
107
108       // if begin_with is still set but rows_per_page is now 0, unset it
109       if(isset($_SESSION['begin_with']) && $this->cfg->rows_per_page == 0)
110          unset($_SESSION['begin_with']);
111
112    } // __construct()
113
114    public function __destruct()
115    {
116
117    } // __destruct()
118
119    /**
120     * show - generate html output
121     *
122     * this function can be called after the constructor has
123     * prepared everyhing. it will load the index.tpl smarty
124     * template. if necessary it will registere pre-selects
125     * (photo index, photo, tag search, date search) into
126     * users session.
127     */
128    public function show()
129    {
130       $this->tmpl->assign('searchfor', $_SESSION['searchfor']);
131       $this->tmpl->assign('page_title', $this->cfg->page_title);
132       $this->tmpl->assign('current_condition', $_SESSION['tag_condition']);
133       $this->tmpl->assign('template_path', 'themes/'. $this->cfg->theme_name);
134
135       if(isset($_GET['mode'])) {
136
137          $_SESSION['start_action'] = $_GET['mode'];
138
139          switch($_GET['mode']) {
140             case 'showpi':
141                if(isset($_GET['tags'])) {
142                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
143                }
144                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
145                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
146                }
147                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
148                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
149                }
150                break;
151             case 'showp':
152                if(isset($_GET['tags'])) {
153                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
154                   $_SESSION['start_action'] = 'showp';
155                }
156                if(isset($_GET['id']) && is_numeric($_GET['id'])) {
157                   $_SESSION['current_photo'] = $_GET['id'];
158                   $_SESSION['start_action'] = 'showp';
159                }
160                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
161                   $_SESSION['from_date'] = strtotime($_GET['from_date']);
162                }
163                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
164                   $_SESSION['to_date'] = strtotime($_GET['to_date']);
165                }
166                break;
167             case 'export':
168                $this->tmpl->show("export.tpl");
169                return;
170                break;
171             case 'slideshow':
172                $this->tmpl->show("slideshow.tpl");
173                return;
174                break;
175             case 'rss':
176                if(isset($_GET['tags'])) {
177                   $_SESSION['selected_tags'] = $this->extractTags($_GET['tags']);
178                }
179                if(isset($_GET['from_date']) && $this->isValidDate($_GET['from_date'])) {
180                   $_SESSION['from_date'] = strtotime($_GET['from_date'] ." 00:00:00");
181                }
182                if(isset($_GET['to_date']) && $this->isValidDate($_GET['to_date'])) {
183                   $_SESSION['to_date'] = strtotime($_GET['to_date'] ." 23:59:59");
184                }
185                $this->getRSSFeed();
186                return;
187                break;
188          }
189       }
190
191       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date']))
192          $this->tmpl->assign('date_search_enabled', true);
193
194       $this->tmpl->assign('from_date', $this->get_calendar('from'));
195       $this->tmpl->assign('to_date', $this->get_calendar('to'));
196       $this->tmpl->assign('sort_field', $this->get_sort_field());
197       $this->tmpl->assign('content_page', 'welcome.tpl');
198       $this->tmpl->show("index.tpl");
199
200    } // show()
201
202    /**
203     * get_tags - grab all tags of f-spot's database
204     *
205     * this function will get all available tags from
206     * the f-spot database and store them within two
207     * arrays within this class for later usage. in
208     * fact, if the user requests (hide_tags) it will
209     * opt-out some of them.
210     *
211     * this function is getting called once by show()
212     */
213    private function get_tags()
214    {
215       $this->avail_tags = Array();
216       $count = 0;
217    
218       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
219          $query_str="
220             SELECT
221                DISTINCT t1.id as id, t1.name as name
222             FROM  
223                photo_tags pt1
224             INNER JOIN photo_tags
225                pt2 ON pt1.photo_id=pt2.photo_id
226             INNER JOIN tags t1
227                ON t1.id=pt1.tag_id
228             INNER JOIN tags t2
229                ON t2.id=pt2.tag_id
230             WHERE
231                t2.name IN  ('".implode("','",$this->cfg->show_tags)."')
232             ORDER BY
233                t1.sort_priority ASC";
234
235          $result = $this->db->db_query($query_str);
236       }
237       else
238       {
239          $result = $this->db->db_query("
240             SELECT id,name
241             FROM tags
242             ORDER BY sort_priority ASC
243          ");
244       }
245       
246       while($row = $this->db->db_fetch_object($result)) {
247
248          $tag_id = $row['id'];
249          $tag_name = $row['name'];
250
251          /* if the user has specified to ignore this tag in phpfspot's
252             configuration, ignore it here so it does not get added to
253             the tag list.
254          */
255          if(in_array($row['name'], $this->cfg->hide_tags))
256             continue;
257
258          /* if you include the following if-clause and the user has specified
259             to only show certain tags which are specified in phpfspot's
260             configuration, ignore all others so they will not be added to the
261             tag list.
262          if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags) &&
263             !in_array($row['name'], $this->cfg->show_tags))
264             continue;
265          */
266
267          $this->tags[$tag_id] = $tag_name; 
268          $this->avail_tags[$count] = $tag_id;
269          $count++;
270
271       }
272
273    } // get_tags()
274
275    /** 
276     * extract all photo details
277     * 
278     * retrieve all available details from f-spot's
279     * database and return them as object
280     */
281    public function get_photo_details($idx)
282    {
283       if($this->dbver < 9) {
284          $query_str = "
285             SELECT p.id, p.name, p.time, p.directory_path, p.description
286             FROM photos p
287          ";
288       }
289       else {
290          $query_str = "
291             SELECT p.id, p.uri, p.time, p.description
292             FROM photos p
293          ";
294       }
295
296       /* if show_tags is set, only return details for photos which
297          are specified to be shown
298       */
299       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
300          $query_str.= "
301             INNER JOIN photo_tags pt
302                ON p.id=pt.photo_id
303             INNER JOIN tags t
304                ON pt.tag_id=t.id
305             WHERE p.id='". $idx ."'
306             AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
307       }
308       else {
309          $query_str.= "
310             WHERE p.id='". $idx ."'
311          ";
312       }
313
314       if($result = $this->db->db_query($query_str)) {
315
316          $row = $this->db->db_fetch_object($result);
317
318          if($this->dbver < 9) {
319             $row['uri'] = "file://". $row['directory_path'] ."/". $row['name'];
320          }
321
322          return $row;
323
324       }
325    
326       return null;
327
328    } // get_photo_details
329
330    /**
331     * returns aligned photo names 
332     *
333     * this function returns aligned (length) names for
334     * an specific photo. If the length of the name exceeds
335     * $limit the name will be shrinked (...)
336     */
337    public function getPhotoName($idx, $limit = 0)
338    {
339       if($details = $this->get_photo_details($idx)) {
340          if($long_name = $this->parse_uri($details['uri'], 'filename')) {
341             $name = $this->shrink_text($long_name, $limit);
342             return $name;
343          }
344       }
345
346       return null;
347
348    } // getPhotoName()
349
350    /**
351     * shrink text according provided limit
352     *
353     * If the length of the name exceeds $limit the
354     * text will be shortend and some content in between
355     * will be replaced with "..." 
356     */
357    private function shrink_text($text, $limit)
358    {
359       if($limit != 0 && strlen($text) > $limit) {
360          $text = substr($text, 0, $limit-5) ."...". substr($text, -($limit-5));
361       }
362
363       return $text;
364
365    } // shrink_text();
366
367    /**
368     * translate f-spoth photo path
369     * 
370     * as the full-qualified path recorded in the f-spot database
371     * is usally not the same as on the webserver, this function
372     * will replace the path with that one specified in the cfg
373     */
374    public function translate_path($path, $width = 0)
375    {  
376       return str_replace($this->cfg->path_replace_from, $this->cfg->path_replace_to, $path);
377
378    } // translate_path
379
380    /**
381     * control HTML ouput for a single photo
382     *
383     * this function provides all the necessary information
384     * for the single photo template.
385     */
386    public function showPhoto($photo)
387    {
388       /* get all photos from the current photo selection */
389       $all_photos = $this->getPhotoSelection();
390       $count = count($all_photos);
391
392       for($i = 0; $i < $count; $i++) {
393          
394          // $get_next will be set, when the photo which has to
395          // be displayed has been found - this means that the
396          // next available is in fact the NEXT image (for the
397          // navigation icons) 
398          if(isset($get_next)) {
399             $next_img = $all_photos[$i];
400             break;
401          }
402
403          /* the next photo is our NEXT photo */
404          if($all_photos[$i] == $photo) {
405             $get_next = 1;
406          }
407          else {
408             $previous_img = $all_photos[$i];
409          }
410
411          if($photo == $all_photos[$i]) {
412                $current = $i;
413          }
414       }
415
416       $details = $this->get_photo_details($photo);
417
418       if(!$details) {
419          print "error";
420          return;
421       }
422
423       $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
424       $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo);
425
426       if(!file_exists($orig_path)) {
427          $this->_error("Photo ". $orig_path ." does not exist!<br />\n");
428       }
429
430       if(!is_readable($orig_path)) {
431          $this->_error("Photo ". $orig_path ." is not readable for user ". $this->getuid() ."<br />\n");
432       }
433
434       /* If the thumbnail doesn't exist yet, try to create it */
435       if(!file_exists($thumb_path)) {
436          $this->gen_thumb($photo, true);
437          $thumb_path = $this->get_thumb_path($this->cfg->photo_width, $photo);
438       }
439
440       /* get f-spot database meta information */
441       $meta = $this->get_meta_informations($orig_path);
442
443       /* If EXIF data are available, use them */
444       if(isset($meta['ExifImageWidth'])) {
445          $meta_res = $meta['ExifImageWidth'] ."x". $meta['ExifImageLength'];
446       } else {
447          $info = getimagesize($orig_path);
448          $meta_res = $info[0] ."x". $info[1]; 
449       }
450
451       $meta_date = isset($meta['FileDateTime']) ? strftime("%a %x %X", $meta['FileDateTime']) : "n/a";
452       $meta_make = isset($meta['Make']) ? $meta['Make'] ." / ". $meta['Model'] : "n/a";
453       $meta_size = isset($meta['FileSize']) ? round($meta['FileSize']/1024, 1) ."kbyte" : "n/a";
454
455       $extern_link = "index.php?mode=showp&id=". $photo;
456       $current_tags = $this->getCurrentTags();
457       if($current_tags != "") {
458          $extern_link.= "&tags=". $current_tags;
459       }
460       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
461          $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
462       }
463
464       $this->tmpl->assign('extern_link', $extern_link);
465
466       if(file_exists($thumb_path)) {
467
468          $info = getimagesize($thumb_path);
469
470          $this->tmpl->assign('description', $details['description']);
471          $this->tmpl->assign('image_name', $this->parse_uri($details['uri'], 'filename'));
472
473          $this->tmpl->assign('width', $info[0]);
474          $this->tmpl->assign('height', $info[1]);
475          $this->tmpl->assign('ExifMadeOn', $meta_date);
476          $this->tmpl->assign('ExifMadeWith', $meta_make);
477          $this->tmpl->assign('ExifOrigResolution', $meta_res);
478          $this->tmpl->assign('ExifFileSize', $meta_size);
479     
480          $this->tmpl->assign('image_url', 'phpfspot_img.php?idx='. $photo ."&amp;width=". $this->cfg->photo_width);
481          $this->tmpl->assign('image_url_full', 'phpfspot_img.php?idx='. $photo);
482
483          $this->tmpl->assign('tags', $this->get_photo_tags($photo));
484          $this->tmpl->assign('current', $current);
485       }
486       else {
487          $this->_error("Can't open file ". $thumb_path ."\n");
488          return;
489       }
490
491       if($previous_img) {
492          $this->tmpl->assign('previous_url', "javascript:showImage(". $previous_img .");");
493          $this->tmpl->assign('prev_img', $previous_img);
494       }
495
496       if($next_img) {
497          $this->tmpl->assign('next_url', "javascript:showImage(". $next_img .");");
498          $this->tmpl->assign('next_img', $next_img);
499       }
500       $this->tmpl->assign('mini_width', $this->cfg->mini_width);
501       $this->tmpl->assign('photo_number', $i);
502       $this->tmpl->assign('photo_count', count($all_photos));
503
504       $this->tmpl->show("single_photo.tpl");
505
506    } // showPhoto()
507
508    /**
509     * all available tags and tag cloud
510     *
511     * this function outputs all available tags (time ordered)
512     * and in addition output them as tag cloud (tags which have
513     * many photos will appears more then others)
514     */
515    public function getAvailableTags()
516    {
517       $this->get_tags();
518
519       $output = "";
520
521       $result = $this->db->db_query("
522          SELECT tag_id as id, count(tag_id) as quantity
523          FROM photo_tags
524          INNER JOIN tags t
525             ON t.id = tag_id
526          GROUP BY tag_id
527          ORDER BY t.name ASC
528       ");
529
530       $tags = Array();
531
532       while($row = $this->db->db_fetch_object($result)) {
533          $tags[$row['id']] = $row['quantity'];
534       }
535
536       // change these font sizes if you will
537       $max_size = 125; // max font size in %
538       $min_size = 75; // min font size in %
539
540       // get the largest and smallest array values
541       $max_qty = max(array_values($tags));
542       $min_qty = min(array_values($tags));
543
544       // find the range of values
545       $spread = $max_qty - $min_qty;
546       if (0 == $spread) { // we don't want to divide by zero
547          $spread = 1;
548       }
549
550       // determine the font-size increment
551       // this is the increase per tag quantity (times used)
552       $step = ($max_size - $min_size)/($spread);
553
554       // loop through our tag array
555       foreach ($tags as $key => $value) {
556
557          if(isset($_SESSION['selected_tags']) && in_array($key, $_SESSION['selected_tags']))
558             continue;
559
560           // calculate CSS font-size
561           // find the $value in excess of $min_qty
562           // multiply by the font-size increment ($size)
563           // and add the $min_size set above
564          $size = $min_size + (($value - $min_qty) * $step);
565           // uncomment if you want sizes in whole %:
566          $size = ceil($size);
567
568          if(isset($this->tags[$key])) {
569             $output.= "<a href=\"javascript:Tags('add', ". $key .");\" class=\"tag\" style=\"font-size: ". $size ."%;\">". $this->tags[$key] ."</a>, ";
570          }
571
572       }
573
574       $output = substr($output, 0, strlen($output)-2);
575       print $output;
576
577    } // getAvailableTags()
578
579    /**
580     * output all selected tags
581     *
582     * this function output all tags which have been selected
583     * by the user. the selected tags are stored in the 
584     * session-variable $_SESSION['selected_tags']
585     */
586    public function getSelectedTags()
587    {
588       $this->get_tags();
589
590       $output = "";
591       foreach($this->avail_tags as $tag)
592       {
593          // return all selected tags
594          if(isset($_SESSION['selected_tags']) && in_array($tag, $_SESSION['selected_tags'])) {
595             $output.= "<a href=\"javascript:Tags('del', ". $tag .");\" class=\"tag\">". $this->tags[$tag] ."</a>, ";
596          }
597       }
598
599       $output = substr($output, 0, strlen($output)-2);
600       print $output;
601
602    } // getSelectedTags()
603
604    /**
605     * add tag to users session variable
606     *
607     * this function will add the specified to users current
608     * tag selection. if a date search has been made before
609     * it will be now cleared
610     */
611    public function addTag($tag)
612    {
613       if(!isset($_SESSION['selected_tags']))
614          $_SESSION['selected_tags'] = Array();
615
616       if(!in_array($tag, $_SESSION['selected_tags']))
617          array_push($_SESSION['selected_tags'], $tag);
618    
619    } // addTag()
620
621    /**
622     * remove tag to users session variable
623     *
624     * this function removes the specified tag from
625     * users current tag selection
626     */
627    public function delTag($tag)
628    {
629       if(isset($_SESSION['selected_tags'])) {
630          $key = array_search($tag, $_SESSION['selected_tags']);
631          unset($_SESSION['selected_tags'][$key]);
632          sort($_SESSION['selected_tags']);
633       }
634
635    } // delTag()
636
637    /**
638     * reset tag selection
639     *
640     * if there is any tag selection, it will be
641     * deleted now
642     */
643    public function resetTags()
644    {
645       if(isset($_SESSION['selected_tags']))
646          unset($_SESSION['selected_tags']);
647
648    } // resetTags()
649
650    /**
651     * reset single photo
652     *
653     * if a specific photo was requested (external link)
654     * unset the session variable now
655     */
656    public function resetPhotoView()
657    {
658       if(isset($_SESSION['current_photo']))
659          unset($_SESSION['current_photo']);
660
661    } // resetPhotoView();
662
663    /**
664     * reset tag search
665     *
666     * if any tag search has taken place, reset
667     * it now
668     */
669    public function resetTagSearch()
670    {
671       if(isset($_SESSION['searchfor']))
672          unset($_SESSION['searchfor']);
673
674    } // resetTagSearch()
675
676     /**
677     * reset date search
678     *
679     * if any date search has taken place, reset
680     * it now
681     */
682    public function resetDateSearch()
683    {
684       if(isset($_SESSION['from_date']))
685          unset($_SESSION['from_date']);
686       if(isset($_SESSION['to_date']))
687          unset($_SESSION['to_date']);
688
689    } // resetDateSearch();
690
691    /**
692     * return all photo according selection
693     *
694     * this function returns all photos based on
695     * the tag-selection, tag- or date-search.
696     * the tag-search also has to take care of AND
697     * and OR conjunctions
698     */
699    public function getPhotoSelection()
700    {  
701       $matched_photos = Array();
702
703       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
704          $from_date = $_SESSION['from_date'];
705          $to_date = $_SESSION['to_date'];
706          $additional_where_cond = "
707                p.time>='". $from_date ."'
708             AND
709                p.time<='". $to_date ."'
710          ";
711       } 
712
713       if(isset($_SESSION['sort_order'])) {
714          $order_str = $this->get_sort_order();
715       }
716
717       /* return a search result */
718       if(isset($_SESSION['searchfor']) && $_SESSION['searchfor'] != '') {
719          $query_str = "
720             SELECT DISTINCT pt1.photo_id
721                FROM photo_tags pt1
722             INNER JOIN photo_tags pt2
723                ON pt1.photo_id=pt2.photo_id
724             INNER JOIN tags t1
725                ON pt1.tag_id=t1.id
726             INNER JOIN photos p
727                ON pt1.photo_id=p.id
728             INNER JOIN tags t2
729                ON pt2.tag_id=t2.id
730             WHERE t1.name LIKE '%". $_SESSION['searchfor'] ."%' ";
731
732          if(isset($additional_where_cond))
733             $query_str.= "AND ". $additional_where_cond ." ";
734
735          if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
736             $query_str.= "AND t2.name IN ('".implode("','",$this->cfg->show_tags)."')";
737          }
738          
739          if(isset($order_str))
740             $query_str.= $order_str;
741
742          $result = $this->db->db_query($query_str);
743          while($row = $this->db->db_fetch_object($result)) {
744             array_push($matched_photos, $row['photo_id']);
745          }
746          return $matched_photos;
747       }
748
749       /* return according the selected tags */
750       if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
751          $selected = "";
752          foreach($_SESSION['selected_tags'] as $tag)
753             $selected.= $tag .",";
754          $selected = substr($selected, 0, strlen($selected)-1);
755
756          /* photo has to match at least on of the selected tags */
757          if($_SESSION['tag_condition'] == 'or') {
758             $query_str = "
759                SELECT DISTINCT pt1.photo_id
760                   FROM photo_tags pt1
761                INNER JOIN photo_tags pt2
762                   ON pt1.photo_id=pt2.photo_id
763                INNER JOIN tags t
764                   ON pt2.tag_id=t.id
765                INNER JOIN photos p
766                   ON pt1.photo_id=p.id
767                WHERE pt1.tag_id IN (". $selected .")
768             ";
769             if(isset($additional_where_cond)) 
770                $query_str.= "AND ". $additional_where_cond ." ";
771
772             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
773                $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags)."')";
774             }
775
776             if(isset($order_str))
777                $query_str.= $order_str;
778          }
779          /* photo has to match all selected tags */
780          elseif($_SESSION['tag_condition'] == 'and') {
781
782             if(count($_SESSION['selected_tags']) >= 32) {
783                print "A SQLite limit of 32 tables within a JOIN SELECT avoids to<br />\n";
784                print "evaluate your tag selection. Please remove some tags from your selection.\n";
785                return Array();
786             } 
787
788             /* Join together a table looking like
789
790                pt1.photo_id pt1.tag_id pt2.photo_id pt2.tag_id ...
791
792                so the query can quickly return all images matching the
793                selected tags in an AND condition
794
795             */
796
797             $query_str = "
798                SELECT DISTINCT pt1.photo_id
799                   FROM photo_tags pt1
800             ";
801
802             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
803                $query_str.= "
804                   INNER JOIN tags t
805                      ON pt1.tag_id=t.id
806                ";
807             }
808
809             for($i = 0; $i < count($_SESSION['selected_tags']); $i++) {
810                $query_str.= "
811                   INNER JOIN photo_tags pt". ($i+2) ."
812                      ON pt1.photo_id=pt". ($i+2) .".photo_id
813                ";
814             }
815             $query_str.= "
816                INNER JOIN photos p
817                   ON pt1.photo_id=p.id
818             ";
819             $query_str.= "WHERE pt2.tag_id=". $_SESSION['selected_tags'][0]." ";
820             for($i = 1; $i < count($_SESSION['selected_tags']); $i++) {
821                $query_str.= "
822                   AND pt". ($i+2) .".tag_id=". $_SESSION['selected_tags'][$i] ."
823                "; 
824             }
825             if(isset($additional_where_cond)) 
826                $query_str.= "AND ". $additional_where_cond;
827
828             if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
829                $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
830             }
831
832             if(isset($order_str))
833                $query_str.= $order_str;
834
835          }
836
837          $result = $this->db->db_query($query_str);
838          while($row = $this->db->db_fetch_object($result)) {
839             array_push($matched_photos, $row['photo_id']);
840          }
841          return $matched_photos;
842       }
843
844       /* return all available photos */
845       $query_str = "
846          SELECT DISTINCT photo_id
847             FROM photo_tags pt
848          INNER JOIN photos p
849             ON p.id=pt.photo_id
850          INNER JOIN tags t
851             ON pt.tag_id=t.id
852       ";
853       if(isset($additional_where_cond)) 
854          $query_str.= "WHERE ". $additional_where_cond ." ";
855
856       if(isset($this->cfg->show_tags) && !empty($this->cfg->show_tags)) {
857          $query_str.= "AND t.name IN ('".implode("','",$this->cfg->show_tags). "')";
858       }
859  
860       if(isset($order_str))
861          $query_str.= $order_str;
862
863       $result = $this->db->db_query($query_str);
864       while($row = $this->db->db_fetch_object($result)) {
865          array_push($matched_photos, $row['photo_id']);
866       }
867       return $matched_photos;
868
869    } // getPhotoSelection()
870
871     /**
872     * control HTML ouput for photo index
873     *
874     * this function provides all the necessary information
875     * for the photo index template.
876     */
877    public function showPhotoIndex()
878    {
879       $photos = $this->getPhotoSelection();
880
881       $count = count($photos);
882
883       if(isset($_SESSION['begin_with']) && $_SESSION['begin_with'] != "")
884          $anchor = $_SESSION['begin_with'];
885
886       if(!isset($this->cfg->rows_per_page) || $this->cfg->rows_per_page == 0) {
887
888          $begin_with = 0;
889          $end_with = $count;
890
891       }
892       elseif($this->cfg->rows_per_page > 0) {
893
894          if(!isset($_SESSION['begin_with']) || $_SESSION['begin_with'] == 0) {
895             $begin_with = 0;
896          }
897          else {
898
899             $begin_with = $_SESSION['begin_with'];
900
901             // verify $begin_with - perhaps the thumbs-per-rows or
902             // rows-per-page variables have changed or the jump back
903             // from a photo wasn't exact - so calculate the real new
904             // starting point
905             $multiplicator = $this->cfg->rows_per_page * $this->cfg->thumbs_per_row;
906             for($i = 0; $i <= $count; $i+=$multiplicator) {
907                if($begin_with >= $i && $begin_with < $i+$multiplicator) {
908                   $begin_with = $i;
909                   break;
910                }
911             }
912          }
913
914          $end_with = $begin_with + ($this->cfg->rows_per_page * $this->cfg->thumbs_per_row);
915       }
916
917    
918       $rows = 0;
919       $cols = 0;
920       $images[$rows] = Array();
921       $img_height[$rows] = Array();
922       $img_width[$rows] = Array();
923       $img_id[$rows] = Array();
924       $img_name[$rows] = Array();
925       $img_title = Array();
926
927       for($i = $begin_with; $i < $end_with; $i++) {
928
929          if(isset($photos[$i])) {
930
931             $images[$rows][$cols] = $photos[$i];
932             $img_id[$rows][$cols] = $i;
933             $img_name[$rows][$cols] = htmlspecialchars($this->getPhotoName($photos[$i], 15));
934             $img_title[$rows][$cols] = "Click to view photo ". htmlspecialchars($this->getPhotoName($photos[$i], 0));
935
936             $thumb_path = $this->get_thumb_path($this->cfg->thumb_width, $photos[$i]);
937
938             if(file_exists($thumb_path)) {
939                $info = getimagesize($thumb_path); 
940                $img_width[$rows][$cols] = $info[0];
941                $img_height[$rows][$cols] = $info[1];
942             }
943
944             if($cols == $this->cfg->thumbs_per_row-1) {
945                $cols = 0;
946                $rows++;
947                $images[$rows] = Array();
948                $img_width[$rows] = Array();
949                $img_height[$rows] = Array();
950             }
951             else {
952                $cols++;
953             }
954          } 
955       }
956
957       // +1 for for smarty's selection iteration
958       $rows++;
959
960       if(isset($_SESSION['searchfor']) && $_SESSION['searchfor'] != '')
961          $this->tmpl->assign('searchfor', $_SESSION['searchfor']);
962
963       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
964          $this->tmpl->assign('from_date', $this->ts2str($_SESSION['from_date']));
965          $this->tmpl->assign('to_date', $this->ts2str($_SESSION['to_date']));
966       }
967
968       if(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
969          $this->tmpl->assign('tag_result', 1);
970       }
971
972       /* do we have to display the page selector ? */
973       if($this->cfg->rows_per_page != 0) {
974
975          $page_select = "";
976       
977          /* calculate the page switchers */
978          $previous_start = $begin_with - ($this->cfg->rows_per_page * $this->cfg->thumbs_per_row);
979          $next_start = $begin_with + ($this->cfg->rows_per_page * $this->cfg->thumbs_per_row);
980
981          if($begin_with != 0) 
982             $this->tmpl->assign("previous_url", "javascript:showPhotoIndex(". $previous_start .");"); 
983          if($end_with < $count)
984             $this->tmpl->assign("next_url", "javascript:showPhotoIndex(". $next_start .");"); 
985
986          $photo_per_page  = $this->cfg->rows_per_page * $this->cfg->thumbs_per_row;
987          $last_page = ceil($count / $photo_per_page);
988
989          /* get the current selected page */
990          if($begin_with == 0) {
991             $current_page = 1;
992          } else {
993             $current_page = 0;
994             for($i = $begin_with; $i >= 0; $i-=$photo_per_page) {
995                $current_page++;
996             }
997          } 
998
999          $dotdot_made = 0;
1000
1001          for($i = 1; $i <= $last_page; $i++) {
1002
1003             if($current_page == $i)
1004                $style = "style=\"font-size: 125%; text-decoration: underline;\"";
1005             elseif($current_page-1 == $i || $current_page+1 == $i)
1006                $style = "style=\"font-size: 105%;\"";
1007             elseif(($current_page-5 >= $i) && ($i != 1) ||
1008                ($current_page+5 <= $i) && ($i != $last_page))
1009                $style = "style=\"font-size: 75%;\"";
1010             else
1011                $style = "";
1012
1013             $select = "<a href=\"javascript:showPhotoIndex(". (($i*$photo_per_page)-$photo_per_page) .");\"";
1014                if($style != "")
1015                   $select.= $style;
1016             $select.= ">". $i ."</a>&nbsp;";
1017
1018             // until 9 pages we show the selector from 1-9
1019             if($last_page <= 9) {
1020                $page_select.= $select;
1021                continue;
1022             } else {
1023                if($i == 1 /* first page */ || 
1024                   $i == $last_page /* last page */ ||
1025                   $i == $current_page /* current page */ ||
1026                   $i == ceil($last_page * 0.25) /* first quater */ ||
1027                   $i == ceil($last_page * 0.5) /* half */ ||
1028                   $i == ceil($last_page * 0.75) /* third quater */ ||
1029                   (in_array($i, array(1,2,3,4,5,6)) && $current_page <= 4) /* the first 6 */ ||
1030                   (in_array($i, array($last_page, $last_page-1, $last_page-2, $last_page-3, $last_page-4, $last_page-5)) && $current_page >= $last_page-4) /* the last 6 */ ||
1031                   $i == $current_page-3 || $i == $current_page-2 || $i == $current_page-1 /* three before */ ||
1032                   $i == $current_page+3 || $i == $current_page+2 || $i == $current_page+1 /* three after */) {
1033
1034                   $page_select.= $select;
1035                   $dotdot_made = 0;
1036                   continue;
1037
1038                }
1039             }
1040
1041             if(!$dotdot_made) {
1042                $page_select.= ".........&nbsp;";
1043                $dotdot_made = 1;
1044             }
1045          }
1046
1047          /* only show the page selector if we have more then one page */
1048          if($last_page > 1)
1049             $this->tmpl->assign('page_selector', $page_select);
1050       }
1051
1052       
1053       $current_tags = $this->getCurrentTags();
1054       $extern_link = "index.php?mode=showpi";
1055       $rss_link = "index.php?mode=rss";
1056       if($current_tags != "") {
1057          $extern_link.= "&tags=". $current_tags;
1058          $rss_link.= "&tags=". $current_tags;
1059       }
1060       if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1061          $extern_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
1062          $rss_link.= "&from_date=". $this->ts2str($_SESSION['from_date']) ."&to_date=". $this->ts2str($_SESSION['to_date']);
1063       }
1064
1065       $export_link = "index.php?mode=export";
1066       $slideshow_link = "index.php?mode=slideshow";
1067
1068       $this->tmpl->assign('extern_link', $extern_link);
1069       $this->tmpl->assign('slideshow_link', $slideshow_link);
1070       $this->tmpl->assign('export_link', $export_link);
1071       $this->tmpl->assign('rss_link', $rss_link);
1072       $this->tmpl->assign('count', $count);
1073       $this->tmpl->assign('width', $this->cfg->thumb_width);
1074       $this->tmpl->assign('images', $images);
1075       $this->tmpl->assign('img_width', $img_width);
1076       $this->tmpl->assign('img_height', $img_height);
1077       $this->tmpl->assign('img_id', $img_id);
1078       $this->tmpl->assign('img_name', $img_name);
1079       $this->tmpl->assign('img_title', $img_title);
1080       $this->tmpl->assign('rows', $rows);
1081       $this->tmpl->assign('columns', $this->cfg->thumbs_per_row);
1082
1083       $this->tmpl->show("photo_index.tpl");
1084
1085       if(isset($anchor))
1086          print "<script language=\"JavaScript\">self.location.hash = '#image". $anchor ."';</script>\n";
1087
1088    } // showPhotoIndex()
1089
1090    /**
1091     * show credit template
1092     */
1093    public function showCredits()
1094    {
1095       $this->tmpl->assign('version', $this->cfg->version);
1096       $this->tmpl->assign('product', $this->cfg->product);
1097       $this->tmpl->assign('db_version', $this->dbver);
1098       $this->tmpl->show("credits.tpl");
1099
1100    } // showCredits()
1101
1102    /**
1103     * create_thumbnails for the requested width
1104     *
1105     * this function creates image thumbnails of $orig_image
1106     * stored as $thumb_image. It will check if the image is
1107     * in a supported format, if necessary rotate the image
1108     * (based on EXIF orientation meta headers) and re-sizing.
1109     */
1110    public function create_thumbnail($orig_image, $thumb_image, $width)
1111    {  
1112       if(!file_exists($orig_image)) {
1113          return false;
1114       }
1115
1116       $details = getimagesize($orig_image);
1117       
1118       /* check if original photo is a support image type */
1119       if(!$this->checkifImageSupported($details['mime']))
1120          return false;
1121
1122       $meta = $this->get_meta_informations($orig_image);
1123
1124       $rotate = 0;
1125       $flip = false;
1126
1127       switch($meta['Orientation']) {
1128
1129          case 1: /* top, left */
1130             $rotate = 0; $flip = false; break;
1131          case 2: /* top, right */
1132             $rotate = 0; $flip = true; break;
1133          case 3: /* bottom, left */
1134             $rotate = 180; $flip = false; break;
1135          case 4: /* bottom, right */
1136             $rotate = 180; $flip = true; break;
1137          case 5: /* left side, top */
1138             $rotate = 90; $flip = true; break;
1139          case 6: /* right side, top */
1140             $rotate = 90; $flip = false; break;
1141          case 7: /* left side, bottom */
1142             $rotate = 270; $flip = true; break;
1143          case 8: /* right side, bottom */
1144             $rotate = 270; $flip = false; break;
1145       }
1146
1147       $src_img = @imagecreatefromjpeg($orig_image);
1148
1149       if(!$src_img) {
1150          print "Can't load image from ". $orig_image ."\n";
1151          return false;
1152       }
1153
1154       /* grabs the height and width */
1155       $cur_width = imagesx($src_img);
1156       $cur_height = imagesy($src_img);
1157
1158       // If requested width is more then the actual image width,
1159       // do not generate a thumbnail, instead safe the original
1160       // as thumbnail but with lower quality
1161
1162       if($width >= $cur_width) {
1163          $result = imagejpeg($src_img, $thumb_image, 75);
1164          imagedestroy($src_img);
1165          return true;
1166       }
1167
1168       // If the image will be rotate because EXIF orientation said so
1169       // 'virtually rotate' the image for further calculations
1170       if($rotate == 90 || $rotate == 270) {
1171          $tmp = $cur_width;
1172          $cur_width = $cur_height;
1173          $cur_height = $tmp;
1174       }
1175
1176       /* calculates aspect ratio */
1177       $aspect_ratio = $cur_height / $cur_width;
1178
1179       /* sets new size */
1180       if($aspect_ratio < 1) {
1181          $new_w = $width;
1182          $new_h = abs($new_w * $aspect_ratio);
1183       } else {
1184          /* 'virtually' rotate the image and calculate it's ratio */
1185          $tmp_w = $cur_height;
1186          $tmp_h = $cur_width;
1187          /* now get the ratio from the 'rotated' image */
1188          $tmp_ratio = $tmp_h/$tmp_w;
1189          /* now calculate the new dimensions */
1190          $tmp_w = $width;
1191          $tmp_h = abs($tmp_w * $tmp_ratio);
1192
1193          // now that we know, how high they photo should be, if it
1194          // gets rotated, use this high to scale the image
1195          $new_h = $tmp_h;
1196          $new_w = abs($new_h / $aspect_ratio);
1197
1198          // If the image will be rotate because EXIF orientation said so
1199          // now 'virtually rotate' back the image for the image manipulation
1200          if($rotate == 90 || $rotate == 270) {
1201             $tmp = $new_w;
1202             $new_w = $new_h;
1203             $new_h = $tmp;
1204          }
1205       }
1206
1207       /* creates new image of that size */
1208       $dst_img = imagecreatetruecolor($new_w, $new_h);
1209
1210       imagefill($dst_img, 0, 0, ImageColorAllocate($dst_img, 255, 255, 255));
1211
1212       /* copies resized portion of original image into new image */
1213       imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $new_w, $new_h, imagesx($src_img), imagesy($src_img));
1214
1215       /* needs the image to be flipped horizontal? */
1216       if($flip) {
1217          print "(FLIP)";
1218          $image = $dst_img;
1219          for($x = 0; $x < $new_w; $x++) {
1220             imagecopy($dst_img, $image, $x, 0, $w - $x - 1, 0, 1, $h);
1221          }
1222       }
1223
1224       if($rotate) {
1225          $this->_debug("(ROTATE)");
1226          $dst_img = $this->rotateImage($dst_img, $rotate);
1227       }
1228
1229       /* write down new generated file */
1230       $result = imagejpeg($dst_img, $thumb_image, 75);
1231
1232       /* free your mind */
1233       imagedestroy($dst_img);
1234       imagedestroy($src_img);
1235
1236       if($result === false) {
1237          print "Can't write thumbnail ". $thumb_image ."\n";
1238          return false;
1239       }
1240
1241       return true;
1242
1243    } // create_thumbnail()
1244
1245    /**
1246     * return all exif meta data from the file
1247     */
1248    public function get_meta_informations($file)
1249    {
1250       return exif_read_data($file);
1251
1252    } // get_meta_informations()
1253
1254    /**
1255     * create phpfspot own sqlite database
1256     *
1257     * this function creates phpfspots own sqlite database
1258     * if it does not exist yet. this own is used to store
1259     * some necessary informations (md5 sum's, ...).
1260     */
1261    public function check_config_table()
1262    {
1263       // if the config table doesn't exist yet, create it
1264       if(!$this->cfg_db->db_check_table_exists("images")) {
1265          $this->cfg_db->db_exec("
1266             CREATE TABLE images (
1267                img_idx int primary key,
1268                img_md5 varchar(32)
1269             )
1270             ");
1271       }
1272
1273    } // check_config_table
1274
1275    /**
1276     * Generates a thumbnail from photo idx
1277     *
1278     * This function will generate JPEG thumbnails from provided F-Spot photo
1279     * indizes.
1280     *
1281     * 1. Check if all thumbnail generations (width) are already in place and
1282     *    readable
1283     * 2. Check if the md5sum of the original file has changed
1284     * 3. Generate the thumbnails if needed
1285     */
1286    public function gen_thumb($idx = 0, $force = 0)
1287    {
1288       $error = 0;
1289
1290       $resolutions = Array(
1291          $this->cfg->thumb_width,
1292          $this->cfg->photo_width,
1293          $this->cfg->mini_width,
1294       );
1295
1296       /* get details from F-Spot's database */
1297       $details = $this->get_photo_details($idx);
1298
1299       /* calculate file MD5 sum */
1300       $full_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
1301
1302       if(!file_exists($full_path)) {
1303          $this->_error("File ". $full_path ." does not exist\n");
1304          return;
1305       }
1306
1307       if(!is_readable($full_path)) {
1308          $this->_error("File ". $full_path ." is not readable for ". $this->getuid() ."\n");
1309          return;
1310       }
1311
1312       $file_md5 = md5_file($full_path);
1313
1314       $this->_debug("Image [". $idx ."] ". $this->shrink_text($this->parse_uri($details['uri'], 'filename'), 20) ." Thumbnails:");
1315
1316       $changes = false;
1317
1318       foreach($resolutions as $resolution) {
1319
1320          $thumb_sub_path = substr($file_md5, 0, 2);
1321          $thumb_path = $this->cfg->thumb_path ."/". $thumb_sub_path ."/". $resolution ."_". $file_md5;
1322
1323          if(!file_exists(dirname($thumb_path))) {
1324             mkdir(dirname($thumb_path), 0755);
1325          }
1326
1327          /* if the thumbnail file doesn't exist, create it */
1328          if(!file_exists($thumb_path)) {
1329
1330             $this->_debug(" ". $resolution ."px");
1331             if(!$this->create_thumbnail($full_path, $thumb_path, $resolution))
1332                $error = 1;
1333
1334             $changes = true;
1335          }
1336          /* if the file hasn't changed there is no need to regen the thumb */
1337          elseif($file_md5 != $this->getMD5($idx) || $force) {
1338
1339             $this->_debug(" ". $resolution ."px");
1340             if(!$this->create_thumbnail($full_path, $thumb_path, $resolution))
1341                $error = 1;
1342
1343             $changes = true;
1344          }
1345       }
1346
1347       if(!$changes) {
1348          $this->_debug(" already exist");
1349       }
1350
1351       /* set the new/changed MD5 sum for the current photo */
1352       if(!$error) {
1353          $this->setMD5($idx, $file_md5);
1354       }
1355
1356       $this->_debug("\n");
1357
1358    } // gen_thumb()
1359
1360    /**
1361     * returns stored md5 sum for a specific photo
1362     *
1363     * this function queries the phpfspot database for a
1364     * stored MD5 checksum of the specified photo
1365     */
1366    public function getMD5($idx)
1367    {
1368       $result = $this->cfg_db->db_query("
1369          SELECT img_md5 
1370          FROM images
1371          WHERE img_idx='". $idx ."'
1372       ");
1373
1374       if(!$result)
1375          return 0;
1376
1377       $img = $this->cfg_db->db_fetch_object($result);
1378       return $img['img_md5'];
1379       
1380    } // getMD5()
1381
1382    /**
1383     * set MD5 sum for the specific photo
1384     */
1385    private function setMD5($idx, $md5)
1386    {
1387       $result = $this->cfg_db->db_exec("
1388          REPLACE INTO images (img_idx, img_md5)
1389          VALUES ('". $idx ."', '". $md5 ."')
1390       ");
1391
1392    } // setMD5()
1393
1394    /**
1395     * store current tag condition
1396     *
1397     * this function stores the current tag condition
1398     * (AND or OR) in the users session variables
1399     */
1400    public function setTagCondition($mode)
1401    {
1402       $_SESSION['tag_condition'] = $mode;
1403
1404    } // setTagCondition()
1405
1406    /** 
1407     * invoke tag & date search 
1408     *
1409     * this function will return all matching tags and store
1410     * them in the session variable selected_tags. furthermore
1411     * it also handles the date search.
1412     * getPhotoSelection() will then only return the matching
1413     * photos.
1414     */
1415    public function startSearch($searchfor, $sort_order, $from = 0, $to = 0)
1416    {
1417       $this->get_tags();
1418
1419       $_SESSION['searchfor'] = $searchfor;
1420       $_SESSION['sort_order'] = $sort_order;
1421       if($from != 0)
1422          $_SESSION['from_date'] = strtotime($from);
1423       else
1424          unset($_SESSION['from_date']);
1425       if($to != 0)
1426          $_SESSION['to_date'] = strtotime($to);
1427       else
1428          unset($_SESSION['to_date']);
1429
1430       if($searchfor != "") {
1431          /* new search, reset the current selected tags */
1432          $_SESSION['selected_tags'] = Array();
1433          foreach($this->avail_tags as $tag) {
1434             if(preg_match('/'. $searchfor .'/i', $this->tags[$tag]))
1435                array_push($_SESSION['selected_tags'], $tag);
1436          }
1437       }
1438
1439    } // startSearch()
1440
1441    /**
1442     * rotate image
1443     *
1444     * this function rotates the image according the
1445     * specified angel.
1446     */
1447    private function rotateImage($img, $degrees)
1448    {
1449       if(function_exists("imagerotate")) {
1450          $img = imagerotate($img, $degrees, 0);
1451       } else {
1452          function imagerotate($src_img, $angle)
1453          {
1454             $src_x = imagesx($src_img);
1455             $src_y = imagesy($src_img);
1456             if ($angle == 180) {
1457                $dest_x = $src_x;
1458                $dest_y = $src_y;
1459             }
1460             elseif ($src_x <= $src_y) {
1461                $dest_x = $src_y;
1462                $dest_y = $src_x;
1463             }
1464             elseif ($src_x >= $src_y) {
1465                $dest_x = $src_y;
1466                $dest_y = $src_x;
1467             }
1468                
1469             $rotate=imagecreatetruecolor($dest_x,$dest_y);
1470             imagealphablending($rotate, false);
1471                
1472             switch ($angle) {
1473             
1474                case 90:
1475                   for ($y = 0; $y < ($src_y); $y++) {
1476                      for ($x = 0; $x < ($src_x); $x++) {
1477                         $color = imagecolorat($src_img, $x, $y);
1478                         imagesetpixel($rotate, $dest_x - $y - 1, $x, $color);
1479                      }
1480                   }
1481                   break;
1482
1483                case 270:
1484                   for ($y = 0; $y < ($src_y); $y++) {
1485                      for ($x = 0; $x < ($src_x); $x++) {
1486                         $color = imagecolorat($src_img, $x, $y);
1487                         imagesetpixel($rotate, $y, $dest_y - $x - 1, $color);
1488                      }
1489                   }
1490                   break;
1491
1492                case 180:
1493                   for ($y = 0; $y < ($src_y); $y++) {
1494                      for ($x = 0; $x < ($src_x); $x++) {
1495                         $color = imagecolorat($src_img, $x, $y);
1496                         imagesetpixel($rotate, $dest_x - $x - 1, $dest_y - $y - 1, $color);
1497                      }
1498                   }
1499                   break;
1500
1501                default:
1502                   $rotate = $src_img;
1503                   break;
1504             };
1505
1506             return $rotate;
1507
1508          }
1509
1510          $img = imagerotate($img, $degrees);
1511
1512       }
1513
1514       return $img;
1515
1516    } // rotateImage()
1517
1518    /**
1519     * return all assigned tags for the specified photo
1520     */
1521    private function get_photo_tags($idx)
1522    {
1523       $result = $this->db->db_query("
1524          SELECT t.id, t.name
1525          FROM tags t
1526          INNER JOIN photo_tags pt
1527             ON t.id=pt.tag_id
1528          WHERE pt.photo_id='". $idx ."'
1529       ");
1530
1531       $tags = Array();
1532
1533       while($row = $this->db->db_fetch_object($result))
1534          $tags[$row['id']] = $row['name'];
1535
1536       return $tags;
1537
1538    } // get_photo_tags()
1539
1540    /**
1541     * create on-the-fly images with text within
1542     */
1543    public function showTextImage($txt, $color=000000, $space=4, $font=4, $w=300)
1544    {
1545       if (strlen($color) != 6) 
1546          $color = 000000;
1547
1548       $int = hexdec($color);
1549       $h = imagefontheight($font);
1550       $fw = imagefontwidth($font);
1551       $txt = explode("\n", wordwrap($txt, ($w / $fw), "\n"));
1552       $lines = count($txt);
1553       $im = imagecreate($w, (($h * $lines) + ($lines * $space)));
1554       $bg = imagecolorallocate($im, 255, 255, 255);
1555       $color = imagecolorallocate($im, 0xFF & ($int >> 0x10), 0xFF & ($int >> 0x8), 0xFF & $int);
1556       $y = 0;
1557
1558       foreach ($txt as $text) {
1559          $x = (($w - ($fw * strlen($text))) / 2);
1560          imagestring($im, $font, $x, $y, $text, $color);
1561          $y += ($h + $space);
1562       }
1563
1564       Header("Content-type: image/png");
1565       ImagePng($im);
1566
1567    } // showTextImage()
1568
1569    /**
1570     * check if all requirements are met
1571     */
1572    private function checkRequirements()
1573    {
1574       if(!function_exists("imagecreatefromjpeg")) {
1575          print "PHP GD library extension is missing<br />\n";
1576          $missing = true;
1577       }
1578
1579       if($this->cfg->db_access == "native" && !function_exists("sqlite3_open")) {
1580          print "PHP SQLite3 library extension is missing<br />\n";
1581          $missing = true;
1582       }
1583
1584       /* Check for HTML_AJAX PEAR package, lent from Horde project */
1585       ini_set('track_errors', 1);
1586       @include_once 'HTML/AJAX/Server.php';
1587       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
1588          print "PEAR HTML_AJAX package is missing<br />\n";
1589          $missing = true;
1590       }
1591       @include_once 'Calendar/Calendar.php';
1592       if(isset($php_errormsg) && preg_match('/Failed opening.*for inclusion/i', $php_errormsg)) {
1593          print "PEAR Calendar package is missing<br />\n";
1594          $missing = true;
1595       }
1596       ini_restore('track_errors');
1597
1598       if(isset($missing))
1599          return false;
1600
1601       return true;
1602
1603    } // checkRequirements()
1604
1605    private function _debug($text)
1606    {
1607       if($this->fromcmd) {
1608          print $text;
1609       }
1610
1611    } // _debug()
1612
1613    /**
1614     * check if specified MIME type is supported
1615     */
1616    public function checkifImageSupported($mime)
1617    {
1618       if(in_array($mime, Array("image/jpeg")))
1619          return true;
1620
1621       return false;
1622
1623    } // checkifImageSupported()
1624
1625    public function _error($text)
1626    {
1627       switch($this->cfg->logging) {
1628          case 'display':
1629             print "<img src=\"resources/green_info.png\" alt=\"warning\" />\n";
1630             print $text;
1631             break;
1632          case 'errorlog':  
1633             error_log($text);
1634             break;
1635          case 'logfile':
1636             error_log($text, 3, $his->cfg->log_file);
1637             break;
1638       }
1639
1640    } // _error()
1641
1642    /**
1643     * output calendard input fields
1644     */
1645    private function get_calendar($mode)
1646    {
1647       $year = isset($_SESSION[$mode .'_date']) ? date("Y", $_SESSION[$mode .'_date']) : date("Y");
1648       $month = isset($_SESSION[$mode .'_date']) ? date("m", $_SESSION[$mode .'_date']) : date("m");
1649       $day = isset($_SESSION[$mode .'_date']) ? date("d", $_SESSION[$mode .'_date']) : date("d");
1650
1651       $output = "<input type=\"text\" size=\"3\" id=\"". $mode ."year\" value=\"". $year ."\"";
1652       if(!isset($_SESSION[$mode .'_date']))
1653          $output.= " disabled=\"disabled\"";
1654       $output.= " />\n";
1655       $output.= "<input type=\"text\" size=\"1\" id=\"". $mode ."month\" value=\"". $month ."\"";
1656       if(!isset($_SESSION[$mode .'_date']))
1657          $output.= " disabled=\"disabled\"";
1658       $output.= " />\n";
1659       $output.= "<input type=\"text\" size=\"1\" id=\"". $mode ."day\" value=\"". $day ."\"";
1660       if(!isset($_SESSION[$mode .'_date']))
1661          $output.= " disabled=\"disabled\"";
1662       $output.= " />\n";
1663
1664       return $output;
1665
1666    } // get_calendar()
1667
1668    /**
1669     * output calendar matrix
1670     */
1671    public function get_calendar_matrix($year = 0, $month = 0, $day = 0)
1672    {
1673       if (!isset($year)) $year = date('Y');
1674       if (!isset($month)) $month = date('m');
1675       if (!isset($day)) $day = date('d');
1676       $rows = 1;
1677       $cols = 1;
1678       $matrix = Array();
1679
1680       require_once CALENDAR_ROOT.'Month/Weekdays.php';
1681       require_once CALENDAR_ROOT.'Day.php';
1682
1683       // Build the month
1684       $month = new Calendar_Month_Weekdays($year,$month);
1685
1686       // Create links
1687       $prevStamp = $month->prevMonth(true);
1688       $prev = "javascript:setMonth(". date('Y',$prevStamp) .", ". date('n',$prevStamp) .", ". date('j',$prevStamp) .");";
1689       $nextStamp = $month->nextMonth(true);
1690       $next = "javascript:setMonth(". date('Y',$nextStamp) .", ". date('n',$nextStamp) .", ". date('j',$nextStamp) .");";
1691
1692       $selectedDays = array (
1693          new Calendar_Day($year,$month,$day),
1694          new Calendar_Day($year,12,25),
1695       );
1696
1697       // Build the days in the month
1698       $month->build($selectedDays);
1699
1700       $this->tmpl->assign('current_month', date('F Y',$month->getTimeStamp()));
1701       $this->tmpl->assign('prev_month', $prev);
1702       $this->tmpl->assign('next_month', $next);
1703
1704       while ( $day = $month->fetch() ) {
1705    
1706          if(!isset($matrix[$rows]))
1707             $matrix[$rows] = Array();
1708
1709          $string = "";
1710
1711          $dayStamp = $day->thisDay(true);
1712          $link = "javascript:setCalendarDate(". date('Y',$dayStamp) .", ". date('n',$dayStamp).", ". date('j',$dayStamp) .");";
1713
1714          // isFirst() to find start of week
1715          if ( $day->isFirst() )
1716             $string.= "<tr>\n";
1717
1718          if ( $day->isSelected() ) {
1719             $string.= "<td class=\"selected\">".$day->thisDay()."</td>\n";
1720          } else if ( $day->isEmpty() ) {
1721             $string.= "<td>&nbsp;</td>\n";
1722          } else {
1723             $string.= "<td><a class=\"calendar\" href=\"".$link."\">".$day->thisDay()."</a></td>\n";
1724          }
1725
1726          // isLast() to find end of week
1727          if ( $day->isLast() )
1728             $string.= "</tr>\n";
1729
1730          $matrix[$rows][$cols] = $string;
1731
1732          $cols++;
1733
1734          if($cols > 7) {
1735             $cols = 1;
1736             $rows++;
1737          }
1738       }
1739
1740       $this->tmpl->assign('matrix', $matrix);
1741       $this->tmpl->assign('rows', $rows);
1742       $this->tmpl->show("calendar.tpl");
1743
1744    } // get_calendar_matrix()
1745
1746    /**
1747     * output export page
1748     */
1749    public function getExport($mode)
1750    {
1751       $pictures = $this->getPhotoSelection();
1752       $current_tags = $this->getCurrentTags();  
1753
1754       foreach($pictures as $picture) {
1755
1756          $orig_url = $this->get_phpfspot_url() ."index.php?mode=showp&id=". $picture;
1757          if($current_tags != "") {
1758             $orig_url.= "&tags=". $current_tags;
1759          } 
1760          if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1761             $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
1762          }
1763
1764          $thumb_url = $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
1765
1766          switch($mode) {
1767
1768             case 'HTML':
1769                // <a href="%pictureurl%"><img src="%thumbnailurl%" ></a>
1770                print htmlspecialchars("<a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>") ."<br />\n";
1771                break;
1772                
1773             case 'MoinMoin':
1774                // "[%pictureurl% %thumbnailurl%]"
1775                print htmlspecialchars("[".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
1776                break;
1777
1778             case 'MoinMoinList':
1779                // " * [%pictureurl% %thumbnailurl%]"
1780                print "&nbsp;" . htmlspecialchars("* [".$orig_url." ".$thumb_url."&fake=1.jpg]") ."<br />\n";
1781                break;
1782          }
1783
1784       }
1785
1786    } // getExport()
1787
1788    /**
1789     * output RSS feed
1790     */
1791    public function getRSSFeed()
1792    {
1793       Header("Content-type: text/xml; charset=utf-8");
1794       print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1795 ?>
1796 <rss version="2.0"
1797    xmlns:media="http://search.yahoo.com/mrss/"
1798    xmlns:dc="http://purl.org/dc/elements/1.1/"
1799  >
1800  <channel>
1801   <title>phpfspot</title>
1802   <description>phpfspot RSS feed</description>
1803   <link><?php print htmlspecialchars($this->get_phpfspot_url()); ?></link>
1804   <pubDate><?php print strftime("%a, %d %b %Y %T %z"); ?></pubDate>
1805   <generator>phpfspot</generator>
1806 <?php
1807
1808       $pictures = $this->getPhotoSelection();
1809       $current_tags = $this->getCurrentTags();  
1810
1811       foreach($pictures as $picture) {
1812
1813          $orig_url = $this->get_phpfspot_url() ."index.php?mode=showp&id=". $picture;
1814          if($current_tags != "") {
1815             $orig_url.= "&tags=". $current_tags;
1816          } 
1817          if(isset($_SESSION['from_date']) && isset($_SESSION['to_date'])) {
1818             $orig_url.= "&from_date=". $_SESSION['from_date'] ."&to_date=". $_SESSION['to_date'];
1819          }
1820
1821          $details = $this->get_photo_details($picture);
1822
1823          $thumb_url = $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $picture ."&width=". $this->cfg->thumb_width;
1824          $thumb_html = htmlspecialchars("
1825 <a href=\"". $orig_url ."\"><img src=\"". $thumb_url ."\" /></a>
1826 <br>
1827 ". $details['description']);
1828
1829          $orig_path = $this->translate_path($this->parse_uri($details['uri'], 'fullpath'));
1830          $meta = $this->get_meta_informations($orig_path);
1831          $meta_date = isset($meta['FileDateTime']) ? $meta['FileDateTime'] : filemtime($orig_path);
1832
1833 ?>
1834   <item>
1835    <title><?php print htmlspecialchars($this->parse_uri($details['uri'], 'filename')); ?></title>
1836    <link><?php print htmlspecialchars($orig_url); ?></link>
1837    <guid><?php print htmlspecialchars($orig_url); ?></guid>
1838    <dc:date.Taken><?php print strftime("%Y-%m-%dT%H:%M:%S+00:00", $meta_date); ?></dc:date.Taken>
1839    <description>
1840     <?php print $thumb_html; ?> 
1841    </description>
1842    <pubDate><?php print strftime("%a, %d %b %Y %T %z", $meta_date); ?></pubDate>
1843   </item>
1844 <?php
1845
1846       }
1847 ?>
1848  </channel>
1849 </rss>
1850 <?php
1851
1852
1853    } // getExport()
1854
1855  
1856    /**
1857     * return all selected tags as one string
1858     */
1859    private function getCurrentTags()
1860    {
1861       $current_tags = "";
1862       if(isset($_SESSION['selected_tags']) && $_SESSION['selected_tags'] != "") {
1863          foreach($_SESSION['selected_tags'] as $tag)
1864             $current_tags.= $tag .",";
1865          $current_tags = substr($current_tags, 0, strlen($current_tags)-1);
1866       }
1867       return $current_tags;
1868
1869    } // getCurrentTags()
1870
1871    /**
1872     * return the current photo
1873     */
1874    public function getCurrentPhoto()
1875    {
1876       if(isset($_SESSION['current_photo'])) {
1877          print $_SESSION['current_photo'];
1878       }
1879    } // getCurrentPhoto()
1880
1881    /**
1882     * tells the client browser what to do
1883     *
1884     * this function is getting called via AJAX by the
1885     * client browsers. it will tell them what they have
1886     * to do next. This is necessary for directly jumping
1887     * into photo index or single photo view when the are
1888     * requested with specific URLs
1889     */
1890    public function whatToDo()
1891    {
1892       if(isset($_SESSION['current_photo']) && $_SESSION['start_action'] == 'showp') {
1893          return "show_photo";
1894       }
1895       elseif(isset($_SESSION['selected_tags']) && !empty($_SESSION['selected_tags'])) {
1896          return "showpi_tags";
1897       }
1898       elseif(isset($_SESSION['start_action']) && $_SESSION['start_action'] == 'showpi') {
1899          return "showpi";
1900       }
1901
1902       return "nothing special";
1903
1904    } // whatToDo()
1905
1906    /**
1907     * return the current process-user
1908     */
1909    private function getuid()
1910    {
1911       if($uid = posix_getuid()) {
1912          if($user = posix_getpwuid($uid)) {
1913             return $user['name'];
1914          }
1915       }
1916    
1917       return 'n/a';
1918    
1919    } // getuid()
1920
1921    /**
1922     * returns a select-dropdown box to select photo index sort parameters
1923     */
1924    private function get_sort_field()
1925    {
1926       $output = "<select name=\"sort_order\">";
1927       foreach(array('date_asc', 'date_desc', 'name_asc', 'name_desc') as $sort_order) {
1928          $output.= "<option value=\"". $sort_order ."\"";
1929          if($sort_order == $_SESSION['sort_order']) {
1930             $output.= " selected=\"selected\"";
1931          }
1932          $output.= ">". $sort_order ."</option>";
1933       }
1934       $output.= "</select>";
1935       return $output;
1936
1937    } // get_sort_field()
1938
1939    /**
1940     * returns the currently selected sort order
1941     */ 
1942    private function get_sort_order()
1943    {
1944       switch($_SESSION['sort_order']) {
1945          case 'date_asc':
1946             return " ORDER BY p.time ASC";
1947             break;
1948          case 'date_desc':
1949             return " ORDER BY p.time DESC";
1950             break;
1951          case 'name_asc':
1952             return " ORDER BY p.name ASC";
1953             break;
1954          case 'name_desc':
1955             return " ORDER BY p.name DESC";
1956             break;
1957       }
1958
1959    } // get_sort_order()
1960
1961    /***
1962      * return the next to be shown slide show image
1963      *
1964      * this function returns the URL of the next image
1965      * in the slideshow sequence.
1966      */
1967    public function getNextSlideShowImage()
1968    {
1969       $all_photos = $this->getPhotoSelection();
1970
1971       if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == count($all_photos)-1) 
1972          $_SESSION['slideshow_img'] = 0;
1973       else
1974          $_SESSION['slideshow_img']++;
1975
1976       return $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;
1977
1978    } // getNextSlideShowImage()
1979
1980    /***
1981      * return the previous to be shown slide show image
1982      *
1983      * this function returns the URL of the previous image
1984      * in the slideshow sequence.
1985      */
1986    public function getPrevSlideShowImage()
1987    {
1988       $all_photos = $this->getPhotoSelection();
1989
1990       if(!isset($_SESSION['slideshow_img']) || $_SESSION['slideshow_img'] == 0)
1991          $_SESSION['slideshow_img'] = 0;
1992       else
1993          $_SESSION['slideshow_img']--;
1994
1995       return $this->get_phpfspot_url() ."phpfspot_img.php?idx=". $all_photos[$_SESSION['slideshow_img']] ."&width=". $this->cfg->photo_width;
1996
1997    } // getPrevSlideShowImage()
1998
1999    public function resetSlideShow()
2000    {
2001       if(isset($_SESSION['slideshow_img']))
2002          unset($_SESSION['slideshow_img']);
2003    } // resetSlideShow()
2004    
2005    /***
2006      * get random photo
2007      *
2008      * this function will get all photos from the fspot
2009      * database and randomly return ONE entry
2010      *
2011      * saddly there is yet no sqlite3 function which returns
2012      * the bulk result in array, so we have to fill up our
2013      * own here.
2014      */ 
2015    public function get_random_photo()
2016    {
2017       $all = Array();
2018
2019       $result = $this->db->db_query("
2020          SELECT id
2021          FROM photos
2022       ");
2023       
2024       while($row = $this->db->db_fetch_object($result)) {
2025          array_push($all, $row['id']);
2026       }
2027
2028       return $all[array_rand($all)];
2029
2030    } // get_random_photo()
2031
2032    /**
2033     * validates provided date
2034     *
2035     * this function validates if the provided date
2036     * contains a valid date and will return true 
2037     * if it is.
2038     */
2039    public function isValidDate($date_str)
2040    {
2041       $timestamp = strtotime($date_str);
2042    
2043       if(is_numeric($timestamp))
2044          return true;
2045       
2046       return false;
2047
2048    } // isValidDate()
2049
2050    /**
2051     * timestamp to string conversion
2052     */
2053    private function ts2str($timestamp)
2054    {
2055       return strftime("%Y-%m-%d", $timestamp);
2056    } // ts2str()
2057
2058    private function extractTags($tags_str)
2059    {
2060       $not_validated = split(',', $_GET['tags']);
2061       $validated = array();
2062
2063       foreach($not_validated as $tag) {
2064          if(is_numeric($tag))
2065             array_push($validated, $tag);
2066       }
2067    
2068       return $validated;
2069    
2070    } // extractTags()
2071
2072    /**
2073     * returns the full path to a thumbnail
2074     */
2075    public function get_thumb_path($width, $photo)
2076    {
2077       $md5 = $this->getMD5($photo);
2078       $sub_path = substr($md5, 0, 2);
2079       return $this->cfg->thumb_path
2080          . "/"
2081          . $sub_path
2082          . "/"
2083          . $width
2084          . "_"
2085          . $md5;
2086
2087    } // get_thumb_path()
2088
2089    /**
2090     * returns server's virtual host name
2091     */
2092    private function get_server_name()
2093    {
2094       return $_SERVER['SERVER_NAME'];
2095    } // get_server_name()
2096
2097    /**
2098     * returns type of webprotocol which is
2099     * currently used
2100     */
2101    private function get_web_protocol()
2102    {
2103       if(!isset($_SERVER['HTTPS']))
2104          return "http";
2105       else
2106          return "https";
2107    } // get_web_protocol()
2108
2109    /**
2110     * return url to this phpfspot installation
2111     */
2112    private function get_phpfspot_url()
2113    {
2114       return $this->get_web_protocol() ."://". $this->get_server_name() . $this->cfg->web_path;
2115    } // get_phpfspot_url()
2116    
2117    /**
2118     * check file exists and is readable
2119     *
2120     * returns true, if everything is ok, otherwise false
2121     * if $silent is not set, this function will output and
2122     * error message
2123     */
2124    private function check_readable($file, $silent = null)
2125    {
2126       if(!file_exists($file)) {
2127          if(!isset($silent))
2128             print "File \"". $file ."\" does not exist.\n";
2129          return false;
2130       }
2131
2132       if(!is_readable($file)) {
2133          if(!isset($silent))
2134             print "File \"". $file ."\" is not reachable for user ". $this->getuid() ."\n";
2135          return false;
2136       }
2137
2138       return true;
2139
2140    } // check_readable()
2141
2142    /**
2143     * check if all needed indices are present
2144     *
2145     * this function checks, if some needed indices are already
2146     * present, or if not, create them on the fly. they are
2147     * necessary to speed up some queries like that one look for
2148     * all tags, when show_tags is specified in the configuration.
2149     */
2150    private function checkDbIndices()
2151    {
2152       $result = $this->db->db_exec("
2153          CREATE INDEX IF NOT EXISTS
2154             phototag
2155          ON
2156             photo_tags
2157                (photo_id, tag_id)
2158       ");
2159
2160    } // checkDbIndices()
2161
2162    /**
2163     * retrive F-Spot database version
2164     *
2165     * this function will return the F-Spot database version number
2166     * It is stored within the sqlite3 database in the table meta
2167     */
2168    public function getFspotDBVersion()
2169    {
2170       if($result = $this->db->db_fetchSingleRow("
2171          SELECT data as version
2172          FROM meta
2173          WHERE
2174             name LIKE 'F-Spot Database Version'
2175       "))
2176          return $result['version'];
2177
2178       return null;
2179
2180    } // getFspotDBVersion()
2181
2182    /**
2183     * parse the provided URI and will returned the
2184     * requested chunk
2185     */
2186    public function parse_uri($uri, $mode)
2187    {
2188       if(($components = parse_url($uri)) !== false) {
2189
2190          switch($mode) {
2191             case 'filename':
2192                return basename($components['path']);
2193                break;
2194             case 'dirname':
2195                return dirname($components['path']);
2196                break;
2197             case 'fullpath':
2198                return $components['path'];
2199                break;
2200          }
2201       }
2202
2203       return $uri;
2204
2205    } // parse_uri()
2206
2207 } // class PHPFSPOT
2208
2209 ?>