'http://en.wikipedia.org/wiki/KEYWORD'); public function getThumbPath($hash) { $dir = CACHE_DIR . '/' . $hash[0]; if (!file_exists($dir)) mkdir($dir, 0777); return $dir . '/' . $hash . '_thumb.jpg'; } public function getSmallPath($hash) { $dir = CACHE_DIR . '/' . $hash[0]; if (!file_exists($dir)) mkdir($dir, 0777); return $dir . '/' . $hash . '_small.jpg'; } public function getTagId($name) { $name = htmlentities($name, ENT_QUOTES, 'UTF-8'); $name = preg_replace('!&([aeiouyAEIOUYNcC]|ae)(?:circ|grave|acute|circ|tilde|uml|ring|slash|cedil|lig);!', '\\1', $name); $name = html_entity_decode($name, ENT_QUOTES, 'UTF-8'); $name = strtolower($name); return $name; } public function __construct() { $init = $upgrade = false; if (!file_exists(CACHE_DIR)) { if (!mkdir(CACHE_DIR, 0777)) { echo '
'; echo 'Please create the directory '.CACHE_DIR.' and make it writable by this script.'; exit; } } if (!is_writable(CACHE_DIR)) { echo 'Please make the directory '.CACHE_DIR.' writable by this script.'; exit; } if (file_exists(CACHE_DIR . '/photos.db') && !file_exists(CACHE_DIR . '/photos.sqlite')) $upgrade = true; elseif (!file_exists(CACHE_DIR . '/photos.sqlite')) $init = true; $this->db = new PDO('sqlite:' . CACHE_DIR . '/photos.sqlite'); $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if ($upgrade) { $this->upgradeDBv3(); } elseif ($init) { header('Location: '.SELF_URL.'?index_all'); $this->initDB(); exit; } } protected function upgradeDBv3() { $this->initDB(); if (class_exists('SQLiteDatabase')) { $this->db->beginTransaction(); $old = new SQLiteDatabase(CACHE_DIR . '/photos.db'); $res = $old->query('SELECT * FROM photos;'); while ($row = $res->fetch(SQLITE_NUM)) { $row = array_map(array($this->db, 'quote'), $row); $this->db->exec('INSERT INTO photos VALUES ('.implode(',', $row).');'); } $res = $old->query('SELECT * FROM tags;'); while ($row = $res->fetch(SQLITE_NUM)) { $row = array_map(array($this->db, 'quote'), $row); $this->db->exec('INSERT INTO tags VALUES ('.implode(',', $row).');'); } $this->db->commit(); } else { // If there is no SQLite v2 driver, we just erease the old DB // and re-create index header('Location: '.SELF_URL.'?index_all'); unlink(CACHE_DIR . '/photos.db'); exit; } unlink(CACHE_DIR . '/photos.db'); return true; } protected function initDB() { $this->db->exec(' CREATE TABLE photos ( id INTEGER PRIMARY KEY, filename TEXT, path TEXT, width INTEGER, height INTEGER, size INTEGER, year INTEGER, month INTEGER, day INTEGER, time INTEGER, comment TEXT, details TEXT, hash TEXT ); CREATE UNIQUE INDEX hash ON photos (hash); CREATE INDEX file ON photos (filename, path); CREATE INDEX date ON photos (year, month, day); CREATE TABLE tags ( name TEXT, name_id TEXT, photo INTEGER, PRIMARY KEY (name_id, photo) ); '); } // Returns informations on a photo public function getInfos($filename, $path, $from_list=false) { $query = $this->db->prepare('SELECT * FROM photos WHERE filename = ? AND path = ?;'); $query->execute(array($filename, $path)); $pic = $query->fetch(PDO::FETCH_ASSOC); if (!$pic) return false; $file = BASE_DIR . '/' . ($path ? $path . '/' : '') . $filename; // If the file doesn't exists anymore, we just delete it's informations if (!file_exists($file)) { $this->cleanInfos($pic['id'], $pic['hash']); return -1; } // Check if the file hash changed and if it's the case, delete the existing informations $hash = $this->getHash($filename, $path, filesize($file), filemtime($file)); if ($hash != $pic['hash']) { $this->cleanInfos($pic['id'], $pic['hash']); if (!$from_list && $pic = $this->addInfos($filename, $path)) return $pic; return false; } $query = $this->db->prepare('SELECT name, name_id FROM tags WHERE photo = ?;'); $query->execute(array($pic['id'])); $pic['tags'] = array(); while ($row = $query->fetch(PDO::FETCH_ASSOC)) { $pic['tags'][$row['name_id']] = $row['name']; } $small_path = $this->getSmallPath($hash); if (GEN_SMALL == 2 && !$from_list && !file_exists($small_path) && $pic['width'] <= MAX_IMAGE_SIZE && $pic['height'] <= MAX_IMAGE_SIZE) { $this->resizeImage($file, $small_path, $pic['width'], $pic['height'], SMALL_IMAGE_SIZE); } return $pic; } public function getPrevAndNext($dir, $file) { $query = $this->db->prepare('SELECT id, hash, path, filename FROM photos WHERE path = ? AND filename < ? ORDER BY filename COLLATE NOCASE DESC LIMIT 0,1;'); $query->execute(array($dir, $file)); $prev = $query->fetch(PDO::FETCH_ASSOC); $query = $this->db->prepare('SELECT id, hash, path, filename FROM photos WHERE path = ? AND filename > ? ORDER BY filename COLLATE NOCASE ASC LIMIT 0,1;'); $query->execute(array($dir, $file)); $next = $query->fetch(PDO::FETCH_ASSOC); return array($prev, $next); } // Delete photo informations and thumb private function cleanInfos($id, $hash) { $this->db->exec('DELETE FROM photos WHERE id="'.(int)$id.'";'); $this->db->exec('DELETE FROM tags WHERE photo="'.(int)$id.'";'); $thumb = $this->getThumbPath($hash); if (file_exists($thumb)) unlink($thumb); $small = $this->getSmallPath($hash); if (file_exists($small)) unlink($small); return true; } // Delete all photos in DB which are deleted in filesystem public function cleanDB() { $query = $this->db->query('SELECT id, hash, path, filename FROM photos ORDER BY id;'); while ($row = $query->fetch(PDO::FETCH_ASSOC)) { $file = BASE_DIR . '/' . ($row['path'] ? $row['path'] . '/' : '') . $row['filename']; if (!file_exists($file)) { $this->cleanInfos($row['id'], $row['hash']); } } unset($query); } public function getNewPhotos($nb=10) { $query = $this->db->query('SELECT * FROM photos ORDER BY time DESC LIMIT 0,'.(int)$nb.';'); return $query->fetchAll(PDO::FETCH_ASSOC); } private function getHash($file, $path, $size, $time) { return md5($file . $path . $size . $time); } // Extract informations about a photo public function addInfos($filename, $path) { $file = BASE_DIR . '/' . ($path ? $path . '/' : '') . $filename; if (!file_exists($file)) { return false; } $file_time = @filemtime($file); if (!$file_time) return false; $file_size = filesize($file); $hash = $this->getHash($filename, $path, $file_size, $file_time); $query = $this->db->prepare('SELECT 1 FROM photos WHERE hash = ? LIMIT 1;'); $query->execute(array($hash)); if ($query->fetchColumn()) { return false; } $size = getimagesize($file, $infos); $width = $size[0]; $height = $size[1]; $comment = ''; $tags = array(); $date = false; $details = array(); // IPTC contains tags if (!empty($infos['APP13'])) { $iptc = iptcparse($infos['APP13']); if (!empty($iptc['2#025'])) { foreach ($iptc['2#025'] as $tag) { $tags[] = $tag; } } unset($iptc); } unset($infos, $size); // EXIF contains date, comment and thumbnail and other details $exif = @exif_read_data($file, 0, true, true); if (!empty($exif)) { if (!empty($exif['IFD0']['DateTimeOriginal'])) $date = strtotime($exif['IDF0']['DateTimeOriginal']); elseif (!empty($exif['EXIF']['DateTimeOriginal'])) $date = strtotime($exif['EXIF']['DateTimeOriginal']); elseif (!empty($exif['IFD0']['DateTime'])) $date = strtotime($exif['IFD0']['DateTime']); elseif (!empty($exif['FILE']['FileDateTime'])) $date = (int) $exif['FILE']['FileDateTime']; if (!empty($exif['COMMENT'])) { $comment = implode("\n", $exif['COMMENT']); $comment = trim($comment); } if (!empty($exif['THUMBNAIL']['THUMBNAIL'])) { $thumb = $exif['THUMBNAIL']['THUMBNAIL']; } if (!empty($exif['IFD0']['Make'])) { $details['maker'] = trim($exif['IFD0']['Make']); } if (!empty($exif['IFD0']['Model'])) { if (!empty($details['maker'])) { $exif['IFD0']['Model'] = str_replace($details['maker'], '', $exif['IFD0']['Model']); } $details['model'] = trim($exif['IFD0']['Model']); } if (!empty($exif['EXIF']['ExposureTime'])) { $details['exposure'] = $exif['EXIF']['ExposureTime']; // To display a human readable number if (preg_match('!^([0-9.]+)/([0-9.]+)$!', $details['exposure'], $match) && (float)$match[1] > 0 && (float)$match[2] > 0) { $result = round((float)$match[1] / (float)$match[2], 3); $details['exposure'] = $result; } } if (!empty($exif['EXIF']['FNumber'])) { $details['fnumber'] = $exif['EXIF']['FNumber']; if (preg_match('!^([0-9.]+)/([0-9.]+)$!', $details['fnumber'], $match)) { $details['fnumber'] = round($match[1] / $match[2], 1); } } if (!empty($exif['EXIF']['ISOSpeedRatings'])) { $details['iso'] = $exif['EXIF']['ISOSpeedRatings']; } if (!empty($exif['EXIF']['Flash'])) { $details['flash'] = ($exif['EXIF']['Flash'] & 0x01) ? true : false; } if (!empty($exif['EXIF']['ExifImageWidth']) && !empty($exif['EXIF']['ExifImageLength'])) { $details['resolution'] = (int)$exif['EXIF']['ExifImageWidth'] . ' x ' . $exif['EXIF']['ExifImageLength']; } if (!empty($exif['EXIF']['FocalLength'])) { $details['focal'] = $exif['EXIF']['FocalLength']; if (preg_match('!^([0-9.]+)/([0-9.]+)$!', $details['focal'], $match)) { $details['focal'] = round($match[1] / $match[2], 1); } } } unset($exif); if (!$date) $date = $file_time; $can_resize = ($width <= MAX_IMAGE_SIZE && $height <= MAX_IMAGE_SIZE); if (isset($thumb)) { file_put_contents($this->getThumbPath($hash), $thumb); } elseif ($can_resize) { $this->resizeImage($file, $this->getThumbPath($hash), $width, $height, 160); } if (GEN_SMALL == 1 && $can_resize) { $this->resizeImage($file, $this->getSmallPath($hash), $width, $height, SMALL_IMAGE_SIZE); } if (!empty($details)) $details = json_encode($details); else $details = ''; $pic = array( 'filename' => $filename, 'path' => $path, 'width' => (int)$width, 'height' => (int)$height, 'size' => (int)$file_size, 'date_y' => date('Y', $date), 'date_m' => date('m', $date), 'date_d' => date('d', $date), 'time' => (int) $date, 'comment' => $comment, 'details' => $details, 'hash' => $hash, ); $query = $this->db->prepare('INSERT INTO photos (id, filename, path, width, height, size, year, month, day, time, comment, details, hash) VALUES (NULL, :filename, :path, :width, :height, :size, :date_y, :date_m, :date_d, :time, :comment, :details, :hash);'); $query->execute($pic); $pic['id'] = $this->db->lastInsertId(); if (!$pic['id']) { return false; } if (!empty($tags)) { $this->db->beginTransaction(); $query = $this->db->prepare('INSERT OR IGNORE INTO tags (name, name_id, photo) VALUES (?, ?, ?);'); foreach ($tags as $tag) { $query->execute(array($tag, $this->getTagId($tag), (int)$pic['id'])); } $this->db->commit(); } $pic['tags'] = $tags; return $pic; } static public function getValidDirectory($path) { $path = preg_replace('!(^/+|/+$)!', '', $path); if (preg_match('![.]{2,}!', $path)) return false; if ($path == '.') return ''; return $path; } // Returns directories and pictures inside a directory public function getDirectory($path='', $dont_check = false) { $path = self::getValidDirectory($path); if ($path === false) return false; if ($path == '.' || empty($path)) $dir_path = BASE_DIR . '/'; else $dir_path = BASE_DIR . '/' . $path . '/'; $dir = @dir($dir_path); if (!$dir) return false; $dirs = array(); $pictures = array(); $to_update = array(); while ($file = $dir->read()) { $file_path = $dir_path . $file; if ($file[0] == '.' || $file_path == CACHE_DIR) continue; if (is_dir($file_path)) { $dirs[] = $file; } elseif (!preg_match('!\.jpe?g$!i', $file)) { continue; } // Don't detect updates when directory has already been updated // (used in 'index_all' process only, to avoid server load) elseif ($dont_check) { continue; } elseif ($pic = $this->getInfos($file, $path, true)) { if (is_array($pic)) $pictures[$file] = $pic; } else { $to_update[] = $file; } } $dir->close(); sort($dirs); ksort($pictures); if (file_exists($dir_path . 'README')) { $description = file_get_contents($dir_path . 'README'); } else { $description = false; } return array($dirs, $pictures, $to_update, $description); } public function getByDate($y=false, $m=false, $d=false) { if ($d) { $query = $this->db->prepare('SELECT * FROM photos WHERE year = ? AND month = ? AND day = ? ORDER BY time;'); $query->execute(array((int)$y, (int)$m, (int)$d)); return $query->fetchAll(PDO::FETCH_ASSOC); } else { // Get all days for a month view, all months for a year view or all years for a global view $req = 'SELECT day, month, year, COUNT(*) AS nb FROM photos WHERE 1 '; if ($y && $m) $req .= 'AND year="'.(int)$y.'" AND month="'.(int)$m.'" GROUP BY day ORDER BY day;'; elseif ($y) $req .= 'AND year="'.(int)$y.'" GROUP BY month ORDER BY month;'; else $req .= 'GROUP BY year ORDER BY year, month;'; $query = $this->db->query($req); $list = array(); while ($row = $query->fetch(PDO::FETCH_ASSOC)) { $start = 0; if ($row['nb'] > 5) { $start = mt_rand(0, $row['nb'] - 5); } // Get 5 random pictures for each line $subquery = 'SELECT * FROM photos WHERE year = '.$this->db->quote((int)$row['year']); if ($y) $subquery .= ' AND month = '.$this->db->quote((int)$row['month']); if ($m) $subquery .= ' AND day = '.$this->db->quote((int)$row['day']); $subquery .= 'ORDER BY time LIMIT '.(int)$start.', 5;'; $subquery = $this->db->query($subquery)->fetchAll(PDO::FETCH_ASSOC); if ($row['nb'] > 5) { $more = $row['nb'] - 5; foreach ($subquery as &$row_sub) { $row_sub['nb'] = $row['nb']; $row_sub['more'] = $more; } } $list = array_merge($list, $subquery); } return $list; } } public function getTagList() { $query = $this->db->query('SELECT COUNT(photo) AS nb, name, name_id FROM tags GROUP BY name_id ORDER BY name_id COLLATE NOCASE;'); $tags = array(); while ($row = $query->fetch(PDO::FETCH_ASSOC)) { $tags[$row['name']] = $row['nb']; } return $tags; } public function getByTag($tag) { $query = $this->db->prepare('SELECT photos.* FROM photos INNER JOIN tags ON tags.photo = photos.id WHERE tags.name_id = ? ORDER BY photos.time, photos.filename COLLATE NOCASE;'); $query->execute(array($this->getTagId($tag))); return $query->fetchAll(PDO::FETCH_ASSOC); } public function getNearTags($tag) { $query = $this->db->prepare('SELECT t2.name, COUNT(t2.photo) AS nb FROM tags INNER JOIN tags AS t2 ON tags.photo = t2.photo WHERE tags.name_id = ? AND t2.name_id != tags.name_id GROUP BY t2.name_id ORDER BY nb DESC;'); $query->execute(array($this->getTagId($tag))); $tags = array(); while ($row = $query->fetch(PDO::FETCH_ASSOC)) { $tags[$row['name']] = $row['nb']; } return $tags; } public function getTagNameFromId($tag) { $query = $this->db->prepare('SELECT name FROM tags WHERE name_id = ? LIMIT 1;'); $query->execute(array($tag)); return $query->fetchColumn(); } private function seems_utf8($str) { $length = strlen($str); for ($i=0; $i < $length; $i++) { $c = ord($str[$i]); if ($c < 0x80) $n = 0; # 0bbbbbbb elseif (($c & 0xE0) == 0xC0) $n=1; # 110bbbbb elseif (($c & 0xF0) == 0xE0) $n=2; # 1110bbbb elseif (($c & 0xF8) == 0xF0) $n=3; # 11110bbb elseif (($c & 0xFC) == 0xF8) $n=4; # 111110bb elseif (($c & 0xFE) == 0xFC) $n=5; # 1111110b else return false; # Does not match any model for ($j=0; $j<$n; $j++) { if ((++$i == $length) || ((ord($str[$i]) & 0xC0) != 0x80)) return false; } } return true; } private function intelligent_utf8_encode($str) { if ($this->seems_utf8($str)) return $str; else return utf8_encode($str); } public function formatText($text) { $text = $this->intelligent_utf8_encode($text); $text = escape($text); // Allow simple, correctly closed, html tags (, , ...) $text = preg_replace('!<([a-z]+)>(.*)</\\1>!isU', '<\\1>\\2', $text); $text = preg_replace('#(^|\s)([a-z]+://([^\s\w/]?[\w/])*)(\s|$)#im', '\\1\\2\\4', $text); $text = str_replace('\http:', 'http:', $text); foreach ($this->html_tags as $tag=>$url) { $tag_class = preg_replace('![^a-zA-Z0-9_]!', '_', $tag); $text = preg_replace('#(^|\s)'.preg_quote($tag, '#').':([^\s,.]+)#iem', "'\\1\\2\\3'", $text); } $text = nl2br($text); return $text; } public function formatDetails($details) { if (empty($details)) return ''; if (!is_array($details)) $details = json_decode($details, true); $out = array(); if (isset($details['maker'])) $out[__('Camera maker:')] = $details['maker']; if (isset($details['model'])) $out[__('Camera model:')] = $details['model']; if (isset($details['exposure'])) $out[__('Exposure:')] = __('%EXPOSURE seconds', 'REPLACE', array('%EXPOSURE' => $details['exposure'])); if (isset($details['fnumber'])) $out[__('Aperture:')] = 'f' . $details['fnumber']; if (isset($details['iso'])) $out[__('ISO speed:')] = $details['iso']; if (isset($details['flash'])) $out[__('Flash:')] = $details['flash'] ? __('On') : __('Off'); if (isset($details['focal'])) $out[__('Focal length:')] = $details['focal'] . ' mm'; if (isset($details['resolution'])) $out[__('Original resolution:')] = $details['resolution']; return $out; } /* * Resize an image using imlib, imagick or GD, if one fails it tries the next */ private function resizeImage($source, $dest, $width, $height, $max) { list($new_width, $new_height) = $this->getNewSize($width, $height, $max); if ($new_width == $width && $new_height == $height) return true; // IMLib (fast!) if (extension_loaded('imlib')) { $src = @imlib_load_image($source); if ($src) { $dst = imlib_create_scaled_image($src, $new_width, $new_height); imlib_free_image($src); if ($dst) { imlib_image_set_format($dst, "jpeg"); if (file_exists($dest)) @unlink($dest); imlib_save_image($dst, $dest); imlib_free_image($dst); return true; } } } // Imagick >= 2.0 API (quite fast) if (extension_loaded('imagick') && class_exists('Imagick')) { $im = new Imagick; if ($im->readImage($source) && $im->resizeImage($new_width, $new_height, Imagick::FILTER_UNDEFINED, 1)) { if (file_exists($dest)) @unlink($dest); $im->stripImage(); $im->writeImage($dest); $im->destroy(); return true; } } // Imagick < 2.0 API (quite fast) if (extension_loaded('imagick') && function_exists('imagick_readimage')) { $handle = imagick_readimage($source); imagick_resize($handle, $new_width, $new_height, IMAGICK_FILTER_UNKNOWN, 1); imagick_convert($handle,'JPEG'); if (file_exists($dest)) @unlink($dest); imagick_writeimage($handle, $dest); imagick_free($handle); if (file_exists($dest)) return true; } // GD >= 2.0 (slow) if (function_exists('imagecopyresampled') && extension_loaded('gd')) { $sourceImage = @imagecreatefromjpeg($source); if($sourceImage) { $newImage = imagecreatetruecolor($new_width, $new_height); imagecopyresampled($newImage, $sourceImage, 0, 0, 0, 0, $new_width, $new_height, $width, $height); if (file_exists($dest)) @unlink($dest); if(imagejpeg($newImage, $dest)) return true; } } return false; } public function getNewSize($width, $height, $max) { if($width > $max OR $height > $max) { if($height <= $width) $ratio = $max / $width; else $ratio = $max / $height; $width = round($width * $ratio); $height = round($height * $ratio); } return array($width, $height); } } ?>