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