lazygal-0.8.2/0000755000175000017500000000000012301073200013074 5ustar niolniol00000000000000lazygal-0.8.2/locale/0000755000175000017500000000000012301073200014333 5ustar niolniol00000000000000lazygal-0.8.2/locale/fr_FR.po0000644000175000017500000004214612301072454015712 0ustar niolniol00000000000000# Lazygal, a static web gallery generator. # Copyright (C) 2008 Alexandre Rossi # This file is distributed under the same license as the lazygal package. # Alexandre Rossi , 2008 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Lazygal\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-19 10:10+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Alexandre Rossi \n" "Language-Team: français \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: ../lazygal.py:47 msgid "usage: %prog [options] albumdir" msgstr "usage: %prog [options] albumdir" #. The help option must be changed to comply with i18n. #: ../lazygal.py:51 msgid "Show this help message and exit." msgstr "Afficher ce message d'aide et quitter." #: ../lazygal.py:56 msgid "Don't output anything except for errors." msgstr "N'afficher que les erreurs." #: ../lazygal.py:60 msgid "Output everything that lazygal is doing." msgstr "Afficher tout ce que lazygal fait." #: ../lazygal.py:64 msgid "" "Directory where web pages, slides and thumbs will be written (default is " "current directory)." msgstr "" "Répertoire dans lequel les pages, les images et les miniatures seront écrits " "(par défaut le répertoire courant)." #: ../lazygal.py:68 msgid "Theme name (looked up in theme directory) or theme full path." msgstr "" "Identifiant du thème (utilisé dans le répertoire des thèmes) ou chemin " "complet d'un thème." #: ../lazygal.py:72 msgid "Default style to apply to the theme." msgstr "Style par défaut à appliquer au thème." #: ../lazygal.py:76 msgid "Common variables to load all templates with." msgstr "Variables communes avec lesquelles charger tous les modèles." #: ../lazygal.py:80 msgid "Force rebuild of all pages." msgstr "Forcer la reconstruction de toutes les pages." #: ../lazygal.py:84 msgid "Clean destination directory of files that should not be there." msgstr "" "Supprimer dans le répertoire de destination tous les fichiers qui ne doivent " "pas s'y trouver." #: ../lazygal.py:88 msgid "Display program version." msgstr "Afficher la version du programme." #: ../lazygal.py:92 msgid "" "Exhaustively go through all directories regardless of source modification " "time." msgstr "" "Parcourir exhaustivement tous les répertoires sans examiner la date de " "modification du répertoire source." #: ../lazygal.py:96 msgid "" "Level below which the directory tree is flattened. Default is 'No' which " "disables this feature." msgstr "" "Niveau de hierarchie au dessous duquel l'arborescence des galleries est " "rassemblée sur une page. Par défaut 'No' qui désactive cette fonctionnalité." #: ../lazygal.py:100 msgid "" "Size of images, define as =SIZE,..., eg. small=800x600," "medium=1024x768. The special value 0x0 uses original size. See manual page " "for SIZE syntax." msgstr "" "Taille des images, à définir comme suit =TAILLE,..., eg. small=800x600," "medium=1024x768. La valeur spéciale 0x0 conserve la taille originale. Voir " "le manuel pour la syntaxe de TAILLE." #: ../lazygal.py:104 msgid "" "Size of thumbnails, define as SIZE, eg. 150x113. See manual page for SIZE " "syntax." msgstr "" "Taille des miniatures, à définir comme suit TAILLE, eg. 150x113. Voir le " "manuel pour la syntaxe de TAILLE." #: ../lazygal.py:108 msgid "Quality of generated JPEG images (default is 85)." msgstr "Qualité des JPEG générés (85 par défaut)" #: ../lazygal.py:112 msgid "Include original photos in output." msgstr "Copier les photos originales dans ce qui est produit." #: ../lazygal.py:116 msgid "" "Do not copy original photos in output directory, instead link them using " "submitted relative path as base." msgstr "" "Ne pas copier les photos originales dans le répertoire de production de " "l'album web, au lieu de cela les référencer directement en utilisant le " "chemin relatif de base fourni" #: ../lazygal.py:120 msgid "" "Do not copy original photos in output directory, instead create symlinks to " "their original locations." msgstr "" "Ne pas copier les photos originales dans le répertoire de production de " "l'album web, au lieu de cela, créer un lien symbolique vers leur emplacement." #: ../lazygal.py:124 msgid "Publication URL (only useful for feed generation)." msgstr "" "Adresse URL de publication (utilisé uniquement pour la génération du flux)." #: ../lazygal.py:128 msgid "" "Generate metadata description files where they don't exist instead of " "generating the web gallery." msgstr "" "Générer les fichiers de description qui n'existent pas au lieu de générer " "des galleries web." #: ../lazygal.py:132 msgid "" "Maximum number of thumbs per index page. This enables index pagination (0 is " "unlimited)." msgstr "" "Nombre maximum de miniatures par page d'index. Ceci active la pagination " "pour les index (0 correspond à sans limite)." #: ../lazygal.py:136 msgid "Make a zip archive of original pictures for each directory." msgstr "" "Générer une archive zip contenant les images originales pour chaque " "répertoire." #: ../lazygal.py:140 msgid "" "Webalbum picture background color. Default is transparent, and implies the " "PNG format. Any other value, e.g. red, white, blue, uses JPEG." msgstr "" "Couleur de fond de l'image représentant une gallerie. Par défaut " "transparente, ce qui implique le format PNG. Pour toutes autre valeur, par " "exemple red, white, blue, c'est le format JPEG qui est utilisé." #: ../lazygal.py:145 msgid "Webalbum picture type. Default is messy." msgstr "Type d'image représentant les galleries. Par défaut 'messy'." #: ../lazygal.py:147 ../lazygal.py:150 msgid "ORDER" msgstr "TRI" #: ../lazygal.py:148 msgid "" "Sort order for images in a folder: filename, mtime, or exif. Add ':reverse' " "to reverse the chosen order." msgstr "" "Ordre de classement pour les images dans un dossier : par nom de " "fichier'filename', date de modification de fichier 'mtime' ou date de prise " "de vue'exif'. Ajouter ':reverse' pour inverser l'ordre'" #: ../lazygal.py:151 msgid "" "Sort order for sub galleries in a folder: dirname, exif or mtime. Add ':" "reverse' to reverse the chosen order." msgstr "" "Ordre de classement pour les sous-galleries : par nom de répertoire " "'dirname' ou date de modification de fichier 'mtime'. Ajouter ':reverse' " "pour inverser l'ordre'" #: ../lazygal.py:153 msgid "TAG" msgstr "MOT-CLÉ" #: ../lazygal.py:154 msgid "" "Only include in the gallery pics whose IPTC keywords match the supplied " "filter(s)." msgstr "" "Ne prendre en compte que les images dont l'un des most-clés IPTC est présent " "dans la liste donnée en argument." #: ../lazygal.py:158 msgid "" "Do not remove GPS location tags from EXIF metadata. Mostly useful with " "holiday photos." msgstr "" "Ne pas enlever les coordonnées GPS dans les métadonnées avant la " "publication. Surtout utile pour les photos de vacances." #: ../lazygal.py:162 #, python-format msgid "lazygal version %s" msgstr "lazygal version %s" #: ../lazygal.py:167 msgid "Bad command line." msgstr "Mauvaise ligne de commande." #: ../lazygal.py:171 #, python-format msgid "Directory %s does not exist." msgstr "Le répertoire %s n'existe pas." #: ../lazygal.py:226 msgid "Option --orig-symlink is not available on this platform." msgstr "L'option --orig-symlink n'est pas disponible sur cette plateforme." #: ../lazygal.py:275 msgid "Interrupted." msgstr "Interrompu." #: ../lazygal/sourcetree.py:90 ../lazygal/sourcetree.py:325 msgid "Root not found" msgstr "La racine n'a pas été trouvée" #: ../lazygal/sourcetree.py:281 #, python-format msgid " Ignoring %s, cannot open file (broken symlink?)." msgstr " Ignore %s, impossible d'ouvrir le fichier (lien symbolique cassé?)." #: ../lazygal/sourcetree.py:292 #, python-format msgid " Ignoring %s, format not supported." msgstr " %s ignoré, format non supporté." #: ../lazygal/generators.py:49 msgid "Could not find themes dir, check your installation!" msgstr "" "Le répertoire des thèmes n'a pas été trouvé, vérifier votre installation!" #: ../lazygal/generators.py:67 msgid " SORTING pics and subdirs" msgstr " TRI des images et des sous-répertoires" #: ../lazygal/generators.py:80 ../lazygal/generators.py:92 #, python-format msgid "Unknown sorting criterion '%s'" msgstr "Critère de classement inconnu '%s'" #: ../lazygal/generators.py:125 msgid " BREAKING web gallery into multiple pages" msgstr " PAGINATION de la gallerie web" #: ../lazygal/generators.py:371 #, python-format msgid " MKDIR %%WEBALBUMROOT%%/%s" msgstr " MKDIR %%WEBALBUMROOT%%/%s" #: ../lazygal/generators.py:409 msgid "" "Sizes is a comma-separated list of size names and specs:\n" "\t e.g. \"small=640x480,medium=1024x768\"." msgstr "" "Les tailles sont décrites sous la forme d'une liste d'éléments séparés par " "des virgules qui contiennes le nom et les spécifications:\n" "\t e.g. \"small=640x480,medium=1024x768\"." #: ../lazygal/generators.py:411 #, python-format msgid "Size name '%s' is reserved for internal processing." msgstr "La taille '%s' est réservée pour le fonctionnement interne." #: ../lazygal/generators.py:424 #, python-format msgid "'%s' for size '%s' does not describe a known size syntax." msgstr "" "'%s' pour la taille '%s' ne décrit pas un type connu de syntaxe de taille." #: ../lazygal/generators.py:458 #, python-format msgid " Trying loading gallery configs: %s" msgstr "" " Tentative de chargement des fichiers de configuration par gallerie : %s" #: ../lazygal/generators.py:501 msgid "Bad syntax for webalbumpic-size." msgstr "Syntaxe de webalbumpic-size erronée." #: ../lazygal/generators.py:690 msgid "MKDIR %SHAREDDIR%" msgstr "MKDIR %SHAREDDIR%" #: ../lazygal/generators.py:749 #, python-format msgid "Progress: dir %d/%d (%d%%), media %d/%d (%d%%)" msgstr "Progression: répertoire %d/%d (%d%%), média %d/%d (%d%%)" #: ../lazygal/generators.py:757 #, python-format msgid ", current task %d%%" msgstr "" #: ../lazygal/generators.py:772 #, python-format msgid "Trying loading user config %s" msgstr "" "Tentative de chargement du fichier de configuration %s de l'utilisateur" #: ../lazygal/generators.py:777 #, python-format msgid "Loading root config %s" msgstr "Chargement du fichier de configuration racine %s" #: ../lazygal/generators.py:781 #, python-format msgid "" "'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page." msgstr "" "'%s' utilise une syntaxe obsolète: merci de consulter la page de " "manuellazygal.conf(5)" #: ../lazygal/generators.py:825 #, python-format msgid "Generating metadata in %s" msgstr "Génération des fichiers de description dans %s" #: ../lazygal/generators.py:831 ../lazygal/generators.py:890 #, python-format msgid "[Entering %%ALBUMROOT%%/%s]" msgstr "[Entrée dans %%ALBUMROOT%%/%s]" #: ../lazygal/generators.py:861 msgid "Fatal error, web gallery directory is within source tree." msgstr "" "Erreur fatale, le répertoire de génération se trouve sous le répertoire " "source." #: ../lazygal/generators.py:863 #, python-format msgid "Generating to %s" msgstr "Génération dans %s" #: ../lazygal/generators.py:883 #, python-format msgid "(%s) has been skipped" msgstr "(%s) a été ignoré" #: ../lazygal/generators.py:887 #, python-format msgid "" "(%s) has been skipped because its name collides with the shared material " "directory name" msgstr "" "(%s) a été ignoré car son nom est le même que celui du répertoire contenant " "les fichiers partagés par toutes les pages de l'album" #: ../lazygal/generators.py:928 msgid "" " SKIPPED because of mtime, touch source or use --check-all-dirs to override." msgstr "" " IGNORE à cause de la mtime, utiliser touch ou --check-all-dirs pour " "débrayer." #: ../lazygal/generators.py:937 #, python-format msgid "[Leaving %%ALBUMROOT%%/%s]" msgstr "[Sortie de %%ALBUMROOT%%/%s]" #: ../lazygal/metadata.py:327 ../lazygal/metadata.py:355 #, python-format msgid " (35 mm equivalent: %s mm)" msgstr " (équivalent 35 mm: %s mm)" #: ../lazygal/metadata.py:447 #, python-format msgid "Could not open metadata file %s" msgstr "Impossible d'ouvrir le fichier de description %s" #: ../lazygal/metadata.py:553 msgid " SKIPPED because metadata exists." msgstr " IGNORE car le fichier de description existe déjà." #: ../lazygal/metadata.py:555 msgid " SKIPPED because directory does not contain images." msgstr " IGNORE car le fichier de description existe déjà." #: ../lazygal/metadata.py:564 #, python-format msgid "GEN %s" msgstr "GEN %s" #: ../lazygal/tpl.py:162 #, python-format msgid "Unknown template type for %s" msgstr "Type de modèle inconnu %s" #: ../lazygal/tpl.py:176 #, python-format msgid "Theme %s not found" msgstr "Thème %s non trouvé" #: ../lazygal/tpl.py:203 #, python-format msgid "Loading %s for theme %s" msgstr "" #: ../lazygal/tpl.py:211 #, python-format msgid "Theme %s does not have a %s" msgstr "" #: ../lazygal/tpl.py:214 #, python-format msgid "Theme %s : %s parsing error" msgstr "" #: ../lazygal/genfile.py:106 #, python-format msgid " ZIP %s" msgstr " ZIP %s" #: ../lazygal/genfile.py:128 #, python-format msgid "CP %%SHAREDDIR%%/%s" msgstr "CP %%SHAREDDIR%%/%s" #: ../lazygal/genmedia.py:81 #, python-format msgid " %s is BROKEN, skipped" msgstr " %s est CASSE, ignoré" #: ../lazygal/genmedia.py:121 msgid "RESIZE" msgstr "REDIMENSIONNE" #: ../lazygal/genmedia.py:209 #, python-format msgid "Could not copy metadata in reduced picture: %s" msgstr "Impossible de recopier les méta-données dans l'image réduite: %s" #: ../lazygal/genmedia.py:222 msgid "VIDEOTHUMB" msgstr "VIDEOTHUMB" #: ../lazygal/genmedia.py:229 #, python-format msgid " creating %s thumbnail failed, skipped" msgstr " la construction de l'aperçu de la vidéo %s a échoué" #: ../lazygal/genmedia.py:262 #, python-format msgid "Supplied album picture %s does not exist." msgstr "L'image fournie pour représenter la gallerie %s n'existe pas." #: ../lazygal/genmedia.py:282 #, python-format msgid " DIRPIC %s" msgstr " DIRPIC %s" #: ../lazygal/genmedia.py:311 #, python-format msgid " TRANSCODE %s" msgstr " TRANSCODAGE %s" #: ../lazygal/genmedia.py:320 #, python-format msgid " transcoding %s failed, skipped" msgstr " le transcodage de la vidéo %s a échoué" #: ../lazygal/genpage.py:129 ../lazygal/genpage.py:266 #, python-format msgid " XHTML %s" msgstr " XHTML %s" #: ../lazygal/genpage.py:213 #, python-format msgid "" " Size '%s' is not available in '%s' due to configuration: medias won't be " "shown on index." msgstr "" " La taille '%s' n'est pas disponible dans '%s' à cause de la " "configuration : les médias correspondants ne seront par présents sur l'index." #: ../lazygal/genpage.py:357 #, python-format msgid "FEED %s" msgstr "FEED %s" #: ../lazygal/genpage.py:375 msgid "" "We have a template with an extension that does not start with a t. Aborting." msgstr "" "Il y a un modèle avec une extension qui ne commence par par t. Abandon." #: ../lazygal/genpage.py:381 #, python-format msgid "TPL %%SHAREDDIR%%/%s" msgstr "TPL %%SHAREDDIR%%/%s" #: ../lazygal/config.py:125 #, python-format msgid " Ignoring unknown section '%s'." msgstr " Section de configuration '%s' ignorée car inconnue." #: ../lazygal/config.py:129 #, python-format msgid " Ignoring unknown option '%s' in section '%s'." msgstr "" " Option de configuration '%s' ignorée car inconnue dans la section '%s'." #: ../themes/default/dirindex.thtml:70 msgid "All full scale pictures as an archive, for" msgstr "Toutes les images en haute résolution dans une archive, pour" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 msgid "Generated by" msgstr "Généré par" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 #, python-format msgid "on %c" msgstr "le %c" #: ../themes/default/browse.thtml:39 msgid "previous" msgstr "précedente" #: ../themes/default/browse.thtml:44 msgid "next" msgstr "suivante" #: ../themes/default/image.thtml:9 msgid "Taken" msgstr "Prise" #: ../themes/default/image.thtml:9 msgid "on %d/%m/%Y at %H:%M" msgstr "le %d/%m/%Y à %H:%M" #: ../themes/default/image.thtml:11 msgid "Author" msgstr "Auteur" #: ../themes/default/image.thtml:12 msgid "Keywords" msgstr "Étiquettes" #: ../themes/default/image.thtml:13 msgid "Original picture" msgstr "Image en taille originale" #: ../themes/default/image.thtml:17 msgid "Camera:" msgstr "Appareil photo :" #: ../themes/default/image.thtml:18 msgid "with" msgstr "avec" #: ../themes/default/image.thtml:20 msgid "Exposure" msgstr "Temps d'exposition de" #: ../themes/default/image.thtml:21 msgid "Sensitivity ISO" msgstr "Sensibilité ISO" #: ../themes/default/image.thtml:22 msgid "Aperture" msgstr "Ouverture" #: ../themes/default/image.thtml:23 msgid "Flash" msgstr "Flash" #: ../themes/default/image.thtml:24 msgid "Focal length" msgstr "Focale" #: ../themes/default/video.thtml:6 msgid "Original video" msgstr "fichier vidéo d'origine" #: ../themes/default/feeditem.thtml:5 ../themes/default/gallerylink.thtml:21 #: ../themes/image-index/gallerylink.thtml:30 msgid "sub-galleries" msgstr "sous-galleries" #: ../themes/default/feeditem.thtml:6 ../themes/default/gallerylink.thtml:17 #: ../themes/image-index/gallerylink.thtml:26 msgid "photos" msgstr "photos" lazygal-0.8.2/locale/lazygal.pot0000644000175000017500000002643312301072370016541 0ustar niolniol00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-19 10:10+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../lazygal.py:47 msgid "usage: %prog [options] albumdir" msgstr "" #. The help option must be changed to comply with i18n. #: ../lazygal.py:51 msgid "Show this help message and exit." msgstr "" #: ../lazygal.py:56 msgid "Don't output anything except for errors." msgstr "" #: ../lazygal.py:60 msgid "Output everything that lazygal is doing." msgstr "" #: ../lazygal.py:64 msgid "" "Directory where web pages, slides and thumbs will be written (default is " "current directory)." msgstr "" #: ../lazygal.py:68 msgid "Theme name (looked up in theme directory) or theme full path." msgstr "" #: ../lazygal.py:72 msgid "Default style to apply to the theme." msgstr "" #: ../lazygal.py:76 msgid "Common variables to load all templates with." msgstr "" #: ../lazygal.py:80 msgid "Force rebuild of all pages." msgstr "" #: ../lazygal.py:84 msgid "Clean destination directory of files that should not be there." msgstr "" #: ../lazygal.py:88 msgid "Display program version." msgstr "" #: ../lazygal.py:92 msgid "" "Exhaustively go through all directories regardless of source modification " "time." msgstr "" #: ../lazygal.py:96 msgid "" "Level below which the directory tree is flattened. Default is 'No' which " "disables this feature." msgstr "" #: ../lazygal.py:100 msgid "" "Size of images, define as =SIZE,..., eg. small=800x600," "medium=1024x768. The special value 0x0 uses original size. See manual page " "for SIZE syntax." msgstr "" #: ../lazygal.py:104 msgid "" "Size of thumbnails, define as SIZE, eg. 150x113. See manual page for SIZE " "syntax." msgstr "" #: ../lazygal.py:108 msgid "Quality of generated JPEG images (default is 85)." msgstr "" #: ../lazygal.py:112 msgid "Include original photos in output." msgstr "" #: ../lazygal.py:116 msgid "" "Do not copy original photos in output directory, instead link them using " "submitted relative path as base." msgstr "" #: ../lazygal.py:120 msgid "" "Do not copy original photos in output directory, instead create symlinks to " "their original locations." msgstr "" #: ../lazygal.py:124 msgid "Publication URL (only useful for feed generation)." msgstr "" #: ../lazygal.py:128 msgid "" "Generate metadata description files where they don't exist instead of " "generating the web gallery." msgstr "" #: ../lazygal.py:132 msgid "" "Maximum number of thumbs per index page. This enables index pagination (0 is " "unlimited)." msgstr "" #: ../lazygal.py:136 msgid "Make a zip archive of original pictures for each directory." msgstr "" #: ../lazygal.py:140 msgid "" "Webalbum picture background color. Default is transparent, and implies the " "PNG format. Any other value, e.g. red, white, blue, uses JPEG." msgstr "" #: ../lazygal.py:145 msgid "Webalbum picture type. Default is messy." msgstr "" #: ../lazygal.py:147 ../lazygal.py:150 msgid "ORDER" msgstr "" #: ../lazygal.py:148 msgid "" "Sort order for images in a folder: filename, mtime, or exif. Add ':reverse' " "to reverse the chosen order." msgstr "" #: ../lazygal.py:151 msgid "" "Sort order for sub galleries in a folder: dirname, exif or mtime. Add ':" "reverse' to reverse the chosen order." msgstr "" #: ../lazygal.py:153 msgid "TAG" msgstr "" #: ../lazygal.py:154 msgid "" "Only include in the gallery pics whose IPTC keywords match the supplied " "filter(s)." msgstr "" #: ../lazygal.py:158 msgid "" "Do not remove GPS location tags from EXIF metadata. Mostly useful with " "holiday photos." msgstr "" #: ../lazygal.py:162 #, python-format msgid "lazygal version %s" msgstr "" #: ../lazygal.py:167 msgid "Bad command line." msgstr "" #: ../lazygal.py:171 #, python-format msgid "Directory %s does not exist." msgstr "" #: ../lazygal.py:226 msgid "Option --orig-symlink is not available on this platform." msgstr "" #: ../lazygal.py:275 msgid "Interrupted." msgstr "" #: ../lazygal/sourcetree.py:90 ../lazygal/sourcetree.py:325 msgid "Root not found" msgstr "" #: ../lazygal/sourcetree.py:281 #, python-format msgid " Ignoring %s, cannot open file (broken symlink?)." msgstr "" #: ../lazygal/sourcetree.py:292 #, python-format msgid " Ignoring %s, format not supported." msgstr "" #: ../lazygal/generators.py:49 msgid "Could not find themes dir, check your installation!" msgstr "" #: ../lazygal/generators.py:67 msgid " SORTING pics and subdirs" msgstr "" #: ../lazygal/generators.py:80 ../lazygal/generators.py:92 #, python-format msgid "Unknown sorting criterion '%s'" msgstr "" #: ../lazygal/generators.py:125 msgid " BREAKING web gallery into multiple pages" msgstr "" #: ../lazygal/generators.py:371 #, python-format msgid " MKDIR %%WEBALBUMROOT%%/%s" msgstr "" #: ../lazygal/generators.py:409 msgid "" "Sizes is a comma-separated list of size names and specs:\n" "\t e.g. \"small=640x480,medium=1024x768\"." msgstr "" #: ../lazygal/generators.py:411 #, python-format msgid "Size name '%s' is reserved for internal processing." msgstr "" #: ../lazygal/generators.py:424 #, python-format msgid "'%s' for size '%s' does not describe a known size syntax." msgstr "" #: ../lazygal/generators.py:458 #, python-format msgid " Trying loading gallery configs: %s" msgstr "" #: ../lazygal/generators.py:501 msgid "Bad syntax for webalbumpic-size." msgstr "" #: ../lazygal/generators.py:690 msgid "MKDIR %SHAREDDIR%" msgstr "" #: ../lazygal/generators.py:749 #, python-format msgid "Progress: dir %d/%d (%d%%), media %d/%d (%d%%)" msgstr "" #: ../lazygal/generators.py:757 #, python-format msgid ", current task %d%%" msgstr "" #: ../lazygal/generators.py:772 #, python-format msgid "Trying loading user config %s" msgstr "" #: ../lazygal/generators.py:777 #, python-format msgid "Loading root config %s" msgstr "" #: ../lazygal/generators.py:781 #, python-format msgid "" "'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page." msgstr "" #: ../lazygal/generators.py:825 #, python-format msgid "Generating metadata in %s" msgstr "" #: ../lazygal/generators.py:831 ../lazygal/generators.py:890 #, python-format msgid "[Entering %%ALBUMROOT%%/%s]" msgstr "" #: ../lazygal/generators.py:861 msgid "Fatal error, web gallery directory is within source tree." msgstr "" #: ../lazygal/generators.py:863 #, python-format msgid "Generating to %s" msgstr "" #: ../lazygal/generators.py:883 #, python-format msgid "(%s) has been skipped" msgstr "" #: ../lazygal/generators.py:887 #, python-format msgid "" "(%s) has been skipped because its name collides with the shared material " "directory name" msgstr "" #: ../lazygal/generators.py:928 msgid "" " SKIPPED because of mtime, touch source or use --check-all-dirs to override." msgstr "" #: ../lazygal/generators.py:937 #, python-format msgid "[Leaving %%ALBUMROOT%%/%s]" msgstr "" #: ../lazygal/metadata.py:327 ../lazygal/metadata.py:355 #, python-format msgid " (35 mm equivalent: %s mm)" msgstr "" #: ../lazygal/metadata.py:447 #, python-format msgid "Could not open metadata file %s" msgstr "" #: ../lazygal/metadata.py:553 msgid " SKIPPED because metadata exists." msgstr "" #: ../lazygal/metadata.py:555 msgid " SKIPPED because directory does not contain images." msgstr "" #: ../lazygal/metadata.py:564 #, python-format msgid "GEN %s" msgstr "" #: ../lazygal/tpl.py:162 #, python-format msgid "Unknown template type for %s" msgstr "" #: ../lazygal/tpl.py:176 #, python-format msgid "Theme %s not found" msgstr "" #: ../lazygal/tpl.py:203 #, python-format msgid "Loading %s for theme %s" msgstr "" #: ../lazygal/tpl.py:211 #, python-format msgid "Theme %s does not have a %s" msgstr "" #: ../lazygal/tpl.py:214 #, python-format msgid "Theme %s : %s parsing error" msgstr "" #: ../lazygal/genfile.py:106 #, python-format msgid " ZIP %s" msgstr "" #: ../lazygal/genfile.py:128 #, python-format msgid "CP %%SHAREDDIR%%/%s" msgstr "" #: ../lazygal/genmedia.py:81 #, python-format msgid " %s is BROKEN, skipped" msgstr "" #: ../lazygal/genmedia.py:121 msgid "RESIZE" msgstr "" #: ../lazygal/genmedia.py:209 #, python-format msgid "Could not copy metadata in reduced picture: %s" msgstr "" #: ../lazygal/genmedia.py:222 msgid "VIDEOTHUMB" msgstr "" #: ../lazygal/genmedia.py:229 #, python-format msgid " creating %s thumbnail failed, skipped" msgstr "" #: ../lazygal/genmedia.py:262 #, python-format msgid "Supplied album picture %s does not exist." msgstr "" #: ../lazygal/genmedia.py:282 #, python-format msgid " DIRPIC %s" msgstr "" #: ../lazygal/genmedia.py:311 #, python-format msgid " TRANSCODE %s" msgstr "" #: ../lazygal/genmedia.py:320 #, python-format msgid " transcoding %s failed, skipped" msgstr "" #: ../lazygal/genpage.py:129 ../lazygal/genpage.py:266 #, python-format msgid " XHTML %s" msgstr "" #: ../lazygal/genpage.py:213 #, python-format msgid "" " Size '%s' is not available in '%s' due to configuration: medias won't be " "shown on index." msgstr "" #: ../lazygal/genpage.py:357 #, python-format msgid "FEED %s" msgstr "" #: ../lazygal/genpage.py:375 msgid "" "We have a template with an extension that does not start with a t. Aborting." msgstr "" #: ../lazygal/genpage.py:381 #, python-format msgid "TPL %%SHAREDDIR%%/%s" msgstr "" #: ../lazygal/config.py:125 #, python-format msgid " Ignoring unknown section '%s'." msgstr "" #: ../lazygal/config.py:129 #, python-format msgid " Ignoring unknown option '%s' in section '%s'." msgstr "" #: ../themes/default/dirindex.thtml:70 msgid "All full scale pictures as an archive, for" msgstr "" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 msgid "Generated by" msgstr "" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 #, python-format msgid "on %c" msgstr "" #: ../themes/default/browse.thtml:39 msgid "previous" msgstr "" #: ../themes/default/browse.thtml:44 msgid "next" msgstr "" #: ../themes/default/image.thtml:9 msgid "Taken" msgstr "" #: ../themes/default/image.thtml:9 msgid "on %d/%m/%Y at %H:%M" msgstr "" #: ../themes/default/image.thtml:11 msgid "Author" msgstr "" #: ../themes/default/image.thtml:12 msgid "Keywords" msgstr "" #: ../themes/default/image.thtml:13 msgid "Original picture" msgstr "" #: ../themes/default/image.thtml:17 msgid "Camera:" msgstr "" #: ../themes/default/image.thtml:18 msgid "with" msgstr "" #: ../themes/default/image.thtml:20 msgid "Exposure" msgstr "" #: ../themes/default/image.thtml:21 msgid "Sensitivity ISO" msgstr "" #: ../themes/default/image.thtml:22 msgid "Aperture" msgstr "" #: ../themes/default/image.thtml:23 msgid "Flash" msgstr "" #: ../themes/default/image.thtml:24 msgid "Focal length" msgstr "" #: ../themes/default/video.thtml:6 msgid "Original video" msgstr "" #: ../themes/default/feeditem.thtml:5 ../themes/default/gallerylink.thtml:21 #: ../themes/image-index/gallerylink.thtml:30 msgid "sub-galleries" msgstr "" #: ../themes/default/feeditem.thtml:6 ../themes/default/gallerylink.thtml:17 #: ../themes/image-index/gallerylink.thtml:26 msgid "photos" msgstr "" lazygal-0.8.2/locale/POTFILES.in0000644000175000017500000000057212301071671016126 0ustar niolniol00000000000000lazygal.py lazygal/sourcetree.py lazygal/generators.py lazygal/metadata.py lazygal/tpl.py lazygal/genfile.py lazygal/genmedia.py lazygal/genpage.py lazygal/config.py themes/default/dirindex.thtml themes/default/browse.thtml themes/default/image.thtml themes/default/video.thtml themes/default/feeditem.thtml themes/default/gallerylink.thtml themes/image-index/gallerylink.thtml lazygal-0.8.2/locale/it_IT.po0000644000175000017500000004163412301072370015722 0ustar niolniol00000000000000# Italian translation of lazygal. # Copyright (C) 2011 lazygal'S COPYRIGHT HOLDER # This file is distributed under the same license as the lazygal package. # Federico Bruni , 2011. # msgid "" msgstr "" "Project-Id-Version: lazygal 0.6\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-19 10:10+0100\n" "PO-Revision-Date: 2011-06-25 10:16+0200\n" "Last-Translator: Federico Bruni \n" "Language-Team: Federico Bruni \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: ../lazygal.py:47 msgid "usage: %prog [options] albumdir" msgstr "uso: %prog [opzioni] albumdir" #. The help option must be changed to comply with i18n. #: ../lazygal.py:51 msgid "Show this help message and exit." msgstr "Mostra questo messaggio di aiuto ed esce." #: ../lazygal.py:56 msgid "Don't output anything except for errors." msgstr "Non crea alcun output eccetto gli errori." #: ../lazygal.py:60 msgid "Output everything that lazygal is doing." msgstr "Mostra nell'output tutto quello che lazygal sta facendo." #: ../lazygal.py:64 msgid "" "Directory where web pages, slides and thumbs will be written (default is " "current directory)." msgstr "" "Directory in cui verranno salvate le pagine web, le immagini e le miniature " "(la directory predefinita è quella corrente)." #: ../lazygal.py:68 msgid "Theme name (looked up in theme directory) or theme full path." msgstr "" "Nome del tema (ricercato nella directory dei temi) o percorso completo al " "tema." #: ../lazygal.py:72 msgid "Default style to apply to the theme." msgstr "Stile predefinito da applicare al tema." #: ../lazygal.py:76 msgid "Common variables to load all templates with." msgstr "Variabili comuni con cui caricare tutti i modelli." #: ../lazygal.py:80 msgid "Force rebuild of all pages." msgstr "" #: ../lazygal.py:84 msgid "Clean destination directory of files that should not be there." msgstr "" "Directory vuota di destinazione dei file che non dovrebbero trovarsi là." #: ../lazygal.py:88 msgid "Display program version." msgstr "Mostra la versione del programma." #: ../lazygal.py:92 msgid "" "Exhaustively go through all directories regardless of source modification " "time." msgstr "" "Passa in rassegna approfonditamente tutte le directory senza tener conto " "dell'ora di modifica del sorgente." #: ../lazygal.py:96 msgid "" "Level below which the directory tree is flattened. Default is 'No' which " "disables this feature." msgstr "" "Livello al di sotto del quale l'albero delle directory viene appiattito. Il " "valore predefinito, \"No\", disabilita questa funzione." #: ../lazygal.py:100 msgid "" "Size of images, define as =SIZE,..., eg. small=800x600," "medium=1024x768. The special value 0x0 uses original size. See manual page " "for SIZE syntax." msgstr "" "Dimensione delle immagini, si definisce con =DIMENSIONE,..., ad " "esempio small=800x600,medium=1024x768. Il valore speciale 0x0 usa la " "dimensione originale. Si veda la pagina di manuale relativa alla sintassi di " "DIMENSIONE." #: ../lazygal.py:104 msgid "" "Size of thumbnails, define as SIZE, eg. 150x113. See manual page for SIZE " "syntax." msgstr "" "Dimensione delle miniature, si definisce con DIMENSIONE, ad esempio 150x113. " "Si veda la pagina di manuale relativa alla sintassi di DIMENSIONE." #: ../lazygal.py:108 msgid "Quality of generated JPEG images (default is 85)." msgstr "Qualità delle immagini JPEG generate (il valore predefinito è 85)." #: ../lazygal.py:112 msgid "Include original photos in output." msgstr "Include le foto originali nell'output." #: ../lazygal.py:116 msgid "" "Do not copy original photos in output directory, instead link them using " "submitted relative path as base." msgstr "" "Invece di copiare le foto originali nella directory di output, crea dei link " "usando come base il percorso relativo inserito." #: ../lazygal.py:120 msgid "" "Do not copy original photos in output directory, instead create symlinks to " "their original locations." msgstr "" "Invece di copiare le foto originali nella directory di output, crea dei link " "usando come base il percorso relativo inserito." #: ../lazygal.py:124 msgid "Publication URL (only useful for feed generation)." msgstr "URL di pubblicazione (utile solo per la generazione di feed)." #: ../lazygal.py:128 msgid "" "Generate metadata description files where they don't exist instead of " "generating the web gallery." msgstr "" "Genera i i file di descrizione dei metadati dove non esistono, invece di " "generare la galleria web." #: ../lazygal.py:132 msgid "" "Maximum number of thumbs per index page. This enables index pagination (0 is " "unlimited)." msgstr "Numero massimo di miniature per pagina. " #: ../lazygal.py:136 msgid "Make a zip archive of original pictures for each directory." msgstr "Crea per ogni directory un archivio zip delle immagini originali." #: ../lazygal.py:140 msgid "" "Webalbum picture background color. Default is transparent, and implies the " "PNG format. Any other value, e.g. red, white, blue, uses JPEG." msgstr "" "Colore di sfondo dell'immagine dell'album. Il valore predefinito è " "trasparente e implica il formato PNG. Qualsiasi altro valore, ad esempio " "rosso, bianco, blu, usa il formato JPEG." #: ../lazygal.py:145 msgid "Webalbum picture type. Default is messy." msgstr "" #: ../lazygal.py:147 ../lazygal.py:150 msgid "ORDER" msgstr "ORDINAMENTO" #: ../lazygal.py:148 msgid "" "Sort order for images in a folder: filename, mtime, or exif. Add ':reverse' " "to reverse the chosen order." msgstr "" "Stabilisce il criterio d'ordine per le immagini in una cartella. I valori " "possibili sono: nome del file ('filename'), data di modifica del file " "('mtime') o dati della macchina fotografica ('exif'). Aggiungere ':reverse' " "per invertire l'ordine scelto." #: ../lazygal.py:151 #, fuzzy msgid "" "Sort order for sub galleries in a folder: dirname, exif or mtime. Add ':" "reverse' to reverse the chosen order." msgstr "" "Stabilisce il criterio d'ordine per le sottogallerie in una cartella. I " "possibili valori sono: nome della cartella ('dirname'), data di modifica del " "file ('mtime'). Aggiungendo \":reverse\" si inverte l'ordine scelto." #: ../lazygal.py:153 msgid "TAG" msgstr "" #: ../lazygal.py:154 msgid "" "Only include in the gallery pics whose IPTC keywords match the supplied " "filter(s)." msgstr "" #: ../lazygal.py:158 msgid "" "Do not remove GPS location tags from EXIF metadata. Mostly useful with " "holiday photos." msgstr "" #: ../lazygal.py:162 #, python-format msgid "lazygal version %s" msgstr "lazygal versione %s" #: ../lazygal.py:167 msgid "Bad command line." msgstr "Errore nella linea di comando." #: ../lazygal.py:171 #, python-format msgid "Directory %s does not exist." msgstr "La directory %s non esiste." #: ../lazygal.py:226 msgid "Option --orig-symlink is not available on this platform." msgstr "L'opzione --orig-symlink non è disponibile in questa piattaforma." #: ../lazygal.py:275 msgid "Interrupted." msgstr "Interrotto." #: ../lazygal/sourcetree.py:90 ../lazygal/sourcetree.py:325 msgid "Root not found" msgstr "La directory radice non è stata trovata" #: ../lazygal/sourcetree.py:281 #, python-format msgid " Ignoring %s, cannot open file (broken symlink?)." msgstr "" #: ../lazygal/sourcetree.py:292 #, python-format msgid " Ignoring %s, format not supported." msgstr " Si ignora %s, formato non supportato." #: ../lazygal/generators.py:49 msgid "Could not find themes dir, check your installation!" msgstr "" "Impossibile trovare la directory dei temi, controllare la propria " "installazione!" #: ../lazygal/generators.py:67 msgid " SORTING pics and subdirs" msgstr " ORDINAMENTO delle immagini e delle sottodirectory" #: ../lazygal/generators.py:80 ../lazygal/generators.py:92 #, python-format msgid "Unknown sorting criterion '%s'" msgstr "Criterio di ordine \"%s\" sconosciuto" #: ../lazygal/generators.py:125 msgid " BREAKING web gallery into multiple pages" msgstr " IMPAGINAZIONE della galleria web" #: ../lazygal/generators.py:371 #, python-format msgid " MKDIR %%WEBALBUMROOT%%/%s" msgstr " MKDIR %%WEBALBUMROOT%%/%s" #: ../lazygal/generators.py:409 msgid "" "Sizes is a comma-separated list of size names and specs:\n" "\t e.g. \"small=640x480,medium=1024x768\"." msgstr "" "Le dimensioni si indicano in una lista di nomi e specifiche di dimensione " "separati dalla virgola:\n" "\tes: \"small=640x480,medium=1024x768\"." #: ../lazygal/generators.py:411 #, python-format msgid "Size name '%s' is reserved for internal processing." msgstr "" "Il nome della dimensione \"%s\" è riservato per l'elaborazione interna." #: ../lazygal/generators.py:424 #, python-format msgid "'%s' for size '%s' does not describe a known size syntax." msgstr "" "\"%s\" per la dimensione \"%s\" non descrive una sintassi di dimensione " "conosciuta." #: ../lazygal/generators.py:458 #, python-format msgid " Trying loading gallery configs: %s" msgstr "" #: ../lazygal/generators.py:501 msgid "Bad syntax for webalbumpic-size." msgstr "" #: ../lazygal/generators.py:690 msgid "MKDIR %SHAREDDIR%" msgstr "MKDIR %SHAREDDIR%" #: ../lazygal/generators.py:749 #, python-format msgid "Progress: dir %d/%d (%d%%), media %d/%d (%d%%)" msgstr "" #: ../lazygal/generators.py:757 #, python-format msgid ", current task %d%%" msgstr "" #: ../lazygal/generators.py:772 #, python-format msgid "Trying loading user config %s" msgstr "" #: ../lazygal/generators.py:777 #, python-format msgid "Loading root config %s" msgstr "" #: ../lazygal/generators.py:781 #, python-format msgid "" "'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page." msgstr "" #: ../lazygal/generators.py:825 #, python-format msgid "Generating metadata in %s" msgstr "Generazione dei metadati in %s" #: ../lazygal/generators.py:831 ../lazygal/generators.py:890 #, python-format msgid "[Entering %%ALBUMROOT%%/%s]" msgstr "[Ingresso in %%ALBUMROOT%%/%s]" #: ../lazygal/generators.py:861 msgid "Fatal error, web gallery directory is within source tree." msgstr "" "Errore fatale, la directory della galleria web si trova dentro l'albero dei " "sorgenti." #: ../lazygal/generators.py:863 #, python-format msgid "Generating to %s" msgstr "Generazione in %s" #: ../lazygal/generators.py:883 #, python-format msgid "(%s) has been skipped" msgstr "(%s) è stato ignorato" #: ../lazygal/generators.py:887 #, python-format msgid "" "(%s) has been skipped because its name collides with the shared material " "directory name" msgstr "" "(%s) è stato ignorato perché il suo nome è in conflitto col nome della " "directory del materiale condiviso" #: ../lazygal/generators.py:928 msgid "" " SKIPPED because of mtime, touch source or use --check-all-dirs to override." msgstr "" " IGNORATO a causa della data di modifica (mtime): usare \"touch file\" o " "l'opzione --check-all-dirs per scavalcare l'impostazione predefinita." #: ../lazygal/generators.py:937 #, python-format msgid "[Leaving %%ALBUMROOT%%/%s]" msgstr "[Uscita da %%ALBUMROOT%%/%s]" #: ../lazygal/metadata.py:327 ../lazygal/metadata.py:355 #, python-format msgid " (35 mm equivalent: %s mm)" msgstr " (equivalente a 35 mm: %s mm)" #: ../lazygal/metadata.py:447 #, python-format msgid "Could not open metadata file %s" msgstr "Impossibile aprire il file dei metadati %s" #: ../lazygal/metadata.py:553 msgid " SKIPPED because metadata exists." msgstr " IGNORATO perché i metadati esistono già." #: ../lazygal/metadata.py:555 msgid " SKIPPED because directory does not contain images." msgstr " IGNORATO perché la directory non contiene immagini." #: ../lazygal/metadata.py:564 #, python-format msgid "GEN %s" msgstr "GEN %s" #: ../lazygal/tpl.py:162 #, python-format msgid "Unknown template type for %s" msgstr "Tipo di modello sconosciuto per %s" #: ../lazygal/tpl.py:176 #, python-format msgid "Theme %s not found" msgstr "Tema %s non trovato" #: ../lazygal/tpl.py:203 #, python-format msgid "Loading %s for theme %s" msgstr "" #: ../lazygal/tpl.py:211 #, python-format msgid "Theme %s does not have a %s" msgstr "" #: ../lazygal/tpl.py:214 #, python-format msgid "Theme %s : %s parsing error" msgstr "" #: ../lazygal/genfile.py:106 #, python-format msgid " ZIP %s" msgstr " ZIP %s" #: ../lazygal/genfile.py:128 #, python-format msgid "CP %%SHAREDDIR%%/%s" msgstr "CP %%SHAREDDIR%%/%s" #: ../lazygal/genmedia.py:81 #, python-format msgid " %s is BROKEN, skipped" msgstr " %s è ROTTO, si ignora" #: ../lazygal/genmedia.py:121 #, fuzzy msgid "RESIZE" msgstr " RIDIMENSIONA %s" #: ../lazygal/genmedia.py:209 #, python-format msgid "Could not copy metadata in reduced picture: %s" msgstr "Impossibile copiare i metadati nell'immagine ridotta: %s" #: ../lazygal/genmedia.py:222 msgid "VIDEOTHUMB" msgstr "" #: ../lazygal/genmedia.py:229 #, fuzzy, python-format msgid " creating %s thumbnail failed, skipped" msgstr " errore nella transcodifica di %s, si ignora" #: ../lazygal/genmedia.py:262 #, python-format msgid "Supplied album picture %s does not exist." msgstr "L'immagine fornita per l'album %s non esiste." #: ../lazygal/genmedia.py:282 #, python-format msgid " DIRPIC %s" msgstr " DIRPIC %s" #: ../lazygal/genmedia.py:311 #, python-format msgid " TRANSCODE %s" msgstr " TRANSCODIFICA %s" #: ../lazygal/genmedia.py:320 #, python-format msgid " transcoding %s failed, skipped" msgstr " errore nella transcodifica di %s, si ignora" #: ../lazygal/genpage.py:129 ../lazygal/genpage.py:266 #, python-format msgid " XHTML %s" msgstr " XHTML %s" #: ../lazygal/genpage.py:213 #, python-format msgid "" " Size '%s' is not available in '%s' due to configuration: medias won't be " "shown on index." msgstr "" #: ../lazygal/genpage.py:357 #, python-format msgid "FEED %s" msgstr "FEED %s" #: ../lazygal/genpage.py:375 msgid "" "We have a template with an extension that does not start with a t. Aborting." msgstr "C'è un modello con un estensione che non inizia con una t. Interrotto." #: ../lazygal/genpage.py:381 #, python-format msgid "TPL %%SHAREDDIR%%/%s" msgstr "TPL %%SHAREDDIR%%/%s" #: ../lazygal/config.py:125 #, python-format msgid " Ignoring unknown section '%s'." msgstr "" #: ../lazygal/config.py:129 #, python-format msgid " Ignoring unknown option '%s' in section '%s'." msgstr "" #: ../themes/default/dirindex.thtml:70 msgid "All full scale pictures as an archive, for" msgstr "Tutte le immagini ad alta risoluzione in un archivio, per" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 msgid "Generated by" msgstr "Generato da" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 #, python-format msgid "on %c" msgstr "" #: ../themes/default/browse.thtml:39 msgid "previous" msgstr "" #: ../themes/default/browse.thtml:44 msgid "next" msgstr "" #: ../themes/default/image.thtml:9 msgid "Taken" msgstr "Scattata" #: ../themes/default/image.thtml:9 msgid "on %d/%m/%Y at %H:%M" msgstr "il %d/%m/%Y alle %H:%M" #: ../themes/default/image.thtml:11 msgid "Author" msgstr "Autore" #: ../themes/default/image.thtml:12 msgid "Keywords" msgstr "" #: ../themes/default/image.thtml:13 msgid "Original picture" msgstr "Immagine originale" #: ../themes/default/image.thtml:17 msgid "Camera:" msgstr "Fotocamera:" #: ../themes/default/image.thtml:18 msgid "with" msgstr "con" #: ../themes/default/image.thtml:20 msgid "Exposure" msgstr "Esposizione" #: ../themes/default/image.thtml:21 msgid "Sensitivity ISO" msgstr "Sensibilità ISO" #: ../themes/default/image.thtml:22 msgid "Aperture" msgstr "Apertura" #: ../themes/default/image.thtml:23 msgid "Flash" msgstr "Flash" #: ../themes/default/image.thtml:24 msgid "Focal length" msgstr "Lunghezza focale" #: ../themes/default/video.thtml:6 msgid "Original video" msgstr "Video originale" #: ../themes/default/feeditem.thtml:5 ../themes/default/gallerylink.thtml:21 #: ../themes/image-index/gallerylink.thtml:30 msgid "sub-galleries" msgstr "sottogallerie" #: ../themes/default/feeditem.thtml:6 ../themes/default/gallerylink.thtml:17 #: ../themes/image-index/gallerylink.thtml:26 msgid "photos" msgstr "foto" #, fuzzy #~ msgid "you should " #~ msgstr "dovresti" #, fuzzy #~ msgid " %sRM %s" #~ msgstr " %s RM %s" #~ msgid "" #~ "Not following symlink '%s' because directory has already been processed." #~ msgstr "" #~ "Il symlink \"%s\" non viene seguito perché la directory è già stata " #~ "elaborata." #~ msgid "(%s) and childs have no known medias, skipped" #~ msgstr "" #~ "(%s) e i suoi elementi figli non hanno dei media conosciuti, si ignora" #~ msgid "on" #~ msgstr "il" #~ msgid "Run an extra optimization pass on each image." #~ msgstr "Esegue un passaggio ulteriore di ottimizzazione su ogni immagine." #~ msgid "Generate Progressive JPEG images." #~ msgstr "Genera immagini JPEG progressive." #~ msgid "'%s' for thumb size does not describe a known size syntax." #~ msgstr "" #~ "\"%s\" per la dimensione delle miniature non descrive una sintassi di " #~ "dimensione conosciuta." #~ msgid "Parent" #~ msgstr "Salire di un livello" #~ msgid "Gallery index" #~ msgstr "Indice della galleria" #~ msgid "Using" #~ msgstr "Usando" lazygal-0.8.2/locale/cs_CZ.po0000644000175000017500000004175312301072371015716 0ustar niolniol00000000000000# Czech translation of lazygal. # Copyright (C) 2008 THE lazygal'S COPYRIGHT HOLDER # This file is distributed under the same license as the lazygal package. # , fuzzy # Michal Čihař , 2008, 2011, 2014. # msgid "" msgstr "" "Project-Id-Version: lazygal 0.4\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-19 10:10+0100\n" "PO-Revision-Date: 2014-02-10 11:26+0100\n" "Last-Translator: Michal Čihař \n" "Language-Team: Czech \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Gtranslator 2.91.6\n" #: ../lazygal.py:47 msgid "usage: %prog [options] albumdir" msgstr "použití: %prog [parametry] adresářsalbem" #. The help option must be changed to comply with i18n. #: ../lazygal.py:51 msgid "Show this help message and exit." msgstr "Zobrazí tuto nápovědu a ukončí program." #: ../lazygal.py:56 msgid "Don't output anything except for errors." msgstr "Nebude vypisovat nic kromě chyb." #: ../lazygal.py:60 msgid "Output everything that lazygal is doing." msgstr "Vypsat vše, co lazygal provádí." #: ../lazygal.py:64 msgid "" "Directory where web pages, slides and thumbs will be written (default is " "current directory)." msgstr "" "Adresář, kam budou zapsány webové stránky, obrázky a náhledy (výchozí je " "aktuální adresář)." #: ../lazygal.py:68 msgid "Theme name (looked up in theme directory) or theme full path." msgstr "" "Jméno tématu (vyhledává se v adresáři s tématy) nebo celá cesta k tématu." #: ../lazygal.py:72 msgid "Default style to apply to the theme." msgstr "Výchozí styl použitý pro téma." #: ../lazygal.py:76 msgid "Common variables to load all templates with." msgstr "Společné proměnné, které budou předány šablonám." #: ../lazygal.py:80 msgid "Force rebuild of all pages." msgstr "Vynutit nové sestavení všech stránek." #: ../lazygal.py:84 msgid "Clean destination directory of files that should not be there." msgstr "Vymazat z výstupního adresáře soubory, které v něm nemají být." #: ../lazygal.py:88 msgid "Display program version." msgstr "Zobrazí verzi programu." #: ../lazygal.py:92 msgid "" "Exhaustively go through all directories regardless of source modification " "time." msgstr "Projít všechny adresáře bez ohledu na jejich čas změny." #: ../lazygal.py:96 msgid "" "Level below which the directory tree is flattened. Default is 'No' which " "disables this feature." msgstr "" "Úroveň pod kterou se již nebudou vytvářet podadresáře. Výchozí 'No' znamená " "neomezené vytváření podadresářů." #: ../lazygal.py:100 msgid "" "Size of images, define as =SIZE,..., eg. small=800x600," "medium=1024x768. The special value 0x0 uses original size. See manual page " "for SIZE syntax." msgstr "" "Velikost obrázků, zadejte v podobě =x,..., např. male=800x600," "stredni=1024x768. Při zadání 0x0 bude použit původní obrázek beze změny " "velikosti. Více informací o syntaxi naleznete v manuálové stránce." #: ../lazygal.py:104 msgid "" "Size of thumbnails, define as SIZE, eg. 150x113. See manual page for SIZE " "syntax." msgstr "" "Velikost náhledů, zadejte v podobě x, např. 150x113. Více informací o " "syntaxi naleznete v manuálové stránce." #: ../lazygal.py:108 msgid "Quality of generated JPEG images (default is 85)." msgstr "Kvalita generovaných JPEG obrázků (výchozí je 85)." #: ../lazygal.py:112 msgid "Include original photos in output." msgstr "Zahrnout původní obrázky ve výstupu." #: ../lazygal.py:116 msgid "" "Do not copy original photos in output directory, instead link them using " "submitted relative path as base." msgstr "" "Nekopírovat původní obrázky do výstupu, jen vytvořit sumbolické odkazy " "pomocí zadané relativní cesty." #: ../lazygal.py:120 msgid "" "Do not copy original photos in output directory, instead create symlinks to " "their original locations." msgstr "" "Nekopírovat původní obrázky do výstupu, jen vytvořit symbolické odkazy na " "jejich původní umístění." #: ../lazygal.py:124 msgid "Publication URL (only useful for feed generation)." msgstr "URL, kde bude album generováno (je potřebné pro vytváření RSS zdrojů)." #: ../lazygal.py:128 msgid "" "Generate metadata description files where they don't exist instead of " "generating the web gallery." msgstr "" "Namísto vytváření galerie vytvoří soubory pro popisky alb tam, kde " "neexistují." #: ../lazygal.py:132 msgid "" "Maximum number of thumbs per index page. This enables index pagination (0 is " "unlimited)." msgstr "" "Maximální počet náhledů na stránce obsahu. Tímto vynutíte stránkování obsahu " "(0 znamená žádné omezení)." #: ../lazygal.py:136 msgid "Make a zip archive of original pictures for each directory." msgstr "Vytvořit zip archív s původními obrázky pro každý adresář." #: ../lazygal.py:140 msgid "" "Webalbum picture background color. Default is transparent, and implies the " "PNG format. Any other value, e.g. red, white, blue, uses JPEG." msgstr "" "Pozadí obrázku alba. Výchozí je průhledné a obrázek pak bude uložen jako " "PNG. Jakákoliv jiná barva (např. red, white, blue) použije JPEG." #: ../lazygal.py:145 msgid "Webalbum picture type. Default is messy." msgstr "Typ obrázku pro album. Výchozí je messy." #: ../lazygal.py:147 ../lazygal.py:150 msgid "ORDER" msgstr "POŘADÍ" #: ../lazygal.py:148 msgid "" "Sort order for images in a folder: filename, mtime, or exif. Add ':reverse' " "to reverse the chosen order." msgstr "" "Pořadí obrázků v adresáři: filename, mtime nebo exif. Přidejte \":reverse\" " "pro otočení pořadí." #: ../lazygal.py:151 msgid "" "Sort order for sub galleries in a folder: dirname, exif or mtime. Add ':" "reverse' to reverse the chosen order." msgstr "" "Pořadí alb v adresáři: dirname, filename nebo mtime. Přidejte \":reverse\" " "pro otočení pořadí." #: ../lazygal.py:153 msgid "TAG" msgstr "ZNAČKY" #: ../lazygal.py:154 msgid "" "Only include in the gallery pics whose IPTC keywords match the supplied " "filter(s)." msgstr "" "Zahrnout jen obrázky jejich IPTC značky se schodují se zadaným filtrem." #: ../lazygal.py:158 msgid "" "Do not remove GPS location tags from EXIF metadata. Mostly useful with " "holiday photos." msgstr "Neodstraňovat GPS souřadnice z EXIFu. Užitečné pro fotky z dovolených." #: ../lazygal.py:162 #, python-format msgid "lazygal version %s" msgstr "lazygal verze %s" #: ../lazygal.py:167 msgid "Bad command line." msgstr "Chybná příkazová řádka." #: ../lazygal.py:171 #, python-format msgid "Directory %s does not exist." msgstr "Adresář %s neexistuje." #: ../lazygal.py:226 msgid "Option --orig-symlink is not available on this platform." msgstr "Volba --orig-symlink není na této platformě dostupná." #: ../lazygal.py:275 msgid "Interrupted." msgstr "Přerušeno." #: ../lazygal/sourcetree.py:90 ../lazygal/sourcetree.py:325 msgid "Root not found" msgstr "Kořenový adresář nebyl nalezen" #: ../lazygal/sourcetree.py:281 #, python-format msgid " Ignoring %s, cannot open file (broken symlink?)." msgstr " Přeskakuji %s, nelze otevřít soubor (poškozený odkaz?)." #: ../lazygal/sourcetree.py:292 #, python-format msgid " Ignoring %s, format not supported." msgstr " Ignoruji %s, formát není podporovaný." #: ../lazygal/generators.py:49 msgid "Could not find themes dir, check your installation!" msgstr "Nepodařilo se nalézt adresář s tématy, zkontrolujte vaší instalaci!" #: ../lazygal/generators.py:67 msgid " SORTING pics and subdirs" msgstr " ŘADÍM obrázky a podadresáře" #: ../lazygal/generators.py:80 ../lazygal/generators.py:92 #, python-format msgid "Unknown sorting criterion '%s'" msgstr "Neznámý typ řazení '%s'" #: ../lazygal/generators.py:125 msgid " BREAKING web gallery into multiple pages" msgstr " ROZDĚLUJI galerii do více stránek" #: ../lazygal/generators.py:371 #, python-format msgid " MKDIR %%WEBALBUMROOT%%/%s" msgstr " ADRESÁŘ %%WEBALBUMROOT%%/%s" #: ../lazygal/generators.py:409 msgid "" "Sizes is a comma-separated list of size names and specs:\n" "\t e.g. \"small=640x480,medium=1024x768\"." msgstr "" "Velikosti jsou čárkou oddělený seznam názvů a jejich definic:\n" "\t např. \"small=640x480,medium=1024x768\"." #: ../lazygal/generators.py:411 #, python-format msgid "Size name '%s' is reserved for internal processing." msgstr "Velikost pojmenovaná '%s' je rezervovaná pro vnitřní potřebu." #: ../lazygal/generators.py:424 #, python-format msgid "'%s' for size '%s' does not describe a known size syntax." msgstr "'%s' pro velikost '%s' není platnou syntaxí velikosti." #: ../lazygal/generators.py:458 #, python-format msgid " Trying loading gallery configs: %s" msgstr "..Načítám nastavení galerie: %s" #: ../lazygal/generators.py:501 msgid "Bad syntax for webalbumpic-size." msgstr "Chybná syntaxe pro webalbumpic-size." #: ../lazygal/generators.py:690 msgid "MKDIR %SHAREDDIR%" msgstr "ADRESÁŘ %SHAREDDIR%" #: ../lazygal/generators.py:749 #, python-format msgid "Progress: dir %d/%d (%d%%), media %d/%d (%d%%)" msgstr "Průběh: adresář %d/%d (%d%%), média %d/%d (%d%%)" #: ../lazygal/generators.py:757 #, python-format msgid ", current task %d%%" msgstr ", současná úloha %d%%" #: ../lazygal/generators.py:772 #, python-format msgid "Trying loading user config %s" msgstr "Načítám uživatelské nastavení %s" #: ../lazygal/generators.py:777 #, python-format msgid "Loading root config %s" msgstr "Načítám hlavní nastavení %s" #: ../lazygal/generators.py:781 #, python-format msgid "" "'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page." msgstr "" "'%s' používá zastaralou syntaxi: prosím přečtěte si manuálovou stránku " "lazygal.conf(5)." #: ../lazygal/generators.py:825 #, python-format msgid "Generating metadata in %s" msgstr "Vytvářím popisky v %s" #: ../lazygal/generators.py:831 ../lazygal/generators.py:890 #, python-format msgid "[Entering %%ALBUMROOT%%/%s]" msgstr "[Vstupuji do %%ALBUMROOT%%/%s]" #: ../lazygal/generators.py:861 msgid "Fatal error, web gallery directory is within source tree." msgstr "Chyba, adresář pro uložení alba je ve zdrojovém adresáři." #: ../lazygal/generators.py:863 #, python-format msgid "Generating to %s" msgstr "Generuji do %s" #: ../lazygal/generators.py:883 #, python-format msgid "(%s) has been skipped" msgstr "(%s) byl přeskočen" #: ../lazygal/generators.py:887 #, python-format msgid "" "(%s) has been skipped because its name collides with the shared material " "directory name" msgstr "" "(%s) byl přeskočen protože jeho jméno je totožné se jménem sdíleného adresáře" #: ../lazygal/generators.py:928 msgid "" " SKIPPED because of mtime, touch source or use --check-all-dirs to override." msgstr "" " PŘESKOČENO kvůli mtime, pro zpracování použijte buď touch, nebo parametr --" "check-all-dirs." #: ../lazygal/generators.py:937 #, python-format msgid "[Leaving %%ALBUMROOT%%/%s]" msgstr "[Opouštím %%ALBUMROOT%%/%s]" #: ../lazygal/metadata.py:327 ../lazygal/metadata.py:355 #, python-format msgid " (35 mm equivalent: %s mm)" msgstr " (ekvivalent 35 mm: %s mm)" #: ../lazygal/metadata.py:447 #, python-format msgid "Could not open metadata file %s" msgstr "Nepodařilo se otevřít soubor s popisky %s" #: ../lazygal/metadata.py:553 msgid " SKIPPED because metadata exists." msgstr " PŘESKOČENO protože metadata existují." #: ../lazygal/metadata.py:555 msgid " SKIPPED because directory does not contain images." msgstr " PŘESKOČENO protože adresář neobsahuje žádné obrázky." #: ../lazygal/metadata.py:564 #, python-format msgid "GEN %s" msgstr "GEN %s" #: ../lazygal/tpl.py:162 #, python-format msgid "Unknown template type for %s" msgstr "Neznámý typ šablony pro %s" #: ../lazygal/tpl.py:176 #, python-format msgid "Theme %s not found" msgstr "Téma %s nebylo nalezeno" #: ../lazygal/tpl.py:203 #, python-format msgid "Loading %s for theme %s" msgstr "Načítám %s pro téma %s" #: ../lazygal/tpl.py:211 #, python-format msgid "Theme %s does not have a %s" msgstr "Téma %s pro neobsahuje %s" #: ../lazygal/tpl.py:214 #, python-format msgid "Theme %s : %s parsing error" msgstr "Téma %s: chyba při zpracování %s" #: ../lazygal/genfile.py:106 #, python-format msgid " ZIP %s" msgstr " ZIP %s" #: ../lazygal/genfile.py:128 #, python-format msgid "CP %%SHAREDDIR%%/%s" msgstr "CP %%SHAREDDIR%%/%s" #: ../lazygal/genmedia.py:81 #, python-format msgid " %s is BROKEN, skipped" msgstr " %s je ROZBITÝ, přeskoče" #: ../lazygal/genmedia.py:121 msgid "RESIZE" msgstr "ZMENŠIT" #: ../lazygal/genmedia.py:209 #, python-format msgid "Could not copy metadata in reduced picture: %s" msgstr "Nepodařilo se zkopírovat metadata do zmenšeného obrázku: %s" #: ../lazygal/genmedia.py:222 msgid "VIDEOTHUMB" msgstr "VIDEONÁHLED" #: ../lazygal/genmedia.py:229 #, python-format msgid " creating %s thumbnail failed, skipped" msgstr " vytváření náhledu pro %s selhalo, přeskakuji" #: ../lazygal/genmedia.py:262 #, python-format msgid "Supplied album picture %s does not exist." msgstr "Zadaný obrázek alba %s neexistuje." #: ../lazygal/genmedia.py:282 #, python-format msgid " DIRPIC %s" msgstr " ADROBR %s" #: ../lazygal/genmedia.py:311 #, python-format msgid " TRANSCODE %s" msgstr " PŘEVÉST %s" #: ../lazygal/genmedia.py:320 #, python-format msgid " transcoding %s failed, skipped" msgstr " převedení %s selhalo, přeskakuji" #: ../lazygal/genpage.py:129 ../lazygal/genpage.py:266 #, python-format msgid " XHTML %s" msgstr " XHTML %s" #: ../lazygal/genpage.py:213 #, python-format msgid "" " Size '%s' is not available in '%s' due to configuration: medias won't be " "shown on index." msgstr "" " Velikost '%s' není v '%s' dostupná kvůli nastavení: média nebudou " "zobrazena v seznamu." #: ../lazygal/genpage.py:357 #, python-format msgid "FEED %s" msgstr "ZDROJ %s" #: ../lazygal/genpage.py:375 msgid "" "We have a template with an extension that does not start with a t. Aborting." msgstr "Nalezena šablona s příponou, která nezačíná na t. Končím." #: ../lazygal/genpage.py:381 #, python-format msgid "TPL %%SHAREDDIR%%/%s" msgstr "ŠAB %%SHAREDDIR%%/%s" #: ../lazygal/config.py:125 #, python-format msgid " Ignoring unknown section '%s'." msgstr " Přeskakuji neznámou sekci '%s'." #: ../lazygal/config.py:129 #, python-format msgid " Ignoring unknown option '%s' in section '%s'." msgstr " Přeskakuji neznámou volbu '%s' v sekci '%s'." #: ../themes/default/dirindex.thtml:70 msgid "All full scale pictures as an archive, for" msgstr "Všechny obrázky v původním rozlišení v archívu pro" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 msgid "Generated by" msgstr "Vygeneroval" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 #, python-format msgid "on %c" msgstr "v %c" #: ../themes/default/browse.thtml:39 msgid "previous" msgstr "předchozí" #: ../themes/default/browse.thtml:44 msgid "next" msgstr "následující" #: ../themes/default/image.thtml:9 msgid "Taken" msgstr "Vyfoceno" #: ../themes/default/image.thtml:9 msgid "on %d/%m/%Y at %H:%M" msgstr "dne %d/%m/%Y v %H:%M" #: ../themes/default/image.thtml:11 msgid "Author" msgstr "Autor" #: ../themes/default/image.thtml:12 msgid "Keywords" msgstr "Klíčová slova" #: ../themes/default/image.thtml:13 msgid "Original picture" msgstr "Původní obrázek" #: ../themes/default/image.thtml:17 msgid "Camera:" msgstr "Fotoaparát:" #: ../themes/default/image.thtml:18 msgid "with" msgstr "s" #: ../themes/default/image.thtml:20 msgid "Exposure" msgstr "Expozice" #: ../themes/default/image.thtml:21 msgid "Sensitivity ISO" msgstr "Citlivost ISO" #: ../themes/default/image.thtml:22 msgid "Aperture" msgstr "Clona" #: ../themes/default/image.thtml:23 msgid "Flash" msgstr "Blesk" #: ../themes/default/image.thtml:24 msgid "Focal length" msgstr "Ohnisková vzdálenost" #: ../themes/default/video.thtml:6 msgid "Original video" msgstr "Původní video" #: ../themes/default/feeditem.thtml:5 ../themes/default/gallerylink.thtml:21 #: ../themes/image-index/gallerylink.thtml:30 msgid "sub-galleries" msgstr "vložená alba" #: ../themes/default/feeditem.thtml:6 ../themes/default/gallerylink.thtml:17 #: ../themes/image-index/gallerylink.thtml:26 msgid "photos" msgstr "fotografie" #, fuzzy #~ msgid "you should " #~ msgstr "měli byste" #, fuzzy #~ msgid " %sRM %s" #~ msgstr " %s RM %s" #~ msgid "(%s) and childs have no known medias, skipped" #~ msgstr "(%s) a podadresáře neobsahují známá media, byl přeskočen" #~ msgid "on" #~ msgstr "dne" #~ msgid "Run an extra optimization pass on each image." #~ msgstr "Provést další optimalizaci pro každý obrázek." #~ msgid "Generate Progressive JPEG images." #~ msgstr "Vytvářet progresivní JPEG." #~ msgid "'%s' for thumb size does not describe a known size syntax." #~ msgstr "'%s' pro velikost náhledů není platnou syntaxí velikosti." #~ msgid "Parent" #~ msgstr "Nahoru" #~ msgid "Gallery index" #~ msgstr "Obsah alba" #~ msgid "Using" #~ msgstr "Použito" lazygal-0.8.2/locale/da_DK.po0000644000175000017500000003017612301072371015654 0ustar niolniol00000000000000# Danish translation of lazygal. # Copyright (C) 2008 THE lazygal'S COPYRIGHT HOLDER # This file is distributed under the same license as the lazygal package. # Thomas Damgaard , 2010. # # msgid "" msgstr "" "Project-Id-Version: lazygal 0.4\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-19 10:10+0100\n" "PO-Revision-Date: 2008-12-03 16:45+0100\n" "Last-Translator: Thomas Damgaard \n" "Language-Team: Thomas Damgaard \n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: ../lazygal.py:47 msgid "usage: %prog [options] albumdir" msgstr "" #. The help option must be changed to comply with i18n. #: ../lazygal.py:51 msgid "Show this help message and exit." msgstr "" #: ../lazygal.py:56 msgid "Don't output anything except for errors." msgstr "" #: ../lazygal.py:60 msgid "Output everything that lazygal is doing." msgstr "" #: ../lazygal.py:64 msgid "" "Directory where web pages, slides and thumbs will be written (default is " "current directory)." msgstr "" #: ../lazygal.py:68 msgid "Theme name (looked up in theme directory) or theme full path." msgstr "" #: ../lazygal.py:72 msgid "Default style to apply to the theme." msgstr "" #: ../lazygal.py:76 msgid "Common variables to load all templates with." msgstr "" #: ../lazygal.py:80 msgid "Force rebuild of all pages." msgstr "" #: ../lazygal.py:84 msgid "Clean destination directory of files that should not be there." msgstr "" #: ../lazygal.py:88 msgid "Display program version." msgstr "" #: ../lazygal.py:92 msgid "" "Exhaustively go through all directories regardless of source modification " "time." msgstr "" #: ../lazygal.py:96 msgid "" "Level below which the directory tree is flattened. Default is 'No' which " "disables this feature." msgstr "" #: ../lazygal.py:100 msgid "" "Size of images, define as =SIZE,..., eg. small=800x600," "medium=1024x768. The special value 0x0 uses original size. See manual page " "for SIZE syntax." msgstr "" #: ../lazygal.py:104 msgid "" "Size of thumbnails, define as SIZE, eg. 150x113. See manual page for SIZE " "syntax." msgstr "" #: ../lazygal.py:108 msgid "Quality of generated JPEG images (default is 85)." msgstr "" #: ../lazygal.py:112 msgid "Include original photos in output." msgstr "" #: ../lazygal.py:116 msgid "" "Do not copy original photos in output directory, instead link them using " "submitted relative path as base." msgstr "" #: ../lazygal.py:120 msgid "" "Do not copy original photos in output directory, instead create symlinks to " "their original locations." msgstr "" #: ../lazygal.py:124 msgid "Publication URL (only useful for feed generation)." msgstr "" #: ../lazygal.py:128 msgid "" "Generate metadata description files where they don't exist instead of " "generating the web gallery." msgstr "" #: ../lazygal.py:132 msgid "" "Maximum number of thumbs per index page. This enables index pagination (0 is " "unlimited)." msgstr "" #: ../lazygal.py:136 msgid "Make a zip archive of original pictures for each directory." msgstr "" #: ../lazygal.py:140 msgid "" "Webalbum picture background color. Default is transparent, and implies the " "PNG format. Any other value, e.g. red, white, blue, uses JPEG." msgstr "" #: ../lazygal.py:145 msgid "Webalbum picture type. Default is messy." msgstr "" #: ../lazygal.py:147 ../lazygal.py:150 msgid "ORDER" msgstr "" #: ../lazygal.py:148 msgid "" "Sort order for images in a folder: filename, mtime, or exif. Add ':reverse' " "to reverse the chosen order." msgstr "" #: ../lazygal.py:151 msgid "" "Sort order for sub galleries in a folder: dirname, exif or mtime. Add ':" "reverse' to reverse the chosen order." msgstr "" #: ../lazygal.py:153 msgid "TAG" msgstr "" #: ../lazygal.py:154 msgid "" "Only include in the gallery pics whose IPTC keywords match the supplied " "filter(s)." msgstr "" #: ../lazygal.py:158 msgid "" "Do not remove GPS location tags from EXIF metadata. Mostly useful with " "holiday photos." msgstr "" #: ../lazygal.py:162 #, python-format msgid "lazygal version %s" msgstr "" #: ../lazygal.py:167 msgid "Bad command line." msgstr "" #: ../lazygal.py:171 #, python-format msgid "Directory %s does not exist." msgstr "" #: ../lazygal.py:226 msgid "Option --orig-symlink is not available on this platform." msgstr "" #: ../lazygal.py:275 msgid "Interrupted." msgstr "" #: ../lazygal/sourcetree.py:90 ../lazygal/sourcetree.py:325 msgid "Root not found" msgstr "" #: ../lazygal/sourcetree.py:281 #, python-format msgid " Ignoring %s, cannot open file (broken symlink?)." msgstr "" #: ../lazygal/sourcetree.py:292 #, python-format msgid " Ignoring %s, format not supported." msgstr "" #: ../lazygal/generators.py:49 msgid "Could not find themes dir, check your installation!" msgstr "" #: ../lazygal/generators.py:67 msgid " SORTING pics and subdirs" msgstr "" #: ../lazygal/generators.py:80 ../lazygal/generators.py:92 #, python-format msgid "Unknown sorting criterion '%s'" msgstr "" #: ../lazygal/generators.py:125 msgid " BREAKING web gallery into multiple pages" msgstr "" #: ../lazygal/generators.py:371 #, python-format msgid " MKDIR %%WEBALBUMROOT%%/%s" msgstr "" #: ../lazygal/generators.py:409 msgid "" "Sizes is a comma-separated list of size names and specs:\n" "\t e.g. \"small=640x480,medium=1024x768\"." msgstr "" #: ../lazygal/generators.py:411 #, python-format msgid "Size name '%s' is reserved for internal processing." msgstr "" #: ../lazygal/generators.py:424 #, python-format msgid "'%s' for size '%s' does not describe a known size syntax." msgstr "" #: ../lazygal/generators.py:458 #, python-format msgid " Trying loading gallery configs: %s" msgstr "" #: ../lazygal/generators.py:501 msgid "Bad syntax for webalbumpic-size." msgstr "" #: ../lazygal/generators.py:690 msgid "MKDIR %SHAREDDIR%" msgstr "" #: ../lazygal/generators.py:749 #, python-format msgid "Progress: dir %d/%d (%d%%), media %d/%d (%d%%)" msgstr "" #: ../lazygal/generators.py:757 #, python-format msgid ", current task %d%%" msgstr "" #: ../lazygal/generators.py:772 #, python-format msgid "Trying loading user config %s" msgstr "" #: ../lazygal/generators.py:777 #, python-format msgid "Loading root config %s" msgstr "" #: ../lazygal/generators.py:781 #, python-format msgid "" "'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page." msgstr "" #: ../lazygal/generators.py:825 #, python-format msgid "Generating metadata in %s" msgstr "Kunne ikke åbne filen %s" #: ../lazygal/generators.py:831 ../lazygal/generators.py:890 #, python-format msgid "[Entering %%ALBUMROOT%%/%s]" msgstr "" #: ../lazygal/generators.py:861 msgid "Fatal error, web gallery directory is within source tree." msgstr "" #: ../lazygal/generators.py:863 #, python-format msgid "Generating to %s" msgstr "" #: ../lazygal/generators.py:883 #, python-format msgid "(%s) has been skipped" msgstr "" #: ../lazygal/generators.py:887 #, python-format msgid "" "(%s) has been skipped because its name collides with the shared material " "directory name" msgstr "" #: ../lazygal/generators.py:928 msgid "" " SKIPPED because of mtime, touch source or use --check-all-dirs to override." msgstr "" #: ../lazygal/generators.py:937 #, python-format msgid "[Leaving %%ALBUMROOT%%/%s]" msgstr "" #: ../lazygal/metadata.py:327 ../lazygal/metadata.py:355 #, python-format msgid " (35 mm equivalent: %s mm)" msgstr " (35 mm-ækvivalent: %s mm)" #: ../lazygal/metadata.py:447 #, python-format msgid "Could not open metadata file %s" msgstr "Kunne ikke åbne filen %s" #: ../lazygal/metadata.py:553 msgid " SKIPPED because metadata exists." msgstr " SPRANG OVER fordi metadata findes i forvejen." #: ../lazygal/metadata.py:555 #, fuzzy msgid " SKIPPED because directory does not contain images." msgstr " SPRANG OVER fordi mappen ikke indeholder billeder." #: ../lazygal/metadata.py:564 #, python-format msgid "GEN %s" msgstr "GEN %s" #: ../lazygal/tpl.py:162 #, fuzzy, python-format msgid "Unknown template type for %s" msgstr "Ukendt skabelontype for %s" #: ../lazygal/tpl.py:176 #, python-format msgid "Theme %s not found" msgstr "" #: ../lazygal/tpl.py:203 #, python-format msgid "Loading %s for theme %s" msgstr "" #: ../lazygal/tpl.py:211 #, python-format msgid "Theme %s does not have a %s" msgstr "" #: ../lazygal/tpl.py:214 #, python-format msgid "Theme %s : %s parsing error" msgstr "" #: ../lazygal/genfile.py:106 #, python-format msgid " ZIP %s" msgstr " ZIP %s" #: ../lazygal/genfile.py:128 #, python-format msgid "CP %%SHAREDDIR%%/%s" msgstr "CP %%SHAREDDIR%%/%s" #: ../lazygal/genmedia.py:81 #, python-format msgid " %s is BROKEN, skipped" msgstr " %s er DEFEKT, sprang over" #: ../lazygal/genmedia.py:121 #, fuzzy msgid "RESIZE" msgstr " RESIZE %s" #: ../lazygal/genmedia.py:209 #, fuzzy, python-format msgid "Could not copy metadata in reduced picture: %s" msgstr "Kunne ikke åbne filen %s" #: ../lazygal/genmedia.py:222 msgid "VIDEOTHUMB" msgstr "" #: ../lazygal/genmedia.py:229 #, python-format msgid " creating %s thumbnail failed, skipped" msgstr "" #: ../lazygal/genmedia.py:262 #, python-format msgid "Supplied album picture %s does not exist." msgstr "" #: ../lazygal/genmedia.py:282 #, python-format msgid " DIRPIC %s" msgstr " DIRPIC %s" #: ../lazygal/genmedia.py:311 #, fuzzy, python-format msgid " TRANSCODE %s" msgstr " TRANSCODE %s" #: ../lazygal/genmedia.py:320 #, python-format msgid " transcoding %s failed, skipped" msgstr "" #: ../lazygal/genpage.py:129 ../lazygal/genpage.py:266 #, python-format msgid " XHTML %s" msgstr " XHTML %s" #: ../lazygal/genpage.py:213 #, python-format msgid "" " Size '%s' is not available in '%s' due to configuration: medias won't be " "shown on index." msgstr "" #: ../lazygal/genpage.py:357 #, python-format msgid "FEED %s" msgstr "FEED %s" #: ../lazygal/genpage.py:375 msgid "" "We have a template with an extension that does not start with a t. Aborting." msgstr "" "We have a template with an extension that does not start with a t. Abording." #: ../lazygal/genpage.py:381 #, python-format msgid "TPL %%SHAREDDIR%%/%s" msgstr "TPL %%SHAREDDIR%%/%s" #: ../lazygal/config.py:125 #, python-format msgid " Ignoring unknown section '%s'." msgstr "" #: ../lazygal/config.py:129 #, python-format msgid " Ignoring unknown option '%s' in section '%s'." msgstr "" #: ../themes/default/dirindex.thtml:70 msgid "All full scale pictures as an archive, for" msgstr "All fuldstørrelse billeder som arkiv, for" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 msgid "Generated by" msgstr "Genereret af" #: ../themes/default/dirindex.thtml:77 ../themes/default/browse.thtml:52 #, python-format msgid "on %c" msgstr "" #: ../themes/default/browse.thtml:39 msgid "previous" msgstr "" #: ../themes/default/browse.thtml:44 msgid "next" msgstr "" #: ../themes/default/image.thtml:9 msgid "Taken" msgstr "Taget" #: ../themes/default/image.thtml:9 msgid "on %d/%m/%Y at %H:%M" msgstr "%d/%m/%Y kl %H:%M" #: ../themes/default/image.thtml:11 msgid "Author" msgstr "" #: ../themes/default/image.thtml:12 msgid "Keywords" msgstr "" #: ../themes/default/image.thtml:13 msgid "Original picture" msgstr "Originalbillede" #: ../themes/default/image.thtml:17 msgid "Camera:" msgstr "" #: ../themes/default/image.thtml:18 msgid "with" msgstr "med" #: ../themes/default/image.thtml:20 msgid "Exposure" msgstr "Exposure" #: ../themes/default/image.thtml:21 msgid "Sensitivity ISO" msgstr "Sensitivity ISO" #: ../themes/default/image.thtml:22 msgid "Aperture" msgstr "Aperture" #: ../themes/default/image.thtml:23 msgid "Flash" msgstr "Flash" #: ../themes/default/image.thtml:24 msgid "Focal length" msgstr "Focal length" #: ../themes/default/video.thtml:6 #, fuzzy msgid "Original video" msgstr "Originalbillede" #: ../themes/default/feeditem.thtml:5 ../themes/default/gallerylink.thtml:21 #: ../themes/image-index/gallerylink.thtml:30 msgid "sub-galleries" msgstr "undergallerier" #: ../themes/default/feeditem.thtml:6 ../themes/default/gallerylink.thtml:17 #: ../themes/image-index/gallerylink.thtml:26 msgid "photos" msgstr "fotografier" #~ msgid "on" #~ msgstr "den" #~ msgid "Parent" #~ msgstr "Niveau op" #~ msgid "Gallery index" #~ msgstr "Oversigt" #~ msgid "Using" #~ msgstr "Ved hjælp af" lazygal-0.8.2/ChangeLog-full0000644000175000017500000012526012301073102015615 0ustar niolniol00000000000000Lazygal 0.8.2 (2014-02-19) * inverted theme: fix plugins.tjs genshi text template syntax * make test_clean_empty_dirs pass (thanks damien_courouss!) * test debugging material cleanup * ensure is_subdir_of processes an absolute path and add inifinite loop guard * use abspath for tpl_dir to prevent inifinite loop in is_subdir_of() * add test_clean_empty_dirs, supporting rev 921 * Fix #12 remove minified js * revert "do not skip generation of empty directories to allow deletion" * dest file cleanup: protect deletion with assertion of context directory * Update Czech locale * Update po files * test_cleanup: remove debugging and useless statements * test_cleanup: put a file in dir to be removed * fix test_cleanup: configure album in constructor * Close branch delete_subdirs * Merged in damien_courouss/lazygal/delete_subdirs (pull request #12) * Created new branch delete_subdirs * add test_cleanup (is NOK) * test_cleanup is now test_foreign_files * do not skip generation of empty directories to allow deletion * fix: clean empty directories at destination * if clean-destination, delete directories instead of advising * improve a bit shared files dest processing * add missing dot in manpage * introduce basic theme manifest in order to include shared files from other dirs * do not link to non-existent subgals when using filter-by-tag * Fix #11 do not generate empty dirs with filter-by-tag * Close branch test_filter_by_tag * Merged in damien_courouss/lazygal/test_filter_by_tag (pull request #11) * Created new branch test_filter_by_tag * minor fix in the man page * improve readability of test_filter_by_tag and test_filter_and_dirzip * fix test test_filter_by_tag and add a test case * report transcoding progress * minor fix in the manpage * new config option video-size + no video scaling or transcoding if not needed Lazygal 0.8.1 (2013-10-30) * set version to 0.8.1 * update translation template * update fr translation * make keywords available in image pages * decode utf-8 keywords * hide exiv2 warnings in normal operation * clear progress info upon exit * Fix #7 handle video size error * make dirzip honor filter-by-tag filters * fix subgal count when filter-by-tag is used * Fix #4 make filter-by-tag= work in sub dirs * manpage whitespace fixes * document how pic comments are loaded * Fix #5 prevent broken img symlinks from crashing lazygal * actually stop stalled pipeline * handle query error which may happen when monitoring the gst pipeline * monitor if pipeline is stalled while transcoding * Fixed typo in man page * update README * translate new string in french translation Lazygal 0.8 (2013-05-28) * set version to 0.8 * update translation template and french translation * add dummy set_log_level method to fix pyexiv2 support * Fix #3 useless warnings while running test suite * add a fallback to pyexiv2 if GExiv2 is not available * bring back metadata processing fixes forgotten in the merge request * test that empty directories are not created on destination * do not push empty dirs * get_date: bypass ValueError exceptions * minor improvements in tests filter_by_tag and filter_and_zipdir * Fix bad merge from upstream * Merge from upstream * merge from the upstream repo * handle KeyError for metadata keywords * improve None JPEG comment case handling * do not try to decode unicode usercomment * handle KeyError in vendor codes * fix filter_by_tag and dirzip test * more usage of assertTrue in tests * remove debugging statements * use assertTrue and assertFalse in filter_by_tag tests * remove extra newline * only check tagfilters on pics (not on videos) * remove trailing whitespace * Merged in damien_courouss/lazygal (pull request #8) * tests for filter-by-tag * Extend keyword support for filter-by-tag * inverted theme: remove all `div` from css * inverted theme: css fixes * inverted theme: hide image caption div if nothing to show * inverted theme: small fix * indicate template can use image metadata * inverted theme: Track original image clicks if google analytics is set * inverted theme: use genshi NewTextTemplate syntax also in JS * inverted theme: display social icons * use gst.discoverer return code to prevent lazygal failure * fix video src path in video tag * do not fail if all medias are filtered out * make config list helpers available to other vars * tag filtering: add support for regexes and for combination of several filters * fix tagfilter empty value check * fix spelling in comment * add --filter-by-tag cmdline help * fix default conf file syntax for filter-by-tag * fix #1 ./setup.py build_manpages command * default value for filter-by-tag in the conf file * describe --filter-by-tag in manpages * merge from niol * fixing the previous commit that was incomplete * add an option 'filter-by-tag' * minor fix: missing semicolons in the default theme * more GExiv2 API cleanup fallout * handle gexiv2 API cleanup * do not overwrite generated media in mediautils test code * make the console output give some basic progress info * make some quick album stats available * use pathutils walk() * update pathutils with walk() and decoding error handling * merge migration to GExiv2 * fix last PIL import * document GEXiv2 dep * restore test cases forgotten in gexiv2 patch * merge GExviv2 porting patch * improve SHARED_ files documentation * Port from pyexiv2 to GExiv2. * put README change in MANIFEST * update TODO * use markdown readme * single page themes support * make flattening basic unit test actually test something * use genshi NewTextTemplate syntax and convert themes CSS templates * fix indentation error * do not crash on bad encoding of EXIF flash info * do not crash on bad encoding of EXIF Artist * rename config option global/destdir to global/output-directory for consistency * fix typo in man page * fix broken --subgal-sort-by=exif * add help hint of exif option for --subgal-sort-by * fix: pep8 E121 continuation line indentation is not a multiple of four * fix: pep8 E126 continuation line over-indented for hanging indent * fix: pep8 E128 continuation line under-indented for visual indent * fix: pep8 E122 continuation line missing indentation or outdented * Fix: pep8 E124 closing bracket does not match visual indentation * Fix: pep8 E123 closing bracket does not match indentation of opening bracket's line * Fix: pep E127 continuation line over-indented for visual indent * Fix: pep8 E211 whitespace before '(' * Fix: pep8 E222 multiple spaces after operator * Fix: pep8 E202 whitespace before ')' or ']' * Fix: pep8 E221 multiple spaces before operator * Fix: pep8 E302 expected 2 blank lines, found 0 * Fix: pep8 E231 missing whitespace after ',' * Fix: pep8 E301 expected 1 blank line, found 0 * Fix: pep8 E251 no spaces around keyword / parameter equals (not in setup.py) * Fix: pep8 E502 the backslash is redundant between brackets * Fix: pep8 E261 at least two spaces before inline comment * Fix: pep8 E203 whitespace before ':' (not all removed) * Fix: pep8 E201 whitespace after '[' or '{' * Fix: pep8 E225 missing whitespace around operator * Fix: pep8 E401 multiple imports on one line * inverted theme: add prev next links for videos and fix key nav * inverted theme: small code fixes * inverted theme: trim whitespaces * inverted theme: inverted social buttons tooltips colors for dark theme * inverted theme: add simple theme js/css only when 'display_theme_selector = True' * inverted theme: remove unnecessary js code * inverted theme: add social buttons js only when 'display_social_buttons = True' * add treat *.tjs files as genshi template files * inverted theme: add text tooltips to share buttons * inverted theme: update Google Analytics code and move it to header * inverted theme: change share buttons order * inverted theme: add google bookmarks share link * inverted theme: fix validation errors * inverted theme: bump jQuery to ver 1.8.2 * do not publish image date when publish-metdata=No * inverted theme: add reddit social button * inverted theme: fix tumblr share link * inverted theme: fix social buttons centering problems * inverted theme: strip media file extension from media page title * inverted theme: add TODO file * inverted theme: add social share buttons [template-vars]: display_social_buttons * fix allow boolean value in 'template-vars' options * inverted theme: clean template-vars, always add lazygal info * inverted theme: remove reference to jQuery on google CDN, use only local version * inverted theme: fix css for '#site_title' * inverted theme: update jQuery to v1.8.0 and fix jQuery googleapis.com link * inverted theme capitalize first letter in the image page title * inverted theme replace "-" and "_" with " " in the image page title * inverted theme clicking big image move to next image * inverted theme update dark theme with div#media_options * inverted theme fix margins and style div#media_options * inverted theme fix duplicate id * inverted theme fix thumbnails margins * inverted theme more template vars * new inverted theme * fix generation of html index for dir with no pics (pagination enabled) * change 'import Image' to 'from PIL import Image' * update TODO * also force regen of shared tpls when -f is used * fix --thumbs-per-page when thumbs % thumbs-per-page = 0 * fix image not centered in browse page Lazygal 0.7.4 (2012-07-16) * set version to 0.7.4 Lazygal 0.7.3 (2012-06-29) * set version to 0.7.3 * update TODO * update french translation * fix album picture link on win32 * fix size probing with bad pic files * add --force-gen-pages * better handling of errors when probing size of orig pics for w/h tags in HTML * remove useless code in orig and symlink building classes * fix typo in transcode error exception code * fix broken generation on win32 * fix media links on win32 when --dir-flattening-depth is changed * remove unnecessary newline in comment * fix spelling, copyright years, remove unnecessary semicolons and some PEP 8 and * fix a wrong clean_output method and do some cleanups found with pyflakes * explain limitations of lazy mechanism * do not fail when using --dir-flattening-depth and a config file with other size defs * fix media links when using --dir-flattening-depth * add unit test for --subgal-sort-by * fix typo breaking --subgal-sort-by * fix %s in strftime not portable to win32 * fr translation fix * fix video src= link (and related cleanups) * fix mediautils cmdline qnd test code * use our own datetime simple implementation to work around limitations of std lib * make more weblbumpic types easier to add in the future * update TODO * ensure time difference in mtime compare unit test * allow to keep GPS tags * fix test suite failure when running with the C locale * prevent original videos conflicting with resized videos * introduce --webalbum-pic-type * remove useless p containing medias in tpls * add header/footer in new module * merge date translated strings (no point translating %c) * make time formats customizable in templates * make it explicit that videos only get resized to the default size * resize videos while transocding Lazygal 0.7.2 (2012-05-10) * set version to 0.7.2 * make test paths unicode * work to move towards supporting win32 style paths * fix re-creation of broken symlinks * strip all whitespace when reading metadata files * fix user config tpl vars not feeded to CSS templates * add missing ul tag for image technical info in default theme * update TODO * do not try to search for BOM in non utf-8 encoded files * skip symlinks unit tests on win3é platform * do not mess with gettext function when testing for symlink support * correctly initialize locale (fix on win32 platform) * handle non-POSIX roots in paths * fix vim modeline in css * fix video thumbnailing handling (Debian bug #662118) * update TODO * be even more explicit in --template-vars usage * document how to use --template-vars * actually print the invalid siaze name message instead of a python traceback * allow non-ascii sizes names * fix some of the issues following os.path.relpath usage * use rel path function from python 2.6 instead of custom one * add experimental mp4 transcode pipeline code * remove useless while loops * fix fr translation format string * webalbumpic-bg is a simple size, not a resize rule Lazygal 0.7.1 (2011-11-26) * set version to 0.7.1 * add publish-metadata configuration option * quick and dirty warning on undefined tpl vars * add basic unit test for dirzip gen * do not blank dirzip var after it has been initialised * fix lazygal.conf(5) installation path * fix -z glitch Lazygal 0.7 (2011-11-16) * fixup lazygal mercurial version retrieval * set version to 0.7 * add config module to translatable modules * update fr translation * update TODO * handle missing pixel X width in EXIF for 35mm equivalent focal calculation * fix 35mm equivalent calculation field retrieval and fallbacks * add unit test for resize and rotate * update TODO * add focale_length metadata unit test * add config option for webalbumpic size * warn on unknown config options/sections * remove trailing whitespace in man page * per-directory config files support * fix forgotten old log api statement * use arg in setup_subgal() in unit tests * install defaults.conf * make album dirpics the same size as thumbs * fix obeying size constraints (again...) for reduced pics * quote some more urls * improve image rotation quality by using lossless rotation and by resizing after rotation * avoid unnecessary output if dir should be skipped * fix --debug and --quiet by removing deprecated code * get hg rev from dir state instead of tip * use HTML5 doctype for the video element * process webm files (now that we do not transcode them, see prev patch * fix double decode of file path in video thumbnailer * fix 'srcdir does not exist' error msg with non-ascii chars * update TODO * copy webm videos instead of transocding into the same format * fix css comment in purple style * do not reuse gst pipeline for media transcoding as it may raise problems * fix non-english locales messing up RFC822 dates in feed * do not forget tpl vars in conf migration script * remove trailing whitspace * shorten footer decl in default theme * add video arrow and localized text to prevnext links * remove whitespace * generate webm vp8 vorbis for web videos * tell genshi to strip xml comments from output * also cleanup shared files for unexpected files * use existing pathutils functions instead of re-implementing * add missing files in src dist manifest * add conffile migration script for new 0.7 format * fix purple style regarding video arrow overlay on video thumbs * fix video info retrieval with unicode path * allow destdir with ~ * fail if parsing config file with old format * update TODO * fix typo in fr translation * --subgal-sort-by=exif : use mtimes only of none of the pics have exif date * handle exif comments with inconsistent encoding * move README info into manpages * integrate video thumbs * handle .png pics * pyexiv2 wrapper comment typo * sort filenames according to locale instead of ascii sort * fix double decode in album picture path in mathew md * fix media sort when webgals are split across multiple pages * add tests for media sort options * remove useless PIL import * fix retrieval of exif flash info * try to get rid of _Suspension not allowed here_ messages from PIL * fix tpl vars from conf file * fix retrieval of pentax and minolta lens types * default theme: simplify CSS output code * fix conf parsing error messages (regression following config overhaul) * use python std module logging instead of custom functions * default theme: make CSS styling easier * default theme: put one media in a div to ease styling * simplify syntax for dirindex gallery and media links * default theme: style enumerations with CSS instead of string generation * the Nexus S seems to fill up the EXIF UserComment with the field name, ignore that * --orig-symlink should imply --original * add new man page output to .hgignore * document config file format * config overhaul: fixup cmdline vs files, new cfg file format * man: fixup docbook-to-man xsl warnings * remove --optimize and --progressive which have defaulted to On for a while * improve --clean-destination desc in man page * minor fix for log indent * fix clean_destination from config file * update TODO * workaround Chrome rendering issue with links (underline is displayed on blanks * merge * try to ignore messed up jpeg comments * fix upside down pics dimensions * make lazygal-setcomment compatible with non-ascii filesystems * add python shebang to lazygal-setcomment.py * merge * do not reference feed in header if it is not generated * make some path utility functions more reusable and test them * drop the s in the distutils command used to run the testsuite Lazygal 0.6.2 (2011-06-30) * set version to 0.6.2 * update fr translation * update TODO * make whitespace in theme files consistent * more fixes for fr translation * add gallery archive size in gallery index * update italian translation * improve video utils quick and dirty test code * video utils: search for thumb only in first 5 minutes of video * add Nikon Lens info retrieval unit tests * fix retrieval of Nikon Lens info * make sure gst pipeline SIGINT check is stopped when pipeline is * make -O work with videos * fix failure when one use -O and there is a video (Debian #631181) * stop pipeline when thumb frame has been extracted, do not wait for EOS * handle SIGINT while transcoding videos * also transcode .3gp videos * remove target video when transcoding has failed * improve failed transocding error message * discard old debugging statement * subgal-sort-by=exif: use all pics in subdirs, not just pics directly in gallery * minor fr translation fixes (thanks to Jonathan Michalon) * print Interrupted error to stderr * fix broken new size fix according to rotation * fix failure when date tags are not recognized by pyexiv2 (Debian #630572) * add webm transocding utility class * also rotate upside down pics * better handling of SIGINT during task build * fix rotated pics EXIF Orientation tag * fix obeying on size constraints when images are auto rotated * gracefuly handle SIGINT * update TODO * make it possible to load puburl from config file * fix typo in manpage due to option parameter change * --sugbgal-sort-by=exif sort by latest EXIF in subgal * support for providing output dir in config file * rename --subgal-sort-by=filename to --subgal-sort-by=dirname which is clearer * fix supplying album picture by file album-picture * add some video utility classes: video info parser and video thumbnailer * add ignore file Lazygal 0.6.1 (2011-04-28) * set version to 0.6.1 * update french translation * updating translation template * credits -> author for author field * add exif author if present in generated web pages * strip spaces from jpeg comment * handle dict.has_key() deprecation * explicity use floor division * fix build with python2.7 * log transcoding errors when they happen if in verbose mode * do not crash when pyexiv2 fails to copy metadata in reduced pictures * fix -O which was symlinks instead of copies * update italian translation * update fr translation * make sure docs are consistent regarding --dir-flattening-depth default value * update TODO and change log * follow symlinks on directories while walking dir trees * add example usage for pyexiv2 wrapper * update TODO * fix installation with a prefix which is not /usr (e.g. /usr/local) * improve description of --dir-flattening-depth in manual page * unfuzz translations because of typo fixes in translated strings * fix many typos in translated strings (many thanks to Frederico Bruni) * add italian translation Lazygal 0.6 (2011-03-09) * add MANIFEST.in in source dist * set version to 0.6 and add simpler changelog * index pages should only depend on source dirs * update TODO * add unit tests samples to manifest * add setcomment script to sdist manifest * no translations in make.py * minor fix in mediautils.py test code * update TODO * some more basic unit tests for special generations * delete half made stuff on key interrupt * fix and optimize file cleanup checks * unit test search for files which should not be there in dest dir * minor fixes to generators tests * no need to sort feed items each time one is added * avoid many calls to os.path.exists() * avoid many calls to getmtime() * depend on source_dir which should be a smaller object * avoid useless is_known_template() call * rebuild dir objects in tests to ensure up to date filesystem vision * avoid some calls to need_build() * fix multiple builds of same index page * fix path debug output in make.py * improve make.py debugging output with arbitrary depth dep tree * fix breadcrumb relative path computation * hide h1 which is useless with breadcrumbs * add breadcrumbs on every page * strip space and null chars from EXIF user comments * fix pyexiv2 mispelling in pyexiv2api module * fix dir metadata which should trigger webgal rebuild when changed * fix parent dir which should need build when subgal has changed (subgal link) * fix second build which should not need build * add unit test for simple file layout of dest dir * add dep build check unit tests * move EXIF user comment decoding in pyexiv2api.py * add debugging helpers to make.py * make WebalbumDir easier to instanciate (for tests) * fix test which required dir to exist * fix typo in exception msg for unknown media * migrate some classes into python new style classes * do not consider broken thumbs for picture mess * remove assertion of file existence in picture mess creation * avoid unit test error because of rounding erratic behaviour * handle more pyexiv2 0.2.x copyMetadata() errros * ensure all supplied pics for pic mess exist as files (and fail if not) * fix picture mess generation for dirs with few pics * fix get_all_media_paths wich was wrong for paths of medias in subdirs * work around pyexiv2 0.1.x copyMetadata() failures * fix typo in generate_default_metadata method name * update some man pages and source file copyright header dates * add basic test for feed creation * do not take into account bogus zero'ed EXIF dates * use new pyexiv2 api wrapper in set comment script * fix obvious error in null comment test, condition was inversed * unit tests for copied metadata in resized image * copy image metadata in resized pics * add unit tests for the different sources of comment * make the pyexiv2 api changes wrapper a separate module * add missing hunk of 'exiv2 0.20 gives us utf-8' change * do not put comment line in tpl if comment could not be found * exiv2 from version 0.20 fives us utf-8 for exif user comment, handle this * pyexiv2 0.3 decodes for us, so directly return exif user comment * fix typo in cmd line help text * [patch 3/3] Modify the way the picture mess thumbnail is created * [patch 2/3] Change the way thumbnails are pasted * [patch 1/3] Make the invokation of eyecandy.py reusable * fix orig-symlink option description * remove useless import * fix error reporting in tpl loading * make it possible to run all test suites with ./setup.py tests * add unit test for original symlink feature * make it possible to symlink original images * improve error message when size syntax is incorrect * fix skipped dirs which did not skip subdirs of skipped dirs * add test for skipped dirs test * update cz translation * add danish translation * add support for file metadata including image captions * set some default values for the album to be easier to construct * changelog gen script: remove tag entries and add devel version header * add .js files to dist MANIFEST * fix last version in changelog * Key navigation in gallery Lazygal 0.5.2 (2010-09-16) * set version to 0.5.2 * fix RSS feed item tpl loading which broke feed generation * update TODO Lazygal 0.5.1 (2010-08-26) * set version to 0.5.1 * another fix forPOTFILES list * add new input theme potfiles * simplify changelog gen script a bit using hg log templates * fix image-index theme * have missing theme files being searched in the default theme * fix metadata generation root path decoding * fix root path decoding * fix metadata generation error logging * fix failure with videos when a size for original pics is set * work around pygst bug that steals help command line swith * improve debug output for tpl decoding errors * fix decoding of Exif.Image.ImageDescription and Iptc.Application2.ObjectName * drop python < 2.5 support (as a consequence of Debian bug #588262) * the t template var trick has disapeared * fix camera model name retrieval with pyexiv2 0.2+ * fix fr translation * fix changelog generation script which was showing the tip tag Lazygal 0.5 (2010-06-15) * set version to 0.5 * to not append hg revision if last commit is a tag * separate lazygal version and hg rev * add png files to theme data * retrieve hg rev from VCS if available * update manpages dates * update changelog generation script for mercurial * reprocess all source files if translated source file list has changed * update translated file list * update potfile * update french translation * update TODO * add some documentation for included templates * fix video ordering which messed up pic ordering * reset GStreamer pipeline when decoding has failed * update TODO * avoid using PIL for JPEG comment when pyexiv2 can do the job * fix typo in error msg * also generate web pages for videos (ogg/theora) * improve included tpls handling * describe high level mode of operation in manual page * more usefull debug output for template decoding errors * support new pyexiv2 0.2 API * fix some function names due to video preparation patch * always store file paths in unicode * bring dates in 2010 * update tags * Read IPTC metadata * multi media support (preparation for video support) * improve broken img handling * probing EXIF data unconditionaly is indeed a perf problem * add a separate task for the breaking of galleries into multiple pages * fix issues with file make objs last built time * improve task output registering * only read EXIF info once per page * do not sort the galleries when nothing needs to be built * fix SubgalSort task according to recent refactoring * record FIXME in unicode path issue * fix typo in webgal dir cleanup * remove useless del im * do not keep EXIF data in memory, reread from file when needed (not that often) * no subgal count in feed item if no subgals * fix album picture path by always recording abs path * Fix typos * correctly initialize album name when generating default metadata * improve description of -m * fix the generating of default metadata * add a FIXME to the second implementation of the same alg * try to fix up the mess that comes from the confusion between a Directory and a WebalbumDir * better handling of broken image files * move webgalpic init out of index page init * some __doc__ enhancements * update TODO * fix sorting of pics in already generated webgals * simplify code by removing the LightWebgal * fix mixups in _last_built_time variable * improve SORTING task debug line * remove prepare() in make machinery * make the sorting of pictures in a gallery a separate task * remove useless regsiter_builder() stuff * add dirty script to set an EXIF comment, to be improved later * fix failure of directory metadata generation on dirs without pics * update homepage url * add subgal and image count on subgal links in default theme * flush out pipes after message print to make sure msg are displayed even when output is piped (ssh, less) * get rid of the "$t." prefix in templates * simplify a bit the file file cleanup code * fix code disalignement * fix presented elements computation when --thumbs-per-page is used with --flatten-below * split the now big generators.py file * return empty jpeg comment whenever field is missing from jpeg file for some reason * memory improvement: only automatically populate outputed items for make objects that produce files * force the cleanup of circular references by the gc after each dir * only set full featured webgal_dir object link if flattening below * light subdir is enough for other img link * fixed thumb links regression in index page * support for templates include and deps * Flatten directory structure below a certain depth * also include jpEg * add missing space in verbose output * fixed issue with dir cleanup wich showed up album dirs * comment typo * skip galleries whose name collide with shared material (e.g. CSS) * fixed gallery conf file not taken into account except for tpl vars * fixed output not in a pipe (less, ssh, etc) that raised a unicode decode error * automatically populate newsize definition plugins list * more control over image resize * bring dates in 2009 * fix manpage installation Lazygal 0.4.1 (2009-05-21) * set version to 0.4.1 * update TODO * fixed typo in french translation * update french translation * bring MANIFEST.in up to date with current dist * urlencode dirzip urls * Make RESIZE translatable * Fix description of --subgal-sort-by * Czech translation update * POT update * fix parent link tpl var name in default theme * new cmdline option --orig-base (Debian Closes: #504039) * default theme: original link is not technical * do not build destgal if the dir has no photos, thus do not create empty webgal dir * do not capword in str_humanize * fix up typos in previous url_quote patch * no subgals for dirs with no photos * quote most urls in page links * do not break up filename and extension for directories * fix unicode error when source_dir has got non-ascii chars * remove useless sorting of subdir names before options are taken into account * allow subgal sorting and reverse sorting of pics and subgals * make webalbum browsable directly on filesystem * improve sorting when no EXIF data is available and give more sorting options * add progressive and optimize image saving options (thanks to Ian Zimmerman) * add missing default values in a config object (thanks to Ian Zimmerman) * handle EXIF flash values localized by some cameras * fix typo in french translation * generate manpage using docbook * %z in strftime hack for feed generation : add python issue link * -s now understands the special dimensions 0x0 which instructs not to resize for this size * useless typo fix in feed generator * make thumb size name a variable * Handle non-ascii dates Lazygal 0.4 (2008-07-04) * set version to 0.4 * output tpl gen date in current local to match i18n * use new out parameter in genshi templates generation (genshi > * add french translation * support for an additionnal config file in source_dir * do not output format not supported message twice * update TODO : better i18n in templates * add some translatable strings in the default theme * Initial czech translation. * Avoid two strings with different format chars. * revert previous patch and use lenient mode for variable lookup * i18n support for templates * Use defined() as it seems to be needed with genshi 0.5.0 * remove version in manpage * fix typo in comment * add a comment about the usage of the 't' dict in templates * fix typo in exception raising * better encoding detection for album_description * allow xhtml tags in album_description * improve README a bit on template vars * compute utf-16 endianess from EXIF tag endianess (patch by Matthieu Castet) * fix typo in source dir does not exist error * fix unicode EXIF UserComment (thanks Matthieu Castet for the hint on endianess) * charset parsing is only for the UserComment EXIF field * strip useless space in exif strings * fallback to utf-8 for jpeg comments * better handling of decoding errors for EXIF commendings * handle charset decl with no text in EXIF comment * Readd lost zero * Better parsing of field * Better charset handling * Handle charset in EXIF comment * Handle old EXIF comment * Fix \0 in comment * add a white frame to webalbum pics which looks better * add an option to choose the webalbum picture color * TODO i18n in tpls * fix filename encoding issue during dest_dir cleanup checks * make genshi templates play better with unicode * TODO video and dirgalpic bg color * optional directory archive with original images for easy download * update-po script reorganization * i18n support for runtime messages * fix lazygal module * import * webalbum index pagination for big directories * remove useless Album constructor parameter * Another int conversion * Force int conversion Lazygal 0.3.1 (2008-03-27) * set version to 0.3.1 * add a script that builds a changelog from the darcs history * better description of touch instead of --check-all-dirs in man page * fix thumb alt in dirindex * fix thumb width/height not being outputed in dirindex * if dep needs build, target should be built * rebuild webalbumpic when pics get removed from dir * use static var instead of instance var for configurable webalbumpic fn Lazygal 0.3 (2008-03-08) * set version to 0.3 * sort subdirs for nice ordering when displayed * better and simplet title for index page * TODO abandon this item because the shell makes it really tricky to put complex stuff in tpl vars' * IndexPage now depends on subdir metadata * try to handle conffile tpl vars with non ascii chars * use subdir objects rather than dirnames to populate subgal links * make more use of _str_humanize() * webalbum pic should be a dep of webalbum dir instead of index page * skip VCS directories in album generation * do not fail on directory that contains no pictures * fix typo in tpl var name: s/zise/size/ * do not display other size links if there is only one image size * Add missing template. * Support for new PyExiv2 * update TODO * reorder Album methods a bit to have loging all in one place * themes mostly through CSS and new default style cmdline option * make the contents of the feed item a template * fix original link * allow template vars given on the cmdline to have differente encodings * fail if dest is in source tree * add webalbum picture to feed items * simple filedep should not be registered as output from the target * fix tpl file dep by normalizing DATAPATH * TODO Webalbumdir done another way * make feed use source dirs mtimes instead of generated dirs mtimes * share more code between LightWebalbumDir and WebalbumDir * maybe fix album_description generation which was broken by previous changes * play a bit with picture mess params to improve effect (making the thumbs larger) * TODO inheritence not possible between WebalbumDir et LightWebalbumDir * add reported bug to TODO * add a todo list to the project * shared file tpl must depend on template file * build some fancier album picture * use some extra_files list for dir cleanup * fix tpl shared files generation * walk source tree from deep to top to be able to reuse stuff processed in subdirs * move _add_size_qualifier() in album so that everybody gets it * Support for generating metadata files * More intelligent quotes stripping * use the new copy target for ImageOriginal * make shared files generation a make target Lazygal 0.2 (2008-01-24) * bump version number * do not complain if elementtree is not here * advertise RSS feed in templates now that it seems to work * full RSS feed generation (not only processed directories) * add missing metadata dependency for index page * quote urls in feed for special characters * update copyright dates in licence headers * Optional lens information * Better model name generation * linux kernel compile like output, which is more readable * fix feed generation, sorting and maxitems * some tidying in album metadata to bring back guessed album pictures in index page * depending on prev/next for browse page should occure at prepare time * make sure that make targets are prepared only once * browse page should depend on directory in case of image deletion * fix indentation in previous patch * feed should not depend on skipped directories * do not load exif data in unprocessed directories * feed generation for new or updated galleries * construct template dependencies with full path * refactor metadata as a make target and refactor WebalbumDir attrs * fix browse page other size links * chain images only once for all * fix root index pages not being built * save initial mtime is only for WebalbumDir * split source tree classes in a separate module * externalize makefile logic into a separate module * fixes and improvements (themes, files and metadata) for manual page * Add man page * Avoid failure if running outside darcs tree * fix unencoded utf-8 in check dest for junk * Add footer option to template * Improve template variables support * warn about manually removing ungenerated directories in dest * Allow to include original pictures in generated album Lazygal 0.1 (2007-10-26) * bump version number * make default theme CSS be intepreted by template engine and escape # * Make default theme accepts same template vars. * Add some more template variables. * template-vars can be configured from config file. * Use interpreted flash value instead of raw number. * fix album subpicture link containing a dot in name * handle tag Exif.Photo.FocalPlaneResolutionUnit no set * make shared stuff logging more accurate with what happens * example of CSS with tpl vars in image-index theme * pass tpl vars on the cmdline * shared files templates * Improve exposure display. * added lazygal section to default config options * Avoid multiple conversion of float values. * Move config to ~/.lazygal directory, we already use it for themes. * Improve 35 mm equivalent calculation. * Move image name bellow comment. * Use 35 mm equivalent from EXIF if available * Use same format for 35 mm equivalent as for normal focal length * Handle if we want int value as float * Fix FIXMEs :-). * Fix handling of images without EXIF. * Do not break vim modeline by end of comment. * Use format string instead of contatenation. * Configuration file support. * Make CSS valid. * do not complain about album metadata not being a supported format * EXIF backend changed to exiv2 * remove duplicated get_date_taken() method * make sur exif data is loaded before using it * improve output and add --quiet and --debug options * make image-index theme more XHML valid regarding subgal links * remove unused variable * put rotation calculation in ExifTags * get exif date selection in ExifTags * correctly load utf-8 from album metadata * catch bogus EXIF focal plane x res * get exif and metadata stuff in a separate file * rename lazygal/lazygal.py to lazygal/generators.py * Mention homepage in README * Create whole path for album if it does not exist * Make setup.py sdist work * Try to use EXIF date when available * Actually use parameter * Generic EXIF date parsing method * also increase default JPEG quality in alum class * Increase default JPEG quality * JPEG quality is now configurable * Display gallery index in more columns on wider screens * Fix typo in help * Fix typo * Update meta data code * Implelement guessing of album picture * Rearrange metadata acquision * Rearrange functions to group same * Add some documentation * Improve vendor/model detection * fix get_last_version which did not work when patches had a long comment * do not advice to remove shared dir * try to fix filesystem encoding issues * make FileImage constructor use dir object * Better detect duplicate information in vendor/model fields * Include sensitivity info * Allow {upper,mixed}-case extensions * Include version in footer * Sync image-index theme with default * append some kind of darcs revision to version if available * make lazygal version available in templates * Include time zone in date * Fix alternate text for image * Show verbose album name if available * Improve albums listing in image-index theme * Update mage-index style to make it look a bit better * Add image-index theme as copy of default * Convert album picture path to allow it's usage * Add some readme, mostly to document meta data * metadata support for subdirs * initial album metadata support * added generation date to templates * remove final link separator in other sizes links * no other size link on currently selected size * Add version to lazygal module * add missing __init__.py file for new lazygal module * Allow user defined themes * Prefer local themes * Install themes * Rename Lazygal module to lazygal * Rename script to avoid name conflict with module * Add distutils support * Configurable thumbnail size * These sould be integers * Configurable image sizes * Use format strings * Fix typo * Handle zero source mtime * enhanced-exif-support * work-on-empty-dir * whitespace-cleanup * use genshi for templates * force update of dest dir mtime * get rel_root into browse page template * save dest dir mtime because it is updated during lazygal's operation * always generate browse page if directory has changed, because files may have been deleted * added licence clauses (GPL v2) * rotate before thunbail in order to have consistent sizes accross gallery * added --check-all-dirs option to force lazygal to check all dirs * add --clean_destination option to clean deleted photos in destination * fix stupid angle error in orientation rotation * skip whole directories according to mtime * basic auto orientation according to EXIF data * copy shared files before generating galleries * compute a title for the root directory page * sort subgal links * handle cases where EXIF date is not available or does not make sense * dest is only relevant for directories * other sizes links on every page * add lazygal website link in default template * make File, Directory and ImageFile more object oriented * test for source dir to exist * fix prev link search * remove superfluous print * sort by EXIF date * rename image_processor for it to be shorter * fix css not found * inital import, it works! lazygal-0.8.2/lazygaltest/0000755000175000017500000000000012301073200015437 5ustar niolniol00000000000000lazygal-0.8.2/lazygaltest/sample-with-gps.jpg0000644000175000017500000000365412301071671021204 0ustar niolniol00000000000000JFIFHH8ExifMM*  (1'20i6%ĥdigiKam-0.9.2-finalFUJIFILMFinePix F30 HHDigital Camera FinePix F30 Ver1.022008:01:01 13:16:54 %"'d0 8@ H PX  `|Th)d X   0d2008:01:01 13:16:542008:01:01 13:16:54( dddd,d dFUJIFILM +: B !"# J012FC D9700951 592D31313030061007839330213B41FINE d1424 1068ASCIIsun trying hard to get through...wwR98dNzE/:?B@wDB@WGS-84 ((0)|HHC$sun trying hard to get through...  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/sample-usercomment-unicode-mm.jpg0000644000175000017500000000103612301071671024026 0ustar niolniol00000000000000JFIFHHExifII*JR(iZ,dUNICODEunicode test : C  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/test_conf.py0000644000175000017500000001113212301071671020005 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import os import ConfigParser from __init__ import LazygalTestGen import lazygal.config from lazygal.generators import WebalbumDir from lazygal.sourcetree import Directory class TestConf(LazygalTestGen): def test_perdir_conf(self): """ Lazygal shall read configuration files in every source directory, the parent directory configuration shall apply to child directories. """ os.makedirs(os.path.join(self.source_dir, 'gal', 'subgal')) # src_dir/.lazygal config = ConfigParser.RawConfigParser() config.add_section('template-vars') config.set('template-vars', 'foo', 'root') config.set('template-vars', 'root', 'root') with open(os.path.join(self.source_dir, '.lazygal'), 'a') as f: config.write(f) # src_dir/gal/.lazygal config = ConfigParser.RawConfigParser() config.add_section('template-vars') config.set('template-vars', 'foo', 'gal') config.set('template-vars', 'gal', 'gal') with open(os.path.join(self.source_dir, 'gal', '.lazygal'), 'a') as f: config.write(f) # src_dir/gal/subgal/.lazygal config = ConfigParser.RawConfigParser() config.add_section('template-vars') config.set('template-vars', 'foo', 'subgal') config.set('template-vars', 'subgal', 'subgal') with open(os.path.join(self.source_dir, 'gal', 'subgal', '.lazygal'), 'a') as f: config.write(f) config = lazygal.config.LazygalConfig() config.set('global', 'puburl', 'http://example.com/album/') self.setup_album(config) source_gal = self.setup_subgal('gal', ['gal_img.jpg']) source_subgal = self.setup_subgal(os.path.join('gal', 'subgal'), ['subgal_img.jpg']) source_root = Directory(self.source_dir, [source_gal], [], self.album) dest_path = self.get_working_path() dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) self.assertEqual(dest_subgal.config.get('global', 'puburl'), 'http://example.com/album/') self.assertEqual(dest_subgal.config.get('template-vars', 'root'), 'root') self.assertEqual(dest_subgal.config.get('template-vars', 'gal'), 'gal') self.assertEqual(dest_subgal.config.get('template-vars', 'subgal'), 'subgal') self.assertEqual(dest_subgal.config.get('template-vars', 'foo'), 'subgal') dest_gal = WebalbumDir(source_gal, [dest_subgal], self.album, dest_path) self.assertEqual(dest_gal.config.get('global', 'puburl'), 'http://example.com/album/') self.assertEqual(dest_gal.config.get('template-vars', 'root'), 'root') self.assertEqual(dest_gal.config.get('template-vars', 'gal'), 'gal') self.assertRaises(ConfigParser.NoOptionError, dest_gal.config.get, 'template-vars', 'subgal') self.assertEqual(dest_gal.config.get('template-vars', 'foo'), 'gal') dest_root = WebalbumDir(source_root, [dest_gal], self.album, dest_path) self.assertEqual(dest_root.config.get('global', 'puburl'), 'http://example.com/album/') self.assertEqual(dest_root.config.get('template-vars', 'root'), 'root') self.assertRaises(ConfigParser.NoOptionError, dest_root.config.get, 'template-vars', 'gal') self.assertRaises(ConfigParser.NoOptionError, dest_root.config.get, 'template-vars', 'subgal') self.assertEqual(dest_root.config.get('template-vars', 'foo'), 'root') if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/sample-image-description.jpg0000644000175000017500000000101012301071671023025 0ustar niolniol00000000000000JFIFHHExifMM*Vlt(i|test ImageDescription02200100C  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/sample-model-nikon2.jpg0000644000175000017500000003600612301071671021735 0ustar niolniol00000000000000JFIFHH:ExifII* (1 2;$iTJacques StotzemNIKON CORPORATIONNIKON D5000HHGIMP 2.6.112011:06:18 18:53:48Federico Bruni#"'0210" 6>  F| 7N2n:5050500100::    X 2011:05:21 23:36:502011:05:21 23:36:50 ` NikonMM*70210          "#:($<%b+p,^H BY@ p #!(& (H0200w(\(r(((()d )jRAW INCANDESCENTAF-A NORMAL . 4 4606713201000100STANDARDSTANDARDX X 01000100d    I00215.2]2_9uDч_>Aiv o`P*EN˜6Va8:d7'4^ (=,Ϗlf}iphJIegnUYzҡsj1 *rv\Ɣ^LD8[݇L-.LqZgIpi{y+-v_ zYgeIJhp}fl,=ͤiԭg,龆81!9A7@鳘LT؈R/o nJKOĪiET71X3k2m? M`KSn.(Y`;0Bq` *1N<$)Ҹ+"6_l:aMLg?ۋX2I&aXί]Ԯ$<)GdʆXr3  a91 %:I[e]3ם%QY %ޡ[;Jm92|?6mk39uYD׆>Eh4aԹzd;5VWN McW`zҝ:dU'K^ (㻰=,Ϗlf}phJIegnUYz ҡ_v-+sZ~jHC[Q݆L//LQ[[HNl"՗tLZs+uRJPd"7zYU|geN=hntfl>i9t ȾE8K8\~MLgDN;p:˙yh[bn]/UX>\EΊƒ@3k27'~ @x./^%b;7Sn+ȭ%.#twbՙf r! <_\vbX@Gic]̯]a)G5yXؚ\#Sy`N=|UJoȟG"1nd&-:O%޴.xSK`bL|?] i89"D!EKt3?S7~LzJPy/E[8%n b1}q~&{O7{ $"w m^뇁Q2S^Uɦ>j&{7=%;/+QX2)G5yڤ j!,d*P"Fo ƝiE>TEx:6j?i8=qIaI^{/溤 c;0Bq&O1{#tw$8R&EiТ oF"P*žWN@AVa:d7'4^ (㻰=,Ϗlf}phJIegQ9z}{ ҡ[op *YZ|jHCՐT̆O6IL[qHj,~ǬH!yV' ҉#Cir^-tG[ a\Np0Y=OuZțQf,42@`cIMLgeF ;+A:wds9bn]/|UAx(1̔?>)mEjHXK!VWϽBS}56*&l_È R~T %kZ7<$.3PP3.$<7zkʆ% T~R ޭ_l&*ۈ65܄}SBĜWv!KXjEm)ɒ>?Ɗ1(xU|/]nb9sݯdw:A+; FegLMIc`@24,fQˡZuDO=Y0pN\a [Gt-^riC# Ԍ'Jq{h0Vo"yгy"oV8h{qJ' ~҉#Cir^-tG[ a\Np0Y=ODuZțQf,42@`cIMLgeF;+A:Uwds9bn]/|Ux(1̔?>)mEjHXK!WϽBS}56*&_À R~^ %kz7<$.3PP3.$<7zkʆ% T~ ޭ_l.*ۈ65Ԅ}΂SBĜWv!KPHjEm)ɒ>?Ί1(xU|/]nb9sݯdw:A+; FegLMIc`@24,fQˡZuDO=Y0pN\a [Gt-^riC# T'Jq{h8Vo"yгy"oh{qJ% ҉#Cir^-tG[ a\Np0Y=ODuZțQf,42B`cIMLgeF ;+A:w|ds9bn]/|Ux*1̔?>)mEjHXK!vWϽBS}56*&l_À( R~T %kz7<$.3PP3.$<7zkʆ% T~R ޭ_l&*ۈ65܄}SBĜWv!KXHjEm)ɒ?Ɗ1xU|/]nb9sݯdw:A+; FegLMIc`@24,fQˡZuDO=YӰpN$\a ([Gt-^riC# Ԍ'Jq{h8Vo"yгy".oVz8h{qJ' ҉#Cir^-tG[ a\Np0Y=ODuZțQf,42@`cIMLgeF ;+A:wd9s9bn]/|Ux(1̔?>)mEjHXK!vWϽBS}56*&l_À R~T %kz7<$.3PP3.$<7zkʄ% T~R ޭ_l&*ۈ65܄}SBĜWv!KXHjEm)ɒ>?Ɗ1*xU|/]nb9sݯDw:A+; FegLMIc`@24,fQˡZuDO=Y0pN\a [Gt-^iC# Ԍ'Jq{h8Vo"yгy"oV8h{qJ' ҉#Cir^-tG[ a\Np0Y=ODuZțQf,42B`cIMLgeF ;+A:wds9bn]/|Ux(1̔?>)mEfjHXK!vWϽBS}56*&l_À" R~T %ȸkz7<$.3PP3.$<7zkʆ% T~R ޭ_l&*ۈ65܄}SBĜWv!KXHjEm)ɒ>?Ɗ1(xU|/]nb9sݯdW:A+; FegLMIc`@24,fQZuFO=Y0pN\a [Gt-^riC# Ԍ'Jq{h8Vo"yгy"oV8h{qJ' ҉#Cir^-tG[ a\ Np0YB=ODuZțQf,42H`cIMLge ;+A:wls9bn]/|Ux(1̔?>)mEjHZK!vWϽBS}56*&l_À R~T %kz7<$3PP3.$<7zkʆ% T~R ޭ_L&*ۈ657܄}SBĜWv!KXHjEm)ɒ>?ƈ1(xU|/]nb9sݯdw:A+; FegLMIc`@2?4,fQˡZuDO=Y0pN\a [Gt-^riC# Ԍ'Hq{j8Vo"yгy"oV8h{qJ' ҉#Cir^-tG[ a\Np0Y=ODuZțQf,2@`cIMLgeF ;+A:wds9bn]/|Ux(1̔?>)mEjHXK!vWϽJS}56"&l_À RvT %k!z7<$.3PP3.$7zkʆ% T~R ޭ_l&*ۈ65܄}SBWv!KXHjEm)ɒ>?1(xU|/]nb9sݯdw:A+; FegLMIc`@24,fQˡZuDO=Y0pn\a [Gt-^riC# Ԍ'Jq{h8Vo"y3гy"oV8h{qJ'҉#Cir^-tG[ a\Np0Y=ODuZțQf,2@`cIMLgeF ;+A:wds9bn]/|Ux(̜?>)mEjHXK!vWϽBS}56*&l_À R~T %kz7<$.3PP3.$<7zkʆ% T~R Cޭ_l&*ӈ65܄}SBĜWv!KXHjEmɒ>?Ɗ1(xU|/]nb9sݯdw:A+; FegLMIC`@24,fQ˩ZuDO=Y0pN\a [Gt-^riC Ԅ'Jq{h8Vo"yгy"oV8h{qJ' ҋ#ir^-tG[ a\$Np0Y=ODuZțQf,42@`CIMLgeF ;+Q:wds9bn]/|Ux(1̔?>)mEjHXK!vWϽBS}56*&l_À # R~T %kz7<$.3PP3.$<7z kʆ% T~R ޭ_l&*ۈ65܄}SBĜWv!KXHjEm)ɒ>?e xU|/]nb9sݯdw:r{;1 FegLMIC`@24$~d''u OFF D HHHH 0@P`pAq LbUuF&  $ D r  R (s]e - Y$Qҫ  (s]d # S,Zӫ 0211PmK39uD׆T?kk Т KF&6d(ǾWN MVq`צ*d$N 894-qݺ2{+XyjagnUYz ҡ^v-sXZ)[y݄L7/Q[CHn~ǗZX-ܳPa!{YTzfeIhO|fm,TDu99k2m" ?hY`KSx:䣧/ c;0Bq&E1{#tw$8 S! =\rLy4Gbx(c\ͯє7)G5xYr\< !R 8$wt#{1O&qB0;c %޴.xSK`M|?6m2k39uDׇT>EiТ oF"P*žWN@A춝W`;e7&4_)㺰<-ώlg}qhKIdgoUXz ӡ^w-+r[jICZQ܆M/.LP[BHk~ǖZs*"ӠCr-/tGuZh Fg\ Nrcpө=Dׅ;8,©@cNMOgF˨+:Շ-o ƝiE>TDu93k2m6?|M`KSx.䷧% }r@7` 1{#,ʄu*t'9 S! 9|\rXk5G)F]̯.$<7zkʆ% T~R ޭ_l&*ۈ65܄}SBĜWv#KXHjz0204 87mtPFQ iTТ0103 0100/01000100dB01000100D" ava#+)R.#06so4r)0!(?;99/-b+(2`#B1M@ ` U/PPava#+)R.#06so4r)0!(?;99/-b+(2`#B1M@ ` U/PP!2!~RJ)b,0+300)Z!2)839cZ1-+("d#A[qp `Y1`P @A1&2. 11-*B&Y%z+8C>:Cm3 -(Bb#na! Н^P3 `  !v+R-,)b'q&f&,8<$JJKB5B)"W"aq0 @0d6"`0 0 0Aw$_&S$,! "L*S6?Xd(ZA3G*J!aQ>p i9#`0 p pAW!!a%R>a'|t'Mc,"? !AG m;$p0 E!at!V!QA[5w]7:&S,21AAFa Pp <%P0 5Q\<a5q=p(gIswqO*BAoQ<!P r0=&p0  0 0*a[q:A218AW<0P];C5&1Q\A.@ s@?*p&p**P  A)ѢAiAG:<aRaPgQ0@C6#P$ uF:ApMJ0 @ PPA/ѹauQQ=9HA.Wbg-R,Bg qyH! о ~c`mP|rE` PPCQvK+ !#r&B,qi1> P ` 0 p O@!p0 m1 Qm3 @ !1{QQ1A Q_1)@ ~F`  ` `QR/#^A  0zefPc:& AjQ:ppY020@ Э Ѡ7$b,A e B7Qp=!рQ'qaw1 m4p  an!!bQ Pd?(P)XMqous!I!q @XaOEa 0=0y $R vC@*`';@ ! paokmG$ `SP#0 b AMN PU1) Ct NюQseQbqCa!B!ѿ PeP0P Yt 1  pjPE2@B )AlQH0p  "C!0Zp uPZ@PO{ a?vQZ/Q  ? @rp00p0aqp v@^jpp @1<1q{QH  prv p ОI %`0ku0 [0[0 uAnj:0 0vn @p8 P `v`{0Z`h q-:PYJ!TA!p p p Pph4`@ ~P w !}cr`11$, а P P ` i2@@ 00` P pXq[O<@P  p @ h0 p0@@` pPќ4"!E%Q1 `  @ eP.`0PPP Aq> R/!"1` `  P `p p @a -`0pp@ (![ P 0  ^+P PP0 Xy1Cp  p   \*P { 1KaUH  p ` Pso pP [)@ PuPrP 0AQ?q3 zq iO_p  [(@  kg~ ;20a1` u0GZPiPY<04V0 \'0  kg~ ;20a1` u0GZPiPY<04V0 \'0  ASCIIconcerto al Six Bars Jail C  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/sample-jpeg-comment.jpg0000644000175000017500000000054212301071671022020 0ustar niolniol00000000000000JFIFHHCtest jpeg comment  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/sample-model-nikon1.jpg0000644000175000017500000003600612301071671021734 0ustar niolniol00000000000000JFIFHH:ExifII* (1 2;$iTJacques StotzemNIKON CORPORATIONNIKON D5000HHGIMP 2.6.112011:06:18 18:55:45Federico Bruni#"'0210" 6>  F| 7N2n:0000000100::4     2011:05:21 22:19:302011:05:21 22:19:30 ^ NikonMM*70210          "#:($<%b+p,^X BY@ p #!(& (H0200J(\(r(((()d )jRAW INCANDESCENTAF-A NORMAL . 4 4606713201000100STANDARDSTANDARDT T 01000100d^ ^   I00215P@x)>鼠 jQnQ}WbdK!G92]=*WbO!^/B"5YwƙkTF$S1{ݬVYзk*4 v~҅t^uzZ(7ýoG ?鮓Z ˞q[;bTc>a4uܻ{YVǫ]d1+\vdg Z-t/^(/NOW&5M^erPQy( 2) ˧ٶ95(fz#ϟ~8C­{Fq@K@ ƝD}_(ILJV9xOR-r/.v[e:+G/&?w1k,.h6'Lh&U #Aqxo./JIQ퀓-_[kޝu tl@cÉw )uޡ#"͕|װ9 xj}F\.WMs6ǰ@O!HCE42w ƩkE$S1d]ʬVYзu4a>؏cTb:[)vg ̪D?讑?D򽽪C@#:ՍbTc4 *bSܻYVǬ] XPE!Ek?ixwۥh^/Xe8gN dOYF_4s%Qf, 4i0(d.ؕ"#-8QȂ-{F 6}͙ėJeZKyJ,*CHP7{mo{A` q<pfyiL*%ER 3ϡEf\ˑܑu|KerT#U;ZMAvg`ͯH]ݮ8?D $)r/C"*˝u%>t( D// 8S-#61qaDEkwBmL! 1O|lACF6TdrJSoYrwka|7j8[v~g#S6zlAPoW#N_*5JR+I{#7, /}^A*ND$M sk:N@K di&2Wo ]5]q4JwfiVcqO1T!]ʬVYз#}>'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`$ E".8Z5,^h<и^OMW&i6+esQn(2}j ˧2ژxt ˖{;MïuD Nl ŝqNH_R݅J,+GIنP9z\)=xX+kKTb:[%vb̪{Mo?sD򽗪 3v)*_C*rp'>}#DH/68TS5- 71q`Eu?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ'pr*C։3UZB LQnnQL BZU3C*rp'>}#DH/68TS5- 71q`u?ivf9cҽ-a4tݻR]ǫX]>e0eSEtke*j[5,Bt/^!OMW&i6""KesQn(2}j ˧90כ|z()ό~8C­{F @l ŝqDH_R݅J,+GIنP7;\mnx@YT=ۖpf`L+%ُbTc:[(vg ̫E>译?D󽤪 fw)Z;Ռb['1>n ˍzD/ Ʃ8SR-k7>q`\Ju?v9cU"{< GQ뭲XO)Ff/DsMpWn(2}j ˧90כ|z"#Ϙ~8C­{F @mKЦ qLHwR݅=,Fל؅B4:\ooyDXU=ȗfy`L*%=rh.v@;ճt Vh$E+Aa|K eȯy&)ӵz"lNw,[mb:xo{pJR=0204jB "8$- jy20103 0100/01000100d01000100D" @@@@@PPP`p pp``PP@@@@@@@@@PPP`p pp``PP@@@@@@@@@@PP`p p``PPP@@@@@@@@@PP`p p``PPP@@@@@@@@@PP`p 0  pp`PPP@@@00@@@@PP`p P3>`( pp``PP@@@PPPP@@PP`p  B 0 @-p pp `@@@ `PP`p p!9t0s    `@@PPp`  ``p @/ ;:rP' "Fi`QP@@`&`(.-% p  C R&J!L(Qe pF 0 p9f`A P@:@`LPH3P p!@(0^ K#3ECI+`x5-70X0 AS1> k`PPWhr n0R*@,P>pN`^r a"R/X63&AxtЍ P^A``y0p@d@l`pl0=,0?]{ 0 0   ")a!Bsa `f pwp~p |@RU Qq&  1*w1y[{цnC S @PP @ s !q;;A/2:Y "7%rR"aAq&P n8` p / q_Q5I,*2I4$-†#ѐ.P P   o11 Т p q(23*3qz=Q.7A+BTIDtC5%RQI@ PMY p[[ ` б 1#9D@1bh1R%<SpTtE3B!ada)!qY 0"QA0  0 к 2 .SEtF/; "r-!3314CT5BNH:c(q{R@Hv!!O'(V P !Kr62KCb')c[@D`E=@mJ4z?7(A~Tdy${)*++"""!"00 ]:cL?#""-FdI0;Sv8?Ӿ1P5 ="1'".361R-*Bw#!FaF}Ёp  @9WK$>$25-3u>>S|1/>5S &a $+1cK7p4S-(RY!qMMpwpzp 1q5$IAm,RX$"w- 4 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import os from __init__ import LazygalTest import lazygal.tpl as tpl class TestTheme(LazygalTest): def setUp(self): super(TestTheme, self).setUp() self.themes_dir = self.get_working_path() self.theme_name = 'test_theme' self.theme_dir = os.path.join(self.themes_dir, self.theme_name) self.theme_manifest = os.path.join(self.theme_dir, 'manifest.json') os.makedirs(self.theme_dir) def test_sharedfiles_prefixed(self): """ File prefixed with SHARED_ in theme dir are copied in shared files directory. """ prefixed = os.path.join(self.theme_dir, 'SHARED_prefixed.txt') self.create_file(prefixed) theme = tpl.Theme(self.themes_dir, self.theme_name) self.assertEqual(theme.shared_files, [(prefixed, 'prefixed.txt')]) def test_shared_file_manifest(self): """ The manifest makes it possible to include files from other directories in shared files. """ prefixed = os.path.join(self.theme_dir, 'SHARED_prefixed.txt') self.create_file(prefixed) jslib = os.path.join(self.themes_dir, 'lib-2.1.js') self.create_file(jslib) self.create_file(self.theme_manifest, """ { "shared": [ { "path": "../lib-2.1.js", "dest": "lib.js" }, { "path": "../lib-2.1.js", "dest": "js/" } ] } """, bom=False) theme = tpl.Theme(self.themes_dir, self.theme_name) self.assertEqual(theme.shared_files, [(jslib, 'lib.js'), (jslib, 'js/lib-2.1.js'), (prefixed, 'prefixed.txt')]) if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/sample-image-keywords.jpg0000644000175000017500000001124712301071671022366 0ustar niolniol00000000000000JFIFHHExifMM*lt(i|02200100Khttp://ns.adobe.com/xap/1.0/ 1 2 3 0 Xmp.MicrosoftPhoto.LastKeywordXMP.lazygal Xmp.MicrosoftPhoto.LastKeywordXMP.lazygal lazygal Xmp.dc.subject.lazygal Xmp.dc.subject.lazygal Xmp.dc.subject.lazygal Xmp.digiKam.TagsList.lazygal Xmp.digiKam.TagsList.lazygal PPhotoshop 3.08BIM4Z%G"Iptc.Application2.Keywords.lazygalxC  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/sample-usercomment-ascii.jpg0000644000175000017500000000100212301071671023052 0ustar niolniol00000000000000JFIFHHExifMM*JR(iZHH02200100ASCIIdeja vuC  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/__init__.py0000644000175000017500000001035412301071671017565 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2010-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import shutil import tempfile import unittest import codecs from lazygal.sourcetree import Directory from lazygal.generators import Album SAMPLES_DIR = os.path.dirname(__file__) SAMPLE_IMG = os.path.join(SAMPLES_DIR, 'sample.jpg') # Init i18n import gettext LOCALES_PATH = os.path.normpath(os.path.join(os.path.dirname(__file__), 'build', 'mo')) gettext.install('lazygal', LOCALES_PATH, unicode=1) # Init quiet logging import logging logging.basicConfig(format='%(message)s', level=logging.ERROR) def has_symlinks(): try: os.symlink except AttributeError: return False else: return True class qnd_skipIf(object): """ Quick and dirty unittest.skipIf decorator implementation to be able to run the test suite using python < 2.7 . """ def __init__(self, cond, reason): self.cond = cond self.reason = reason def __call__(self, f): def nf(*args, **kwargs): if self.cond: print 'skipped: %s' % self.reason return f(*args, **kwargs) return nf try: # Python >= 2.7 skip = unittest.skipIf except AttributeError: # Python < 2.7 skip = qnd_skipIf class LazygalTest(unittest.TestCase): def setUp(self): self.__workdirs = [] def get_working_path(self): new_wd = tempfile.mkdtemp() self.__workdirs.append(new_wd) return unicode(new_wd) def get_sample_path(self, sample): return os.path.join(SAMPLES_DIR, sample) def add_img(self, dest_dir, name): img_path = os.path.join(dest_dir, name) shutil.copy(SAMPLE_IMG, img_path) return img_path def create_file(self, path, contents='', bom=True): f = open(path, 'w') enc = 'utf-8' if bom: f.write(codecs.BOM_UTF8) f.write(contents.encode(enc)) f.close() def tearDown(self): for wd in self.__workdirs: shutil.rmtree(wd) class LazygalTestGen(LazygalTest): def setUp(self, album=True): super(LazygalTestGen, self).setUp() self.tmpdir = self.get_working_path() self.source_dir = os.path.join(self.tmpdir, 'src') os.mkdir(self.source_dir) if album: self.setup_album() def setup_album(self, config=None): self.album = Album(self.source_dir, config) def setup_subgal(self, name, pic_names): subgal_path = os.path.join(self.source_dir, name) if not os.path.isdir(subgal_path): os.mkdir(subgal_path) for pic_name in pic_names: self.add_img(subgal_path, pic_name) return Directory(subgal_path, [], pic_names, self.album) # Workaround for __import__ behavior, see # http://docs.python.org/lib/built-in-funcs.html def my_import(name): mod = __import__(name) components = name.split('.') for comp in components[1:]: mod = getattr(mod, comp) return mod def run(): import glob suitelist = [] for fn in glob.glob(os.path.join(os.path.dirname(__file__), "test_*.py")): module_path = '.'.join(['lazygaltest', os.path.basename(fn[:-3])]) m = my_import(module_path) suitelist.append(unittest.defaultTestLoader.loadTestsFromModule(m)) runner = unittest.TextTestRunner(verbosity=2) runner.run(unittest.TestSuite(suitelist)) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/sample-usercomment-unicode-ii.jpg0000644000175000017500000001460212301071671024021 0ustar niolniol00000000000000JFIFHH4ExifII* z(2i^ CanonCanon PowerShot A710 IS2008:04:22 18:49:016>0220FZn v~   |L& 0100 (    ( 2008:04:22 18:49:012008:04:22 18:49:01  }h. ("0tʓ   &0 E"2 # '( \@ }hD IMG:PowerShot A710 IS JPEGFirmware Version 1.00s@ 00000 L;=;-TK0[;2~;Pi V7` )))))))))*** ;QTh UNICODEunicode test ' R980100   ( o     !#"! $)4,$'1'-=-167:::"*?D>8B3796    OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOx!  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzw!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?(p)EԢQKEQN1his@h%!eS2G p4.h’(D4.\4n@ 3Hbu.7P7RI<۶.Txeۉ?J?shm198Nּ̑$nr| :~wOxʳf$鏯j\s\kL_sUiK 9 0ǿ8╘(R!\`_zͰI q2co2-9+p)X ;T-ݲ>Qڭa IP2sP6'XE8ʩ<un| T:!ˍcPX`}boqtӤQzV n#(b3Qmtd@IYHVa+: ^Bˀ`SooPH0r3!>&>ҳ  S;3RVoz a0+|yOk'waJP]4'iaox?;R!ßȴ|e?Z;AksO4/G]%l㺩b/%0qJRy:NzUfu$:"4M#}ef#$UgnPTq6&>q In[9Մ7i$˳;?;Kwv"ɤ44 D*3MrLY?ƀЕt(t MSm!I'"v9q08H|;$4ؖQY8;4SJ2 8(Hu7H_GiMnr?Kl:eנ |2BbK >k^R Cu<{w]N͎|y7Z0N ,';a/x@}M4y%R 78U46A;c@w \׉ʁܯ.O.?Z>r od)X.RJO̤}>ojz䱓hk,;mסLhFzzF2x}4߳#1%ZĎ2hU_nsGD7?ӊi?pn)J-O@T\ʃM&ʌgB7@ٗCj3ʓ4LJ.`YSg MЎ)Ҁz>i~AeZ /=3@-60.4%X⁈;!4U 4q܏LPI@ ݎ֐^}E@jd]7LC8T t PZ~ʁbFfv!AӓHgU횅Lw,{ o&PO@+n%>sQOUbO}PHTn%r(2O {e zNh(~吃c"H&ݼ ϥ)֐KQSOF$il=A)"e1ր$VSJ% 0H#Hd8 S|n@hDP(?>$@[LP9rrOHcpG8 ?O( 3Y#>#?J n郊C%ß{lcQ4QT*' 1@(n*uh((+W>ԀO) dCE?|ӌ p0_0= #@zOĥFz S$)['3C mϧ4;GF?POl9HQ ~A!9ԄFᏥ.ۂQ~x%WF:2}(#g$:T}@ii`{րlA)␄  ?C  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/test_sourcetree.py0000644000175000017500000000425312301071671021246 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import os from __init__ import LazygalTest from lazygal.generators import Album from lazygal.sourcetree import Directory class TestSourceTree(LazygalTest): def setUp(self): super(TestSourceTree, self).setUp() self.source_dir = self.get_working_path() self.album = Album(self.source_dir) def get_dir(self, drpath): """ Returns a directory object inside the test album root. """ dpath = os.path.join(self.source_dir, drpath) os.makedirs(dpath) return Directory(dpath, [], [], self.album) def test_skipped(self): d = self.get_dir('joe/young') self.assertEqual(d.should_be_skipped(), False, d.path) d = self.get_dir('.svn/young') self.assertEqual(d.should_be_skipped(), True, d.path) d = self.get_dir('joe/young/.git') self.assertEqual(d.should_be_skipped(), True, d.path) def test_dir_parent_paths(self): d = self.get_dir('joe/young/early_years') expected = [ os.path.join(self.source_dir, 'joe/young/early_years'), os.path.join(self.source_dir, 'joe/young'), os.path.join(self.source_dir, 'joe'), self.source_dir, ] self.assertEqual(d.parent_paths(), map(unicode, expected)) if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/test_generators.py0000644000175000017500000006766112301071671021253 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import os import datetime import shutil from PIL import Image from __init__ import LazygalTestGen, skip, has_symlinks import lazygal.config from lazygal.generators import WebalbumDir from lazygal.sourcetree import Directory from lazygal.metadata import GEXIV2_DATE_FORMAT from lazygal.pygexiv2 import GExiv2 class TestGenerators(LazygalTestGen): def test_albumstats(self): pics = ['img%d.jpg' % i for i in range(0, 8)] self.setup_subgal('subgal', pics) self.setup_subgal('vidgal', ['vid.webm']) expected_stats = { self.source_dir: 0, os.path.join(self.source_dir, 'subgal') : 8, os.path.join(self.source_dir, 'vidgal') : 1, } self.assertEqual(self.album.stats()['total'], 9) self.assertEqual(self.album.stats()['bydir'], expected_stats) def test_genfile_filelayout(self): source_subgal = self.setup_subgal('subgal', ['subgal_img.jpg']) dest_path = self.get_working_path() self.album.generate(dest_path) # Check root dir contents self.assertTrue(os.path.isdir(dest_path)) for fn in ('index.html', 'index_medium.html'): self.assertTrue(os.path.isfile(os.path.join(dest_path, fn))) # Check subgal dir contents dest_subgal_path = os.path.join(dest_path, 'subgal') self.assertTrue(os.path.isdir(dest_subgal_path)) for fn in ('index.html', 'index_medium.html', 'subgal_img.html', 'subgal_img_medium.html', 'subgal_img_thumb.jpg', 'subgal_img_small.jpg', 'subgal_img_medium.jpg'): self.assertTrue(os.path.isfile(os.path.join(dest_subgal_path, fn))) def test_spot_foreign_files(self): """ Files that are not part of what was generated or updated shall be spotted. """ pics = ['img%d.jpg' % i for i in range(0, 8)] source_subgal = self.setup_subgal('subgal', pics) dest_path = self.get_working_path() self.album.generate(dest_path) # add a thumbs that should not be there self.add_img(dest_path, 'extra_thumb.jpg') self.add_img(os.path.join(dest_path, 'subgal'), 'extra_thumb2.jpg') os.mkdir(os.path.join(dest_path, 'extra_dir')) # remove a pic in source_dir os.unlink(os.path.join(self.source_dir, 'subgal', 'img6.jpg')) # new objects to probe filesystem pics.remove('img6.jpg') source_subgal = Directory(os.path.join(self.source_dir, 'subgal'), [], pics, self.album) dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) expected = map(lambda fn: unicode(os.path.join(dest_path, 'subgal', fn)), ['extra_thumb2.jpg', 'img6_thumb.jpg', 'img6_small.jpg', 'img6_medium.jpg', 'img6.html', 'img6_medium.html', ] ) self.assertEqual(sorted(dest_subgal.list_foreign_files()), sorted(expected)) source_gal = Directory(self.source_dir, [source_subgal], [], self.album) dest_gal = WebalbumDir(source_gal, [dest_subgal], self.album, dest_path) expected = map(lambda fn: unicode(os.path.join(dest_path, fn)), ['extra_thumb.jpg', 'extra_dir'] ) self.assertEqual(sorted(dest_gal.list_foreign_files()), sorted(expected)) def test_cleanup(self): """ Check that the foreign files are deleted. """ config = lazygal.config.LazygalConfig() config.set('global', 'clean-destination', "true") self.setup_album(config) pics = ['img%02d.jpg' % i for i in range(4, 8)] source_subgal = self.setup_subgal('subgal', pics) dest_path = self.get_working_path() self.album.generate(dest_path) # add thumbs and directories that should not be there self.add_img(dest_path, 'extra_thumb.jpg') self.add_img(os.path.join(dest_path, 'subgal'), 'extra_thumb2.jpg') os.mkdir(os.path.join(dest_path, 'extra_dir')) self.add_img(os.path.join(dest_path, 'extra_dir'), 'extra_thumb3.jpg') # remove a pic in source_dir os.unlink(os.path.join(self.source_dir, 'subgal', 'img06.jpg')) self.album.generate(dest_path) try: for f in ['extra_thumb2.jpg', 'img06_thumb.jpg', 'img06_small.jpg', 'img06_medium.jpg', 'img06.html', 'img06_medium.html', ]: self.assertFalse(os.path.isfile(os.path.join(dest_path, 'subgal', f))) for f in ['extra_thumb.jpg']: self.assertFalse(os.path.isfile(os.path.join(dest_path, f))) for f in ['extra_dir']: self.assertFalse(os.path.isdir(os.path.join(dest_path, f))) except AssertionError: print "\n contents of dest_path : " print sorted(os.listdir(dest_path)) print sorted(os.listdir(os.path.join(dest_path, 'subgal'))) raise def test_clean_empty_dirs(self): """ Check that empty dirs at destination are removed . This case might happen when (1) the destination dir is created according to the contents of source_dir. (2) Then source_dir becomes empty, either because all the images have been removed, or because tag filtering is applied with different settings. The next lazygal generation should remove the destination dir. """ config = lazygal.config.LazygalConfig() config.set('global', 'clean-destination', "true") self.setup_album(config) pics = ['img.jpg'] source_subgal = self.setup_subgal('subgal', pics) dest_path = self.get_working_path() self.album.generate(dest_path) # all the files in subgal should be here after the first generation try: for f in ['img_thumb.jpg', 'img_small.jpg', 'img_medium.jpg', 'img.html', 'img_medium.html', ]: self.assertTrue(os.path.isfile(os.path.join(dest_path, 'subgal', f))) for f in ['subgal']: self.assertTrue(os.path.isdir(os.path.join(dest_path, f))) except AssertionError: print "\n contents of dest_path after first generation: " print sorted(os.listdir(dest_path)) print sorted(os.listdir(os.path.join(dest_path, 'subgal'))) raise # remove the pic in subgal, and force a new generation os.unlink(os.path.join(self.source_dir, 'subgal', 'img.jpg')) self.album.generate(dest_path) # now the subdirectory subgal and its contents should no longer be there try: for f in ['img.jpg', 'img_thumb.jpg', 'img_small.jpg', 'img_medium.jpg', 'img.html', 'img_medium.html', ]: self.assertFalse(os.path.isfile(os.path.join(dest_path, 'subgal', f))) for f in ['subgal']: self.assertFalse(os.path.isdir(os.path.join(dest_path, f))) except AssertionError: print "\n contents of dest_path after the second generation: " print sorted(os.listdir(dest_path)) print sorted(os.listdir(os.path.join(dest_path, 'subgal'))) raise @skip(not has_symlinks(), 'symlinks not supported on platform') def test_originals_symlinks(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'original', 'Yes') config.set('webgal', 'original-symlink', 'Yes') self.setup_album(config) img_path = self.add_img(self.source_dir, 'symlink_target.jpg') dest_dir = self.get_working_path() self.album.generate(dest_dir) symlink = os.path.join(dest_dir, os.path.basename(img_path)) # Test if the original in the webgal is a symlink self.assertTrue(os.path.islink(symlink)) # Test if that symlink point to the image in the source_dir self.assertEqual(os.path.realpath(symlink), img_path) def test_metadata_osize_copy(self): img_path = self.add_img(self.source_dir, 'md_filled.jpg') # Add some metadata gps_data = GExiv2.Metadata(self.get_sample_path('sample-with-gps.jpg')) source_image = GExiv2.Metadata(img_path) for tag in gps_data.get_exif_tags(): source_image[tag] = gps_data[tag] dummy_comment = 'nice photo' source_image['Exif.Photo.UserComment'] = dummy_comment dummy_date = datetime.datetime(2011, 2, 3, 12, 51, 43) source_image['Exif.Photo.DateTimeDigitized'] = dummy_date.strftime(GEXIV2_DATE_FORMAT) assert 'Exif.GPSInfo.GPSLongitude' in source_image assert 'Exif.GPSInfo.GPSLatitude' in source_image source_image.save_file() # Generate album dest_dir = self.get_working_path() self.album.generate(dest_dir) dest_img_path = os.path.join(dest_dir, 'md_filled_small.jpg') dest_image = GExiv2.Metadata(dest_img_path) # Check that metadata is still here for reduced pictures. self.assertEqual(dest_image['Exif.Photo.UserComment'], dummy_comment) self.assertEqual(dest_image['Exif.Photo.DateTimeDigitized'], dummy_date.strftime('%Y:%m:%d %H:%M:%S')) # Check that blacklised tags are not present anymore in the reduced # picture. def lat(): return dest_image['Exif.GPSInfo.GPSLongitude'] self.assertRaises(KeyError, lat) def long(): return dest_image['Exif.GPSInfo.GPSLatitude'] self.assertRaises(KeyError, long) def test_metadata_osize_nopublish(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'publish-metadata', 'No') self.setup_album(config) img_path = self.add_img(self.source_dir, 'md_filled.jpg') # Add some metadata source_image = GExiv2.Metadata(img_path) dummy_comment = 'nice photo' source_image['Exif.Photo.UserComment'] = dummy_comment source_image.save_file() # Generate album dest_dir = self.get_working_path() self.album.generate(dest_dir) dest_img_path = os.path.join(dest_dir, 'md_filled_small.jpg') dest_image = GExiv2.Metadata(dest_img_path) # Check that metadata is not here for reduced pictures. def com(): return dest_image['Exif.Photo.UserComment'] self.assertRaises(KeyError, com) def test_resize_rotate_size(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'image-size', 'std=800x600') self.setup_album(config) norotate_path = self.add_img(self.source_dir, 'norotate.jpg') torotate_path = self.add_img(self.source_dir, 'torotate.jpg') torotate = GExiv2.Metadata(torotate_path) torotate['Exif.Image.Orientation'] = '8' torotate.save_file() # Generate album dest_dir = self.get_working_path() self.album.generate(dest_dir) dest_norotate_path = os.path.join(dest_dir, 'norotate_std.jpg') self.assertEqual(Image.open(dest_norotate_path).size, (800, 533, )) dest_torotate_path = os.path.join(dest_dir, 'torotate_std.jpg') self.assertEqual(Image.open(dest_torotate_path).size, (400, 600, )) def test_feed(self): config = lazygal.config.LazygalConfig() config.set('global', 'puburl', 'http://example.com/album/') self.setup_album(config) img_path = self.add_img(self.source_dir, 'img01.jpg') dest_dir = self.get_working_path() self.album.generate(dest_dir) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'index.xml'))) def test_dirzip(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'dirzip', 'Yes') self.setup_album(config) img_path = self.add_img(self.source_dir, 'img01.jpg') img_path = self.add_img(self.source_dir, 'img02.jpg') dest_dir = self.get_working_path() self.album.generate(dest_dir) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'src.zip'))) def test_filter_by_tag(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'filter-by-tag', 'lazygal') self.setup_album(config) # tagfound pictures will be pushed on the destination, tagnotfound pictures # should be filtered out. # The sub-directory 'sdir_tagnotfound' should not be created on the destination # side, because it should be empty. tagfound_path = self.add_img(self.source_dir, 'tagfound.jpg') tagfound2_path = self.add_img(self.source_dir, 'tagfound2.jpg') tagnotfound_path = self.add_img(self.source_dir, 'tagnotfound.jpg') untagged_path = self.add_img(self.source_dir, 'untagged.jpg') sdir_tagfound = self.setup_subgal('sdir_tagfound', ['sdir_tagfound.jpg', 'sdir_tagnotfound.jpg']) sdir_tagnotfound = self.setup_subgal('sdir_tagnotfound', ['sdir_tagfound.jpg', 'sdir_tagnotfound.jpg']) sdir_untagged = self.setup_subgal('sdir_untagged', ['untagged.jpg']) tagfound_subdir_path = os.path.join(self.source_dir, sdir_tagfound.name, 'sdir_tagfound.jpg') tagnotfound_subdir_path = os.path.join(self.source_dir, sdir_tagnotfound.name, 'sdir_tagnotfound.jpg') tagfound = GExiv2.Metadata(tagfound_path) tagfound2 = GExiv2.Metadata(tagfound2_path) tagnotfound = GExiv2.Metadata(tagnotfound_path) tagfound_sd = GExiv2.Metadata(tagfound_subdir_path) tagnotfound_sd = GExiv2.Metadata(tagnotfound_subdir_path) tagfound['Iptc.Application2.Keywords'] = 'lazygal' tagfound['Xmp.dc.subject'] = 'lazygal2' tagfound.save_file() tagfound2['Iptc.Application2.Keywords'] = 'lazygalagain' tagfound2['Xmp.dc.subject'] = 'lazygal' tagfound2.save_file() tagnotfound['Iptc.Application2.Keywords'] = 'another_tag' tagnotfound.save_file() tagfound_sd['Iptc.Application2.Keywords'] = 'lazygal' tagfound_sd['Xmp.dc.subject'] = 'lazygal2' tagfound_sd.save_file() tagnotfound_sd['Iptc.Application2.Keywords'] = 'lazygal_lazygal' tagnotfound_sd['Xmp.dc.subject'] = 'lazygal2' tagnotfound_sd.save_file() # generate album dest_dir = self.get_working_path() self.album.generate(dest_dir) try: self.assertTrue(os.path.isdir(os.path.join(dest_dir, 'sdir_tagfound'))) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'tagfound_thumb.jpg'))) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'tagfound2_thumb.jpg'))) self.assertFalse(os.path.isfile(os.path.join(dest_dir, 'tagnotfound_thumb.jpg'))) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'sdir_tagfound', 'sdir_tagfound_thumb.jpg'))) self.assertFalse(os.path.isfile(os.path.join(dest_dir, 'sdir_tagnotfound', 'sdir_tagnotfound_thumb.jpg'))) self.assertFalse(os.path.isdir(os.path.join(dest_dir, 'sdir_tagnotfound'))) self.assertFalse(os.path.isfile(os.path.join(dest_dir, 'sdir_untagged', 'untagged.jpg'))) self.assertFalse(os.path.isdir(os.path.join(dest_dir, 'sdir_untagged'))) except AssertionError: print "\n contents of dest_dir : " print os.listdir(dest_dir) raise def test_filter_and_dirzip(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'dirzip', 'Yes') config.set('webgal', 'filter-by-tag', 'lazygal') self.setup_album(config) # We need at least two pictures to display, because lazygal generates a # zip archive only if there is more than one picture. tagfound_path = self.add_img(self.source_dir, 'tagfound.jpg') tagfound_path2 = self.add_img(self.source_dir, 'tagfound2.jpg') tagnotfound_path = self.add_img(self.source_dir, 'tagnotfound.jpg') tagfound = GExiv2.Metadata(tagfound_path) tagfound2 = GExiv2.Metadata(tagfound_path2) tagnotfound = GExiv2.Metadata(tagnotfound_path) tagfound['Iptc.Application2.Keywords'] = 'lazygal' tagfound['Xmp.dc.subject'] = 'lazygal2' tagfound.save_file() tagfound2['Iptc.Application2.Keywords'] = 'lazygalagain' tagfound2['Xmp.dc.subject'] = 'lazygal' tagfound2.save_file() tagnotfound['Iptc.Application2.Keywords'] = 'another_tag' tagnotfound.save_file() # generate album dest_dir = self.get_working_path() self.album.generate(dest_dir) try: self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'src.zip'))) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'tagfound_thumb.jpg'))) self.assertTrue(os.path.isfile(os.path.join(dest_dir, 'tagfound2_thumb.jpg'))) self.assertFalse(os.path.isfile(os.path.join(dest_dir, 'false_thumb.jpg'))) except AssertionError: print "\n contents of dest_dir : " print os.listdir(dest_dir) raise class TestSpecialGens(LazygalTestGen): def setUp(self): super(TestSpecialGens, self).setUp(False) self.dest_path = os.path.join(self.tmpdir, 'dst') def test_paginate(self): """ It shall be possible to split big galleries on mutiple index pages. """ config = lazygal.config.LazygalConfig() config.set('webgal', 'thumbs-per-page', 4) self.setup_album(config) pics = ['img%d.jpg' % i for i in range(0, 9)] source_subgal = self.setup_subgal('subgal', pics) self.album.generate(self.dest_path) # FIXME: Check dest dir contents, test only catches uncaught exceptions # for now... def test_flatten(self): config = lazygal.config.LazygalConfig() config.set('global', 'dir-flattening-depth', 0) self.setup_album(config) source_subgal = self.setup_subgal('subgal', ['subgal_img.jpg']) self.album.generate(self.dest_path) # FIXME: Check dest dir contents, test only catches uncaught exceptions # for now... def test_flattenpaginate(self): config = lazygal.config.LazygalConfig() config.set('webgal', 'thumbs-per-page', 4) config.set('global', 'dir-flattening-depth', 0) self.setup_album(config) pics = ['img%d.jpg' % i for i in range(0, 9)] source_subgal = self.setup_subgal('subgal', pics) self.album.generate(self.dest_path) # FIXME: Check dest dir contents, test only catches uncaught exceptions # for now... @skip(not has_symlinks(), 'symlinks not supported on platform') def test_dir_symlink(self): """ The generator should follow symlinks on directories, but should not get stuck in infinite recursion if two distinct directory trees have symbolic links to each other. """ self.setup_album() pics = ['img%d.jpg' % i for i in range(0, 2)] source_subgal = self.setup_subgal('subgal', pics) # cp -ar to create an out-of-tree source dir with pics src2_path = os.path.join(self.get_working_path(), 'symlink_target') shutil.copytree(self.source_dir, src2_path) # symlink src2 so it show in album os.symlink(src2_path, os.path.join(self.source_dir, 'symlinked')) # symlink src in src2 to check if the generator goes in an infinite # loop. os.symlink(self.source_dir, os.path.join(src2_path, 'do_not_follow')) self.album.generate(self.dest_path) # Check root dir contents self.assertTrue(os.path.isdir(self.dest_path)) for fn in ('index.html', 'index_medium.html'): self.assertTrue(os.path.isfile(os.path.join(self.dest_path, fn))) for dn in ('subgal', 'symlinked'): self.assertTrue(os.path.isdir(os.path.join(self.dest_path, dn))) # Check symlinked root contents dest_path = os.path.join(self.dest_path, 'symlinked') for fn in ('index.html', 'index_medium.html'): self.assertTrue(os.path.isfile(os.path.join(dest_path, fn))) # Check symlinked subgal contents dest_path = os.path.join(self.dest_path, 'symlinked', 'subgal') for fn in ('index.html', 'index_medium.html', 'img0.html', 'img0_medium.html', 'img0_thumb.jpg', 'img0_small.jpg', 'img0_medium.jpg', 'img1.html', 'img1_medium.html', 'img1_thumb.jpg', 'img1_small.jpg', 'img1_medium.jpg'): fp = os.path.join(dest_path, fn) self.assertTrue(os.path.isfile(fp), "%s is missing" % fp) # Check that symlinked initial root has not been processed self.assertFalse(os.path.isdir(os.path.join(self.dest_path, 'symlinked', 'subgal', 'do_not_follow'))) class TestSorting(LazygalTestGen): def setUp(self): super(TestSorting, self).setUp(False) self.dest_path = os.path.join(self.tmpdir, 'dst') def __setup_pics(self, subgal_name=None): if subgal_name is None: subgal_name = 'subgal' subgal_path = os.path.join(self.source_dir, subgal_name) os.mkdir(subgal_path) pics = ['4-december.jpg', '6-january.jpg', '1-february.jpg', '3-june.jpg', '5-august.jpg'] months = [12, 1, 2, 6, 8] for index, pic in enumerate(pics): img_path = self.add_img(subgal_path, pic) img_exif = GExiv2.Metadata(img_path) for tag in ('Exif.Photo.DateTimeDigitized', 'Exif.Photo.DateTimeOriginal', 'Exif.Image.DateTime', ): img_exif[tag] = datetime.datetime( 2011, months[index], 1).strftime('%Y:%m:%d %H:%M:%S') img_exif.save_file() return subgal_path, pics def test_sortbyexif(self): """ It shall be possible to sort images in a gallery according to EXIF date. """ config = lazygal.config.LazygalConfig() config.set('webgal', 'sort-medias', 'exif') self.setup_album(config) subgal_path, pics = self.__setup_pics() src_dir = Directory(subgal_path, [], pics, self.album) dest_subgal = WebalbumDir(src_dir, [], self.album, self.dest_path) dest_subgal.sort_task.make() self.assertEqual([media.media.filename for media in dest_subgal.medias], [u'6-january.jpg', u'1-february.jpg', u'3-june.jpg', u'5-august.jpg', u'4-december.jpg']) def test_sortbyexif_galsplit(self): """ It shall be possible to sort images in galleries split on multiple pages according to EXIF date. """ config = lazygal.config.LazygalConfig() config.set('webgal', 'sort-medias', 'exif') config.set('webgal', 'thumbs-per-page', 3) self.setup_album(config) subgal_path, pics = self.__setup_pics() src_dir = Directory(subgal_path, [], pics, self.album) dest_subgal = WebalbumDir(src_dir, [], self.album, self.dest_path) dest_subgal.sort_task.make() self.assertEqual([media.media.filename for media in dest_subgal.medias], [u'6-january.jpg', u'1-february.jpg', u'3-june.jpg', u'5-august.jpg', u'4-december.jpg']) # page #1 page_medias = dest_subgal.index_pages[0][0].galleries[0][1] self.assertEqual([media.media.filename for media in page_medias], [u'6-january.jpg', u'1-february.jpg', u'3-june.jpg']) # page #2 page_medias = dest_subgal.index_pages[1][0].galleries[0][1] self.assertEqual([media.media.filename for media in page_medias], [u'5-august.jpg', u'4-december.jpg']) def test_sortbyfilename(self): """ It shall be possible to sort images in a gallery by filename. """ config = lazygal.config.LazygalConfig() config.set('webgal', 'sort-medias', 'filename') self.setup_album(config) subgal_path, pics = self.__setup_pics() src_dir = Directory(subgal_path, [], pics, self.album) dest_subgal = WebalbumDir(src_dir, [], self.album, self.dest_path) dest_subgal.sort_task.make() self.assertEqual([media.media.filename for media in dest_subgal.medias], [u'1-february.jpg', u'3-june.jpg', u'4-december.jpg', u'5-august.jpg', u'6-january.jpg']) def test_sortbyfilename_galsplit(self): """ It shall be possible to sort images in galleries split on multiple pages by filename. """ config = lazygal.config.LazygalConfig() config.set('webgal', 'sort-medias', 'filename') config.set('webgal', 'thumbs-per-page', 3) self.setup_album(config) subgal_path, pics = self.__setup_pics() src_dir = Directory(subgal_path, [], pics, self.album) dest_subgal = WebalbumDir(src_dir, [], self.album, self.dest_path) dest_subgal.sort_task.make() self.assertEqual([media.media.filename for media in dest_subgal.medias], [u'1-february.jpg', u'3-june.jpg', u'4-december.jpg', u'5-august.jpg', u'6-january.jpg']) # page #1 page_medias = dest_subgal.index_pages[0][0].galleries[0][1] self.assertEqual([media.media.filename for media in page_medias], [u'1-february.jpg', u'3-june.jpg', u'4-december.jpg']) # page #2 page_medias = dest_subgal.index_pages[1][0].galleries[0][1] self.assertEqual([media.media.filename for media in page_medias], [u'5-august.jpg', u'6-january.jpg']) def test_sortsubgals_dirnamereverse(self): """ It shall be possible to sort sub-galleries accoring to the directory name. """ config = lazygal.config.LazygalConfig() config.set('webgal', 'sort-subgals', 'dirname:reverse') self.setup_album(config) subgal_names = ('john', '2012_Trip', 'albert', '1999_Christmas', 'joe', ) subgals_src = [] subgals_dst = [] for subgal_name in subgal_names: path, pics = self.__setup_pics(subgal_name) src = Directory(path, [], pics, self.album) dst = WebalbumDir(src, [], self.album, os.path.join(self.dest_path, subgal_name)) subgals_src.append(src) subgals_dst.append(dst) src_dir = Directory(self.source_dir, subgals_src, [], self.album) dest_subgal = WebalbumDir(src_dir, subgals_dst, self.album, self.dest_path) dest_subgal.sort_task.make() self.assertEqual( [subgal.source_dir.name for subgal in dest_subgal.subgals], [u'john', u'joe', u'albert', u'2012_Trip', u'1999_Christmas'] ) if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/sample-jpeg-comment-unicode.jpg0000644000175000017500000000054712301071671023451 0ustar niolniol00000000000000JFIFHHCtest jpeg comment éù  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/test_gendeps.py0000644000175000017500000001241512301071671020512 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import time import unittest from __init__ import LazygalTestGen from lazygal.generators import WebalbumDir from lazygal.sourcetree import Directory from lazygal.genpage import WebalbumIndexPage class TestDeps(LazygalTestGen): """ Dependencies of built items """ def test_second_build(self): """ Once built, a webgal shall not need build. """ source_subgal = self.setup_subgal('subgal', ['subgal_img.jpg']) dest_path = os.path.join(self.tmpdir, 'dst') dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) self.assertTrue( dest_subgal.needs_build(), 'Webalbum subgal has not been built and does not need build.') self.album.generate(dest_path) dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) self.assertFalse( dest_subgal.needs_build(), 'Webalbum subgal has been built and does need build because of %s.' % str(dest_subgal.needs_build(True))) def test_dirmetadata_update(self): """ Updated directory metadata file shall trigger the rebuild of the corresponding webgal directory. """ subgal_path = os.path.join(self.source_dir, 'subgal') os.mkdir(subgal_path) self.add_img(subgal_path, 'subgal_img.jpg') # metadata must exist before creating the Directory() object (md files # are probed in the constructor. self.album.generate_default_metadata() source_subgal = Directory(subgal_path, [], ['subgal_img.jpg'], self.album) dest_path = os.path.join(self.tmpdir, 'dst') self.album.generate(dest_path) dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) self.assertFalse( dest_subgal.needs_build(), 'Webalbum subgal has been built and does need build because of %s.' % str(dest_subgal.needs_build(True))) # touch the description file time.sleep(1) # ensure time diffrence for some systems os.utime(os.path.join(source_subgal.path, 'album_description'), None) # New objects in order to probe filesystem source_subgal = Directory(subgal_path, [], ['subgal_img.jpg'], self.album) dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) self.assertTrue( dest_subgal.needs_build(), 'Webalbum subgal should need build because of updated dir md.') def test_subgal_update(self): """ Updated subgals shall trigger the webgal rebuild and the parent directory index rebuild. """ source_subgal = self.setup_subgal('subgal', ['subgal_img.jpg']) dest_path = os.path.join(self.tmpdir, 'dst') self.album.generate(dest_path) dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) self.assertFalse( dest_subgal.needs_build(), 'Webalbum subgal has been built and does need build because of %s.' % str(dest_subgal.needs_build(True))) self.add_img(source_subgal.path, 'subgal_img2.jpg') # New objects to ensure pic is taken into account source_subgal = Directory(source_subgal.path, [], ['subgal_img.jpg', 'subgal_img2.jpg'], self.album) dest_subgal = WebalbumDir(source_subgal, [], self.album, dest_path) # Subgal should need build. self.assertTrue( dest_subgal.needs_build(), 'Webalbum subgal should need build because of added pic in subgal.') # Parent directory should need build. source_gal = Directory(self.source_dir, [source_subgal], [], self.album) dest_gal = WebalbumDir(source_gal, [dest_subgal], self.album, dest_path) self.assertTrue( dest_gal.needs_build(), 'Webalbum gal should need build because of added pic in subgal.') # Parent directory should need build. parent_index = WebalbumIndexPage(dest_gal, 'small', 0, [dest_subgal], [(dest_subgal, dest_subgal.medias)]) self.assertTrue( parent_index.needs_build(), 'Webalbum gal index should need build because of added pic in subgal.') if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/sample-author-badencoding.jpg0000644000175000017500000000114612301071671023171 0ustar niolniol00000000000000JFIFHHExifMM*HTC nx(2;iHTC-P3300HH2007:06:29 07:01:12 0210 @2007:06:29 07:01:12C  }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?lazygal-0.8.2/lazygaltest/sample-model-pentax1.jpg0000644000175000017500000034415612301071671022125 0ustar niolniol00000000000000JFIFHHExifMM* (12;  iĥ^*PENTAX PENTAX K-7 ,,K-7 Ver 1.10 2011:08:16 14:35:50Michal CiharMichal CiharPrintIM0300!  4   D  ' ''''^''''"'0221 .  6|R>0100@    2# 2011:08:16 14:35:502011:08:16 14:35:50 dAOCMM^[*-#2  #2 !'(-"2347= >?@2AGHIM OP\2&]p`bghikIBlmnopq8rstvwxyzC 2 M J M  M  4 H V6 .@RZ!n"&  '@(@T)*Y+,. -d81&9 )  |t @0qa&@aa@dR0R(sitxi4;@)s ڀ09~`TÌ)F:Q+i6-30N/|2,q5)e>8h&;W#ٷ\>N!]AP <DS먶GG d} !H 3 @v3399846" q+V%zCUĝ2d 9. *D > %# NQH: '=H -F9. 8JB5 $2W H29. *9. *&q @@@ @ #2:xhF]\toVq= 5 ]kBXpiB\Sڂ/1sf}[Ҳ_u~ջH,m .a^;x,Wu/QY/Lwt{R0,ۇPڞw*L-YvSrM(v=-%R<q ?H ,dTSJ ?l/KU=* |yM#/:VhF ]״. 5Tcs5 X kA"Xkܷ÷0.0!Yj|fc}[ 潲栉!hՓ,* CaTxiV)冭i埄 к/(`={HRS 傇Dyl*Gb?* vGM)䵂`䞙l Z%DE<00qfH̽۔C2i j~?G(l5KY/t#:YRIo,dFv]-Y+5u kOAdXYy,#r8ú0 Qf|.[uXg R\՛3- uaxVǭ_0%, {sвg/N\{Q/fԞ/\*i(BbSvM:a\=%8]E hDsW5e  #2:rhF]XtoVp= 5 ]kBXqiB\Sڅ/1sf}[Ҳ_r~ջH,m )a^;x,Wu(QY/Lwt{R0+ۇPڞw*L*YvTrM(v=-%R<q?H ,dUSJ ?l/KS=* |yM#.:WhF ]״.!5Wcs5 X kA Xiܷ ô0.0"Yh|fc}[潲栉#jՐ,* CaTxiV$冭k埄д/&l={HRS傇Kyl%Gn?*vKM$䵂l䞙lZ%GE<05qmH̽ ۔F5i m~?G(l 5K@5m# :BRIo5dFl]-@25j kOAdXGy5#l8à0Jf|4[oXgT\՜3+ raxV ǭT0%& qsпg/N S{Q"kԞ/Y*i-BfSvM])_=%8]ERhslދ5  #2oq\ModoZ#o0ho&Ƒoo4oLoio@"o_konwnmnCOn9n/7=njndnCnZnNnn m`Im ~mU?m׬dm͊mmmHle-l[>l1cgl'ylPllKlܜl9lfl(ktkjk@uJk6Sk-8kakkkk$Ekk;j}p`jSNjFj<jDjj:j'cj=jjsjJGiciY5iObi%ii"i9Fiin4iE}ihrhhh^Ah4Ǝh*7h4|hhih@h_@hh6g{gQgGg=7gCg dgC1gZ~gȰgggBf`ۋfV20fL yf"gf~fUf]fۊf3fxf֡es-eie_c\e5ye+P2e{eeee_e(edxzdntdDSd;d^dddud+dddpYcaNcTcJtc cc c'Xc=ccswcIbqbgb][b3b)b"vb8bbn-bEZbbavqalaBa8,a.4Uaaia@pa^aa/aT``U`K7s`! `d` C.`ZW`ְ`̏`r``d2)Z V0g&~U (Q֞-~w ~mb~Cy~9P+~,P~~~~Ѵ~(~*~S}|t}RS}I}?}} %}R}+}} }w}N|b$|Xm|N|$||&|=|'|sl|I|{u {k{A{7&{-"o{8{{n {D{{!{nzzzPzFz<;zzi z@iz^z˵zzMzyc#yY7hyO y%dyCyZLyyڏ"ykyyxr2xh Ox^gx4~=x*UjxxxxNxx-vWgvMv#vvKv+v9vfvwvNupufJu\u28u(au&u=uusEuIu;u`tytotEt;"Dt8t:tnctDtƣttGts~5sTbsJ;s sis @Fs^sٵ4sό}ssrqrg7Ar] r3d7r)C|rYrrr@rr6r2qvqlgqB~q8UCq/q1q~qqq-qBqbpy0pUPypHp>pp ]pъp(3pxpptpSOe\O[O12O'{O̠O+OO_OwONOzNtNjN@N6^N,&N=NuNrNINNYNM}MStMI!M?8MM nXMDMԣMʺwMMMLb[LX;LNL$ivLGL^L-L݌ZLLLqKu6Kk KAd,K7CUK-YKKpKKK/K2TKJzgJP~sJFTJ=J.J WJJJ-rJ JbJy)IcPVIVILI" III((IQIItIS HsHiH_+H5PH+̙H+HHHwHN*HSHGxGnGDϷG:&G=%GRGrGIGɠ GGG$FamFW!FM8F#FmFDF'FغlFΐFF EpܰEf;E\&E2ioE(GE^E E듳EE!EnE6Dy DodDEBD;YD. DiDDDD2MDDg#C~~hCTTCKC!CLC C"C-kC CbCyBqPOBdBZ=B0jB&BB(BNBBt@|g@RϬ@H&@>=@K@ r@I9@נf@;@@@J_e!_[88_1a_'m_D__E_ܐ_;_`_ܩ^t;^j^@iD^6G^,^:^c^^^^G^6^ 5]}db]SB]IY]<]F]]4]}]1]]g]~A\bT\Y7\O|\%\\\-@\ \b6\y\W[r[h[^C[4[*1[(~[[[t[SB[[0Z{yZQZGZ=+Z]Z Zw3ZNxZȤZZZ\Y`υYV&2YL={Y"YrYIY_Y۾YYzYʣXs!Xi8X_^X5mX+DXuXXXXYX܆X;WxtWnhWDGW:^WXWWWwW߼W6W Wd[VaBVWYVJvV VV -VZV1VVgqV}UpTUg,U]UU3U)UpU,U Ub/UyTUWUTvsTlTBT8.T.(WTTTtrTRTT)TVSSUSK+ S!SS w(SNQS֤S̃S SSRd&+RZ=PR0R&rRIRRRߕ*RSRʘR!Qw8QmQCmQ9D%Q,RQQQ QŶQQ;$QmP|hPRGPH^P?PP 'PlPߕP6P PkPB/eY&/Xo/N/$/ /ڳ/1/!/gn/}/T.u.k.A.7 .-i.,. .b.yM.W.#.h-z-P-F-<(L--"-tk-R-˩--O-,c=,Y+j,O,%,w,NN,,ڃ<,e,,+r&+h=I+^+4r?+*Id++++H++!>+8g*{*Qm*GD*:K**9*f*ů**;*J*h)`G8)V^a)M)#))E))6;) `)k)B(sY(fD(\(2:((c(ڨ(1((gG(}(T5(b'y'o'E';F',' 4'b}'x'W''A'&~7&T|&J/& && t@&R&٩6&π&&%q%g+C%]%31%)w~%M%%%ޚB%%0%&y$v<$l$Br$8I]$/$3$x$$$!$8\$#m2#UD{#H#>## _#ń##;z##h#G"d^^"["1"'u"""6" Y"k"B"Yt!t!j!@!6X!,ځ!1!w!f!}!T![! } Sv Iտ ?,  - bZ x W ʮq ,?bU?X/?N?$p?{?R?/?݀T???s>u*>k>A.>7wW>-M>>r>왻>>)>&V><=z=Pr =FH===(= Q===! =?==m+7<;ENcTEI^K;QFOIIɛ>`쌞'?"L'?\V$'=<5^zw , ~C/EDxQp~_zOD/0/}_z?A?.{*H)+nvS^x8~?>jr 9PE8P(8P!i€)˜ZQ@G0SERHGYu9TtrnOcqەc q8hp(@մ](< @wP7& BA 4nS?=f b%= !Z04&M[ 9&((Z( (`%%y}jX Vvc)vs5!ƀ, S 7"e<ՅE8u;4M G@#ԥs 9\( u0S@LZZu(PPHFFy;G~q[ }}4_*[lc#XWzM8MɥP\UP0omZB?Wor]r=F)uÞdD֘mPc [O@PM)<[LnGN@ h=h1l h@)c<S:(E8S”P8P:F tPs €A攞(qxA4pc0C›Ly =M+cv}*h'#;谮H ;!GJS@#1 N3)P,oEx)XJ/R,H~TDdЇl%R@ PEP@Q@%SRsP!@nWZ4)q97<=U@ :ӛTjx p p( p4(0S:)Cb44flRP$*q>"YC5" qIS ~8!@(KP1 >cQ 8s: K)ɧ[hLv]8rGhom>ՊM% (()( jYA1> 0?6y.(R8tCs'QҜ(”P8S”pP8P!EI@}&#i卛cUo2S0(|qV-8ÿ4 eĜMRuώVT.I vSޓV H)Ԁ8( (JJ))䎄r HbWBNAV:3N \`8iS NZP84h<x4vif L&M9sRDn2y!!pjS}GdI<Zwl1 Prwp$E@vSR[c2DQ~= 8Udfv22jV0pc)P#@rzT K43}MNƝ 3ޣ-di\4ɚnZ($dRp*(K1 Ż$$ 1 fYPy\/ Tڹ)*W#O;h(Z( J(^郂* mnVhamp9^٭(_'o˜Ru85858pj]Kr1@RC LPe:6\(#f[=Zϰ5IWBP1 Tm!S$z/ `Z~0)8)SowmːzjQ1I!` ivADv/\UdȎkprd KrB5 *,}h?U0` 7V7uObHS15{,r7$*łr~(>G?$,#hE8#_I6:ly``6ӿ*h ( "@%P@Q@ E%<,OåWa3Y7Jb6}42WY8njАn=87=i@F`(jRzɑO@N @ O L3*aJ ʏA,p? >ð҅ENJ\`(j\un}FaNn EFm]R5!KG7Zpc?1WxÑY_atED 9'Q&m"m1⩋ m%dyGN"}ӡ8ETI$Z /C3o`=im @ bHܫ*Ae+hu+R5A&=y 6+4F"N''@Zk^[L(.KЮWaPUR$wGegA;↘ӊS`zzS(((i/RGM5 12*F@єl̄!g!CӃ̜,T$ߑ@ N@r{Eu)Lȧ&~҃-Hgvr;=X>$vLWhϵXYZO4je`=EKϵLc֐Ȫ2\~p#i<#`R{v; h>;1μֳCQ%`M<@HO bJ [&,Ux6 a'n)x 6FF2?:LdbA*)'9T 4O䟐uU[-{fG}0(\]% ;*uG't$lS߽R.]K:5cյbSfC4枍%DGP!ܐ*)ph2*HȨ̻( :UC< -̦ؼ[PT$r VN\P߽)()(8]*O1CTڐq P*(ЌTcU i5 Ji^>ii*[ppUK:04I69B#1pKvPaSC_zVNf ރJg*NH H%Vzqcp1' M q\zG1֒2!3 w^# }MuʎU[<{ELdNWF8 3RH(1`A@bD*8R644noSLʸnT^D[wP!CRLJ 87Žjbc49 w9ҭ"**@ Zɿu p,JiVm9*JCYwcU$rUӱaNT+2ƷQ@ EPI@%%%!mgߌHc麥7qYw @=_ycI!5,748#J1KFrj2#xozuB;M1c>b3 d` 54r:|F|Q`"K1r<= !YO4:)YN\Jz`2wdy=(^EGH[)p`?w2EE]FSs2T}XbYX&;.6I"p7Kѣ(pJn`ӕ[p4:Ts yj4F<ݕWwcj~.Z(vPHslJkK6 |j[fPszY7st%TbzsBT N V@i9b1Hc6H_LbUU1)@udZnDݐ([)U%1z>襬DW[QoH{qR#ݯKs Iȕzԑ},ysl~F"3&} c3NoSBBrl{V#ȾhnÌniCg m\w6JM:HdKtgE TƆ p4iuN' ȦN ϸL)CpWoΐKOւp#3 C4y& qR ޑ=Hۑs A `5IOMz!qRF@2hK|}*brpGcPJ$'ӃKpNqHdm SB[qR==)H,H,6/&R(m"O6l$OiԒ=) ϵ +>Z#OSJn=ěboʦ ZX$Ef Nx¡ƑhF;\N `21M(U繨;b fBTp[#*e蹍QYni-cA9(ؙ!^ I@HN)AP26o`W:ӵĭeA&R3wX'k[7gt;*{[nU.+/'*e4j%#H~FqY'S}+F5(}*!{:) 4 $c}WKh̲?SXFpi~?wk#Z9#Z@xQ& VZ/HTTf8S&R vYF|Ij X2?&[%E@R  J)()() yFH=('H8(= igRN1S\z q@ 9N ב*@~1ҁ <1ieU8!0OBĈJފ8c"!jn;7mΡ֛ .Z@>EVe]A8p͎LM yFE!SLuYFԓj-ȍX6FpF1T%]C,AvCx/mX1?1+_Re,8*閡PcV+i-AEJLLQUl*Ԛ%1>ߥ0VRpÖ2Ʀצ*\qKfG LQܠ2Cw1 b%LG5G97 2@'}3*ղ,d: n(RPI@_ʆto-3*zA$RyT.Ť0h)n2l0QDBGn {=vv/w^:vx,ߍ&#^OLA@7t4W4McJ㰢H(9pjerz)1#Vt؎c'UrᦗN0ZD@2줎NE[8J-%PRPM4#lvSI&'r6֐#kOEE'ҭjt'NߌzV;_:/dO&ʊ)73`,5"ڃ̮{ذQ; PBwo}&O2Lu ~4)5m}ٔ"W$t T`$)(RCM41(Fb8 &'66?gkȦw; [SRN f?ZqӸXbe$6'ʘ =)O!ǵW1HMN'V=iVun}PLIFy1ɠf|11Ҵ!>8zrߥ !)?S+yA&~e/HF7feQՅ&!qQ۲sKĈ/~w&aG֤1@.h.3@ iA☇Vx(Q3rBd'TL)W4Ć!7SG#{UՈ)s$`jss@ ^J}%%#֚")fuUI8aH?''&F<1dQ /LL6G"QnzvpoB#֬ )N(O;m&RBJOmѹON%"NzO4X/~(XQLkn5fmޭm.d/+6>]$, |7\ɧ'}3R:pq@xSY9^gIJĻ~*i[>`שj&C>=4ĵ|}[2p77R#QAD1G4׭8d4p~EdT(RPpjL'Jp*QƄ֯0B(*@8=2;T hռg&k ;~e2 <l*礆DVݜ\qSơAM'Z.bUv#>q1$Z.H!+|Tu@Qf'c(͏4)M4qju hi[QzV*p7B*r /cJFaҥF{~Qpɩy#W&Li|P^H"HD2V4DiJ)IA#u rG7zA \4@@TL 0FV{nT*px4Ixq@dc@qȩ-¬M:)PkVJX }Hǵ)EFbt8`E;r"ϮiB#1GZl@),|+TryaJqjSCh!#;84;94-B1JM KZ-)h E4ɉCNE(рrRY8IfS;d8c;$gցdĺʙ]q;Iǧ=ldЉdf\gf#/LCJ[WˋpOG>)wv/GLb_AUk뵴1ՏSJryN_[ )Q@䙠DR>zc7E&٥nG%'S0>_Fdbpp8Ddeop1@kb(9=e1֚%a~tg ϵ0^HC:Cު/$e}E! S!4czpr:օ^y_jhfQ,C ?P#D-!@`r5lESZuF^)&En~3>U:Uhfc$R9ǎ)3zvp(* (pA9 <4 ? ǵ7#c)&-($p--P0AOý+Q+J%lc-vɳi?#ݟ֭0I(9͝2i%!,O;Z"$fD2})BxܿJZQ_J| [K8ORrZMrCf8HSP1i$֐=J*}*Qq*}G4;[H>E9fZr\{+Њ}Yh-Pɤ9aw/BXQ7j>GIҰV7IHH۹szW' RZ0s aQVD#J82 5l`lyaޘvˋ:ؑ ?ָ3iC ( (fbR')80jru,  ǓL ֱ8֯,X☇: `Hڞ>dgI#Y WPkIpU°*D9=@#fNJBWڙDjURx%X[T9ۺj8暅sNrIUȭ9Q3=qNjZ-;!QҀ8RMG PHvPTmHcVIǽ];qE-1 K@ E 1@ (hQ*ܢHʍ"ٕ?G5$3I]Rݣ]q ZRMN@"ZK>b` n}_Ayy֤ tXP=?՝u=֡xtH/GM4Q$1,Q(TA0 ( (3;@5"p(yhHL 8: /L ;TRT!rdPJ{RiU@ K s2@~heznH=PZCO8<{  J%-4}FJiffxOz8zPDcҀiA֤q-KsOb Ab P@P<軟¥OQҊb)i Z(R(Gd+dpE@G\nKIޅKG,OuCu]%ae hPj<^Z@#d7vW8^!Q+ po5>¦[;,1w'szE ( (<=z wzx'QzӔd.8UW# U` zn=H Bu QS ^E MA{ji @7BQuݘCJNbJ@HcSZ;2fgJ0 cSP4Pu3I`ǡך\&C$pҖQRP0iwZ#%|qIl-IN1ihQH ($ )۷㹃Twsҵ&G v?fl!cjٻU1M|d/SyM|UdPǰivo뫰>7kEh$- ((,S@L"sSF"85810.EҲf 2?*pU= /L`wh.Wpw)d)MC  cN)dc&)ufL{(+2}o]8يԫ/QJ7E'$ oʟT•f(jpjv][ Qށ\_0Q .X®Z$hy b8qTV= >'by vt5%Mø}LjڳRH`O< E>ԡG@ ɑ"OSLnKKB}Ԅ!r qMl|}pxYe 0" 1UE$l}Fu + ǼgC>+xP0 ( (<: 4]P!^⧏ʟ˜ NmI@)8#R ҐuU3\=iJkEv8@ Mܹ7%:վ-:v1/?=:- (((#4-@-J>iT|Bj*@J)*D<eN*hE!0S;yFEOjX G(n1@>):rR'Q@MHiQ0P8+/7)n=^΁n]jh[]K|[ȸ )jCܙ >&h8bOjѣl08SJ+{wC$?uQ]AqTdaJ8@js u8iT(iO @Ȏ}hњ(4F@ x ;=!BK CkQ)3JRI4!B)Z@p2h-Q@Q@G8%aQ4cb,'EH)PsR@ >8Ԁs@"R(hҀ^i/4>\ɖ3\RejxFRQL*$m$g(U\ Wp CCZ'+ JbB/xq"".$F<rp)3ɡAse()恊cv9EN)(r=h @SRhESi :e# }h1(q1@ HҘEj qJ(Aqޕ@uQ@Q@FT1L@!=ZSb :ƧmCi)F)ɠ (@pj\P3L@A4As@ K4k$eHs5ܯf"7q]۾ B8>\@3qjѰ:>)ބF A@zjg<rO,\g#n;~5iH) w,vPFxd'l؟ZpPK1K@I=P,U@Oj-7E0 (Mn@~-Є$L*Ҧ( Z( ( ("Zz{sNFTTaU0p"c52P+֤V8 `.) d^XUyL IîFsREGRlb2DXݸzo'{L HI8l} 9V;^O@99hj`[B@ +mqUp44]*J@PK|銎(JC_Z)r|=j3AġTlKqAB@Q@Q@F:T1R0jJ@I(L vW^3Vq-@[\RLҁE+na4P8ROFx R!$'BEJ42BǨUJ)h C@!@F@IѿZP:1|Γߥ8\?)~ރg {/sZSKkTjߡy1R@ǣ|4r*ESW! Mq0GZzD8Ӏ4"qRJ(~4QO0L(v Zd#UIbYi+@SHbQ֜:g(\~RFr:PrAQ0'ާpm@Ҁ Zpx8JJpAJ)h(@#; qNSPPJvsHhJnlP֧\/cT!ڜ% }LFH4pcOR4.Q0&&9<Ң)~t`gPy5\x"{Ӱ 1u 4'3@:G)9 P[&p#[F)$~H4#ӊ@5xLLӀNP^GZzjdݿC@3֖-0 ( i4_Vh]9tQOQHLXiT HYtiҀḠf#Aby>l#M&>Y>HOS0L18!Ou?4Q@h_4fu?/zp9&Hp9 df:i:ӶL@!(q]J{S1SN6HTC.rȧڥ{I( 4HS2^[bکKz}dSBh|S{qޥC"4} S8`hU)0{%Ҍ8Bh#Cnf :KD8emώMY[ҳh$Td \B3SbO$2*JgjI) 'aY;0gbNI5-֞ 0vh(3I4<? @ K@ K@Q@Q@㝩@4J:fͤt5E1*=1Lrc"G rL>WduqzOP^X{Q=ʻr=M'P4y s#(yPEMU"0Xz O4m8"b7Ԡ) 9JЃ.ǭV!C 9洴Pu{ 1T Yt ɴ Zp%p=h}*plq@)[&V}J/)V!ӁC44f4P;}ibYcJ(K@Q@Q@?КiH\uMl7?֣p@4 pglG6UFgixU#XtoΟaӟq3X,O&sGQ7q%`*1Gқr ԥqܰ@$04o*FOWuGӸKQun-NE55 e5d(}*z>jBj9cԢ–S`f \0 N9@-w50& L:P118@Je@5"LXE3tfNi1nrۀj:Y O T!is@iT䏭0,AҬZ(uQ3@ EPEPEy)  4SMQLk0 ۽Y0 @)ٷi$^RqZ8aI)xm4L_ ɼ(),O10)Sɵh uPCхJ[Hc=  m1P0~5b-N!R%17xI"z;zĘ'Utqaiy` b*7ȝ1U"6!8*'m7e9*ye@TP"+OZ,e+m0 !z44$"O$Nh1E;)CRivis@h-@rv`Xjz*)d*/' P&*~ ԀP@Š((=vTy~lP@?!`{SM{ a+ ~"`\ ̭Jøҗ~{eOO9Oe:zw]EfI)`CUr'nFA E!hQ.W*j](5hR?,"1*ߑL _\GeũDir=HzRDle:S^(P1IRFhԪ(2hP2zg-#ɲ2]=ĜqQ@ݞ)76pWU ,DpN{\ *ySV[z2GQPh٦3qR# IB`H98J0'wGU4s+{ӅPg8M1@N(!va]lF]lyBzP =zPf£m=OҰțO08iXwm)J4X.(P8+чcvG4kl(Ml?JWx梳C1Nɤm [O$R[DtB4 眏zb% T< @ ,zV@KiԌ}(ߟΤP m(@;Ӂ?0Gbh@4d~A87)8.:1C@:;~t??Q@ /UM<_4 V{">aZ?E?+ԟȿj, RϏERPX/_!"qȿj,O˗ESN/֢0qsȟjcktR : Ir[U'Yj}~o;}ƸO'jPgY;AS֧ "/{N{H/ ]?Szƍon=Wv?Z N Bn_;̆Ȱ?f=lSǎ}tƀx7#4Wa6`7h s屁Ɯ|hJl:oƀn7cK 7c@ C7cH|]cWV>tp5|U~?]t2~n4:.ϯ8|FG¯`Իw7{Wt/)nS?? ˿7K c9w1G?tOtZxcRv ('uǖa@|c>sdk AgxgP^L,l8:{ַmѦn[өZmдIN+ Ӡ0`7+D#!EO@$uNkU%Έ}sXu}qtG_y X( 0S~)7иc~f^VYyf` mJATIa|za~qR[I7,~YOD>߷EII-qښVeXEU"X4\t&|n=GΙaVhA%~C\M<7$ *k9$F8tfՉUx 0+wM֨LݴGx E!G\d~`])|,eٸ,WԜvE4 |fETcՔqWXbv0XOM$)J[Y[G4U\ܶ3 &idll+dc9Q˅aqZv?Cjr62>P>qF3S&=ٿ$ 6-Y3A)!-WxI:0!FSpuqK (J}kҼ $lwNxƚ\T68~FDMYOEt݅r:ZtQM_%ϚvN}vrwR1\gs뮡gFM.V!HUQrxi574Q!\L%Qd@Vm:;#rNӓm/IfMTHr6oK*ēov8U,}+OZ^=r}5JPt-3=7b 9`z]>/ř@?tUT+oĘb#Qk7 W:5c%M2Y{O jP,[8VYec$dd^HR\A";1~z(2 /scx)1X0nx֔{seEy8*0 > R[ynD@P w&iKo2]J'(?u՞1Gs4Bg4g`XQ[\qr;cL++ݱ7?oo ][h`9:t o)nf~$Ҟqm@= {c4w: [X.5 mX$7 }6Mt F*?:vݚ9QW3{tc㇌ukfI H\`·I#*uC[Nw*[b$.΄~B1]rP$%pHkiVhJY]2]*$Jݜ)魮,D7#i8cv=+>* rcE"  qͯMWoCTSʖ_SѼ-4XƧ+20`XdA \ؼkMtkրk۴4XⲨ1 *76>Xx]MH j#P0@8T~C uI>8'B4SSΫNDۛ-De8S\~qۯJR5faOM#oF5뫸N`oy ]?9Kkle r1ָnڝΝZя6,ompmaч'+;2QpY%֯k}o>?r?x)cu1EƟy &^7&x Ќd) WW{/,*]+, }v}t<3;) u[9CjQ#-)+ίL-2Y1͖+b[Fo(R>rД->I~Rȹ퓊 #]rNI$O|d[[1̊`$`G֮^i6J`b }*fFnd`:A# sMd9&LY>R) Q: i `0Şsno=kJ6'9\`{Q@M<[s){J~O z,tsӴ:pk.̳|3hD>w;Wxb" <DFhb#2e;槼9c+vʾ:`r>2֕6uq& 027t9zW]Z*<0 ֳ\N/%dYǦs8Ӿ}Ռz/>6)_\g:mhlo%޸?\B ,krݫ@66XNYQ .;T4dbc0َK9jl@ˉ.Yqm79*x43ZHv(Y YJ1җN3F|q)dcX/'4;N=Ptx~~-Y$p]F@=qǼxǖ2Z(p^U]ZxpeLVkVJړg}p!u$49i.Az'jt/*Ej,cc~pjՌ): k"g,:S^KF?ɯm3"9F:n%[y#b;m&ahϰnO5h#~*?Y#?t-FU#uMyg^*'}6e!"h16(>6&a+yȅnO\iw#WvבK# fH@cYgbF{mbqFZq{4߉l)NCs$ y{Yn>tk .贯jd.Dw 9^YJpcMHq ';$")Fsį~]zs}Մ>+xUԫ(0 OԂ`tƽs?N>+ wz_/jKƀ77\yLYI2L[_9wŒ/ g9?(.5-Ur M:?H~*7_~0?]zVGC     C  " I!1AQ"aq2 B#R $3br%C4S'D; !1AQ"aq2#B3Rbr$%5 ?![X$ke vP]^"/aR51Q}F_X-D{Bq[ZŐ75F[V9v@-@ ik)'[H WКP9nLk*z@Pi,mh%7X xMhnFBuR~tJlܘ\٠.!7[Ň[Y1ZU-YSHZVҔQA"u#%M2RSlSҺ˼ԫ QqN!@ RlEyDV\[+G&ɸ94"uHEF6RZJSi55S^G[F#5bn7U3qbvV,o yn՜WDPaJ>';]O,p2CPI8:&ĄCYkڻ SN1GۤHe{iqIڛn^U iai4&H뒈ԕKJi5XaQI_ ݲ:}%X]Pʾ }ÈNic0(/-]"ԝJRwa]]v-˷as -]+,T9LU+:.:M#uIu'/p3xkG$qljQxꐻZ./KiE vpS)uSe^!ɢʛ P%gu$C2[ : ",ۈ2\X38eg4nRR>KveRnNbϩM!!bM"9t DHq)asmby4)u |"Sq$+rN$0 '{[ )m $~vA!v7KZ6s_-n`H?bkyFd0 ((BtX N-y I s u:1E)H@Q Wmp`$vm#àHi{V,v՘lhӗ(<#R X9P?h43[AFD܋HFlj'[i^u,9\U"@UyAzG=xR'-mMEbڐ;ePA#DT3eHc2 >cn{HE~( 9:ƯrSshTmDeCCX pMytBn.u&FdZ6Z^:A*̠Ty ܢX0sv @FP6\(2uBrQ[x!I.Gvde P2@:0&g$%7+9BoM@m"@'񨨨ܪ(:$_e-;DԐ.:A{7QӚm)xӪJ|9M&q`us70' ޞJ FbA{}!MB0(inPZE6]$A@IviH(Qnt/slyaTW BK1%z=" 9nPh}IMQY%N'X#*'N"(zڅz^m|`PlUOSu(kt$ R&+jĸ Jo8iO؍nLE[V,T<ЍcY6k5kלYE A#v8 !,iq0M~4%GBR^E v"[y VXG @S`MpF[jcz-Cp59p{X6ʠ,z╠\--3 YŇيxIrFR,UE^0 Gw!gva B#$ =u1 ) $@'Hd'%+A(t^ ]J'TH(9Ǒ16&TyMm) \t'bEn6.X9!C-D7%W7ԃ Q2\Q+hm2=sKêIBt:ne(H5IwFMu1# 0(; iZr`Q.W TK`.xӪ JIo:H7"K kceݤPB/070IK vmBP 3h P.km<}29[@Ahc%V "DzmIQ$"؝.]C=0@aF-n;rɑMb\$˵#G[HvT*UƛD0+ǽԬwp^XܕrN>$yF&7; kxf*' 1-6VlEtck('*G>b BG2I!ZFt}vaX~\4*=-OqkRo@O1V?x 8.:2Gu6nR.uH-b;/Ŕ˔ENA{@*NQ]yH)YX:`PK&{sN$ 4' 5AO8BmrE՗F(Nl{ʠm~pG׬O0^׿(+!(}MރhHܷނܡ4J.H:6Q,#L)jVXh=:J_)**i zi,>GR&&wJNcѐܲowbqO4eR$YbJAJ^BHB|%3Nb|({n:[RJl$]i6%)he,IRSyqmWn`BY)N&udkˤL9Q𪂓(%TH z S2_Y3 \нԌXk",92Pn9#T=dl%JwkUyq$=2P* 'SJe"9 0q.AM΢uBmSU9'OH-G5_CF*3~17/,x]E)Rl=D-%N}ܴ{*u^f:&ÊAԃUmL y)g2xؓf'M]^)ma|_IQQMNIesr``.%A*ȵ@#i%WF =m@l,AF(m9' وH 6 - Nx5h wcz%t3 ;e$[!TX:P5-{r3xNmMtؤߠ]hqd| s0]&Ka6P% 렎IA(PQ} VA;@A϶^3)Kk-wv#}tr- ,7:AJ8kYU@"%#63LZ6qAFY֛?0Ӱ } MFaIE h/w*Ik>*l$#xm^ZI;]n=#sh6#umn\Ia  q;$5㒶o(&֍:B(?ꬃqjSlM'c7q}AF6Z4Q !xck^4uQ@N܄iWQ Fʶ]8Mŀ;oP*Fbߜ 2Bːn Y7>v02d_40ˢdRH$bVmI(z u$ӧ8$%Aa!0F 8ܣ -iTExN \ej 9C)”Ua]]´: SG)ss#1hNxuSCmM_ mrzEH_Cmc c**#-LS3=-V}>RÖ%GR>)ʂT^w#XƔZh,~ZA}:6U;0ݥ˨M|C:εvA. D[db䁭 daÊp&4HM-X\*{MP9H*E[iI*YSjS+J씎[_96Lq7LAĝcKH}ѵjt^Ƞ7 IGt}#wPΖ{[ <7]J뎫YI(Չ<`\8$ mt T +Cӧ>As1#MO:ɺJ]XHHC;l:a6Eeܤ߬(.-@8a Nl0`Mըm-Ü -ݵpAĞY@{X'i3I$$y6 FjT.Lx*w )< ~c_Cwf tQ0 Ki/UꍇU(I/(JXE,6YNe;yolTU)W.n>|# {ĶٗGi3*u2[īMH|y훩@n{> ˷+C’m<(JV04i@ԒB!5zi"] 7!Rij$R9 #ji6I$Ͳ[kNzFPRT-uٱ˚wFe%6Z;B֛A7%\kk* vF CM@5*ʹ `+Ehak FN4h÷Q@<'NMUR-tMop#4Xc=NHL+'4ۉ!AF?RaJ2kp}f:VEYpnB]gOU;35zEyf+[SE6RZlƘ+I9%$-L2S<4YCi%ZECO1(s\q-MlEYgc<>s P2մw56l(55nLv:ozqV]0ShB77Ğ ~`l"wV`Bn $yKq1H ʒTܓbm_JZ+ZSCEY.)"U>VALf2)u\+yEʾ2[-mIr?jqI6^+0e}/qIo%9m(ص+E!'Sn?=?>赒VFueXzƖTNO*㭢V _Ur&킢51o1JB'\6Mո;d1Js X[Ply@Qߔ8+qNl(6p H6u?Q1uPHkaq}>:G$ jJkkF˨(Rb N`wH1B3tfRRYRADۺ#x`Fqh W!FT}tq%_ {Ckr*w6KxF!6 ͪ0drc %)IZFC|4ۚȡWi.V*fHxBT:SIY|YYAVmL6Hh]@!43(:C$:$e}51S`cO@zC߲{obAx#6&hWkxͩ鵌oakEq\a&"1@iV$uR $Ih|mm#b"ɢ*Góra!6VЂA?tx1}_HR$Y?XR, ( ygËZq@bRVq/@BI<@8ai ;-3,^a`Yne&2i2)yP6jpR.:$Rt8<┏YdT쇒4n}c/@#L+T)유/"-+gڈP +@ģH}WQO\KM.`%*װ'shR8Ena$}F-sFȠE~țQ˓N`@ M=1Ve5P`RS^_h^!xng^OHTI;PBlz8c]|B4i*a0f'`4i Xn (|HUq兾 <BHjoὡE:YiQ%DƫoD.K7QӴfqVjKGe8V^h27 mU##up?}MLTdq$eG:P ԩW SxCܕ~qL/Qo' gЩl/&I7?)tؤʮK7H(ԞSj+SkUH)Tt"%GS^*L i)*xh{/q5-WsĒm/"`+H(4Z!^ qi$SR4QNm@4T6NWyɍ4'^@$hyhĒv6򝫩]6#1I RccXtxS958Z-(n~@b^m3.'2Ά#& *](^vĪkIQź&gOh2Lͷ*AQPL8ʵF*)2e-VRiPCri <ɴJ͢1RS.J%c( L4P{KtNDpYHQV .u=`AZ`?ycZ"<\kl`:tIoH<NA~Z0n0M4rM9I IiE:vM†4[{{i 7qӜ*hJiHUƗɉek~IYfZD yAuO)PCr @VSbP (! ;TzV|:֐Otܔ8PfD.!۩M2 Oxr-c,7. ]AHY/=9.@K'Ak๺[e/f8snT̫BbBv^$P=uyrX85WvW2ISE o$vĐUJlvTXv?JXƬB?Gmqk[&UJA:Vdr "ߔrKx)Zst}3X*iZiΟwS닣m~&='Q88^Cdl uՕߦb',q>UjclB1cVrm>ߦOEѓ=PNh %Z''}vķ81jӏӝqFڦ-IQVRBBlAĈAifL1A @<<9QQ+^ȷ]qS}?*䓧̤-OP}!ՉYJKaԩ+YO[o8fV tP(X(ptG+|T*oSb E0BG|ݔu vf\qJX  mp\Z6N]0c aՇ,mbЛ^xˈH#& 82D Rv$D6.HJ_ "a6maLGR tip#Zk$p] IRhmd!bwNސRfQU"щ]X PPyG[D9-p I=ȃ9^ [}j %?x[jCqiRAM:^_psS]X*6" e97Tf:A1lbnMyQWW.|qH2Ү|Ca~< 4i {2t{۬4P5IBA9 Na{(}}}: w6Viu3p ,|"QQJP0vG;UfZpp TN[ߗËuag]eiZ|_M5iJR԰9I'ja8y tk躃~3Q k<)l>3:H*9VTs$$Ĥj/Scs-H9TVʋ]e[띩Qq#~V8g}Hme64jTI&:Ãݜe jXo&ȻKMma߈K Fު1伧a~/yCꃝ˟vU@qXby%Q噠*]ƜBsͥNCǶ8ǂ}0SMfQUOJěu)L!&zҤt%5)H{CUL%v S) [ ]:B%7s+ljXm&3[H[Rw"9.Rl\t1ufiR@荝L$hGyoJԛ'q,(. h [h0MXrm:-`r94Va`:mzFh/פlmțƮHTrq\Nm+W_NqsALjBD;PJM0&] jNm{zu/)XKhSMGm PU SsHiT'c.x"2^oVtBI{">5e$xOB D|.U%4n'6$h9>EVP鵯 PVe m }X/JU= d^(w?]`M)$u6ӜF,'06QיB֚d%.n5o LL(hkAθ@HH/0.iNK#q 儷ƙi~ q+/r3 :饡\(uXP%baeZDP0mlTxs&P-%* &-E[2Ʈ:h&oXr&&JT };v,_0XB}@"TQQh0Pv`jOM1N:+ph۽eL0]CI*߬A1jZU I|Y+jW*7!kRf, )7쫦I11>83 }O"Ռ\ZPR"eWbw)N:pZlDѴ=R^vu8 ~]-BH<ɶ 26.Q6[mLxk.>`L@,: UGhS  97 }cɎT:Q:@ KE66q3$XNM2kD9WaK0ݓOzF]Tbɥ%.8A1ÿE,[Ix3uK*P|U>+9(MI ˗%fH-(F ,gB7 #7 &3c9y+ k51=qtERTWP]L*>2b9t1NQ@BB co+~M?ya6;Me%CFn}&j$,yM Jq feh |-( _JڊWZޟ҈,.3N|H[Sx<6f{n&ʚRZ GH [! +إ=K~]7V\IZue˃̘h-7*@f{в-oDX:nDZSP&ZR /xcbrl$B|)Ma̍Seݸ*%m(h<;C,Ps4mR wA/Y5HKj.eFP? %$ȫDjš>=BȌ Ũ >!wHEU$;Լ9.q-+ ^ՆUqa{f\Ve OK S8Vy 1p?bIU5 |rb<2 ]6u:E} HNDoc5]5Xtd4Fd= @(loxM[ZA\Z/5a%ugT|H^(3ٓdP5ړaKX;cH!%`/GnIʩUGħef M6.̛ZGrq."Hn^vBmmL.(+˪e~!)HZؓb[.%e9;xKq|56Tr)$~Ax1iiḽ鿞hWEhJJJH+UI7ǎ2-p'{:4 KTzp:BiYY% a-j|rmJH%hj;EcR隖ZQWbIrJ04]/Aۉ@̝F0N9VhBP_x龢9x]idhsnr&~hMTJaV3BrRPI̯GW HhLvP}.)Rgx1Ɗ fiK#Ē7JBPDooJͻ$&Jcyk3biҷffyܘv8XRyK+2+owBvϖUhu ͨ1eeocMr>7nbk~7}n@ 9mOX#9";;FkoCZro{(1xsu& m@%DT]|I7 8L/Öm%`FebP D  x0c]H-*QEH*+*`ha M8Yw)@o ZcfnW{?hE[p~d.e:uAhq@۔86$y&]rtT TKK:$86fEԼ!.30qQ-JMݡ*WxM 05˷S!Ħķ+0HʳIe&–aN (PX+9t܍".Ɉ,<%yR 2&7$"m@ey0LPj7:~CUVZ.rS|A)M}G){W>ګ+ 8ᙉ b9PjbI.HB6/QJ2?{%Q * nE-.GI|z L~P TЂ8' Ҧ[3҉Qk9>wsMf} )̉&]ۺ*AN+4HFF^QuG>⏊# o40lTs1m:E>Ue/(A'X]DPؘ L˩-(yM=hi{C~@ڎ %N8Z6o TW,ۨ*K,|OyL@]iM_]%>"ύic.:7mم -Q4i6D-p隅=#?LIxA "e)I0nd$YF6{vV9Eԅhn<X ߉Yrjc)(՛Rh44t[#./Z.ZĀ~P 6>6_AKm¥hY߬c3O0xB?}6n>2U  .Mn5,R$ } h >]UӍTX+Cw!s^1c9e;($>iJ;,e"REKf#P㺉&NѵzD,  ӋӔ BmE- HˬX贋}b\u&PvQ +Oo҈IBlaoPl*(&oLS@s*3BJy-`"|Sԝ悔NUs=u(r h7^&M)B:7;fjli'WA\R>TX%ɀt=X 6X"'2$>E[S1Xa P~!XVΪ{uS0GF"N { LUeҀ$ACSRRJ[h9Ty{0 H;.Â[b:\fh7[-褡W^xR&tNv@4 дR Ny3Nc!κ %6n%ԁC2a>SJ3Н~ե—7k 1l4 /V+ *R Ҁ oXּf.Q[C &=6.F-`E[ ,o7=c`N5܍&jytxːl5Ckt4 XFT>_( /e @x7Ep 0ͨ` H&n>Bڵ&A_?hޖ5 10iHyu\7+-$lр.[PcZQ"岠Sh6&$}J N) CF;,U4bYy##׸y-8]=]$)"TĬtCL]6 )6WEu6-rU`fQ[@#%TZRlҜ“KUJB'4<RڸHRDs:Gp%?숙5m' mi teVͦ-EXѪq$*ny7&22H! P#khaˤ-BD-KR,v VTC\0ĒͿtj}-*"0OB@kzDR. [}xCG\^ M%xRv=L.ememm!RRF]h]HiiJ'weܤa\!_Rꭻ56R3bOcyG|B moY:˓"vAs(N\m̷!+ZᵽhItX2~||Ty#kS%bFk @;zt dM]Ғ2 Y}?Ui[8iBgx q74 IU}gU%B!GMM*g_Q"в=b;'i [.[/v/`T2s1䴕j;=>oş!' ]/V {̲›i ۦ(P}>JZ5הܨ_]95)Lu-G\dQ,ʱQ EJ?Sc%#Kb|C*ē(u# T}r JVuljա:2Do84J^TysҌr :6I ^Cy;o8I)4Ye0*mZ?sHaɏv3wDbH(Q*jW|'%5,ҭc%E|uMבA0P]K'_ߡJ%]DXXK*l DCL#%#NyҊtAq b 郮ǑTyic-r\ Mľ wLaJPs8gNHKpMCU- :c+}F f0K%%J3݂D]q> ցeZ er_{{Cs Pd-c/.@ Ǫi'!`>"BDUywfJ%ݣe9G̑*t}D):]6{S[amĒ<"JP ;I jH ">GRm)tgEiG Fy1OsPHW"QyZ<8!JiX2BX|C-nGDWF,uiIcDk {8N3]`)E o 'p,uJN*l! syrGvJIMl6U)/8eH&$} "\<+c l,|W2弗yCDݩIbs-x-ԊBYjT8lh#;8gY L˲Nre :j XUTm+!@;מ,(Q[C'|#S<0rJaHV[ 苁{KxMy3<jɷ?/<֐dNa>)pk \N Pzl ܻU}E)6 |ʆiimSI|9-E(L D tj1S.o"CQ7:GYeE "aܨ:*<4*4ě}bJ唺{Rթeܿr%T``+k*akA=E'rHS'K(̓1WJ=.BϷ&$mA$K??bT|\۪R i{ˈ7&C_:OK%]"y%ge'9{ݟxU)nV]̩s@)*3|?%]r.Lye,:BDkDH5 P43i#b#ù3al53s{lo.[$ F$CXvQzFr>Z% `sHkx$[]ot^Sx'J vX9SOUmS|zNLu4P;IbZu JqdC7C՝<+9.dZK2TMϔf ڦI=a[30jT\[*(F_Gi([,=~~p7FJVf$e3nel_I]O{enmyŎh^L4_y #HWHMD`YZ8~}u!&4+NzqWX}Z͒\X"n'AaCnDT5`Ej~s>i_%aQ}?KhPHb gI7zåd5:|BQ/ͪ#F]U4.'8pN<.o$yu u2whQs{k. aj]^W_]QKwir#x&RN}VTt-T Y펈o2Aೄ:y,TZJIJlIXVE7OHTZu^]@}Hٛҥ\MқM̼ nlXrh<nynzIJje 3:ؾfe5;s&оO ПZFZRiB@SMA:@v*|$Q^cL0SAW?2RXS(ePTyCe// YN~gV QMNȡc[%ԫ?k,*ii ɲB~]! [+̽kE'@\mzX`o%V+Wv,o!;_S%^NIi蔤!ʹre^(vv̕mےqV^X|jR6|n!@xղ,3aRԲn-&!@&&YcTV8TtU([РJ}c8;4?M(,*!:|B _aR5#V:E1OUSd /2I9&*@ k1WD];c,B &b?E,Iv-&R-i)Eksr|Tz;e༣Oڌ;;Q̝K"Z3ms~$Mm7pUrUUҁL[>ʱCAςZ=TaoMIKJ/U] wKAJxu1b7^ v:+jS72˩p0jzP͢E VRi`ahkU:Id5-2&ε*䠩&XKW>$Nn]RRܙFR*HSgۭ-&yUw22NR"Ux/d RɋkIԻ :X/L8&KW\nUh*J& HuU J>z Ym[D'T* kz۶ؿLZB@J}O[RYLӒV4iJy,$!@H6Nc,ەNXCEy! PO*WmR:4Yh~\-*"3ym4$BWwN_Q4Ol*qdž8fKWn9/m0 M-*)C}dO.P Ҍ@s qʪE'n~!٪-36rISc M)-qbR@sS3 JRGo\Tu%1*R y3qZuiʼRnPtP3DJqoo(l3<.pf]I iKLmA%I)v82k.Q{:z󜫒.H&7P-+8 lJЛw#^wRo/;3Xp{SKGxOU.c!]ENT)f uv')CR\f҂pB$_tHNe$:u*3?탊#tʙf3)oSHU3;:UxR/a kWmslWw f9w;*o"Mϔ*SkmI7 .)>PF@c&ihQǘ0gY6[dyKB;s5BY iDƣsѫZ% li!Kx3^C|Of+ݍ*0TM̘q)_#0[XJFjX) ;8Ԑ0)uNY.@-w8!NYy@hy=v%[ /]hġ&MYr*))Y zbϢo*ޠ>ږnzZ~G9MIKd])\6TnFI -*hV<88.%E*4*rvY Se+Q m-Ghj'J3ׂ ;w1%IQ=O i;C/.-bD@6Hz;/}kOM:@eוfRJ%U V$@ȿ1@Sۋ&?aiI,w)|SUi$,\Bp5 tہ #AxUd6O !iPp\d֜qLdZT|ֹ27.n ZfM)0%д_89:@ ^ǘl%1KL,`Y*=7J02 #/ cM ZpfcˊAA6%85OM^i{xU:A4䙜 6K.P?Z x'Hq.q*i $]K/Hpxv*SqeJlħw$̮HΒY*7{ q V~MMUA#h%CMvf G2MaR-|dn)]D1,aQN0i睗m|K8埯˺]BPN}"YĵgQj!Im ""۟!e,5o\UnaYcԒV:i'cxXVYx6Ӥxnpfk8"<6VۖwB}H8ųr SS*رmk^ثlN-S8nY+?2=nXK8_A^9Q%PeYHMNoy]^x$%ŕ6A츠Vu9[XOͧbQK٧&j΅&eДzm -Ty.Rҍ‰فK 6^5s 멢!ŻNr`XXު~;KkCK kÜ?Tߵߝv2妥.) bHUuSlm4i!+Rs Uǭe+EG$I6 4_3[?N 0'wN*RwJeTQXârUג\X.-(biըRĢ qn:awQEX˗ ^[v8`diEgIY*O;GXrt |e2;G$\ KL -I?]!V%Ib.Bueٚq [6xN3W4Ie (5SH x7/{cָ7t)I.H}q[OŶYXY3m.zj)*bQqXsVSqoQ,)+>Qlu1ӇxXڊj" wc0Wo]j/qE-&μLj(^: FSJM1xG[Ī0JM]mbW7LyBdKcP5|DnC}hv>yļMLLVJ&.s#xQ]V+ U4RcJ}yt&%&t\w/xm8̤Y&[,pRC87WrU<øMGPv|)ǼR3 #Of1>› O0X8t bbh܂Vxq7Ԯu|Ivz[xSEYs] V/ff ̴.o 4lsƎ.4蔕|dIRG? }8!Go:\T*n-?~鑸ctE2&im mD_䳵0A1:&jbԱ]Br`ThCN*ФHqA^Ë ]*O4L(^|&*",<b˜&Ư$MU}MWw+N vXHeZ /\\D.%[*Xʇ0]JlJ<;v@S5B咃M qEHK5O)sty(h~q,W䴸y&v ]*ai+'"ʓ_]-/3Om)Q66w4ֶǸ*=,&ZBQDx)|[xxLU%&q'W߄˕7*FA zquWUR Wi2DH92Ts)jJV/bTҴ w${u4b@Fs Jqn.!#:|*I5NEc_X.[D܏{G3HN͸RA:ˤdn릺 .B1YX%z]JT.qcK$!_;EFe=$gVxAWy}@HE+XkN!ڌ؛ 6յ) 9B=SGmy^]lUZ6WoN_1հc#el|=3 zL yO]a,p(\KVSL"b6oZL Xۯ$BdeZQtfK;,*eOC@襏x_UY#UӤ,{L  ٘NZa~Fń ߕEqg U&EE:yrJԁa|1ECOε2naLiT(sesz湇}q UBE:Gv^UTӞSz (ɔ2T,-Ek>ǁuZ4kv|X4KqʵʛP;1tZ|U,]ɧP,:NTZeJiԅR[Cv[׺&0BRO8JrjeR6J̠ \k}KeiͼNTQsL ޵C.k}.]':0\t5 M| u f36@? hb8b*SeHLf˳̋Z>a j]{d@b0eB盝U+)F]N+K:l[r51&b G\ĕ3.?4ֵ(Rc>Ҹ* |@ ⥨XIrFAjQM;$v^ 7S /2)[r˼&t<;˅E t,r^zQT37acdo (H$BY>[JY]C6*;"JTb9D;'0젾)D%沔-Uq_CLEA2`] 9Yr?x pB+RIFd?\UAʴdkk:ˉ]JVP\4/C4zLɗJ|G*Дߞ&qSvW d'~NBK,?ߤ``ʸ@S2x2,(yG7x %;/[X_;Jԩ՛kZ޺Cw>=u;y U먕ItӦ6wsQ0؋ KD>de!Ʊ{KOJ\iEsoPHݍd16Âz\)Б%au&6j#N7N}64ߜ"ATǘRKOXaP⑲08lWtm*e$oZ08⚴KZH<~ QVK$˨9R !ni ڧgut\Q$PNP>))‡7=54܄`QML30!`(oOg*E+RԁdD)h ~Cp ҙi QRP0Eke&6jZɿHs\^U.%з I7hku2)孡 S4-+Xy:LxKyB'h RS%~1[#?'KEF911LmJ"7%Sj,5B@LfؐIܔ/3/R䫩&;#O-7EDAaL8?|FJ.x}L+ -8n1Z.w̕h,w'lݨf\SZ%W'LkE)S{_B-E0kg lhTL#a) ,;H]RIște0NK r(xwiP =9F^eMc6zqY51(@΅rhh4;Ϳa WyA؝gj*B9QN ؤnJG9uz>* S1ɭARv2Sk K&KDrw~ś JїQVq +8mStCbFY)#(HIM <7XKxyYs&i:ú2vxyTOҔb"@u6וذ.{lbRbذ{kvoH 1T|:ݾb 67eRm`@yYawjxMnD~DiIXnuG_̇1$Y(`@R_0-8m43MqyХ3_'l"]^'5(2$yU=n$5D항<,yMuPxG?Ն U:T0T:1,$^uJ$rTPYEUV*$ ܿҾ[ ʘ *RㄞUXy:n*SqJSU E s(.O'WP֗rV ƪX_JXw~Jm,9r1i*{O2c@g&TV_Tԟn(qI>vKZ nYS2s-*[DT6{H+fpIs;|h>"gyS'LOi*REoO;Vp2ٚ$\i`y%Q싌S_w b she N%T*t(T^rXI+$hEELvQFR9Gx?򽍦bU3wP\eESsI&G0 xN.8F$S3ˀ: `aE@ikaI8Y g1?Kzy. NSsO=I'*{ S1U00 P׎ ~2q"qjӔ<.¾.lnOU noQ_.f62PEu]bQIeԶA7Ϝpoc '_2ʹvo)*nv;jitR%|J& @3c~xSfM6HSs襖2 |Y2)@&STl1,ǸIMkHp})רn dEZnwGNRa%hIX)Rܬ"3$$\Z״ uKBF]|lu$)n?+A?OmeW?8*>rb\~Db%N_$(Xe2 M38Bk z{Bs=tL4B+]^' 웤3 8#0:y o/Я:w~XJcM1Q\E`)[cԂECH-(bS^ABw^βjV4Q!J NU4Ҵe1s[JjA 6)ч+L,$ÊRԵU,BMa۟C<:I4wY<[DQ,]Vu$t")FAPK1cVu@ .P̦"e$7*wBYiޝ vኰ5UyIf|wyM;rlTלr$ _> U0Pc3Y8xkt+ͺOGcF N Il  Nj~~Q4,OXF,MH${o )!CT 50Q o"rDr @&jSoX~nXjZBC䪆p&-])f{u$Y( ԂyDBVkkAs@߂Hܖ)*|MxW_3.܃-b?摾KD-oXĈm3_M/h<5V4uc1 mMS }\-y[H=`:MeAHe녥6#m|k 714 zT!'&MҖMɶ:4).ǙCGvE5HQ7QqÁ-NoqP-)ޱjaǒX#^e^g+;nG?1a|5=OU|.!PwgIܡ{"'R*JK}MCڰGHU0yتmKIٙkKiUt:jo{kd}z]sQ,bZa) ?/XwK)3)YKX땹P=Q jS[Y^p+P#R5޻*opHfi7'u仓i[ )Gƾ =/ T׆ْTHVE.R2Y U;C3Q+ 4KO4GG_\^;+<1[M(e, \ǧ}0 ᚼʦ+&3>$\hwsG v\ !m'2܉a_fZJ& uVXzF01COR@whWt5?ISRsMPCRHQO)my0Ļ%kix1#GCj>U?/@ 0@T$ >1qnjر>ܫwT ĺ/(Rlopk$.D\`ET,|Y.;w|+TMzbkU]l:JV,6^Q^JJטL˒Wxܢ3($&\Z<%? Q3#) XX[quAE#lWhˆиVsI?8fRʼnVQ'+Z>k *g`w?”t[B G0vBwJJ JU bcwj*qgLNp0܋_MtH{G>v1vzAԲ2Aqg+:_.p= `VL7]K?p i;3-Y9􊋊T\[b 䬼īAȸEcs`c).+L`}IiMm{.>66k3ƕ3I=FkZޱ1LEH9 ދ Lי:/)3 ai)J&Re5ܿ~Vth:Y0ZiM)LL0Rfn5ʢ4EoyCSDw2m uW%[] QTR)zV}J|Kҥt%Yrx4A J7qSnT'mE4[R;Eqء౑ (>aFnSLNJ>-)jQʔS'zT' lY.K-&ZFaa.0CMl?HQ+!]EIj?vʨvlgk~2>_rANuqJYuE/n9XDNp)NN[q[5rI{OH8ŽPWbTyﵡ¦gR+5gJIݶHKnҞpԙi49V@ (@#.]AjT$2 NvA;hR5|RS)zp)YANpViXuqŒ)\{S!RJUG0r7+P@6^~Q[ B=kX`6p-yCR,ptפh[SghsA],t]bNet=I9: aE!!J:YL 'iuXOX EDbO)8?s-PvDWJiA=QWY`H&Duԓ~QWmS&Rs%>EvҜ4_O(YCa-GGUsȫ PĂQ&4VUJ Q]6Z)@It ` 1n")ƎJ¢Sha'3 rȗq2k ePu nBXѧҹB(!ʢ0[l\@~Rh',j4U8FJeԧ`e* (rSg ziFQ|b\ n1EsF~e TQb3!m|牖DBWrbm q(PCSՃ0mkKSSrv] BO?5,WHI>=pupU veזR|_[ǐ2VR2 zYsJ0զPdʗ&X3;Zظة\ vW].O>2v9>_;ę@SgK'M J蝞hVPoiVeUoxpŸ AOP)i61qy1-stU/2_zOl51ҝ%Mg1%@a &\ yu۳1_h!1XBҩ%$4:zadP̤! JSbG!UL;#nH~#v?%$۔e(myBPV қ4_24O*ZQC g'堽~/%p7U:=oxړU2.LT@Wq}71ȼZ|&Ÿjܝe5X?2y^inpe-2YKI3@P J^#bN8 ©m!-%դi"s^c"e5q[.`@4MbrO֏I ]*Ș̕6gqDGQ\gxʿ/?&ZD=*)TDz[.I?/є=n1ԼԕAKl$@x+f*nK'2kOh:38^F:3jW6a’ijK T,0ADгiG*heοC{cʧ!yGwu!'@E.eM)f$)kQ/6jrA F۟#ҜpYT`3j&BI&V9Fe4#`ogeCGu?@(H\|) ;$|l7XG3]W;C2jO+B U '/q"J~Cʼn@!7:Љ']ah%'mD"y%W8;F7>3nchXjB G&Гc ;eѩwS \h(VAyٯkpC ϥ[v<[˳, u^^RAM@,wC$+T ݘ1N V3C VS-gHJpha%0QH-[YI$~>nI dX |v4j[(r"D̡kem+{jmN2N˫Nuˀ{k]L˒eee6Q|iDVG48Z->Ӣip500ԑ^dK7'TBSs*1WiKjuk>$=|b^CN%biyiJT@:&KY[*[I6-}:!F]J׽XJ(4+li|'HIl2 "K`h^Bہb*#5C>›-ٲk$3Li#{!W}H(fZR@.ӷזU4o e>x큝X QbaI D N^=8v3't;Rظ4RM^k>'WmǁIhOʽxu!Pַmh .p9McZg E㓵X@akI17ROm|@9w?wpF #S_)l8&EOz[dOSJ@{0HekzXz06)CkI=㩿 x\WxSrBĔii(xE"훏tU=!R(&edڑ1.Q ea@AVTjod יS4,!Eƥ٫eX/xs5e*fL;*WӾc;:3$ސx)n8 v=Ms8w`_ P0ړ⌏@ᩉ15:-jILTͰhtզU9$^P?Xp OqIPp=NNfU.J;_"#΅ayueCHpI1gaCҊEe%;Z({7GRy~{+88!i]Fluv-V;WFHPʴ[Ǯ !`c^7U4nJU,mk T-WZlu<`b4 2 }ŕߕIP9EYw V@?Fuۭ I[8g窸} Rqu9GJdIk/ b^`~ɠ0F1Lge-4mߕT*%:yԴ.̕)kEeICd\\۞Wj>,Sg8/u¢PQ|j#|fK'WQA~E*TUAPi6-_M JʰmÝۻPAGY; w7cNa5iJA;8۟f|_-ݝ1W BRR-=+Rn9VJ][2*VXb ;sN%8fE I <^kG!BkLSQ˒t$2t5)Z_[|*MM53]zv oV 4KdhEEQPj)EIj;Xx?qTJiB:7=+{\`Jf4`̮!$di * thX۝.,-eo@xYs ԀYY@CfN%FS8dҷVFbymox!Mp0Ug8A*aR2ok03i-o]U8ܮ8T\}ٵ~(eANXL>ړiA@otٌƊHIe[}`6еQ{ɬb]/(/L7$  ǜS>Kvj(xBU1S[0î"iģ;ÞiUPik$;RE#(kt;0;^JJII mohIո9wtYdk\NJ՛,SeTvEwZnGU2Op0u?XˈR(KĬΕ? :FSi q6NDL0A${8yRQ ,\Rʱr!bi~&>7@XE}$:bw3r6Y}"_ӟdoqvo^ەg{T-qƫuX.Q􉌟5^L#BFn)ZỸ&An}˯~ H`n2zLjPy0($/*×8H\*(㞛C$(X[xO: w 4mO􃍐]ܛ^eQqjvR i-@閍8F-6;37QB)⵭ư("Ib ?*4ˡ&a9@N)* JTx,sNRYrSu^Gh|aX/,=3Ozd] M*CdNRoZ6_M+Sromewa,$IHzr.;gIPVHH]eA{1(²~VW2}ATKvЙ3Ϟt]oL fB 'O? H2Em) ٵ=,썼]%*+J\HXҔFP!wЌO*jZeNV@=JTTʚPMT"#wۍWSB[N2.>+sS"G8UskTiejZTrҋrYBOzU^K m3u_Mn#xBʢU[[m6ln/~Uۅn?kV4?_*״@Gn+dϝbXF)1S8lMl9k@60 j7@FXꏕ_vuFôJŹ_1~^`\Oc|MLgQg"Ůaړ[AIt+RI##TN֋gLS6- C+Mڍ=˶ FU551uʮB~{2^ # ԦJE;8]L?JqJBLl~tGb5*^rż\:#}eZG\t SXJ4(af2SOHy(ZC\W1o OU4ҫwM"{6*'S+7üuJhnnc1aD}mW g6 œ 9p+ ۟A+Zgddw?%S.VjSguEDf!㹦j *myQrYDl<7x5<;,D7KP#C#[3섉*j儜%aòl *qZ댜g{{s;Ӷi]|#'Ԉ;.QqÉH/ARW:m9T l@06%mT44W)Wb7]R茨e=a#ogxyX[SD8$2UC%+{CZʘ+wkZ`6MM)ݴe\ gv㉉Qi$9ąL),]VqnvYMJPHiNvvR1:~YI :m|k[evGlxUW$ q7S"mt$yZ:[g,*K̹o]u'k}bǭk}pokt&<>.\Z[wNXhOڥ@9=#vnR /kE1ayuRIQh]'M1(b/n/'P<.$)u5GIѡ~f]=aIyز⢚2*4oq 'Ia.%P \K~|0K )İZin3XSD@NEhꭿxq6!2KB&WNlt Bpi'Y6)MU[0"eli7Lo ^ITC+ .)Xk6|?S.i/;\U opG1UD0|>h[$ XP2ږeLK%v[xjS-%*Kn-h;Q%LGIoD,2[:ܢ%mCkK;TMrxJMKrZ)tE;0;&aUdA@h0 ;>#s*Y/vD94R1"{>tK-YT2T ctN$eU7ͩAܛ+指[R.(J,9CKp΁F|< ܹP]Vsm@ju9VQ!*92?Y?BLFXNn<)FXF«k妱tYNLɰ alWM$RH H>*ӱ.6u*#e˔@^*Hs%WE~R*2S0/-~_^a8Mqk,ZGű\5keR~&M IH:xVhn8#z.Κtl tSRϞ\];x+E( ["tt0c+( XuIRMMFvk|%Vq`3)It#؋RRy):)#SR'А7(+tBqzl?N{"~.e?V˵ĚR'|{\T”\i)H gc+R0zb;dyUR-]4`H)[4X;HѲd`̿sb+,14kG8즲uE4ˊm@C!D}䦆~+"IU.ME)6!nCHR7<Resk|aL-S/~O ,"6Z Bi%!8vHVxՅ'B\Ip l51IJ0u)efhi\l,8rK IjkLʓI",.YXX R?J|95ql6SeohW>-|G4/(x$ܒc,mq˜ell5h VcZqm>"~jx#Hбצіo0!H$6oncǭrEAVo= :;BVSEC^?Jka`\J6"(H)q ǜwgb,IJ˴iW5$of](rRHB1=/+pA=7{j/>QQ:qU?OMg$_p籦‹lLL)BP"ב3nwH#(*ɴKF)3%Piq qJ;j@<7XHmݧ^GanQS- 2$hq YWhp;eIfND8kZq$ɚ^'`)C‰ !71Yfs("3my.8$XO㬉bM pW,\Y*s/*E|ɲ'.-2b.{U{Qm'U1LRTJSm^)+'@6;Ov c lxƾ!pfxT.>%k]8 03 D7`xoF7L!$;PuFXh9F Xta-yN=F6FHox)+S76o^Qpkoa>`M.eB &k]–0T c.)$k~ë?ࣾ1tS9k!?Sx?dJcn]R kS%K ˔5(&]䅤'33,aK0p^fc4jLULEQ=%5R\O:G;Nj ( ߴڒA؏8Ca9IIɐT'ZoǘnwKKո).U*ESM7UrN-'I 5@2֪;n~]q{NsHfxGY>싲-l gRtxzq^ @jQmd- 1[5/8?Ѷ-}*qfcNqZ SpmYV nZYrZ4KR#AvӐŅlIP44 @* %7IøCQ[P @L)bzDpjZď^WYq&4@-<!vYF(; ⷞE>VyҴ'"/@)/bD8Vplꎠ{GKmܩ*kEi^Z\%@J́F5<08Sn7[PrҨSyq9CJ7PrE%̪nEo+nԱT* QKn,)ĦYBīQ~$QKx(ntM h`SS3K! ԺWucumL/3(u@Q) }"b3&l8TЙµE^*#bI.۔eùXhN/`L9P܏炍=#+#1o9ݰ>jb%wa_ijzaM2HlZ`'R{J]bFZ.^9b|I::]Fɹcv8rMЕjJ>|J*8'.M6*84*ڌz54TR $Y=!PI^׾5 ͹}C؞p IŶ_;;p DX-KX*'<`a8l lEEve_ h) )4r$E7ڪ'oJwI-uRLW)So$;8? ;GzܥHБ8yPȔqԥIPulYnuױ[rkYM= ɭB̢LěWS$bAQuK#po^$G75sfa+kZ$o;7Vej=.Ѳ}`8z6 "E$*m'R~hHBNF3-gw_a.2f i͎>6B j5&Q̍Q%:(.uZ!J=, 4DܒKSj7x,)AqMxZp6L7T1.}q[Ky@!-I"Tᑔ+V[Pu?EJ[y6Up7Jޫ6_'YBbOzB '"!mSQlWT6ԙ`VZVG%TQal=F3d6Dō"~2*( BOnN"Cj@gfܐi7+Jq)t9I]7UbFWTY 2 3'.)'en=U=}\ǒߊ+fiml!W]a-FQ{1Ma$.Ҡ)Ƭ}#-aQ#:&,t󆧃`F\Zl9)<kݵy4@ J/Im. *ihF^![Gp%ZKE#`ީԷ|665+.Ŧɽx"D˩u#6(:h/ujE!X.uyqXØqu0-))>RaQƓ75q6{gJH[A:NOĔzi)g%K2C䘦0ʑ?5]pHx[FjYn#ã\Wgf1 nU#4^&^VZ|JJ榓:XZJ5RtMO +0-"',b\ cICS)W*N,%)C4 "PLz]vTv8N*zeiT?sI~|Wmq'Xt䤝 !CTzNI7+$PH B07rXNc4Нyy{M]< i%{,:@3E4e_ЬFA)򍅌s(l-}KQ;zF*iV]7k¼&0]d6d}!JVnZx/hQ* )&OUuHci^Rvs.q2m/C@A4-Pj8ʏvVT\ANN Ceu7Jv)J)-n9S}SU. qlW0U!e_ᒕM@hxڵM5^4)63lb܉8‹Cį. ӥM8o"B_K9Ëޯz k:W&SOGzq#2GPjʐ-1Nn@)lEUFR\_^^IO*sriQ)#Th%,W Aty?tt<ϥ/5Tw?o0C^zà K6ғ.RxSOm:U=}mxspqj7q+ fg 9R.5s{=&d*RtK]MG2g z=-D:\vc!$gmjԎ0ِvc Q~9n9G[Hh7*Im}OzvM KHo=6w,Ҕ4.,RUp-a0.N}k&F ] z` ؆b]kdfFE;A#{8O`9ZIe Z6ϥFW#Te\L#F=P %/@U~^i3t܌oK$ېH#1f=uJ$f$ޜ1O5UKp'1^oYf=)P!Ev:\/T@Ol؋IlWtT)/2TPo x8ÄjSUOJ)%+:Gܒ;3v8fDL]$8;;m8=/Y^}INr6F˵-z*t[dH]٪wlbʘKn6;pS\hJӬh,J=:m?=wS?e}ATf761 Ap}bҞpwח%_w+!בcמ` 7R?Ma oO\.aiZFd̷,94BƼ9)$}) 8\E _*.--ԤRse as.$Ŀ\Y*̢|@^N&i?5Sb=OEbv[TFe qEO@>9P@MjZ؆Cl՘^MoޢX rYѨ;֮L-6p[Pb>sZE:0 R,<;_2QlX(X9@׼k[FRu"x Hr9 tV9)!WOԓew'(P3psʖ@#i\\'j,ĬOʥYt= R3w^)CMm9,#:rEzp)5fnRRa "ThΞ%az/Bds%{O_ҦRӘ6X@CJl[ ~rL;gRy''-G]Vt*&SxMӋ䰭' j߭)% {٫8k%T.9mOR GangZ㆒#O푿!w&XfܴBl˜?>S &n,tX ,^^Py>V fAM`H'vq5Q3t.ozü0 K#*Wݩ$)*{לN8eK.?JOzfRO,=3&2dN*}FLb 0ڔP:+kkQCt̹>Rm9v EP?X;]NjuBPڒ w$X pJR0u5K*{mբ7 _ƖZJCmm| f,. kpRȷWEpK>T,mQ#}#gd4zА[N]k컋0=hHT yGE`%Bl!Qv-kM>*gsrȯ4+5QT N~=7OqRMӓpOv.齈$!J9JNL#u-e:XXy٥t&3ت\A*$! !B9Rs_b(!iҡO>)f]4 wHQ!Cuߴlsʵ17Y &/hQSx yZka{Q/vq]Rt<4w>&7PMɲzjڨeT'Ȱ,Ix J4Z Ei{B%H}u!_m;+U}ʻԓ@QHǘYVBω+%_;JIIJ*}Vlΐ?!A g [:,dXRNM e^$`((o0%uL*]3m*\#{Doo65 N& F?S('NAq g$-6Mi B%jeD[SW\qP U $=cXȃmL!u[K ["r\"]!"%zNUD8MX!YsӋ :v6\%SeeYjs\aaBpS؝iBx83Sx ^n1*tlbW50Σu^!dx/0[RTDZrڍBZ^"Gb[]=vZ_ a?.+Q2}ͷ(u4]|s Jkt$b~e9$_uAv2b٧ _Jm#F|*ʰӎ\ ~P~`ncI/):ԜBrzYe@}H>AGF>J@xqVD۔-ڽUye$mu`!2QplIj_0c:ߥqCx._oT )7*OLK_p%\bmǎռDNኵf&bm$Ge`.\2~=ٻmㇸ1^xiڿ8຦%eP $,ٯqxG+d1IXȼTnˡe`[b/%J#6px@ߋq׍JaKRCLNFK x:^q2ݴ>F/^9rBI7-6q%SK4g\ qd![Щԩ =2PRrQkb|DzftwISd=ty N´:%:S*/0׉"v6 kFϱWaR 52}G !NC![TK "cW)ߐ#( HH*x6YyS#wF\.o Nv #6[-\ 9*w\@:XS@ ahtrV2m!Z$ aRm{ߞѠzqIInQOm e,(u(Qgl$Kh ' TQ%H۬ 1״<\|;`̣XT27ߥg?xUFָX,{n|iT̆T.^[6Ge Rn<. |ڀ1Cj4TnHQ 6v>y=Tx Rȗ]J:W ª,1*&Qx'?=mor&Zbn;!`Jn&J'd&e;ͯX;8trv hFr8ɧKiDW )'x Ff΄|H`tmx*b͠FbZyBГam砆}ÁЄث'cdNMF^][1AהPCܫ,2MvV&VI7nHevM;}L^auBM_-3&pL%*9 0uȬ+.ݨfRKZ:C\e%{ѼZ\,&z9{&Y˛MJl,ve֬Gå<jJ$遼CZ.yN2ٖ+ʤV"1ĦbnX2{yG[^s O6 _ͳP R4JEO Cs}W^q֔MH6LkJve6yCZD w{k%6ˬm]͂) "ď3o Ԟu(wܓm -T KSU(_/T&[iګ 'Shp_;T7"0:?bq'Wu;˱7YIOΤbt n]C0&){6vA1ZpM˪bM7 }7'G]R{ =dS%)FH$jV?G+2%ěyXx60R'AJEDpi%T]h馸Zʨ>| ,y0+9m iiOx4ЛƘ.?2I?#! Y@FH#C{9LHRС~sMA]&y칐eGṡ[z9i3'0O.kzINdД Y*&QlJI0$:. Jy\DxXrD&LbMBkw9~JPOlDd5(%KapS҂I> vjrmYTc;n}#|Yz"9Yt]m$zyA擮ha/p2hNa*MB|Cһ.H;љe$L6V"*lvZsuymq ˪xxv;[ Ld6pP[͒c+mI˦А6EoSdkU䕇L d+Mbo=Jh3.$DU]h&EplEߺ(*8X.G->/!V|G%j2 *o$ƣ"Xt#^^QғRlNdj TMƕ4f%[=ɷc]ёMzqؽ;^V')P}-JU'&bNihZNBNI(Ѝ¼#R<-rJu }SBe_Ь ,J|M2q+O>9m3}<1jXnJF` m>WQU5ybidvW?s*Kl%v9EIZ?&T$h0 gљ!?'ƓK8!L*& #"/K*龶hER`-GeP'n0zq t'sM.]!I; 6=cI [j ҥ$ME:.&[_v)J66PE1\BJR.O)ih`w 8n cg̺B 1N9cRqu*]UBn͸6\R֖oV CږaJv@+@MVO"ފ혓%ﮩwFeXvc_hdʺT ;BWv@*%TꜭFQ2!bTtwlbEGVӕ }$a_L <@1`v.*hJ)Jjh tw8`4G̭\RJϭ~KrN:WsyY4=;-2ґEP& 8RCG-WGlp7i_CEYkJH]*҂uW U;mbT/.;ܖ9]G.#Ar@<mu\k(Zp 'MuT{Ԯ:.֥J. <+{kyRr|[\hj4J~Z[ ?sYLIiPW{u+d B<骜+0.=9*̗ QaEϭO8Ŏ Rrb&T5mzr i&)i P4G/~;س[0K皷O~[A1BJ.N5KZ+).IUа,|JxI!M+RI6#OCQ xk=2TBґ,ZIs(Xq knqKS#Qohi"W%_ U :c]/+_aT'FQb‚|v!IH.mY}erM'BmwWy4N+|0XiW&]ZbbY/S AT&VhZlNܮHÒm"PxU8e@uM8_D˹)/@u0]]"USluP@QA"'Shc=eB|Go8eXR)Yz0R!V9 VnNa&6kL>P|r`UXx5#GyEFX}O8@ DT`Ɠ=/r6ң.k^|I?3+ݠJlZ-v"8Fin)*M4U{I|N;2L 1d%i9Ί:^(SbwRJ Bk$k;KTk%|[m2[tF/UElea}psg~7wt#ʹM ۡܔ![I[S|mK~6;8YoL*G5Z\ime>&Uw>d DmUratm5P< d㞥QSvxT[5$%>Hc7mu6Q`sD~V݅ ښ7K~v5u Ds^2sy+N%[RoΆQ0'Xr>5'H皥 ʯIwna@ RTi er6". ~60`*6 `tj U\@wA^b!Kd#AI}qTk\;< o1dK$w❵:(}Giyj|',jNVPIK(H% <[>)NTŶULJHF>:wb oHְ5%willrpW*uILSqz}_JZ)MMGML*yBٖoXmU%\6j᫦I%Fk~HsKTY=C*ʾirhXC d|$Ϭ8KMXhT6.c ,%h7Se.Kʥ[fٖZRJOPahLR܌Z&ZU{jMNH(%ܾ qU[ QEc^ *`^bpʠƑ]0j; BOyFzFME>;M**Y6;B){@ۥ1@-ܨ@/? F)SiveܥॴH?xH8mRg 1n9-'Ǚ_ ))JN􂖐 >5,a qm`#tc꒡ix+xqkxj A%SZo#huN2JV}BK㖷Y^-,9lLmHrb kapE3x%^7r\_|:$0G"BCdplw<*ɲThP7򁽕s'&o d'A(R>&p5 fmc݁-qx[Tx̺sUX${!ڇɉ~*%=fF/NaU-PsSim K &^[ !×%+):VH>cp$5:\=ug:ѢYwKYKNA y)r,'\,[Xf`yn P%윻/e@FKí LxF\RM=(.{U{̡IT+e]ztBuj7*3 yuhY[M%?5ϼ DCjzcj;sm^#Mv'?4YUAXx!6$b_RFl`bDIS~}gp]4o%u¼~>0hLQ񜚊PVqfy34G;(10-~!ݪx\qBy6npzD90;3q]}}^R}Adr ]#c1TXu 4ຂǡBߧK7O?>RX%={{JJ"ɦ3`Dm+ȗA*/lㄥf.̽gAqEs$+W*-AI.>Sv95tdž δBUŷÃllPrOukJ@tk@V-:@ 6kM )+l:AL{:RTG(J2yo Z-{|[7ө.̤CR֔ z֖Zm@ 8HQ]Ţ^ŒҮߕI6{7Э o<4M.p \][q:BthPAuC-ah^BBWk )\Nmrѕ-I1v]h{;)n `%+:nuPR~B0hPSpR>5!B&*4#\KjHHPNuXaDsq&BgjII9')[NJ젘B&A}RqPJ6 vZ7#2aѴIDKZ,+bZ{Fa7)G@-]$8rӆGSC+>`\U+zSUY{d!ؓ07%$^5BSJJ~oZShR`.?ڜٛm ?)q`[;$RabֿURbi\-mxWӊp|@u`p>,YƼ"=Y!(IFb52^o*6X~۫,H$~1mŅ|bfmJC~!-Jgl`/jaP{>asÞ%2|UM /ZQsa:9Ve(kMaIP;qRL}-~R1Vg7>)1Xr\N3')9.CpeX^R m),C),ohe:7C%Ýu_QW}Ql!SmbmED%NRu 2;If;*ے[Kw@YS֯(G( 2v[w4~?͜ޓ')):kWT[AJɭ39\RYJ ->(%Ku.2Jb )PJ1u7wZjׅ鼘NC촦Et! jSoM(qp-ག V):Y HIk/IJ)}N↹R~'Sb;h>$zYFc]//<ƴg()W+=&lyʫV3RX JBTwC#9r«դJ#OHxX30XQԓkpK( .Z!V*u,&`>7A(4HB>~8xzXEA)pDXiY2̮7䢰T`$*ĕnQ L*6SYLڳGB8!{*z%oCEGeLLgl3 Z| 7VbahP)*]@g}Sɰ$1 L\?)4iW1suNYURA:_?;K2B{Jn>Mx_vÍz[l!k)?ug,DN 38ӹq YEmM,%EпrwL)#brxN|<>WYf%>%ځcuc qul.e&@MeǑ&Mqaͤtlu R'še8ٟ&p?H*KδSPqoXvhNK]bNq.-29@܂OzV%IԳe:+<Z,@(>=v ฻Q9"k9&mo3D[Bt\ZlǨ6085I'ZJ7ܧs )7[ZtҢC̓0C|̻.<_K h ۣU 8y^C:E Jʕځqƅpw:) .t` |0/Sd] ;o-\Rm}xFP2 A hI}!Z u\f 'q&,2sh˸{^LL4 *}"Ü9"%4eot蹄[ am/m@@\ܦo}湶JH#R'x\hr ƖQꮓc ]V87C+_8LmÀ0Z۬&v0cn'AdX{r4""ڪL{U'q>HFևE\AaY18<Ӛ~XxWx2XsJ**RrHhV9j0j0$oKNUc¾$Juٜ$Q*rSLyQ9Mku"XK|Uw%MOqPSMIZ/k"fk1ٍ_u1 ߶ T">%r'MS?22T|jtX+ Ze[C^ֆ:^Fk\Om*^aR54UIVXW\t5|eC_M|;eߨM&c |zNiבÉ`{QAP?Gx\YS MKw/4R{#`z\c6awSxmS̳r% 61M.]4`,MVCtJ M_` 71Jہ?U5OEp6Kɚ47b1.]aID# :d;ǧXn=1fT B,5!^Ɗ({)nݯ_5:Z~]µ+̵f3.~dIQR5͗aE$kv;,呤f he76( :ZX6R@ @NOxaozY7.'[wmC %IWBɹJ5)*7Aj?qJ-&㸘R1 *V6OHBָX|go<>u qm y-Ǘv[Mb%i p)]c|YCL|=),Ī-UGw Q_%E?]Du@KmMWS{Q9G^$A)Wp#dJIxit侞%SKq Hx[W}̮6d\xN~>< 5-T2)@]/(s1H"a”m9lxVbcw6yo:[ b&444ÛA?O[BwA3Ԥm ,ef+eY#`BsZt 8 6 fr%D.]7RuSJHaqQГ vv hsA1'e%W[ Q+Y'C):s3Se8onpodv> < ɀMVS؍ 9 "ZE6 BGH&`JR-,†R.c{*Cvlt.Q߱<(᯳Z/6⁳2>{A[53)$>v񴫒8#)RqQwT VXsu]NҪo $rE 3t*tTq3%YʯanCR'G[oME@I.mtaV%SLei@+M n ߜEFIM#l7V #}U@s%-<Ç*eIN&iZ{NB~&X.։!(p|^qY01mR_THR 5_/HO;c (h$}aݙ Gי}:Ė6QRMP` jfxV); lG69nys'_,-B>v\I6$Y,B!~QIRMaHymJ΅ Y 6yRVou}*̼PrQ3wk{ %n6$3m?HjkjeiHpXhw*dZV5̱UW 36݌U,MTf S~3cl'/QeN3JReU!$R}ݭxlSDK,3"zѦ1Z~]+^HMX% k|iŠqrB "8>69oZq'iޜ{o` +3?R@}LIĮA*VCԝ>NYտv SmKmG">r[{lrnJC :ڐ|M)D$k:ᜓQe ]* ks$LJm $|rSWEuO9 @{ r{dÚ;$&L(ZY}a5*iS]܈%D\~1õP:7~Iei7H9MA%(j6*n"ukvMMй zIU l>`ͯ%VfP(I)?fY9LXUV@tCkY؛ SG\[S'-r/x#v=JfhTU`Gtm J2amͬ1ŵ)麋ș|$fUVQHD2ۑ M%%c.Z{j-dP~^*a؃^C<>es""۝5n,}r"e(EMH<êZux {yطMXZ{xVõʤNmH>m-'Pc {ü '^XJBqb)dSs$2xYV%%Ui*Y$D] u+sE(#[nU1*N 'ITfV\! .O j Vbg|D]IM huZ Kh$<όPHGQ s_}iBR 3LCE@7jǬh&9m8֐Apr 3.3n+qt0Mx\&ƕ RneVop,G×2x5C-#WwG ~-JoJ#.+gŤ%[1K)ow)'¦)-^}L2xaK So'Vev⦜ERUxٯsO8(UƽS~*(wݺRNd#X#Lku+΁ps TE<3i3^P?HBo}(Ϫ)Q7Hб6?HJ0MJ)^+DUR uĤG..T#CϬ(amk$y@\'!BqQ#K )>K-$c矏tY'tV&t bh #B?]%%-CM?k # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import random import time import unittest from lazygal import newsize class TestSizeParser(unittest.TestCase): def setUp(self): random.seed(time.time()) def get_random_dimint(self): return random.randint(300, 2000) def get_random_size(self): return (self.get_random_dimint(), self.get_random_dimint()) def get_random_percentage(self): return random.randint(10, 200) def assert_ratio_matches(self, orig_size, new_size): forig_size = map(float, orig_size) fnew_size = map(float, new_size) orig_ratio = forig_size[1] // forig_size[0] new_ratio = fnew_size[1] // fnew_size[0] self.assertAlmostEqual(orig_ratio, new_ratio, 2) def check(self, orig_size, size_string, expected_new_size, ratiocheck=True): newsizer = newsize.get_newsizer(size_string) dest_size = newsizer.dest_size(orig_size) self.assertEqual(expected_new_size, dest_size) if ratiocheck: self.assert_ratio_matches(orig_size, dest_size) def test_scale(self): orig_size = self.get_random_size() scale = self.get_random_percentage() size_string = '%d%%' % scale real_dest_size = tuple(map(lambda x: x * scale // 100, orig_size)) self.check(orig_size, size_string, real_dest_size) orig_size = (1600, 1200) size_string = '50%' real_dest_size = (800, 600) self.check(orig_size, size_string, real_dest_size) def test_xyscale(self): orig_size = self.get_random_size() xscale = self.get_random_percentage() yscale = self.get_random_percentage() size_string = '%d%%%d%%' % (xscale, yscale) x = orig_size[0] y = orig_size[1] real_dest_size = (x * xscale // 100, y * yscale // 100) self.check(orig_size, size_string, real_dest_size, ratiocheck=False) orig_size = (1600, 1200) size_string = '50%75%' real_dest_size = (800, 900) self.check(orig_size, size_string, real_dest_size, ratiocheck=False) def test_width(self): orig_size = (1024, 768) size_string = '640' real_dest_size = (640, 480) self.check(orig_size, size_string, real_dest_size) def test_height(self): orig_size = (1024, 768) size_string = 'x1200' real_dest_size = (1600, 1200) self.check(orig_size, size_string, real_dest_size) def test_maxwidthheight(self): self.check((1024, 768), '800x600', (800, 600)) self.check((768, 1024), '800x600', (450, 600)) def test_minwidthheight(self): self.check((1024, 768), '800x600^', (800, 600)) self.check((768, 1024), '800x600^', (800, 1066)) def test_mandatorywidthheight(self): self.check((1024, 768), '800x600!', (800, 600), ratiocheck=False) self.check((768, 1024), '800x600!', (800, 600), ratiocheck=False) def test_widthheightiflarger(self): self.check((1024, 768), '800x600>', (800, 600)) self.check((768, 1024), '800x600>', (450, 600)) self.check((420, 200), '800x600>', (420, 200)) def test_widthheightifsmaller(self): self.check((1024, 768), '800x600<', (1024, 768)) self.check((768, 1024), '800x600<', (768, 1024)) self.check((420, 200), '800x600<', (1260, 600)) self.check((420, 700), '800x600<', (420, 700)) def test_area(self): self.check((1024, 768), '480000@', (800, 600)) self.check((768, 1024), '480000@', (600, 800)) if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/test_pathutils.py0000644000175000017500000000641712301071671021107 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import os import posixpath import ntpath from __init__ import LazygalTest from lazygal.pathutils import * from lazygal.sourcetree import Directory class TestPathutils(LazygalTest): def setUp(self): super(TestPathutils, self).setUp() self.test_root = self.get_working_path() def d(self, dpath): """ Create a working directory in a test location if it does not exist. """ if dpath[0] == '/': dpath = dpath[1:] dpath = os.path.join(self.test_root, dpath) if not os.path.exists(dpath): os.makedirs(dpath) assert os.path.isdir(dpath) return dpath def f(self, fpath): """ Create a working file in a test location if it does not exist. """ dpath = self.d(os.path.dirname(fpath)) fpath = os.path.join(dpath, os.path.basename(fpath)) if not os.path.exists(fpath): with file(fpath, 'a'): os.utime(fpath, None) assert os.path.isfile(fpath) return fpath def test_is_subdir_of_dirs(self): self.assertTrue(is_subdir_of(self.d('/tmp'), self.d('/tmp/foo'))) self.assertFalse(is_subdir_of(self.d('/tmp'), self.d('/tmpx/foo'))) self.assertTrue(is_subdir_of(self.d('/tmp/bar'), self.f('/tmp/bar/baz/jay'))) self.assertFalse(is_subdir_of(self.d('/tmp/john/mail'), self.d('/tmpz'))) def test_is_subdir_of_files(self): self.assertTrue(is_subdir_of(self.d('/tmp'), self.f('/tmp/foo'))) self.assertFalse(is_subdir_of(self.d('/tmp'), self.f('/tmpx/foo'))) self.assertTrue(is_subdir_of(self.d('/tmp/bar'), self.f('/tmp/bar/baz/jay'))) self.assertFalse(is_subdir_of(self.d('/tmp/john/mail'), self.f('/tmpz'))) def test_url_path(self): self.assertEqual(url_path('/usr/bin/lazygal', posixpath), '/usr/bin/lazygal') self.assertEqual(url_path('../bin/lazygal', posixpath), '../bin/lazygal') self.assertEqual(url_path('C:\\Program Files\\Lazygal\\Lazygal.exe', ntpath), 'Program Files/Lazygal/Lazygal.exe') self.assertEqual(url_path('..\\Lazygal\\Lazygal.exe', ntpath), '../Lazygal/Lazygal.exe') if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/test_metadata.py0000644000175000017500000001543112301071671020646 0ustar niolniol00000000000000# coding=utf-8 # Lazygal, a lazy static web gallery generator. # Copyright (C) 2010-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import unittest import os import locale from __init__ import LazygalTest from lazygal import metadata metadata.FILE_METADATA_ENCODING = 'utf-8' # force for these tests from lazygal.generators import Album from lazygal.sourcetree import Directory from lazygal.pygexiv2 import GExiv2 class TestFileMetadata(LazygalTest): def setUp(self): super(TestFileMetadata, self).setUp() self.source_dir = self.get_working_path() album = Album(self.source_dir) self.album_root = Directory(self.source_dir, [], [], album) def test_album_name(self): album_name = u'Album de l\'école' self.create_file(os.path.join(self.source_dir, 'album-name'), album_name) md = metadata.DirectoryMetadata(self.album_root.path) self.assertEqual(md.get()['album_name'], album_name) def test_album_desc(self): album_desc = u'Allons voir la fête de l\'école tous ensemble' self.create_file(os.path.join(self.source_dir, 'album-description'), album_desc) md = metadata.DirectoryMetadata(self.album_root.path) self.assertEqual(md.get()['album_description'], album_desc) def test_img_desc(self): img_path = self.add_img(self.source_dir, 'captioned_pic.jpg') # Set dumy comment which should be ignored. im = GExiv2.Metadata(img_path) im['Exif.Photo.UserComment'] = 'comment not to show' im.save_file() # Output real comment which should be chosen over the dummy comment. image_caption = u'Les élèves forment une ronde dans la cour.' self.create_file(os.path.join(self.source_dir, img_path + '.comment'), image_caption) imgmd = metadata.ImageInfoTags(img_path) self.assertEqual(imgmd.get_comment(), image_caption) def test_album_picture(self): img_names = ('ontop.jpg', 'second.jpg', ) for img_name in img_names: self.add_img(self.source_dir, img_name) self.create_file(os.path.join(self.source_dir, 'album-picture'), '\n'.join(img_names)) # Reload the directory now that the metadata file is present self.album_root = Directory(self.source_dir, [], [], self.album_root.album) self.assertEqual(img_names[0], self.album_root.album_picture) def test_comment_none(self): im_md = metadata.ImageInfoTags(self.get_sample_path('sample.jpg')) self.assertEqual(im_md.get_comment(), '') def test_image_description(self): sample = 'sample-image-description.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_comment(), 'test ImageDescription') def test_jpeg_comment(self): sample = 'sample-jpeg-comment.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_comment(), 'test jpeg comment') def test_jpeg_comment_unicode(self): sample = 'sample-jpeg-comment-unicode.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_comment(), u'test jpeg comment éù') def test_usercomment_ascii(self): sample = 'sample-usercomment-ascii.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_comment(), u'deja vu') def test_usercomment_unicode_le(self): sample = 'sample-usercomment-unicode-ii.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_comment(), u'unicode test éà') def test_usercomment_unicode_be(self): sample = 'sample-usercomment-unicode-mm.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_comment(), u'unicode test : éàê') def test_model(self): sample = 'sample-model-nikon1.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_camera_name(), 'NIKON D5000') def test_lens(self): sample = 'sample-model-nikon1.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_lens_name(), '35mm F1.8 D G') sample = 'sample-model-nikon2.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_lens_name(), '70-200mm F2.8 D G') sample = 'sample-model-pentax1.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_lens_name(), 'smc PENTAX-DA 18-55mm F3.5-5.6 AL WR') def test_flash(self): sample = 'sample-model-pentax1.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_flash(), 'Yes, auto') def test_focal_length(self): sample = 'sample-model-pentax1.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_focal_length(), '18 mm (35 mm equivalent: 27 mm)') def test_authorship(self): sample = 'sample-author-badencoding.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_authorship(), u'\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd \ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd') def test_keywords(self): sample = 'sample-image-keywords.jpg' im_md = metadata.ImageInfoTags(self.get_sample_path(sample)) self.assertEqual(im_md.get_keywords(), set(['lazygal', 'Iptc.Application2.Keywords.lazygal', 'Xmp.MicrosoftPhoto.LastKeywordXMP.lazygal', 'Xmp.dc.subject.lazygal', 'Xmp.digiKam.TagsList.lazygal' #'Xmp.lr.hierarchicalSubject.lazygal' ]) ) if __name__ == '__main__': unittest.main() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygaltest/sample.jpg0000644000175000017500000023652712301071671017453 0ustar niolniol00000000000000JFIFHHExifMM* HTC (1 iĥHTC DreamHHGIMP 2.6.11PrintIM0300 0220Rf0100z2010:02:05 23:56:242010:02:05 23:56:24R980100(HHJFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?)iʅ Ҋܱwum"fM8,֐ş̿5.iu-vҭ¹$WM*|3qJֻ=$>Jo/G'^lԝB1HuH?K+ng'5 qyk$9Yx?Jdu]I*Q,\NNZX]o?5ɐkGM* AXͥDP(4 %8)n6C;z!|¤xm4&$) 7 ZӉD "JTTtS ظ/9U sEO*/95;‘v淋ޔkJG^@ϧH6 9(X T W0xRUc0sax~,X}b2=Iw~ 4&TB?H 5dޣ{sKv!lJlSq4Zglu;46*M*\ ?eIYK>䚱gۂӚe#KD2ApΫ/*,MrͨqUiM,\\dc抽&8*&{kU"{5ЯE=z3kƪ膚E4P sڣ*&Sy,xO񨜹U˧gb+y*`qt &cN{pYDĞwFtBXZVltf.<Θkwz!n"Đ {kh%gfQITIeTJ)>hfUY]zΕ`Q4O$]:zl+;\]8)E\̼081k\ٙUNpCI֤KnMu'7i"8Ih$.:7?sӃͅF#nny4AxT@JN6A'{ͣj&ןx]7~y}EtޤQʥJnz Qz֓M6e:IDEa mۏs1MT)+ 0@Gu8IH={%*hmyu UC[35id~G֡Sk^/)9QkWgvn@U]nkY,l:.;E#yNhL1Ӛ.- )zilZ@Kd,uR2k;\:lnu2ڨ=:Ӧ8sM8&g`ʜ% `(zG=!ʃ֥wmvG +#c?J|b ?RLoGp!\{lta-J+ɈڝŽIQxfҵJFy#ҡᡗpa"qh{XI&O*%6iRFQW/%0 ׃W7p U=?w?h8p6B_xE-Ԣ0 ~U|#?gNU9${)'s5:;"y-5f "'ךHI^$s*uN]F:,&8I2hvZ(N7!pr;t#'9"1s\HM)%̈́И8+aS1E9'kk}k L4Kޒ]'(Ia]vQyS=#pjE'9t]+cs^f[~Xi)KT1^ WSD?TFkXhzױTPv:''&vת LcDMuMuR^Y;lY|3,qRue[rrt69$,wU;RyU ^UY Mv&r]W`yzD` \ϭMW,q¯Um̒%Z18ѦPY 8ۃ>^]GV>ቦ5hsu5Z.7 M<;s q@kx| [hM{XCag1YKOgc|X𞣴i g?p?^e{N L{-Z^A:TjFћwd#u]x>v(=^+ Î=y n4ToJ:2^k[)5ɡp W3sxW5MO79c9|hﴅ[v%^N*#f;`.Tzъ )1dب̐O^4^XE>+JO-{0-|R`uR3SymB޾C,n=O5ڮE7 A 7sb% cYEn~%frwABX%HJEzy!ipkyz #k}Nya".n-\=DҺm3W4uyA|fau*OY^ O/}gkJ_ՏugZ.(vZg ^miqf~b" j(aL[Yxve"Je/i^.խ$c>`52ɹNWr+p̧*|a-"RGi!{CZd1{O{L (&?N˅t٠=& ?Ɨc`Ik$[RǕG?t( Otpj}im4BYy'i-/4:1QVFRz\׍|g}NX"?ƽ4>Q2cU{!E]-*ww1\Z}iW#:$5 +A-_MW=r8?(:8*(y"QߪPzE r609$yp6ӵT1&oe =(]ʮFEU ';]O,p2CPI8:&ĄCYkڻ SN1GۤHe{iqIڛn^U iai4&H뒈ԕKJi5XaQI_ ݲ:}%X]Pʾ }ÈNic0(/-]"ԝJRwa]]v-˷as -]+,T9LU+:.:M#uIu'/p3xkG$qljQxꐻZ./KiE vpS)uSe^!ɢʛ P%gu$C2[ : ",ۈ2\X38eg4nRR>KveRnNbϩM!!bM"9t DHq)asmby4)u |"Sq$+rN$0 '{[ )m $~vA!v7KZ6s_-n`H?bkyFd0 ((BtX N-y I s u:1E)H@Q Wmp`$vm#àHi{V,v՘lhӗ(<#R X9P?h43[AFD܋HFlj'[i^u,9\U"@UyAzG=xR'-mMEbڐ;ePA#DT3eHc2 >cn{HE~( 9:ƯrSshTmDeCCX pMytBn.u&FdZ6Z^:A*̠Ty ܢX0sv @FP6\(2uBrQ[x!I.Gvde P2@:0&g$%7+9BoM@m"@'񨨨ܪ(:$_e-;DԐ.:A{7QӚm)xӪJ|9M&q`us70' ޞJ FbA{}!MB0(inPZE6]$A@IviH(Qnt/slyaTW BK1%z=" 9nPh}IMQY%N'X#*'N"(zڅz^m|`PlUOSu(kt$ R&+jĸ Jo8iO؍nLE[V,T<ЍcY6k5kלYE A#v8 !,iq0M~4%GBR^E v"[y VXG @S`MpF[jcz-Cp59p{X6ʠ,z╠\--3 YŇيxIrFR,UE^0 Gw!gva B#$ =u1 ) $@'Hd'%+A(t^ ]J'TH(9Ǒ16&TyMm) \t'bEn6.X9!C-D7%W7ԃ Q2\Q+hm2=sKêIBt:ne(H5IwFMu1# 0(; iZr`Q.W TK`.xӪ JIo:H7"K kceݤPB/070IK vmBP 3h P.km<}29[@Ahc%V "DzmIQ$"؝.]C=0@aF-n;rɑMb\$˵#G[HvT*UƛD0+ǽԬwp^XܕrN>$yF&7; kxf*' 1-6VlEtck('*G>b BG2I!ZFt}vaX~\4*=-OqkRo@O1V?x 8.:2Gu6nR.uH-b;/Ŕ˔ENA{@*NQ]yH)YX:`PK&{sN$ 4' 5AO8BmrE՗F(Nl{ʠm~pG׬O0^׿(+!(}MރhHܷނܡ4J.H:6Q,#L)jVXh=:J_)**i zi,>GR&&wJNcѐܲowbqO4eR$YbJAJ^BHB|%3Nb|({n:[RJl$]i6%)he,IRSyqmWn`BY)N&udkˤL9Q𪂓(%TH z S2_Y3 \нԌXk",92Pn9#T=dl%JwkUyq$=2P* 'SJe"9 0q.AM΢uBmSU9'OH-G5_CF*3~17/,x]E)Rl=D-%N}ܴ{*u^f:&ÊAԃUmL y)g2xؓf'M]^)ma|_IQQMNIesr``.%A*ȵ@#i%WF =m@l,AF(m9' وH 6 - Nx5h wcz%t3 ;e$[!TX:P5-{r3xNmMtؤߠ]hqd| s0]&Ka6P% 렎IA(PQ} VA;@A϶^3)Kk-wv#}tr- ,7:AJ8kYU@"%#63LZ6qAFY֛?0Ӱ } MFaIE h/w*Ik>*l$#xm^ZI;]n=#sh6#umn\Ia  q;$5㒶o(&֍:B(?ꬃqjSlM'c7q}AF6Z4Q !xck^4uQ@N܄iWQ Fʶ]8Mŀ;oP*Fbߜ 2Bːn Y7>v02d_40ˢdRH$bVmI(z u$ӧ8$%Aa!0F 8ܣ -iTExN \ej 9C)”Ua]]´: SG)ss#1hNxuSCmM_ mrzEH_Cmc c**#-LS3=-V}>RÖ%GR>)ʂT^w#XƔZh,~ZA}:6U;0ݥ˨M|C:εvA. D[db䁭 daÊp&4HM-X\*{MP9H*E[iI*YSjS+J씎[_96Lq7LAĝcKH}ѵjt^Ƞ7 IGt}#wPΖ{[ <7]J뎫YI(Չ<`\8$ mt T +Cӧ>As1#MO:ɺJ]XHHC;l:a6Eeܤ߬(.-@8a Nl0`Mըm-Ü -ݵpAĞY@{X'i3I$$y6 FjT.Lx*w )< ~c_Cwf tQ0 Ki/UꍇU(I/(JXE,6YNe;yolTU)W.n>|# {ĶٗGi3*u2[īMH|y훩@n{> ˷+C’m<(JV04i@ԒB!5zi"] 7!Rij$R9 #ji6I$Ͳ[kNzFPRT-uٱ˚wFe%6Z;B֛A7%\kk* vF CM@5*ʹ `+Ehak FN4h÷Q@<'NMUR-tMop#4Xc=NHL+'4ۉ!AF?RaJ2kp}f:VEYpnB]gOU;35zEyf+[SE6RZlƘ+I9%$-L2S<4YCi%ZECO1(s\q-MlEYgc<>s P2մw56l(55nLv:ozqV]0ShB77Ğ ~`l"wV`Bn $yKq1H ʒTܓbm_JZ+ZSCEY.)"U>VALf2)u\+yEʾ2[-mIr?jqI6^+0e}/qIo%9m(ص+E!'Sn?=?>赒VFueXzƖTNO*㭢V _Ur&킢51o1JB'\6Mո;d1Js X[Ply@Qߔ8+qNl(6p H6u?Q1uPHkaq}>:G$ jJkkF˨(Rb N`wH1B3tfRRYRADۺ#x`Fqh W!FT}tq%_ {Ckr*w6KxF!6 ͪ0drc %)IZFC|4ۚȡWi.V*fHxBT:SIY|YYAVmL6Hh]@!43(:C$:$e}51S`cO@zC߲{obAx#6&hWkxͩ鵌oakEq\a&"1@iV$uR $Ih|mm#b"ɢ*Góra!6VЂA?tx1}_HR$Y?XR, ( ygËZq@bRVq/@BI<@8ai ;-3,^a`Yne&2i2)yP6jpR.:$Rt8<┏YdT쇒4n}c/@#L+T)유/"-+gڈP +@ģH}WQO\KM.`%*װ'shR8Ena$}F-sFȠE~țQ˓N`@ M=1Ve5P`RS^_h^!xng^OHTI;PBlz8c]|B4i*a0f'`4i Xn (|HUq兾 <BHjoὡE:YiQ%DƫoD.K7QӴfqVjKGe8V^h27 mU##up?}MLTdq$eG:P ԩW SxCܕ~qL/Qo' gЩl/&I7?)tؤʮK7H(ԞSj+SkUH)Tt"%GS^*L i)*xh{/q5-WsĒm/"`+H(4Z!^ qi$SR4QNm@4T6NWyɍ4'^@$hyhĒv6򝫩]6#1I RccXtxS958Z-(n~@b^m3.'2Ά#& *](^vĪkIQź&gOh2Lͷ*AQPL8ʵF*)2e-VRiPCri <ɴJ͢1RS.J%c( L4P{KtNDpYHQV .u=`AZ`?ycZ"<\kl`:tIoH<NA~Z0n0M4rM9I IiE:vM†4[{{i 7qӜ*hJiHUƗɉek~IYfZD yAuO)PCr @VSbP (! ;TzV|:֐Otܔ8PfD.!۩M2 Oxr-c,7. ]AHY/=9.@K'Ak๺[e/f8snT̫BbBv^$P=uyrX85WvW2ISE o$vĐUJlvTXv?JXƬB?Gmqk[&UJA:Vdr "ߔrKx)Zst}3X*iZiΟwS닣m~&='Q88^Cdl uՕߦb',q>UjclB1cVrm>ߦOEѓ=PNh %Z''}vķ81jӏӝqFڦ-IQVRBBlAĈAifL1A @<<9QQ+^ȷ]qS}?*䓧̤-OP}!ՉYJKaԩ+YO[o8fV tP(X(ptG+|T*oSb E0BG|ݔu vf\qJX  mp\Z6N]0c aՇ,mbЛ^xˈH#& 82D Rv$D6.HJ_ "a6maLGR tip#Zk$p] IRhmd!bwNސRfQU"щ]X PPyG[D9-p I=ȃ9^ [}j %?x[jCqiRAM:^_psS]X*6" e97Tf:A1lbnMyQWW.|qH2Ү|Ca~< 4i {2t{۬4P5IBA9 Na{(}}}: w6Viu3p ,|"QQJP0vG;UfZpp TN[ߗËuag]eiZ|_M5iJR԰9I'ja8y tk躃~3Q k<)l>3:H*9VTs$$Ĥj/Scs-H9TVʋ]e[띩Qq#~V8g}Hme64jTI&:Ãݜe jXo&ȻKMma߈K Fު1伧a~/yCꃝ˟vU@qXby%Q噠*]ƜBsͥNCǶ8ǂ}0SMfQUOJěu)L!&zҤt%5)H{CUL%v S) [ ]:B%7s+ljXm&3[H[Rw"9.Rl\t1ufiR@荝L$hGyoJԛ'q,(. h [h0MXrm:-`r94Va`:mzFh/פlmțƮHTrq\Nm+W_NqsALjBD;PJM0&] jNm{zu/)XKhSMGm PU SsHiT'c.x"2^oVtBI{">5e$xOB D|.U%4n'6$h9>EVP鵯 PVe m }X/JU= d^(w?]`M)$u6ӜF,'06QיB֚d%.n5o LL(hkAθ@HH/0.iNK#q 儷ƙi~ q+/r3 :饡\(uXP%baeZDP0mlTxs&P-%* &-E[2Ʈ:h&oXr&&JT };v,_0XB}@"TQQh0Pv`jOM1N:+ph۽eL0]CI*߬A1jZU I|Y+jW*7!kRf, )7쫦I11>83 }O"Ռ\ZPR"eWbw)N:pZlDѴ=R^vu8 ~]-BH<ɶ 26.Q6[mLxk.>`L@,: UGhS  97 }cɎT:Q:@ KE66q3$XNM2kD9WaK0ݓOzF]Tbɥ%.8A1ÿE,[Ix3uK*P|U>+9(MI ˗%fH-(F ,gB7 #7 &3c9y+ k51=qtERTWP]L*>2b9t1NQ@BB co+~M?ya6;Me%CFn}&j$,yM Jq feh |-( _JڊWZޟ҈,.3N|H[Sx<6f{n&ʚRZ GH [! +إ=K~]7V\IZue˃̘h-7*@f{в-oDX:nDZSP&ZR /xcbrl$B|)Ma̍Seݸ*%m(h<;C,Ps4mR wA/Y5HKj.eFP? %$ȫDjš>=BȌ Ũ >!wHEU$;Լ9.q-+ ^ՆUqa{f\Ve OK S8Vy 1p?bIU5 |rb<2 ]6u:E} HNDoc5]5Xtd4Fd= @(loxM[ZA\Z/5a%ugT|H^(3ٓdP5ړaKX;cH!%`/GnIʩUGħef M6.̛ZGrq."Hn^vBmmL.(+˪e~!)HZؓb[.%e9;xKq|56Tr)$~Ax1iiḽ鿞hWEhJJJH+UI7ǎ2-p'{:4 KTzp:BiYY% a-j|rmJH%hj;EcR隖ZQWbIrJ04]/Aۉ@̝F0N9VhBP_x龢9x]idhsnr&~hMTJaV3BrRPI̯GW HhLvP}.)Rgx1Ɗ fiK#Ē7JBPDooJͻ$&Jcyk3biҷffyܘv8XRyK+2+owBvϖUhu ͨ1eeocMr>7nbk~7}n@ 9mOX#9";;FkoCZro{(1xsu& m@%DT]|I7 8L/Öm%`FebP D  x0c]H-*QEH*+*`ha M8Yw)@o ZcfnW{?hE[p~d.e:uAhq@۔86$y&]rtT TKK:$86fEԼ!.30qQ-JMݡ*WxM 05˷S!Ħķ+0HʳIe&–aN (PX+9t܍".Ɉ,<%yR 2&7$"m@ey0LPj7:~CUVZ.rS|A)M}G){W>ګ+ 8ᙉ b9PjbI.HB6/QJ2?{%Q * nE-.GI|z L~P TЂ8' Ҧ[3҉Qk9>wsMf} )̉&]ۺ*AN+4HFF^QuG>⏊# o40lTs1m:E>Ue/(A'X]DPؘ L˩-(yM=hi{C~@ڎ %N8Z6o TW,ۨ*K,|OyL@]iM_]%>"ύic.:7mم -Q4i6D-p隅=#?LIxA "e)I0nd$YF6{vV9Eԅhn<X ߉Yrjc)(՛Rh44t[#./Z.ZĀ~P 6>6_AKm¥hY߬c3O0xB?}6n>2U  .Mn5,R$ } h >]UӍTX+Cw!s^1c9e;($>iJ;,e"REKf#P㺉&NѵzD,  ӋӔ BmE- HˬX贋}b\u&PvQ +Oo҈IBlaoPl*(&oLS@s*3BJy-`"|Sԝ悔NUs=u(r h7^&M)B:7;fjli'WA\R>TX%ɀt=X 6X"'2$>E[S1Xa P~!XVΪ{uS0GF"N { LUeҀ$ACSRRJ[h9Ty{0 H;.Â[b:\fh7[-褡W^xR&tNv@4 дR Ny3Nc!κ %6n%ԁC2a>SJ3Н~ե—7k 1l4 /V+ *R Ҁ oXּf.Q[C &=6.F-`E[ ,o7=c`N5܍&jytxːl5Ckt4 XFT>_( /e @x7Ep 0ͨ` H&n>Bڵ&A_?hޖ5 10iHyu\7+-$lр.[PcZQ"岠Sh6&$}J N) CF;,U4bYy##׸y-8]=]$)"TĬtCL]6 )6WEu6-rU`fQ[@#%TZRlҜ“KUJB'4<RڸHRDs:Gp%?숙5m' mi teVͦ-EXѪq$*ny7&22H! P#khaˤ-BD-KR,v VTC\0ĒͿtj}-*"0OB@kzDR. [}xCG\^ M%xRv=L.ememm!RRF]h]HiiJ'weܤa\!_Rꭻ56R3bOcyG|B moY:˓"vAs(N\m̷!+ZᵽhItX2~||Ty#kS%bFk @;zt dM]Ғ2 Y}?Ui[8iBgx q74 IU}gU%B!GMM*g_Q"в=b;'i [.[/v/`T2s1䴕j;=>oş!' ]/V {̲›i ۦ(P}>JZ5הܨ_]95)Lu-G\dQ,ʱQ EJ?Sc%#Kb|C*ē(u# T}r JVuljա:2Do84J^TysҌr :6I ^Cy;o8I)4Ye0*mZ?sHaɏv3wDbH(Q*jW|'%5,ҭc%E|uMבA0P]K'_ߡJ%]DXXK*l DCL#%#NyҊtAq b 郮ǑTyic-r\ Mľ wLaJPs8gNHKpMCU- :c+}F f0K%%J3݂D]q> ցeZ er_{{Cs Pd-c/.@ Ǫi'!`>"BDUywfJ%ݣe9G̑*t}D):]6{S[amĒ<"JP ;I jH ">GRm)tgEiG Fy1OsPHW"QyZ<8!JiX2BX|C-nGDWF,uiIcDk {8N3]`)E o 'p,uJN*l! syrGvJIMl6U)/8eH&$} "\<+c l,|W2弗yCDݩIbs-x-ԊBYjT8lh#;8gY L˲Nre :j XUTm+!@;מ,(Q[C'|#S<0rJaHV[ 苁{KxMy3<jɷ?/<֐dNa>)pk \N Pzl ܻU}E)6 |ʆiimSI|9-E(L D tj1S.o"CQ7:GYeE "aܨ:*<4*4ě}bJ唺{Rթeܿr%T``+k*akA=E'rHS'K(̓1WJ=.BϷ&$mA$K??bT|\۪R i{ˈ7&C_:OK%]"y%ge'9{ݟxU)nV]̩s@)*3|?%]r.Lye,:BDkDH5 P43i#b#ù3al53s{lo.[$ F$CXvQzFr>Z% `sHkx$[]ot^Sx'J vX9SOUmS|zNLu4P;IbZu JqdC7C՝<+9.dZK2TMϔf ڦI=a[30jT\[*(F_Gi([,=~~p7FJVf$e3nel_I]O{enmyŎh^L4_y #HWHMD`YZ8~}u!&4+NzqWX}Z͒\X"n'AaCnDT5`Ej~s>i_%aQ}?KhPHb gI7zåd5:|BQ/ͪ#F]U4.'8pN<.o$yu u2whQs{k. aj]^W_]QKwir#x&RN}VTt-T Y펈o2Aೄ:y,TZJIJlIXVE7OHTZu^]@}Hٛҥ\MқM̼ nlXrh<nynzIJje 3:ؾfe5;s&оO ПZFZRiB@SMA:@v*|$Q^cL0SAW?2RXS(ePTyCe// YN~gV QMNȡc[%ԫ?k,*ii ɲB~]! [+̽kE'@\mzX`o%V+Wv,o!;_S%^NIi蔤!ʹre^(vv̕mےqV^X|jR6|n!@xղ,3aRԲn-&!@&&YcTV8TtU([РJ}c8;4?M(,*!:|B _aR5#V:E1OUSd /2I9&*@ k1WD];c,B &b?E,Iv-&R-i)Eksr|Tz;e༣Oڌ;;Q̝K"Z3ms~$Mm7pUrUUҁL[>ʱCAςZ=TaoMIKJ/U] wKAJxu1b7^ v:+jS72˩p0jzP͢E VRi`ahkU:Id5-2&ε*䠩&XKW>$Nn]RRܙFR*HSgۭ-&yUw22NR"Ux/d RɋkIԻ :X/L8&KW\nUh*J& HuU J>z Ym[D'T* kz۶ؿLZB@J}O[RYLӒV4iJy,$!@H6Nc,ەNXCEy! PO*WmR:4Yh~\-*"3ym4$BWwN_Q4Ol*qdž8fKWn9/m0 M-*)C}dO.P Ҍ@s qʪE'n~!٪-36rISc M)-qbR@sS3 JRGo\Tu%1*R y3qZuiʼRnPtP3DJqoo(l3<.pf]I iKLmA%I)v82k.Q{:z󜫒.H&7P-+8 lJЛw#^wRo/;3Xp{SKGxOU.c!]ENT)f uv')CR\f҂pB$_tHNe$:u*3?탊#tʙf3)oSHU3;:UxR/a kWmslWw f9w;*o"Mϔ*SkmI7 .)>PF@c&ihQǘ0gY6[dyKB;s5BY iDƣsѫZ% li!Kx3^C|Of+ݍ*0TM̘q)_#0[XJFjX) ;8Ԑ0)uNY.@-w8!NYy@hy=v%[ /]hġ&MYr*))Y zbϢo*ޠ>ږnzZ~G9MIKd])\6TnFI -*hV<88.%E*4*rvY Se+Q m-Ghj'J3ׂ ;w1%IQ=O i;C/.-bD@6Hz;/}kOM:@eוfRJ%U V$@ȿ1@Sۋ&?aiI,w)|SUi$,\Bp5 tہ #AxUd6O !iPp\d֜qLdZT|ֹ27.n ZfM)0%д_89:@ ^ǘl%1KL,`Y*=7J02 #/ cM ZpfcˊAA6%85OM^i{xU:A4䙜 6K.P?Z x'Hq.q*i $]K/Hpxv*SqeJlħw$̮HΒY*7{ q V~MMUA#h%CMvf G2MaR-|dn)]D1,aQN0i睗m|K8埯˺]BPN}"YĵgQj!Im ""۟!e,5o\UnaYcԒV:i'cxXVYx6Ӥxnpfk8"<6VۖwB}H8ųr SS*رmk^ثlN-S8nY+?2=nXK8_A^9Q%PeYHMNoy]^x$%ŕ6A츠Vu9[XOͧbQK٧&j΅&eДzm -Ty.Rҍ‰فK 6^5s 멢!ŻNr`XXު~;KkCK kÜ?Tߵߝv2妥.) bHUuSlm4i!+Rs Uǭe+EG$I6 4_3[?N 0'wN*RwJeTQXârUג\X.-(biըRĢ qn:awQEX˗ ^[v8`diEgIY*O;GXrt |e2;G$\ KL -I?]!V%Ib.Bueٚq [6xN3W4Ie (5SH x7/{cָ7t)I.H}q[OŶYXY3m.zj)*bQqXsVSqoQ,)+>Qlu1ӇxXڊj" wc0Wo]j/qE-&μLj(^: FSJM1xG[Ī0JM]mbW7LyBdKcP5|DnC}hv>yļMLLVJ&.s#xQ]V+ U4RcJ}yt&%&t\w/xm8̤Y&[,pRC87WrU<øMGPv|)ǼR3 #Of1>› O0X8t bbh܂Vxq7Ԯu|Ivz[xSEYs] V/ff ̴.o 4lsƎ.4蔕|dIRG? }8!Go:\T*n-?~鑸ctE2&im mD_䳵0A1:&jbԱ]Br`ThCN*ФHqA^Ë ]*O4L(^|&*",<b˜&Ư$MU}MWw+N vXHeZ /\\D.%[*Xʇ0]JlJ<;v@S5B咃M qEHK5O)sty(h~q,W䴸y&v ]*ai+'"ʓ_]-/3Om)Q66w4ֶǸ*=,&ZBQDx)|[xxLU%&q'W߄˕7*FA zquWUR Wi2DH92Ts)jJV/bTҴ w${u4b@Fs Jqn.!#:|*I5NEc_X.[D܏{G3HN͸RA:ˤdn릺 .B1YX%z]JT.qcK$!_;EFe=$gVxAWy}@HE+XkN!ڌ؛ 6յ) 9B=SGmy^]lUZ6WoN_1հc#el|=3 zL yO]a,p(\KVSL"b6oZL Xۯ$BdeZQtfK;,*eOC@襏x_UY#UӤ,{L  ٘NZa~Fń ߕEqg U&EE:yrJԁa|1ECOε2naLiT(sesz湇}q UBE:Gv^UTӞSz (ɔ2T,-Ek>ǁuZ4kv|X4KqʵʛP;1tZ|U,]ɧP,:NTZeJiԅR[Cv[׺&0BRO8JrjeR6J̠ \k}KeiͼNTQsL ޵C.k}.]':0\t5 M| u f36@? hb8b*SeHLf˳̋Z>a j]{d@b0eB盝U+)F]N+K:l[r51&b G\ĕ3.?4ֵ(Rc>Ҹ* |@ ⥨XIrFAjQM;$v^ 7S /2)[r˼&t<;˅E t,r^zQT37acdo (H$BY>[JY]C6*;"JTb9D;'0젾)D%沔-Uq_CLEA2`] 9Yr?x pB+RIFd?\UAʴdkk:ˉ]JVP\4/C4zLɗJ|G*Дߞ&qSvW d'~NBK,?ߤ``ʸ@S2x2,(yG7x %;/[X_;Jԩ՛kZ޺Cw>=u;y U먕ItӦ6wsQ0؋ KD>de!Ʊ{KOJ\iEsoPHݍd16Âz\)Б%au&6j#N7N}64ߜ"ATǘRKOXaP⑲08lWtm*e$oZ08⚴KZH<~ QVK$˨9R !ni ڧgut\Q$PNP>))‡7=54܄`QML30!`(oOg*E+RԁdD)h ~Cp ҙi QRP0Eke&6jZɿHs\^U.%з I7hku2)孡 S4-+Xy:LxKyB'h RS%~1[#?'KEF911LmJ"7%Sj,5B@LfؐIܔ/3/R䫩&;#O-7EDAaL8?|FJ.x}L+ -8n1Z.w̕h,w'lݨf\SZ%W'LkE)S{_B-E0kg lhTL#a) ,;H]RIște0NK r(xwiP =9F^eMc6zqY51(@΅rhh4;Ϳa WyA؝gj*B9QN ؤnJG9uz>* S1ɭARv2Sk K&KDrw~ś JїQVq +8mStCbFY)#(HIM <7XKxyYs&i:ú2vxyTOҔb"@u6וذ.{lbRbذ{kvoH 1T|:ݾb 67eRm`@yYawjxMnD~DiIXnuG_̇1$Y(`@R_0-8m43MqyХ3_'l"]^'5(2$yU=n$5D항<,yMuPxG?Ն U:T0T:1,$^uJ$rTPYEUV*$ ܿҾ[ ʘ *RㄞUXy:n*SqJSU E s(.O'WP֗rV ƪX_JXw~Jm,9r1i*{O2c@g&TV_Tԟn(qI>vKZ nYS2s-*[DT6{H+fpIs;|h>"gyS'LOi*REoO;Vp2ٚ$\i`y%Q싌S_w b she N%T*t(T^rXI+$hEELvQFR9Gx?򽍦bU3wP\eESsI&G0 xN.8F$S3ˀ: `aE@ikaI8Y g1?Kzy. NSsO=I'*{ S1U00 P׎ ~2q"qjӔ<.¾.lnOU noQ_.f62PEu]bQIeԶA7Ϝpoc '_2ʹvo)*nv;jitR%|J& @3c~xSfM6HSs襖2 |Y2)@&STl1,ǸIMkHp})רn dEZnwGNRa%hIX)Rܬ"3$$\Z״ uKBF]|lu$)n?+A?OmeW?8*>rb\~Db%N_$(Xe2 M38Bk z{Bs=tL4B+]^' 웤3 8#0:y o/Я:w~XJcM1Q\E`)[cԂECH-(bS^ABw^βjV4Q!J NU4Ҵe1s[JjA 6)ч+L,$ÊRԵU,BMa۟C<:I4wY<[DQ,]Vu$t")FAPK1cVu@ .P̦"e$7*wBYiޝ vኰ5UyIf|wyM;rlTלr$ _> U0Pc3Y8xkt+ͺOGcF N Il  Nj~~Q4,OXF,MH${o )!CT 50Q o"rDr @&jSoX~nXjZBC䪆p&-])f{u$Y( ԂyDBVkkAs@߂Hܖ)*|MxW_3.܃-b?摾KD-oXĈm3_M/h<5V4uc1 mMS }\-y[H=`:MeAHe녥6#m|k 714 zT!'&MҖMɶ:4).ǙCGvE5HQ7QqÁ-NoqP-)ޱjaǒX#^e^g+;nG?1a|5=OU|.!PwgIܡ{"'R*JK}MCڰGHU0yتmKIٙkKiUt:jo{kd}z]sQ,bZa) ?/XwK)3)YKX땹P=Q jS[Y^p+P#R5޻*opHfi7'u仓i[ )Gƾ =/ T׆ْTHVE.R2Y U;C3Q+ 4KO4GG_\^;+<1[M(e, \ǧ}0 ᚼʦ+&3>$\hwsG v\ !m'2܉a_fZJ& uVXzF01COR@whWt5?ISRsMPCRHQO)my0Ļ%kix1#GCj>U?/@ 0@T$ >1qnjر>ܫwT ĺ/(Rlopk$.D\`ET,|Y.;w|+TMzbkU]l:JV,6^Q^JJטL˒Wxܢ3($&\Z<%? Q3#) XX[quAE#lWhˆиVsI?8fRʼnVQ'+Z>k *g`w?”t[B G0vBwJJ JU bcwj*qgLNp0܋_MtH{G>v1vzAԲ2Aqg+:_.p= `VL7]K?p i;3-Y9􊋊T\[b 䬼īAȸEcs`c).+L`}IiMm{.>66k3ƕ3I=FkZޱ1LEH9 ދ Lי:/)3 ai)J&Re5ܿ~Vth:Y0ZiM)LL0Rfn5ʢ4EoyCSDw2m uW%[] QTR)zV}J|Kҥt%Yrx4A J7qSnT'mE4[R;Eqء౑ (>aFnSLNJ>-)jQʔS'zT' lY.K-&ZFaa.0CMl?HQ+!]EIj?vʨvlgk~2>_rANuqJYuE/n9XDNp)NN[q[5rI{OH8ŽPWbTyﵡ¦gR+5gJIݶHKnҞpԙi49V@ (@#.]AjT$2 NvA;hR5|RS)zp)YANpViXuqŒ)\{S!RJUG0r7+P@6^~Q[ B=kX`6p-yCR,ptפh[SghsA],t]bNet=I9: aE!!J:YL 'iuXOX EDbO)8?s-PvDWJiA=QWY`H&Duԓ~QWmS&Rs%>EvҜ4_O(YCa-GGUsȫ PĂQ&4VUJ Q]6Z)@It ` 1n")ƎJ¢Sha'3 rȗq2k ePu nBXѧҹB(!ʢ0[l\@~Rh',j4U8FJeԧ`e* (rSg ziFQ|b\ n1EsF~e TQb3!m|牖DBWrbm q(PCSՃ0mkKSSrv] BO?5,WHI>=pupU veזR|_[ǐ2VR2 zYsJ0զPdʗ&X3;Zظة\ vW].O>2v9>_;ę@SgK'M J蝞hVPoiVeUoxpŸ AOP)i61qy1-stU/2_zOl51ҝ%Mg1%@a &\ yu۳1_h!1XBҩ%$4:zadP̤! JSbG!UL;#nH~#v?%$۔e(myBPV қ4_24O*ZQC g'堽~/%p7U:=oxړU2.LT@Wq}71ȼZ|&Ÿjܝe5X?2y^inpe-2YKI3@P J^#bN8 ©m!-%դi"s^c"e5q[.`@4MbrO֏I ]*Ș̕6gqDGQ\gxʿ/?&ZD=*)TDz[.I?/є=n1ԼԕAKl$@x+f*nK'2kOh:38^F:3jW6a’ijK T,0ADгiG*heοC{cʧ!yGwu!'@E.eM)f$)kQ/6jrA F۟#ҜpYT`3j&BI&V9Fe4#`ogeCGu?@(H\|) ;$|l7XG3]W;C2jO+B U '/q"J~Cʼn@!7:Љ']ah%'mD"y%W8;F7>3nchXjB G&Гc ;eѩwS \h(VAyٯkpC ϥ[v<[˳, u^^RAM@,wC$+T ݘ1N V3C VS-gHJpha%0QH-[YI$~>nI dX |v4j[(r"D̡kem+{jmN2N˫Nuˀ{k]L˒eee6Q|iDVG48Z->Ӣip500ԑ^dK7'TBSs*1WiKjuk>$=|b^CN%biyiJT@:&KY[*[I6-}:!F]J׽XJ(4+li|'HIl2 "K`h^Bہb*#5C>›-ٲk$3Li#{!W}H(fZR@.ӷזU4o e>x큝X QbaI D N^=8v3't;Rظ4RM^k>'WmǁIhOʽxu!Pַmh .p9McZg E㓵X@akI17ROm|@9w?wpF #S_)l8&EOz[dOSJ@{0HekzXz06)CkI=㩿 x\WxSrBĔii(xE"훏tU=!R(&edڑ1.Q ea@AVTjod יS4,!Eƥ٫eX/xs5e*fL;*WӾc;:3$ސx)n8 v=Ms8w`_ P0ړ⌏@ᩉ15:-jILTͰhtզU9$^P?Xp OqIPp=NNfU.J;_"#΅ayueCHpI1gaCҊEe%;Z({7GRy~{+88!i]Fluv-V;WFHPʴ[Ǯ !`c^7U4nJU,mk T-WZlu<`b4 2 }ŕߕIP9EYw V@?Fuۭ I[8g窸} Rqu9GJdIk/ b^`~ɠ0F1Lge-4mߕT*%:yԴ.̕)kEeICd\\۞Wj>,Sg8/u¢PQ|j#|fK'WQA~E*TUAPi6-_M JʰmÝۻPAGY; w7cNa5iJA;8۟f|_-ݝ1W BRR-=+Rn9VJ][2*VXb ;sN%8fE I <^kG!BkLSQ˒t$2t5)Z_[|*MM53]zv oV 4KdhEEQPj)EIj;Xx?qTJiB:7=+{\`Jf4`̮!$di * thX۝.,-eo@xYs ԀYY@CfN%FS8dҷVFbymox!Mp0Ug8A*aR2ok03i-o]U8ܮ8T\}ٵ~(eANXL>ړiA@otٌƊHIe[}`6еQ{ɬb]/(/L7$  ǜS>Kvj(xBU1S[0î"iģ;ÞiUPik$;RE#(kt;0;^JJII mohIո9wtYdk\NJ՛,SeTvEwZnGU2Op0u?XˈR(KĬΕ? :FSi q6NDL0A${8yRQ ,\Rʱr!bi~&>7@XE}$:bw3r6Y}"_ӟdoqvo^ەg{T-qƫuX.Q􉌟5^L#BFn)ZỸ&An}˯~ H`n2zLjPy0($/*×8H\*(㞛C$(X[xO: w 4mO􃍐]ܛ^eQqjvR i-@閍8F-6;37QB)⵭ư("Ib ?*4ˡ&a9@N)* JTx,sNRYrSu^Gh|aX/,=3Ozd] M*CdNRoZ6_M+Sromewa,$IHzr.;gIPVHH]eA{1(²~VW2}ATKvЙ3Ϟt]oL fB 'O? H2Em) ٵ=,썼]%*+J\HXҔFP!wЌO*jZeNV@=JTTʚPMT"#wۍWSB[N2.>+sS"G8UskTiejZTrҋrYBOzU^K m3u_Mn#xBʢU[[m6ln/~Uۅn?kV4?_*״@Gn+dϝbXF)1S8lMl9k@60 j7@FXꏕ_vuFôJŹ_1~^`\Oc|MLgQg"Ůaړ[AIt+RI##TN֋gLS6- C+Mڍ=˶ FU551uʮB~{2^ # ԦJE;8]L?JqJBLl~tGb5*^rż\:#}eZG\t SXJ4(af2SOHy(ZC\W1o OU4ҫwM"{6*'S+7üuJhnnc1aD}mW g6 œ 9p+ ۟A+Zgddw?%S.VjSguEDf!㹦j *myQrYDl<7x5<;,D7KP#C#[3섉*j儜%aòl *qZ댜g{{s;Ӷi]|#'Ԉ;.QqÉH/ARW:m9T l@06%mT44W)Wb7]R茨e=a#ogxyX[SD8$2UC%+{CZʘ+wkZ`6MM)ݴe\ gv㉉Qi$9ąL),]VqnvYMJPHiNvvR1:~YI :m|k[evGlxUW$ q7S"mt$yZ:[g,*K̹o]u'k}bǭk}pokt&<>.\Z[wNXhOڥ@9=#vnR /kE1ayuRIQh]'M1(b/n/'P<.$)u5GIѡ~f]=aIyز⢚2*4oq 'Ia.%P \K~|0K )İZin3XSD@NEhꭿxq6!2KB&WNlt Bpi'Y6)MU[0"eli7Lo ^ITC+ .)Xk6|?S.i/;\U opG1UD0|>h[$ XP2ږeLK%v[xjS-%*Kn-h;Q%LGIoD,2[:ܢ%mCkK;TMrxJMKrZ)tE;0;&aUdA@h0 ;>#s*Y/vD94R1"{>tK-YT2T ctN$eU7ͩAܛ+指[R.(J,9CKp΁F|< ܹP]Vsm@ju9VQ!*92?Y?BLFXNn<)FXF«k妱tYNLɰ alWM$RH H>*ӱ.6u*#e˔@^*Hs%WE~R*2S0/-~_^a8Mqk,ZGű\5keR~&M IH:xVhn8#z.Κtl tSRϞ\];x+E( ["tt0c+( XuIRMMFvk|%Vq`3)It#؋RRy):)#SR'А7(+tBqzl?N{"~.e?V˵ĚR'|{\T”\i)H gc+R0zb;dyUR-]4`H)[4X;HѲd`̿sb+,14kG8즲uE4ˊm@C!D}䦆~+"IU.ME)6!nCHR7<Resk|aL-S/~O ,"6Z Bi%!8vHVxՅ'B\Ip l51IJ0u)efhi\l,8rK IjkLʓI",.YXX R?J|95ql6SeohW>-|G4/(x$ܒc,mq˜ell5h VcZqm>"~jx#Hбצіo0!H$6oncǭrEAVo= :;BVSEC^?Jka`\J6"(H)q ǜwgb,IJ˴iW5$of](rRHB1=/+pA=7{j/>QQ:qU?OMg$_p籦‹lLL)BP"ב3nwH#(*ɴKF)3%Piq qJ;j@<7XHmݧ^GanQS- 2$hq YWhp;eIfND8kZq$ɚ^'`)C‰ !71Yfs("3my.8$XO㬉bM pW,\Y*s/*E|ɲ'.-2b.{U{Qm'U1LRTJSm^)+'@6;Ov c lxƾ!pfxT.>%k]8 03 D7`xoF7L!$;PuFXh9F Xta-yN=F6FHox)+S76o^Qpkoa>`M.eB &k]–0T c.)$k~ë?ࣾ1tS9k!?Sx?dJcn]R kS%K ˔5(&]䅤'33,aK0p^fc4jLULEQ=%5R\O:G;Nj ( ߴڒA؏8Ca9IIɐT'ZoǘnwKKո).U*ESM7UrN-'I 5@2֪;n~]q{NsHfxGY>싲-l gRtxzq^ @jQmd- 1[5/8?Ѷ-}*qfcNqZ SpmYV nZYrZ4KR#AvӐŅlIP44 @* %7IøCQ[P @L)bzDpjZď^WYq&4@-<!vYF(; ⷞE>VyҴ'"/@)/bD8Vplꎠ{GKmܩ*kEi^Z\%@J́F5<08Sn7[PrҨSyq9CJ7PrE%̪nEo+nԱT* QKn,)ĦYBīQ~$QKx(ntM h`SS3K! ԺWucumL/3(u@Q) }"b3&l8TЙµE^*#bI.۔eùXhN/`L9P܏炍=#+#1o9ݰ>jb%wa_ijzaM2HlZ`'R{J]bFZ.^9b|I::]Fɹcv8rMЕjJ>|J*8'.M6*84*ڌz54TR $Y=!PI^׾5 ͹}C؞p IŶ_;;p DX-KX*'<`a8l lEEve_ h) )4r$E7ڪ'oJwI-uRLW)So$;8? ;GzܥHБ8yPȔqԥIPulYnuױ[rkYM= ɭB̢LěWS$bAQuK#po^$G75sfa+kZ$o;7Vej=.Ѳ}`8z6 "E$*m'R~hHBNF3-gw_a.2f i͎>6B j5&Q̍Q%:(.uZ!J=, 4DܒKSj7x,)AqMxZp6L7T1.}q[Ky@!-I"Tᑔ+V[Pu?EJ[y6Up7Jޫ6_'YBbOzB '"!mSQlWT6ԙ`VZVG%TQal=F3d6Dō"~2*( BOnN"Cj@gfܐi7+Jq)t9I]7UbFWTY 2 3'.)'en=U=}\ǒߊ+fiml!W]a-FQ{1Ma$.Ҡ)Ƭ}#-aQ#:&,t󆧃`F\Zl9)<kݵy4@ J/Im. *ihF^![Gp%ZKE#`ީԷ|665+.Ŧɽx"D˩u#6(:h/ujE!X.uyqXØqu0-))>RaQƓ75q6{gJH[A:NOĔzi)g%K2C䘦0ʑ?5]pHx[FjYn#ã\Wgf1 nU#4^&^VZ|JJ榓:XZJ5RtMO +0-"',b\ cICS)W*N,%)C4 "PLz]vTv8N*zeiT?sI~|Wmq'Xt䤝 !CTzNI7+$PH B07rXNc4Нyy{M]< i%{,:@3E4e_ЬFA)򍅌s(l-}KQ;zF*iV]7k¼&0]d6d}!JVnZx/hQ* )&OUuHci^Rvs.q2m/C@A4-Pj8ʏvVT\ANN Ceu7Jv)J)-n9S}SU. qlW0U!e_ᒕM@hxڵM5^4)63lb܉8‹Cį. ӥM8o"B_K9Ëޯz k:W&SOGzq#2GPjʐ-1Nn@)lEUFR\_^^IO*sriQ)#Th%,W Aty?tt<ϥ/5Tw?o0C^zà K6ғ.RxSOm:U=}mxspqj7q+ fg 9R.5s{=&d*RtK]MG2g z=-D:\vc!$gmjԎ0ِvc Q~9n9G[Hh7*Im}OzvM KHo=6w,Ҕ4.,RUp-a0.N}k&F ] z` ؆b]kdfFE;A#{8O`9ZIe Z6ϥFW#Te\L#F=P %/@U~^i3t܌oK$ېH#1f=uJ$f$ޜ1O5UKp'1^oYf=)P!Ev:\/T@Ol؋IlWtT)/2TPo x8ÄjSUOJ)%+:Gܒ;3v8fDL]$8;;m8=/Y^}INr6F˵-z*t[dH]٪wlbʘKn6;pS\hJӬh,J=:m?=wS?e}ATf761 Ap}bҞpwח%_w+!בcמ` 7R?Ma oO\.aiZFd̷,94BƼ9)$}) 8\E _*.--ԤRse as.$Ŀ\Y*̢|@^N&i?5Sb=OEbv[TFe qEO@>9P@MjZ؆Cl՘^MoޢX rYѨ;֮L-6p[Pb>sZE:0 R,<;_2QlX(X9@׼k[FRu"x Hr9 tV9)!WOԓew'(P3psʖ@#i\\'j,ĬOʥYt= R3w^)CMm9,#:rEzp)5fnRRa "ThΞ%az/Bds%{O_ҦRӘ6X@CJl[ ~rL;gRy''-G]Vt*&SxMӋ䰭' j߭)% {٫8k%T.9mOR GangZ㆒#O푿!w&XfܴBl˜?>S &n,tX ,^^Py>V fAM`H'vq5Q3t.ozü0 K#*Wݩ$)*{לN8eK.?JOzfRO,=3&2dN*}FLb 0ڔP:+kkQCt̹>Rm9v EP?X;]NjuBPڒ w$X pJR0u5K*{mբ7 _ƖZJCmm| f,. kpRȷWEpK>T,mQ#}#gd4zА[N]k컋0=hHT yGE`%Bl!Qv-kM>*gsrȯ4+5QT N~=7OqRMӓpOv.齈$!J9JNL#u-e:XXy٥t&3ت\A*$! !B9Rs_b(!iҡO>)f]4 wHQ!Cuߴlsʵ17Y &/hQSx yZka{Q/vq]Rt<4w>&7PMɲzjڨeT'Ȱ,Ix J4Z Ei{B%H}u!_m;+U}ʻԓ@QHǘYVBω+%_;JIIJ*}Vlΐ?!A g [:,dXRNM e^$`((o0%uL*]3m*\#{Doo65 N& F?S('NAq g$-6Mi B%jeD[SW\qP U $=cXȃmL!u[K ["r\"]!"%zNUD8MX!YsӋ :v6\%SeeYjs\aaBpS؝iBx83Sx ^n1*tlbW50Σu^!dx/0[RTDZrڍBZ^"Gb[]=vZ_ a?.+Q2}ͷ(u4]|s Jkt$b~e9$_uAv2b٧ _Jm#F|*ʰӎ\ ~P~`ncI/):ԜBrzYe@}H>AGF>J@xqVD۔-ڽUye$mu`!2QplIj_0c:ߥqCx._oT )7*OLK_p%\bmǎռDNኵf&bm$Ge`.\2~=ٻmㇸ1^xiڿ8຦%eP $,ٯqxG+d1IXȼTnˡe`[b/%J#6px@ߋq׍JaKRCLNFK x:^q2ݴ>F/^9rBI7-6q%SK4g\ qd![Щԩ =2PRrQkb|DzftwISd=ty N´:%:S*/0׉"v6 kFϱWaR 52}G !NC![TK "cW)ߐ#( HH*x6YyS#wF\.o Nv #6[-\ 9*w\@:XS@ ahtrV2m!Z$ aRm{ߞѠzqIInQOm e,(u(Qgl$Kh ' TQ%H۬ 1״<\|;`̣XT27ߥg?xUFָX,{n|iT̆T.^[6Ge Rn<. |ڀ1Cj4TnHQ 6v>y=Tx Rȗ]J:W ª,1*&Qx'?=mor&Zbn;!`Jn&J'd&e;ͯX;8trv hFr8ɧKiDW )'x Ff΄|H`tmx*b͠FbZyBГam砆}ÁЄث'cdNMF^][1AהPCܫ,2MvV&VI7nHevM;}L^auBM_-3&pL%*9 0uȬ+.ݨfRKZ:C\e%{ѼZ\,&z9{&Y˛MJl,ve֬Gå<jJ$遼CZ.yN2ٖ+ʤV"1ĦbnX2{yG[^s O6 _ͳP R4JEO Cs}W^q֔MH6LkJve6yCZD w{k%6ˬm]͂) "ď3o Ԟu(wܓm -T KSU(_/T&[iګ 'Shp_;T7"0:?bq'Wu;˱7YIOΤbt n]C0&){6vA1ZpM˪bM7 }7'G]R{ =dS%)FH$jV?G+2%ěyXx60R'AJEDpi%T]h馸Zʨ>| ,y0+9m iiOx4ЛƘ.?2I?#! Y@FH#C{9LHRС~sMA]&y칐eGṡ[z9i3'0O.kzINdД Y*&QlJI0$:. Jy\DxXrD&LbMBkw9~JPOlDd5(%KapS҂I> vjrmYTc;n}#|Yz"9Yt]m$zyA擮ha/p2hNa*MB|Cһ.H;љe$L6V"*lvZsuymq ˪xxv;[ Ld6pP[͒c+mI˦А6EoSdkU䕇L d+Mbo=Jh3.$DU]h&EplEߺ(*8X.G->/!V|G%j2 *o$ƣ"Xt#^^QғRlNdj TMƕ4f%[=ɷc]ёMzqؽ;^V')P}-JU'&bNihZNBNI(Ѝ¼#R<-rJu }SBe_Ь ,J|M2q+O>9m3}<1jXnJF` m>WQU5ybidvW?s*Kl%v9EIZ?&T$h0 gљ!?'ƓK8!L*& #"/K*龶hER`-GeP'n0zq t'sM.]!I; 6=cI [j ҥ$ME:.&[_v)J66PE1\BJR.O)ih`w 8n cg̺B 1N9cRqu*]UBn͸6\R֖oV CږaJv@+@MVO"ފ혓%ﮩwFeXvc_hdʺT ;BWv@*%TꜭFQ2!bTtwlbEGVӕ }$a_L <@1`v.*hJ)Jjh tw8`4G̭\RJϭ~KrN:WsyY4=;-2ґEP& 8RCG-WGlp7i_CEYkJH]*҂uW U;mbT/.;ܖ9]G.#Ar@<mu\k(Zp 'MuT{Ԯ:.֥J. <+{kyRr|[\hj4J~Z[ ?sYLIiPW{u+d B<骜+0.=9*̗ QaEϭO8Ŏ Rrb&T5mzr i&)i P4G/~;س[0K皷O~[A1BJ.N5KZ+).IUа,|JxI!M+RI6#OCQ xk=2TBґ,ZIs(Xq knqKS#Qohi"W%_ U :c]/+_aT'FQb‚|v!IH.mY}erM'BmwWy4N+|0XiW&]ZbbY/S AT&VhZlNܮHÒm"PxU8e@uM8_D˹)/@u0]]"USluP@QA"'Shc=eB|Go8eXR)Yz0R!V9 VnNa&6kL>P|r`UXx5#GyEFX}O8@ DT`Ɠ=/r6ң.k^|I?3+ݠJlZ-v"8Fin)*M4U{I|N;2L 1d%i9Ί:^(SbwRJ Bk$k;KTk%|[m2[tF/UElea}psg~7wt#ʹM ۡܔ![I[S|mK~6;8YoL*G5Z\ime>&Uw>d DmUratm5P< d㞥QSvxT[5$%>Hc7mu6Q`sD~V݅ ښ7K~v5u Ds^2sy+N%[RoΆQ0'Xr>5'H皥 ʯIwna@ RTi er6". ~60`*6 `tj U\@wA^b!Kd#AI}qTk\;< o1dK$w❵:(}Giyj|',jNVPIK(H% <[>)NTŶULJHF>:wb oHְ5%willrpW*uILSqz}_JZ)MMGML*yBٖoXmU%\6j᫦I%Fk~HsKTY=C*ʾirhXC d|$Ϭ8KMXhT6.c ,%h7Se.Kʥ[fٖZRJOPahLR܌Z&ZU{jMNH(%ܾ qU[ QEc^ *`^bpʠƑ]0j; BOyFzFME>;M**Y6;B){@ۥ1@-ܨ@/? F)SiveܥॴH?xH8mRg 1n9-'Ǚ_ ))JN􂖐 >5,a qm`#tc꒡ix+xqkxj A%SZo#huN2JV}BK㖷Y^-,9lLmHrb kapE3x%^7r\_|:$0G"BCdplw<*ɲThP7򁽕s'&o d'A(R>&p5 fmc݁-qx[Tx̺sUX${!ڇɉ~*%=fF/NaU-PsSim K &^[ !×%+):VH>cp$5:\=ug:ѢYwKYKNA y)r,'\,[Xf`yn P%윻/e@FKí LxF\RM=(.{U{̡IT+e]ztBuj7*3 yuhY[M%?5ϼ DCjzcj;sm^#Mv'?4YUAXx!6$b_RFl`bDIS~}gp]4o%u¼~>0hLQ񜚊PVqfy34G;(10-~!ݪx\qBy6npzD90;3q]}}^R}Adr ]#c1TXu 4ຂǡBߧK7O?>RX%={{JJ"ɦ3`Dm+ȗA*/lㄥf.̽gAqEs$+W*-AI.>Sv95tdž δBUŷÃllPrOukJ@tk@V-:@ 6kM )+l:AL{:RTG(J2yo Z-{|[7ө.̤CR֔ z֖Zm@ 8HQ]Ţ^ŒҮߕI6{7Э o<4M.p \][q:BthPAuC-ah^BBWk )\Nmrѕ-I1v]h{;)n `%+:nuPR~B0hPSpR>5!B&*4#\KjHHPNuXaDsq&BgjII9')[NJ젘B&A}RqPJ6 vZ7#2aѴIDKZ,+bZ{Fa7)G@-]$8rӆGSC+>`\U+zSUY{d!ؓ07%$^5BSJJ~oZShR`.?ڜٛm ?)q`[;$RabֿURbi\-mxWӊp|@u`p>,YƼ"=Y!(IFb52^o*6X~۫,H$~1mŅ|bfmJC~!-Jgl`/jaP{>asÞ%2|UM /ZQsa:9Ve(kMaIP;qRL}-~R1Vg7>)1Xr\N3')9.CpeX^R m),C),ohe:7C%Ýu_QW}Ql!SmbmED%NRu 2;If;*ے[Kw@YS֯(G( 2v[w4~?͜ޓ')):kWT[AJɭ39\RYJ ->(%Ku.2Jb )PJ1u7wZjׅ鼘NC촦Et! jSoM(qp-ག V):Y HIk/IJ)}N↹R~'Sb;h>$zYFc]//<ƴg()W+=&lyʫV3RX JBTwC#9r«դJ#OHxX30XQԓkpK( .Z!V*u,&`>7A(4HB>~8xzXEA)pDXiY2̮7䢰T`$*ĕnQ L*6SYLڳGB8!{*z%oCEGeLLgl3 Z| 7VbahP)*]@g}Sɰ$1 L\?)4iW1suNYURA:_?;K2B{Jn>Mx_vÍz[l!k)?ug,DN 38ӹq YEmM,%EпrwL)#brxN|<>WYf%>%ځcuc qul.e&@MeǑ&Mqaͤtlu R'še8ٟ&p?H*KδSPqoXvhNK]bNq.-29@܂OzV%IԳe:+<Z,@(>=v ฻Q9"k9&mo3D[Bt\ZlǨ6085I'ZJ7ܧs )7[ZtҢC̓0C|̻.<_K h ۣU 8y^C:E Jʕځqƅpw:) .t` |0/Sd] ;o-\Rm}xFP2 A hI}!Z u\f 'q&,2sh˸{^LL4 *}"Ü9"%4eot蹄[ am/m@@\ܦo}湶JH#R'x\hr ƖQꮓc ]V87C+_8LmÀ0Z۬&v0cn'AdX{r4""ڪL{U'q>HFևE\AaY18<Ӛ~XxWx2XsJ**RrHhV9j0j0$oKNUc¾$Juٜ$Q*rSLyQ9Mku"XK|Uw%MOqPSMIZ/k"fk1ٍ_u1 ߶ T">%r'MS?22T|jtX+ Ze[C^ֆ:^Fk\Om*^aR54UIVXW\t5|eC_M|;eߨM&c |zNiבÉ`{QAP?Gx\YS MKw/4R{#`z\c6awSxmS̳r% 61M.]4`,MVCtJ M_` 71Jہ?U5OEp6Kɚ47b1.]aID# :d;ǧXn=1fT B,5!^Ɗ({)nݯ_5:Z~]µ+̵f3.~dIQR5͗aE$kv;,呤f he76( :ZX6R@ @NOxaozY7.'[wmC %IWBɹJ5)*7Aj?qJ-&㸘R1 *V6OHBָX|go<>u qm y-Ǘv[Mb%i p)]c|YCL|=),Ī-UGw Q_%E?]Du@KmMWS{Q9G^$A)Wp#dJIxit侞%SKq Hx[W}̮6d\xN~>< 5-T2)@]/(s1H"a”m9lxVbcw6yo:[ b&444ÛA?O[BwA3Ԥm ,ef+eY#`BsZt 8 6 fr%D.]7RuSJHaqQГ vv hsA1'e%W[ Q+Y'C):s3Se8onpodv> < ɀMVS؍ 9 "ZE6 BGH&`JR-,†R.c{*Cvlt.Q߱<(᯳Z/6⁳2>{A[53)$>v񴫒8#)RqQwT VXsu]NҪo $rE 3t*tTq3%YʯanCR'G[oME@I.mtaV%SLei@+M n ߜEFIM#l7V #}U@s%-<Ç*eIN&iZ{NB~&X.։!(p|^qY01mR_THR 5_/HO;c (h$}aݙ Gי}:Ė6QRMP` jfxV); lG69nys'_,-B>v\I6$Y,B!~QIRMaHymJ΅ Y 6yRVou}*̼PrQ3wk{ %n6$3m?HjkjeiHpXhw*dZV5̱UW 36݌U,MTf S~3cl'/QeN3JReU!$R}ݭxlSDK,3"zѦ1Z~]+^HMX% k|iŠqrB "8>69oZq'iޜ{o` +3?R@}LIĮA*VCԝ>NYտv SmKmG">r[{lrnJC :ڐ|M)D$k:ᜓQe ]* ks$LJm $|rSWEuO9 @{ r{dÚ;$&L(ZY}a5*iS]܈%D\~1õP:7~Iei7H9MA%(j6*n"ukvMMй zIU l>`ͯ%VfP(I)?fY9LXUV@tCkY؛ SG\[S'-r/x#v=JfhTU`Gtm J2amͬ1ŵ)麋ș|$fUVQHD2ۑ M%%c.Z{j-dP~^*a؃^C<>es""۝5n,}r"e(EMH<êZux {yطMXZ{xVõʤNmH>m-'Pc {ü '^XJBqb)dSs$2xYV%%Ui*Y$D] u+sE(#[nU1*N 'ITfV\! .O j Vbg|D]IM huZ Kh$<όPHGQ s_}iBR 3LCE@7jǬh&9m8֐Apr 3.3n+qt0Mx\&ƕ RneVop,G×2x5C-#WwG ~-JoJ#.+gŤ%[1K)ow)'¦)-^}L2xaK So'Vev⦜ERUxٯsO8(UƽS~*(wݺRNd#X#Lku+΁ps TE<3i3^P?HBo}(Ϫ)Q7Hб6?HJ0MJ)^+DUR uĤG..T#CϬ(amk$y@\'!BqQ#K )>K-$c矏tY'tV&t bh #B?]%%-CM?k # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import gettext import locale import logging import os import sys from optparse import OptionParser # i18n from lazygal import INSTALL_MODE, INSTALL_PREFIX if INSTALL_MODE == 'source': LOCALES_PATH = os.path.normpath(os.path.join(os.path.dirname(__file__), 'build', 'mo')) elif INSTALL_MODE == 'installed': LOCALES_PATH = os.path.join(INSTALL_PREFIX, 'share', 'locale') gettext.install('lazygal', LOCALES_PATH, unicode=1) locale.setlocale(locale.LC_ALL, '') import lazygal from lazygal.generators import Album import lazygal.config import lazygal.eyecandy import lazygal.log usage = _("usage: %prog [options] albumdir") parser = OptionParser(usage=usage) # The help option must be changed to comply with i18n. parser.get_option('-h').help = _("Show this help message and exit.") parser.add_option("", "--quiet", action="store_true", dest="quiet", help=_("Don't output anything except for errors.")) parser.add_option("", "--debug", action="store_true", dest="debug", help=_("Output everything that lazygal is doing.")) parser.add_option("-o", "--output-directory", action="store", type="string", dest="dest_dir", help=_("Directory where web pages, slides and thumbs will be written (default is current directory).")) parser.add_option("-t", "--theme", action="store", type="string", dest="theme", help=_("Theme name (looked up in theme directory) or theme full path.")) parser.add_option("", "--default-style", action="store", type="string", dest="default_style", help=_("Default style to apply to the theme.")) parser.add_option("", "--template-vars", action="store", type="string", dest="tpl_vars", help=_("Common variables to load all templates with.")) parser.add_option("-f", "--force-gen-pages", action="store_true", dest="force_gen_pages", help=_("Force rebuild of all pages.")) parser.add_option("", "--clean-destination", action="store_true", dest="clean_destination", help=_("Clean destination directory of files that should not be there.")) parser.add_option("-v", "--version", action="store_true", dest="show_version", help=_("Display program version.")) parser.add_option("", "--check-all-dirs", action="store_true", dest="check_all_dirs", help=_("Exhaustively go through all directories regardless of source modification time.")) parser.add_option("", "--dir-flattening-depth", action="store", type="int", dest="dir_flattening_depth", help=_("Level below which the directory tree is flattened. Default is 'No' which disables this feature.")) parser.add_option("-s", "--image-size", action="store", type="string", dest="image_size", help=_("Size of images, define as =SIZE,..., eg. small=800x600,medium=1024x768. The special value 0x0 uses original size. See manual page for SIZE syntax.")) parser.add_option("-T", "--thumbnail-size", action="store", type="string", dest="thumbnail_size", help=_("Size of thumbnails, define as SIZE, eg. 150x113. See manual page for SIZE syntax.")) parser.add_option("-q", "--quality", action="store", type="int", dest="quality", help=_("Quality of generated JPEG images (default is 85).")) parser.add_option("-O", "--original", action="store_true", dest="original", help=_("Include original photos in output.")) parser.add_option("", "--orig-base", action="store", type="string", dest="orig_base", help=_("Do not copy original photos in output directory, instead link them using submitted relative path as base.")) parser.add_option("", "--orig-symlink", action="store_true", dest="orig_symlink", help=_("Do not copy original photos in output directory, instead create symlinks to their original locations.")) parser.add_option("", "--puburl", action="store", type="string", dest="puburl", help=_("Publication URL (only useful for feed generation).")) parser.add_option("-m", "--generate-metadata", action="store_true", dest="metadata", help=_("Generate metadata description files where they don't exist instead of generating the web gallery.")) parser.add_option("-n", "--thumbs-per-page", action="store", type="int", dest="thumbs_per_page", help=_("Maximum number of thumbs per index page. This enables index pagination (0 is unlimited).")) parser.add_option("-z", "--make-dir-zip", action="store_true", dest="dirzip", help=_("Make a zip archive of original pictures for each directory.")) parser.add_option("", "--webalbum-pic-bg", action="store", type="string", dest="webalbumpic_bg", help=_("Webalbum picture background color. Default is transparent, and implies the PNG format. Any other value, e.g. red, white, blue, uses JPEG.")) parser.add_option("", "--webalbum-pic-type", action="store", type="choice", choices=lazygal.eyecandy.WEBALBUMPIC_TYPES.keys(), dest="webalbumpic_type", help=_("Webalbum picture type. Default is messy.")) parser.add_option("", "--pic-sort-by", action="store", metavar=_('ORDER'), dest="pic_sort_by", help=_("Sort order for images in a folder: filename, mtime, or exif. Add ':reverse' to reverse the chosen order.")) parser.add_option("", "--subgal-sort-by", action="store", metavar=_('ORDER'), dest="subgal_sort_by", help=_("Sort order for sub galleries in a folder: dirname, exif or mtime. Add ':reverse' to reverse the chosen order.")) parser.add_option("", "--filter-by-tag", type="string", action="append", metavar=_('TAG'), dest="filter_by_tag", help=_("Only include in the gallery pics whose IPTC keywords match the supplied filter(s).")) parser.add_option("", "--keep-gps-data", action="store_true", dest="keep_gps", help=_("Do not remove GPS location tags from EXIF metadata. Mostly useful with holiday photos.")) (options, args) = parser.parse_args() if options.show_version: print _('lazygal version %s') % lazygal.__version__ sys.exit(0) if len(args) != 1: parser.print_help() sys.exit(_("Bad command line.")) source_dir = args[0].decode(sys.getfilesystemencoding()) if not os.path.isdir(source_dir): print _("Directory %s does not exist.") % source_dir sys.exit(1) cmdline_config = lazygal.config.BetterConfigParser() for section in lazygal.config.DEFAULT_CONFIG.sections(): cmdline_config.add_section(section) if options.quiet: cmdline_config.set('runtime', 'quiet', 'Yes') if options.debug: cmdline_config.set('runtime', 'debug', 'Yes') if options.check_all_dirs: cmdline_config.set('runtime', 'check-all-dirs', 'Yes') if options.dest_dir is not None: cmdline_config.set('global', 'output-directory', options.dest_dir.decode(sys.getfilesystemencoding())) if options.force_gen_pages: cmdline_config.set('global', 'force-gen-pages', 'Yes') if options.clean_destination: cmdline_config.set('global', 'clean-destination', 'Yes') if options.dir_flattening_depth is not None: cmdline_config.set('global', 'dir-flattening-depth', options.dir_flattening_depth) if options.puburl is not None: cmdline_config.set('global', 'puburl', options.puburl) if options.theme is not None: cmdline_config.set('global', 'theme', options.theme) if options.default_style is not None: cmdline_config.set('webgal', 'default-style', options.default_style) if options.webalbumpic_bg is not None: cmdline_config.set('webgal', 'webalbumpic-bg', options.webalbumpic_bg) if options.webalbumpic_type is not None: cmdline_config.set('webgal', 'webalbumpic-type', options.webalbumpic_type) if options.image_size is not None: cmdline_config.set('webgal', 'image-size', options.image_size) if options.thumbnail_size is not None: cmdline_config.set('webgal', 'thumbnail-size', options.thumbnail_size) if options.thumbs_per_page is not None: cmdline_config.set('webgal', 'thumbs-per-page', options.thumbs_per_page) if options.pic_sort_by is not None: cmdline_config.set('webgal', 'sort-medias', options.pic_sort_by) if options.subgal_sort_by is not None: cmdline_config.set('webgal', 'sort-subgals', options.subgal_sort_by) if options.filter_by_tag is not None: cmdline_config.set('webgal', 'filter-by-tag', options.filter_by_tag) if options.original: cmdline_config.set('webgal', 'original', 'Yes') if options.orig_base is not None: cmdline_config.set('webgal', 'original-baseurl', options.orig_base) if options.orig_symlink: try: os.symlink except AttributeError: print _("Option --orig-symlink is not available on this platform.") sys.exit(1) else: cmdline_config.set('webgal', 'original-symlink', 'Yes') if options.dirzip: cmdline_config.set('webgal', 'dirzip', 'Yes') if options.quality is not None: cmdline_config.set('webgal', 'jpeg-quality', options.quality) if options.keep_gps: cmdline_config.set('webgal', 'keep-gps', 'Yes') if options.tpl_vars is not None: cmdline_config.add_section('template-vars') tpl_vars_defs = options.tpl_vars.split(',') for single_def in tpl_vars_defs: name, value = single_def.split('=') cmdline_config.set('template-vars', name, value.decode(sys.stdin.encoding)) logger = logging.getLogger() logger.setLevel(logging.INFO) output_log = lazygal.log.ProgressConsoleHandler() output_log.setFormatter(logging.Formatter('%(message)s')) logger.addHandler(output_log) try: album = Album(source_dir, cmdline_config) except ValueError, e: print unicode(e) sys.exit(1) else: if sys.stdout.isatty(): progress = lazygal.generators.AlbumGenProgress(\ len(album.stats()['bydir'].keys()), album.stats()['total']) def update_progress(): output_log.update_progress(unicode(progress)) progress.updated = update_progress progress.updated() else: progress = None if options.metadata: album.generate_default_metadata() else: try: album.generate(progress=progress) except KeyboardInterrupt: print >> sys.stderr, _("Interrupted.") sys.exit(1) except ValueError, e: print unicode(e) sys.exit(1) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/TODO0000644000175000017500000000261612301071671013603 0ustar niolniol00000000000000Ideas to improve Lazygal (in no particular order) * better i18n in templates : better string extractor and use of Translator to make syntax nicer. Work to be done in lazygal/tpl.py and mainly in devscripts/update-po . See http://genshi.edgewall.org/wiki/Documentation/i18n.html and http://genshi.edgewall.org/wiki/ApiDocs/genshi.filters.i18n * SMP support, perhaps using parallel python (http://www.parallelpython.com/) though not sure about its ability to transmit MakeObject.build. Perhaps os.fork() would be a better alternative. Work to be done mostly in lazygal/make.py * Add some styles to the default theme. * Integrate Gallerific * Archive (zip) link on directory index. * --exclude to exclude directories from processing. * Document input formats. * xinclude for XHTML * Some sort of plugin architecture for generate pic post processing at least (with an example for watermarking pics). * Picture links in breadcrumbs. * Alternate theme with one HTML page per dir and JS to change pics. * Detect config changes since last run et regenerate files (html, thumbs, etc.) according to configuration changes. * Add an option to paginate by sub-galleries number per page. * Copy instead of processing through PIL pics which are not resized (because of XxY> for instance). * Build picture and gallery map using EXIF GPS data. * Add a way to disable video transcoding. lazygal-0.8.2/devscripts/0000755000175000017500000000000012301073200015262 5ustar niolniol00000000000000lazygal-0.8.2/devscripts/update-changelog0000755000175000017500000000121512301071671020430 0ustar niolniol00000000000000#!/bin/bash # Small script to easily generate a ChangeLog from hg log. project=Lazygal HG=$(which hg) AWK=$(which awk) repo=$(dirname $0)/.. $HG log --template '{tags}|{date|shortdate}|{desc|firstline}\n' -R $repo |\ $AWK -F'|'\ '/^\|.*\|Added tag [0-9\_]+ for changeset / { next }\ /^\|/ { print " * " $3 }\ /^tip\|/ { print "\nLazygal ##DEVELOPMENT VERSION## (" $2 ")";\ print " * " $3\ } /^[0-9]/ { gsub(/\_/,".",$1);\ print "\nLazygal " $1 " (" $2 ")";\ print " * " $3\ }'\ > $repo/ChangeLog-full lazygal-0.8.2/devscripts/update-po0000755000175000017500000000464312301072413017122 0ustar niolniol00000000000000#!/usr/bin/env python # Lazygal, a static web gallery generator. # Copyright (C) 2007-2010 Mickael Royer # Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import glob,os from distutils.dep_util import newer from distutils.spawn import spawn def update_translation(po_dir, po_package, po): # Update runtime translations os.chdir(po_dir) spawn(["intltool-update", "--dist", "--gettext-package", po_package, os.path.basename(po[:-3])]) def update_template(po_dir, po_package): os.chdir(po_dir) # We force here the python language for gettext strings extraction because # xgettext (which is called by intltool-update) reverts to C for # templates, as they do not have a .py extension, and this does not work # and translatable strings are not extracted. os.environ['XGETTEXT_ARGS'] = "-L Python" spawn(["intltool-update", "--pot", "--gettext-package", po_package]) def update_po(): po_package = "lazygal" po_dir = os.path.abspath("locale") pot_file = os.path.join(po_dir, po_package + ".pot") po_files = glob.glob(os.path.join(po_dir, "*.po")) infilename = os.path.join(po_dir, "POTFILES.in") infiles = file(infilename).read().splitlines() oldpath = os.getcwd() need_tpl_update = False if newer(infilename, pot_file): need_tpl_update = True else: for filename in infiles: if newer(filename, pot_file): need_tpl_update = True if need_tpl_update: update_template(po_dir, po_package) for po in po_files: update_translation(po_dir, po_package, po) os.chdir(oldpath) if __name__ == "__main__": update_po() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/devscripts/copy-metadata0000755000175000017500000000337312301071671017760 0ustar niolniol00000000000000#!/usr/bin/env python # Lazygal, a static web gallery generator. # Copyright (C) 2011 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import sys import os sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from gi.repository import GExiv2 def copy_metadata(src, dst): source_image = GExiv2.Metadata(src) dest_image = GExiv2.Metadata(dst) # Save dimensions if 'Exif.Photo.PixelXDimension' in dest_image \ and 'Exif.Photo.PixelYDimension' in dest_image: dst_width = dest_image["Exif.Photo.PixelXDimension"] dst_height = dest_image["Exif.Photo.PixelYDimension"] has_dims = True else: has_dims = False for tag in source_image.get_exif_tags(): dest_image[tag] = source_image[tag] if has_dims: # set EXIF image size info to resized size dest_image["Exif.Photo.PixelXDimension"] = dst_width dest_image["Exif.Photo.PixelYDimension"] = dst_height dest_image.save_file() if __name__ == "__main__": copy_metadata(sys.argv[1], sys.argv[2]) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal.ent0000644000175000017500000000053412301071671015263 0ustar niolniol00000000000000 lazygal 1 " > lazygal.conf 5 " > lazygal-0.8.2/README.md0000644000175000017500000001314112301071671014365 0ustar niolniol00000000000000# Lazygal ## About `lazygal` is another static web gallery generator written in [Python][1]. It can be summed up by the following features : * Command line based (thus scriptable). * Handles album updates : - Lazy : do not build what's already there. - Tells you what should not be in your generated directories (and delete it if you want to). * Presents all your pictures and videos and associated data: - Recursive : generates subgalleries. Follows symlinks for flexibility. - Sort pictures in the same directory by EXIF date if available. More sorting options available. - Auto rotates pictures if they contain sensor info. - Reads and present selected image metadata. - Copies image metadata in reduced pictures. * Makes browsing sharing pictures easy : - Can generate multiple sizes to browse pictures. - Breadcrumbs on every page. - RSS feed generation for your album updates. - Optional generation of ZIP archives of original pictures. - Output internationalization. - Optional breaking of big galleries (directories) on multiple pages. - HTML5 video pages for videos * Make customization easy : - Theming. - XHTML and CSS compliance for provided themes. - Multiple options for album and picture metadata (picture metadata, flat files). - Add template variables from the command line or from a configuration file. - Per-directory configuration. - Javascript or fully static navigation. * Does not change your original pictures directories (the source argument). [1]: http://python.org ## Example demos * [Photos from Japan](http://photos.cihar.com/2007-japan/) * [Michal Čihař Photography](http://photos.cihar.com/gallery/) ## Requirements `lazygal` requires : * [Python][1] >= 2.6. * [Python imaging library (PIL)][4] >= 1.1.6. * [GExiv2][5] >= 0.5 (to have GObject introspection) which provides Python binding to [exiv2][6], a library to access image metadata. * [Genshi][7] >= 0.5, a *Python toolkit for generation of output for the web*. * [Python GStreamer][23] and associated plugins for video transcoding. Building a `lazygal` installation requires : * `msgfmt` for translations. `intltool-update` and `xgettext` are also needed to update translation files. All are included in the GNU `gettext` package. * `xsltproc` to build manpages from docbook sources. It is included in the [libxslt package][8]. [4]: http://www.pythonware.com/products/pil/ [5]: http://redmine.yorba.org/projects/gexiv2/wiki [6]: http://exiv2.org/ [7]: http://genshi.edgewall.org/ [23]: http://gstreamer.freedesktop.org/modules/gst-python.html [8]: http://xmlsoft.org/XSLT/xsltproc2.html ## Usage Usage is straightforward : $ cd /var/www/album $ lazygal ~/pics $ More information can be found on the manual pages [lazygal(1)][30] and [lazygal.conf(5)][31]. If you want to force `lazygal` into checking a directory's contents, simply `touch` the source directory to modify its modification time : $ touch album_source/gallery_to_check [30]: http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal/lazygal.1.html [31]: http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal/lazygal.conf.5.html ## Download & Changelog A [user friendly changelog for lazygal][32] exists. [32]: http://sousmonlit.dyndns.org/~niol/repositories/lazygal/raw-file/tip/ChangeLog The latest version is [Lazygal 0.8][10]. [10]: http://sousmonlit.dyndns.org/~niol/reposnapshots/lazygal-0.8.tar.gz (full log of changes may be browsed in [Lazygal's repository browser][16]) [16]: http://sousmonlit.dyndns.org/~niol/repositories/lazygal Lazygal is part of [Debian][17] (and thus [Ubuntu][18] universe), which should make it one `aptitude install` away if you use one of those. [17]: http://debian.org [18]: http://ubuntu.com ## Contributing ### Code Code may be downloaded using [Mercurial][19] : $ hg clone http://sousmonlit.dyndns.org/~niol/repositories/lazygal/ It is browsable online in [Lazygal's repository browser][16], and this page also provides an up to date snapshot of the development source tree. `lazygal` may be used directly in the source repository, by calling the `lazygal.py` script. Building simply prepares the translations and the `man` pages. Updating a source checkout of the `lazygal` repository is done using `hg pull -u` in the source directory. Patches are very welcome. [19]: http://www.selenic.com/mercurial/ ### Translations To start a new translation, for example `cs_CZ`, you can proceed as follows. The first script requires `intltool-update` and `xgettext` from the GNU `gettext` package. $ devscripts/update-po $ cp locale/lazygal.pot locale/cz_CZ.po $ $EDITOR locale/cz_CZ.po (do not bother committing or sending in changes to `lazygal.pot`, they contain a lot of noise because of changes in line numbers) Another side-note : in templates, translatable strings are declared in a character noisy way (I hope to fix this one day). As an example :

Parent

becomes

${_('Parent')}

## Bugs & feature requests This project has too few users/contributors to justify the use of a dedicated bug tracking application. For now, bug reports and feature requests may go in : * by e-mail, directly to (please put `lazygal` somewhere in the subject), * through the [Debian Bug Tracking System][22] to which I think I subscribed, * through the [Lazygal's Bitbucket bug tracker][24]. [22]: http://bugs.debian.org/lazygal [24]: https://bitbucket.org/niol/lazygal/issues?status=new&status=open lazygal-0.8.2/lazygal.conf.5.xml0000644000175000017500000002646112301071671016373 0ustar niolniol00000000000000 %lazygalent; August 2011"> Debian"> GNU"> GPL"> ]> 2011 &dhdate; LAZYGAL.CONF &dhsection; &dhpackage; &dhsectiontitle; &dhtitle; Configuration file for lazygal, a static web gallery generator. DESCRIPTION The configuration file is an INI like file which configures lazygal. The format looks like this: [sectionname] variable = value othervariable = othervalue [othersection] foo = bar Boolean values can be conveniently set in the following ways: For True: 1, yes, true, and on. For False: 0, no, false, and off. Please refer to the python ConfigParser documentation for more information on the file format. runtime section The runtime defines the runtime parameters. quiet Boolean. Same as in &lazygal; if True. (default is False). debug Boolean. Same as in &lazygal; if True (default is False). check-all-dirs Boolean. Same as in &lazygal; if True. (default is False). global section The global defines the global parameters. Those parameters apply to all the sub-galleries. output-directory Same as in &lazygal; (default is current directory). clean-destination Boolean. Same as in &lazygal; if True. dir-flattening-depth Same as in &lazygal;. puburl Same as in &lazygal;. theme Same as in &lazygal;. webgal section The webgal defines the parameters for a web-gallery. default-style Same as in &lazygal;. webalbumpic-bg Same as in &lazygal;. webalbumpic-type Same as in &lazygal;. If you set this to 'tidy' you may also consider setting webalbumpic-size (see below) to something smaller than the default 200x150. webalbumpic-size Size of picture mash-up representing galleries, eg. 200x150. image-size Same as in &lazygal;. thumbnail-size Same as in &lazygal;. video-size Size of videos, eg. 0x0. Refer to the IMAGE RESIZE DESCRIPTION section for more information on the available syntax. In addition, size can be the name of a previously declared image-size. thumbs-per-page Same as in &lazygal;. sort-medias Same as in &lazygal;. sort-subgals Same as in &lazygal;. original Boolean. Same as in &lazygal; if True (default is False). original-baseurl Same as in &lazygal;. original-symlink Boolean. Same as in &lazygal; if True (default is False). dirzip Same as in &lazygal; if True (default is False). jpeg-quality Same as in &lazygal;. jpeg-optimize Boolean. Run an extra optimization pass for each generated thumbnail if True, the default. jpeg-progressive Generate progressive JPEG images if True, the default. publish-metadata Publish image metadata if True, the default: copy original image metadata in reduced picture, and include some information in the image page. filter-by-tag Same as in &lazygal;. template-vars section The template-vars defines the custom template variables. The variables and their value are listed in this section. $footer in the default template For instance, $footer is a template variable in the default template. Its value can be defined with this configuration file: [template-vars] footer = <p>All pics are copyright 2011 me</p> SEE ALSO &lazygal;. AUTHOR This manual page was written for the &debian; system (but may be used by others). Permission is granted to copy, distribute and/or modify this document under the terms of the &gnu; General Public License, Version 2 any later version published by the Free Software Foundation. On Debian systems, the complete text of the GNU General Public License can be found in /usr/share/common-licenses/GPL. lazygal-0.8.2/setup.py0000755000175000017500000002333312301071671014627 0ustar niolniol00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- # Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Michal Čihař, Mickaël Royer, Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from distutils.core import setup, Command import distutils.sysconfig import distutils.command.build_scripts import distutils.command.build from distutils.dep_util import newer from distutils.spawn import find_executable import re import os import sys import glob import lazygal from stat import ST_MODE class test_lazygal(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): import lazygaltest lazygaltest.run() class build_manpages(Command): user_options = [] manpages = None db2mans = [ # debian "/usr/share/sgml/docbook/stylesheet/xsl/nwalsh/manpages/docbook.xsl", # gentoo "/usr/share/sgml/docbook/xsl-stylesheets/manpages/docbook.xsl", ] mandir = "./" executable = find_executable('xsltproc') def initialize_options(self): pass def finalize_options(self): self.manpages = glob.glob(os.path.join(self.mandir, "*.xml")) def __get_man_section(self, filename): # filename should be file.mansection.xml return filename.split('.')[-2] def run(self): data_files = self.distribution.data_files db2man = None for path in self.__class__.db2mans: if os.path.exists(path): db2man = path continue for xmlmanpage in self.manpages: manpage = xmlmanpage[:-4] # remove '.xml' at the end section = manpage[-1:] if newer(xmlmanpage, manpage): cmd = (self.executable, "--nonet", "-o", self.mandir, db2man, xmlmanpage) self.spawn(cmd) targetpath = os.path.join("share", "man", 'man%s' % section) data_files.append((targetpath, (manpage, ), )) class build_i18n_lazygal(Command): user_options = [] po_package = None po_directory = None po_files = None def initialize_options(self): pass def finalize_options(self): self.po_directory = "locale" self.po_package = "lazygal" self.po_files = glob.glob(os.path.join(self.po_directory, "*.po")) def run(self): data_files = self.distribution.data_files for po_file in self.po_files: lang = os.path.basename(po_file[:-3]) mo_dir = os.path.join("build", "mo", lang, "LC_MESSAGES") mo_file = os.path.join(mo_dir, "%s.mo" % self.po_package) if not os.path.exists(mo_dir): os.makedirs(mo_dir) cmd = ["msgfmt", po_file, "-o", mo_file] self.spawn(cmd) targetpath = os.path.join("share/locale", lang, "LC_MESSAGES") data_files.append((targetpath, (mo_file,))) class build_lazygal(distutils.command.build.build): def __has_manpages(self, command): has_db2man = False for path in build_manpages.db2mans: if os.path.exists(path): has_db2man = True return 'build_manpages' in self.distribution.cmdclass\ and has_db2man and build_manpages.executable is not None def __has_i18n(self, command): return 'build_i18n' in self.distribution.cmdclass def finalize_options(self): distutils.command.build.build.finalize_options(self) self.sub_commands.append(("build_i18n", self.__has_i18n)) self.sub_commands.append(("build_manpages", self.__has_manpages)) # check if Python is called on the first line with this expression first_line_re = re.compile('^#!.*python[0-9.]*([ \t].*)?$') class build_scripts_lazygal(distutils.command.build_scripts.build_scripts, object): """ This is mostly distutils copy, it just renames script according to platform (.py for Windows, without extension for others) """ def copy_scripts(self): """Copy each script listed in 'self.scripts'; if it's marked as a Python script in the Unix way (first line matches 'first_line_re', ie. starts with "\#!" and contains "python"), then adjust the first line to refer to the current Python interpreter as we copy. """ self.mkpath(self.build_dir) outfiles = [] for script in self.scripts: adjust = 0 script = distutils.util.convert_path(script) outfile = os.path.join(self.build_dir, os.path.splitext(os.path.basename(script))[0]) if sys.platform == 'win32': outfile += os.extsep + 'py' outfiles.append(outfile) if not self.force and not distutils.dep_util.newer(script, outfile): distutils.log.debug("not copying %s (up-to-date)", script) continue # Always open the file, but ignore failures in dry-run mode -- # that way, we'll get accurate feedback if we can read the # script. try: f = open(script, "r") except IOError: if not self.dry_run: raise f = None else: first_line = f.readline() if not first_line: self.warn("%s is an empty file (skipping)" % script) continue match = first_line_re.match(first_line) if match: adjust = 1 post_interp = match.group(1) or '' if adjust: distutils.log.info("copying and adjusting %s -> %s", script, self.build_dir) if not self.dry_run: outf = open(outfile, "w") if not distutils.sysconfig.python_build: outf.write("#!%s%s\n" % (os.path.normpath(sys.executable), post_interp)) else: outf.write( "#!%s%s\n" % (os.path.join( distutils.sysconfig.get_config_var("BINDIR"), "python" + distutils.sysconfig.get_config_var("EXE")), post_interp)) outf.writelines(f.readlines()) outf.close() if f: f.close() else: f.close() self.copy_file(script, outfile) if os.name == 'posix': for file in outfiles: if self.dry_run: distutils.log.info("changing mode of %s", file) else: oldmode = os.stat(file)[ST_MODE] & 07777 newmode = (oldmode | 0555) & 07777 if newmode != oldmode: distutils.log.info("changing mode of %s from %o to %o", file, oldmode, newmode) os.chmod(file, newmode) # copy_scripts () # list themes to install theme_data = [] themes = glob.glob(os.path.join('themes', '*')) for theme in themes: themename = os.path.basename(theme) theme_data.append( (os.path.join('share', 'lazygal', 'themes', themename), glob.glob(os.path.join('themes', themename, '*')))) setup( name = 'lazygal', version = lazygal.__version__, description = 'Static web gallery generator', long_description = '', author = 'Alexandre Rossi', author_email = 'alexandre.rossi@gmail.com', maintainer = 'Alexandre Rossi', maintainer_email = 'alexandre.rossi@gmail.com', platforms = ['Linux', 'Mac OSX', 'Windows XP/2000/NT', 'Windows 95/98/ME'], keywords = ['gallery', 'exif', 'photo', 'image'], url = 'http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal', download_url = 'http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal', license = 'GPL', classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Win32 (MS Windows)', 'Environment :: X11 Applications :: GTK', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: Microsoft :: Windows :: Windows 95/98/2000', 'Operating System :: Microsoft :: Windows :: Windows NT/2000', 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Python', 'Topic :: Utilities', 'Natural Language :: English', ], packages = ['lazygal'], package_data = {'lazygal': ['defaults.conf'], }, scripts = ['lazygal.py'], # Override certain command classes with our own ones cmdclass = { 'build' : build_lazygal, 'build_scripts' : build_scripts_lazygal, 'build_i18n' : build_i18n_lazygal, 'build_manpages': build_manpages, 'test' : test_lazygal, }, data_files = theme_data ) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/COPYING0000644000175000017500000003625212301071671014151 0ustar niolniol00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Any part of this program are available under these terms and conditions. lazygal-0.8.2/userscripts/0000755000175000017500000000000012301073200015462 5ustar niolniol00000000000000lazygal-0.8.2/userscripts/lazygal-conf-migrate-070000755000175000017500000000602312301071671021663 0ustar niolniol00000000000000#!/usr/bin/env python # # Lazygal, a lazy static web gallery generator: v0.7 conf migration tool. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import ConfigParser TRANSLATION_TABLE_07 = { 'quiet' : ('runtime', 'quiet'), 'output-directory' : ('global' , 'output-directory'), 'puburl' : ('global' , 'puburl'), 'theme' : ('global' , 'theme'), 'default-style' : ('webgal' , 'default-style'), 'clean-destination' : ('global' , 'clean-destination'), 'check-all-dirs' : ('runtime', 'check-all-dirs'), 'dir-flattening-depth': ('global' , 'dir-flattening-depth'), 'original' : ('webgal' , 'original'), 'orig-base' : ('webgal' , 'original-baseurl'), 'orig-symlink' : ('webgal' , 'original-symlink'), 'image-size' : ('webgal' , 'image-size'), 'thumbnail-size' : ('webgal' , 'thumbnail-size'), 'make-dir-zip' : ('webgal' , 'dirzip'), 'thumbs-per-page' : ('webgal' , 'thumbs-per-page'), 'pic-sort-by' : ('webgal' , 'sort-medias'), 'subgal-sort-by' : ('webgal' , 'sort-subgals'), 'quality' : ('webgal' , 'jpeg-quality'), 'optimize' : ('webgal' , 'jpeg-optimize'), 'progressive' : ('webgal' , 'jpeg-progressive'), 'webalbumpic-bg' : ('webgal' , 'webalbumpic-bg'), } def config_to_0_7(file_path): oldconfig = ConfigParser.RawConfigParser() config_fp = open(file_path) oldconfig.readfp(config_fp) config_fp.close() newconfig = ConfigParser.RawConfigParser() for oldkey, (new_section, new_key) in TRANSLATION_TABLE_07.items(): if oldconfig.has_option('lazygal', oldkey): if not newconfig.has_section(new_section): newconfig.add_section(new_section) newconfig.set(new_section, new_key, oldconfig.get('lazygal', oldkey)) if oldconfig.has_section('template-vars'): newconfig.add_section('template-vars') for tplvar in oldconfig.options('template-vars'): newconfig.set('template-vars', tplvar, oldconfig.get('template-vars', tplvar)) return newconfig if __name__ == '__main__': import sys config_to_0_7(sys.argv[1]).write(sys.stdout) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/0000755000175000017500000000000012301073200014537 5ustar niolniol00000000000000lazygal-0.8.2/lazygal/log.py0000644000175000017500000000330212301071671015702 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2013 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import logging class ProgressConsoleHandler(logging.StreamHandler): last_progress_lengh = 0 progress_msg = '' def clear_last_progress(self): self.stream.write('\r' + ' '*self.last_progress_lengh + '\r') def emit(self, record=None): try: self.clear_last_progress() if record is not None: self.stream.write(self.format(record) + '\n') self.stream.write(self.progress_msg) self.last_progress_lengh = len(self.progress_msg) self.flush() except (KeyboardInterrupt, SystemExit): raise except: self.handleError(record) def update_progress(self, s): self.progress_msg = s self.emit() def close(self): self.clear_last_progress() self.flush() super(ProgressConsoleHandler, self).close() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/tplvars.py0000644000175000017500000001735512301071671016631 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2013 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import re import posixpath import pathutils class TemplateVariables(object): def __init__(self, page): self.page = page class Media(TemplateVariables): def __init__(self, page, webalbum_media): super(Media, self).__init__(page) self.webalbum_media = webalbum_media self.media = webalbum_media.media def link(self): if self.webalbum_media: link_vals = {} link_vals['type'] = self.media.type if self.page.dir.album.theme.kind == 'static': link_vals['link'] = self.webalbum_media.browse_pages[self.page.size_name].rel_path(self.page.dir, url=True) elif self.page.dir.album.theme.kind == 'dynamic': link_vals['link'] = self.webalbum_media.resized[self.page.size_name].rel_path(self.page.dir, url=True) link_vals['link'] = self.page.url_quote(link_vals['link']) link_vals['thumb'] = self.webalbum_media.thumb.rel_path(self.page.dir, url=True) link_vals['thumb'] = self.page.url_quote(link_vals['thumb']) if not self.media.broken: link_vals['thumb_width'],\ link_vals['thumb_height'] = self.webalbum_media.thumb.get_size() link_vals['thumb_name'] = self.page.dir.album._str_humanize(self.media.name) return link_vals else: return None class Image(Media): def full(self): tpl_values = self.link() tpl_values['img_src'] = self.webalbum_media.resized[self.page.size_name].filename tpl_values['img_src'] = self.page.url_quote(tpl_values['img_src']) tpl_values['image_name'] = self.media.filename tpl_values['img_width'], tpl_values['img_height'] = self.webalbum_media.resized[self.page.size_name].get_size() if self.page.dir.config.getboolean('webgal', 'publish-metadata'): tpl_values['publish_metadata'] = True img_date = self.media.get_date_taken() tpl_values['image_date'] = img_date.strftime(_("on %d/%m/%Y at %H:%M")) tpl_values['image_datetime'] = img_date image_info = self.media.info() if image_info: comment = image_info.get_comment() if comment == '' or comment is None: tpl_values['comment'] = None else: tpl_values['comment'] = self.page._do_not_escape(comment) tpl_values['camera_name'] = image_info.get_camera_name() tpl_values['lens_name'] = image_info.get_lens_name() tpl_values['flash'] = image_info.get_flash() tpl_values['exposure'] = image_info.get_exposure() tpl_values['iso'] = image_info.get_iso() tpl_values['fnumber'] = image_info.get_fnumber() tpl_values['focal_length'] = image_info.get_focal_length() tpl_values['authorship'] = image_info.get_authorship() tpl_values['keywords'] = ', '.join(image_info.get_keywords()) return tpl_values class Video(Media): def full(self): tpl_values = self.link() tpl_values['video_src'] = self.webalbum_media.resized[self.page.size_name].filename tpl_values['video_src'] = self.page.url_quote(tpl_values['video_src']) return tpl_values def media_vars(page, webalbum_media): cls = None if webalbum_media.media.type == 'image': cls = Image elif webalbum_media.media.type == 'video': cls = Video else: raise NotImplementedError return cls(page, webalbum_media) class SrcPath(TemplateVariables): def __init__(self, page, srcpath): super(SrcPath, self).__init__(page) self.srcpath = srcpath def should_be_flattened(self): return self.page.dir.should_be_flattened(self.srcpath) def id(self): if self.should_be_flattened(): rawid = self.page.dir.source_dir.rel_path(self.page.dir.flattening_srcpath(self.srcpath), self.srcpath) else: rawid = os.path.basename(self.srcpath) return re.sub(r'[/ \\]', '_', rawid) def link(self): link_target = self.page._add_size_qualifier('index.html') if self.should_be_flattened(): # Add anchor target to get straight to gallery listing link_target = link_target + '#' + self.id() # Add relative path to link if needed index_path = None if self.should_be_flattened(): index_path = self.page.dir.flattening_srcpath(self.srcpath) elif self.srcpath != self.page.dir.source_dir.path: index_path = self.srcpath if index_path is not None: index_path = self.page.dir.rel_path_to_src(index_path) index_path = pathutils.url_path(index_path) link_target = posixpath.join(index_path, link_target) return self.page.url_quote(link_target) def path(self): wg_path = [] for dirmd in self.page.dir.source_dir.parents_metadata(): wg = {} wg['link'] = SrcPath(self.page, dirmd.dir_path).link() wg['name'] = dirmd.get_title() wg['root'] = dirmd.dir_path == self.page.dir.album.source_dir wg['current'] = dirmd.dir_path == self.page.dir.source_dir.path wg_path.append(wg) wg_path.reverse() return wg_path class Webgal(SrcPath): def __init__(self, page, webgal): super(Webgal, self).__init__(page, webgal.source_dir.path) self.webgal = webgal def info(self): dir_info = {} if self.webgal.source_dir.metadata: dir_info.update(self.webgal.source_dir.metadata.get()) if 'album_description' in dir_info.keys(): dir_info['album_description'] =\ self.page._do_not_escape(dir_info['album_description']) if 'album_name' not in dir_info: dir_info['album_name'] = self.webgal.source_dir.human_name if self.webgal.dirzip: archive_rel_path = self.webgal.dirzip.rel_path(self.page.dir, url=True) dir_info['dirzip'] = self.page.url_quote(archive_rel_path) dir_info['dirzip_size'] = self.page.format_filesize(self.webgal.dirzip.size()) dir_info['is_main'] = self.webgal is self.page.dir dir_info['image_count'] = self.webgal.get_media_count('image') dir_info['subgal_count'] = len(self.webgal.source_dir.subdirs) dir_info['id'] = self.page.url_quote(self.id()) return dir_info def link_info(self): link_info = self.info() link_info['link'] = self.link() link_info['album_picture'] = \ posixpath.join(self.webgal.source_dir.name, self.webgal.get_webalbumpic_filename()) link_info['album_picture'] = self.page.url_quote(link_info['album_picture']) return link_info # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/pathutils.py0000644000175000017500000000750112301071671017143 0ustar niolniol00000000000000# Deejayd, a media player daemon # Copyright (C) 2013 Mickael Royer # Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import sys import posixpath import logging def is_root_posix(path): return path == '/' def is_root_win32(path): return path[1:] == ':\\' # strip drive letter in comparison if sys.platform == 'win32': is_root = is_root_win32 else: is_root = is_root_posix def path2unicode(path, errors='strict'): if type(path) is unicode: return path else: return path.decode(sys.getfilesystemencoding(), errors) def is_subdir_of(dir_path, path): """ Returns whether path is a subdirectory of dir_path. """ dir_path = os.path.abspath(dir_path) path = os.path.abspath(path) test_path = path while test_path != dir_path and not is_root(test_path): test_path, tail = os.path.split(test_path) if test_path == '': raise RuntimeError('subdir test failure: please report a bug') if test_path == dir_path: return True else: return False def url_path(physical_path, input_pathmodule=os.path): """ Convert a physical path to a path suitable for use in a URL link, i.e. using forward slashes. This can only be used for relative paths because while converting, the root (either '/' or 'C:\\') is irrelevant. """ if input_pathmodule == posixpath: return physical_path head = physical_path path_list = [] while head != '' and not is_root_posix(head) and not is_root_win32(head): head, tail = input_pathmodule.split(head) path_list.append(tail) if path_list == []: return '' path_list.reverse() return posixpath.join(*path_list) def walk(top, walked=None, topdown=False): """ This is a wrapper around os.walk() from the standard library: - following symbolic links on directories - whith barriers in place against walking twice the same directory, which may happen when two directory trees have symbolic links to each other's contents. """ if walked is None: walked = [] for root, dirs, files in os.walk(top, topdown=topdown): walked.append(os.path.realpath(root)) # Follow symlinks if they have not been walked yet for d in dirs: d_path = os.path.join(root, d) if os.path.islink(d_path): if os.path.realpath(d_path) not in walked: for x in walk(d_path, walked): yield x else: logging.error("Not following symlink '%s' because directory has already been processed." % d_path) yield root, dirs, files def walk_and_do(top=None, walked=None, dcb=None, fcb=None, topdown=False): """ This walk calls dcb on each found directory and fcb on each found file with the following arguments : - path """ for root, dirs, files in walk(top, walked, topdown=topdown): if dcb is not None: dcb(root, dirs, files) if fcb is not None: map(lambda f: fcb(os.path.join(root, f)), files) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/generators.py0000644000175000017500000010632712301071671017305 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import locale import logging import gc import genshi import sys import re import shutil from config import LazygalConfig, LazygalWebgalConfig from config import USER_CONFIG_PATH, LazygalConfigDeprecated from sourcetree import SOURCEDIR_CONFIGFILE from pygexiv2 import GExiv2 import make import pathutils import sourcetree import tpl import newsize import metadata import genpage import genmedia import genfile from lazygal import INSTALL_MODE, INSTALL_PREFIX if INSTALL_MODE == 'source': DATAPATH = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) elif INSTALL_MODE == 'installed': DATAPATH = os.path.join(INSTALL_PREFIX, 'share', 'lazygal') if not os.path.exists(os.path.join(DATAPATH, 'themes')): print _('Could not find themes dir, check your installation!') sys.exit(1) DEST_SHARED_DIRECTORY_NAME = 'shared' class SubgalSort(make.MakeTask): """ This task sorts the medias within a gallery according to the chosen rule. """ def __init__(self, webgal_dir): make.MakeTask.__init__(self) self.set_dep_only() self.webgal_dir = webgal_dir def build(self): logging.info(_(" SORTING pics and subdirs")) if self.webgal_dir.subgal_sort_by[0] == 'exif': subgal_sorter = \ lambda x, y: x.source_dir.compare_latest_exif(y.source_dir) elif self.webgal_dir.subgal_sort_by[0] == 'mtime': subgal_sorter = \ lambda x, y: x.source_dir.compare_mtime(y.source_dir) elif self.webgal_dir.subgal_sort_by[0] == 'dirname'\ or self.webgal_dir.subgal_sort_by[0] == 'filename': # Backward compatibility subgal_sorter = \ lambda x, y: x.source_dir.compare_filename(y.source_dir) else: raise ValueError(_("Unknown sorting criterion '%s'") % self.webgal_dir.subgal_sort_by[0]) self.webgal_dir.subgals.sort(subgal_sorter, reverse=self.webgal_dir.subgal_sort_by[1]) if self.webgal_dir.pic_sort_by[0] == 'exif': sorter = lambda x, y: x.media.compare_to_sort(y.media) elif self.webgal_dir.pic_sort_by[0] == 'mtime': sorter = lambda x, y: x.media.compare_mtime(y.media) elif self.webgal_dir.pic_sort_by[0] == 'filename': sorter = lambda x, y: x.media.compare_filename(y.media) else: raise ValueError(_("Unknown sorting criterion '%s'") % self.webgal_dir.pic_sort_by[0]) self.webgal_dir.medias.sort(sorter, reverse=self.webgal_dir.pic_sort_by[1]) # chain medias previous = None for media in self.webgal_dir.medias: if previous: previous.set_next(media) media.set_previous(previous) previous = media class SubgalBreak(make.MakeTask): """ This task breaks galleries into multiple pages. """ def __init__(self, webgal_dir): make.MakeTask.__init__(self) self.webgal_dir = webgal_dir self.__last_page_number = -1 def next_page_number(self): self.__last_page_number += 1 return self.__last_page_number def how_many_pages(self): return self.__last_page_number + 1 def build(self): logging.info(_(" BREAKING web gallery into multiple pages")) if self.webgal_dir.thumbs_per_page == 0: self.__fill_no_pagination() else: if self.webgal_dir.flatten_below(): self.__fill_loose_pagination() else: self.__fill_real_pagination() def __fill_no_pagination(self): galleries = [] galleries.append((self.webgal_dir, self.webgal_dir.medias)) if self.webgal_dir.flatten_below(): subgals = [] for dir in self.webgal_dir.get_all_subgals(): galleries.append((dir, dir.medias)) else: subgals = self.webgal_dir.subgals self.webgal_dir.add_index_page(subgals, galleries) def __fill_loose_pagination(self): """ Loose pagination not breaking subgals (chosen if subgals are flattened). """ subgals = [] # No subgal links as they are flattened galleries = [] how_many_medias = 0 subgals_it = iter([self.webgal_dir] + self.webgal_dir.get_all_subgals()) try: while True: subgal = subgals_it.next() how_many_medias += subgal.get_media_count() galleries.append((subgal, subgal.medias)) if how_many_medias > self.webgal_dir.thumbs_per_page: self.webgal_dir.add_index_page(subgals, galleries) galleries = [] how_many_medias = 0 except StopIteration: if len(galleries) > 0: self.webgal_dir.add_index_page(subgals, galleries) def __fill_real_pagination(self): medias_amount = len(self.webgal_dir.medias) how_many_pages = medias_amount // self.webgal_dir.thumbs_per_page if medias_amount == 0\ or medias_amount % self.webgal_dir.thumbs_per_page > 0: how_many_pages = how_many_pages + 1 for page_number in range(0, how_many_pages): step = page_number * self.webgal_dir.thumbs_per_page end_index = step + self.webgal_dir.thumbs_per_page shown_medias = self.webgal_dir.medias[step:end_index] galleries = [(self.webgal_dir, shown_medias)] # subgal links only for first page if page_number == 0: subgals = self.webgal_dir.subgals else: subgals = [] self.webgal_dir.add_index_page(subgals, galleries) class WebalbumMediaTask(make.GroupTask): def __init__(self, webgal, media): super(WebalbumMediaTask, self).__init__() self.webgal = webgal self.media = media self.previous = None self.next = None self.original = None self.resized = {} self.browse_pages = {} for size_name in self.webgal.browse_sizes: if self.webgal.newsizers[size_name] == 'original': self.resized[size_name] = self.get_original() else: self.resized[size_name] = self.get_resized(size_name) self.add_dependency(self.resized[size_name]) if self.webgal.original and not self.webgal.orig_base: self.add_dependency(self.get_original()) if self.webgal.album.theme.kind == 'static': self.browse_pages[size_name] = self.get_browse_page(size_name) if self.webgal.album.force_gen_pages: self.browse_pages[size_name].stamp_delete() def set_next(self, media): self.next = media if media: for bpage in self.browse_pages.values(): if media.thumb: bpage.add_dependency(media.thumb) def set_previous(self, media): self.previous = media if media: for bpage in self.browse_pages.values(): if media.thumb: bpage.add_dependency(media.thumb) def get_original_or_symlink(self): if not self.webgal.orig_symlink: return genfile.CopyMediaOriginal(self.webgal, self.media) else: return genfile.SymlinkMediaOriginal(self.webgal, self.media) def get_original(self): if not self.original: self.original = self.get_original_or_symlink() return self.original def get_browse_page(self, size_name): return genpage.WebalbumBrowsePage(self.webgal, size_name, self) def make(self): super(WebalbumMediaTask, self).make() self.webgal.media_done() class WebalbumImageTask(WebalbumMediaTask): """ This task builds all items related to one picture. """ def __init__(self, webgal, image): super(WebalbumImageTask, self).__init__(webgal, image) self.thumb = genmedia.ImageOtherSize(self.webgal, self.media, genmedia.THUMB_SIZE_NAME) self.add_dependency(self.thumb) def get_resized(self, size_name): if self.webgal.newsizers[size_name] == 'original': return self.get_original_or_symlink() else: sized = genmedia.ImageOtherSize(self.webgal, self.media, size_name) self.media.get_size() # probe size to check if media is broken if not self.media.broken\ and sized.get_size() == sized.source_media.get_size(): # Do not process if size is the same return self.get_original() else: return sized class WebalbumVideoTask(WebalbumMediaTask): """ This task builds all items related to one video. """ def __init__(self, webgal, video): self.webvideo = None super(WebalbumVideoTask, self).__init__(webgal, video) self.thumb = genmedia.VideoThumb(self.webgal, self.media, genmedia.THUMB_SIZE_NAME) self.add_dependency(self.webvideo) def get_resized(self, size_name): if not self.webvideo: if self.webgal.newsizers[genmedia.VIDEO_SIZE_NAME] == 'original'\ and self.media.extension == '.webm': # do not transcode webm videos self.webvideo = self.get_original_or_symlink() else: self.webvideo = genmedia.WebVideo(self.webgal, self.media, genmedia.VIDEO_SIZE_NAME, self.webgal.progress) return self.webvideo class WebalbumDir(make.FileMakeObject): """ This is a built web gallery with its files, thumbs and reduced pics. """ def __init__(self, dir, subgals, album, album_dest_dir, progress=None): self.source_dir = dir self.path = os.path.join(album_dest_dir, self.source_dir.strip_root()) if self.path.endswith(os.sep): self.path = os.path.dirname(self.path) super(WebalbumDir, self).__init__(self.path) self.progress = progress self.add_dependency(self.source_dir) self.subgals = [s for s in subgals if s.get_all_media_count() > 0] for srcdir in self.source_dir.subdirs: self.add_dependency(srcdir) self.album = album self.feed = None self.flattening_dir = None self.config = LazygalWebgalConfig(self.album.config) self.__configure() tagfilters = self.config.getlist('webgal', 'filter-by-tag') self.medias = [] self.sort_task = SubgalSort(self) self.sort_task.add_dependency(self.source_dir) for media in self.source_dir.medias: self.sort_task.add_dependency(media) if len(tagfilters) > 0 and media.info() is not None: # tag-filtering is requested res = True for tagf in tagfilters: # concatenate the list of tags as a string of words, # space-separated. to ensure that we match the full # keyword and not only a subpart of it, we also surround # the matching pattern with spaces # we look for tag words, partial matches are not wanted regex = re.compile(r"\b" + tagf + r"\b") kwlist = ' '.join(media.info().get_keywords()) if re.search(regex, kwlist) is None: res = False break if res is False: continue if media.type == 'image': media_task = WebalbumImageTask(self, media) elif media.type == 'video': media_task = WebalbumVideoTask(self, media) else: raise NotImplementedError("Unknown media type '%s'" % media.type) self.medias.append(media_task) self.add_dependency(media_task) # Create the directory if it does not exist if not os.path.isdir(self.path) and (self.get_media_count() > 0): logging.info(_(" MKDIR %%WEBALBUMROOT%%/%s") % self.source_dir.strip_root()) logging.debug("(%s)" % self.path) os.makedirs(self.path, mode=0755) self.stamp_delete() # Directory did not exist, mark it as so if self.config.getboolean('webgal', 'dirzip')\ and self.get_media_count() > 1: self.dirzip = genfile.WebalbumArchive(self) self.add_dependency(self.dirzip) else: self.dirzip = None self.index_pages = [] if (self.get_all_media_count() > 0) and not self.should_be_flattened(): self.break_task = SubgalBreak(self) if self.thumbs_per_page > 0: # FIXME: If pagination is 'on', galleries need to be sorted # before being broken on multiple pages, and thus this slows # down a lot the checking of a directory's need to be built. self.break_task.add_dependency(self.sort_task) # This task is special because it populates dependencies. This is # why it needs to be built before a build check. self.break_task.make() self.webgal_pic = genmedia.WebalbumPicture(self) self.add_dependency(self.webgal_pic) else: self.break_task = None def __parse_browse_sizes(self, sizes_string): for single_def in sizes_string.split(','): name, string_size = single_def.split('=') name = name.decode(locale.getpreferredencoding()) if name == '': raise ValueError(_("Sizes is a comma-separated list of size names and specs:\n\t e.g. \"small=640x480,medium=1024x768\".")) if name == genmedia.THUMB_SIZE_NAME: raise ValueError(_("Size name '%s' is reserved for internal processing.") % genmedia.THUMB_SIZE_NAME) self.__parse_size(name, string_size) self.browse_sizes.append(name) def __parse_size(self, size_name, size_string): if size_string == '0x0': self.newsizers[size_name] = 'original' elif size_string in self.newsizers: pass # size reference, do nothing else: try: self.newsizers[size_name] = newsize.get_newsizer(size_string) except newsize.NewsizeStringParseError: raise ValueError(_("'%s' for size '%s' does not describe a known size syntax.") % (size_string.decode(locale.getpreferredencoding()), size_name, )) def __parse_sort(self, sort_string): try: sort_method, reverse = sort_string.split(':') except ValueError: sort_method = sort_string reverse = False if reverse == 'reverse': return sort_method, True else: return sort_method, False def __load_tpl_vars(self): # Load tpl vars from config tpl_vars = {} if self.config.has_section('template-vars'): tpl_vars = {} for option in self.config.options('template-vars'): try: value = self.config.getboolean('template-vars', option) tpl_vars[option] = value except ValueError: value = self.config.get('template-vars', option) value = value.decode(locale.getpreferredencoding()) tpl_vars[option] = genshi.core.Markup(value) return tpl_vars def __configure(self): config_dirs = self.source_dir.parent_paths()[:-1] # strip root dir config_dirs.reverse() # from root to deepest config_files = map(lambda d: os.path.join(d, SOURCEDIR_CONFIGFILE), config_dirs) logging.debug(_(" Trying loading gallery configs: %s") % ', '.join(map(self.source_dir.strip_root, config_files))) self.config.read(config_files) self.browse_sizes = [] self.newsizers = {} self.__parse_browse_sizes(self.config.get('webgal', 'image-size')) self.__parse_size(genmedia.THUMB_SIZE_NAME, self.config.get('webgal', 'thumbnail-size')) self.__parse_size(genmedia.VIDEO_SIZE_NAME, self.config.get('webgal', 'video-size')) self.default_size_name = self.browse_sizes[0] self.tpl_vars = self.__load_tpl_vars() styles = self.album.theme.get_avail_styles( self.config.get('webgal', 'default-style')) self.tpl_vars.update({'styles': styles}) self.set_original(self.config.getboolean('webgal', 'original'), self.config.getstr('webgal', 'original-baseurl'), self.config.getboolean('webgal', 'original-symlink')) self.thumbs_per_page = self.config.getint('webgal', 'thumbs-per-page') self.quality = self.config.getint('webgal', 'jpeg-quality') self.save_options = {} if self.config.getboolean('webgal', 'jpeg-optimize'): self.save_options['optimize'] = True if self.config.getboolean('webgal', 'jpeg-progressive'): self.save_options['progressive'] = True self.pic_sort_by = self.__parse_sort(self.config.get('webgal', 'sort-medias')) self.subgal_sort_by = self.__parse_sort(self.config.get('webgal', 'sort-subgals')) self.filter_by_tag = self.config.get('webgal', 'filter-by-tag') self.webalbumpic_bg = self.config.get('webgal', 'webalbumpic-bg') self.webalbumpic_type = self.config.get('webgal', 'webalbumpic-type') try: self.webalbumpic_size = map(int, self.config.get('webgal', 'webalbumpic-size').split('x')) if len(self.webalbumpic_size) != 2: raise ValueError except ValueError: logging.error(_('Bad syntax for webalbumpic-size.')) sys.exit(1) self.keep_gps = self.config.getboolean('webgal', 'keep-gps') def set_original(self, original=False, orig_base=None, orig_symlink=False): self.original = original or orig_symlink self.orig_symlink = orig_symlink if self.original and orig_base and not orig_symlink: self.orig_base = orig_base else: self.orig_base = None def get_webalbumpic_filename(self): if self.webalbumpic_bg == 'transparent': ext = '.png' # JPEG does not have an alpha channel else: ext = '.jpg' return genmedia.WebalbumPicture.BASEFILENAME + ext def _add_size_qualifier(self, path, size_name, force_extension=None): filename, extension = os.path.splitext(path) if force_extension is not None: extension = force_extension if size_name == self.default_size_name and extension == '.html': # Do not append default size name to HTML page filename return path elif size_name in self.browse_sizes\ and self.newsizers[size_name] == 'original'\ and extension != '.html': # Do not append size_name to unresized images. return path else: return "%s_%s%s" % (filename, size_name, extension) def add_index_page(self, subgals, galleries): page_number = self.break_task.next_page_number() pages = [] for size_name in self.browse_sizes: page = genpage.WebalbumIndexPage(self, size_name, page_number, subgals, galleries) if self.album.force_gen_pages: page.stamp_delete() self.add_dependency(page) pages.append(page) self.index_pages.append(pages) def register_output(self, output): # We only care about output in the current directory if os.path.dirname(output) == self.path: super(WebalbumDir, self).register_output(output) def register_feed(self, feed): self.feed = feed def get_subgal_count(self): if self.flatten_below(): return 0 else: len(self.source_dir.subdirs) def get_all_subgals(self): all_subgals = list(self.subgals) # We want a copy here. for subgal in self.subgals: all_subgals.extend(subgal.get_all_subgals()) return all_subgals def get_media_count(self, media_type=None): if media_type is None: return len(self.medias) else: typed_media_count = 0 for mediatask in self.medias: if mediatask.media.type == media_type: typed_media_count += 1 return typed_media_count def get_all_media_count(self): count = len(self.medias) for subgal in self.subgals: count += subgal.get_all_media_count() return count def get_all_medias_tasks(self): all_medias = list(self.medias) # We want a copy here. for subgal in self.subgals: all_medias.extend(subgal.get_all_medias_tasks()) return all_medias def should_be_flattened(self, path=None): if path is None: path = self.source_dir.path return self.album.dir_flattening_depth is not False\ and self.source_dir.get_album_level(path) > self.album.dir_flattening_depth def flatten_below(self): if self.album.dir_flattening_depth is False: return False elif len(self.source_dir.subdirs) > 0: # As all subdirs are at the same level, if one should be flattened, # all should. return self.subgals[0].should_be_flattened() else: return False def rel_path_to_src(self, target_srcdir_path): """ Returns the relative path to go from this directory to target_srcdir_path. """ return self.source_dir.rel_path(self.source_dir.path, target_srcdir_path) def rel_path(self, path): """ Returns the relative path to go from this directory to the path supplied as argument. """ return os.path.relpath(path, self.path) def flattening_srcpath(self, srcdir_path): """ Returns the source path in which srcdir_path should flattened, that is the path of the gallery index that will point to srcdir_path's pictures. """ if self.should_be_flattened(srcdir_path): cur_path = srcdir_path while self.should_be_flattened(cur_path): cur_path, dummy = os.path.split(cur_path) return cur_path else: return '' def list_foreign_files(self): if not os.path.isdir(self.path): return [] foreign_files = [] # Check dest for junk files extra_files = [] if self.source_dir.is_album_root(): extra_files.append(os.path.join(self.path, DEST_SHARED_DIRECTORY_NAME)) dirnames = [d.source_dir.name for d in self.subgals] expected_dirs = map(lambda dn: os.path.join(self.path, dn), dirnames) for dest_file in os.listdir(self.path): dest_file = os.path.join(self.path, dest_file) if not isinstance(dest_file, unicode): # FIXME: No clue why this happens, but it happens! dest_file = dest_file.decode(sys.getfilesystemencoding()) if dest_file not in self.output_items and\ dest_file not in expected_dirs and\ dest_file not in extra_files: foreign_files.append(dest_file) return foreign_files def build(self): for dest_file in self.list_foreign_files(): self.album.cleanup(dest_file, self.path) def make(self, force=False): needed_build = self.needs_build() super(WebalbumDir, self).make(force or needed_build) # Although we should have modified the directory contents and thus its # mtime, it is possible that the directory mtime has not been updated # if we regenerated without adding/removing pictures (to take into # account a rotation for example). This is why we force directory mtime # update here if something has been built. if needed_build and os.path.isdir(self.path): os.utime(self.path, None) def media_done(self): if self.progress is not None: self.progress.media_done() class SharedFiles(make.FileMakeObject): def __init__(self, album, dest_dir, tpl_vars): self.path = os.path.join(dest_dir, DEST_SHARED_DIRECTORY_NAME) self.album = album # Create the shared files directory if it does not exist if not os.path.isdir(self.path): logging.info(_("MKDIR %SHAREDDIR%")) logging.debug("(%s)" % self.path) os.makedirs(self.path, mode=0755) super(SharedFiles, self).__init__(self.path) self.expected_shared_files = [] for shared_file, shared_file_rel_dest in self.album.theme.shared_files: shared_file_dest = os.path.join(self.path, shared_file_rel_dest) if self.album.theme.tpl_loader.is_known_template_type(shared_file): sf = genpage.SharedFileTemplate(album, shared_file, shared_file_dest, tpl_vars) if self.album.force_gen_pages: sf.stamp_delete() self.expected_shared_files.append(sf.path) else: sf = genfile.SharedFileCopy(shared_file, shared_file_dest) self.expected_shared_files.append(shared_file_dest) self.add_dependency(sf) def build(self): # Cleanup themes files which are not in themes anymore. for present_file in os.listdir(self.path): file_path = os.path.join(self.path, present_file) if file_path not in self.expected_shared_files: self.album.cleanup(file_path, self.path) class AlbumGenProgress(object): def __init__(self, dirs_total, medias_total): self._dirs_total = dirs_total self._dirs_done = 0 self._medias_total = medias_total self._medias_done = 0 self._task_percent = None def dir_done(self): self._dirs_done = self._dirs_done + 1 self.updated() def media_done(self, how_many=1): self._medias_done = self._medias_done + how_many self.updated() def set_task_progress(self, percent): self._task_percent = percent self.updated() def set_task_done(self): self._task_percent = None self.updated() def __unicode__(self): msg = _("Progress: dir %d/%d (%d%%), media %d/%d (%d%%)")\ % (self._dirs_done, self._dirs_total, 100 * self._dirs_done // self._dirs_total, self._medias_done, self._medias_total, 100 * self._medias_done // self._medias_total, ) if self._task_percent is not None: msg = msg + _(", current task %d%%") % self._task_percent return msg def updated(self): pass class Album(object): def __init__(self, source_dir, config=None): self.source_dir = os.path.abspath(source_dir) self.config = LazygalConfig() logging.info(_("Trying loading user config %s") % USER_CONFIG_PATH) self.config.read(USER_CONFIG_PATH) sourcedir_configfile = os.path.join(source_dir, SOURCEDIR_CONFIGFILE) if os.path.isfile(sourcedir_configfile): logging.info(_("Loading root config %s") % sourcedir_configfile) try: self.config.read(sourcedir_configfile) except LazygalConfigDeprecated: logging.error(_("'%s' uses a deprecated syntax: please refer to lazygal.conf(5) manual page.") % sourcedir_configfile) sys.exit(1) if config is not None: # Supplied config self.config.load(config) if self.config.getboolean('runtime', 'quiet'): logging.getLogger().setLevel(logging.ERROR) if self.config.getboolean('runtime', 'debug'): logging.getLogger().setLevel(logging.DEBUG) GExiv2.log_set_level(GExiv2.LogLevel.INFO) self.clean_dest = self.config.getboolean('global', 'clean-destination') self.force_gen_pages = self.config.getboolean('global', 'force-gen-pages') self.set_theme(self.config.get('global', 'theme')) self.dir_flattening_depth = self.config.getint('global', 'dir-flattening-depth') self.__statistics = None def set_theme(self, theme=tpl.DEFAULT_THEME): self.theme = tpl.Theme(os.path.join(DATAPATH, 'themes'), theme) def _str_humanize(self, text): dash_replaced = text.replace('_', ' ') return dash_replaced def is_in_sourcetree(self, path): return pathutils.is_subdir_of(self.source_dir, path) def cleanup(self, file_path, context_path): if self.clean_dest: # Do not delete something out of dest-dir. assert pathutils.is_subdir_of(context_path, file_path) if os.path.isdir(file_path): shutil.rmtree(file_path) else: os.unlink(file_path) logging.info('RM %s' % (file_path)) def generate_default_metadata(self): """ Generate default metadata files if no exists. """ logging.debug(_("Generating metadata in %s") % self.source_dir) for root, dirnames, filenames in pathutils.walk(self.source_dir): filenames.sort() # This is required for the ignored files # checks to be reliable. source_dir = sourcetree.Directory(root, [], filenames, self) logging.info(_("[Entering %%ALBUMROOT%%/%s]") % source_dir.strip_root()) logging.debug("(%s)" % source_dir.path) metadata.DefaultMetadata(source_dir, self).make() def stats(self): if self.__statistics is None: self.__statistics = { 'total' : 0, 'bydir' : {} } for root, dirnames, filenames in pathutils.walk(self.source_dir): dir_medias = len([f for f in filenames\ if sourcetree.MediaHandler.is_known_media(f)]) self.__statistics['total'] = self.__statistics['total']\ + dir_medias self.__statistics['bydir'][root] = dir_medias return self.__statistics def generate(self, dest_dir=None, progress=None): if dest_dir is None: dest_dir = self.config.getstr('global', 'output-directory') else: dest_dir = dest_dir.decode(sys.getfilesystemencoding()) sane_dest_dir = os.path.abspath(os.path.expanduser(dest_dir)) pub_url = self.config.getstr('global', 'puburl') check_all_dirs = self.config.getboolean('runtime', 'check-all-dirs') if self.is_in_sourcetree(sane_dest_dir): raise ValueError(_("Fatal error, web gallery directory is within source tree.")) logging.debug(_("Generating to %s") % sane_dest_dir) if pub_url: feed = genpage.WebalbumFeed(self, sane_dest_dir, pub_url) else: feed = None dir_heap = {} for root, dirnames, filenames in pathutils.walk(self.source_dir): if root in dir_heap: subdirs, subgals = dir_heap[root] del dir_heap[root] # No need to keep it there else: subdirs = [] subgals = [] checked_dir = sourcetree.File(root, self) if checked_dir.should_be_skipped(): logging.debug(_("(%s) has been skipped") % checked_dir.path) continue if checked_dir.path == os.path.join(sane_dest_dir, DEST_SHARED_DIRECTORY_NAME): logging.error(_("(%s) has been skipped because its name collides with the shared material directory name") % checked_dir.path) continue logging.info(_("[Entering %%ALBUMROOT%%/%s]") % checked_dir.strip_root()) logging.debug("(%s)" % checked_dir.path) source_dir = sourcetree.Directory(root, subdirs, filenames, self) destgal = WebalbumDir(source_dir, subgals, self, sane_dest_dir, progress) if source_dir.is_album_root(): # Use root config tpl vars for shared files tpl_vars = destgal.tpl_vars if not source_dir.is_album_root(): container_dirname = os.path.dirname(root) if container_dirname not in dir_heap: dir_heap[container_dirname] = ([], []) container_subdirs, container_subgals = dir_heap[container_dirname] container_subdirs.append(source_dir) container_subgals.append(destgal) if feed and source_dir.is_album_root(): feed.set_title(source_dir.human_name) md = destgal.source_dir.metadata.get() if 'album_description' in md.keys(): feed.set_description(md['album_description']) destgal.register_output(feed.path) if feed: feed.push_dir(destgal) destgal.register_feed(feed) if check_all_dirs: destgal.make() elif destgal.needs_build(): destgal.make(force=True) # avoid another needs_build() call in make() else: if progress is not None: progress.media_done(len(destgal.medias)) logging.debug(_(" SKIPPED because of mtime, touch source or use --check-all-dirs to override.")) # Force some memory cleanups, this is usefull for big albums. del destgal gc.collect() if progress is not None: progress.dir_done() logging.info(_("[Leaving %%ALBUMROOT%%/%s]") % source_dir.strip_root()) if feed: feed.make() # Force to check for unexpected files SharedFiles(self, sane_dest_dir, tpl_vars).make(True) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/metadata.py0000644000175000017500000004536112301071671016714 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import division import os import locale import logging import codecs import datetime from lazygal.pygexiv2 import GExiv2 from PIL import Image as PILImage from lazygal import make from fractions import Fraction FILE_METADATA_ENCODING = locale.getpreferredencoding() MATEW_TAGS = { 'album_name': 'Album name', 'album_description': 'Album description', 'album_picture': 'Album image identifier', } MATEW_METADATA = 'album_description' FILE_METADATA = ('album-name', 'album-description', 'album-picture', ) FILE_METADATA_MEDIA_SUFFIX = '.comment' FALLBACK_ENCODING = 'utf-8' # encoding used when guessing # As per http://www.exiv2.org/tags.html VENDOR_EXIF_CODES = ( 'Exif.Canon.LensModel', 'Exif.Minolta.LensID', 'Exif.Nikon3.Lens', 'Exif.Nikon3.LensType', 'Exif.OlympusEq.LensModel', 'Exif.OlympusEq.LensType', 'Exif.Panasonic.LensType', 'Exif.Pentax.LensType', 'Exif.Samsung2.LensType', 'Exif.Sigma.LensRange', 'Exif.Sony1.LensID', ) GEXIV2_DATE_FORMAT = '%Y:%m:%d %H:%M:%S' GExiv2.log_set_level(GExiv2.LogLevel.MUTE) # hide exiv2 errors def decode_exif_user_comment(raw, imgpath): """ GExiv2 does not decode EXIF user comment. """ # This field can contain charset information if raw.startswith('charset='): tokens = raw.split(' ') csetfield = tokens[0] text = ' '.join(tokens[1:]) ignore, cset = csetfield.split('=') cset = cset.strip('"') else: cset = None text = raw if cset == 'Unicode': encoding = None try: text.decode('utf-8') except UnicodeDecodeError: im = PILImage.open(imgpath) endianess = im.app['APP1'][6:8] if endianess == 'MM': encoding = 'utf-16be' elif endianess == 'II': encoding = 'utf-16le' else: raise ValueError else: encoding = 'utf-8' elif cset == 'Ascii': encoding = 'ascii' elif cset == 'Jis': encoding = 'shift_jis' else: encoding = FALLBACK_ENCODING # Return the decoded string according to the found encoding. try: return text.decode(encoding) except UnicodeDecodeError: return text.decode(encoding, 'replace') class FileMetadata(object): def __init__(self, path): self.path = path def contents(self, splitter=None): try: with codecs.open(self.path, 'r', FILE_METADATA_ENCODING) as f: # Not sure why codecs.open() does not skip the UTF-8 BOM. Maybe # this is because the BOM is not required and utf-8-sig handles # this in a better way. Anyway, the following code skips the # UTF-8 BOM if it is present. if FILE_METADATA_ENCODING == 'utf-8': maybe_bom = f.read(1).encode(FILE_METADATA_ENCODING) if maybe_bom != codecs.BOM_UTF8: f.seek(0) c = f.read() except IOError: return None if splitter is not None: return map(lambda s: s.strip(), c.split(splitter)) else: return c.strip() class ImageInfoTags(object): def __init__(self, image_path): self.image_path = image_path self._metadata = GExiv2.Metadata(self.image_path) def get_date(self): """ Get real time when photo has been taken. We prefer EXIF fields as those were filled by camera, Image DateTime can be updated by software when editing photos later. """ for tag in ('Exif.Photo.DateTimeOriginal', 'Exif.Image.DateTimeDigitized', 'Exif.Image.DateTime', ): try: dt_str = self._metadata[tag] dt = datetime.datetime.strptime(dt_str, GEXIV2_DATE_FORMAT) except (KeyError, ValueError) as StrptimeError: # ValueError: bypass errors such as "time data '0000:00:00 # 00:00:00' does not match format '%Y:%m:%d %H:%M:%S'" pass else: return dt def get_required_rotation(self): try: orientation_code = int(self._metadata['Exif.Image.Orientation']) if orientation_code == 8: return 90 elif orientation_code == 3: return 180 elif orientation_code == 6: return 270 else: # Should be orientation_code == 1 but catch all return 0 except KeyError: return 0 def get_camera_name(self): """ Gets vendor and model name from EXIF and tries to construct camera name out of this. This is a bit fuzzy, because diferent vendors put different information to both tags. """ try: model = self._metadata['Exif.Image.Model'].strip() # Terminate string at \x00 pos = model.find('\x00') if pos != -1: model = model[:14] try: vendor = self._metadata['Exif.Image.Make'].strip() vendor_l = vendor.lower() model_l = model.lower() # Split vendor to words and check whether they are # already in model, for example: # Canon/Canon A40 # PENTAX Corporation/PENTAX K10D # Eastman Kodak Company/KODAK DIGITAL SCIENCE DC260 (V01.00) for word in vendor_l.split(' '): if model_l.find(word) != -1: return model return ' '.join([vendor, model]) except KeyError: return model except KeyError: return '' def get_lens_name(self): """ Return name of used lenses. This usually makes sense only for SLR cameras and uses various maker notes. """ interpret = self._metadata.get_tag_interpreted_string vendor_values = [] for key in VENDOR_EXIF_CODES: try: v = self._metadata.get_tag_interpreted_string(key) if v is None: raise KeyError except KeyError: pass else: vendor_values.append(v.strip()) return ' '.join([s for s in vendor_values if s]) def get_exif_string(self, name): """ Reads string from EXIF information. """ return self._metadata[name].strip(' ') def get_exif_float(self, name): """ Reads float number from EXIF information (where it is stored as fraction). """ val = self.get_exif_float_value(name) return str(round(val, 1)) def get_exif_float_value(self, name): """ Reads float number from EXIF information (where it is stored as fraction or int). """ val = self._metadata.get_exif_tag_rational(name) return val.numerator / val.denominator def _fallback_to_encoding(self, encoded_string, encoding=FALLBACK_ENCODING): if encoded_string is None: raise ValueError if type(encoded_string) is unicode: return encoded_string try: return encoded_string.decode(encoding) except UnicodeDecodeError: return encoded_string.decode(encoding, 'replace') def get_exif_usercomment(self): ret = self._metadata['Exif.Photo.UserComment'].strip(' \0\x00') if type(ret) is not unicode: # the EXIF lib did not do the work for us ret = decode_exif_user_comment(ret, self.image_path) if ret == 'User comments': return '' return ret def get_file_comment(self): fmd = FileMetadata(self.image_path + FILE_METADATA_MEDIA_SUFFIX) return fmd.contents() def get_comment(self): try: ret = self.get_file_comment() if ret is None: ret = self.get_exif_usercomment() if ret == '': raise ValueError except (ValueError, KeyError): try: ret = self._metadata['Exif.Image.ImageDescription'] ret = self._fallback_to_encoding(ret) except (ValueError, KeyError): try: ret = self._metadata['Iptc.Application2.ObjectName'] ret = self._fallback_to_encoding(ret) except (ValueError, KeyError): ret = self.get_jpeg_comment() return ret def get_flash(self): try: flash_info = self._metadata.get_tag_interpreted_string('Exif.Photo.Flash') return self._fallback_to_encoding(flash_info) except (ValueError, KeyError): return '' def get_exposure(self): try: return str( self._metadata.get_exif_tag_rational('Exif.Photo.ExposureTime')) except (ValueError, KeyError): return '' def get_iso(self): try: return self._metadata['Exif.Photo.ISOSpeedRatings'] except KeyError: return '' def get_fnumber(self): try: val = float(self._metadata.get_exif_tag_rational('Exif.Photo.FNumber')) except (KeyError, TypeError): return '' else: return 'f/{}'.format(val) def get_focal_length(self): try: flen = self._metadata.get_exif_tag_rational('Exif.Photo.FocalLength') except KeyError: return '' else: flen = '%s mm' % flen try: flen35 = self._metadata.get_exif_tag_rational('Exif.Photo.FocalLengthIn35mmFilm') except KeyError: pass else: flen += _(' (35 mm equivalent: %s mm)') % flen35 return flen try: try: iwidth = self.get_exif_float_value('Exif.Photo.ImageWidth') except (IndexError, KeyError): iwidth = self.get_exif_float_value('Exif.Photo.PixelXDimension') fresunit = self._metadata['Exif.Photo.FocalPlaneResolutionUnit'] factors = {'1': 25.4, '2': 25.4, '3': 10, '4': 1, '5': 0.001} try: fresfactor = factors[fresunit] except IndexError: fresfactor = 0 fxres = self.get_exif_float_value('Exif.Photo.FocalPlaneXResolution') try: ccdwidth = float(iwidth * fresfactor / fxres) except ZeroDivisionError: return '' foclength = self.get_exif_float_value('Exif.Photo.FocalLength') try: lenstr = '%.01f' % (foclength / ccdwidth * 36 + 0.5) except ZeroDivisionError: raise ValueError flen += _(' (35 mm equivalent: %s mm)') % lenstr except (IndexError, KeyError, ValueError): return flen return flen def get_jpeg_comment(self): try: comment = self._metadata.get_comment() if comment is None or '\x00' in comment: raise ValueError # ignore missing or broken JPEG comments return self._fallback_to_encoding(comment.strip(' ')) except ValueError: return '' def get_authorship(self): try: author = self._metadata['Exif.Image.Artist'] return self._fallback_to_encoding(author) except KeyError: return '' def get_keywords(self): """ Returns all the image tags in a list. Try to find the maximum number of keywords. Photo applications store keywords in various places. For a comprehensive list, see http://redmine.yorba.org/projects/shotwell/wiki/PhotoTags """ kw = list() for key in ('Iptc.Application2.Keywords', 'Xmp.MicrosoftPhoto.LastKeywordXMP', 'Xmp.dc.subject', 'Xmp.digiKam.TagsList', ): try: values = self._metadata.get_tag_multiple(key) except KeyError: pass else: for value in values: kw.append(self._fallback_to_encoding(value)) # FIXME # Reading the metadata Xmp.lr.hierarchicalSubject produces error # messages: # "No namespace info available for XMP prefix `lr'" #kw += self._metadata.get_tag_multiple('Xmp.lr.hierarchicalSubject') #remove duplicates kw = set(kw) return kw class NoMetadata(Exception): """ Exception indicating that no meta data has been found. """ pass class DirectoryMetadata(make.GroupTask): def __init__(self, dir_path): super(DirectoryMetadata, self).__init__() self.dir_path = dir_path self.add_file_dependency(self.dir_path) self.description_filename = os.path.join(self.dir_path, MATEW_METADATA) if os.path.isfile(self.description_filename): self.description_file = self.description_filename self.add_file_dependency(self.description_filename) else: self.description_file = None # Add dependency to "file metadata" files if they exist. for file_md_fn in FILE_METADATA: file_md_path = os.path.join(self.dir_path, file_md_fn) if os.path.isfile(file_md_path): self.add_file_dependency(file_md_path) def get_matew_metadata(self, metadata, subdir=None): """ Return dictionary with meta data parsed from Matew like format. """ if subdir is None: path = self.description_file else: path = os.path.join(self.dir_path, subdir, MATEW_METADATA) if path is None or not os.path.exists(path): raise NoMetadata(_('Could not open metadata file %s') % path) f = file(path, 'r') for line in f: for tag in MATEW_TAGS.keys(): tag_text = MATEW_TAGS[tag] tag_len = len(tag_text) if line[:tag_len] == tag_text: data = line[tag_len:] data = data.strip() # Strip quotes if data[0] == '"': data = data[1:] if data[-1] == '"': data = data[:-1] data = data.decode(FILE_METADATA_ENCODING) if tag == 'album_picture': if subdir is not None: data = os.path.join(subdir, data) data = os.path.join(self.dir_path, data) metadata[tag] = data break return metadata def get_file_metadata(self, metadata, subdir=None): """ Returns the file metadata that could be found in the directory. """ if subdir is None: subdir = self.dir_path if 'album_name' not in metadata.keys(): fmd = FileMetadata(os.path.join(subdir, 'album-name')).contents() if fmd is not None: metadata['album_name'] = fmd if 'album_description' not in metadata.keys(): fmd = FileMetadata(os.path.join(subdir, 'album-description')).contents() if fmd is not None: metadata['album_description'] = fmd if 'album_picture' not in metadata.keys(): fmd = FileMetadata(os.path.join(subdir, 'album-picture')).contents(splitter='\n') if fmd is not None: metadata['album_picture'] = os.path.join(subdir, fmd[0]) return metadata def get(self, subdir=None, dir=None): """ Returns directory meta data. First tries to parse known formats and then fall backs to built in defaults. """ result = {} try: result = self.get_matew_metadata(result, subdir) except NoMetadata: pass result = self.get_file_metadata(result, subdir) # Add album picture if 'album_picture' not in result: try: if dir is not None: picture = dir.get_all_medias_paths()[0] else: raise IndexError except IndexError: picture = None if picture is not None: result['album_picture'] = picture return result def get_title(self): try: return self.get()['album_name'] except KeyError: return os.path.basename(self.dir_path).replace('_', ' ') class DefaultMetadata(make.FileMakeObject): """ This is a the building of the default metadata file in the source directory. """ def __init__(self, source_dir, album): self.source_dir = source_dir metadata_path = os.path.join(self.source_dir.path, MATEW_METADATA) super(DefaultMetadata, self).__init__(metadata_path) self.album = album def build(self): md = DirectoryMetadata(self.source_dir.path) md_data = md.get(None, self.source_dir) if 'album_description' in md_data.keys()\ or 'album_name' in md_data.keys(): logging.debug(_(" SKIPPED because metadata exists.")) elif self.source_dir.get_all_medias_count() < 1: logging.debug(_(" SKIPPED because directory does not contain images.")) else: self.generate(md_data) def generate(self, md): """ Generates new metadata file with default values. """ logging.info(_("GEN %s") % self._path) f = file(self._path, 'w') f.write(codecs.BOM_UTF8) f.write('# Directory metadata for lazygal, Matew format\n') f.write('Album name "%s"\n' % self.source_dir.human_name.encode('utf-8')) f.write('Album description ""\n') f.write('Album image identifier "%s"\n' % md['album_picture'].encode('utf-8')) f.close() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/eyecandy.py0000644000175000017500000001373212301071671016732 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import random from PIL import Image, ImageChops, ImageFilter class Color: TRANSPARENT = (0, 0, 0, 0) class PictureMess: STEP = 5 THUMB_HOW_MANY = 5 THUMB_MAX_ROTATE_ANGLE = 40 THUMB_WHITE_WIDTH = 5 def __init__(self, images_paths, top_image_path=None, bg='transparent', result_size=(200, 150, )): if len(images_paths) > self.THUMB_HOW_MANY: self.images_paths = random.sample(images_paths, self.THUMB_HOW_MANY) else: self.images_paths = images_paths if top_image_path: # Put the top image as the last in the list (the top of the image # stack). if top_image_path in self.images_paths: self.images_paths.remove(top_image_path) elif len(self.images_paths) >= self.THUMB_HOW_MANY: self.images_paths.pop() self.images_paths.append(top_image_path) self.bg = bg != 'transparent' and bg or Color.TRANSPARENT self.result_size = result_size self.thumb_size = [3 * max(self.result_size) // 5 for i in range(2)] self.picture_mess = None def __build_mess_thumb(self, image_path): img = Image.open(image_path) img.thumbnail(self.thumb_size, Image.ANTIALIAS) white_size = [x + 2 * self.THUMB_WHITE_WIDTH for x in img.size] white = Image.new('RGB', white_size, 'white') white.paste(img, (self.THUMB_WHITE_WIDTH, self.THUMB_WHITE_WIDTH)) maxi = 2 * max(white_size) thumb = Image.new('RGBA', (maxi, maxi)) thumb.paste(white, ((maxi - white_size[0]) // 2, (maxi - white_size[1]) // 2)) rotation = random.randint(-self.THUMB_MAX_ROTATE_ANGLE, self.THUMB_MAX_ROTATE_ANGLE) thumb = thumb.rotate(rotation, resample=Image.BILINEAR) thumb = thumb.crop(thumb.getbbox()) thumb.thumbnail(self.thumb_size, Image.ANTIALIAS) return thumb def __rand_coord_with_step(self, coord, holding_coord): return random.randint(0 + self.STEP, holding_coord - coord - self.STEP) def __place_thumb_box(self, thumb): x_to_fit = self.__rand_coord_with_step(thumb.size[0], self.picture_mess.size[0]) y_to_fit = self.__rand_coord_with_step(thumb.size[1], self.picture_mess.size[1]) return (x_to_fit, y_to_fit) def __paste_img_to_mess_top(self, img, pos): # http://www.mail-archive.com/image-sig@python.org/msg03387.html img_without_alpha = img.convert('RGB') if self.picture_mess.mode == 'RGBA': invert_alpha = ImageChops.invert( Image.merge('L', self.picture_mess.split()[3:])) if invert_alpha.size != img.size: w, h = img.size box = pos + (pos[0] + w, pos[1] + h) invert_alpha = invert_alpha.crop(box) else: invert_alpha = None self.picture_mess.paste(img_without_alpha, pos, img) if invert_alpha: dest_alpha = Image.merge('L', self.picture_mess.split()[3:]) self.picture_mess.paste(img_without_alpha, pos, invert_alpha) self.picture_mess.putalpha(dest_alpha) def __add_img_to_mess_top(self, img): self.__paste_img_to_mess_top(img, self.__place_thumb_box(img)) def __build_picture_mess(self): self.picture_mess = Image.new("RGBA", self.result_size) added_one_thumb = False for image_path in self.images_paths: try: mess_thumb = self.__build_mess_thumb(image_path) except IOError: # Do not add this thumb to the picture mess pass else: self.__add_img_to_mess_top(mess_thumb) added_one_thumb = True if not added_one_thumb: raise ValueError("No readable image found in submitted list.") def write(self, output_file): self.__build_picture_mess() shadow = Image.new('RGBA', self.picture_mess.size, self.bg) shadow.paste('black', None, self.picture_mess) shadow = shadow.filter(ImageFilter.BLUR) tmp = self.picture_mess self.picture_mess = shadow self.__paste_img_to_mess_top(tmp, (0, 0)) self.picture_mess.save(output_file) class PictureTidy(PictureMess): THUMB_MAX_ROTATE_ANGLE = 0 WEBALBUMPIC_TYPES = { 'messy': PictureMess, 'tidy' : PictureTidy, } if __name__ == '__main__': from optparse import OptionParser parser = OptionParser() parser.add_option("-s", "--seed", type="int", dest="seed") parser.add_option("-t", "--type", dest="type", action="store_true", default=False) parser.add_option("-b", "--background", type="string", dest="color", default="transparent") (options, args) = parser.parse_args() if options.seed: random.seed(options.seed) MultipicRepr = PictureMess if options.type: MultipicRepr = WEBALBUMPIC_TYPES[options.type] MultipicRepr(args[0:], bg=options.color).write('test.png') # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/newsize.py0000644000175000017500000001377012301071671016617 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import types import re import math class NewsizeStringParseError(Exception): pass class _Newsize(object): def __init__(self, resize_string): self.resize_string = resize_string def matches(self): match = re.match(self.regexp, self.resize_string) if match: return match else: raise NewsizeStringParseError def dest_size(self, orig_size): raise NotImplementedError class Scale(_Newsize): regexp = "^(?P\\d+)%$" def dest_size(self, orig_size): match = self.matches() scale = int(match.group('scale')) return tuple(map(lambda x: x * scale // 100, orig_size)) class XYScale(_Newsize): regexp = "^(?P\\d+)%(?P\\d+)%$" def dest_size(self, orig_size): match = self.matches() xscale = int(match.group('xscale')) yscale = int(match.group('yscale')) x, y = orig_size return (x * xscale // 100, y * yscale // 100) class Width(_Newsize): regexp = "^(?P\\d+)$" def dest_size(self, orig_size): match = self.matches() width = int(match.group('width')) x, y = orig_size height = y * width // x return (width, height) class Height(_Newsize): regexp = "^x(?P\\d+)$" def dest_size(self, orig_size): match = self.matches() height = int(match.group('height')) x, y = orig_size width = x * height // y return (width, height) class _WidthHeight(_Newsize): def requested_widthheight(self): match = self.matches() width = int(match.group('width')) height = int(match.group('height')) return width, height def appropriate_widthheight(self, orig_size, width, height, constraint): x, y = orig_size new_height = y * width // x if constraint(new_height, height): return (width, new_height) else: new_width = x * height // y # y * width / x >= height, # therefore x * height / y = new_width <= width # with contraint being '<='. assert constraint(new_width, width) return (new_width, height) class MaximumWidthHeight(_WidthHeight): regexp = "^(?P\\d+)x(?P\\d+)$" def dest_size(self, orig_size): width, height = self.requested_widthheight() return self.appropriate_widthheight(orig_size, width, height, lambda x, y: x <= y) class MinimumWidthHeight(_WidthHeight): regexp = "^(?P\\d+)x(?P\\d+)\\^$" def dest_size(self, orig_size): width, height = self.requested_widthheight() return self.appropriate_widthheight(orig_size, width, height, lambda x, y: x >= y) class MandatoryWidthHeight(_WidthHeight): regexp = "^(?P\\d+)x(?P\\d+)!$" def dest_size(self, orig_size): width, height = self.requested_widthheight() return (width, height) class WidthHeightIfLarger(_WidthHeight): regexp = "^(?P\\d+)x(?P\\d+)\\>$" def dest_size(self, orig_size): width, height = self.requested_widthheight() x, y = orig_size if x > width or y > height: return self.appropriate_widthheight(orig_size, width, height, lambda x, y: x <= y) else: return orig_size class WidthHeightIfSmaller(_WidthHeight): regexp = "^(?P\\d+)x(?P\\d+)\\<$" def dest_size(self, orig_size): width, height = self.requested_widthheight() x, y = orig_size if x < width and y < height: return self.appropriate_widthheight(orig_size, width, height, lambda x, y: x >= y) else: return orig_size class Area(_Newsize): regexp = "^(?P\\d+)@$" def dest_size(self, orig_size): match = self.matches() area = int(match.group('area')) x, y = orig_size # { x0 * y0 = area # { x / x0 = y / y0 # x * y0 / area = y / y0 # y0 = sqrt( y * area / x ) # x0 = sqrt( x * area / y ) return (int(math.sqrt(x * area // y)), int(math.sqrt(y * area // x))) resize_patterns = [] for name, obj in globals().items(): if not name.startswith('_')\ and isinstance(obj, (type, types.ClassType))\ and issubclass(obj, _Newsize): resize_patterns.append(obj) def get_newsizer(resize_string): for newsizer_class in resize_patterns: newsizer = newsizer_class(resize_string) try: newsizer.matches() except NewsizeStringParseError: # This is not the syntax used pass else: return newsizer raise NewsizeStringParseError def is_known_newsizer(resize_string): for newsizer_class in resize_patterns: try: newsizer_class(resize_string).matches() except NewsizeStringParseError: # This is not the syntax used pass else: return True raise NewsizeStringParseError # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/genfile.py0000644000175000017500000001006212301071671016533 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import locale import logging import zipfile import make import pathutils class WebalbumFile(make.FileMakeObject): def __init__(self, path, dir): super(WebalbumFile, self).__init__(path) self.dir = dir self.path = path def rel_path(self, dir, url=False): """ Returns the path of the current object relative to the supplied dir object argument. Force forward slashes if url is True. """ ret = None if dir is None or dir is self.dir: ret = os.path.basename(self.path) else: ret = dir.rel_path(self.path) if url: return pathutils.url_path(ret) else: return ret class MediaOriginal(WebalbumFile): def __init__(self, dir, source_media): self.filename = source_media.filename path = os.path.join(dir.path, self.filename) super(MediaOriginal, self).__init__(path, dir) self.source_media = source_media self.set_dep_only() def get_size(self): return self.source_media.get_size() class CopyMediaOriginal(MediaOriginal): def __init__(self, dir, source_media): super(CopyMediaOriginal, self).__init__(dir, source_media) self.add_dependency(make.FileCopy(self.source_media.path, self.path)) def build(self): logging.info(" CP %s" % self.filename) logging.debug("(%s)" % self.path) class SymlinkMediaOriginal(MediaOriginal): def __init__(self, dir, source_media): super(SymlinkMediaOriginal, self).__init__(dir, source_media) self.add_dependency(make.FileSymlink(self.source_media.path, self.path)) def build(self): logging.info(" SYMLINK %s" % self.filename) logging.debug("(%s)" % self.path) class WebalbumArchive(WebalbumFile): def __init__(self, webgal_dir): self.path = os.path.join(webgal_dir.path, webgal_dir.source_dir.name + '.zip') WebalbumFile.__init__(self, self.path, webgal_dir) self.add_dependency(self.dir.source_dir) self.pics = map(lambda x: os.path.join(self.dir.source_dir.path, x.media.filename), self.dir.medias) for pic in self.pics: self.add_file_dependency(pic) def build(self): zip_rel_path = self.rel_path(self.dir.flattening_dir) logging.info(_(" ZIP %s") % zip_rel_path) logging.debug("(%s)" % self.path) archive = zipfile.ZipFile(self.path, mode='w') for pic in self.pics: inzip_filename = os.path.join(self.dir.source_dir.name, os.path.basename(pic)) # zipfile dislikes unicode inzip_fn = inzip_filename.encode(locale.getpreferredencoding()) archive.write(pic, inzip_fn) archive.close() def size(self): return os.path.getsize(self.path) class SharedFileCopy(make.FileCopy): def __init__(self, src, dst): make.FileCopy.__init__(self, src, dst) def build(self): logging.info(_("CP %%SHAREDDIR%%/%s") % os.path.basename(self.dst)) logging.debug("(%s)" % self.dst) make.FileCopy.build(self) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/make.py0000644000175000017500000001720212301071671016042 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import time import shutil class CircularDependency(Exception): pass class MakeTask(object): """ A simple task that remembers the last time it was built. """ def __init__(self): self.deps = [] self.output_items = [] self.stamp_delete() self.__dep_only = False self.update_build_status() def add_dependency(self, dependency): if self in dependency.deps: raise CircularDependency("%s <-> %s" % (self, dependency)) self.deps.append(dependency) for output_item in dependency.output_items: self.register_output(output_item) def add_file_dependency(self, file_path): self.add_dependency(FileSimpleDependency(file_path)) def get_mtime(self): return self.__last_build_time def set_dep_only(self): """ Set this task to being only used as an intermediate work, its output is not required in the end. It won't be taken into account when computing whether a depending task should be built, even if it is older. But make() will be called if the depending task is built. """ self.__dep_only = True def is_dep_only(self): return self.__dep_only def stamp_build(self, build_time=None): if not build_time: build_time = time.time() self.__last_build_time = build_time self.__built_once = True def stamp_delete(self): self.__last_build_time = -1 # older than oldest epoch self.__built_once = False def built_once(self): return self.__built_once def update_build_status(self): """ Do stuff to update the build status (e.g. probe filesystem, deps). """ pass def needs_build(self, return_culprit=False): if not self.built_once(): if return_culprit: return 'never built' else: return True for dependency in self.deps: if dependency.get_mtime() > self.get_mtime()\ or dependency.needs_build(): if not dependency.is_dep_only(): if return_culprit: mtime_gap = dependency.get_mtime() - self.get_mtime() if mtime_gap > 0: reason = 'dep newer by %s s' % mtime_gap elif dependency.needs_build(): reason = dependency.needs_build(True) else: raise RuntimeError # should never go here return dependency, reason else: return True return False def make(self, force=False): if force or self.needs_build(): for d in self.deps: d.make() # dependency building not forced self.call_build() def call_build(self): """ This method is really simple in this implementation, but it can be overridden with more complicated things in subclasses. The purpose is to setup some state before and/or after build. """ try: self.build() except KeyboardInterrupt: self.clean_output() raise self.stamp_build() def build(self): """ This method should be implemented in subclasses to define what the task should do. """ raise NotImplementedError def register_output(self, output): """ This provides a facility to register within the makefile machinery what items are built from the task. """ self.output_items.append(output) def clean_output(self): """ Clean-up in case of interruption (KeyboardInterrupt). """ pass def print_dep_entry(self, level=0): indent = '' for index in range(0, level): indent = indent + '\t' print indent, self, self.get_mtime() def print_dep_tree(self, depth=1, parent_level=-1): level = parent_level + 1 if level > depth: return self.print_dep_entry(level) for d in self.deps: d.print_dep_tree(depth, level) class GroupTask(MakeTask): """ A class that builds nothing but groups subtasks. """ def built_once(self): return True # GroupTask is all about the deps. def update_build_status(self): super(GroupTask, self).update_build_status() # Find youngest dep, which should indicate latest build. mtime = None for dependency in self.deps: if dependency.built_once(): new_mtime = dependency.get_mtime() if mtime is None or new_mtime > mtime: mtime = new_mtime if mtime is None: self.stamp_delete() else: self.stamp_build(mtime) def add_dependency(self, dependency): super(GroupTask, self).add_dependency(dependency) dep_mtime = dependency.get_mtime() if dep_mtime > self.get_mtime(): self.stamp_build(dep_mtime) def build(self): pass class FileMakeObject(MakeTask): def __init__(self, path): self._path = path super(FileMakeObject, self).__init__() self.register_output(self._path) def update_build_status(self): super(FileMakeObject, self).update_build_status() # Update build info according to file existence if os.path.exists(self._path): self.stamp_build(os.path.getmtime(self._path)) else: self.stamp_delete() def clean_output(self): if os.path.lexists(self._path): os.unlink(self._path) def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self._path.encode('utf-8')) class FileSimpleDependency(FileMakeObject): """ Simple file dependency that needn't build. It just should be there. """ def __init__(self, path): super(FileSimpleDependency, self).__init__(path) assert self.built_once() def build(self): pass def clean_output(self): pass class FileCopy(FileMakeObject): """ Simple file copy make target. """ def __init__(self, src, dst): self.src = src self.dst = dst FileMakeObject.__init__(self, dst) self.add_file_dependency(self.src) def build(self): shutil.copy(self.src, self.dst) class FileSymlink(FileMakeObject): """ Simple file symlink make target. """ def __init__(self, src, dst): self.src = src self.dst = dst FileMakeObject.__init__(self, dst) self.add_file_dependency(self.src) def build(self): if os.path.islink(self.dst): os.remove(self.dst) os.symlink(self.src, self.dst) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/defaults.conf0000644000175000017500000000116312301071671017230 0ustar niolniol00000000000000[runtime] quiet = No debug = No check-all-dirs = No [global] output-directory = . force-gen-pages = No clean-destination = No dir-flattening-depth = No puburl = No theme = default [webgal] default-style = default webalbumpic-bg = transparent webalbumpic-type = messy webalbumpic-size = 200x150 image-size = small=800x600,medium=1024x768 thumbnail-size = 150x113 video-size = 0x0 thumbs-per-page = 0 filter-by-tag = sort-medias = exif sort-subgals = dirname original = No original-baseurl = No original-symlink = No dirzip = No jpeg-quality = 85 jpeg-optimize = Yes jpeg-progressive = Yes publish-metadata = Yes keep-gps = No lazygal-0.8.2/lazygal/genpage.py0000644000175000017500000003446412301071671016544 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import posixpath import sys import logging import urllib import genshi import make import pathutils import genfile import feeds import tplvars class WebalbumPage(genfile.WebalbumFile): def __init__(self, dir, size_name, base_name): self.dir = dir self.size_name = size_name page_filename = self._add_size_qualifier(base_name + '.html', self.size_name) self.page_path = os.path.join(dir.path, page_filename) genfile.WebalbumFile.__init__(self, self.page_path, dir) self.page_template = None def set_template(self, tpl_ident): self.page_template = self.load_tpl(tpl_ident) def load_tpl(self, tpl_ident): tpl = self.dir.album.theme.tpl_loader.load(tpl_ident) self.add_file_dependency(tpl.path) for subtpl in tpl.subtemplates(): self.add_file_dependency(subtpl.path) return tpl def init_tpl_values(self): tpl_values = {} tpl_values.update(self.dir.tpl_vars) return tpl_values def _get_osize_links(self, filename): osize_index_links = [] for osize_name in self.dir.browse_sizes: osize_info = {} if osize_name == self.size_name: # No link if we're on the current page osize_info['name'] = osize_name else: osize_info['name'] = osize_name osize_info['link'] = self._add_size_qualifier(filename + '.html', osize_name) osize_info['link'] = self.url_quote(osize_info['link']) osize_index_links.append(osize_info) return osize_index_links def _add_size_qualifier(self, path, size_name=None): if size_name is None: size_name = self.size_name return self.dir._add_size_qualifier(path, size_name) def _do_not_escape(self, value): return genshi.core.Markup(value) def url_quote(self, url): return urllib.quote(url.encode(sys.getfilesystemencoding()), safe=':/#') UNIT_PREFIXES = (('T', 2 ** 40), ('G', 2 ** 30), ('M', 2 ** 20), ('K', 2 ** 10),) def format_filesize(self, size_bytes): for unit_prefix, limit in self.UNIT_PREFIXES: if size_bytes >= limit: return '%.1f %siB'\ % (round(float(size_bytes) / limit, 1), unit_prefix) return '%.1f B' % size_bytes class WebalbumBrowsePage(WebalbumPage): def __init__(self, dir, size_name, webalbum_media): self.webalbum_media = webalbum_media self.media = self.webalbum_media.media WebalbumPage.__init__(self, dir, size_name, self.media.name) self.add_dependency(self.webalbum_media.resized[size_name]) if webalbum_media.original: self.add_dependency(self.webalbum_media.original) # Depends on source directory in case an image was deleted self.add_dependency(self.dir.source_dir) # Depend on the comment file if it exists. if self.webalbum_media.media.comment_file_path is not None: self.add_file_dependency(self.webalbum_media.media.comment_file_path) self.add_dependency(self.dir.sort_task) self.set_template('browse.thtml') self.load_tpl(self.media.type + '.thtml') def add_extra_vals(self, tpl_values): tpl_values.update(tplvars.media_vars(self, self.webalbum_media).full()) def build(self): page_rel_path = self.rel_path(self.dir.flattening_dir) logging.info(_(" XHTML %s") % page_rel_path) logging.debug("(%s)" % self.page_path) tpl_values = self.init_tpl_values() # Breadcrumbs tpl_values['webgal_path'] = tplvars.Webgal(self, self.dir).path() tpl_values['name'] = self.media.filename tpl_values['mediatype'] = self.media.type tpl_values['dir'] = self.dir.source_dir.strip_root() prev = self.webalbum_media.previous if prev: tpl_values['prev_link'] = tplvars.Media(self, prev).link() next = self.webalbum_media.next if next: tpl_values['next_link'] = tplvars.Media(self, next).link() tpl_values['index_link'] = self._add_size_qualifier('index.html', self.size_name) if self.dir.should_be_flattened(): index_rel_dir = self.dir.flattening_dir.source_dir.rel_path(self.dir.source_dir) tpl_values['index_link'] = index_rel_dir + tpl_values['index_link'] tpl_values['osize_links'] = self._get_osize_links(self.media.name) tpl_values['rel_root'] = pathutils.url_path(self.dir.source_dir.rel_root()) + '/' if self.dir.feed is not None: tpl_values['feed_url'] = os.path.relpath(self.dir.feed.path, self.dir.path) tpl_values['feed_url'] = pathutils.url_path(tpl_values['feed_url']) tpl_values['feed_url'] = self.url_quote(tpl_values['feed_url']) else: tpl_values['feed_url'] = None if self.dir.original: if self.dir.orig_base: tpl_values['original_link'] = posixpath.join( pathutils.url_path(self.dir.source_dir.rel_root()), self.dir.orig_base, pathutils.url_path(self.dir.source_dir.strip_root()), self.media.filename) else: tpl_values['original_link'] = self.media.filename tpl_values['original_link'] =\ self.url_quote(tpl_values['original_link']) self.add_extra_vals(tpl_values) self.page_template.dump(tpl_values, self.page_path) class WebalbumIndexPage(WebalbumPage): FILENAME_BASE_STRING = 'index' def __init__(self, dir, size_name, page_number, subgals, galleries): page_paginated_name = self._get_paginated_name(page_number) WebalbumPage.__init__(self, dir, size_name, page_paginated_name) self.page_number = page_number self.subgals = subgals self.galleries = galleries for dir, medias in self.galleries: self.add_dependency(dir.source_dir.metadata) if dir is not self.dir: dir.flattening_dir = self.dir self.add_dependency(dir.source_dir) self.add_dependency(dir.sort_task) if size_name in dir.browse_sizes: for media in medias: if media.thumb: self.add_dependency(media.thumb) if self.dir.album.theme.kind == 'static': self.add_dependency(media.browse_pages[size_name]) # Ensure dir depends on browse page (usefull for cleanup # checks when dir is flattenend). dir.add_dependency(media.browse_pages[size_name]) else: logging.warning(_(" Size '%s' is not available in '%s' due to configuration: medias won't be shown on index.") % (size_name, dir.path)) if self.dir.dirzip is not None: self.add_dependency(self.dir.dirzip) for subgal in self.subgals: self.add_dependency(subgal.source_dir) if self.dir.album.theme.kind == 'static': self.set_template('dirindex.thtml') elif self.dir.album.theme.kind == 'dynamic': self.set_template('dynindex.thtml') def _get_paginated_name(self, page_number=None): if page_number is None: page_number = self.page_number assert page_number is not None if page_number < 1: return WebalbumIndexPage.FILENAME_BASE_STRING else: return '_'.join([WebalbumIndexPage.FILENAME_BASE_STRING, str(page_number)]) def _get_related_index_fn(self): return self._add_size_qualifier( WebalbumIndexPage.FILENAME_BASE_STRING + '.html', self.size_name) def _get_onum_links(self): onum_index_links = [] for onum in range(0, self.dir.break_task.how_many_pages()): onum_info = {} if onum == self.page_number: # No link if we're on the current page onum_info['name'] = onum else: onum_info['name'] = onum filename = self._get_paginated_name(onum) onum_info['link'] = self._add_size_qualifier(filename + '.html', self.size_name) onum_info['link'] = self.url_quote(onum_info['link']) onum_index_links.append(onum_info) return onum_index_links def _get_subgal_links(self): subgal_links = [] for subgal in self.dir.subgals: dir_info = tplvars.Webgal(self, subgal).link_info() subgal_links.append(dir_info) return subgal_links def build(self): logging.info(_(" XHTML %s") % os.path.basename(self.page_path)) logging.debug("(%s)" % self.page_path) values = self.init_tpl_values() # Breadcrumbs (current is static, see dirindex.thtml, that's why the # last item of the list is removed). values['webgal_path'] = tplvars.Webgal(self, self.dir).path()[:-1] if not self.dir.source_dir.is_album_root(): # Parent index link not for album root values['parent_index_link'] = self._get_related_index_fn() values['osize_index_links'] = self._get_osize_links(self._get_paginated_name()) values['onum_index_links'] = self._get_onum_links() if self.dir.flatten_below(): values['subgal_links'] = [] else: values['subgal_links'] = self._get_subgal_links() values['medias'] = [] for subdir, medias in self.galleries: info = tplvars.Webgal(self, subdir).info() if self.size_name in subdir.browse_sizes: media_links = [tplvars.media_vars(self, media).full() for media in medias] else: # This happens when this dir index size is not available in the # subdir. media_links = [] values['medias'].append((info, media_links, )) values.update(tplvars.Webgal(self, self.dir).info()) values['rel_root'] = pathutils.url_path(self.dir.source_dir.rel_root()) + '/' values['rel_path'] = pathutils.url_path(self.dir.source_dir.strip_root()) if self.dir.feed is not None: values['feed_url'] = os.path.relpath(self.dir.feed.path, self.dir.path) values['feed_url'] = pathutils.url_path(values['feed_url']) values['feed_url'] = self.url_quote(values['feed_url']) else: values['feed_url'] = None self.page_template.dump(values, self.page_path) class WebalbumFeed(make.FileMakeObject): def __init__(self, album, dir_path, pub_url): self.path = os.path.join(dir_path, 'index.xml') super(WebalbumFeed, self).__init__(self.path) self.album = album self.pub_url = pub_url if not self.pub_url: self.pub_url = 'http://example.com' if not self.pub_url.endswith('/'): self.pub_url = self.pub_url + '/' self.feed = feeds.RSS20(self.pub_url) self.item_template = self.album.theme.tpl_loader.load('feeditem.thtml') def set_title(self, title): self.feed.title = title def set_description(self, description): self.feed.description = description def push_dir(self, webalbumdir): if webalbumdir.get_media_count() > 0: self.add_dependency(webalbumdir) self.__add_item(webalbumdir) def __add_item(self, webalbumdir): url = os.path.join(self.pub_url, webalbumdir.source_dir.strip_root()) desc_values = {} desc_values['album_pic_path'] = \ os.path.join(url, webalbumdir.get_webalbumpic_filename()) desc_values['subgal_count'] = webalbumdir.get_subgal_count() desc_values['picture_count'] = webalbumdir.get_media_count('image') desc_values['desc'] = webalbumdir.source_dir.desc desc = self.item_template.instanciate(desc_values) self.feed.push_item(webalbumdir.source_dir.title, url, desc, webalbumdir.source_dir.get_mtime()) def build(self): logging.info(_("FEED %s") % os.path.basename(self.path)) logging.debug("(%s)" % self.path) self.feed.dump(self.path) class SharedFileTemplate(make.FileMakeObject): def __init__(self, album, shared_tpl_name, shared_file_dest_tplname, tpl_vars): self.album = album self.tpl = self.album.theme.tpl_loader.load(shared_tpl_name) self.tpl_vars = tpl_vars # Remove the 't' from the beginning of ext filename, ext = os.path.splitext(shared_file_dest_tplname) if ext.startswith('.t'): self.path = filename + '.' + ext[2:] else: raise ValueError(_('We have a template with an extension that does not start with a t. Aborting.')) make.FileMakeObject.__init__(self, self.path) self.add_file_dependency(shared_tpl_name) def build(self): logging.info(_("TPL %%SHAREDDIR%%/%s") % os.path.basename(self.path)) logging.debug("(%s)" % self.path) self.tpl.dump(self.tpl_vars, self.path) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/sourcetree.py0000644000175000017500000003164412301071671017313 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import time import locale import logging from PIL import Image from lazygal import pathutils, make, metadata from lazygal import mediautils, timeutils SOURCEDIR_CONFIGFILE = '.lazygal' class File(make.FileSimpleDependency): def __init__(self, path, album): make.FileSimpleDependency.__init__(self, path) self.path = pathutils.path2unicode(path) self.album = album self.filename = os.path.basename(self.path) self.name, self.extension = os.path.splitext(self.filename) def strip_root(self, path=None): if not path: path = self.path relative_path = os.path.relpath(path, self.album.source_dir) if relative_path == '.': return '' else: return relative_path def rel_root(self): return os.path.relpath(self.album.source_dir, self.path) def rel_path(self, from_dir, path=None): try: from_dir = from_dir.path except AttributeError: pass if path is None: path = self.path return os.path.relpath(path, from_dir) def is_subdir_of(self, dir, path=None): if path is None: path = self.path try: dir_path = dir.path except AttributeError: dir_path = dir return pathutils.is_subdir_of(dir_path, path) def get_album_level(self, path=None): if path is None: path = self.path if os.path.isdir(path): cur_path = path else: cur_path = os.path.dirname(path) album_level = 0 while cur_path != self.album.source_dir: cur_path, tail = os.path.split(cur_path) album_level += 1 if pathutils.is_root(cur_path): raise RuntimeError(_('Root not found')) return album_level SKIPPED_DIRS = ('.svn', '_darcs', '.bzr', '.git', '.hg', 'CVS', ) def should_be_skipped(self): head = self.strip_root() while head != '': head, tail = os.path.split(head) if tail in File.SKIPPED_DIRS: return True return False def get_datetime(self): return timeutils.Datetime(self.get_mtime()) def compare_mtime(self, other_file): return int(self.get_mtime() - other_file.get_mtime()) def compare_filename(self, other_file): return locale.strcoll(self.filename, other_file.filename) class MediaFile(File): def __init__(self, path, album): File.__init__(self, path, album) self.broken = False comment_file_path = self.path + metadata.FILE_METADATA_MEDIA_SUFFIX if os.path.isfile(comment_file_path): self.comment_file_path = comment_file_path else: self.comment_file_path = None def compare_date_taken(self, other_img): date1 = self.get_date_taken().timestamp date2 = other_img.get_date_taken().timestamp delta = date1 - date2 return int(delta) def compare_no_reliable_date(self, other_img): # Comparison between 'no EXIF' and 'EXIF' sorts EXIF after # (reliable here means encoded by the camera). if self.has_reliable_date(): return 1 else: return -1 def compare_to_sort(self, other_media): if self.has_reliable_date() and other_media.has_reliable_date(): return self.compare_date_taken(other_media) elif not self.has_reliable_date()\ and not other_media.has_reliable_date(): return self.compare_filename(other_media) else: # One of the picture has no EXIF date, so we arbitrary sort it # before the one with EXIF. return self.compare_no_reliable_date(other_media) class ImageFile(MediaFile): type = 'image' def __init__(self, path, album): MediaFile.__init__(self, path, album) self.date_taken = None self.reliable_date = None self.__date_probed = False def info(self): if self.broken: return None try: exif = metadata.ImageInfoTags(self.path) except IOError: exif = None self.broken = True else: self.reliable_date = timeutils.Datetime(datetime=exif.get_date()) self.__date_probed = True return exif def get_size(self, img_path=None): if not img_path: img_path = self.path try: im = Image.open(img_path) except IOError: self.broken = True return (None, None) else: return im.size def has_reliable_date(self): if not self.__date_probed: self.info() if self.reliable_date: return True else: return False def get_date_taken(self): if not self.__date_probed: self.info() if self.reliable_date: self.date_taken = self.reliable_date else: # No date available in EXIF, or bad format, use file mtime self.date_taken = self.get_datetime() return self.date_taken class VideoFile(MediaFile): type = 'video' def get_size(self): inspector = mediautils.GstVideoInfo(self.path) inspector.inspect() if inspector.broken is True: self.broken = True return (None, None) return inspector.videowidth, inspector.videoheight def has_reliable_date(self): return False def get_date_taken(self): return self.get_datetime() def info(self): return None class MediaHandler(object): FORMATS = {'.jpeg': ImageFile, '.jpg' : ImageFile, '.png' : ImageFile, '.mov' : VideoFile, '.avi' : VideoFile, '.mp4' : VideoFile, '.3gp' : VideoFile, '.webm': VideoFile, } def __init__(self, album): self.album = album @staticmethod def is_known_media(path): filename, extension = os.path.splitext(path) extension = extension.lower() return extension in MediaHandler.FORMATS.keys() def get_media(self, path): filename, extension = os.path.splitext(path) extension = extension.lower() if extension in MediaHandler.FORMATS.keys(): media_class = MediaHandler.FORMATS[extension] if media_class == VideoFile and not mediautils.HAVE_GST: return None return media_class(path, self.album) else: return None class Directory(File): def __init__(self, source, subdirs, filenames, album): File.__init__(self, source, album) # No breaking up of filename and extension for directories self.name = self.filename self.extension = None self.subdirs = subdirs self.filenames = map(pathutils.path2unicode, filenames) self.human_name = self.album._str_humanize(self.name) media_handler = MediaHandler(self.album) self.medias = [] self.medias_names = [] for filename in self.filenames: media_path = os.path.join(self.path, filename) if not os.path.isfile(media_path): logging.info(_(" Ignoring %s, cannot open file (broken symlink?).") % filename) logging.debug("(%s)" % os.path.join(self.path, filename)) continue media = media_handler.get_media(media_path) if media: self.medias_names.append(filename) self.medias.append(media) elif not self.is_metadata(filename) and\ filename != SOURCEDIR_CONFIGFILE: logging.info(_(" Ignoring %s, format not supported.") % filename) logging.debug("(%s)" % os.path.join(self.path, filename)) self.metadata = metadata.DirectoryMetadata(self.path) md = self.metadata.get(None, self) if 'album_name' in md.keys(): self.title = md['album_name'] else: self.title = self.human_name if 'album_description' in md.keys(): self.desc = md['album_description'] else: self.desc = None if 'album_picture' in md.keys(): self.album_picture = self.rel_path(self, md['album_picture']) else: self.album_picture = None def is_album_root(self): return self.path == pathutils.path2unicode(self.album.source_dir) def parent_paths(self): parent_paths = [self.path] found = False head = self.path while not found: if head == self.album.source_dir: found = True elif pathutils.is_root(head): raise RuntimeError(_("Root not found")) else: head, tail = os.path.split(head) parent_paths.append(os.path.join(self.path, head)) return parent_paths def parents_metadata(self): return map(metadata.DirectoryMetadata, self.parent_paths()) def is_metadata(self, filename): if filename == metadata.MATEW_METADATA: return True if filename in metadata.FILE_METADATA: return True # Check for media metadata related_media = filename[:-len(metadata.FILE_METADATA_MEDIA_SUFFIX)] # As the list self.medias_names is being constructed while this # check takes place, the following is only reliable if the filenames # list is sorted (thus t.jpg.comment is after t.jpg, and t.jpg is # already in self.medias_names), which is the case. if related_media in self.medias_names: return True return False def get_media_count(self, media_type=None): if media_type is None: return len(self.medias_names) else: typed_media_count = 0 for media in self.medias: if media.type == media_type: typed_media_count += 1 return typed_media_count def get_all_medias_count(self, media_type=None): all_medias_count = self.get_media_count(media_type) for subdir in self.subdirs: all_medias_count += subdir.get_all_medias_count(media_type) return all_medias_count def get_all_medias(self): all_medias = list(self.medias) # We want a copy here. for subdir in self.subdirs: all_medias.extend(subdir.get_all_medias()) return all_medias def get_all_medias_paths(self): return [m.path for m in self.get_all_medias()] def get_all_subdirs(self): all_subdirs = list(self.subdirs) # We want a copy here. for subdir in self.subdirs: all_subdirs.extend(subdir.get_all_subdirs()) return all_subdirs def latest_media_stamp(self, hint=None): """ Returns the latest media date: - first considering all pics that have an EXIF date - if none have a reliable date, use file mtimes. `hint` stops processing if one of the found values is higher. This is to speed up compare_latest_exif(). """ all_medias = self.get_all_medias() media_stamp_max = None for m in all_medias: if m.has_reliable_date(): media_stamp = m.get_date_taken().timestamp if media_stamp_max is None or media_stamp > media_stamp_max: media_stamp_max = media_stamp if hint is not None and media_stamp_max > hint: return media_stamp_max if media_stamp_max is None: # none of the media had a reliable date, use mtime instead for m in all_medias: media_stamp = time.mktime(m.get_mtime().timetuple()) if media_stamp_max is None or media_stamp > media_stamp_max: media_stamp_max = media_stamp return media_stamp_max def compare_latest_exif(self, other_gallery): date1 = self.latest_media_stamp() date2 = other_gallery.latest_media_stamp(date1) return int(date1 - date2) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/config.py0000644000175000017500000001117312301071671016373 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2011-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import logging import ConfigParser class BetterConfigParser(ConfigParser.RawConfigParser): def getint(self, section, option): try: if not self.getboolean(section, option): return False else: raise ValueError except (ValueError, AttributeError): return ConfigParser.RawConfigParser.getint(self, section, option) def getstr(self, section, option): try: if not self.getboolean(section, option): return False else: raise ValueError except (ValueError, AttributeError): return ConfigParser.RawConfigParser.get(self, section, option) def getlist(self, section, option): str_vlist = self.get(section, option) if str_vlist == '': return [] if type(str_vlist) is not list: str_vlist = str_vlist.split(',') # handle the case several vals were given at once (separated by comas) vlist = [] for v in str_vlist: multiple_v = v.split(',') for mv in multiple_v: vlist.append(mv) return vlist def load(self, other_config, init=False, sections=None): """ Take another configuration object and overload values in this config object. """ all_sections = False if sections is None: sections = other_config.sections() all_sections = True for section in sections: if all_sections or other_config.has_section(section): if not self.has_section(section): if not init: self.new_section_cb(section) self.add_section(section) for option in other_config.options(section): if not init and not self.has_option(section, option): self.new_option_cb(section, option) self.set(section, option, other_config.get(section, option)) def new_section_cb(self, section): pass def new_option_cb(self, section, option): pass USER_CONFIG_PATH = os.path.expanduser('~/.lazygal/config') DEFAULT_CONFIG = BetterConfigParser() DEFAULT_CONFIG.readfp(open(os.path.join(os.path.dirname(__file__), 'defaults.conf'))) class LazygalConfigDeprecated(BaseException): pass class LazygalConfig(BetterConfigParser): def __init__(self): BetterConfigParser.__init__(self) self.load(DEFAULT_CONFIG, init=True) def check_deprecation(self, config=None): if config is None: config = self if config.has_section('lazygal'): raise LazygalConfigDeprecated("'lazygal' section is deprecated") def read(self, filenames): conf = BetterConfigParser() conf.read(filenames) self.load(conf) self.check_deprecation() def load(self, other_config, init=False, sections=None): self.check_deprecation(other_config) BetterConfigParser.load(self, other_config, init, sections) def new_section_cb(self, section): if section != 'template-vars': logging.warning(_(" Ignoring unknown section '%s'.") % section) def new_option_cb(self, section, option): if section != 'template-vars': logging.warning(_(" Ignoring unknown option '%s' in section '%s'.") % (option, section, )) class LazygalWebgalConfig(LazygalConfig): def __init__(self, global_config): LazygalConfig.__init__(self) LazygalConfig.load(self, global_config, init=True) def load(self, other_config, init=False): LazygalConfig.load(self, other_config, init, sections=('webgal', 'template-vars', )) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/feeds.py0000644000175000017500000000671112301071671016216 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import email.utils import os import sys import urllib from xml.etree import cElementTree as ET class RSS20: def __init__(self, link, maxitems=10): self.items = [] self.title = None self.description = None self.link = link self.__maxitems = maxitems def __get_root_and_channel(self, feed_filename): root = ET.Element('rss', {'version' : '2.0', 'xmlns:atom': 'http://www.w3.org/2005/Atom'}) channel = ET.SubElement(root, 'channel') ET.SubElement(channel, 'title').text = self.title ET.SubElement(channel, 'link').text = self.__url_quote(self.link) ET.SubElement(channel, 'description').text = self.description ET.SubElement(channel, 'atom:link', {'href': self.link + feed_filename, 'rel' : 'self', 'type': 'application/rss+xml'}) return root, channel def __url_quote(self, url): return urllib.quote(url.encode(sys.getfilesystemencoding()), safe=':/') def __item_older(self, x, y): return int(y['timestamp'] - x['timestamp']) def push_item(self, title, link, contents, timestamp): item = {} item['title'] = title item['link'] = link item['contents'] = contents item['timestamp'] = timestamp i = 0 while i < len(self.items)\ and self.__item_older(item, self.items[i]) > 0: i = i + 1 if i < self.__maxitems: # We have a candidate self.items.insert(i, item) if len(self.items) > self.__maxitems: # We have one too much, so get rid of it self.items.pop() def dump(self, path): (root, channel) = self.__get_root_and_channel(os.path.basename(path)) pubdate = ET.SubElement(channel, 'pubDate') self.items.sort(lambda x, y: int(x['timestamp'] - y['timestamp'])) for item in self.items: rssitem = ET.SubElement(channel, 'item') ET.SubElement(rssitem, 'title').text = item['title'] ET.SubElement(rssitem, 'link').text = self.__url_quote(item['link']) ET.SubElement(rssitem, 'guid').text = self.__url_quote(item['link']) date = email.utils.formatdate(item['timestamp'], localtime=True) ET.SubElement(rssitem, 'pubDate').text = date ET.SubElement(rssitem, 'description').text = item['contents'] pubdate.text = email.utils.formatdate(localtime=True) feedtree = ET.ElementTree(root) feedtree.write(path, 'utf-8') # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/pygexiv2.py0000644000175000017500000000624312301071671016705 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2013 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ This is a transition module to provide pyexiv2 compatibility waiting for a properly supported GExiv2 windows port. """ import datetime import logging class Metadata_withPyExiv2(object): def __init__(self, image_path): self.pyexiv2md = pyexiv2.metadata.ImageMetadata(image_path) self.pyexiv2md.read() def __getitem__(self, key): try: val = self.pyexiv2md[key].value except ValueError: return None if type(val) is datetime.datetime: val = val.strftime('%Y:%m:%d %H:%M:%S') return val def __setitem__(self, key, value): if value == '' or value is None: del self.pyexiv2md[key] return if key in ('Iptc.Application2.Keywords', 'Xmp.MicrosoftPhoto.LastKeywordXMP', 'Xmp.dc.subject', 'Xmp.digiKam.TagsList', ): if type(value) is not list: value = [value] if key in self.pyexiv2md.exif_keys: tag = pyexiv2.exif.ExifTag(key) if tag.type in ('Long', 'SLong', 'Short', 'SShort', ): value = int(value) elif tag.type == 'Undefined': tag.value = value self.pyexiv2md[key] = tag return self.pyexiv2md[key] = value def __contains__(self, key): return key in self.pyexiv2md def get_tag_interpreted_string(self, key): return self.pyexiv2md[key].human_value def get_tag_multiple(self, key): return self.pyexiv2md[key].value def get_exif_tag_rational(self, key): return self.pyexiv2md[key].value def get_comment(self): return self.pyexiv2md.comment def get_exif_tags(self): return self.pyexiv2md.exif_keys def clear_tag(self, key): del self.pyexiv2md[key] def save_file(self): self.pyexiv2md.write() class GExiv2_withPyExiv2(object): Metadata = Metadata_withPyExiv2 class LogLevel(object): ERROR = None @staticmethod def log_set_level(dummy): pass try: from gi.repository import GExiv2 except ImportError: import warnings logging.warning('Falling back to try using pyexiv2, which is deprecated.') import pyexiv2 warnings.warn("deprecated", DeprecationWarning) GExiv2 = GExiv2_withPyExiv2 # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/mediautils.py0000644000175000017500000004430512301071671017271 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2010-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import sys import signal try: import gobject import pygst pygst.require('0.10') # http://29a.ch/tags/pygst argv = sys.argv sys.argv = [] import gst sys.argv = argv import gst.extend.discoverer except ImportError: HAVE_GST = False else: HAVE_GST = True gobjects_threads_init = False interrupted = False def signal_handler(signum, frame): global interrupted interrupted = True def gobject_init(): global gobjects_threads_init gobject.threads_init() gobjects_threads_init = True signal.signal(signal.SIGINT, signal_handler) from PIL import Image as PILImage class TranscodeError(Exception): pass class GstVideoOpener(object): def __init__(self, input_file): self.input_file = input_file self.progress = None self.pipeline = gst.Pipeline() self.running = False self.pipeline.set_auto_flush_bus(True) # Input self.filesrc = gst.element_factory_make("filesrc", "source") self.pipeline.add(self.filesrc) # Decoding self.decode = gst.element_factory_make("decodebin", "decode") self.decode.connect("new-decoded-pad", self.on_dynamic_pad) self.pipeline.add(self.decode) self.filesrc.link(self.decode) self.aqueue = None self.vqueue = None def on_dynamic_pad(self, dbin, pad, islast): pad_type = pad.get_caps().to_string()[0:5] if pad_type == 'audio': if self.aqueue is not None: pad.link(self.aqueue.get_pad("sink")) elif pad_type == 'video': if self.vqueue is not None: pad.link(self.vqueue.get_pad("sink")) else: print "E: Unknown PAD detected: %s" % pad_type def open(self): # Init gobjects threads only if a conversion is initiated if not gobjects_threads_init: gobject_init() self.filesrc.set_property( "location", self.input_file.encode(sys.getfilesystemencoding())) def __post_msg(self, msg_txt): msg = gst.message_new_application(self.pipeline, gst.Structure(msg_txt)) self.pipeline.get_bus().post(msg) def set_progress(self, progress): self.progress = progress def monitor_progress(self): # check if CTRL+C'd if interrupted: self.__post_msg('interrupted') # check if stalled stalled = False try: current_position, fmt = self.pipeline.query_position(gst.FORMAT_TIME) if self.media_duration is None: self.media_duration, fmt = self.pipeline.query_duration(gst.FORMAT_TIME) if self.progress is not None: self.progress.set_task_progress(100 * current_position // self.media_duration) except gst.QueryError: pass else: if self.last_position is not None\ and current_position <= self.last_position: stalled = True self.__post_msg('stalled') else: self.last_position = current_position if interrupted or stalled or not self.running: return False # Remove timeout handler return True def __stop_pipeline(self): self.running = False self.pipeline.set_state(gst.STATE_NULL) def run_pipeline(self): self.pipeline.set_state(gst.STATE_PLAYING) self.running = True self.last_position = None self.media_duration = None gobject.timeout_add(1000, self.monitor_progress) while self.running: message = self.pipeline.get_bus().poll(gst.MESSAGE_ANY, -1) if message.type == gst.MESSAGE_EOS: self.__stop_pipeline() elif message.type == gst.MESSAGE_ERROR: self.__stop_pipeline() raise TranscodeError(message.parse_error()) elif message.type == gst.MESSAGE_APPLICATION: if message.src == self.pipeline: struct_name = message.structure.get_name() if struct_name == 'aborded_playback': self.__stop_pipeline() elif struct_name == 'interrupted': self.__stop_pipeline() raise KeyboardInterrupt elif struct_name == 'stalled': self.__stop_pipeline() raise TranscodeError('Pipeline is stalled, this is a problem either in gst or in lazygal\'s use of gst') if self.progress is not None: self.progress.set_task_done() def stop_pipeline(self): msg = gst.message_new_application(self.pipeline, gst.Structure('aborded_playback')) self.pipeline.get_bus().post(msg) class GstVideoInfo(object): def __init__(self, path): self.path = path self.loop = gobject.MainLoop() self.broken = False def __stream_discovered(self, obj, success): if success: self.videowidth = obj.videowidth self.videoheight = obj.videoheight self.videorate = obj.videorate self.audiorate = obj.audiorate self.audiodepth = obj.audiodepth self.audiowidth = obj.audiowidth self.audiochannels = obj.audiochannels self.audiolength = obj.audiolength self.videolength = obj.videolength self.is_video = obj.is_video self.is_audio = obj.is_audio else: self.broken = True self.loop.quit() def inspect(self): # Init gobjects threads only if an inspection is initiated if not gobjects_threads_init: gobject_init() discoverer = gst.extend.discoverer.Discoverer(self.path.encode('utf-8')) discoverer.connect('discovered', self.__stream_discovered) discoverer.discover() self.loop.run() class GstVideoReader(GstVideoOpener): def __init__(self, mediapath): super(GstVideoReader, self).__init__(mediapath) # Output self.oqueue = gst.element_factory_make("queue") self.pipeline.add(self.oqueue) def decode_audio(self): # Audio self.aqueue = gst.element_factory_make("queue") self.pipeline.add(self.aqueue) convert = gst.element_factory_make("audioconvert", "convert") self.pipeline.add(convert) self.aqueue.link(convert) self.resample = gst.element_factory_make("audioresample", "resample") self.pipeline.add(self.resample) convert.link(self.resample) def decode_video(self): # Video self.vqueue = gst.element_factory_make("queue") self.pipeline.add(self.vqueue) self.ff = gst.element_factory_make("ffmpegcolorspace") self.pipeline.add(self.ff) self.vqueue.link(self.ff) class GstVideoTranscoder(GstVideoReader): def __init__(self, mediapath, audiocodec, videocodec, muxer, width=None, height=None): super(GstVideoTranscoder, self).__init__(mediapath) # Audio self.decode_audio() self.audioenc = gst.element_factory_make(audiocodec, 'audioenc') self.pipeline.add(self.audioenc) self.resample.link(self.audioenc) aoqueue = gst.element_factory_make("queue") self.pipeline.add(aoqueue) self.audioenc.link(aoqueue) # Video self.decode_video() self.videoenc = gst.element_factory_make(videocodec, 'videoenc') self.pipeline.add(self.videoenc) if width is not None and height is not None: self.videoscale = gst.element_factory_make('videoscale', 'videoscale') self.videoscale.set_property('method', 'bilinear') self.pipeline.add(self.videoscale) self.ff.link(self.videoscale) caps_str = "video/x-raw-yuv" caps_str += ", width=%d, height=%d" % (width, height) caps = gst.Caps(caps_str) self.caps_filter = gst.element_factory_make("capsfilter", "filter") self.caps_filter.set_property("caps", caps) self.pipeline.add(self.caps_filter) self.videoscale.link(self.caps_filter) self.caps_filter.link(self.videoenc) else: self.ff.link(self.videoenc) voqueue = gst.element_factory_make("queue") self.pipeline.add(voqueue) self.videoenc.link(voqueue) self.muxer = gst.element_factory_make(muxer, 'muxer') self.pipeline.add(self.muxer) aoqueue.link(self.muxer) voqueue.link(self.muxer) # Output self.muxer.link(self.oqueue) # Add output file self.sink = gst.element_factory_make("filesink", "sink") self.sink.set_property("sync", False) self.pipeline.add(self.sink) self.oqueue.link(self.sink) def convert(self, output_file): self.open() output_file = output_file.encode(sys.getfilesystemencoding()) self.sink.set_property("location", output_file) self.run_pipeline() class OggTheoraTranscoder(GstVideoTranscoder): def __init__(self, mediapath): # Working pipeline # gst-launch-0.10 filesrc location=surf_luge.mov ! decodebin name=decode # decode. ! queue ! ffmpegcolorspace ! theoraenc ! queue ! oggmux name=muxer # decode. ! queue ! audioconvert ! vorbisenc ! queue ! muxer. # muxer. ! queue ! filesink location=surf_luge.ogg sync=false super(OggTheoraTranscoder, self).__init__(mediapath, 'vorbisenc', 'theoraenc', 'oggmux') class WebMTranscoder(GstVideoTranscoder): def __init__(self, mediapath, width=None, height=None): # Working pipeline # gst-launch-0.10 filesrc location=oldfile.ext ! decodebin name=demux ! # queue ! ffmpegcolorspace ! vp8enc ! webmmux name=mux ! filesink # location=newfile.webm demux. ! queue ! progressreport ! audioconvert # ! audioresample ! vorbisenc ! mux. # (Thanks # http://stackoverflow.com/questions/4649925/convert-video-to-webm-using-gstreamer/4649990#4649990 # ! ) super(WebMTranscoder, self).__init__(mediapath, 'vorbisenc', 'vp8enc', 'webmmux', width, height) self.videoenc.set_property('quality', 7) class MP4Transcoder(GstVideoTranscoder): def __init__(self, mediapath): # Working pipeline # gst-launch-0.10 filesrc location=oldfile.ext ! decodebin name=demux ! # demux. ! queue ! audioconvert ! faac profile=2 ! queue ! # ffmux_mp4 name=muxer # demux. ! queue ! ffmpegcolorspace ! x264enc pass=4 quantizer=30 # subme=4 threads=0 ! queue ! # muxer. muxer. ! queue ! filesink location=newfile.mp4 super(MP4Transcoder, self).__init__(mediapath, 'faac', 'x264enc', 'ffmux_mp4') self.audioenc.set_property('profile', 2) self.videoenc.set_property('pass', 4) self.videoenc.set_property('quantizer', 30) self.videoenc.set_property('subme', 4) self.videoenc.set_property('threads', 0) class VideoFrameExtractor(GstVideoReader): def __init__(self, path, fps): super(VideoFrameExtractor, self).__init__(path) self.fps = fps self.decode_video() # Grab video size when possible self.video_size = None input_pad = self.ff.get_pad('sink') input_pad.connect('notify::caps', self.cb_new_caps) videorate = gst.element_factory_make('videorate') self.pipeline.add(videorate) self.ff.link(videorate) # RGB is what is assumed in order to load the frame into a PIL Image. self.capsfilter = gst.element_factory_make('capsfilter') self.capsfilter.set_property( 'caps', gst.Caps('video/x-raw-rgb,framerate=%s/1' % fps)) self.pipeline.add(self.capsfilter) videorate.link(self.capsfilter) self.app_sink = gst.element_factory_make('appsink') self.app_sink.set_property('emit-signals', True) self.app_sink.set_property('max-buffers', 10) self.app_sink.set_property('sync', False) self.app_sink.connect('new-buffer', self.on_new_buffer) self.pipeline.add(self.app_sink) self.capsfilter.link(self.app_sink) def cb_new_caps(self, pad, args): caps = pad.get_negotiated_caps() if not caps: return if 'video' in caps.to_string(): self.video_size = (caps[0]['width'], caps[0]['height'], ) def open_frame(self, buf): return PILImage.fromstring('RGB', self.video_size, buf) def on_new_buffer(self, appsink): buf = appsink.emit('pull-buffer') self.cb_grabbed_frame_buf(buf) def cb_grabbed_frame_buf(self, buf): raise NotImplementedError class VideoFrameNthExtractor(VideoFrameExtractor): def __init__(self, path, frame_no, fps): super(VideoFrameNthExtractor, self).__init__(path, fps) self.frame_index = -1 self.frame_no = frame_no self.frame = None def cb_grabbed_frame_buf(self, buf): # We're searching for the self.frame_no'th frame. self.frame_index = self.frame_index + 1 if self.frame_index == self.frame_no: self.frame = self.open_frame(buf) # Abord playback as frame has been found. self.stop_pipeline() def get_frame(self): self.open() self.run_pipeline() return self.frame class VideoBestFrameFinder(VideoFrameExtractor): def __init__(self, path, fps, intro_seconds): super(VideoBestFrameFinder, self).__init__(path, fps) self.max_frames = self.fps * intro_seconds # Frames to go through self.histograms = [] def cb_grabbed_frame_buf(self, buf): self.frame_number = self.frame_number + 1 if self.frame_number < self.max_frames: # We're searching for the best frame, grab histogram self.histograms.append(self.open_frame(buf).histogram()) else: self.stop_pipeline() def get_best_frame(self): self.open() self.frame_number = -1 self.run_pipeline() n_samples = len(self.histograms) n_values = len(self.histograms[0]) # Average each histogram value average_hist = [] for value_index in range(n_values): average = 0.0 for histogram in self.histograms: average = average + (float(histogram[value_index]) / n_samples) average_hist.append(average) # Find histogram closest to average histogram min_mse = None best_frame_no = None for hist_index in range(len(self.histograms)): hist = self.histograms[hist_index] mse = 0.0 for value_index in range(n_values): gap = average_hist[value_index] - hist[value_index] mse = mse + gap * gap if min_mse is None or mse < min_mse: min_mse = mse best_frame_no = hist_index frame_finder = VideoFrameNthExtractor(self.input_file, best_frame_no, self.fps) return frame_finder.get_frame() class VideoThumbnailer(object): def __init__(self, input_file, thumb_size=None): self.input_file = input_file self.thumb_size = thumb_size # fps images per second should be enough to find a suitable thumbnail self.fps = 1 # search thumb in first intro_seconds seconds self.intro_seconds = 300 def get_thumb(self): thumb_finder = VideoBestFrameFinder(self.input_file, self.fps, self.intro_seconds) return thumb_finder.get_best_frame() def convert(self, thumbnail_path): thumb = self.get_thumb() if self.thumb_size is not None: thumb.draft(None, self.thumb_size) thumb = thumb.resize(self.thumb_size, PILImage.ANTIALIAS) thumb.save(thumbnail_path) if __name__ == '__main__': import sys import os converter_types = sys.argv[1].split(',') converters = {} for converter_type in converter_types: if converter_type == 'ogg': converter = OggTheoraTranscoder elif converter_type == 'webm': converter = WebMTranscoder elif converter_type == 'mp4': converter = MP4Transcoder elif converter_type == 'jpeg': converter = VideoThumbnailer else: raise ValueError('unknwon converter type %s' % converter_type) converters[converter_type] = converter for file_path in sys.argv[2:]: file_path = file_path.decode(sys.getfilesystemencoding()) fn, ext = os.path.splitext(os.path.basename(file_path)) for converter_type, converter in converters.items(): counter_str = '' counter = 0 filename_free = False while not filename_free: target_path = fn + counter_str + '.' + converter_type if os.path.isfile(target_path): counter = counter + 1 counter_str = '_%d' % counter else: filename_free = True converter(file_path).convert(target_path) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/__init__.py0000644000175000017500000000434612301073127016667 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os __all__ = ['generators', ] # Compute installation prefix if os.path.isfile(os.path.join(os.path.dirname(__file__), '..', 'setup.py')): INSTALL_MODE = 'source' INSTALL_PREFIX = '' else: # Lazygal is installed, assume we are in # $prefix/lib/python2.X/dist-packages/lazygal INSTALL_MODE = 'installed' INSTALL_PREFIX = os.path.join(os.path.dirname(__file__), '..', '..', '..', '..') INSTALL_PREFIX = os.path.normpath(INSTALL_PREFIX) def get_hg_rev(): try: lazygal_dir = os.path.join(os.path.dirname(__file__), '..') if not os.path.isdir(os.path.join(lazygal_dir, '.hg')): raise IOError import mercurial.hg import mercurial.node import mercurial.ui repo = mercurial.hg.repository(mercurial.ui.ui(), lazygal_dir) last_revs = repo.changelog.parents(repo.dirstate.parents()[0]) known_tags = repo.tags().items() for tag, rev in known_tags: if tag != 'tip': for last_rev in last_revs: if rev == last_rev: # This is a tagged revision, assume this is a release. return '' return mercurial.node.short(last_revs[0]) except (IOError, OSError, ImportError): return '' __version__ = '0.8.2' hg_rev = get_hg_rev() if hg_rev: __version__ += '+hg' + hg_rev # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/genmedia.py0000644000175000017500000002605612301071671016705 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import logging from PIL import Image as PILImage # lazygal has her own ImageFile class, so avoid trouble from PIL import ImageFile as PILImageFile PILImageFile.MAXBLOCK = 1024 * 1024 # default is 64k, not enough for big pics import make import genfile import eyecandy import mediautils from lazygal.pygexiv2 import GExiv2 THUMB_SIZE_NAME = 'thumb' VIDEO_SIZE_NAME = 'video' class ResizedImage(genfile.WebalbumFile): force_extension = None def __init__(self, webgal, source_media, size_name): self.webgal = webgal self.source_media = source_media self.filename = self.webgal._add_size_qualifier(self.source_media.filename, size_name, self.force_extension) path = os.path.join(self.webgal.path, self.filename) genfile.WebalbumFile.__init__(self, path, webgal) self.newsizer = self.webgal.newsizers[size_name] self.size = None self.add_dependency(self.source_media) def get_size(self): if self.size is None: self.size = self.newsizer.dest_size(self.source_media.get_size()) return self.size def get_verb(self): raise NotImplementedError VERB = property(get_verb) def build(self): media_rel_path = self.rel_path(self.webgal.flattening_dir) logging.info(" %s %s" % (self.VERB, media_rel_path)) logging.debug("(%s)" % self.path) im = self.resize(self.get_image()) self.save(im) def get_image(self): raise NotImplementedError def call_build(self): try: self.build() except IOError: if not self.source_media.broken: logging.error(_(" %s is BROKEN, skipped") % self.source_media.filename) self.source_media.broken = True # Make the system believe the file was built a long time ago. self.stamp_build(0) else: self.stamp_build() def resize(self, im): self.source_media.get_size() # Probe brokenness if self.source_media.broken: raise IOError() new_size = self.get_size() im.draft(None, new_size) return im.resize(new_size, PILImage.ANTIALIAS) def save(self, im): calibrated = False while not calibrated: try: im.save(self.path, quality=self.webgal.quality, **self.webgal.save_options) except IOError, e: if str(e).startswith('encoder error'): PILImageFile.MAXBLOCK = 2 * PILImageFile.MAXBLOCK continue else: raise calibrated = True class ImageOtherSize(ResizedImage): def __init__(self, webgal, source_image, size_name): super(ImageOtherSize, self).__init__(webgal, source_image, size_name) self.rotation = None def get_verb(self): return _('RESIZE') VERB = property(get_verb) def get_rotation(self): if self.rotation is None: source_media_md = self.source_media.info() if source_media_md is not None: self.rotation = source_media_md.get_required_rotation() else: self.rotation = 0 return self.rotation def get_size(self): if self.size is None: orig_size = self.source_media.get_size() if self.source_media.broken: return orig_size if self.get_rotation() in (90, 270, ): # swap coords orig_size = (orig_size[1], orig_size[0], ) self.size = self.newsizer.dest_size(orig_size) self.unrotated_size = (self.size[1], self.size[0], ) else: self.size = self.newsizer.dest_size(orig_size) self.unrotated_size = self.size return self.size def get_image(self): return PILImage.open(self.source_media.path) TRANSPOSE_METHODS = { 90 : PILImage.ROTATE_90, 180: PILImage.ROTATE_180, 270: PILImage.ROTATE_270, } def resize(self, im): self.source_media.get_size() # Probe brokenness if self.source_media.broken: raise IOError() rotation = self.get_rotation() self.get_size() im.draft(None, self.unrotated_size) im = im.resize(self.unrotated_size, PILImage.ANTIALIAS) # Use EXIF data to rotate target image if available and required if rotation != 0: im = im.transpose(self.TRANSPOSE_METHODS[rotation]) return im PRIVATE_IMAGE_TAGS = ( 'Exif.GPSInfo.GPSLongitude', 'Exif.GPSInfo.GPSLatitude', 'Exif.GPSInfo.GPSDestLongitude', 'Exif.GPSInfo.GPSDestLatitude', ) def copy_metadata(self): imgtags = GExiv2.Metadata(self.source_media.path) dest_imgtags = GExiv2.Metadata(self.path) for tag in imgtags.get_exif_tags(): dest_imgtags[tag] = imgtags[tag] new_size = self.get_size() dest_imgtags['Exif.Photo.PixelXDimension'] = str(new_size[0]) dest_imgtags['Exif.Photo.PixelYDimension'] = str(new_size[1]) if self.get_rotation() != 0: # Smaller image has been rotated in order to be displayed correctly # in a web browser. Fix orientation tag accordingly. dest_imgtags['Exif.Image.Orientation'] = '1' # Those are removed from published pics due to pivacy concerns, # unless explicitly told to keep them in. Option to retain GPS # tags can only be set from command line. if not self.webgal.keep_gps: for tag in self.PRIVATE_IMAGE_TAGS: try: dest_imgtags.clear_tag(tag) except KeyError: pass try: dest_imgtags.save_file() except Exception, e: logging.error(_("Could not copy metadata in reduced picture: %s") % e) def save(self, im): super(ImageOtherSize, self).save(im) if self.webgal.config.getboolean('webgal', 'publish-metadata'): self.copy_metadata() class VideoThumb(ResizedImage): force_extension = '.jpg' def get_verb(self): return _('VIDEOTHUMB') VERB = property(get_verb) def get_image(self): try: thumb = mediautils.VideoThumbnailer(self.source_media.path).get_thumb() except mediautils.TranscodeError, e: logging.error(_(" creating %s thumbnail failed, skipped") % self.source_media.filename) logging.info(str(e)) self.clean_output() raise IOError() else: return thumb class WebalbumPicture(make.FileMakeObject): BASEFILENAME = 'index' def __init__(self, webgal_dir): self.path = os.path.join(webgal_dir.path, webgal_dir.get_webalbumpic_filename()) make.FileMakeObject.__init__(self, self.path) self.add_dependency(webgal_dir.source_dir) # Use already generated thumbs for better performance (lighter to # rotate, etc.). thumbs = [image.thumb for image in webgal_dir.get_all_medias_tasks() if image.thumb and not image.media.broken] for thumb in thumbs: self.add_dependency(thumb) if webgal_dir.source_dir.album_picture: albumpic_path = os.path.join(webgal_dir.source_dir.path, webgal_dir.source_dir.album_picture) if not os.path.isfile(albumpic_path): logging.error(_("Supplied album picture %s does not exist.") % webgal_dir.source_dir.album_picture) md_dirpic_thumb = webgal_dir._add_size_qualifier( webgal_dir.source_dir.album_picture, THUMB_SIZE_NAME) md_dirpic_thumb = os.path.join(webgal_dir.path, md_dirpic_thumb) else: md_dirpic_thumb = None pics = [thumb.path for thumb in thumbs] multipic_repr = eyecandy.WEBALBUMPIC_TYPES[webgal_dir.webalbumpic_type] # Use 800x600 as a random value to obtain a 4:3 aspect ratio (if # thumb size preserves aspect ratio) self.dirpic = multipic_repr(pics, md_dirpic_thumb, bg=webgal_dir.webalbumpic_bg, result_size=webgal_dir.webalbumpic_size) def build(self): logging.info(_(" DIRPIC %s") % os.path.basename(self.path)) logging.debug("(%s)" % self.path) try: self.dirpic.write(self.path) except ValueError, ex: logging.error(str(ex)) class WebVideo(genfile.WebalbumFile): def __init__(self, webgal, source_video, size_name, progress=None): self.progress = progress self.webgal = webgal self.source_video = source_video self.filename = self.webgal._add_size_qualifier(self.source_video.filename, size_name, '.webm') path = os.path.join(self.webgal.path, self.filename) genfile.WebalbumFile.__init__(self, path, webgal) newsizer = self.webgal.newsizers[size_name] if newsizer == 'original': self.new_width, self.new_height = (None, None) else: self.new_width, self.new_height = newsizer.dest_size(self.source_video.get_size()) self.add_dependency(self.source_video) def build(self): vid_rel_path = self.rel_path(self.webgal.flattening_dir) logging.info(_(" TRANSCODE %s") % vid_rel_path) try: transcoder = mediautils.WebMTranscoder(self.source_video.path, self.new_width, self.new_height) transcoder.set_progress(self.progress) transcoder.convert(self.path) except mediautils.TranscodeError, e: logging.error(_(" transcoding %s failed, skipped") % self.source_video.filename) logging.info(str(e)) self.source_video.broken = True self.clean_output() # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/timeutils.py0000644000175000017500000000263012301071671017143 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import time import locale class Datetime(object): def __init__(self, timestamp=None, datetime=None): if datetime is not None: self.timestamp = time.mktime(datetime.timetuple()) elif timestamp is not None: self.timestamp = timestamp else: self.timestamp = time.time() def strftime(self, format): # strftime does not work with unicode... enc = locale.getpreferredencoding() return time.strftime(format.encode(enc), time.localtime(self.timestamp)).decode(enc) # vim: ts=4 sw=4 expandtab lazygal-0.8.2/lazygal/tpl.py0000644000175000017500000002321212301071671015722 0ustar niolniol00000000000000# Lazygal, a lazy static web gallery generator. # Copyright (C) 2007-2012 Alexandre Rossi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import glob import logging import json import codecs import locale from genshi.core import START from genshi.template import TemplateLoader, MarkupTemplate, NewTextTemplate from genshi.template import TemplateNotFound from genshi.template.eval import UndefinedError from genshi.input import XMLParser import __init__ import timeutils import pathutils DEFAULT_THEME = 'default' USER_THEME_DIR = os.path.expanduser(os.path.join('~', '.lazygal', 'themes')) THEME_SHARED_FILE_PREFIX = 'SHARED_' THEME_MANIFEST = 'manifest.json' class LazygalTemplate(object): def __init__(self, loader, genshi_tpl): self.loader = loader self.path = genshi_tpl.filepath self.genshi_tpl = genshi_tpl def __complement_values(self, values): values['gen_datetime'] = timeutils.Datetime() values['gen_date'] = values['gen_datetime'].strftime('%c') values['lazygal_version'] = __init__.__version__ return values def __generate(self, values): # The cryptic values['_'] = _ is the way to pass the gettext # translation function to the templates : the _() callable is assigned # to the '_' keyword arg. values['_'] = _ return self.genshi_tpl.generate(**values) def instanciate(self, values): self.__complement_values(values) # encoding=None gives us a unicode string instead of an utf-8 encoded # string. This is because we are not out of lazygal yet. return self.__generate(values).render(self.serialization_method, encoding=None) def dump(self, values, dest): self.__complement_values(values) page = open(dest, 'w') try: self.__generate(values).render(method=self.serialization_method, out=page, encoding='utf-8') except UnicodeDecodeError: problematic_vars = [] for key, value in values.items(): if type(value) is not unicode: try: str(value).decode('utf-8') except UnicodeDecodeError: problematic_vars.append(key) print 'Problematic template vars : %s' % ', '.join(problematic_vars) raise except UndefinedError, e: print 'W: %s' % e raise finally: page.close() class XmlTemplate(LazygalTemplate): serialization_method = 'xhtml' genshi_tpl_class = MarkupTemplate def subtemplates(self, tpl=None): if tpl is None: tpl = self subtemplates = [] f = open(tpl.path, 'r') try: for kind, data, pos in XMLParser(f, filename=tpl.path): if kind is START: tag, attrib = data if tag.namespace == 'http://www.w3.org/2001/XInclude'\ and tag.localname == 'include': subtpl_ident = attrib.get('href') try: subtpl = self.loader.load(subtpl_ident) except TemplateNotFound: # This will fail later, here we just need to ignore # template idents that are dynamically computed. pass else: subtemplates.append(subtpl) finally: f.close() for subtemplate in subtemplates: for new_subtpl in self.subtemplates(subtemplate): if new_subtpl not in subtemplates: subtemplates.append(new_subtpl) return subtemplates class PlainTemplate(LazygalTemplate): serialization_method = 'text' genshi_tpl_class = NewTextTemplate class TplFactory(object): known_exts = { '.thtml': XmlTemplate, '.tcss' : PlainTemplate, '.tjs' : PlainTemplate, } def __init__(self, default_tpl_dir, tpl_dir): # We use lenient mode here because we want an easy way to check whether # a template variable is defined, or the empty string, thus defined() # will only work for the 'whether it is defined' part of the test. self.loader = TemplateLoader([tpl_dir, default_tpl_dir], variable_lookup='lenient') def is_known_template_type(self, file): filename, ext = os.path.splitext(os.path.basename(file)) return ext in self.known_exts.keys() def load(self, tpl_ident): if self.is_known_template_type(tpl_ident): filename, ext = os.path.splitext(os.path.basename(tpl_ident)) tpl_class = self.known_exts[ext] tpl = self.loader.load(tpl_ident, cls=tpl_class.genshi_tpl_class) return tpl_class(self, tpl) else: raise ValueError(_('Unknown template type for %s' % tpl_ident)) class Theme(object): def __init__(self, themes_dir, name): self.name = name # First try user directory self.tpl_dir = os.path.join(USER_THEME_DIR, self.name) if not os.path.exists(self.tpl_dir): # Fallback to system themes self.tpl_dir = os.path.join(themes_dir, self.name) if not os.path.exists(self.tpl_dir): raise ValueError(_('Theme %s not found') % self.name) self.tpl_dir = os.path.abspath(self.tpl_dir) self.__load_manifest() self.tpl_loader = TplFactory(os.path.join(themes_dir, DEFAULT_THEME), self.tpl_dir) # Load styles templates for style in self.get_avail_styles(): style_filename = style['filename'] try: self.tpl_loader.load(style_filename) except ValueError: # Not a known emplate ext, ignore pass # find out theme kind try: self.tpl_loader.load('dynindex.thtml') except TemplateNotFound: self.kind = 'static' else: self.kind = 'dynamic' def __load_manifest(self): theme_manifest_path = os.path.join(self.tpl_dir, THEME_MANIFEST) logging.debug(_('Loading %s for theme %s') % (THEME_MANIFEST, self.name)) theme_manifest = {} try: with codecs.open(theme_manifest_path, 'r', locale.getpreferredencoding()) as f: theme_manifest.update(json.load(f)) except IOError: logging.debug(_('Theme %s does not have a %s') % (self.name, THEME_MANIFEST)) except ValueError: logging.error(_('Theme %s : %s parsing error') % (self.name, THEME_MANIFEST)) raise if 'shared' not in theme_manifest: theme_manifest['shared'] = [] theme_manifest['shared'].append({'path': THEME_SHARED_FILE_PREFIX + '*'}) self.shared_files = [] for shared_file_entry in theme_manifest['shared']: entry_path = os.path.join(self.tpl_dir, shared_file_entry['path']) entry_path = os.path.normpath(entry_path) entry_dest = 'dest' in shared_file_entry and shared_file_entry['dest'] for sf in glob.glob(entry_path): sf_name = os.path.basename(sf) if sf_name.startswith(THEME_SHARED_FILE_PREFIX): sf_name = sf_name[len(THEME_SHARED_FILE_PREFIX):] if entry_dest: # There is dest info in manifest for this file if entry_dest[-1] == os.sep: # dest is a dir dest = os.path.join(entry_dest, sf_name) else: dest = entry_dest else: if pathutils.is_subdir_of(self.tpl_dir, sf): sf_dir = os.path.dirname(sf) dest = os.path.join(sf_dir[len(self.tpl_dir)+1:], sf_name) else: dest = sf_name self.shared_files.append((sf, dest)) def get_avail_styles(self, default_style=None): style_files_mask = os.path.join(self.tpl_dir, THEME_SHARED_FILE_PREFIX + '*' + 'css') styles = [] for style_tpl_file in glob.glob(style_files_mask): style = {} tpl_filename = os.path.basename(style_tpl_file).split('.')[0] style['filename'] = tpl_filename[len(THEME_SHARED_FILE_PREFIX):] style['name'] = style['filename'].replace('_', ' ') if default_style is not None: if style['filename'] == default_style: style['rel'] = 'stylesheet' else: style['rel'] = 'alternate stylesheet' styles.append(style) return styles # vim: ts=4 sw=4 expandtab lazygal-0.8.2/PKG-INFO0000644000175000017500000000206112301073200014170 0ustar niolniol00000000000000Metadata-Version: 1.1 Name: lazygal Version: 0.8.2 Summary: Static web gallery generator Home-page: http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal Author: Alexandre Rossi Author-email: alexandre.rossi@gmail.com License: GPL Download-URL: http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal Description: UNKNOWN Keywords: gallery,exif,photo,image Platform: Linux Platform: Mac OSX Platform: Windows XP/2000/NT Platform: Windows 95/98/ME Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Win32 (MS Windows) Classifier: Environment :: X11 Applications :: GTK Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: Microsoft :: Windows :: Windows 95/98/2000 Classifier: Operating System :: Microsoft :: Windows :: Windows NT/2000 Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Topic :: Utilities Classifier: Natural Language :: English lazygal-0.8.2/lazygal-setcomment.py0000755000175000017500000000055412301071671017306 0ustar niolniol00000000000000#!/usr/bin/env python import sys from lazygal.pygexiv2 import GExiv2 fn = sys.argv[1] comment = sys.argv[2] im = GExiv2.Metadata(fn.decode(sys.getfilesystemencoding())) # Assume comment is in utf-8, more encoding processing example using # sys.stdin.encoding and example processing in lazygal/metadata.py im['Exif.Photo.UserComment'] = comment im.save_file() lazygal-0.8.2/themes/0000755000175000017500000000000012301073200014361 5ustar niolniol00000000000000lazygal-0.8.2/themes/image-index/0000755000175000017500000000000012301073200016550 5ustar niolniol00000000000000lazygal-0.8.2/themes/image-index/gallerylink.thtml0000644000175000017500000000260312301071671022152 0ustar niolniol00000000000000 lazygal-0.8.2/themes/image-index/SHARED_style.tcss0000644000175000017500000000210512301071671021644 0ustar niolniol00000000000000{% if bgcolor %} body{ background-color:${bgcolor} } {% end %} {% if txtcolor %} body{ color:${txtcolor} } {% end %} p{ text-align:center; } a{ {% if lnkcolor %} color:${lnkcolor}; {% end %} {% if not lnkcolor %} color:blue; {% end %} {% if lnkdecoration %} text-decoration:${lnkdecoration}; {% end %} {% if not lnkdecoration %} text-decoration:none; {% end %} } #image_links{ clear: both; padding-left:7em; padding-right:7em; } #prev_link{ position:absolute; top:50%; left:0.1em; } #next_link{ position:absolute; top:50%; right:0.1em; } img { border:solid black 1px; margin:1em; } img.subgal { border: none; } div.subgal_link { float: left; padding: 1em; width: 40em; } div.subgal_image { float: left; width: 180px; /* This is based on thumbnail size */ } div.subgal_image img { margin: 0; } #subgal_links li { margin: 0.5em; } #image_links p{ text-align:justify; } #footer { clear: both; } #footer p { text-align:justify; } /* * vim: ts=4 sw=4 expandtab */ lazygal-0.8.2/themes/inverted/0000755000175000017500000000000012301073200016201 5ustar niolniol00000000000000lazygal-0.8.2/themes/inverted/TODO0000644000175000017500000000045612301071671016710 0ustar niolniol00000000000000Ideas to improve Inverted theme for Lazygal (in no particular order) + Add Open Graph metadata (if possible, need image direct link) http://ogp.me/ + Add Pinterest share link (if possible, need image direct link) + Fix Tumblr to go straight to photo share page (if possible, need image direct link) lazygal-0.8.2/themes/inverted/thumb.thtml0000644000175000017500000000124212301071671020403 0ustar niolniol00000000000000 lazygal-0.8.2/themes/inverted/SHARED_video_arrow.svg0000644000175000017500000001130512301071671022302 0ustar niolniol00000000000000 image/svg+xml lazygal-0.8.2/themes/inverted/feeditem.thtml0000644000175000017500000000070412301071671021050 0ustar niolniol00000000000000

album picture

$subgal_count ${_('sub-galleries')}, $picture_count ${_('photos')}

lazygal-0.8.2/themes/inverted/header.thtml0000644000175000017500000000516512301071671020524 0ustar niolniol00000000000000 <!--! Album title = 'album_description': 'Album name' --> <title py:if="name and (image_name == None)" py:content="name" /> <!--! Image title --> <title py:if="name and image_name" py:content="name.rsplit('.', 1)[0].replace('-', ' ').replace('_', ' ').capitalize()" /> <!--! Site description = '.lazygal': section: '[template-vars]', variable: 'site_description' --> <meta py:if="site_description" name="description" content="${site_description}" /> <!--! Site author = '.lazygal': section: '[template-vars]', variable: 'site_author' --> <meta py:if="site_author" name="author" content="${site_author}" /> <!--! Site keywords = '.lazygal': section: '[template-vars]', variable: 'site_keywords' --> <meta py:if="site_keywords" name="keywords" content="${site_keywords}" /> <!--! Feed url: turn on with cmd option 'puburl' --> <!--! or set in configuration file: '.lazygal': section: '[global]', variable: 'puburl' --> <link py:if="feed_url" rel="alternate" type="application/rss+xml" title="Recent galleries" href="$feed_url" /> <!--! Display with what version of lazygal, site was generated --> <meta name="Generator" content="lazygal $lazygal_version" /> <!--! Load all styles from theme folder 'SHARED_.*\.[t]?css' --> <link py:for="style in styles" type="text/css" rel="$style.rel" media="screen,projection" title="$style.name" href="${rel_root}shared/${style.filename}.css" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta name="viewport" content="width=device-width" /> <!--[if lt IE 9]> <script src="shared/respond.js"></script> <![endif]--> <!--! Google analytics tracking ID = '.lazygal': section: '[template-vars]', variable: 'google_analytics_tracking_id' --> <py:if test="google_analytics_tracking_id"> <script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', '${google_analytics_tracking_id}']); _gaq.push(['_trackPageview']); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script> </py:if> </head> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_images_data.tcss�����������������������������������������������0000644�0001750�0001750�00000051302�12301071671�022376� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ============================================================================ Free icons from http://www.axialis.com/free/icons/index.html Icon set 'Web 2.0 Buttons - Rounded' ========================================================================== */ .image_size ul li:first-child a, .image_size ul li:first-child span { /* Reduced Size Blue.png */ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABa0lEQVQ4jbWTzy5DQRjFf9/MXKpX4mrTWFp7AgmJJcvSLTuxshFrxMID8AxY2FArib3EG3gGRP+oXuR2ZiyqtNU/LJxkkknmO+ebM98ZASic+nxSr+wAWcAxGAp4CsLo4HxVLmXlxOcbcbUIvqPKeXDe/2SLoKS5D8JoWTXiym432Toox5YRJWj5XiNKKMcW+3nHpF7ZNUCmu3Pl1XKzle3rYf7wicm0RgkZ1e3ZeU8u1AMfIRfqlj2nehXYn9b7nvcU+Av+R8C5wR609BB4qDnq755y7AnM4K6PdYuSpoppkW+3J9m7emEqVGwuhMwdlcimFLR109IkR2P6K0wGwHpPw8L6bJrpjGbjrMqoEiy+I2PW05r/F0y7jemM5vrujSglGA2mvbI3lAIpGSXcP1vAsziTYn8p5OV9SBiaKEnh2OWTt1rxsdYg+UxIoIXc+LAJC0E4sSwAhWOXT16fd8D/+jubdHRwsSaXHx0Gh/VDB1fiAAAAAElFTkSuQmCC'); background-attachment: center; background-color: transparent; background-position: left; background-repeat: no-repeat; padding-left: 18px; padding-bottom:1px; } .image_size ul li:last-child a, .image_size ul li:last-child span { /* Full Size Blue.png */ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABfUlEQVQ4ja2TvU4bURCFv5m7C3iXnzXIoabkDZDoaQ0IiYK8QLo8AEiRoEZpUpFISEDLT5mIComCJ6CmoEAW9oawBtl776Xgz5HNmoKRRrqaO+fMnHN1BWBxz1fbWboKTACO4lDgOoyTjf0VOZKFXV/Nm38PwReiareOtn3sCY1QGVbCOJkP8ma61mcitVvH8Zcx4iEDeC4blqXtGyqkawqM9yMYHhS+/c74c34PCJOjhtx5gHHt1Ow85M53p4X03jM3PcRF3XZyO30+WQeNpmVABSMdidBynq3lMX6cZPw6axIYMCIABM+T0zvL6deJN2XM/2xw9c8ThcLMZoNPI9pJ4KnEptCHdg7lSAhUiAflpf4qofgVUZXe9WJY//g4AtN7Q/rdK6AqQi2zvTueopZZVLpYNACpq/ippGSY/X5NJTb/GWrkEZyUDD18rGtYGl1HFKNQjgwt57H+NVvOU44MpsstIYyTdQFY3HHV9t3NKvh3f+cgSjYOPsvRA5e/lhrSvbP0AAAAAElFTkSuQmCC'); background-attachment: center; background-color: transparent; background-position: right; background-repeat: no-repeat; padding-right: 18px; padding-bottom:1px; } {% if display_social_buttons %} /* ============================================================================ Free social icons from http://icondock.com/free/vector-social-media-icons/ Vector Social Media Icons by IconDock.com ========================================================================== */ #share {margin: 0 auto;} /* Hide social icons if javascript is turned off */ #share_icons {background:transparent; display: none;} #share_icons a {display:inline-block; height:32px; width:32px;} #share_icons a.share_delicious {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEx0lEQVR4XpWX3WsdRRjGn/k4u3uSph9JSbUlNu2NhoIoLYIgWAEVvBO8FDhe+ieIYAwFUaCI0GIFsblTRLxRQBAKvVEBqVqEtmkNaSRovyJN0rN7dnZmfPfd6UAotLtDn74fYfP85mXKSwXCee/kqUGS9OazrD87PTmJrJ8CHnDewXsP73wT4WN9bWUVn63uwVP7dsFSz1Gv8mhy/jZ84wDhHS79excSfkUACwAWSWCAE5+cOXtwZmbw0vPHsH96L+IRAg87n3/5LT5cmsCZ14/CWg/LZo25b4Bibq1jXVy7ha9+vYIbG8Ma4C39/senBwefmBm8dvwF8hNYu7UOW1n46A/uS1Id0cQI2BMeJ3/5B2uFgSGI0rkYC4o5xYoopnoKkz2Jo1NjePPYEXzx8x+D/4bFeZ2m2fzTc09iVBoMR2U0epS5lJLHp0i7E4WbpQXgSPKBmFO8YyzuFBWu3i0wN5HimQOP49zS8rzu9/uzO8b6yEvT1pyjDgBCeIz3JHqqruRDISqA80tbJZKxFNa7Wa2VasyA1uaylmz6CiAAhZ5kszYQXK6Z5kFrpTXSNIExtq05R8WGgKonkKgwgfYQu5Ie1msAvjlEJ3MKkPcBSGM8AYZsDTFF0MsAtA8Pqr359lrCo9+TSAJAW4jJVAMM4GsA0ckcAZrSCKAlm7SG2JkoCHhoEIEUspt5BGwAsjiB9hATBKAp174GkOhsrvjRNWCZlvENtIXYkWhongC6m0cxgkeWSKgA0BZid6ahIgC6m0do75HqCNAaYldfQ1DUiA9KcNSyiYokQ09IEfJoznVjSQA9hTfmJlFWDiNDqhoVhhRiThoaW4vznZliFF2MCvx04SIcr1sXtpgLm4x7XHsX+lxTn/K8KLAxBE58cw4+rF8XoqXI65lzkgv98HvPX6S8qiBmDs76x6anIfnG4bZSskToxSnEWvDP1u+s490PPsL42BiD+2YgCFnT45TFff4Tmn9evgKtlcahQ4eR9fvIsoyVprVSVlLHJOHIdZJEfXr6FGiZ4fhzzwaA+Mu31aQ4IZBc6F2+tgxdjgpsbW1hY2MDSql4e85DrUgxD/HlV17FiL611vJIN+7l28zZJOpBoPEshbMWuqoqbG5u8o26nKIoUI5GDEAm7c1ZXME6AiiNgSEB6AxgyjI81o7mAdJaB22riptlWXYCyPOcvwkT6GIe4QKAgZSCb9TljOoJGAJwjiA6mgcD5ywBOA8hZPcJFDlMaeD4EbpO5lwDYQJEIQQ6AxQ5ARjDE/CumzmJp84T8NZRoToDDIcEUBm+BUF0MqcAJSXD63p81lYk2wkiz4cwpqpvQXJdzPn2BM7/jDW/XirSNKVbDVsD3NvaIoA4gbbmHLXSdbf+ngFWbt++NTs9vQ9LS5fb+jOsc80ErPOtzR1px3gfK6tr9YVXNICF66vXz05OTmFu7ghu3rxBZGWrN1FZFyfwCHNe4Vnaw8T4GCpj8P2P50CbeEEDWDTGvPjb7xcGh2kpHdh/gBdTnyTv7wAhYk4xytpmVe/fu2cbmI9JzKL+uv43vv7uB1xdXl4kqEWBeDAgzQuIWa4EIJq/4hEssW29vv3OAnpJEhqxv43Go9n9lbUoinzFlGX87/n/5j9tinG2LmQAAAAASUVORK5CYII='); } #share_icons a.share_facebook {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADHUlEQVR4Xr2Tf2hNYRzGn/PeH2yNLcY2s5lWhMIwMjAzFA1hlNFuJkULIpUIYwWlUixVVoBViWyV/KcoJRVKtBpRGdzZGKN73q+e5tT29rLT2dpTn26dTs/ne7/vezB3zSHMX3cURWsOxxZtPNGyvPKsrKg6J6u2XxhQ2MluOuiik26F7tSPH5dbX7l2Wd7+rWXYW7kCuzYvG1DYyW468sfn1QMgHACx7KysWOnc6Yi3/0Ljg1douPccl+48HVDYyW46SuZMxZjMjBjdWFhe03LoTJPsqmmUjXsbBgW66KQbSypOS/WR27Jhz41BhU66leMofPr6A1rLoEIn3SjdclbWVl/rNxX7GuRm0xN58fqNNL99b2J10K201v3+NxnpyTiwrRBFBWMxNmMkMtNHIC87sxd2hwaKN52Rsh1XAsPzfPj0pXyOt0kikfgXVgfdSotA+kFRQRbyc0cjbfgw/CdWB91KtMDtB7OnZiI9LRV9xOqgW3mTBCU/ZxTMXGt8hpU7rvbE6pDuDWhOEoi0lCiGRqMwc/9Rs88ODcVJgGAb+NLehUgkDDMfPn/vs8NzKgocIPAnGI1EYMRXB53eJYRyHH6TvqjdPbsXtvC5idlDJ93cAEIcQOCLGVMm9sLV2oTPTcyebqd3CUMOBdofOhBmD53eJfSOQPzhuoEwe5S3AQr8H0Hgy2o/Ag4iXIdyeHa+KNl2uSfWdfO5idlDJ93K/TsNfwOhXRNfHZ5Tua4LB4KEmwiCbQO+OuikW/3u6sTvnx3o+hYPhOtqE18ddNKtsidNwoR5CzC5eGkQbBvw1UEn3aqz/Qd+tn1DvDUeBNsAvjropJt3EQoaYeUgQKwD9JUhYUUn6OYAcByN1KTIoA2QMiRMJ+hWShJv2jo6kJOWjACxXsK+QheddCvp+n6s+d17JIVczMgZgVEpQ5EUCcFvRLQJbAmHFLvpoAt00g1m1urD9cVb62RnTaMcO/9Ijtc9ltqLT3zxofWjifU9drKbjsVVdUInemZW2cFY4fralpnlp6Sg/KRMW++P67fumljfYye76aDL8/4BOblVOfpydcIAAAAASUVORK5CYII='); } #share_icons a.share_google_bookmarks {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAHmUlEQVR4Xp3XC4wV1QHG8f85M/fO3Pfe3cteZGFZFhdRVqEJKVBE28ZU+7KmTdIoiVlTmyaxrWls00pt1VibGLGNoiS1wtqkUkFra1Ftm6BaFQWfCLDQpewud124sO/7nNc5VciNi7AP/SVfkklmcr6cM5Mzx+Q0fte5paO+Lnn77ExDi8Jk444co1aKeDLC8f5hvr04wjeWN1Hj5XIcuuUWUqUyAOm5aSKzUkzGrTg4FZexwUJvaax8J/AogADYuOXJzva2hR1Lzm8lFrXZ2z2KmYgSDpsAaKBadJiXDtPYEKHGKxUYenMH0ZCBEQoRTjciQxbn4vs+fuAzfuggPVu20vfmgUeBG8R9mx/rWPW5SzovWbSQWDTCy/vz3PPyByQa6jCkoKbqBxw90McLt15GMhqi5t37N1B64CHqMzaJpnpSy9s5FxGJIqIxjPPmUgnb7NnwR/reP3KD2Pzk9p5vfnlNSyadAuC+pw/wzKCi3rKYSKHpz4/w05VZvru6hRp3fJzty5bTVmeTzMZILJqPkUowFaMxy8lilV2bn+41s5n6lng0Qo1GU3U8qsLgk6yozVNvD5xRIJxMklrxefz976H9gKBQRFghpqKO9hFJJIjYZouMWBamYVCjtcZxPDwvOCsy0OzqG+WT0itX4HkK5QWoQhlVqUwb8ieQSiPtjwqYBjXzMjEKIwU81z8rUmmEKXmtK89Ezvg4SmuUH6BcF1WtTBvDqSKUxtRoJlq9uJHCE3sZtm3iloXgY0prRqpVtFZMVOzqIilA+wqtNarsMBO+pzC11tTUZmD57AivjIwzKxolbBiYQuAqxZjjUCyXmfhMof8DTr6xm1RYoj2FDgJUucpM+H6AeeSky2rO9ItvtfPve3dwPB1gmCaB1gRKE/guolyheVaCmlfvupsEgpKn0eMeFbfAZMJRA4BQWCKlIFAacdnPtuntd1xT+7Zr+MGGF9m0qxcVi6GlBN9DVsp8Z8lstt36dQB+3vkbXtn5HJYBURPSFkgBAoEQIPiYLARkjnkANI24RFyN2V9ELLv1bzobq2PbT1afVeLB7e/xwLN76T1ZoCUT4/ovXsBt164E4OY//5atR1+gflaIeXHF0myAIZlUoCFQAgUMDUPga44eriCW/vofOp6ZQ7xcYP21S2lvTjOdm7d8OPjxl4nHJAviijXNLqbkUwm0YPceD1l1XJLxKHkrypX3v8ZND+/kmTf7GCu5nMvWN55nS/9LpOKSRAgubfYwJZ+aITRBoJGO66N9RTYWY/6CObw0JrjusXfI/ngb0bWbeb9niIlyQ8cwIxJTQNpWhKTmsxAIlAbpuz54AZ7jE8EgFQqRSCWR9Sn8kMH8xjgTXbV0Df6Aw4gL+ZJkqCz5LKQwCDRI1/NQno/rno6hBK4fgJAEpsF/9g0wUfvcNl686WGixw2GypqdR0MMjBooJT5V0Aa+r5Fl10f5AYWqw/FCkcODwxSHR3FODiIrVe5+fBfAWSXeXvc4FzlzGBhRdA8a5IYNfJ8ZRwen7xfm2o36gtZFdJ8YoV4qrm7PcklzmotbGli6IEMqZjGZ8UqR2596gOePv8jirCAb02TiAZLpRc0423cOYQrHZWHIZ921y7ju8vOZTLF/P/G5S5goGYnz+7XrKG0qsmvoLZIhKJQlMUszHS0knq+RV184i6d/dWVtcPzKOPnXt9K9+Ua61l/KwbtaTyW/5Rrev3vVqSKftP66dRijIQYKAtcD153BEigDLwD5/SsWUDN2eCf/3fg1/HfvpDG6j6YWg6b21lNpvKCV+a0BuT9dzYld286aiR9d3sFQUaMU+IGYQQGJG0zYjksD+znx3A+Z3xZHRi8EIZFhG2GaCMMEQFUzzE+mGHrnDnJjOeZ95RZqVrQto/qqouIKQgKkEExFBadnwNRKAzC4exNzFthYmWakbSMt61xvDoFtko1Y5A8/wnjPF0guWAXARU1t+K6i4pnEDI0STO3UEmjM2tZu6jx2Koq0fNBFdLXIuUgTRDLKeQvryb29qVaAo4PHCBSgNL4CwdS0MvAnLoEKAnCGwXSZjjgVh7BRoua1g++iDIktIPBBzGAG/ADMcqWC47pY865gvPt1UrMGEYZmOt6YRNmXnbFJ1UUEgQIpNFMxhYlTBe2rjwpUqToujStu5OCeJ+CDfcTTPkYk4Fy0FvgFk1zOoGnt9wD4y6vPsXtgLxc2mwQBoJmSHbJwHU3gaeTJ4ZHe/OAwAG3XP0Fx9o30dJkMHrap5MM4I6FaTl0P/c+ipy9N5quPEJ29hL193dz21w001BnYEpTS+AFTpi6c5siJAriqV/xy/YMd7YvaOr+0ajnZTD0AznCOoX3/onJsPxT7qSE+l0TrKurbr8KMJHnon1u555lOjIRLc4MgFWbSfwNDGMTDUTKRNKWC5tn3DlEqVW4QAHfe/4fOi85v7bh4cRuzMw1YVhj7w0zmwJEuntrxdw71d2OnQjQ0hLAtgWByrhfgugFH8iO81ZOjWKmcPpxyGuvu3dCRisdvz9TXtUQsm4htMe4UGC0XAMiPDrKnt4s9PV3kx4aQIUkoLAhFJFIKZkoHqldrXTue83+EARhNDn0EzgAAAABJRU5ErkJggg=='); } #share_icons a.share_google_plus {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEuklEQVR4Xr2SD0yUdRjHfxFHIqIKUyVSRQATQZWQsJZCIIEgXgS5FPVsFC010aCS2ZjqcsJsI1KH5gCUokRkimauzVaUqpHZGjINDuD+CRz3F+jk7r7dM33zXu+46/DWd/ts4/0+7/N59nKszs+PnZkymdX7+4suBk4TXw+dgT/Cg3B7QYhboZ20mxzkIie5Pdj9VITOC6lYUlgwK/LgITan9CCb+WmZW6GdtJsccyNDK8hJYrpC9OcLUdAfKYO+eDc0m0RQb8iEKjPZrdBO2k0Oct2MXQRys/MBU8R9ebmWciP6M5L+F8hFTnKzpllPQ5mzFkrhcgLqt7Oh/6wEg7XVLtG/Vkg7eNAz7c58gvbyOnKSm12ZHYi+VcuhLy2Gsf0vuBrTXQXUW3OhTE+0QVP4PrgMflXF68hJbvZ7TCRPPNRQB82O7dDu/Rh/f38B9mK43ITBmirojxyEcnU6+tIS7EJ7uAx+WcXvVyagOXg6mKxoG7ioNr+FvhXxPOiZWa+DVehvbtYh6g8f7h6oqbTprwcFgin2FNwfOF6B3uQ4u6jezbE5wqSQU+cQ9Qd5Dw84Ybv/Gv0G5A++gHLdavQkLRsR3eEyPBptyT7ezL2bN3gMt90BF6NCbtNfnhEAJi3cAkpP4lKn0BLrDH33La93NT8/Mw0eZqOJUWA2O8XQ9BOzzpNTp/J6V2Omdzq354KiiHvRKaqdH8E6hhu/OZzvz9sMLvrKYzb9jwGT6QsYGcVzdghjZjgEWh2ziZN3uAC2ncmCBx78C7yTUxjMcAzAcxvv3P7v82C8jirq//0C3imp7ImxPg5/A57Bocw6+q9rqXOA9QHgdczDg5mov/3mG+AyeK4RstiYETE0N4OL9uhR6hzSsy6b3iGg2rOL18njluKi/ySw1nVZsM5A41lIYxbboNq96+HMWW5m9MhfjsMFv4lgTbHReDSG5l/Rm5sLaXQ0ZPFxUB84AC7a8nJ6/tjIExNwbuIEsMYJ46HMz8fQpUtwlv6iIkiiotyCPCkJ5GZnxvtC9koyuhcuItCTk4O+vG3Qnajhye+1tlLvNshJbtbgOw6ylBXonr+Qh+bwYVjHKJVS5zbISW5W72M5IHUluiIW8NAc4h9AqPYXU+cWyEluVjfWB7J0ITrnRfLo2bIV9tJXuJP6x4ac5GbfeI+FTPgaOudG2DB09RrsRX+6Ad0xS2hm1JCT3Kx2jDdkGVlQZIugqawmKUxaLZzF0HILXdGx6AibNyrISW7WEBCAoZYWcBmWSKCpqIJy7z7I126AqvRz6E6dHvGIzqjnIQ4Jd4mO8AXoejUL1U+NAfslOgr61lYMd0vQW7AD4uBn7SJNE5IQ/IAOpN4lOp+LRZvlgC/ogB8iIsS3Nm2G7PU1aA+a45CO+YuhO1kP65g0WupcQpIqxNWXlqHUy0vMysaNE1VN8keLMBOStAx0RMWiPSwSbTPDRkR78hSsI0lZRZ1D2meH025ykAvHLc79Ai8Ro5QIBBXVfv64Ep+ItjXr0ZW9EfL1OSNy9533YB4YABflJyXUOYR20m5yVFpc5GTW2ecpEB3wFIgPeQpQbuGYE84HTsel4FACNT6+TudpJ+0mB7k47z8SiPuU69FMTQAAAABJRU5ErkJggg=='); } #share_icons a.share_pinterest {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFM0lEQVR4Xr3PDUyUdRwH8P/cBDSRNSW11tIZMV95E1XEiDveBXlVADXO90Vl1VxqqbWtdGu1VumsWqJBSJJrvhITFEE6hYODOw5OMWDl210mE6AGKd/u28az57nnPJOZ3+27+fz+P36fUwzl59BpurrwWR3m2Dn4P0uD1pA7gnhVoF++t+/Y/PFhCyaPS80U47N0wleX91DLm7xNgxZNwVQH++kaY0LQseM1dO7agra31uPS67mw5mU91PImb9OgRZO2qAn172jVJaNt01pYNyx7JKVFk7aoXTgdrQ6gdX3GIy1N2kI/3x+WVcloWZN63zY59k5nxaE0Ixo/JEeqejw9CueWJ8C8OuW+t2jSFjVz/fAvsCrlnm3KXYLypTE4uTYLbUdLYDcb4Spd7W1o/Ho3juck4WxWnNubNGmL6pCpaNElo8WBuGpdTgLK1mais7wU/zUDvT2wFOWjLE0L84tJLu/SpC2qgqaghUsrE1Wty47D2S2v8qAa+d2GvhaTVFe5qq/C2ZVLXN6mSVucmf0MLCsWw7I8QVFTTjyqHbhziHW+v1m1b123FPbDhbjT1wt5Ok+dRE1GlGqfJm1RMeNpWLLjYcmKU7Qye7Hqf24vKeCb217enKf6EfUff6Dao0lbnJr2FJozY9G8LEZq09JodJSdUOKHCqS3E/HhOBQ9Hz/GhsGQHsW5otyVZ6CnB/pUjcKgSVuU+U+C2QGaM6KkXsherMB7mxult9K0aNgb6wHOb1yD/hUd54q25KbAORd3blMYNGmLUr+JMKdrYU7TSG3/dBfkad/+BueoTlyErssXIU/X+Wq+qcofLY+t+IDCoElbnJg6AaZUDUzJkVJtB/dLQL/tujRv/2QnnNNrNkrv6htS+K0waNIWxyb7wpyigSkpQqqtKB8SYDK6mqvelVXv3iovVRg0aY/AIAQgBAYHpQIQQxlQzZUZsF3nm6p/3bnrdo8E7RF3MehgIIQDkCqDPJ6YKPoG/haCR3+5JJxzW1/FN1VHTZmqXITSoElbHH7ycZiWRKIxZoHUjvc2Qx79xnWco2lFCuS509PDuarno+ah99pVyHN5U57CoElblEzygSkpEsaoeVJNyVrH8W4JstXXcg7zuuWQx374IOeqntNlwimqHZq0xaEJY2FKjIQxMlTR9m2bFBhnzVvflPv8Vv1dZXgQfqusUOB//HRMtUeTtij29XZ8aGCMmKOqdXUOeowG2EuKOENncSHksXy5B5VhgahdFAJ9eDBOzQ+A/t234Zy2jRtUt2nSFkXjxsCUoEHDwhC3vRAWhFvWVsjT390N/fatOBKvRemyVJj37lbh9u+/c3mPJm1R+Phjjg8tGhYEuW1tcjweNH9esqIp+nmX92jSFt/68AdEo35uoNs273hHAdw8dsQtbjtYiEbtonveo0lb7PceDVN8DAwhAW57taJcARgjwnFx/RrcPHoE3YY6Fl1nTuPXjz6EKTH+vvdo0hb5Y0ahPloLQ7ADCpzlsjWzZ6K/+7aE91mtnA+79SFBNEFb7BvthQuaSDTMDYVh1kyXNaenQ54bBQWcD7u0aNIWX3h5dZwMCECTRou66TNctiU3F/JY0tI5H3Zp0aQt9nh66vb5+OBcxAswaaLQEBwKw4zZqPOfLrX5pZclvP/KFc4euIZpM3mbBi3QpC2Yzz08879xDEqDQ1AXmwBjQhKak1Kk2ouKh3z+m7MHLm/yNg1aNIU8n4300O0e6dGxd6QHnHvAeyzOPOePimf98JWnF2fDLg1aQ+4/1PogSIIOw84AAAAASUVORK5CYII='); } #share_icons a.share_reddit {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFaElEQVR4Xr2XezNbeRzGz3vZl7M7/W/X+mN3dU0vs7S7UEnaKpCojoa2YIWiNZALYmEpFYVAEBCC5iIaRYrcoKt99jyd2cycis7WGGfmMyff35Pf85k5FyMCD5KvM8WVtI8660wz0A3b0Da+fK6wk9100EVnBHXzYHXbuB2enRCCh8cIHn1A6JxhJ7vp6LCsgE66hfva/jjKvXuHWPe/x7LvCLatQ8y9PThX2MluOuhqNi+CbuGxcdg559mFffsIM96DC4EuOukWnr6Ywpw3BKsYXCR00i08ezkDy3oYk28OLhQ66RbqB+Zh4cIZaR9dxNCKTzJf/e0mLl26hB9iYlGl7zq1g26hzjSLcc/+maGIXBGl6aqHPENdWIjj4w+YtloRGxuLFzPuqA7RzVswizFxOAsDyz7KYV4Lo7F3Ao9r9ZyxEwhFSJPJodF1RXXQLdT0zYgF+2eiqc+ChGQZRtbCnDHiDuOnuHhoW4xwbfiw6FjD5V9/RcuILaqDbvEtsHLjmXhUq0fSnSxJVmnsx/cxPyIpJRUx4lmeU4BBVyiqg26hqmcKQ67wmchWl0FVUiPJBhxBGEZXUd01ivr+WfSt+E/toFvQdE9hUBw+xzBsQ33vBNqnXJx5RlGNHln5aiiVSjQ0NETQaDTIzlEi5W42ypo60W3bQt9qEK0TDnawK6qDbqHibwsGnOEIPeLmhGQ54uPjIZcr+FAhWSwuLCrC6uvX+PgRX2TEbEZWTi4Uyofcyw52sZMOCXQL5V0WmByhCOpqHRISE+F+8xbTs/OQKxSwL6/g/T/HX0V3Tw9kcjnWvJvsYie7JS66hbLOT1cAL1+HCHKLa1BaUQnL3CJkMjm23+1g//AoQnhpRjJ/KbMt2ZGekQmHd5udkOcWRDx00i2UdIxzQK8YEGVFI/5ITiVwuNYQCB9E2Ev8FrvffQN/7UPOEvylmcz4HUmm1RvwrFHHPnZHPHTSLTxpH8MrVxg9qyGChnEPfr6agMysbOwEwoiwsUEBPnErRpKJcI0ZkWSejS0+C+xkd8RDJ93C47ZRDuheCRJ0LQdQ2PwKlU9rsL0bkOCreoAdUbRtnTiRcY2Zr6HsRHYrTYbnk5votAciHjrpFoqMo3wlRHEwQnm7GRVV1fD69s6F1LQ0yiUOOukW1K0jGHSH0SkGhNSNryP9XiY8m+8kWG12mC3TnKPCzO70SDLOcYmp7JVAJ91CQcsw/yqh3R6I0Lywi+u3VRgan4LTux2hrKKK9xPKvHw0GVrxwjRE+JlrzDif2HNPY2CvBDrpFh40D2PYvY+/lgISnvSv4kZSKmaXnVjxbBLM2p0oLtfgl8uXKZPANWb8HiEmswXXFCrUTm6d6KeTbiHfMISRtX0YlwISdPO7yNGbce33FHT2mrDk9kqwLq5ynfDzibyytg5XFEoUD6+z7wR00i3k6QdhFodWMfic+pkd3O9ewvUMNbJy86AzdmDBsX4qloUVPG/U4+rNFKQUN+HRoAcGmz9qN510CyrtAEY9B2i2BaLSNLcnFq0jp3MBNwqqcV2Ri7QMJTJzVCgqKSeQ3b6LpHQl4mW5SK0wIv+lC5qxt+w4FTrpFrLr+zDiCqJ5MQCD7XQa5nZRavaiYGANub2OqKgHPdBMbEI7v/fFLl4BOukWMp51Y8ixh47lIHQL/v9NnfUdaqd9BE2i8Gv20kUn3YJCY3S2Trow5N6Hdt5/IdBFJ91CWqkuLq+pn/9gMkC7PchbgMY5/7miE8XspoMuOukWeKSVG6pVWhPaptwYcwf4owHWjcNzhZ3spkPV2A866Y5wq0Qbl/Zni1PxtBN3artx93nvucJOdtNB13/efwGjbFg0QpsmIQAAAABJRU5ErkJggg=='); } #share_icons a.share_stumbleupon {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAD/0lEQVR4Xr2UX2xTZRjGn+/rOaXbUDAQo2EatwGKw0gmbNG5KNzAkAlqYgISdRGd4hxqYgH/cCEoQWNM1MR4oRfeOh2ZcQZjjAEuvdAQFsTdsHWtWy/o2vXMdud8r30uTtPmjNEmo0/yS07e7/ne583p2wNfk4d7+icHumLxl9vcxEv3S0X0bZLEwc0VE3+1w40fenR8MtrTj1LFXt92eqrvHuMMviv5P0/L/OivNwT2dr5/T5jFTFCx6K7+xIF1Zm7klDjfPCuZEw9I+tiGGwJ7M4NZzIy9tasfEwcfimc+fUoyxzdL+nBTTWAWMycGHhnHRO9GL3OiU9LRpprCzFjfFteC62kzMw0lqKkknYQyDSEtAog7DxHUFm8egECLEcCgKnJZF1MTGYxfvkr4zFrVfTiILgBI5WRn8siuasOaY8NoHUoSPrPGsyp6+QMYPinAXB8zD5iNj+Ou40Oob30QKmQRPrPGM3rorQhmaxFeqGzinLoJt7/yMaAUAlKKZ/RU8Qb8ASokfN9W6EgDfLl//ULgi2f0VPcT+AVzfaxVjSiV8/VrBKWyV99R3RJWszRKaZTJdQkkmwLFjpY7W9UiaohUOnHgY+XXZ9/ugvNlHzLRduR/+7bSfv4OcAwrMFk6ni6Sn82zFpDvzcSScM6PIHtlEvkMveCd0h6B/sxktjYswIIYVQAEiNyMxuFkkbrtB1gPyPevPjkCe1svVhz6CpHuF1njndIerJUBZZUsobYDEwYkgArWCMLr27DihfdR19kDXbf8WvfL0XbJACGr3OR55TkeYHIu4Al8iZNecFh6xDX0Lj5AqPgGBErZ5cvhZCG5OfiKdO5GqKkDy9q74Yvnvt9cnYYvu7UTiNyG8KatAW8ZygazcbFnrcy88bCknl8rqeeai+T/OCOLKXf2u6I3d25wMSt70VdG+s0uufR0q3AHuBCBj9F/g59Acg4WkmRnkBv6vOjN/fAZawgK7MFewTegufj+dyAUXEITG0P25H6YyTGUyrsyiuwHe2GSMd/L50JtH89QIt5lD/ZacAmZjQs7W2Qm2i2p/eskta85yDMtkjnymMye6pVMdAdri0EPvbzDu9f2Hdkpo09uEFzoLuzA0d0F83pJ7W2pGZl39sjFJ+7lDghU8W9YQ7RV+im2AaMKoDaIYiYEChohyxjDRQxDBDUBdh08z8Bo7WldXz81l0xC33pnzd4As5gZCkfi2l658sPpS/+IV38LQk2tUJEGAGrpdwCKvZkBZjHTalj+kQbwhd24Znj83Flxsnmou9thb9kOu2PHksKe7O0482AWM5nNAag9y1qaB6anEomx87+bv8/8hMs//7iksCd7T/8bTzCLmQz+HxfKHLUKJ7/QAAAAAElFTkSuQmCC'); } #share_icons a.share_tumblr {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADiElEQVR4Xr2VWWgTaxiGv+TunHNzOOdOEetaW9VaNVq7pI2GWrFpa0uqXWxiuqhtNVK7WZW6b0VFFIWiREERqICgioCCqOiNoEBBVKFeKeCNVYtbZl7nDSSq2eaH4BeeCfMxvM/PP9/PSLjm55d684urR0qqmlBW04oqz+aUwkxm00FX2GvlZWFRRSAjIyOwocmbtq2zXXr866WzrTGlMJPZdMycNTNAp7Dm5a3wrvZ14NLlmzh74Sr2HzuPHQcG0dV/MqUwk9l00FXt8YNuWby0cmTvwCD2HQmgY8fxPwJddNItjtJ6bNt7Gv6+Y6bZNXAWJ84MhWFPGTrplqWuBrT3DKC994hprt96gJ+rpWM/+0rQSbc4y71o7TqsxJPh59CBCCvX+NlXhm5ZYqxi3ZaDpukztk7T9F9w1bYxQxm6rbqmiRFimiqXQ36vYDCUoQzdYi+pgW/TnqS0dQ/g7sMnCGp6FMVVzcxQhm7JX7YKnrZdCblx+yFGP4zhW1AzCzOSQrfkOt2o39CfkK9BTZmi0ga4fT0Jc+m2anroXSQEEGU0zlaSXLqtunHRkiCAMiG5riXMpVvmF5bD3bgVbl9vQnKcboLPX4NRsB8LV50/bh6ddEt2gcu46UWlt9sUn758i8LmqOQzStBJtxW6LhYIt8QUsUoLb7UCdCI8Ayz+myDuwDFHBZYeXoDForAAkSh0TU3+s9OqA2IJb6MJBIgiNO2K0El3aAZYZr8FAKLQ+YwiLERegVhMDc7///0b8xX8/c9fykNIJ90yPduB2uatKKpcb4oPY1+iuDh0DfmuxkhGdUsfrty8lzCHTrplalYh6oybgvIWU4wawli8fvsO9x8N48WrN5HerDxX3Bw66ZYpc+yob+5DblmTKS7fuIPRj59NkTa7IG4OnXT/OIaaborT54bk8fBTQfJfwpzIMQQg0DVhadCT8v7jmGzpPyq7D52Qp89eRp1K9k4NnpdlZbWCOBlWqyXkpFvSMnNR5+uEwxiMeSUNSmTmVWDibDvGpy/6Bfam2UqQ5ayJmUEXnXTLpMzFI8srPHCv7cLc4jV/BLropFsmZ+Z40+faUePZjGpfNworWmBb7sEcZ11KyTbEzKaDrvQsO+jmAmRSRk6AjdKVXqxd14vG1u1o3rgzpTCT2XTQRSfd4QUQ78QZC0cmpNswfroN46YtSCnMZDYddNFJ93f5E2V6eYk6zwAAAABJRU5ErkJggg=='); } #share_icons a.share_twitter {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEs0lEQVR4Xr2UD2iUdRjHv7/f+97f3W6nM91k6kpJgsygMAojJEAsS1kwUYwRRAVOiwASCMioiFVRa0STGmqMzCREWxlaOnBzf1C0beJ0Y+r+b3rOtp13997v97SH2yG3u9t5OfeB7x3vGN8PPO/znMAk2/c1lwatyE5LqTxNwkCmCIIpBKZDClKGMPpsplFWUbKqAjHeqGo4tPn7Br3v7CCd7hmncwOB+xLuZseWCRc7wZTubSwtrqzXv7T56YvTw7TjaC+9WdNzX8Ld7DjQ6qdNu+v0tj2NpSipPNX34bEu2v5HD5UcujorYRc7X9996hqKy0+od2qu0Ku/zm7YWfJtbcS0tJY3bmsoCMwm/iCgAUOS0ghbClrpewppjQVuAws9JgyitB3hiAaRhtQgKJ0Yn9OABPg5bZ4qyMI3Ly7Gl+sW4bO1BajcsAQbH/EBBCzOsWPryrlAsi4i4KWyP6nox3Z6pfpyXGraR6ixe4w2/dTBzynzdd0ApeK2pYgpr++nLT93Jjg2lR8nqUmDo3V8lngNrCrIwvtr8uG1C/5bQgxBeO2JeUiF05QY+jeI5xa7UeizJThIEyR/6CQJhCwwjy5w46v1hXh+qReCEPc/S7w2ZDsMTEeO244jrUNo6w8kOIgIUhNBTUlEazRf8SOG12lg29N5+K7oQWx+fB6W5jogBGDoENKxv/4y/m7rhUKih91Y+/ERWvdDC71Q1RaX9RWnqGt4jFJhKU03A2FKR1VdF/clTdHnv1P0FajE7Hp5BUaDFlJhSgGfy3YX9859yUME8BImnEdEaXQMB7CiwId75dL1YMrz5UWURARBNGU7NarrLmMkEMa94B8LotMfTnpBAhS9Aq34AfEbSoA/BOyoPoOJPcD/5eg/vdAaSa8s5pREgARh6jlKRxbGQhodQ6MYD0WQKZbSOHB2MKk8Jibib4pOQBHvwp1oAoYsOzwuB7IcJjJlz8mLuG6Z3JU0ELEduDOOhKiIhZ37m3HiwgAyoa3bj6rTPSBhpJ4AxOQE+AGUdFFg2DCuTLxb3YTiipNo77+FdHTfGMPb++pBrhzuSBkJnnLsCgDoFKeSmzsHRc8sx6fFT2J5fg6m4/zV69ha/hdG5IScBHekjJicgEmkIQWi20oEpjDXhbdWF+CxhR7Mz7YjHfyDVXnsAvbWXoTpWwDDsPOYMR1CAKQ0ZPR9xO9A58AtvHewFR/91o6jLQMYHg1iKoFQBPWXBrHr4Bms+eAwqmo7YfgWQphOlqeNBGIToMkJ6MkJMBLhUADHz3Xh2NkOgAhelw0+tw2mFBi9HcbgrQA3QBo2GO5s2L0eMNxzNxggkAZMlvI4WM47EAN2N4yJgKK/jONEGAsRojhg83khpASEBMP7kgkxJ+/AnStIVcISgTiIw07SYDKVS0EgUpCGhNZKQQrwBGYl/BqVFYEhhJJZbteg/+YI8r0OnsCshF3sdLqcfTLXl/NJ97VrlG1qLHvABacpQMSvY2bDndzNDnax0+PxlEkAFQ8XzD987nwLWaMjWJHnwuqH5uDZZXNnNNzJ3exgFzvZLRFl48pli3ZExm/2t7e26OamZjQ2NM1ouJO72cEudrL4P74uWBMV03CNAAAAAElFTkSuQmCC'); } /* TipTip CSS - Version 1.2 */ #tiptip_holder { display: none; position: absolute; top: 0; left: 0; z-index: 99999; } #tiptip_holder.tip_top { padding-bottom: 5px; } #tiptip_holder.tip_bottom { padding-top: 5px; } #tiptip_holder.tip_right { padding-left: 5px; } #tiptip_holder.tip_left { padding-right: 5px; } #tiptip_content { font-size: 11px; color: #fff; text-shadow: 0 0 2px #000; padding: 4px 8px; border: 1px solid rgba(255,255,255,0.25); background-color: rgb(25,25,25); background-color: rgba(25,25,25,0.92); background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(transparent), to(#000)); border-radius: 3px; -webkit-border-radius: 3px; -moz-border-radius: 3px; box-shadow: 0 0 3px #555; -webkit-box-shadow: 0 0 3px #555; -moz-box-shadow: 0 0 3px #555; } #tiptip_arrow, #tiptip_arrow_inner { position: absolute; border-color: transparent; border-style: solid; border-width: 6px; height: 0; width: 0; } #tiptip_holder.tip_top #tiptip_arrow { border-top-color: #fff; border-top-color: rgba(255,255,255,0.35); } #tiptip_holder.tip_bottom #tiptip_arrow { border-bottom-color: #fff; border-bottom-color: rgba(255,255,255,0.35); } #tiptip_holder.tip_right #tiptip_arrow { border-right-color: #fff; border-right-color: rgba(255,255,255,0.35); } #tiptip_holder.tip_left #tiptip_arrow { border-left-color: #fff; border-left-color: rgba(255,255,255,0.35); } #tiptip_holder.tip_top #tiptip_arrow_inner { margin-top: -7px; margin-left: -6px; border-top-color: rgb(25,25,25); border-top-color: rgba(25,25,25,0.92); } #tiptip_holder.tip_bottom #tiptip_arrow_inner { margin-top: -5px; margin-left: -6px; border-bottom-color: rgb(25,25,25); border-bottom-color: rgba(25,25,25,0.92); } #tiptip_holder.tip_right #tiptip_arrow_inner { margin-top: -6px; margin-left: -5px; border-right-color: rgb(25,25,25); border-right-color: rgba(25,25,25,0.92); } #tiptip_holder.tip_left #tiptip_arrow_inner { margin-top: -6px; margin-left: -7px; border-left-color: rgb(25,25,25); border-left-color: rgba(25,25,25,0.92); } /* Webkit Hacks */ @media screen and (-webkit-min-device-pixel-ratio:0) { #tiptip_content { padding: 4px 8px 5px 8px; background-color: rgba(45,45,45,0.88); } #tiptip_holder.tip_bottom #tiptip_arrow_inner { border-bottom-color: rgba(45,45,45,0.88); } #tiptip_holder.tip_top #tiptip_arrow_inner { border-top-color: rgba(20,20,20,0.92); } } {% end %} ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/footer.thtml����������������������������������������������������������0000644�0001750�0001750�00000002124�12301071671�020562� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" id="footer_container"> <footer class="wrapper"> <div class="footer_info"> <!--! Site footer = '.lazygal': section: '[template-vars]', variable: 'footer' --> <h3 class="footer" py:if="footer" py:content="footer" /> <div class="footer" id="lazygalfooter"> <p>${_('Generated by')} <a href="http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal">lazygal</a> ${gen_datetime.strftime(_('on %c'))}.</p> </div> </div> <!--! Simple theming selection --> <ul py:if="display_theme_selector" class="simple_theme"> <li><a class="theme_loader light_theme" href="javascript:;">light</a></li> <li><a class="theme_loader dark_theme" href="javascript:;">dark</a></li> </ul> </footer> <!--! Additional javascript --> <script src="${rel_root}shared/jquery.js"></script> <script src="${rel_root}shared/plugins.js"></script> <script src="${rel_root}shared/scripts.js"></script> </div> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_plugins.tjs����������������������������������������������������0000644�0001750�0001750�00000011203�12301071671�021441� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// place any jQuery/helper plugins in here, instead of separate, slower script files. {% if display_social_buttons %} /* * TipTip v1.3 - Copyright 2010 Drew Wilson * code.drewwilson.com/entry/tiptip-jquery-plugin * This TipTip jQuery plug-in is dual licensed under the MIT and GPL licenses */ (function($){$.fn.tipTip=function(options){var defaults={activation:"hover",keepAlive:false,maxWidth:"200px",edgeOffset:3,defaultPosition:"bottom",delay:400,fadeIn:200,fadeOut:200,attribute:"title",content:false,enter:function(){},exit:function(){}};var opts=$.extend(defaults,options);if($("#tiptip_holder").length<=0){var tiptip_holder=$('<div id="tiptip_holder" style="max-width:'+opts.maxWidth+';"></div>');var tiptip_content=$('<div id="tiptip_content"></div>');var tiptip_arrow=$('<div id="tiptip_arrow"></div>');$("body").append(tiptip_holder.html(tiptip_content).prepend(tiptip_arrow.html('<div id="tiptip_arrow_inner"></div>')))}else{var tiptip_holder=$("#tiptip_holder");var tiptip_content=$("#tiptip_content");var tiptip_arrow=$("#tiptip_arrow")}return this.each(function(){var org_elem=$(this);if(opts.content){var org_title=opts.content}else{var org_title=org_elem.attr(opts.attribute)}if(org_title!=""){if(!opts.content){org_elem.removeAttr(opts.attribute)}var timeout=false;if(opts.activation=="hover"){org_elem.hover(function(){active_tiptip()},function(){if(!opts.keepAlive){deactive_tiptip()}});if(opts.keepAlive){tiptip_holder.hover(function(){},function(){deactive_tiptip()})}}else if(opts.activation=="focus"){org_elem.focus(function(){active_tiptip()}).blur(function(){deactive_tiptip()})}else if(opts.activation=="click"){org_elem.click(function(){active_tiptip();return false}).hover(function(){},function(){if(!opts.keepAlive){deactive_tiptip()}});if(opts.keepAlive){tiptip_holder.hover(function(){},function(){deactive_tiptip()})}}function active_tiptip(){opts.enter.call(this);tiptip_content.html(org_title);tiptip_holder.hide().removeAttr("class").css("margin","0");tiptip_arrow.removeAttr("style");var top=parseInt(org_elem.offset()['top']);var left=parseInt(org_elem.offset()['left']);var org_width=parseInt(org_elem.outerWidth());var org_height=parseInt(org_elem.outerHeight());var tip_w=tiptip_holder.outerWidth();var tip_h=tiptip_holder.outerHeight();var w_compare=Math.round((org_width-tip_w)/2);var h_compare=Math.round((org_height-tip_h)/2);var marg_left=Math.round(left+w_compare);var marg_top=Math.round(top+org_height+opts.edgeOffset);var t_class="";var arrow_top="";var arrow_left=Math.round(tip_w-12)/2;if(opts.defaultPosition=="bottom"){t_class="_bottom"}else if(opts.defaultPosition=="top"){t_class="_top"}else if(opts.defaultPosition=="left"){t_class="_left"}else if(opts.defaultPosition=="right"){t_class="_right"}var right_compare=(w_compare+left)<parseInt($(window).scrollLeft());var left_compare=(tip_w+left)>parseInt($(window).width());if((right_compare&&w_compare<0)||(t_class=="_right"&&!left_compare)||(t_class=="_left"&&left<(tip_w+opts.edgeOffset+5))){t_class="_right";arrow_top=Math.round(tip_h-13)/2;arrow_left=-12;marg_left=Math.round(left+org_width+opts.edgeOffset);marg_top=Math.round(top+h_compare)}else if((left_compare&&w_compare<0)||(t_class=="_left"&&!right_compare)){t_class="_left";arrow_top=Math.round(tip_h-13)/2;arrow_left=Math.round(tip_w);marg_left=Math.round(left-(tip_w+opts.edgeOffset+5));marg_top=Math.round(top+h_compare)}var top_compare=(top+org_height+opts.edgeOffset+tip_h+8)>parseInt($(window).height()+$(window).scrollTop());var bottom_compare=((top+org_height)-(opts.edgeOffset+tip_h+8))<0;if(top_compare||(t_class=="_bottom"&&top_compare)||(t_class=="_top"&&!bottom_compare)){if(t_class=="_top"||t_class=="_bottom"){t_class="_top"}else{t_class=t_class+"_top"}arrow_top=tip_h;marg_top=Math.round(top-(tip_h+5+opts.edgeOffset))}else if(bottom_compare|(t_class=="_top"&&bottom_compare)||(t_class=="_bottom"&&!top_compare)){if(t_class=="_top"||t_class=="_bottom"){t_class="_bottom"}else{t_class=t_class+"_bottom"}arrow_top=-12;marg_top=Math.round(top+org_height+opts.edgeOffset)}if(t_class=="_right_top"||t_class=="_left_top"){marg_top=marg_top+5}else if(t_class=="_right_bottom"||t_class=="_left_bottom"){marg_top=marg_top-5}if(t_class=="_left_top"||t_class=="_left_bottom"){marg_left=marg_left+5}tiptip_arrow.css({"margin-left":arrow_left+"px","margin-top":arrow_top+"px"});tiptip_holder.css({"margin-left":marg_left+"px","margin-top":marg_top+"px"}).attr("class","tip"+t_class);if(timeout){clearTimeout(timeout)}timeout=setTimeout(function(){tiptip_holder.stop(true,true).fadeIn(opts.fadeIn)},opts.delay)}function deactive_tiptip(){opts.exit.call(this);if(timeout){clearTimeout(timeout)}tiptip_holder.fadeOut(opts.fadeOut)}}})}})(jQuery); {% end %} ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/image.thtml�����������������������������������������������������������0000644�0001750�0001750�00000010465�12301071671�020355� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude" id="image_container"> <!--! Display image --> <div class="image_position"> <py:choose test=""> <py:when test="next_link"> <a href="$next_link.link" title="${_('next')}"> <img class="image_file" src="$img_src" width="$img_width" height="$img_height" alt="Image $image_name" /> </a> </py:when> <py:otherwise> <img class="image_file" src="$img_src" width="$img_width" height="$img_height" alt="Image $image_name" /> </py:otherwise> </py:choose> </div> <div id="media_options" class="wrapper clearfix"> <!--! Display social buttons = '.lazygal': section: '[template-vars]', variable: 'display_social_buttons' --> <div id="share" py:if="display_social_buttons"> <div id="share_icons"> <p>Share:</p> <a href="#" class="share_facebook" title="Post to Facebook" target="_blank"></a> <a href="#" class="share_twitter" title="Post to Twitter" target="_blank"></a> <a href="#" class="share_google_plus" title="Post to Google+" target="_blank"></a> <!--! FIXME: Go straight to posting photo on Thumblr, need 'source' (direct image link) http://www.tumblr.com/docs/en/buttons http://www.tumblr.com/share/photo?clickthru=IMAGE_PAGE_URL&source=IMAGE_URL --> <a href="#" class="share_tumblr" title="Add to Tumblr" target="_blank"></a> <a href="#" class="share_reddit" title="Add to Reddit" target="_blank"></a> <!--! FIXME: Link to Pinterest need 'media' (direct image link) <a href="https://pinterest.com/pin/create/button/?url=IMAGE_PAGE_URL&media=IMAGE_URL" class="share_pinterest" title="Post to Pinterest" target="_blank"></a> --> <a href="#" class="share_google_bookmarks" title="Add to Google Bookmarks" target="_blank"></a> <a href="#" class="share_stumbleupon" title="Post to StumbleUpon" target="_blank"></a> <a href="#" class="share_delicious" title="Add to Delicious" target="_blank"></a> </div> <noscript> <p>Javascript is not enabled, social media share links won't work</p> </noscript> </div> <!--! Display previous image thumb --> <div class="prevnext" id="prev_link" py:if="prev_link"> <xi:include href="thumb.thtml" py:with="media=prev_link" /> <a class="prevnext_text" href="$prev_link.link" title="${_('previous')}"></a> </div> <!--! Display next image thumb --> <div class="prevnext" id="next_link" py:if="next_link"> <xi:include href="thumb.thtml" py:with="media=next_link" /> <a class="prevnext_text" href="$next_link.link" title="${_('next')}"></a> </div> <!--! Display image info --> <div id="image_caption" py:if="publish_metadata or original_link"> <div class="image_comment" py:if="comment">$comment</div> <div class="image_original_link" py:if="original_link"><a href="$original_link">${_('Original picture')}</a></div> <div class="image_date" py:if="image_datetime"> ${_('Taken')} ${image_datetime.strftime(_('on %d/%m/%Y at %H:%M'))} </div> <div class="authorship" py:if="authorship">${_('Author')} : $authorship</div> <div class="image_caption_tech"> <ul> <li>$image_name</li> <li py:if="camera_name">${_('Camera:')} $camera_name <py:if test="lens_name"> ${_('with')} $lens_name</py:if> </li> <li py:if="exposure">${_('Exposure')} $exposure</li> <li py:if="iso">${_('Sensitivity ISO')} $iso</li> <li py:if="fnumber">${_('Aperture')} $fnumber</li> <li py:if="flash">${_('Flash')} $flash</li> <li py:if="focal_length">${_('Focal length')} $focal_length</li> </ul> </div> </div> </div> <!-- #media_options --> </div> <!-- #image_container --> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_scripts.tjs����������������������������������������������������0000644�0001750�0001750�00000006745�12301071671�021466� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/** * Load page where link in given element id points. */ function gotopage(divid) { var div = $('#' + divid); if (div) { var link = div.children('a')[0]; if (link) { document.location = link; } } } $(document).keypress(function(e) { switch(e.keyCode) { case 37: /* Left */ gotopage('prev_link'); break; case 38: /* Up */ gotopage('index_link'); break; case 39: /* Right */ case 13: /* Enter */ case 32: /* Space */ gotopage('next_link'); break; } }); {% if display_theme_selector %} /** * Change background colors */ $(document).ready( function() { /* THEME LOADER */ $(".theme_loader").click (function () { // Clear current value var curVal = readCookie('simple_theme'); $("body").removeClass("simple_theme_" + curVal); // Udpate value var val = $(this).html(); $("body").addClass("simple_theme_" + val); createCookie('simple_theme', val, 365); }); }); $(document).ready( function() { document.body.className = "simple_theme_" + readCookie("simple_theme"); }); // cookie functions http://www.quirksmode.org/js/cookies.html function createCookie(name, value, days) { if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name + "=" + value + expires + "; path=/"; } function readCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i=0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0)==' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); } return null; } function eraseCookie(name) { createCookie(name, "", -1); } // /cookie functions {% end %} {% if display_social_buttons %} /** * Set social buttons proper link */ // Show social icons only if javascript is turned on $('#share_icons').show(); encoded_location_href = encodeURIComponent(location.href); $('.share_delicious').attr('href', 'https://delicious.com/post?url=' + encoded_location_href) $('.share_facebook').attr('href', 'https://www.facebook.com/share.php?u=' + encoded_location_href) $('.share_google_bookmarks').attr('href', 'https://www.google.com/bookmarks/mark?op=edit&bkmk=' + encoded_location_href) $('.share_google_plus').attr('href', 'https://plus.google.com/share?url=' + encoded_location_href) /* $('.share_pinterest').attr('href', 'https://pinterest.com/pin/create/button/?url=' + encoded_location_href + '&media=IMAGE_URL') */ $('.share_reddit').attr('href', 'http://reddit.com/submit?url=' + encoded_location_href) $('.share_stumbleupon').attr('href', 'https://www.stumbleupon.com/submit?url=' + encoded_location_href) $('.share_tumblr').attr('href', 'http://www.tumblr.com/share?v=3&s=&u=' + encoded_location_href) $('.share_twitter').attr('href', 'https://twitter.com/intent/tweet?url=' + encoded_location_href) // Share buttons tooltip $(function(){ $('#share_icons').children().not('p').tipTip({defaultPosition: 'top', delay: 300, maxWidth: 'auto'}); }); {% end %} {% if google_analytics_tracking_id %} /** * Track image_original_link clicks */ $('.image_original_link a').click(function(){ _gaq.push(['_trackEvent', 'image_original_link', $(this).attr('href'), 'Show original image']); }); {% end %} ���������������������������lazygal-0.8.2/themes/inverted/browse.thtml����������������������������������������������������������0000644�0001750�0001750�00000003046�12301071671�020571� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE HTML> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude"> <!--! <head> --> <xi:include href="header.thtml"/> <body> <!--! Main site content --> <div id="main_container"> <div id="main" class="wrapper clearfix"> <!--! Breadcrumbs --> <div class="breadcrumbs"> <ul> <li py:for="webgal in webgal_path"> <a href="${webgal.link}" py:content="webgal.name + '     » '" /> </li> <li> <a class="bc_current" href="" py:content="name" /> </li> </ul> </div> <!--! Image size options --> <div class="image_size" py:if="len(osize_links) > 1"> <ul> <li py:for="osize_link in osize_links"> <span py:if=" not osize_link.link" href="$osize_link.link">$osize_link.name</span> <a py:if="osize_link.link" href="$osize_link.link">$osize_link.name</a> </li> </ul> </div> </div> <!-- #main --> <!--! Display media (image or video) --> <xi:include href="${mediatype}.thtml" /> </div> <!-- #main_container --> <!--! Site footer --> <xi:include href="footer.thtml" /> </body> </html> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/manifest.json���������������������������������������������������������0000644�0001750�0001750�00000000074�12301071671�020715� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "shared": [ { "path": "../default/SHARED_jquery.js" } ] } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/gallerylink.thtml�����������������������������������������������������0000644�0001750�0001750�00000002621�12301071671�021603� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" class="sub_gallery_link"> <!--! Display thumb for sub-gallery --> <div py:if="subgal_link.album_picture" class="sub_gallery_image"> <a href="$subgal_link.link"> <img src="$subgal_link.album_picture" alt="$subgal_link.album_name album picture" /> </a> </div> <!--! Display sub-gallery info --> <div class="sub_gallery_name"> <!--! Sub-gallery title = 'sub_gallery/album_description': 'Album name' --> <h4 class="sub_gallery_name"> <a href="$subgal_link.link" py:content="subgal_link.album_name" /> </h4> <!--! Display sub-gallery stats --> <div py:if="subgal_link.image_count > 0 or subgal_link.subgal_count > 0" class="sub_gallery_stats"> <py:if test="subgal_link.image_count > 0"> $subgal_link.image_count ${_('photos')} <py:if test="subgal_link.subgal_count > 0">, </py:if> </py:if> <py:if test="subgal_link.subgal_count > 0"> $subgal_link.subgal_count ${_('sub-galleries')} </py:if> </div> <!--! Sub-gallery description = 'sub_gallery/album_description': 'Album description' --> <div py:if="subgal_link.album_description" class="sub_gallery_description"> <p>$subgal_link.album_description</p> </div> </div> </div> ���������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/dirindex.thtml��������������������������������������������������������0000644�0001750�0001750�00000010440�12301071671�021072� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE HTML> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude"> <!--! <head> --> <xi:include href="header.thtml"/> <body> <!--! Site title = 'album_description': 'Album name' --> <div id="header_container"> <header class="wrapper clearfix"> <h1 id="site_title" py:content="album_name" /> </header> </div> <!--! Main site content --> <div id="main_container"> <div id="main" class="wrapper clearfix"> <!--! Album description = 'album_description': 'Album description' --> <div class="album_description"> <p py:if="album_description" py:content="album_description" /> </div> <!--! Breadcrumbs --> <div class="breadcrumbs"> <ul> <li py:for="webgal in webgal_path"> <a href="${webgal.link}" py:content="webgal.name + '     » '" /> </li> <li> <a class="bc_current" href="" py:content="album_name" /> </li> </ul> </div> <!--! Image size options --> <div class="image_size" py:if="len(osize_index_links) > 1"> <ul> <li py:for="osize_index_link in osize_index_links"> <span py:if=" not osize_index_link.link">$osize_index_link.name</span> <a py:if="osize_index_link.link" href="$osize_index_link.link">$osize_index_link.name</a> </li> </ul> </div> <!--! List of sub-galleries --> <div id="sub_gallery_links"> <py:for each="subgal_link in subgal_links"> <xi:include href="gallerylink.thtml" /> </py:for> </div> <!--! Display media (image or video) --> <div py:for="subdir, medias in medias" py:if="medias" class="media_links"> <py:if test="not subdir.is_main"> <h2><a name="$subdir.id"></a>$subdir.album_name</h2> <div py:if="subdir.album_description" class="sub_gallery_description"> <p>$subdir.album_description</p> </div> </py:if> <!--! Pagination --> <div class="pagination" py:if="len(onum_index_links) > 1"> <ul> <li py:for="onum_index_link in onum_index_links"> <span py:if="not onum_index_link.link" class="pg_current">${onum_index_link.name + 1}</span> <a py:if="onum_index_link.link" href="$onum_index_link.link">${onum_index_link.name + 1}</a> </li> </ul> </div> <!--! Display media thumbs --> <div id="media_thumbs"> <xi:include href="thumb.thtml" py:for="media in medias" /> </div> <!--! Pagination --> <div class="pagination" py:if="len(onum_index_links) > 1"> <ul> <li py:for="onum_index_link in onum_index_links"> <!---! <a py:strip="not onum_index_link.link" --> <span py:if="not onum_index_link.link" class="pg_current">${onum_index_link.name + 1}</span> <a py:if="onum_index_link.link" href="$onum_index_link.link">${onum_index_link.name + 1}</a> </li> </ul> </div> <!--! Zipped folder --> <div class="directory_zip" py:if="subdir.dirzip"> <a class="zip_link" href="$subdir.dirzip">${_('All full scale pictures as an archive, for')} "$subdir.album_name"</a> ($subdir.dirzip_size). </div> </div> </div> <!-- #main --> </div> <!-- #main_container --> <!--! Site footer --> <xi:include href="footer.thtml" /> </body> </html> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_simple_theme_dark.css������������������������������������������0000644�0001750�0001750�00000020207�12301071671�023430� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* =============== Dark Theme =============== */ body.simple_theme_dark { color: #dddddd; background-color: #0e0e0e; } body.simple_theme_dark a:link { color: #ff7733; } body.simple_theme_dark a:active { color: #ff7733; } body.simple_theme_dark a:visited { color: #ff7733; } body.simple_theme_dark a:hover { color: #ff7733; } body.simple_theme_dark div#site_title { color: black; } body.simple_theme_dark ::-moz-selection { background: #bb7711; color: #000; } body.simple_theme_dark ::selection { background: #bb7711; color: #000; } /* =============== Main site content =============== */ body.simple_theme_dark header, body.simple_theme_dark div#main, body.simple_theme_dark div#media_options, body.simple_theme_dark footer { background-color: #000000; border: 1px solid #282828; -webkit-box-shadow: 0 2px 5px 0 #333333; box-shadow: 0 2px 5px 0 #333333; } /* =============== Breadcrumbs =============== */ body.simple_theme_dark .breadcrumbs ul { background: #0e0e0e; border-color: #333333; -webkit-box-shadow: inset 0 2px 5px 0 rgba(255,255,255,.2); box-shadow: inset 0 2px 5px 0 rgba(255,255,255,.2); } body.simple_theme_dark .breadcrumbs ul li a:hover { border-color: #333333; -webkit-box-shadow: 0 2px 5px 0 rgba(255,255,255,.2); box-shadow: 0 2px 5px 0 rgba(255,255,255,.2); } body.simple_theme_dark .breadcrumbs ul li a, body.simple_theme_dark .breadcrumbs ul li span { color: #bbb; text-shadow: 0 1px 0 rgba(0,0,0,.5); } body.simple_theme_dark .breadcrumbs ul li a:hover { background: #000; } body.simple_theme_dark .breadcrumbs ul .bc_current, body.simple_theme_dark .breadcrumbs ul .bc_current:hover { background: none; } /* =============== Image size select =============== */ body.simple_theme_dark .image_size ul li:first-child a, body.simple_theme_dark .image_size ul li:first-child span { /* Reduced Size Orange */ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABOElEQVQ4jbWTQUrDQBSGv5lMbBZSaWJABPeewI2HSEFwoZ7AVevagohbceEVFF3WQ3gQd4XQ1HYT2yYzLsbGamKKggMDw//e/7/5570RAOYsjHjjHAgATf2SwJAGV+ImfhKmG0ZM6ZfSjAadV9AdENKeG7QlU3qlJJ3DeAjKA6k+t/IsvhCe0lOAX6o8SeDe/GziWMBGAEL6suRZ59DaqnkCbNzeQsvKBJ3VCyzFqwV+sf5JoKp9X1iqQiCJIR3D5BVcr15gNLDzAKiC/GDg9hCCHTi6tq1qboL4Vnk0gKZfDJMVyIF8BgeXsL0LF3vgOmAyWB4HnS36X0CqsKGFJT/fwXoAzho4br0VkBJIUEDyYqH9Ezh9hDRdRQZIhOmEEXP6JDHMP2AXaIWr6R5tAWA6YcTsb9/5HX9aX4VJfhYmAAAAAElFTkSuQmCC'); } body.simple_theme_dark .image_size ul li:last-child a, body.simple_theme_dark .image_size ul li:last-child span { /* Full Size Orange */ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABO0lEQVQ4ja2TvUoDQRDHfzu3+bJIvAsHYiMWPoUPEUtFX0Ew2hoRYm9nZal2gj6EjU9gYa2RcBcVi0vuy2JNcpDjTsSBgZnZ/f+WmWEVQHrodgg4BtpAQrEJ4FHjTJ0P71XadTuMuSsRwWgI4U9cAWwXamxpxvR+Jb58h6WWyd+eobsBttsTwCkFNBpwsQ0PVyZ31iAykZDtOU0gDnN8Al8ebO7ByxNIOlUkMhMnMXx4oOsgeu5KQxjD6SPcHMHtCVhVsIxMz17+9OF6Rl60/VXwXqFehR0FjpsBJDHYK8VzCANoLoNVgca8nGkhKgaIlV8uVpXbPwJEl9zMPxdAEAtGg2LAaJA3B9GAj5J1mg7sKrON7EBFG3HTAbXQsS9U6aMw9FYbosAAph4Fpp63hTp9BZAeuB0mf/vO38krZYT7X62kAAAAAElFTkSuQmCC'); } /* =============== Pagination =============== */ body.simple_theme_dark .pagination a { border: 1px solid #222; color: #555; } body.simple_theme_dark .pagination a:hover, body.simple_theme_dark .pagination a:active { border: 1px solid #5f5f5f; } body.simple_theme_dark .pagination span.pg_current { border: 1px solid #303030; background-color: #0f0f0f; color: #bbb; } body.simple_theme_dark .pagination span.pg_disabled { border: 1px solid #0c0c0c; color: #333; } /* =============== List of sub-galleries =============== */ body.simple_theme_dark .sub_gallery_link { background-color: #000000; border-color: #232323 #232323 #1e1e1e; } body.simple_theme_dark .sub_gallery_image img { background-color: #000000; border: 0 solid #999999; } body.simple_theme_dark h4.sub_gallery_name a:link { color: #ffffff; } body.simple_theme_dark h4.sub_gallery_name a:active { color: #ffffff; } body.simple_theme_dark h4.sub_gallery_name a:visited { color: #ffffff; } body.simple_theme_dark h4.sub_gallery_name a:hover { color: #ffffff; } body.simple_theme_dark .sub_gallery_stats { color: #cccccc; } body.simple_theme_dark .sub_gallery_description { color: #cccccc; } /* =============== Thumbs =============== */ body.simple_theme_dark .thumb_image { background-color: #0e0e0e; border: 1px solid #232323; } body.simple_theme_dark .thumb_image:hover { background-color: #080808; -webkit-box-shadow: 0 0 5px rgba(255,255,255, 0.2); box-shadow: 0 0 5px rgba(255,255,255, 0.2); } body.simple_theme_dark .image_file:hover { background-color: #0e0e0e; } /* =============== Big image =============== */ body.simple_theme_dark .image_file { background-color: #000000; border: 1px solid #282828; -webkit-box-shadow: 5px 0 5px -5px #333333, -5px 0 5px -5px #333333; box-shadow: 5px 0 5px -5px #333333, -5px 0 5px -5px #333333; } body.simple_theme_dark div#image_caption { background-color: #060606; border: 1px solid #292929; } /* =============== Footer =============== */ body.simple_theme_dark .footer { color: #999999; } /* =============== Simple themes =============== */ body.simple_theme_dark .light_theme { color: #0088CC; background-color: #F1F1F1; border: 1px solid #D7D7D7; -webkit-box-shadow: 0 2px 5px 0 #CCCCCC; box-shadow: 0 2px 5px 0 #CCCCCC; } body.simple_theme_dark a.light_theme, body.simple_theme_dark a.light_theme:hover { color: #0088CC; } body.simple_theme_dark .dark_theme { color: #FF7733; background-color: #0e0e0e; border: 1px solid #282828; -webkit-box-shadow: 0 2px 5px 0 #333333; box-shadow: 0 2px 5px 0 #333333; } body.simple_theme_dark a.dark_theme, body.simple_theme_dark a.dark_theme:hover { color: #FF7733; } /* Inverted TipTip CSS - Version 1.2 */ body.simple_theme_dark div#tiptip_content { color: #000; text-shadow: 0 0 2px #fff; border: 1px solid rgba(0,0,0,0.25); background-color: rgb(230,230,230); background-color: rgba(230,230,230,0.92); background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(transparent), to(#fff)); box-shadow: 0 0 3px #aaa; -webkit-box-shadow: 0 0 3px #aaa; -moz-box-shadow: 0 0 3px #aaa; } body.simple_theme_dark div#tiptip_holder.tip_top #tiptip_arrow { border-top-color: #000; border-top-color: rgba(0,0,0,0.35); } body.simple_theme_dark div#tiptip_holder.tip_bottom #tiptip_arrow { border-bottom-color: #000; border-bottom-color: rgba(0,0,0,0.35); } body.simple_theme_dark div#tiptip_holder.tip_right #tiptip_arrow { border-right-color: #000; border-right-color: rgba(0,0,0,0.35); } body.simple_theme_dark div#tiptip_holder.tip_left #tiptip_arrow { border-left-color: #000; border-left-color: rgba(0,0,0,0.35); } body.simple_theme_dark div#tiptip_holder.tip_top #tiptip_arrow_inner { border-top-color: rgb(230,230,230); border-top-color: rgba(230,230,230,0.92); } body.simple_theme_dark div#tiptip_holder.tip_bottom #tiptip_arrow_inner { border-bottom-color: rgb(230,230,230); border-bottom-color: rgba(230,230,230,0.92); } body.simple_theme_dark div#tiptip_holder.tip_right #tiptip_arrow_inner { border-right-color: rgb(230,230,230); border-right-color: rgba(230,230,230,0.92); } body.simple_theme_dark div#tiptip_holder.tip_left #tiptip_arrow_inner { border-left-color: rgb(230,230,230); border-left-color: rgba(230,230,230,0.92); } /* Webkit Hacks */ @media screen and (-webkit-min-device-pixel-ratio:0) { body.simple_theme_dark div#tiptip_content { background-color: rgba(215,215,215,0.88); } body.simple_theme_dark div#tiptip_holder.tip_bottom #tiptip_arrow_inner { border-bottom-color: rgba(215,215,215,0.88); } body.simple_theme_dark div#tiptip_holder.tip_top #tiptip_arrow_inner { border-top-color: rgba(235,235,235,0.92); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_default.tcss���������������������������������������������������0000644�0001750�0001750�00000036170�12301071671�021572� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������@import url("basic.css"); @import url("images_data.css"); {% if display_theme_selector %} @import url("simple_theme_dark.css"); {% end %} /* ============================================================================ HTML5 Boilerplate CSS: h5bp.com/css ========================================================================== */ article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } audio:not([controls]) { display: none; } [hidden] { display: none; } html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } html, button, input, select, textarea { font-family: sans-serif; color: #222; } body { margin: 0; font-size: 1em; line-height: 1.4; } ::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; } ::selection { background: #fe57a1; color: #fff; text-shadow: none; } a { color: #00e; } a:visited { color: #551a8b; } a:hover { color: #06e; } a:focus { outline: thin dotted; } a:hover, a:active { outline: 0; } abbr[title] { border-bottom: 1px dotted; } b, strong { font-weight: bold; } blockquote { margin: 1em 40px; } dfn { font-style: italic; } hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } ins { background: #ff9; color: #000; text-decoration: none; } mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; } pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } q { quotes: none; } q:before, q:after { content: ""; content: none; } small { font-size: 85%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } /* ul, ol { margin: 1em 0; padding: 0 0 0 40px; } */ ul, ol { margin: 0; padding: 0; list-style:none outside none;} dd { margin: 0 0 0 40px; } nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; } img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } svg:not(:root) { overflow: hidden; } figure { margin: 0; } form { margin: 0; } fieldset { border: 0; margin: 0; padding: 0; } label { cursor: pointer; } legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; } button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; } button, input { line-height: normal; } button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; } button[disabled], input[disabled] { cursor: default; } input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; } input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; } input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } textarea { overflow: auto; vertical-align: top; resize: vertical; } input:valid, textarea:valid { } input:invalid, textarea:invalid { background-color: #f0dddd; } table { border-collapse: collapse; border-spacing: 0; } td { vertical-align: top; } /* ===== Primary Styles ======================================================= Author: Dobrosław Żybort With help of Initializr theme Author: Jonathan Verrecchia - verekia.com/initializr/responsive-template ========================================================================== */ /* =============== Light Theme =============== */ body { color: #222222; font: 12px Helvetica, Helvetica Neue, Arial; background-color: #F1F1F1; margin-top: 5px; margin-bottom: 5px; text-align: center; } .wrapper { width: 90%; margin: 0 5%; } a:link { color: #0088CC; text-decoration: none; } a:active { color: #0088CC; text-decoration: none; } a:visited { color: #0088CC; text-decoration: none; } a:hover { color: #0088CC; text-decoration: underline; } #header_container { border-bottom: 20px solid transparent; } h1#site_title { color: black; margin: 10px 0; } #main, #media_options { padding: 15px 0; } #main article h1 { font-size:2em; } #footer_container { border-top: 20px solid transparent; } ::-moz-selection { background: #4488ee; color: #fff; text-shadow: none; } ::selection { background: #4488ee; color: #fff; text-shadow: none; } /* =============== ALL: IE Fixes =============== */ .ie7 h1#site_title { padding-top:20px; } /* =============== Main site content =============== */ header, #main, #media_options, footer { background-color: #FFFFFF; border: 1px solid #D7D7D7; border-radius: 6px 6px 6px 6px; -webkit-box-shadow: 0 2px 5px 0 #CCCCCC; box-shadow: 0 2px 5px 0 #CCCCCC; display: inline-block; } .album_description { margin: auto; padding: 0 10px 10px; width: 90%; } /* =============== Breadcrumbs =============== */ .breadcrumbs { margin: auto; width: 90%; font-size: 10px; text-align: left; } .breadcrumbs ul { list-style: none; background: #F1F1F1; border-width: 1px; border-style: solid; border-color: #cccccc; border-radius: 6px; -webkit-box-shadow: inset 0 2px 5px 0 rgba(0,0,0,.2); box-shadow: inset 0 2px 5px 0 rgba(0,0,0,.2); overflow: hidden; width: 100%; } .breadcrumbs ul li { float: left; } .breadcrumbs ul li a:hover { border-color: #cccccc; -webkit-box-shadow: 0 2px 5px 0 rgba(0,0,0,.2); box-shadow: 0 2px 5px 0 rgba(0,0,0,.2); } .breadcrumbs ul li a, .breadcrumbs ul li span { padding: 0.7em 0.7em 0.7em 0.7em; float: left; text-decoration: none; color: #444; position: relative; text-shadow: 0 1px 0 rgba(255,255,255,.5); } .breadcrumbs ul li a:hover { background: #fff; } .breadcrumbs ul li:first-child a:hover { border-radius: 6px 0px 0px 6px; } /* Chrome fix */ .breadcrumbs ul .bc_current, .breadcrumbs ul .bc_current:hover { font-weight: bold; background: none; } .breadcrumbs ul .bc_current::after, .breadcrumbs ul .bc_current::before { content: normal; } /* =============== Image size select =============== */ .image_size { padding-top: 15px; padding-bottom: 5px; text-align: center; } .image_size li { display: inline; } .image_size ul li:after { content: " |"; } .image_size ul li:last-child:after { content: ""; } /* =============== Pagination =============== */ .pagination { line-height: 2em; margin: auto; padding-top: 15px; padding-bottom: 0px; text-align: center; width: 90%; } .pagination li { display: inline; } .pagination a { padding: 2px 5px 2px 5px; margin-right: 2px; border: 1px solid #ddd; text-decoration: none; color: #aaa; } .pagination a:hover, .pagination a:active { padding: 2px 5px 2px 5px; margin-right: 2px; border: 1px solid #a0a0a0; } .pagination span.pg_current { padding: 2px 5px 2px 5px; margin-right: 2px; border: 1px solid #e0e0e0; font-weight: bold; background-color: #cfcfcf; color: #444; } .pagination span.pg_disabled { padding: 2px 5px 2px 5px; margin-right: 2px; border: 1px solid #f3f3f3; color: #ccc; } /* =============== List of sub-galleries =============== */ .sub_gallery_link { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; background-color: #FFFFFF; border-color: #DCDCDC #DCDCDC #E1E1E1; border-radius: 0 0 0 0; border-style: solid; border-width: 0 0 1px; display: inline-block; padding-bottom: 20px; padding-top: 20px; text-align: left; width: 90%; } .sub_gallery_image img { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; background-color: #FFFFFF; border: 0 solid #666666; border-radius: 0 0 0 0; display: inline-block; float: left; padding: 0; text-align: left; } .sub_gallery_name { display: table; } h4.sub_gallery_name { font-size: 18px; font-weight: bold; } h4.sub_gallery_name a:link { color: #000000; font-weight: bold; text-decoration: none; } h4.sub_gallery_name a:active { color: #000000; font-weight: bold; text-decoration: none; } h4.sub_gallery_name a:visited { color: #000000; font-weight: bold; text-decoration: none; } h4.sub_gallery_name a:hover { color: #000000; font-weight: bold; text-decoration: underline; } .sub_gallery_stats { color: #333333; font-size: 10px; } .sub_gallery_description { color: #333333; font-size: 14px; } /* =============== Thumbs =============== */ #media_thumbs { margin-top: 15px; } .thumb_image { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; background-color: #F1F1F1; border: 1px solid #DCDCDC; border-radius: 6px 6px 6px 6px; display: inline-block; margin: 5px; padding: 10px; text-align: left; } .thumb_image:hover { background-color: #F7F7F7; -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); } .image_file:hover { background-color: #F1F1F1; } /* =============== Big image =============== */ #image_container, #video_container { margin: 0px auto; } .image_position, .video_position { position: relative; margin: 10px 0px; text-align: center; } .image_file { background-color: #FFFFFF; border: 1px solid #D7D7D7; border-radius: 6px 6px 6px 6px; -webkit-box-shadow: 5px 0 5px -5px #CCCCCC, -5px 0 5px -5px #CCCCCC; box-shadow: 5px 0 5px -5px #CCCCCC, -5px 0 5px -5px #CCCCCC; padding: 10px; } #image_caption { border-radius: 6px; background-color: #f9f9f9; border: 1px solid #d6d6d6; display: inline-block; font-size: 13px; margin: 10px auto 0; padding: 10px; width: 85%; } .image_comment { margin: 5px 0; } .image_original_link { margin: 5px 0; } .image_date, .authorship, .image_caption_tech { text-align: left; } .image_caption_tech ul { list-style: disc outside none; padding: 5px 20px; } /* =============== Previous/Next image links =============== */ .prevnext { margin: 0px 15px; } .prevnext_text { display: none; } #prev_link { position: relative; float: left; left: 0.1em; } #next_link { position: relative; float: right; right: 0.1em; } /* =============== Zipped folder =============== */ .directory_zip { margin-top: 15px; } .zip_link { padding: 0px 5px; } /* =============== Footer =============== */ .footer { font-size: 12px; overflow: auto; color: #666666; margin-bottom: 10px; } .footer_info { margin: auto; width: 75%; padding: 0 10px; } /* =============== Simple themes =============== */ .simple_theme { display: table; width: 20%; padding: 0; margin: auto; } .simple_theme li { display: table-cell; padding: 0 2px; } .theme_loader { display: table-cell; padding: 10px 0; margin: 10px 1.7%; margin-bottom: 0; min-width: 80px; text-align: center; text-decoration: none; font-weight: bold; } .theme_loader:hover { text-decoration: underline; } .light_theme { color: #0088CC; background-color: #F1F1F1; border: 1px solid #D7D7D7; border-radius: 6px 6px 0px 0px; -webkit-box-shadow: 0 2px 5px 0 #CCCCCC; box-shadow: 0 2px 5px 0 #CCCCCC; } a.light_theme, a.light_theme:hover { color: #0088CC; } .dark_theme { color: #FF7733; background-color: #0e0e0e; border: 1px solid #282828; border-radius: 6px 6px 0px 0px; -webkit-box-shadow: 0 2px 5px 0 #333333; box-shadow: 0 2px 5px 0 #333333; } a.dark_theme, a.dark_theme:hover { color: #FF7733; } /* ============================================================================ Media Queries ========================================================================== */ @media only screen and (min-width: 480px) { /* ==================== INTERMEDIATE: Menu ==================== */ nav a { float:left; width:27%; padding:25px 2%; margin-bottom:0; } nav li:first-child a{ margin-left:0; } nav li:last-child a{ margin-right:0; } .simple_theme li:first-child .theme_loader{ margin-left:0; } .simple_theme li:last-child .theme_loader{ margin-right:0; } .theme_loader { min-width:150px; } /* ======================== INTERMEDIATE: IE Fixes ======================== */ nav ul li { display:inline; } .oldie nav a { margin:0 0.7%; } } @media only screen and (min-width: 768px) { /* ============ WIDE: Menu ============ */ h1#site_title { padding: 0 10px; } nav { float:right; width:38%; } /* ============ WIDE: Main ============ */ #main article { float:left; width:57%; } #main aside { float:right; width:28%; } } @media only screen and (min-width: 1140px) { /* =============== Maximal Width =============== */ .wrapper { width:1026px; /* 1140px - 10% for margins */ margin:0 auto; } } /* ============================================================================ Non-Semantic Helper Classes ========================================================================== */ .ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; } .ir br { display: none; } .hidden { display: none !important; visibility: hidden; } .visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } .visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } .invisible { visibility: hidden; } .clearfix:before, .clearfix:after { content: ""; display: table; } .clearfix:after { clear: both; } .clearfix { *zoom: 1; } /* ============================================================================ Print Styles ========================================================================== */ @media print { * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */ a, a:visited { text-decoration: underline; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */ pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } thead { display: table-header-group; } /* h5bp.com/t */ tr, img { page-break-inside: avoid; } img { max-width: 100% !important; } @page { margin: 0.5cm; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_basic.css������������������������������������������������������0000644�0001750�0001750�00000000304�12301071671�021031� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.media_thumb { position: relative; display: inline-block; } .video_arrow { position: absolute; border: none; width: 40%; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/video.thtml�����������������������������������������������������������0000644�0001750�0001750�00000002204�12301071671�020371� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude" id="video_container"> <!--! Display video --> <div id="video_position"> <video src="$video_src" controls="controls"> your browser does not support the video tag</video> <div id="media_options" class="wrapper clearfix"> <p py:if="original_link"><a href="$original_link">${_('Original video')}</a></p> </div> <!-- #media_options --> </div> <div id="media_options" class="wrapper clearfix"> <!--! Display previous image thumb --> <div py:if="prev_link" class="prevnext" id="prev_link"> <xi:include href="thumb.thtml" py:with="media=prev_link" /> <a class="prevnext_text" href="$prev_link.link" title="${_('previous')}"></a> </div> <!--! Display next image thumb --> <div py:if="next_link" class="prevnext" id="next_link"> <xi:include href="thumb.thtml" py:with="media=next_link" /> <a class="prevnext_text" href="$next_link.link" title="${_('next')}"></a> </div> </div> <!-- #media_options --> </div> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/inverted/SHARED_respond.js�����������������������������������������������������0000644�0001750�0001750�00000023664�12301071671�021264� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Respond.js: min/max-width media query polyfill. (c) Scott Jehl. MIT Lic. j.mp/respondjs */ (function( w ){ "use strict"; //exposed namespace var respond = {}; w.respond = respond; //define update even in native-mq-supporting browsers, to avoid errors respond.update = function(){}; //define ajax obj var requestQueue = [], xmlHttp = (function() { var xmlhttpmethod = false; try { xmlhttpmethod = new w.XMLHttpRequest(); } catch( e ){ xmlhttpmethod = new w.ActiveXObject( "Microsoft.XMLHTTP" ); } return function(){ return xmlhttpmethod; }; })(), //tweaked Ajax functions from Quirksmode ajax = function( url, callback ) { var req = xmlHttp(); if (!req){ return; } req.open( "GET", url, true ); req.onreadystatechange = function () { if ( req.readyState !== 4 || req.status !== 200 && req.status !== 304 ){ return; } callback( req.responseText ); }; if ( req.readyState === 4 ){ return; } req.send( null ); }, isUnsupportedMediaQuery = function( query ) { return query.replace( respond.regex.minmaxwh, '' ).match( respond.regex.other ); }; //expose for testing respond.ajax = ajax; respond.queue = requestQueue; respond.unsupportedmq = isUnsupportedMediaQuery; respond.regex = { media: /@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi, keyframes: /@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi, comments: /\/\*[^*]*\*+([^/][^*]*\*+)*\//gi, urls: /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g, findStyles: /@media *([^\{]+)\{([\S\s]+?)$/, only: /(only\s+)?([a-zA-Z]+)\s?/, minw: /\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/, maxw: /\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/, minmaxwh: /\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi, other: /\([^\)]*\)/g }; //expose media query support flag for external use respond.mediaQueriesSupported = w.matchMedia && w.matchMedia( "only all" ) !== null && w.matchMedia( "only all" ).matches; //if media queries are supported, exit here if( respond.mediaQueriesSupported ){ return; } //define vars var doc = w.document, docElem = doc.documentElement, mediastyles = [], rules = [], appendedEls = [], parsedSheets = {}, resizeThrottle = 30, head = doc.getElementsByTagName( "head" )[0] || docElem, base = doc.getElementsByTagName( "base" )[0], links = head.getElementsByTagName( "link" ), lastCall, resizeDefer, //cached container for 1em value, populated the first time it's needed eminpx, // returns the value of 1em in pixels getEmValue = function() { var ret, div = doc.createElement('div'), body = doc.body, originalHTMLFontSize = docElem.style.fontSize, originalBodyFontSize = body && body.style.fontSize, fakeUsed = false; div.style.cssText = "position:absolute;font-size:1em;width:1em"; if( !body ){ body = fakeUsed = doc.createElement( "body" ); body.style.background = "none"; } // 1em in a media query is the value of the default font size of the browser // reset docElem and body to ensure the correct value is returned docElem.style.fontSize = "100%"; body.style.fontSize = "100%"; body.appendChild( div ); if( fakeUsed ){ docElem.insertBefore( body, docElem.firstChild ); } ret = div.offsetWidth; if( fakeUsed ){ docElem.removeChild( body ); } else { body.removeChild( div ); } // restore the original values docElem.style.fontSize = originalHTMLFontSize; if( originalBodyFontSize ) { body.style.fontSize = originalBodyFontSize; } //also update eminpx before returning ret = eminpx = parseFloat(ret); return ret; }, //enable/disable styles applyMedia = function( fromResize ){ var name = "clientWidth", docElemProp = docElem[ name ], currWidth = doc.compatMode === "CSS1Compat" && docElemProp || doc.body[ name ] || docElemProp, styleBlocks = {}, lastLink = links[ links.length-1 ], now = (new Date()).getTime(); //throttle resize calls if( fromResize && lastCall && now - lastCall < resizeThrottle ){ w.clearTimeout( resizeDefer ); resizeDefer = w.setTimeout( applyMedia, resizeThrottle ); return; } else { lastCall = now; } for( var i in mediastyles ){ if( mediastyles.hasOwnProperty( i ) ){ var thisstyle = mediastyles[ i ], min = thisstyle.minw, max = thisstyle.maxw, minnull = min === null, maxnull = max === null, em = "em"; if( !!min ){ min = parseFloat( min ) * ( min.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); } if( !!max ){ max = parseFloat( max ) * ( max.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); } // if there's no media query at all (the () part), or min or max is not null, and if either is present, they're true if( !thisstyle.hasquery || ( !minnull || !maxnull ) && ( minnull || currWidth >= min ) && ( maxnull || currWidth <= max ) ){ if( !styleBlocks[ thisstyle.media ] ){ styleBlocks[ thisstyle.media ] = []; } styleBlocks[ thisstyle.media ].push( rules[ thisstyle.rules ] ); } } } //remove any existing respond style element(s) for( var j in appendedEls ){ if( appendedEls.hasOwnProperty( j ) ){ if( appendedEls[ j ] && appendedEls[ j ].parentNode === head ){ head.removeChild( appendedEls[ j ] ); } } } appendedEls.length = 0; //inject active styles, grouped by media type for( var k in styleBlocks ){ if( styleBlocks.hasOwnProperty( k ) ){ var ss = doc.createElement( "style" ), css = styleBlocks[ k ].join( "\n" ); ss.type = "text/css"; ss.media = k; //originally, ss was appended to a documentFragment and sheets were appended in bulk. //this caused crashes in IE in a number of circumstances, such as when the HTML element had a bg image set, so appending beforehand seems best. Thanks to @dvelyk for the initial research on this one! head.insertBefore( ss, lastLink.nextSibling ); if ( ss.styleSheet ){ ss.styleSheet.cssText = css; } else { ss.appendChild( doc.createTextNode( css ) ); } //push to appendedEls to track for later removal appendedEls.push( ss ); } } }, //find media blocks in css text, convert to style blocks translate = function( styles, href, media ){ var qs = styles.replace( respond.regex.comments, '' ) .replace( respond.regex.keyframes, '' ) .match( respond.regex.media ), ql = qs && qs.length || 0; //try to get CSS path href = href.substring( 0, href.lastIndexOf( "/" ) ); var repUrls = function( css ){ return css.replace( respond.regex.urls, "$1" + href + "$2$3" ); }, useMedia = !ql && media; //if path exists, tack on trailing slash if( href.length ){ href += "/"; } //if no internal queries exist, but media attr does, use that //note: this currently lacks support for situations where a media attr is specified on a link AND //its associated stylesheet has internal CSS media queries. //In those cases, the media attribute will currently be ignored. if( useMedia ){ ql = 1; } for( var i = 0; i < ql; i++ ){ var fullq, thisq, eachq, eql; //media attr if( useMedia ){ fullq = media; rules.push( repUrls( styles ) ); } //parse for styles else{ fullq = qs[ i ].match( respond.regex.findStyles ) && RegExp.$1; rules.push( RegExp.$2 && repUrls( RegExp.$2 ) ); } eachq = fullq.split( "," ); eql = eachq.length; for( var j = 0; j < eql; j++ ){ thisq = eachq[ j ]; if( isUnsupportedMediaQuery( thisq ) ) { continue; } mediastyles.push( { media : thisq.split( "(" )[ 0 ].match( respond.regex.only ) && RegExp.$2 || "all", rules : rules.length - 1, hasquery : thisq.indexOf("(") > -1, minw : thisq.match( respond.regex.minw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ), maxw : thisq.match( respond.regex.maxw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ) } ); } } applyMedia(); }, //recurse through request queue, get css text makeRequests = function(){ if( requestQueue.length ){ var thisRequest = requestQueue.shift(); ajax( thisRequest.href, function( styles ){ translate( styles, thisRequest.href, thisRequest.media ); parsedSheets[ thisRequest.href ] = true; // by wrapping recursive function call in setTimeout // we prevent "Stack overflow" error in IE7 w.setTimeout(function(){ makeRequests(); },0); } ); } }, //loop stylesheets, send text content to translate ripCSS = function(){ for( var i = 0; i < links.length; i++ ){ var sheet = links[ i ], href = sheet.href, media = sheet.media, isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet"; //only links plz and prevent re-parsing if( !!href && isCSS && !parsedSheets[ href ] ){ // selectivizr exposes css through the rawCssText expando if (sheet.styleSheet && sheet.styleSheet.rawCssText) { translate( sheet.styleSheet.rawCssText, href, media ); parsedSheets[ href ] = true; } else { if( (!/^([a-zA-Z:]*\/\/)/.test( href ) && !base) || href.replace( RegExp.$1, "" ).split( "/" )[0] === w.location.host ){ // IE7 doesn't handle urls that start with '//' for ajax request // manually add in the protocol if ( href.substring(0,2) === "//" ) { href = w.location.protocol + href; } requestQueue.push( { href: href, media: media } ); } } } } makeRequests(); }; //translate CSS ripCSS(); //expose update for re-running respond later on respond.update = ripCSS; //expose getEmValue respond.getEmValue = getEmValue; //adjust on resize function callMedia(){ applyMedia( true ); } if( w.addEventListener ){ w.addEventListener( "resize", callMedia, false ); } else if( w.attachEvent ){ w.attachEvent( "onresize", callMedia ); } })(this); ����������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/��������������������������������������������������������������������0000755�0001750�0001750�00000000000�12301073200�016477� 5����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_video_arrow.svg����������������������������������������������0000644�0001750�0001750�00000011305�12301071671�022600� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="478.2052" height="478.2052" id="svg2" version="1.1" inkscape:version="0.48.2 r9819" sodipodi:docname="video_arrow.svg"> <defs id="defs4"> <filter inkscape:collect="always" id="filter3799" color-interpolation-filters="sRGB"> <feGaussianBlur inkscape:collect="always" stdDeviation="10.227795" id="feGaussianBlur3801" /> </filter> <filter inkscape:collect="always" id="filter3828" color-interpolation-filters="sRGB"> <feGaussianBlur inkscape:collect="always" stdDeviation="4.1097545" id="feGaussianBlur3830" /> </filter> </defs> <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.98994949" inkscape:cx="106.34645" inkscape:cy="105.03316" inkscape:document-units="px" inkscape:current-layer="layer2" showgrid="false" inkscape:window-width="1920" inkscape:window-height="1061" inkscape:window-x="1596" inkscape:window-y="-4" inkscape:window-maximized="1" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" borderlayer="false" showborder="true" inkscape:showpageshadow="false" /> <metadata id="metadata7"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label="Circle" inkscape:groupmode="layer" id="layer1" transform="translate(-138.18936,-45.038574)"> <path sodipodi:type="arc" style="fill:none;stroke:#000000;stroke-width:20;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3799)" id="path3765" sodipodi:cx="377.29196" sodipodi:cy="284.14117" sodipodi:rx="204.55589" sodipodi:ry="204.55589" d="m 581.84785,284.14117 c 0,112.9731 -91.58279,204.5559 -204.55589,204.5559 -112.9731,0 -204.55589,-91.5828 -204.55589,-204.5559 0,-112.9731 91.58279,-204.555889 204.55589,-204.555889 112.9731,0 204.55589,91.582789 204.55589,204.555889 z" /> <path sodipodi:type="arc" style="opacity:0.26582277;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" id="path2985" sodipodi:cx="377.29196" sodipodi:cy="284.14117" sodipodi:rx="204.55589" sodipodi:ry="204.55589" d="m 581.84785,284.14117 c 0,112.9731 -91.58279,204.5559 -204.55589,204.5559 -112.9731,0 -204.55589,-91.5828 -204.55589,-204.5559 0,-112.9731 91.58279,-204.555889 204.55589,-204.555889 112.9731,0 204.55589,91.582789 204.55589,204.555889 z" /> <path d="m 581.84785,284.14117 c 0,112.9731 -91.58279,204.5559 -204.55589,204.5559 -112.9731,0 -204.55589,-91.5828 -204.55589,-204.5559 0,-112.9731 91.58279,-204.555889 204.55589,-204.555889 112.9731,0 204.55589,91.582789 204.55589,204.555889 z" sodipodi:ry="204.55589" sodipodi:rx="204.55589" sodipodi:cy="284.14117" sodipodi:cx="377.29196" id="path3763" style="fill:none;stroke:#ffffff;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" sodipodi:type="arc" /> </g> <g inkscape:groupmode="layer" id="layer2" inkscape:label="Arrow" transform="translate(-138.18936,-45.038574)"> <path inkscape:connector-curvature="0" id="path3826" d="m 313.79521,195.24775 0,177.78685 150.99351,-87.17615 z" style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3828)" /> <path style="fill:#ffffff;fill-opacity:1;stroke:none" d="m 313.79521,195.24775 0,177.78685 150.99351,-87.17615 z" id="path3805" inkscape:connector-curvature="0" /> </g> </svg> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_default.css��������������������������������������������������0000644�0001750�0001750�00000002731�12301071671�021700� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������@import url("basic.css"); @import url("colorbox.css"); body{ background: white; } a { color: black; } .title { display: none; } .title h1{ margin-top: .1em; } .inline_enum ul{ margin-left: 0; padding-left: 0; display: inline; } .inline_enum ul li { margin-left: 0; padding-left: 2px; border: none; list-style: none; display: inline; } #breadcrumbs { padding: 3px; margin-bottom: 25px; font-size: small; } #breadcrumbs ul li:after { content: "\0020 \0020 \0020 \00BB \0020"; } #breadcrumbs ul li.bc_current:after { content: " "; } #osize_links ul li:after { content: " |"; } #osize_links ul li:last-child:after { content: " "; } #onum_links ul li:after { content: " |"; } #onum_links ul li:last-child:after { content: " "; } #osize_links { font-size: small; text-align:center; margin: 1em; } #onum_links{ font-size: small; text-align:center; } div.subgal_link { float: left; padding: 1em; width: 50em; } div.subgal_image { float: left; margin-right: 2em; margin-left: 4em; width:180px; } div.subgal_image img { margin: 0; border: none; } div.subgal_description p{ text-align:justify; } .media_links{ padding: 2em; } img.media{ border: solid black 1px; margin: 1em; } .caption { display:none; } #lazygalfooter{ padding-top: 3em; clear: both; font-size: x-small; } /* * vim: ts=4 sw=4 expandtab */ ���������������������������������������lazygal-0.8.2/themes/singlepage/dynindex.thtml������������������������������������������������������0000644�0001750�0001750�00000005762�12301071671�021417� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE HTML> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude"> <head> <title py:content="album_name" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta name="Generator" content="lazygal $lazygal_version" /> <link py:for="style in styles" type="text/css" rel="$style.rel" media="screen,projection" title="$style.name" href="${rel_root}shared/${style.filename}.css" /> <link py:if="feed_url" rel="alternate" type="application/rss+xml" title="Recent galleries" href="$feed_url" /> <script type="text/javascript" src="${rel_root}shared/jquery.js"></script> <script type="text/javascript" src="${rel_root}shared/jquery.colorbox.js"></script> <script type="text/javascript" src="${rel_root}shared/lazygal.js"></script> </head> <body> <div class="title"> <h1 py:content="album_name" /> <p class="header" py:if="album_description" py:content="album_description" /> </div> <div class="inline_enum" id="breadcrumbs"> <ul> <li py:for="webgal in webgal_path"> <a href="${webgal.link}" py:content="webgal.name" /> </li> <li class="bc_current" py:content="album_name" /> </ul> </div> <div class="inline_enum" id="osize_links" py:if="len(osize_index_links) > 1"> <ul> <li py:for="osize_index_link in osize_index_links"> <a py:strip="not osize_index_link.link" href="$osize_index_link.link">$osize_index_link.name</a> </li> </ul> </div> <div class="inline_enum" id="onum_links" py:if="len(onum_index_links) > 1"> <ul> <li py:for="onum_index_link in onum_index_links"> <a py:strip="not onum_index_link.link" href="$onum_index_link.link">$onum_index_link.name</a> </li> </ul> </div> <div id="subgal_links" py:if="subgal_links"> <py:for each="subgal_link in subgal_links"> <xi:include href="gallerylink.thtml" /> </py:for> </div> <div py:for="subdir, medias in medias" py:if="medias" class="media_links"> <py:if test="not subdir.is_main"> <h2><a name="$subdir.id"></a>$subdir.album_name</h2> <div py:if="subdir.album_description" class="subgal_description"> <p>$subdir.album_description</p> </div> </py:if> <ul class="thumbs noscript"> <xi:include href="thumb.${media.type}.thtml" py:for="media in medias" /> </ul> <div class="dirzip" py:if="subdir.dirzip"> <a href="$subdir.dirzip">${_('All full scale pictures as an archive, for')} "$subdir.album_name"</a> ($subdir.dirzip_size). </div> </div> <div class="footer" py:if="footer" py:content="footer" /> <div class="footer" id="lazygalfooter"> <p>${_('Generated by')} <a href="http://sousmonlit.dyndns.org/~niol/playa/oss/projects/lazygal">lazygal</a> ${gen_datetime.strftime(_('on %c'))}.</p> </div> </body> </html> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> ��������������lazygal-0.8.2/themes/singlepage/SHARED_loading.gif��������������������������������������������������0000644�0001750�0001750�00000022323�12301071671�021645� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������GIF89a � ������### <<<pppggg777hhhqqqSSS~~~nnn sssQQQGGGuuuNNN000UUUwww888'''HHH111,,,%%%AAA:::LLL ```!!!jjjչXXXEEEWWW\\\ccc^^^aaa***(((555ZZZʾ>>>PPP|||Ǽ...333@@@JJJ̌zzzyyyCCCllleee���������������!Created with ajaxload.info�!����! NETSCAPE2.0���,���� � ��� 964!5:; "";=5 �? 7@)2�6<>(�B(.*0�(#"3B/(8$+1  $�"pA&X("["hDBhH‡ A D ED^:*ŋiH[ @N"^`:G@=IBTD "nD  (-$`ܹxwR �!���,���� � ���*X[ VY\ 1-LQ � IJ3ZQ $FJLP TVZ] 7@GKMQLS�UW<^�B(. &KPR�(@KJB/(8B12˄4 :j/Q "E�% 'RPA!BZH۶$0 �Ep"q耈BGxѭz@2=P'.�8daۋG4/H"-)�dA^N8ƶ pJBHΆ ˷߿�!���,���� � ���$N?<e K<fJ B2(4M<gL � `cCTP�._ad`d\ 3@Dbc]],�?>h�6b-(.$ b\H�!!US((1@/(8.'S u .FbŔ �@@^"C.A x(:*zr ~�/)*@3_=sς W>  Ƞ  . T#ŀ$A<x전:L'$n\˷߽��!���,���� � ���$UX>G cXn] 7AdXhJ +_[\�6_l� FF n8_km-�o�HZ=pS.?b�-im=eG4 D'GXDϛ!i �"R2�2�%E8օP@  9,� EBJT$�ǍA EuM_AP8Y @6}XD%2")TRDa�L `1TNBT=T`ѹxWo �!���,���� � ���BCHQ"E kQq b`�DNj*c� .  � #� )JO6 (�3PQC�UrY:?N9+�RO#:.9,6p;a(/ Qn�)r" HXt6EEB66ġpHA$6,hCT>*B$^�IcEaQ& _`yRȊ7) 0 yDh:(,K�HQ<[�lʝK@�!���,���� � ���.+l %T3 &M#sb /Ol( tPk _nQR^?Hi� It� _egrN+EN�u`l�?vVu So6[�/FfrpFjo%GF$ AƂQ@P(Јhт@EPhDAC0nHHFn܈�@.2bQ2 Q@/\"ȦY�Fh|ё荋&5 }��h4 ` L+pc젵5Lx0$˷߿}�!���,���� � ���l#m2 mc w_ 2C$(\t�$)nG j1Gm\� 0hvc!38�2#F)<?,+!�7#nve ,[EWek$$ (\BT#81"AE``(2PmP¨Q6 X`ƍ �D 耈<JxQ @43Ql ㋍Goh<7Ș@K πAvP(&d u˷@�!���,���� � ��� AE?@_? � lB4DBwQHAO!8�5G 9x*)b/(-S$�!wt-�3)X@(R!7t)0_  D)>Bu+\|jb>FBhB7d, ~ؘD"/*:ңdܸPTY.@S (^,DI$B$GȞX|UBBG5�,U A72Ks*@bܻx݋7�!���,���� � ��� i! SH 2C?,2�H96�.GM%/B�uHw NTPL7((/�/?/�C]ML3@($x .^Lx.ц[u4@LH=h EȆ+VPq"YBj\dFBEĈX9·}3D H; Bp8=p_ǎ 8 ` "砱.'8#PT ʝKݹ��!�� �,���� � ���$ C37,C/j %�4Dmc$/BiD�%F\] ((/�@u�]K8(4$Nc]T9 /4d.̆ '[ABƉ 0aT$ϐc"+2Lb!fBc@ 7 *z! &N3TG: b1Dg \"K!@6)P@zL�[7t і�HDЂ8�t &$:PH˷_��!�� �,���� � ��� BO,$i2,@ 7i2o&-B/7B[S�+&k((/�B-�jb[8( 4_kFH /ۅ[q6.Ȇ ,' @@B)0J&bP$0ZC9RB)D*cG\!0JPCb^d!C؄T 1h2#N1 =(f�Z6$#5Ȉ2*tY^.M(v+n$˷߿�!�� �,���� � ���7 (( �/ D477BR�2G9(A�B(.�R6 8( u&^ /Åjt. O!皋$88.@cd@T'0PEQ L'EQ p ȀSY<B:tx(Bbx@ 1"HNG$y!@PZ�B/nCH 8 � ܈8jЍ= `B'Zl  3ݻx�!�� �,���� � ��� @@@((S77B(AO348/�B�A+EU.(  ,EGu (/ąRUy .߅7埉By5mb V%BWdt!"iB{nS9h]smْQ ݖʆH8�#V1�A/]$^UJP/2 �p1enЍL{  ۷p6 �!�� �,���� � ��� @88(($77B(A@,!/�B$!2ul(  3(/8u А.ۆ8Aៈ4$$ȉ D&H@(4}ذA$L`̘0m(w"G|cB c4! o.|T H62#,( 4>(x,[@/+XN<A X(<)K^"t�hxd']pS F~˷߿�!���,���� � ��� 8(( .7B(A/(8/�B-/  (-j@(/B-S(.ֆB܈B+EN6(41 [˗—c(Rw"!D$ܹ�9B 2vsːl:v B"!440و1OJ)E"=ȕ 4G VBŎ1@xahZA>R⊎"؄DHʝK.@�!���,���� � ���$$8((  B(A///B�B � (/(8.І4 NjS++R(ۉ,UEHH?S 6\(w7Hlaǭ�(eF  "L`M" LjįrZ!F*y(6l[9X �p BC*+ p:)�BB$& @ a "OmD�FOٰ۷pF �!���,���� � ���$$((  B(A/4/�B7(8 �(/B.҆ /j ,̉(݈7uC'RQp㜢fq[!⚹e 5 ͅ!/cTֈįs��K.q(xT ,<D@%81 p<Vl(�"9":(C6hܡ卧Vx!͔&p֡۷pʕ�!���,���� � ���$B((   �(A/7(/B7�B( �B/(8.ц4@@Ȋ(݉O-iiA22QpÜ"-N›!pIÈAPH a&:rc!P;Цiz �'>�0$Ns<B ؁aX0?@P ;> HU1#QPb.ࡒn$˗P �!���,���� � ���$.(($ B(A (/�B7( �B/(8.І4@Ջ(܉ÿٟ7A/FZ@R"X~ 04/9pfXg"$�``<-&KRc@"@ &)D|);R0=Z㛀0h" <D}Ђo0K@�!���,���� � ���B.(($ B(A(/�B7( �˄B/(8.Ѕ4nj@ċ(܉›B788�FH  㛣 Iz)a(i:C9�<!hZ@2#K�46|x𨅆Nte�$tBDr-? 5 nyAxuK@�!���,���� � ���B.(($ B(A 7A(/�B7( �˄B/(8.Ѕ4nj@(܉›B78p׭'"q)%YxX0tC #L�^pbŖ)#yc�^x@lԀx "b)^VH1�.ts arK<FBG8lʝ �!���,���� � ���B.(($ B(A 7@(/�B7( ʃB/(8.τ4@(ۉ$7ވj 1B-aM&A EF! )H3n0e��j�P#!ݨXB �9# vܤ#@5&h)ʑQܢQc %�Q�8A4 C!! s|`j  �x �!���,���� � ���B.(($ B(A 7@(/�B7( ʃB/(8.τ47@B`J57$$)==;L)5 A HC\xfh 8 *"#!m|�9b� {H(u�lPG"C@0h䄠 <bA$4S(80$"-c 8$gZKn �!���,���� � ���B.(($� AB(A$@(/�B7( �ʃB/(8.τ4Bƌ+0k@ۊB;o$$YYp3콢CP<Q0�p!`=*UZ]�Dأ*&p1�@͏!ÄD&M( HR=x[#'M"HF,uP�5\t(Rċ|(bZB8 .$XVúx �!���,���� � ���B.(($� AB(A$@(/�B7( �ʃB/(8.τ4.ƌC"#@؆BxVpp$BQffeobtI%4Q\H &Q…!Ҁ@rQ 8^P H/o,!f�Rb%G \!CP�ibC#R-0$VX 91 7 ~K]C��!���,���� � ���B.(($� AB(A$@2-(/�B7(9 �@P_ÕB/(88Ň4.$BȊ$_"@^vv<<e羢3E_ʒx`Q%G!"&9C9Zp NHT8� 5zcd O�y2M K� ypʆ�58I '/ Dݻx* �!���,���� � ���B.(($�iM?B(A$@s=)�B7(;=FÎ8I;eOB/(88?l44߉,MLI_@ FX00H$·!A DQ dȌaQ@,!D8S\$xngH8s!>P΁< �Gؘ8 �0Zi#" q x6RPnl@IFڤ $!^ -dvݻx �!���,���� � ���B.(($B�-M)E(A$@Y:V�B7( pZ;�Î8TpgB/(8%[̅44B4isdK@ 5QQx(""t(T8C`@I1Ѧ! ".\@3uFQȅ<?n"-#oTB�p*^b<�@s@:b�Vl.ld(pc) a(pݻx]�!���,���� � ���B.(($4B�-PU(A$@fVe2�B7(G<gV9�Îd<vuB/(8^2̅4.4Rbaq9@ ]]KK.(" ;c&HHAE0<DpH�L)! QC$|tC)��dzG$V9d\!ni^her  Md0۷pE�!���,���� � ���. _MR$�SIx%*JRnWnD�d(A$@N0nvh�Nm7�B(Ɏa0h<�(Ύ7>B/(84؋/GsN@(H$ Ϛ6ma6 |8Xe &P'*cE:PJBQDO."QNS�I�8tBw<tPMgP�@FA nH7Dvf Z0ܻx݋)�!���,���� � ��� E3 B!t;:G"x;=&4\)*�D$@ X2�6<X(�B(*0(Џ#"3B/(8$+14 B9%�2�DCTDE$QE,IT䂍NtPT�G/(@i�#Q�/\"n"$n&G78{!瀑͞A6g>P�n8 2(_K(4$ڵpʝKWn �;����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_jquery.colorbox.js�������������������������������������������0000644�0001750�0001750�00000067355�12301071671�023262� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/*! Colorbox v1.4.36 - 2014-02-01 jQuery lightbox and modal window plugin (c) 2014 Jack Moore - http://www.jacklmoore.com/colorbox license: http://www.opensource.org/licenses/mit-license.php */ (function ($, document, window) { var // Default settings object. // See http://jacklmoore.com/colorbox for details. defaults = { // data sources html: false, photo: false, iframe: false, inline: false, // behavior and appearance transition: "elastic", speed: 300, fadeOut: 300, width: false, initialWidth: "600", innerWidth: false, maxWidth: false, height: false, initialHeight: "450", innerHeight: false, maxHeight: false, scalePhotos: true, scrolling: true, href: false, title: false, rel: false, opacity: 0.9, preloading: true, className: false, overlayClose: true, escKey: true, arrowKey: true, top: false, bottom: false, left: false, right: false, fixed: false, data: undefined, closeButton: true, fastIframe: true, open: false, reposition: true, loop: true, slideshow: false, slideshowAuto: true, slideshowSpeed: 2500, slideshowStart: "start slideshow", slideshowStop: "stop slideshow", photoRegex: /\.(gif|png|jp(e|g|eg)|bmp|ico|webp)((#|\?).*)?$/i, // alternate image paths for high-res displays retinaImage: false, retinaUrl: false, retinaSuffix: '@2x.$1', // internationalization current: "image {current} of {total}", previous: "previous", next: "next", close: "close", xhrError: "This content failed to load.", imgError: "This image failed to load.", // accessbility returnFocus: true, trapFocus: true, // callbacks onOpen: false, onLoad: false, onComplete: false, onCleanup: false, onClosed: false }, // Abstracting the HTML and event identifiers for easy rebranding colorbox = 'colorbox', prefix = 'cbox', boxElement = prefix + 'Element', // Events event_open = prefix + '_open', event_load = prefix + '_load', event_complete = prefix + '_complete', event_cleanup = prefix + '_cleanup', event_closed = prefix + '_closed', event_purge = prefix + '_purge', // Cached jQuery Object Variables $overlay, $box, $wrap, $content, $topBorder, $leftBorder, $rightBorder, $bottomBorder, $related, $window, $loaded, $loadingBay, $loadingOverlay, $title, $current, $slideshow, $next, $prev, $close, $groupControls, $events = $('<a/>'), // $([]) would be prefered, but there is an issue with jQuery 1.4.2 // Variables for cached values or use across multiple functions settings, interfaceHeight, interfaceWidth, loadedHeight, loadedWidth, element, index, photo, open, active, closing, loadingTimer, publicMethod, div = "div", className, requests = 0, previousCSS = {}, init; // **************** // HELPER FUNCTIONS // **************** // Convenience function for creating new jQuery objects function $tag(tag, id, css) { var element = document.createElement(tag); if (id) { element.id = prefix + id; } if (css) { element.style.cssText = css; } return $(element); } // Get the window height using innerHeight when available to avoid an issue with iOS // http://bugs.jquery.com/ticket/6724 function winheight() { return window.innerHeight ? window.innerHeight : $(window).height(); } // Determine the next and previous members in a group. function getIndex(increment) { var max = $related.length, newIndex = (index + increment) % max; return (newIndex < 0) ? max + newIndex : newIndex; } // Convert '%' and 'px' values to integers function setSize(size, dimension) { return Math.round((/%/.test(size) ? ((dimension === 'x' ? $window.width() : winheight()) / 100) : 1) * parseInt(size, 10)); } // Checks an href to see if it is a photo. // There is a force photo option (photo: true) for hrefs that cannot be matched by the regex. function isImage(settings, url) { return settings.photo || settings.photoRegex.test(url); } function retinaUrl(settings, url) { return settings.retinaUrl && window.devicePixelRatio > 1 ? url.replace(settings.photoRegex, settings.retinaSuffix) : url; } function trapFocus(e) { if ('contains' in $box[0] && !$box[0].contains(e.target)) { e.stopPropagation(); $box.focus(); } } // Assigns function results to their respective properties function makeSettings() { var i, data = $.data(element, colorbox); if (data == null) { settings = $.extend({}, defaults); if (console && console.log) { console.log('Error: cboxElement missing settings object'); } } else { settings = $.extend({}, data); } for (i in settings) { if ($.isFunction(settings[i]) && i.slice(0, 2) !== 'on') { // checks to make sure the function isn't one of the callbacks, they will be handled at the appropriate time. settings[i] = settings[i].call(element); } } settings.rel = settings.rel || element.rel || $(element).data('rel') || 'nofollow'; settings.href = settings.href || $(element).attr('href'); settings.title = settings.title || element.title; if (typeof settings.href === "string") { settings.href = $.trim(settings.href); } } function trigger(event, callback) { // for external use $(document).trigger(event); // for internal use $events.triggerHandler(event); if ($.isFunction(callback)) { callback.call(element); } } var slideshow = (function(){ var active, className = prefix + "Slideshow_", click = "click." + prefix, timeOut; function clear () { clearTimeout(timeOut); } function set() { if (settings.loop || $related[index + 1]) { clear(); timeOut = setTimeout(publicMethod.next, settings.slideshowSpeed); } } function start() { $slideshow .html(settings.slideshowStop) .unbind(click) .one(click, stop); $events .bind(event_complete, set) .bind(event_load, clear); $box.removeClass(className + "off").addClass(className + "on"); } function stop() { clear(); $events .unbind(event_complete, set) .unbind(event_load, clear); $slideshow .html(settings.slideshowStart) .unbind(click) .one(click, function () { publicMethod.next(); start(); }); $box.removeClass(className + "on").addClass(className + "off"); } function reset() { active = false; $slideshow.hide(); clear(); $events .unbind(event_complete, set) .unbind(event_load, clear); $box.removeClass(className + "off " + className + "on"); } return function(){ if (active) { if (!settings.slideshow) { $events.unbind(event_cleanup, reset); reset(); } } else { if (settings.slideshow && $related[1]) { active = true; $events.one(event_cleanup, reset); if (settings.slideshowAuto) { start(); } else { stop(); } $slideshow.show(); } } }; }()); function launch(target) { if (!closing) { element = target; makeSettings(); $related = $(element); index = 0; if (settings.rel !== 'nofollow') { $related = $('.' + boxElement).filter(function () { var data = $.data(this, colorbox), relRelated; if (data) { relRelated = $(this).data('rel') || data.rel || this.rel; } return (relRelated === settings.rel); }); index = $related.index(element); // Check direct calls to Colorbox. if (index === -1) { $related = $related.add(element); index = $related.length - 1; } } $overlay.css({ opacity: parseFloat(settings.opacity), cursor: settings.overlayClose ? "pointer" : "auto", visibility: 'visible' }).show(); if (className) { $box.add($overlay).removeClass(className); } if (settings.className) { $box.add($overlay).addClass(settings.className); } className = settings.className; if (settings.closeButton) { $close.html(settings.close).appendTo($content); } else { $close.appendTo('<div/>'); } if (!open) { open = active = true; // Prevents the page-change action from queuing up if the visitor holds down the left or right keys. // Show colorbox so the sizes can be calculated in older versions of jQuery $box.css({visibility:'hidden', display:'block'}); $loaded = $tag(div, 'LoadedContent', 'width:0; height:0; overflow:hidden'); $content.css({width:'', height:''}).append($loaded); // Cache values needed for size calculations interfaceHeight = $topBorder.height() + $bottomBorder.height() + $content.outerHeight(true) - $content.height(); interfaceWidth = $leftBorder.width() + $rightBorder.width() + $content.outerWidth(true) - $content.width(); loadedHeight = $loaded.outerHeight(true); loadedWidth = $loaded.outerWidth(true); // Opens inital empty Colorbox prior to content being loaded. settings.w = setSize(settings.initialWidth, 'x'); settings.h = setSize(settings.initialHeight, 'y'); $loaded.css({width:'', height:settings.h}); publicMethod.position(); trigger(event_open, settings.onOpen); $groupControls.add($title).hide(); $box.focus(); if (settings.trapFocus) { // Confine focus to the modal // Uses event capturing that is not supported in IE8- if (document.addEventListener) { document.addEventListener('focus', trapFocus, true); $events.one(event_closed, function () { document.removeEventListener('focus', trapFocus, true); }); } } // Return focus on closing if (settings.returnFocus) { $events.one(event_closed, function () { $(element).focus(); }); } } load(); } } // Colorbox's markup needs to be added to the DOM prior to being called // so that the browser will go ahead and load the CSS background images. function appendHTML() { if (!$box && document.body) { init = false; $window = $(window); $box = $tag(div).attr({ id: colorbox, 'class': $.support.opacity === false ? prefix + 'IE' : '', // class for optional IE8 & lower targeted CSS. role: 'dialog', tabindex: '-1' }).hide(); $overlay = $tag(div, "Overlay").hide(); $loadingOverlay = $([$tag(div, "LoadingOverlay")[0],$tag(div, "LoadingGraphic")[0]]); $wrap = $tag(div, "Wrapper"); $content = $tag(div, "Content").append( $title = $tag(div, "Title"), $current = $tag(div, "Current"), $prev = $('<button type="button"/>').attr({id:prefix+'Previous'}), $next = $('<button type="button"/>').attr({id:prefix+'Next'}), $slideshow = $tag('button', "Slideshow"), $loadingOverlay ); $close = $('<button type="button"/>').attr({id:prefix+'Close'}); $wrap.append( // The 3x3 Grid that makes up Colorbox $tag(div).append( $tag(div, "TopLeft"), $topBorder = $tag(div, "TopCenter"), $tag(div, "TopRight") ), $tag(div, false, 'clear:left').append( $leftBorder = $tag(div, "MiddleLeft"), $content, $rightBorder = $tag(div, "MiddleRight") ), $tag(div, false, 'clear:left').append( $tag(div, "BottomLeft"), $bottomBorder = $tag(div, "BottomCenter"), $tag(div, "BottomRight") ) ).find('div div').css({'float': 'left'}); $loadingBay = $tag(div, false, 'position:absolute; width:9999px; visibility:hidden; display:none; max-width:none;'); $groupControls = $next.add($prev).add($current).add($slideshow); $(document.body).append($overlay, $box.append($wrap, $loadingBay)); } } // Add Colorbox's event bindings function addBindings() { function clickHandler(e) { // ignore non-left-mouse-clicks and clicks modified with ctrl / command, shift, or alt. // See: http://jacklmoore.com/notes/click-events/ if (!(e.which > 1 || e.shiftKey || e.altKey || e.metaKey || e.ctrlKey)) { e.preventDefault(); launch(this); } } if ($box) { if (!init) { init = true; // Anonymous functions here keep the public method from being cached, thereby allowing them to be redefined on the fly. $next.click(function () { publicMethod.next(); }); $prev.click(function () { publicMethod.prev(); }); $close.click(function () { publicMethod.close(); }); $overlay.click(function () { if (settings.overlayClose) { publicMethod.close(); } }); // Key Bindings $(document).bind('keydown.' + prefix, function (e) { var key = e.keyCode; if (open && settings.escKey && key === 27) { e.preventDefault(); publicMethod.close(); } if (open && settings.arrowKey && $related[1] && !e.altKey) { if (key === 37) { e.preventDefault(); $prev.click(); } else if (key === 39) { e.preventDefault(); $next.click(); } } }); if ($.isFunction($.fn.on)) { // For jQuery 1.7+ $(document).on('click.'+prefix, '.'+boxElement, clickHandler); } else { // For jQuery 1.3.x -> 1.6.x // This code is never reached in jQuery 1.9, so do not contact me about 'live' being removed. // This is not here for jQuery 1.9, it's here for legacy users. $('.'+boxElement).live('click.'+prefix, clickHandler); } } return true; } return false; } // Don't do anything if Colorbox already exists. if ($.colorbox) { return; } // Append the HTML when the DOM loads $(appendHTML); // **************** // PUBLIC FUNCTIONS // Usage format: $.colorbox.close(); // Usage from within an iframe: parent.jQuery.colorbox.close(); // **************** publicMethod = $.fn[colorbox] = $[colorbox] = function (options, callback) { var $this = this; options = options || {}; appendHTML(); if (addBindings()) { if ($.isFunction($this)) { // assume a call to $.colorbox $this = $('<a/>'); options.open = true; } else if (!$this[0]) { // colorbox being applied to empty collection return $this; } if (callback) { options.onComplete = callback; } $this.each(function () { $.data(this, colorbox, $.extend({}, $.data(this, colorbox) || defaults, options)); }).addClass(boxElement); if (($.isFunction(options.open) && options.open.call($this)) || options.open) { launch($this[0]); } } return $this; }; publicMethod.position = function (speed, loadedCallback) { var css, top = 0, left = 0, offset = $box.offset(), scrollTop, scrollLeft; $window.unbind('resize.' + prefix); // remove the modal so that it doesn't influence the document width/height $box.css({top: -9e4, left: -9e4}); scrollTop = $window.scrollTop(); scrollLeft = $window.scrollLeft(); if (settings.fixed) { offset.top -= scrollTop; offset.left -= scrollLeft; $box.css({position: 'fixed'}); } else { top = scrollTop; left = scrollLeft; $box.css({position: 'absolute'}); } // keeps the top and left positions within the browser's viewport. if (settings.right !== false) { left += Math.max($window.width() - settings.w - loadedWidth - interfaceWidth - setSize(settings.right, 'x'), 0); } else if (settings.left !== false) { left += setSize(settings.left, 'x'); } else { left += Math.round(Math.max($window.width() - settings.w - loadedWidth - interfaceWidth, 0) / 2); } if (settings.bottom !== false) { top += Math.max(winheight() - settings.h - loadedHeight - interfaceHeight - setSize(settings.bottom, 'y'), 0); } else if (settings.top !== false) { top += setSize(settings.top, 'y'); } else { top += Math.round(Math.max(winheight() - settings.h - loadedHeight - interfaceHeight, 0) / 2); } $box.css({top: offset.top, left: offset.left, visibility:'visible'}); // this gives the wrapper plenty of breathing room so it's floated contents can move around smoothly, // but it has to be shrank down around the size of div#colorbox when it's done. If not, // it can invoke an obscure IE bug when using iframes. $wrap[0].style.width = $wrap[0].style.height = "9999px"; function modalDimensions() { $topBorder[0].style.width = $bottomBorder[0].style.width = $content[0].style.width = (parseInt($box[0].style.width,10) - interfaceWidth)+'px'; $content[0].style.height = $leftBorder[0].style.height = $rightBorder[0].style.height = (parseInt($box[0].style.height,10) - interfaceHeight)+'px'; } css = {width: settings.w + loadedWidth + interfaceWidth, height: settings.h + loadedHeight + interfaceHeight, top: top, left: left}; // setting the speed to 0 if the content hasn't changed size or position if (speed) { var tempSpeed = 0; $.each(css, function(i){ if (css[i] !== previousCSS[i]) { tempSpeed = speed; return; } }); speed = tempSpeed; } previousCSS = css; if (!speed) { $box.css(css); } $box.dequeue().animate(css, { duration: speed || 0, complete: function () { modalDimensions(); active = false; // shrink the wrapper down to exactly the size of colorbox to avoid a bug in IE's iframe implementation. $wrap[0].style.width = (settings.w + loadedWidth + interfaceWidth) + "px"; $wrap[0].style.height = (settings.h + loadedHeight + interfaceHeight) + "px"; if (settings.reposition) { setTimeout(function () { // small delay before binding onresize due to an IE8 bug. $window.bind('resize.' + prefix, publicMethod.position); }, 1); } if (loadedCallback) { loadedCallback(); } }, step: modalDimensions }); }; publicMethod.resize = function (options) { var scrolltop; if (open) { options = options || {}; if (options.width) { settings.w = setSize(options.width, 'x') - loadedWidth - interfaceWidth; } if (options.innerWidth) { settings.w = setSize(options.innerWidth, 'x'); } $loaded.css({width: settings.w}); if (options.height) { settings.h = setSize(options.height, 'y') - loadedHeight - interfaceHeight; } if (options.innerHeight) { settings.h = setSize(options.innerHeight, 'y'); } if (!options.innerHeight && !options.height) { scrolltop = $loaded.scrollTop(); $loaded.css({height: "auto"}); settings.h = $loaded.height(); } $loaded.css({height: settings.h}); if(scrolltop) { $loaded.scrollTop(scrolltop); } publicMethod.position(settings.transition === "none" ? 0 : settings.speed); } }; publicMethod.prep = function (object) { if (!open) { return; } var callback, speed = settings.transition === "none" ? 0 : settings.speed; $loaded.empty().remove(); // Using empty first may prevent some IE7 issues. $loaded = $tag(div, 'LoadedContent').append(object); function getWidth() { settings.w = settings.w || $loaded.width(); settings.w = settings.mw && settings.mw < settings.w ? settings.mw : settings.w; return settings.w; } function getHeight() { settings.h = settings.h || $loaded.height(); settings.h = settings.mh && settings.mh < settings.h ? settings.mh : settings.h; return settings.h; } $loaded.hide() .appendTo($loadingBay.show())// content has to be appended to the DOM for accurate size calculations. .css({width: getWidth(), overflow: settings.scrolling ? 'auto' : 'hidden'}) .css({height: getHeight()})// sets the height independently from the width in case the new width influences the value of height. .prependTo($content); $loadingBay.hide(); // floating the IMG removes the bottom line-height and fixed a problem where IE miscalculates the width of the parent element as 100% of the document width. $(photo).css({'float': 'none'}); callback = function () { var total = $related.length, iframe, frameBorder = 'frameBorder', allowTransparency = 'allowTransparency', complete; if (!open) { return; } function removeFilter() { // Needed for IE7 & IE8 in versions of jQuery prior to 1.7.2 if ($.support.opacity === false) { $box[0].style.removeAttribute('filter'); } } complete = function () { clearTimeout(loadingTimer); $loadingOverlay.hide(); trigger(event_complete, settings.onComplete); }; $title.html(settings.title).add($loaded).show(); if (total > 1) { // handle grouping if (typeof settings.current === "string") { $current.html(settings.current.replace('{current}', index + 1).replace('{total}', total)).show(); } $next[(settings.loop || index < total - 1) ? "show" : "hide"]().html(settings.next); $prev[(settings.loop || index) ? "show" : "hide"]().html(settings.previous); slideshow(); // Preloads images within a rel group if (settings.preloading) { $.each([getIndex(-1), getIndex(1)], function(){ var src, img, i = $related[this], data = $.data(i, colorbox); if (data && data.href) { src = data.href; if ($.isFunction(src)) { src = src.call(i); } } else { src = $(i).attr('href'); } if (src && isImage(data, src)) { src = retinaUrl(data, src); img = document.createElement('img'); img.src = src; } }); } } else { $groupControls.hide(); } if (settings.iframe) { iframe = $tag('iframe')[0]; if (frameBorder in iframe) { iframe[frameBorder] = 0; } if (allowTransparency in iframe) { iframe[allowTransparency] = "true"; } if (!settings.scrolling) { iframe.scrolling = "no"; } $(iframe) .attr({ src: settings.href, name: (new Date()).getTime(), // give the iframe a unique name to prevent caching 'class': prefix + 'Iframe', allowFullScreen : true, // allow HTML5 video to go fullscreen webkitAllowFullScreen : true, mozallowfullscreen : true }) .one('load', complete) .appendTo($loaded); $events.one(event_purge, function () { iframe.src = "//about:blank"; }); if (settings.fastIframe) { $(iframe).trigger('load'); } } else { complete(); } if (settings.transition === 'fade') { $box.fadeTo(speed, 1, removeFilter); } else { removeFilter(); } }; if (settings.transition === 'fade') { $box.fadeTo(speed, 0, function () { publicMethod.position(0, callback); }); } else { publicMethod.position(speed, callback); } }; function load () { var href, setResize, prep = publicMethod.prep, $inline, request = ++requests; active = true; photo = false; element = $related[index]; makeSettings(); trigger(event_purge); trigger(event_load, settings.onLoad); settings.h = settings.height ? setSize(settings.height, 'y') - loadedHeight - interfaceHeight : settings.innerHeight && setSize(settings.innerHeight, 'y'); settings.w = settings.width ? setSize(settings.width, 'x') - loadedWidth - interfaceWidth : settings.innerWidth && setSize(settings.innerWidth, 'x'); // Sets the minimum dimensions for use in image scaling settings.mw = settings.w; settings.mh = settings.h; // Re-evaluate the minimum width and height based on maxWidth and maxHeight values. // If the width or height exceed the maxWidth or maxHeight, use the maximum values instead. if (settings.maxWidth) { settings.mw = setSize(settings.maxWidth, 'x') - loadedWidth - interfaceWidth; settings.mw = settings.w && settings.w < settings.mw ? settings.w : settings.mw; } if (settings.maxHeight) { settings.mh = setSize(settings.maxHeight, 'y') - loadedHeight - interfaceHeight; settings.mh = settings.h && settings.h < settings.mh ? settings.h : settings.mh; } href = settings.href; loadingTimer = setTimeout(function () { $loadingOverlay.show(); }, 100); if (settings.inline) { // Inserts an empty placeholder where inline content is being pulled from. // An event is bound to put inline content back when Colorbox closes or loads new content. $inline = $tag(div).hide().insertBefore($(href)[0]); $events.one(event_purge, function () { $inline.replaceWith($loaded.children()); }); prep($(href)); } else if (settings.iframe) { // IFrame element won't be added to the DOM until it is ready to be displayed, // to avoid problems with DOM-ready JS that might be trying to run in that iframe. prep(" "); } else if (settings.html) { prep(settings.html); } else if (isImage(settings, href)) { href = retinaUrl(settings, href); photo = document.createElement('img'); $(photo) .addClass(prefix + 'Photo') .bind('error',function () { settings.title = false; prep($tag(div, 'Error').html(settings.imgError)); }) .one('load', function () { var percent; if (request !== requests) { return; } $.each(['alt', 'longdesc', 'aria-describedby'], function(i,val){ var attr = $(element).attr(val) || $(element).attr('data-'+val); if (attr) { photo.setAttribute(val, attr); } }); if (settings.retinaImage && window.devicePixelRatio > 1) { photo.height = photo.height / window.devicePixelRatio; photo.width = photo.width / window.devicePixelRatio; } if (settings.scalePhotos) { setResize = function () { photo.height -= photo.height * percent; photo.width -= photo.width * percent; }; if (settings.mw && photo.width > settings.mw) { percent = (photo.width - settings.mw) / photo.width; setResize(); } if (settings.mh && photo.height > settings.mh) { percent = (photo.height - settings.mh) / photo.height; setResize(); } } if (settings.h) { photo.style.marginTop = Math.max(settings.mh - photo.height, 0) / 2 + 'px'; } if ($related[1] && (settings.loop || $related[index + 1])) { photo.style.cursor = 'pointer'; photo.onclick = function () { publicMethod.next(); }; } photo.style.width = photo.width + 'px'; photo.style.height = photo.height + 'px'; setTimeout(function () { // A pause because Chrome will sometimes report a 0 by 0 size otherwise. prep(photo); }, 1); }); setTimeout(function () { // A pause because Opera 10.6+ will sometimes not run the onload function otherwise. photo.src = href; }, 1); } else if (href) { $loadingBay.load(href, settings.data, function (data, status) { if (request === requests) { prep(status === 'error' ? $tag(div, 'Error').html(settings.xhrError) : $(this).contents()); } }); } } // Navigates to the next page/image in a set. publicMethod.next = function () { if (!active && $related[1] && (settings.loop || $related[index + 1])) { index = getIndex(1); launch($related[index]); } }; publicMethod.prev = function () { if (!active && $related[1] && (settings.loop || index)) { index = getIndex(-1); launch($related[index]); } }; // Note: to use this within an iframe use the following format: parent.jQuery.colorbox.close(); publicMethod.close = function () { if (open && !closing) { closing = true; open = false; trigger(event_cleanup, settings.onCleanup); $window.unbind('.' + prefix); $overlay.fadeTo(settings.fadeOut || 0, 0); $box.stop().fadeTo(settings.fadeOut || 0, 0, function () { $box.add($overlay).css({'opacity': 1, cursor: 'auto'}).hide(); trigger(event_purge); $loaded.empty().remove(); // Using empty first may prevent some IE7 issues. setTimeout(function () { closing = false; trigger(event_closed, settings.onClosed); }, 1); }); } }; // Removes changes Colorbox made to the document, but does not remove the plugin. publicMethod.remove = function () { if (!$box) { return; } $box.stop(); $.colorbox.close(); $box.stop().remove(); $overlay.remove(); closing = false; $box = null; $('.' + boxElement) .removeData(colorbox) .removeClass(boxElement); $(document).unbind('click.'+prefix); }; // A method for fetching the current element Colorbox is referencing. // returns a jQuery object. publicMethod.element = function () { return $(element); }; publicMethod.settings = defaults; }(jQuery, document, window)); �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_colorbox.css�������������������������������������������������0000644�0001750�0001750�00000005475�12301071671�022113� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Colorbox Core Style: The following CSS is consistent between example themes and should not be altered. */ #colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;} #cboxWrapper {max-width:none;} #cboxOverlay{position:fixed; width:100%; height:100%;} #cboxMiddleLeft, #cboxBottomLeft{clear:left;} #cboxContent{position:relative;} #cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;} #cboxTitle{margin:0;} #cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;} #cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;} .cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none; -ms-interpolation-mode:bicubic;} .cboxIframe{width:100%; height:100%; display:block; border:0;} #colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;} /* User Style: Change the following styles to modify the appearance of Colorbox. They are ordered & tabbed in a way that represents the nesting of the generated HTML. */ #cboxOverlay{background:#000;} #colorbox{outline:0;} #cboxContent{margin-top:20px;background:#000;} .cboxIframe{background:#fff;} #cboxError{padding:50px; border:1px solid #ccc;} #cboxLoadedContent{border:5px solid #000; background:#fff;} #cboxTitle{position:absolute; top:-20px; left:0; color:#ccc;} #cboxCurrent{position:absolute; top:-20px; right:0px; color:#ccc;} #cboxLoadingGraphic{background:url(loading.gif) no-repeat center center;} /* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */ #cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; width:auto; background:none; } /* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */ #cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;} #cboxSlideshow{position:absolute; top:-20px; right:90px; color:#fff;} #cboxPrevious{position:absolute; top:50%; left:5px; margin-top:-32px; background:url(controls.png) no-repeat top left; width:28px; height:65px; text-indent:-9999px;} #cboxPrevious:hover{background-position:bottom left;} #cboxNext{position:absolute; top:50%; right:5px; margin-top:-32px; background:url(controls.png) no-repeat top right; width:28px; height:65px; text-indent:-9999px;} #cboxNext:hover{background-position:bottom right;} #cboxClose{position:absolute; top:5px; right:5px; display:block; background:url(controls.png) no-repeat top center; width:38px; height:19px; text-indent:-9999px;} #cboxClose:hover{background-position:bottom center;} ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_controls.png�������������������������������������������������0000644�0001750�0001750�00000003141�12301071671�022107� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���b������?���PLTE���������������������������������������������fffNOPJJJ)))ZZZkkk{{{vwxsssnoqRRREFGBBB:::--.!!!333ԭ䃃ܷ״ʩ'q���tRNS�"3DUfwF*��/IDATx^W8�:v !`g/vޛp1Tp:IV ʨɲN*82$sCddG�?%yI HZPC!A(Dp;66",Ah񤸁Hv펚͞GJx DD:C9& jW(!N'? b&҄P~ibtn  1nF6u$ FIq3͑hXv j^XXta$D 'W̕�ȑ�Uɕc@G&ɇ&h섳A,uoƃX4-As%OM~6҄okY~zH09n8YuL`v3VbVgãNLGyp%>Q kYQH!\ .NP#$֘E L?Ka!A/JP7vnE;4"dFZjڦ(džJ61" {e#bN <CiyaGBDP DHȟ!f>JywU>M|j^Be&ļJTA\@@"7`n~0Q爊&ǒ�SKDDU%%V C J \BD@%lBZل !T AL '(FUJBJ[1VuЅ�AWj(`6B\rh H 9 ߙ o»3yI[:<ًT!:Iy>Dyp h c8Oð?ri?vAH|qb2~k Gz$G`ox$Z۷;O-!Oa"©c``de'LEF@Za`cd@T!3qCf"2"R ƀ@c%n8E_O7[pno߆!2O-[4x,p K'{׫ Ó"Ɗ1zj2@bݪrHWs@MYH p 0 UA Gt !۸<mq7|x',XHxJ5qNMB|R�Y(4Y>o6v�Qr95jPݝ>Mэӄy64}fwkITOB˻XZjy4B 3!1_s|� \|]%�}"AdRЄ,sE����IENDB`�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/thumb.image.thtml���������������������������������������������������0000644�0001750�0001750�00000003136�12301071671�021766� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<li xmlns:py="http://genshi.edgewall.org/" class="media media_$media.type"> <a class="thumb" href="$media.link" title="$media.comment"> <img class="media media_$media.type" src="$media.thumb" width="$media.thumb_width" height="$media.thumb_height" alt="$media.thumb_name thumb" /> </a> <div class="caption"> <div class="image_comment" py:if="media.comment">$media.comment</div> <div class="image_date" py:if="media.image_datetime"> ${_('Taken')} ${media.image_datetime.strftime(_('on %d/%m/%Y at %H:%M'))} </div> <div class="authorship" py:if="media.authorship">${_('Author')} : $media.authorship</div> <div py:if="media.original_link" class="original_link"><a href="$media.original_link">${_('Original picture')}</a></div> <div class="image_caption_tech"> <ul> <li>$media.image_name</li> <li py:if="media.camera_name">${_('Camera:')} $media.camera_name <py:if test="media.lens_name"> ${_('with')} $media.lens_name</py:if> </li> <li py:if="media.exposure">${_('Exposure')} $media.exposure</li> <li py:if="media.iso">${_('Sensitivity ISO')} $media.iso</li> <li py:if="media.fnumber">${_('Aperture')} $media.fnumber</li> <li py:if="media.flash">${_('Flash')} $media.flash</li> <li py:if="media.focal_length">${_('Focal length')} $media.focal_length</li> </ul> </div> </div> </li> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/manifest.json�������������������������������������������������������0000644�0001750�0001750�00000000074�12301071671�021213� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "shared": [ { "path": "../default/SHARED_jquery.js" } ] } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/gallerylink.thtml���������������������������������������������������0000644�0001750�0001750�00000002153�12301071671�022101� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" class="subgal_link"> <div py:if="subgal_link.album_picture" class="subgal_image"> <a href="$subgal_link.link"> <img src="$subgal_link.album_picture" alt="$subgal_link.album_name album picture" /> </a> </div> <div class="subgal_name"> <h4 class="subgal_name"> <a href="$subgal_link.link" py:content="subgal_link.album_name" /> </h4> <div py:if="subgal_link.album_description" class="subgal_description"> <p>$subgal_link.album_description</p> </div> <div py:if="subgal_link.image_count > 0 or subgal_link.subgal_count > 0" class="subgal_stats"> <py:if test="subgal_link.image_count > 0"> $subgal_link.image_count ${_('photos')} <py:if test="subgal_link.subgal_count > 0">, </py:if> </py:if> <py:if test="subgal_link.subgal_count > 0"> $subgal_link.subgal_count ${_('sub-galleries')} </py:if> </div> </div> </div> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_basic.css����������������������������������������������������0000644�0001750�0001750�00000000276�12301071671�021337� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.media { position: relative; display: inline-block; } .video_arrow { position: absolute; border: none; width: 40%; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/SHARED_lazygal.js���������������������������������������������������0000644�0001750�0001750�00000000377�12301071671�021547� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������$(document).ready(function(e) { $(".media_links").each(function(index, gal){ $(this).find(".thumb").colorbox({ rel: gal, maxWidth:"80%", maxHeight:"80%", scalePhotos:true, }) }); }); �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/singlepage/thumb.video.thtml���������������������������������������������������0000644�0001750�0001750�00000001455�12301071671�022014� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<li xmlns:py="http://genshi.edgewall.org/" class="media media_$media.type"> <a class="thumb" href="$media.link"><img class="media media_$media.type" src="$media.thumb" width="$media.thumb_width" height="$media.thumb_height" alt="$media.thumb_name thumb" /></a> <a href="$media.link"><img class="video_arrow" src="${rel_root}shared/video_arrow.svg" alt="video arrow overlay" /><span class="video_length" py:content="media.length" /></a> <div class="caption"> <p py:if="original_link"><a href="$original_link">${_('Original video')}</a></p> </div> </li> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/�����������������������������������������������������������������������0000755�0001750�0001750�00000000000�12301073200�016005� 5����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/SHARED_jquery.js�������������������������������������������������������0000644�0001750�0001750�00001050500�12301071671�020723� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/*! * jQuery JavaScript Library v1.11.0 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * * Date: 2014-01-23T21:02Z */ (function( global, factory ) { if ( typeof module === "object" && typeof module.exports === "object" ) { // For CommonJS and CommonJS-like environments where a proper window is present, // execute the factory and get jQuery // For environments that do not inherently posses a window with a document // (such as Node.js), expose a jQuery-making factory as module.exports // This accentuates the need for the creation of a real window // e.g. var jQuery = require("jquery")(window); // See ticket #14549 for more info module.exports = global.document ? factory( global, true ) : function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } // Pass this if window is not defined yet }(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { // Can't do this because several apps including ASP.NET trace // the stack via arguments.caller.callee and Firefox dies if // you try to trace through "use strict" call chains. (#13335) // Support: Firefox 18+ // var deletedIds = []; var slice = deletedIds.slice; var concat = deletedIds.concat; var push = deletedIds.push; var indexOf = deletedIds.indexOf; var class2type = {}; var toString = class2type.toString; var hasOwn = class2type.hasOwnProperty; var trim = "".trim; var support = {}; var version = "1.11.0", // Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); }, // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, // Matches dashed string for camelizing rmsPrefix = /^-ms-/, rdashAlpha = /-([\da-z])/gi, // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return letter.toUpperCase(); }; jQuery.fn = jQuery.prototype = { // The current version of jQuery being used jquery: version, constructor: jQuery, // Start with an empty selector selector: "", // The default length of a jQuery object is 0 length: 0, toArray: function() { return slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { return num != null ? // Return a 'clean' array ( num < 0 ? this[ num + this.length ] : this[ num ] ) : // Return just the object slice.call( this ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems ) { // Build a new jQuery matched element set var ret = jQuery.merge( this.constructor(), elems ); // Add the old object onto the stack (as a reference) ret.prevObject = this; ret.context = this.context; // Return the newly-formed element set return ret; }, // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) each: function( callback, args ) { return jQuery.each( this, callback, args ); }, map: function( callback ) { return this.pushStack( jQuery.map(this, function( elem, i ) { return callback.call( elem, i, elem ); })); }, slice: function() { return this.pushStack( slice.apply( this, arguments ) ); }, first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); }, eq: function( i ) { var len = this.length, j = +i + ( i < 0 ? len : 0 ); return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); }, end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: push, sort: deletedIds.sort, splice: deletedIds.splice }; jQuery.extend = jQuery.fn.extend = function() { var src, copyIsArray, copy, name, options, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; // skip the boolean and the target target = arguments[ i ] || {}; i++; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // extend jQuery itself if only one argument is passed if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; jQuery.extend({ // Unique for each copy of jQuery on the page expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), // Assume jQuery is ready without the ready module isReady: true, error: function( msg ) { throw new Error( msg ); }, noop: function() {}, // See test/unit/core.js for details concerning isFunction. // Since version 1.3, DOM methods and functions like alert // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { return jQuery.type(obj) === "function"; }, isArray: Array.isArray || function( obj ) { return jQuery.type(obj) === "array"; }, isWindow: function( obj ) { /* jshint eqeqeq: false */ return obj != null && obj == obj.window; }, isNumeric: function( obj ) { // parseFloat NaNs numeric-cast false positives (null|true|false|"") // ...but misinterprets leading-number strings, particularly hex literals ("0x...") // subtraction forces infinities to NaN return obj - parseFloat( obj ) >= 0; }, isEmptyObject: function( obj ) { var name; for ( name in obj ) { return false; } return true; }, isPlainObject: function( obj ) { var key; // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } try { // Not own constructor property must be Object if ( obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { return false; } } catch ( e ) { // IE8,9 Will throw exceptions on certain host objects #9897 return false; } // Support: IE<9 // Handle iteration over inherited properties before own properties. if ( support.ownLast ) { for ( key in obj ) { return hasOwn.call( obj, key ); } } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. for ( key in obj ) {} return key === undefined || hasOwn.call( obj, key ); }, type: function( obj ) { if ( obj == null ) { return obj + ""; } return typeof obj === "object" || typeof obj === "function" ? class2type[ toString.call(obj) ] || "object" : typeof obj; }, // Evaluates a script in a global context // Workarounds based on findings by Jim Driscoll // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { if ( data && jQuery.trim( data ) ) { // We use execScript on Internet Explorer // We use an anonymous function so that context is window // rather than jQuery in Firefox ( window.execScript || function( data ) { window[ "eval" ].call( window, data ); } )( data ); } }, // Convert dashed to camelCase; used by the css and data modules // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, // args is for internal usage only each: function( obj, callback, args ) { var value, i = 0, length = obj.length, isArray = isArraylike( obj ); if ( args ) { if ( isArray ) { for ( ; i < length; i++ ) { value = callback.apply( obj[ i ], args ); if ( value === false ) { break; } } } else { for ( i in obj ) { value = callback.apply( obj[ i ], args ); if ( value === false ) { break; } } } // A special, fast, case for the most common use of each } else { if ( isArray ) { for ( ; i < length; i++ ) { value = callback.call( obj[ i ], i, obj[ i ] ); if ( value === false ) { break; } } } else { for ( i in obj ) { value = callback.call( obj[ i ], i, obj[ i ] ); if ( value === false ) { break; } } } } return obj; }, // Use native String.trim function wherever possible trim: trim && !trim.call("\uFEFF\xA0") ? function( text ) { return text == null ? "" : trim.call( text ); } : // Otherwise use our own trimming functionality function( text ) { return text == null ? "" : ( text + "" ).replace( rtrim, "" ); }, // results is for internal usage only makeArray: function( arr, results ) { var ret = results || []; if ( arr != null ) { if ( isArraylike( Object(arr) ) ) { jQuery.merge( ret, typeof arr === "string" ? [ arr ] : arr ); } else { push.call( ret, arr ); } } return ret; }, inArray: function( elem, arr, i ) { var len; if ( arr ) { if ( indexOf ) { return indexOf.call( arr, elem, i ); } len = arr.length; i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; for ( ; i < len; i++ ) { // Skip accessing in sparse arrays if ( i in arr && arr[ i ] === elem ) { return i; } } } return -1; }, merge: function( first, second ) { var len = +second.length, j = 0, i = first.length; while ( j < len ) { first[ i++ ] = second[ j++ ]; } // Support: IE<9 // Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists) if ( len !== len ) { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; } } first.length = i; return first; }, grep: function( elems, callback, invert ) { var callbackInverse, matches = [], i = 0, length = elems.length, callbackExpect = !invert; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { callbackInverse = !callback( elems[ i ], i ); if ( callbackInverse !== callbackExpect ) { matches.push( elems[ i ] ); } } return matches; }, // arg is for internal usage only map: function( elems, callback, arg ) { var value, i = 0, length = elems.length, isArray = isArraylike( elems ), ret = []; // Go through the array, translating each of the items to their new values if ( isArray ) { for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret.push( value ); } } // Go through every key on the object, } else { for ( i in elems ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret.push( value ); } } } // Flatten any nested arrays return concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { var args, proxy, tmp; if ( typeof context === "string" ) { tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind args = slice.call( arguments, 2 ); proxy = function() { return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || jQuery.guid++; return proxy; }, now: function() { return +( new Date() ); }, // jQuery.support is not used in Core but other projects attach their // properties to it so it needs to exist. support: support }); // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); function isArraylike( obj ) { var length = obj.length, type = jQuery.type( obj ); if ( type === "function" || jQuery.isWindow( obj ) ) { return false; } if ( obj.nodeType === 1 && length ) { return true; } return type === "array" || length === 0 || typeof length === "number" && length > 0 && ( length - 1 ) in obj; } var Sizzle = /*! * Sizzle CSS Selector Engine v1.10.16 * http://sizzlejs.com/ * * Copyright 2013 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * * Date: 2014-01-13 */ (function( window ) { var i, support, Expr, getText, isXML, compile, outermostContext, sortInput, hasDuplicate, // Local document vars setDocument, document, docElem, documentIsHTML, rbuggyQSA, rbuggyMatches, matches, contains, // Instance-specific data expando = "sizzle" + -(new Date()), preferredDoc = window.document, dirruns = 0, done = 0, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; } return 0; }, // General-purpose constants strundefined = typeof undefined, MAX_NEGATIVE = 1 << 31, // Instance methods hasOwn = ({}).hasOwnProperty, arr = [], pop = arr.pop, push_native = arr.push, push = arr.push, slice = arr.slice, // Use a stripped-down indexOf if we can't use a native one indexOf = arr.indexOf || function( elem ) { var i = 0, len = this.length; for ( ; i < len; i++ ) { if ( this[i] === elem ) { return i; } } return -1; }, booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", // Regular expressions // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", // http://www.w3.org/TR/css3-syntax/#characters characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", // Loosely modeled on CSS identifier characters // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier identifier = characterEncoding.replace( "w", "w#" ), // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", // Prefer arguments quoted, // then not containing pseudos/brackets, // then attribute selectors/non-parenthetical expressions, // then anything else // These preferences are here to reduce the number of selectors // needing tokenize in the PSEUDO preFilter pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), matchExpr = { "ID": new RegExp( "^#(" + characterEncoding + ")" ), "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), // For use in libraries implementing .is() // We use this for POS matching in `select` "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, rnative = /^[^{]+\{\s*\[native \w/, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, rsibling = /[+~]/, rescape = /'|\\/g, // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), funescape = function( _, escaped, escapedWhitespace ) { var high = "0x" + escaped - 0x10000; // NaN means non-codepoint // Support: Firefox // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : high < 0 ? // BMP codepoint String.fromCharCode( high + 0x10000 ) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); }; // Optimize for push.apply( _, NodeList ) try { push.apply( (arr = slice.call( preferredDoc.childNodes )), preferredDoc.childNodes ); // Support: Android<4.0 // Detect silently failing push.apply arr[ preferredDoc.childNodes.length ].nodeType; } catch ( e ) { push = { apply: arr.length ? // Leverage slice if possible function( target, els ) { push_native.apply( target, slice.call(els) ); } : // Support: IE<9 // Otherwise append directly function( target, els ) { var j = target.length, i = 0; // Can't trust NodeList.length while ( (target[j++] = els[i++]) ) {} target.length = j - 1; } }; } function Sizzle( selector, context, results, seed ) { var match, elem, m, nodeType, // QSA vars i, groups, old, nid, newContext, newSelector; if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { setDocument( context ); } context = context || document; results = results || []; if ( !selector || typeof selector !== "string" ) { return results; } if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { return []; } if ( documentIsHTML && !seed ) { // Shortcuts if ( (match = rquickExpr.exec( selector )) ) { // Speed-up: Sizzle("#ID") if ( (m = match[1]) ) { if ( nodeType === 9 ) { elem = context.getElementById( m ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document (jQuery #6963) if ( elem && elem.parentNode ) { // Handle the case where IE, Opera, and Webkit return items // by name instead of ID if ( elem.id === m ) { results.push( elem ); return results; } } else { return results; } } else { // Context is not a document if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && contains( context, elem ) && elem.id === m ) { results.push( elem ); return results; } } // Speed-up: Sizzle("TAG") } else if ( match[2] ) { push.apply( results, context.getElementsByTagName( selector ) ); return results; // Speed-up: Sizzle(".CLASS") } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { push.apply( results, context.getElementsByClassName( m ) ); return results; } } // QSA path if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { nid = old = expando; newContext = context; newSelector = nodeType === 9 && selector; // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { groups = tokenize( selector ); if ( (old = context.getAttribute("id")) ) { nid = old.replace( rescape, "\\$&" ); } else { context.setAttribute( "id", nid ); } nid = "[id='" + nid + "'] "; i = groups.length; while ( i-- ) { groups[i] = nid + toSelector( groups[i] ); } newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; newSelector = groups.join(","); } if ( newSelector ) { try { push.apply( results, newContext.querySelectorAll( newSelector ) ); return results; } catch(qsaError) { } finally { if ( !old ) { context.removeAttribute("id"); } } } } } // All others return select( selector.replace( rtrim, "$1" ), context, results, seed ); } /** * Create key-value caches of limited size * @returns {Function(string, Object)} Returns the Object data after storing it on itself with * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) * deleting the oldest entry */ function createCache() { var keys = []; function cache( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) if ( keys.push( key + " " ) > Expr.cacheLength ) { // Only keep the most recent entries delete cache[ keys.shift() ]; } return (cache[ key + " " ] = value); } return cache; } /** * Mark a function for special use by Sizzle * @param {Function} fn The function to mark */ function markFunction( fn ) { fn[ expando ] = true; return fn; } /** * Support testing using an element * @param {Function} fn Passed the created div and expects a boolean result */ function assert( fn ) { var div = document.createElement("div"); try { return !!fn( div ); } catch (e) { return false; } finally { // Remove from its parent by default if ( div.parentNode ) { div.parentNode.removeChild( div ); } // release memory in IE div = null; } } /** * Adds the same handler for all of the specified attrs * @param {String} attrs Pipe-separated list of attributes * @param {Function} handler The method that will be applied */ function addHandle( attrs, handler ) { var arr = attrs.split("|"), i = attrs.length; while ( i-- ) { Expr.attrHandle[ arr[i] ] = handler; } } /** * Checks document order of two siblings * @param {Element} a * @param {Element} b * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b */ function siblingCheck( a, b ) { var cur = b && a, diff = cur && a.nodeType === 1 && b.nodeType === 1 && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); // Use IE sourceIndex if available on both nodes if ( diff ) { return diff; } // Check if b follows a if ( cur ) { while ( (cur = cur.nextSibling) ) { if ( cur === b ) { return -1; } } } return a ? 1 : -1; } /** * Returns a function to use in pseudos for input types * @param {String} type */ function createInputPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === type; }; } /** * Returns a function to use in pseudos for buttons * @param {String} type */ function createButtonPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && elem.type === type; }; } /** * Returns a function to use in pseudos for positionals * @param {Function} fn */ function createPositionalPseudo( fn ) { return markFunction(function( argument ) { argument = +argument; return markFunction(function( seed, matches ) { var j, matchIndexes = fn( [], seed.length, argument ), i = matchIndexes.length; // Match elements found at the specified indexes while ( i-- ) { if ( seed[ (j = matchIndexes[i]) ] ) { seed[j] = !(matches[j] = seed[j]); } } }); }); } /** * Checks a node for validity as a Sizzle context * @param {Element|Object=} context * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value */ function testContext( context ) { return context && typeof context.getElementsByTagName !== strundefined && context; } // Expose support vars for convenience support = Sizzle.support = {}; /** * Detects XML nodes * @param {Element|Object} elem An element or a document * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = elem && (elem.ownerDocument || elem).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }; /** * Sets document-related variables once based on the current document * @param {Element|Object} [doc] An element or document object to use to set the document * @returns {Object} Returns the current document */ setDocument = Sizzle.setDocument = function( node ) { var hasCompare, doc = node ? node.ownerDocument || node : preferredDoc, parent = doc.defaultView; // If no document and documentElement is available, return if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { return document; } // Set our document document = doc; docElem = doc.documentElement; // Support tests documentIsHTML = !isXML( doc ); // Support: IE>8 // If iframe document is assigned to "document" variable and if iframe has been reloaded, // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 // IE6-8 do not support the defaultView property so parent will be undefined if ( parent && parent !== parent.top ) { // IE11 does not have attachEvent, so all must suffer if ( parent.addEventListener ) { parent.addEventListener( "unload", function() { setDocument(); }, false ); } else if ( parent.attachEvent ) { parent.attachEvent( "onunload", function() { setDocument(); }); } } /* Attributes ---------------------------------------------------------------------- */ // Support: IE<8 // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) support.attributes = assert(function( div ) { div.className = "i"; return !div.getAttribute("className"); }); /* getElement(s)By* ---------------------------------------------------------------------- */ // Check if getElementsByTagName("*") returns only elements support.getElementsByTagName = assert(function( div ) { div.appendChild( doc.createComment("") ); return !div.getElementsByTagName("*").length; }); // Check if getElementsByClassName can be trusted support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { div.innerHTML = "<div class='a'></div><div class='a i'></div>"; // Support: Safari<4 // Catch class over-caching div.firstChild.className = "i"; // Support: Opera<10 // Catch gEBCN failure to find non-leading classes return div.getElementsByClassName("i").length === 2; }); // Support: IE<10 // Check if getElementById returns elements by name // The broken getElementById methods don't pick up programatically-set names, // so use a roundabout getElementsByName test support.getById = assert(function( div ) { docElem.appendChild( div ).id = expando; return !doc.getElementsByName || !doc.getElementsByName( expando ).length; }); // ID find and filter if ( support.getById ) { Expr.find["ID"] = function( id, context ) { if ( typeof context.getElementById !== strundefined && documentIsHTML ) { var m = context.getElementById( id ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 return m && m.parentNode ? [m] : []; } }; Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { return elem.getAttribute("id") === attrId; }; }; } else { // Support: IE6/7 // getElementById is not reliable as a find shortcut delete Expr.find["ID"]; Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); return node && node.value === attrId; }; }; } // Tag Expr.find["TAG"] = support.getElementsByTagName ? function( tag, context ) { if ( typeof context.getElementsByTagName !== strundefined ) { return context.getElementsByTagName( tag ); } } : function( tag, context ) { var elem, tmp = [], i = 0, results = context.getElementsByTagName( tag ); // Filter out possible comments if ( tag === "*" ) { while ( (elem = results[i++]) ) { if ( elem.nodeType === 1 ) { tmp.push( elem ); } } return tmp; } return results; }; // Class Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { return context.getElementsByClassName( className ); } }; /* QSA/matchesSelector ---------------------------------------------------------------------- */ // QSA and matchesSelector support // matchesSelector(:active) reports false when true (IE9/Opera 11.5) rbuggyMatches = []; // qSa(:focus) reports false when true (Chrome 21) // We allow this because of a bug in IE8/9 that throws an error // whenever `document.activeElement` is accessed on an iframe // So, we allow :focus to pass through QSA all the time to avoid the IE error // See http://bugs.jquery.com/ticket/13378 rbuggyQSA = []; if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { // Build QSA regex // Regex strategy adopted from Diego Perini assert(function( div ) { // Select is set to empty string on purpose // This is to test IE's treatment of not explicitly // setting a boolean content attribute, // since its presence should be enough // http://bugs.jquery.com/ticket/12359 div.innerHTML = "<select t=''><option selected=''></option></select>"; // Support: IE8, Opera 10-12 // Nothing should be selected when empty strings follow ^= or $= or *= if ( div.querySelectorAll("[t^='']").length ) { rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); } // Support: IE8 // Boolean attributes and "value" are not treated correctly if ( !div.querySelectorAll("[selected]").length ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); } // Webkit/Opera - :checked should return selected option elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked // IE8 throws error here and will not see later tests if ( !div.querySelectorAll(":checked").length ) { rbuggyQSA.push(":checked"); } }); assert(function( div ) { // Support: Windows 8 Native Apps // The type and name attributes are restricted during .innerHTML assignment var input = doc.createElement("input"); input.setAttribute( "type", "hidden" ); div.appendChild( input ).setAttribute( "name", "D" ); // Support: IE8 // Enforce case-sensitivity of name attribute if ( div.querySelectorAll("[name=d]").length ) { rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) // IE8 throws error here and will not see later tests if ( !div.querySelectorAll(":enabled").length ) { rbuggyQSA.push( ":enabled", ":disabled" ); } // Opera 10-11 does not throw on post-comma invalid pseudos div.querySelectorAll("*,:x"); rbuggyQSA.push(",.*:"); }); } if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector) )) ) { assert(function( div ) { // Check to see if it's possible to do matchesSelector // on a disconnected node (IE 9) support.disconnectedMatch = matches.call( div, "div" ); // This should fail with an exception // Gecko does not error, returns false instead matches.call( div, "[s!='']:x" ); rbuggyMatches.push( "!=", pseudos ); }); } rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); /* Contains ---------------------------------------------------------------------- */ hasCompare = rnative.test( docElem.compareDocumentPosition ); // Element contains another // Purposefully does not implement inclusive descendent // As in, an element does not contain itself contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!( bup && bup.nodeType === 1 && ( adown.contains ? adown.contains( bup ) : a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 )); } : function( a, b ) { if ( b ) { while ( (b = b.parentNode) ) { if ( b === a ) { return true; } } } return false; }; /* Sorting ---------------------------------------------------------------------- */ // Document order sorting sortOrder = hasCompare ? function( a, b ) { // Flag for duplicate removal if ( a === b ) { hasDuplicate = true; return 0; } // Sort on method existence if only one input has compareDocumentPosition var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; if ( compare ) { return compare; } // Calculate position if both inputs belong to the same document compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? a.compareDocumentPosition( b ) : // Otherwise we know they are disconnected 1; // Disconnected nodes if ( compare & 1 || (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { // Choose the first element that is related to our preferred document if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { return -1; } if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { return 1; } // Maintain original order return sortInput ? ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : 0; } return compare & 4 ? -1 : 1; } : function( a, b ) { // Exit early if the nodes are identical if ( a === b ) { hasDuplicate = true; return 0; } var cur, i = 0, aup = a.parentNode, bup = b.parentNode, ap = [ a ], bp = [ b ]; // Parentless nodes are either documents or disconnected if ( !aup || !bup ) { return a === doc ? -1 : b === doc ? 1 : aup ? -1 : bup ? 1 : sortInput ? ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : 0; // If the nodes are siblings, we can do a quick check } else if ( aup === bup ) { return siblingCheck( a, b ); } // Otherwise we need full lists of their ancestors for comparison cur = a; while ( (cur = cur.parentNode) ) { ap.unshift( cur ); } cur = b; while ( (cur = cur.parentNode) ) { bp.unshift( cur ); } // Walk down the tree looking for a discrepancy while ( ap[i] === bp[i] ) { i++; } return i ? // Do a sibling check if the nodes have a common ancestor siblingCheck( ap[i], bp[i] ) : // Otherwise nodes in our document sort first ap[i] === preferredDoc ? -1 : bp[i] === preferredDoc ? 1 : 0; }; return doc; }; Sizzle.matches = function( expr, elements ) { return Sizzle( expr, null, null, elements ); }; Sizzle.matchesSelector = function( elem, expr ) { // Set document vars if needed if ( ( elem.ownerDocument || elem ) !== document ) { setDocument( elem ); } // Make sure that attribute selectors are quoted expr = expr.replace( rattributeQuotes, "='$1']" ); if ( support.matchesSelector && documentIsHTML && ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { try { var ret = matches.call( elem, expr ); // IE 9's matchesSelector returns false on disconnected nodes if ( ret || support.disconnectedMatch || // As well, disconnected nodes are said to be in a document // fragment in IE 9 elem.document && elem.document.nodeType !== 11 ) { return ret; } } catch(e) {} } return Sizzle( expr, document, null, [elem] ).length > 0; }; Sizzle.contains = function( context, elem ) { // Set document vars if needed if ( ( context.ownerDocument || context ) !== document ) { setDocument( context ); } return contains( context, elem ); }; Sizzle.attr = function( elem, name ) { // Set document vars if needed if ( ( elem.ownerDocument || elem ) !== document ) { setDocument( elem ); } var fn = Expr.attrHandle[ name.toLowerCase() ], // Don't get fooled by Object.prototype properties (jQuery #13807) val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? fn( elem, name, !documentIsHTML ) : undefined; return val !== undefined ? val : support.attributes || !documentIsHTML ? elem.getAttribute( name ) : (val = elem.getAttributeNode(name)) && val.specified ? val.value : null; }; Sizzle.error = function( msg ) { throw new Error( "Syntax error, unrecognized expression: " + msg ); }; /** * Document sorting and removing duplicates * @param {ArrayLike} results */ Sizzle.uniqueSort = function( results ) { var elem, duplicates = [], j = 0, i = 0; // Unless we *know* we can detect duplicates, assume their presence hasDuplicate = !support.detectDuplicates; sortInput = !support.sortStable && results.slice( 0 ); results.sort( sortOrder ); if ( hasDuplicate ) { while ( (elem = results[i++]) ) { if ( elem === results[ i ] ) { j = duplicates.push( i ); } } while ( j-- ) { results.splice( duplicates[ j ], 1 ); } } // Clear input after sorting to release objects // See https://github.com/jquery/sizzle/pull/225 sortInput = null; return results; }; /** * Utility function for retrieving the text value of an array of DOM nodes * @param {Array|Element} elem */ getText = Sizzle.getText = function( elem ) { var node, ret = "", i = 0, nodeType = elem.nodeType; if ( !nodeType ) { // If no nodeType, this is expected to be an array while ( (node = elem[i++]) ) { // Do not traverse comment nodes ret += getText( node ); } } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent for elements // innerText usage removed for consistency of new lines (jQuery #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { // Traverse its children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { ret += getText( elem ); } } } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } // Do not include comment or processing instruction nodes return ret; }; Expr = Sizzle.selectors = { // Can be adjusted by the user cacheLength: 50, createPseudo: markFunction, match: matchExpr, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }, preFilter: { "ATTR": function( match ) { match[1] = match[1].replace( runescape, funescape ); // Move the given value to match[3] whether quoted or unquoted match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); if ( match[2] === "~=" ) { match[3] = " " + match[3] + " "; } return match.slice( 0, 4 ); }, "CHILD": function( match ) { /* matches from matchExpr["CHILD"] 1 type (only|nth|...) 2 what (child|of-type) 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) 4 xn-component of xn+y argument ([+-]?\d*n|) 5 sign of xn-component 6 x of xn-component 7 sign of y-component 8 y of y-component */ match[1] = match[1].toLowerCase(); if ( match[1].slice( 0, 3 ) === "nth" ) { // nth-* requires argument if ( !match[3] ) { Sizzle.error( match[0] ); } // numeric x and y parameters for Expr.filter.CHILD // remember that false/true cast respectively to 0/1 match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); // other types prohibit arguments } else if ( match[3] ) { Sizzle.error( match[0] ); } return match; }, "PSEUDO": function( match ) { var excess, unquoted = !match[5] && match[2]; if ( matchExpr["CHILD"].test( match[0] ) ) { return null; } // Accept quoted arguments as-is if ( match[3] && match[4] !== undefined ) { match[2] = match[4]; // Strip excess characters from unquoted arguments } else if ( unquoted && rpseudo.test( unquoted ) && // Get excess from tokenize (recursively) (excess = tokenize( unquoted, true )) && // advance to the next closing parenthesis (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { // excess is a negative index match[0] = match[0].slice( 0, excess ); match[2] = unquoted.slice( 0, excess ); } // Return only captures needed by the pseudo filter method (type and argument) return match.slice( 0, 3 ); } }, filter: { "TAG": function( nodeNameSelector ) { var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); return nodeNameSelector === "*" ? function() { return true; } : function( elem ) { return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; }; }, "CLASS": function( className ) { var pattern = classCache[ className + " " ]; return pattern || (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && classCache( className, function( elem ) { return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); }); }, "ATTR": function( name, operator, check ) { return function( elem ) { var result = Sizzle.attr( elem, name ); if ( result == null ) { return operator === "!="; } if ( !operator ) { return true; } result += ""; return operator === "=" ? result === check : operator === "!=" ? result !== check : operator === "^=" ? check && result.indexOf( check ) === 0 : operator === "*=" ? check && result.indexOf( check ) > -1 : operator === "$=" ? check && result.slice( -check.length ) === check : operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : false; }; }, "CHILD": function( type, what, argument, first, last ) { var simple = type.slice( 0, 3 ) !== "nth", forward = type.slice( -4 ) !== "last", ofType = what === "of-type"; return first === 1 && last === 0 ? // Shortcut for :nth-*(n) function( elem ) { return !!elem.parentNode; } : function( elem, context, xml ) { var cache, outerCache, node, diff, nodeIndex, start, dir = simple !== forward ? "nextSibling" : "previousSibling", parent = elem.parentNode, name = ofType && elem.nodeName.toLowerCase(), useCache = !xml && !ofType; if ( parent ) { // :(first|last|only)-(child|of-type) if ( simple ) { while ( dir ) { node = elem; while ( (node = node[ dir ]) ) { if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { return false; } } // Reverse direction for :only-* (if we haven't yet done so) start = dir = type === "only" && !start && "nextSibling"; } return true; } start = [ forward ? parent.firstChild : parent.lastChild ]; // non-xml :nth-child(...) stores cache data on `parent` if ( forward && useCache ) { // Seek `elem` from a previously-cached index outerCache = parent[ expando ] || (parent[ expando ] = {}); cache = outerCache[ type ] || []; nodeIndex = cache[0] === dirruns && cache[1]; diff = cache[0] === dirruns && cache[2]; node = nodeIndex && parent.childNodes[ nodeIndex ]; while ( (node = ++nodeIndex && node && node[ dir ] || // Fallback to seeking `elem` from the start (diff = nodeIndex = 0) || start.pop()) ) { // When found, cache indexes on `parent` and break if ( node.nodeType === 1 && ++diff && node === elem ) { outerCache[ type ] = [ dirruns, nodeIndex, diff ]; break; } } // Use previously-cached element index if available } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { diff = cache[1]; // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) } else { // Use the same loop as above to seek `elem` from the start while ( (node = ++nodeIndex && node && node[ dir ] || (diff = nodeIndex = 0) || start.pop()) ) { if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { // Cache the index of each encountered element if ( useCache ) { (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; } if ( node === elem ) { break; } } } } // Incorporate the offset, then check against cycle size diff -= last; return diff === first || ( diff % first === 0 && diff / first >= 0 ); } }; }, "PSEUDO": function( pseudo, argument ) { // pseudo-class names are case-insensitive // http://www.w3.org/TR/selectors/#pseudo-classes // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters // Remember that setFilters inherits from pseudos var args, fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || Sizzle.error( "unsupported pseudo: " + pseudo ); // The user may use createPseudo to indicate that // arguments are needed to create the filter function // just as Sizzle does if ( fn[ expando ] ) { return fn( argument ); } // But maintain support for old signatures if ( fn.length > 1 ) { args = [ pseudo, pseudo, "", argument ]; return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? markFunction(function( seed, matches ) { var idx, matched = fn( seed, argument ), i = matched.length; while ( i-- ) { idx = indexOf.call( seed, matched[i] ); seed[ idx ] = !( matches[ idx ] = matched[i] ); } }) : function( elem ) { return fn( elem, 0, args ); }; } return fn; } }, pseudos: { // Potentially complex pseudos "not": markFunction(function( selector ) { // Trim the selector passed to compile // to avoid treating leading and trailing // spaces as combinators var input = [], results = [], matcher = compile( selector.replace( rtrim, "$1" ) ); return matcher[ expando ] ? markFunction(function( seed, matches, context, xml ) { var elem, unmatched = matcher( seed, null, xml, [] ), i = seed.length; // Match elements unmatched by `matcher` while ( i-- ) { if ( (elem = unmatched[i]) ) { seed[i] = !(matches[i] = elem); } } }) : function( elem, context, xml ) { input[0] = elem; matcher( input, null, xml, results ); return !results.pop(); }; }), "has": markFunction(function( selector ) { return function( elem ) { return Sizzle( selector, elem ).length > 0; }; }), "contains": markFunction(function( text ) { return function( elem ) { return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; }; }), // "Whether an element is represented by a :lang() selector // is based solely on the element's language value // being equal to the identifier C, // or beginning with the identifier C immediately followed by "-". // The matching of C against the element's language value is performed case-insensitively. // The identifier C does not have to be a valid language name." // http://www.w3.org/TR/selectors/#lang-pseudo "lang": markFunction( function( lang ) { // lang value must be a valid identifier if ( !ridentifier.test(lang || "") ) { Sizzle.error( "unsupported lang: " + lang ); } lang = lang.replace( runescape, funescape ).toLowerCase(); return function( elem ) { var elemLang; do { if ( (elemLang = documentIsHTML ? elem.lang : elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { elemLang = elemLang.toLowerCase(); return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; } } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); return false; }; }), // Miscellaneous "target": function( elem ) { var hash = window.location && window.location.hash; return hash && hash.slice( 1 ) === elem.id; }, "root": function( elem ) { return elem === docElem; }, "focus": function( elem ) { return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); }, // Boolean properties "enabled": function( elem ) { return elem.disabled === false; }, "disabled": function( elem ) { return elem.disabled === true; }, "checked": function( elem ) { // In CSS3, :checked should return both checked and selected elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked var nodeName = elem.nodeName.toLowerCase(); return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); }, "selected": function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly if ( elem.parentNode ) { elem.parentNode.selectedIndex; } return elem.selected === true; }, // Contents "empty": function( elem ) { // http://www.w3.org/TR/selectors/#empty-pseudo // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), // but not by others (comment: 8; processing instruction: 7; etc.) // nodeType < 6 works because attributes (2) do not appear as children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { if ( elem.nodeType < 6 ) { return false; } } return true; }, "parent": function( elem ) { return !Expr.pseudos["empty"]( elem ); }, // Element/input types "header": function( elem ) { return rheader.test( elem.nodeName ); }, "input": function( elem ) { return rinputs.test( elem.nodeName ); }, "button": function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === "button" || name === "button"; }, "text": function( elem ) { var attr; return elem.nodeName.toLowerCase() === "input" && elem.type === "text" && // Support: IE<8 // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); }, // Position-in-collection "first": createPositionalPseudo(function() { return [ 0 ]; }), "last": createPositionalPseudo(function( matchIndexes, length ) { return [ length - 1 ]; }), "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { return [ argument < 0 ? argument + length : argument ]; }), "even": createPositionalPseudo(function( matchIndexes, length ) { var i = 0; for ( ; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), "odd": createPositionalPseudo(function( matchIndexes, length ) { var i = 1; for ( ; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { var i = argument < 0 ? argument + length : argument; for ( ; --i >= 0; ) { matchIndexes.push( i ); } return matchIndexes; }), "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { var i = argument < 0 ? argument + length : argument; for ( ; ++i < length; ) { matchIndexes.push( i ); } return matchIndexes; }) } }; Expr.pseudos["nth"] = Expr.pseudos["eq"]; // Add button/input type pseudos for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { Expr.pseudos[ i ] = createInputPseudo( i ); } for ( i in { submit: true, reset: true } ) { Expr.pseudos[ i ] = createButtonPseudo( i ); } // Easy API for creating new setFilters function setFilters() {} setFilters.prototype = Expr.filters = Expr.pseudos; Expr.setFilters = new setFilters(); function tokenize( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } soFar = selector; groups = []; preFilters = Expr.preFilter; while ( soFar ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } groups.push( (tokens = []) ); } matched = false; // Combinators if ( (match = rcombinators.exec( soFar )) ) { matched = match.shift(); tokens.push({ value: matched, // Cast descendant combinators to space type: match[0].replace( rtrim, " " ) }); soFar = soFar.slice( matched.length ); } // Filters for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); tokens.push({ value: matched, type: type, matches: match }); soFar = soFar.slice( matched.length ); } } if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); } function toSelector( tokens ) { var i = 0, len = tokens.length, selector = ""; for ( ; i < len; i++ ) { selector += tokens[i].value; } return selector; } function addCombinator( matcher, combinator, base ) { var dir = combinator.dir, checkNonElements = base && dir === "parentNode", doneName = done++; return combinator.first ? // Check against closest ancestor/preceding element function( elem, context, xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { return matcher( elem, context, xml ); } } } : // Check against all ancestor/preceding elements function( elem, context, xml ) { var oldCache, outerCache, newCache = [ dirruns, doneName ]; // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching if ( xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { if ( matcher( elem, context, xml ) ) { return true; } } } } else { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { outerCache = elem[ expando ] || (elem[ expando ] = {}); if ( (oldCache = outerCache[ dir ]) && oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { // Assign to newCache so results back-propagate to previous elements return (newCache[ 2 ] = oldCache[ 2 ]); } else { // Reuse newcache so results back-propagate to previous elements outerCache[ dir ] = newCache; // A match means we're done; a fail means we have to keep checking if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { return true; } } } } } }; } function elementMatcher( matchers ) { return matchers.length > 1 ? function( elem, context, xml ) { var i = matchers.length; while ( i-- ) { if ( !matchers[i]( elem, context, xml ) ) { return false; } } return true; } : matchers[0]; } function condense( unmatched, map, filter, context, xml ) { var elem, newUnmatched = [], i = 0, len = unmatched.length, mapped = map != null; for ( ; i < len; i++ ) { if ( (elem = unmatched[i]) ) { if ( !filter || filter( elem, context, xml ) ) { newUnmatched.push( elem ); if ( mapped ) { map.push( i ); } } } } return newUnmatched; } function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { if ( postFilter && !postFilter[ expando ] ) { postFilter = setMatcher( postFilter ); } if ( postFinder && !postFinder[ expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } return markFunction(function( seed, results, context, xml ) { var temp, i, elem, preMap = [], postMap = [], preexisting = results.length, // Get initial elements from seed or context elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), // Prefilter to get matcher input, preserving a map for seed-results synchronization matcherIn = preFilter && ( seed || !selector ) ? condense( elems, preMap, preFilter, context, xml ) : elems, matcherOut = matcher ? // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, postFinder || ( seed ? preFilter : preexisting || postFilter ) ? // ...intermediate processing is necessary [] : // ...otherwise use results directly results : matcherIn; // Find primary matches if ( matcher ) { matcher( matcherIn, matcherOut, context, xml ); } // Apply postFilter if ( postFilter ) { temp = condense( matcherOut, postMap ); postFilter( temp, [], context, xml ); // Un-match failing elements by moving them back to matcherIn i = temp.length; while ( i-- ) { if ( (elem = temp[i]) ) { matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); } } } if ( seed ) { if ( postFinder || preFilter ) { if ( postFinder ) { // Get the final matcherOut by condensing this intermediate into postFinder contexts temp = []; i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) ) { // Restore matcherIn since elem is not yet a final match temp.push( (matcherIn[i] = elem) ); } } postFinder( null, (matcherOut = []), temp, xml ); } // Move matched elements from seed to results to keep them synchronized i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) && (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { seed[temp] = !(results[temp] = elem); } } } // Add elements to results, through postFinder if defined } else { matcherOut = condense( matcherOut === results ? matcherOut.splice( preexisting, matcherOut.length ) : matcherOut ); if ( postFinder ) { postFinder( null, results, matcherOut, xml ); } else { push.apply( results, matcherOut ); } } }); } function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, leadingRelative = Expr.relative[ tokens[0].type ], implicitRelative = leadingRelative || Expr.relative[" "], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) matchContext = addCombinator( function( elem ) { return elem === checkContext; }, implicitRelative, true ), matchAnyContext = addCombinator( function( elem ) { return indexOf.call( checkContext, elem ) > -1; }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( (checkContext = context).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); } ]; for ( ; i < len; i++ ) { if ( (matcher = Expr.relative[ tokens[i].type ]) ) { matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; } else { matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); // Return special upon seeing a positional matcher if ( matcher[ expando ] ) { // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { if ( Expr.relative[ tokens[j].type ] ) { break; } } return setMatcher( i > 1 && elementMatcher( matchers ), i > 1 && toSelector( // If the preceding token was a descendant combinator, insert an implicit any-element `*` tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) ).replace( rtrim, "$1" ), matcher, i < j && matcherFromTokens( tokens.slice( i, j ) ), j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), j < len && toSelector( tokens ) ); } matchers.push( matcher ); } } return elementMatcher( matchers ); } function matcherFromGroupMatchers( elementMatchers, setMatchers ) { var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, superMatcher = function( seed, context, xml, results, outermost ) { var elem, j, matcher, matchedCount = 0, i = "0", unmatched = seed && [], setMatched = [], contextBackup = outermostContext, // We must always have either seed elements or outermost context elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), // Use integer dirruns iff this is the outermost matcher dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), len = elems.length; if ( outermost ) { outermostContext = context !== document && context; } // Add elements passing elementMatchers directly to results // Keep `i` a string if there are no elements so `matchedCount` will be "00" below // Support: IE<9, Safari // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id for ( ; i !== len && (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; while ( (matcher = elementMatchers[j++]) ) { if ( matcher( elem, context, xml ) ) { results.push( elem ); break; } } if ( outermost ) { dirruns = dirrunsUnique; } } // Track unmatched elements for set filters if ( bySet ) { // They will have gone through all possible matchers if ( (elem = !matcher && elem) ) { matchedCount--; } // Lengthen the array for every element, matched or not if ( seed ) { unmatched.push( elem ); } } } // Apply set filters to unmatched elements matchedCount += i; if ( bySet && i !== matchedCount ) { j = 0; while ( (matcher = setMatchers[j++]) ) { matcher( unmatched, setMatched, context, xml ); } if ( seed ) { // Reintegrate element matches to eliminate the need for sorting if ( matchedCount > 0 ) { while ( i-- ) { if ( !(unmatched[i] || setMatched[i]) ) { setMatched[i] = pop.call( results ); } } } // Discard index placeholder values to get only actual matches setMatched = condense( setMatched ); } // Add matches to results push.apply( results, setMatched ); // Seedless set matches succeeding multiple successful matchers stipulate sorting if ( outermost && !seed && setMatched.length > 0 && ( matchedCount + setMatchers.length ) > 1 ) { Sizzle.uniqueSort( results ); } } // Override manipulation of globals by nested matchers if ( outermost ) { dirruns = dirrunsUnique; outermostContext = contextBackup; } return unmatched; }; return bySet ? markFunction( superMatcher ) : superMatcher; } compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], cached = compilerCache[ selector + " " ]; if ( !cached ) { // Generate a function of recursive functions that can be used to check each element if ( !group ) { group = tokenize( selector ); } i = group.length; while ( i-- ) { cached = matcherFromTokens( group[i] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { elementMatchers.push( cached ); } } // Cache the compiled function cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); } return cached; }; function multipleContexts( selector, contexts, results ) { var i = 0, len = contexts.length; for ( ; i < len; i++ ) { Sizzle( selector, contexts[i], results ); } return results; } function select( selector, context, results, seed ) { var i, tokens, token, type, find, match = tokenize( selector ); if ( !seed ) { // Try to minimize operations if there is only one group if ( match.length === 1 ) { // Take a shortcut and set the context if the root selector is an ID tokens = match[0] = match[0].slice( 0 ); if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && support.getById && context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) { context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; if ( !context ) { return results; } selector = selector.slice( tokens.shift().value.length ); } // Fetch a seed set for right-to-left matching i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; while ( i-- ) { token = tokens[i]; // Abort if we hit a combinator if ( Expr.relative[ (type = token.type) ] ) { break; } if ( (find = Expr.find[ type ]) ) { // Search, expanding context for leading sibling combinators if ( (seed = find( token.matches[0].replace( runescape, funescape ), rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context )) ) { // If seed is empty or no tokens remain, we can return early tokens.splice( i, 1 ); selector = seed.length && toSelector( tokens ); if ( !selector ) { push.apply( results, seed ); return results; } break; } } } } } // Compile and execute a filtering function // Provide `match` to avoid retokenization if we modified the selector above compile( selector, match )( seed, context, !documentIsHTML, results, rsibling.test( selector ) && testContext( context.parentNode ) || context ); return results; } // One-time assignments // Sort stability support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; // Support: Chrome<14 // Always assume duplicates if they aren't passed to the comparison function support.detectDuplicates = !!hasDuplicate; // Initialize against the default document setDocument(); // Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) // Detached nodes confoundingly follow *each other* support.sortDetached = assert(function( div1 ) { // Should return 1, but returns 4 (following) return div1.compareDocumentPosition( document.createElement("div") ) & 1; }); // Support: IE<8 // Prevent attribute/property "interpolation" // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx if ( !assert(function( div ) { div.innerHTML = "<a href='#'></a>"; return div.firstChild.getAttribute("href") === "#" ; }) ) { addHandle( "type|href|height|width", function( elem, name, isXML ) { if ( !isXML ) { return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); } }); } // Support: IE<9 // Use defaultValue in place of getAttribute("value") if ( !support.attributes || !assert(function( div ) { div.innerHTML = "<input/>"; div.firstChild.setAttribute( "value", "" ); return div.firstChild.getAttribute( "value" ) === ""; }) ) { addHandle( "value", function( elem, name, isXML ) { if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { return elem.defaultValue; } }); } // Support: IE<9 // Use getAttributeNode to fetch booleans when getAttribute lies if ( !assert(function( div ) { return div.getAttribute("disabled") == null; }) ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { return elem[ name ] === true ? name.toLowerCase() : (val = elem.getAttributeNode( name )) && val.specified ? val.value : null; } }); } return Sizzle; })( window ); jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.pseudos; jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; var rneedsContext = jQuery.expr.match.needsContext; var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); var risSimple = /^.[^:#\[\.,]*$/; // Implement the identical functionality for filter and not function winnow( elements, qualifier, not ) { if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep( elements, function( elem, i ) { /* jshint -W018 */ return !!qualifier.call( elem, i, elem ) !== not; }); } if ( qualifier.nodeType ) { return jQuery.grep( elements, function( elem ) { return ( elem === qualifier ) !== not; }); } if ( typeof qualifier === "string" ) { if ( risSimple.test( qualifier ) ) { return jQuery.filter( qualifier, elements, not ); } qualifier = jQuery.filter( qualifier, elements ); } return jQuery.grep( elements, function( elem ) { return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; }); } jQuery.filter = function( expr, elems, not ) { var elem = elems[ 0 ]; if ( not ) { expr = ":not(" + expr + ")"; } return elems.length === 1 && elem.nodeType === 1 ? jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { return elem.nodeType === 1; })); }; jQuery.fn.extend({ find: function( selector ) { var i, ret = [], self = this, len = self.length; if ( typeof selector !== "string" ) { return this.pushStack( jQuery( selector ).filter(function() { for ( i = 0; i < len; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { return true; } } }) ); } for ( i = 0; i < len; i++ ) { jQuery.find( selector, self[ i ], ret ); } // Needed because $( selector, context ) becomes $( context ).find( selector ) ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); ret.selector = this.selector ? this.selector + " " + selector : selector; return ret; }, filter: function( selector ) { return this.pushStack( winnow(this, selector || [], false) ); }, not: function( selector ) { return this.pushStack( winnow(this, selector || [], true) ); }, is: function( selector ) { return !!winnow( this, // If this is a positional/relative selector, check membership in the returned set // so $("p:first").is("p:last") won't return true for a doc with two "p". typeof selector === "string" && rneedsContext.test( selector ) ? jQuery( selector ) : selector || [], false ).length; } }); // Initialize a jQuery object // A central reference to the root jQuery(document) var rootjQuery, // Use the correct document accordingly with window argument (sandbox) document = window.document, // A simple way to check for HTML strings // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) // Strict HTML recognition (#11290: must start with <) rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, init = jQuery.fn.init = function( selector, context ) { var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false) if ( !selector ) { return this; } // Handle HTML strings if ( typeof selector === "string" ) { if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = rquickExpr.exec( selector ); } // Match html or make sure no context is specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; // scripts is true for back-compat // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( match[1], context && context.nodeType ? context.ownerDocument || context : document, true ) ); // HANDLE: $(html, props) if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { for ( match in context ) { // Properties of context are called as methods if possible if ( jQuery.isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes } else { this.attr( match, context[ match ] ); } } } return this; // HANDLE: $(#id) } else { elem = document.getElementById( match[2] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id !== match[2] ) { return rootjQuery.find( selector ); } // Otherwise, we inject the element directly into the jQuery object this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(DOMElement) } else if ( selector.nodeType ) { this.context = this[0] = selector; this.length = 1; return this; // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return typeof rootjQuery.ready !== "undefined" ? rootjQuery.ready( selector ) : // Execute immediately if ready is not present selector( jQuery ); } if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } return jQuery.makeArray( selector, this ); }; // Give the init function the jQuery prototype for later instantiation init.prototype = jQuery.fn; // Initialize central reference rootjQuery = jQuery( document ); var rparentsprev = /^(?:parents|prev(?:Until|All))/, // methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { children: true, contents: true, next: true, prev: true }; jQuery.extend({ dir: function( elem, dir, until ) { var matched = [], cur = elem[ dir ]; while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); } cur = cur[dir]; } return matched; }, sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { r.push( n ); } } return r; } }); jQuery.fn.extend({ has: function( target ) { var i, targets = jQuery( target, this ), len = targets.length; return this.filter(function() { for ( i = 0; i < len; i++ ) { if ( jQuery.contains( this, targets[i] ) ) { return true; } } }); }, closest: function( selectors, context ) { var cur, i = 0, l = this.length, matched = [], pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? jQuery( selectors, context || this.context ) : 0; for ( ; i < l; i++ ) { for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { // Always skip document fragments if ( cur.nodeType < 11 && (pos ? pos.index(cur) > -1 : // Don't pass non-elements to Sizzle cur.nodeType === 1 && jQuery.find.matchesSelector(cur, selectors)) ) { matched.push( cur ); break; } } } return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); }, // Determine the position of an element within // the matched set of elements index: function( elem ) { // No argument, return index in parent if ( !elem ) { return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; } // index in selector if ( typeof elem === "string" ) { return jQuery.inArray( this[0], jQuery( elem ) ); } // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used elem.jquery ? elem[0] : elem, this ); }, add: function( selector, context ) { return this.pushStack( jQuery.unique( jQuery.merge( this.get(), jQuery( selector, context ) ) ) ); }, addBack: function( selector ) { return this.add( selector == null ? this.prevObject : this.prevObject.filter(selector) ); } }); function sibling( cur, dir ) { do { cur = cur[ dir ]; } while ( cur && cur.nodeType !== 1 ); return cur; } jQuery.each({ parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { return jQuery.dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { return jQuery.dir( elem, "parentNode", until ); }, next: function( elem ) { return sibling( elem, "nextSibling" ); }, prev: function( elem ) { return sibling( elem, "previousSibling" ); }, nextAll: function( elem ) { return jQuery.dir( elem, "nextSibling" ); }, prevAll: function( elem ) { return jQuery.dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { return jQuery.dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { return jQuery.dir( elem, "previousSibling", until ); }, siblings: function( elem ) { return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); }, children: function( elem ) { return jQuery.sibling( elem.firstChild ); }, contents: function( elem ) { return jQuery.nodeName( elem, "iframe" ) ? elem.contentDocument || elem.contentWindow.document : jQuery.merge( [], elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); if ( name.slice( -5 ) !== "Until" ) { selector = until; } if ( selector && typeof selector === "string" ) { ret = jQuery.filter( selector, ret ); } if ( this.length > 1 ) { // Remove duplicates if ( !guaranteedUnique[ name ] ) { ret = jQuery.unique( ret ); } // Reverse order for parents* and prev-derivatives if ( rparentsprev.test( name ) ) { ret = ret.reverse(); } } return this.pushStack( ret ); }; }); var rnotwhite = (/\S+/g); // String to Object options format cache var optionsCache = {}; // Convert String-formatted options into Object-formatted ones and store in cache function createOptions( options ) { var object = optionsCache[ options ] = {}; jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; }); return object; } /* * Create a callback list using the following parameters: * * options: an optional list of space-separated options that will change how * the callback list behaves or a more traditional option object * * By default a callback list will act like an event callback list and can be * "fired" multiple times. * * Possible options: * * once: will ensure the callback list can only be fired once (like a Deferred) * * memory: will keep track of previous values and will call any callback added * after the list has been fired right away with the latest "memorized" * values (like a Deferred) * * unique: will ensure a callback can only be added once (no duplicate in the list) * * stopOnFalse: interrupt callings when a callback returns false * */ jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ? ( optionsCache[ options ] || createOptions( options ) ) : jQuery.extend( {}, options ); var // Flag to know if list is currently firing firing, // Last fire value (for non-forgettable lists) memory, // Flag to know if list was already fired fired, // End of the loop when firing firingLength, // Index of currently firing callback (modified by remove if needed) firingIndex, // First callback to fire (used internally by add and fireWith) firingStart, // Actual callback list list = [], // Stack of fire calls for repeatable lists stack = !options.once && [], // Fire callbacks fire = function( data ) { memory = options.memory && data; fired = true; firingIndex = firingStart || 0; firingStart = 0; firingLength = list.length; firing = true; for ( ; list && firingIndex < firingLength; firingIndex++ ) { if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { memory = false; // To prevent further calls using add break; } } firing = false; if ( list ) { if ( stack ) { if ( stack.length ) { fire( stack.shift() ); } } else if ( memory ) { list = []; } else { self.disable(); } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { // First, we save the current length var start = list.length; (function add( args ) { jQuery.each( args, function( _, arg ) { var type = jQuery.type( arg ); if ( type === "function" ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } } else if ( arg && arg.length && type !== "string" ) { // Inspect recursively add( arg ); } }); })( arguments ); // Do we need to add the callbacks to the // current firing batch? if ( firing ) { firingLength = list.length; // With memory, if we're not firing then // we should call right away } else if ( memory ) { firingStart = start; fire( memory ); } } return this; }, // Remove a callback from the list remove: function() { if ( list ) { jQuery.each( arguments, function( _, arg ) { var index; while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( firing ) { if ( index <= firingLength ) { firingLength--; } if ( index <= firingIndex ) { firingIndex--; } } } }); } return this; }, // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. has: function( fn ) { return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); }, // Remove all callbacks from the list empty: function() { list = []; firingLength = 0; return this; }, // Have the list do nothing anymore disable: function() { list = stack = memory = undefined; return this; }, // Is it disabled? disabled: function() { return !list; }, // Lock the list in its current state lock: function() { stack = undefined; if ( !memory ) { self.disable(); } return this; }, // Is it locked? locked: function() { return !stack; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { if ( list && ( !fired || stack ) ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; if ( firing ) { stack.push( args ); } else { fire( args ); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!fired; } }; return self; }; jQuery.extend({ Deferred: function( func ) { var tuples = [ // action, add listener, listener list, final state [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], [ "notify", "progress", jQuery.Callbacks("memory") ] ], state = "pending", promise = { state: function() { return state; }, always: function() { deferred.done( arguments ).fail( arguments ); return this; }, then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[1] ](function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() .done( newDefer.resolve ) .fail( newDefer.reject ) .progress( newDefer.notify ); } else { newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } }); }); fns = null; }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { return obj != null ? jQuery.extend( obj, promise ) : promise; } }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ], stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add promise[ tuple[1] ] = list.add; // Handle state if ( stateString ) { list.add(function() { // state = [ resolved | rejected ] state = stateString; // [ reject_list | resolve_list ].disable; progress_list.lock }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); } // deferred[ resolve | reject | notify ] deferred[ tuple[0] ] = function() { deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); return this; }; deferred[ tuple[0] + "With" ] = list.fireWith; }); // Make the deferred a promise promise.promise( deferred ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; }, // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, // the master Deferred. If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !(--remaining) ) { deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() .done( updateFunc( i, resolveContexts, resolveValues ) ) .fail( deferred.reject ) .progress( updateFunc( i, progressContexts, progressValues ) ); } else { --remaining; } } } // if we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); } }); // The deferred used on DOM ready var readyList; jQuery.fn.ready = function( fn ) { // Add the callback jQuery.ready.promise().done( fn ); return this; }; jQuery.extend({ // Is the DOM ready to be used? Set to true once it occurs. isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 readyWait: 1, // Hold (or release) the ready event holdReady: function( hold ) { if ( hold ) { jQuery.readyWait++; } else { jQuery.ready( true ); } }, // Handle when the DOM is ready ready: function( wait ) { // Abort if there are pending holds or we're already ready if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { return; } // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( !document.body ) { return setTimeout( jQuery.ready ); } // Remember that the DOM is ready jQuery.isReady = true; // If a normal DOM Ready event fired, decrement, and wait if need be if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // If there are functions bound, to execute readyList.resolveWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.trigger ) { jQuery( document ).trigger("ready").off("ready"); } } }); /** * Clean-up method for dom ready events */ function detach() { if ( document.addEventListener ) { document.removeEventListener( "DOMContentLoaded", completed, false ); window.removeEventListener( "load", completed, false ); } else { document.detachEvent( "onreadystatechange", completed ); window.detachEvent( "onload", completed ); } } /** * The ready event handler and self cleanup method */ function completed() { // readyState === "complete" is good enough for us to call the dom ready in oldIE if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { detach(); jQuery.ready(); } } jQuery.ready.promise = function( obj ) { if ( !readyList ) { readyList = jQuery.Deferred(); // Catch cases where $(document).ready() is called after the browser event has already occurred. // we once tried to use readyState "interactive" here, but it caused issues like the one // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 if ( document.readyState === "complete" ) { // Handle it asynchronously to allow scripts the opportunity to delay ready setTimeout( jQuery.ready ); // Standards-based browsers support DOMContentLoaded } else if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", completed, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", completed, false ); // If IE event model is used } else { // Ensure firing before onload, maybe late but safe also for iframes document.attachEvent( "onreadystatechange", completed ); // A fallback to window.onload, that will always work window.attachEvent( "onload", completed ); // If IE and not a frame // continually check to see if the document is ready var top = false; try { top = window.frameElement == null && document.documentElement; } catch(e) {} if ( top && top.doScroll ) { (function doScrollCheck() { if ( !jQuery.isReady ) { try { // Use the trick by Diego Perini // http://javascript.nwbox.com/IEContentLoaded/ top.doScroll("left"); } catch(e) { return setTimeout( doScrollCheck, 50 ); } // detach all dom ready events detach(); // and execute any waiting functions jQuery.ready(); } })(); } } } return readyList.promise( obj ); }; var strundefined = typeof undefined; // Support: IE<9 // Iteration over object's inherited properties before its own var i; for ( i in jQuery( support ) ) { break; } support.ownLast = i !== "0"; // Note: most support tests are defined in their respective modules. // false until the test is run support.inlineBlockNeedsLayout = false; jQuery(function() { // We need to execute this one support test ASAP because we need to know // if body.style.zoom needs to be set. var container, div, body = document.getElementsByTagName("body")[0]; if ( !body ) { // Return for frameset docs that don't have a body return; } // Setup container = document.createElement( "div" ); container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; div = document.createElement( "div" ); body.appendChild( container ).appendChild( div ); if ( typeof div.style.zoom !== strundefined ) { // Support: IE<8 // Check if natively block-level elements act like inline-block // elements when setting their display to 'inline' and giving // them layout div.style.cssText = "border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1"; if ( (support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 )) ) { // Prevent IE 6 from affecting layout for positioned elements #11048 // Prevent IE from shrinking the body in IE 7 mode #12869 // Support: IE<8 body.style.zoom = 1; } } body.removeChild( container ); // Null elements to avoid leaks in IE container = div = null; }); (function() { var div = document.createElement( "div" ); // Execute the test only if not already executed in another module. if (support.deleteExpando == null) { // Support: IE<9 support.deleteExpando = true; try { delete div.test; } catch( e ) { support.deleteExpando = false; } } // Null elements to avoid leaks in IE. div = null; })(); /** * Determines whether an object can have data */ jQuery.acceptData = function( elem ) { var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], nodeType = +elem.nodeType || 1; // Do not set data on non-element DOM nodes because it will not be cleared (#8335). return nodeType !== 1 && nodeType !== 9 ? false : // Nodes accept data unless otherwise specified; rejection can be conditional !noData || noData !== true && elem.getAttribute("classid") === noData; }; var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, rmultiDash = /([A-Z])/g; function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : // Only convert to a number if it doesn't change the string +data + "" === data ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} // Make sure we set the data so it isn't changed later jQuery.data( elem, key, data ); } else { data = undefined; } } return data; } // checks a cache object for emptiness function isEmptyDataObject( obj ) { var name; for ( name in obj ) { // if the public data object is empty, the private is still empty if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { continue; } if ( name !== "toJSON" ) { return false; } } return true; } function internalData( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var ret, thisCache, internalKey = jQuery.expando, // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary isNode = elem.nodeType, // Only DOM nodes need the global jQuery cache; JS object data is // attached directly to the object so GC can occur automatically cache = isNode ? jQuery.cache : elem, // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { return; } if ( !id ) { // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; } else { id = internalKey; } } if ( !cache[ id ] ) { // Avoid exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; } // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache if ( typeof name === "object" || typeof name === "function" ) { if ( pvt ) { cache[ id ] = jQuery.extend( cache[ id ], name ); } else { cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } thisCache = cache[ id ]; // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined // data. if ( !pvt ) { if ( !thisCache.data ) { thisCache.data = {}; } thisCache = thisCache.data; } if ( data !== undefined ) { thisCache[ jQuery.camelCase( name ) ] = data; } // Check for both converted-to-camel and non-converted data property names // If a data property was specified if ( typeof name === "string" ) { // First Try to find as-is property data ret = thisCache[ name ]; // Test for null|undefined property data if ( ret == null ) { // Try to find the camelCased property ret = thisCache[ jQuery.camelCase( name ) ]; } } else { ret = thisCache; } return ret; } function internalRemoveData( elem, name, pvt ) { if ( !jQuery.acceptData( elem ) ) { return; } var thisCache, i, isNode = elem.nodeType, // See jQuery.data for more information cache = isNode ? jQuery.cache : elem, id = isNode ? elem[ jQuery.expando ] : jQuery.expando; // If there is already no cache entry for this object, there is no // purpose in continuing if ( !cache[ id ] ) { return; } if ( name ) { thisCache = pvt ? cache[ id ] : cache[ id ].data; if ( thisCache ) { // Support array or space separated string names for data keys if ( !jQuery.isArray( name ) ) { // try the string as a key before any manipulation if ( name in thisCache ) { name = [ name ]; } else { // split the camel cased version by spaces unless a key with the spaces exists name = jQuery.camelCase( name ); if ( name in thisCache ) { name = [ name ]; } else { name = name.split(" "); } } } else { // If "name" is an array of keys... // When data is initially created, via ("key", "val") signature, // keys will be converted to camelCase. // Since there is no way to tell _how_ a key was added, remove // both plain key and camelCase key. #12786 // This will only penalize the array argument path. name = name.concat( jQuery.map( name, jQuery.camelCase ) ); } i = name.length; while ( i-- ) { delete thisCache[ name[i] ]; } // If there is no data left in the cache, we want to continue // and let the cache object itself get destroyed if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { return; } } } // See jQuery.data for more information if ( !pvt ) { delete cache[ id ].data; // Don't destroy the parent cache unless the internal data object // had been the only thing left in it if ( !isEmptyDataObject( cache[ id ] ) ) { return; } } // Destroy the cache if ( isNode ) { jQuery.cleanData( [ elem ], true ); // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) /* jshint eqeqeq: false */ } else if ( support.deleteExpando || cache != cache.window ) { /* jshint eqeqeq: true */ delete cache[ id ]; // When all else fails, null } else { cache[ id ] = null; } } jQuery.extend({ cache: {}, // The following elements (space-suffixed to avoid Object.prototype collisions) // throw uncatchable exceptions if you attempt to set expando properties noData: { "applet ": true, "embed ": true, // ...but Flash objects (which have this classid) *can* handle expandos "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" }, hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; return !!elem && !isEmptyDataObject( elem ); }, data: function( elem, name, data ) { return internalData( elem, name, data ); }, removeData: function( elem, name ) { return internalRemoveData( elem, name ); }, // For internal use only. _data: function( elem, name, data ) { return internalData( elem, name, data, true ); }, _removeData: function( elem, name ) { return internalRemoveData( elem, name, true ); } }); jQuery.fn.extend({ data: function( key, value ) { var i, name, data, elem = this[0], attrs = elem && elem.attributes; // Special expections of .data basically thwart jQuery.access, // so implement the relevant behavior ourselves // Gets all values if ( key === undefined ) { if ( this.length ) { data = jQuery.data( elem ); if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { i = attrs.length; while ( i-- ) { name = attrs[i].name; if ( name.indexOf("data-") === 0 ) { name = jQuery.camelCase( name.slice(5) ); dataAttr( elem, name, data[ name ] ); } } jQuery._data( elem, "parsedAttrs", true ); } } return data; } // Sets multiple values if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); } return arguments.length > 1 ? // Sets one value this.each(function() { jQuery.data( this, key, value ); }) : // Gets one value // Try to fetch any internally stored data first elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; }, removeData: function( key ) { return this.each(function() { jQuery.removeData( this, key ); }); } }); jQuery.extend({ queue: function( elem, type, data ) { var queue; if ( elem ) { type = ( type || "fx" ) + "queue"; queue = jQuery._data( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !queue || jQuery.isArray(data) ) { queue = jQuery._data( elem, type, jQuery.makeArray(data) ); } else { queue.push( data ); } } return queue || []; } }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), startLength = queue.length, fn = queue.shift(), hooks = jQuery._queueHooks( elem, type ), next = function() { jQuery.dequeue( elem, type ); }; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); startLength--; } if ( fn ) { // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { queue.unshift( "inprogress" ); } // clear up the last queue stop function delete hooks.stop; fn.call( elem, next, hooks ); } if ( !startLength && hooks ) { hooks.empty.fire(); } }, // not intended for public consumption - generates a queueHooks object, or returns the current one _queueHooks: function( elem, type ) { var key = type + "queueHooks"; return jQuery._data( elem, key ) || jQuery._data( elem, key, { empty: jQuery.Callbacks("once memory").add(function() { jQuery._removeData( elem, type + "queue" ); jQuery._removeData( elem, key ); }) }); } }); jQuery.fn.extend({ queue: function( type, data ) { var setter = 2; if ( typeof type !== "string" ) { data = type; type = "fx"; setter--; } if ( arguments.length < setter ) { return jQuery.queue( this[0], type ); } return data === undefined ? this : this.each(function() { var queue = jQuery.queue( this, type, data ); // ensure a hooks for this queue jQuery._queueHooks( this, type ); if ( type === "fx" && queue[0] !== "inprogress" ) { jQuery.dequeue( this, type ); } }); }, dequeue: function( type ) { return this.each(function() { jQuery.dequeue( this, type ); }); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, obj ) { var tmp, count = 1, defer = jQuery.Deferred(), elements = this, i = this.length, resolve = function() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } }; if ( typeof type !== "string" ) { obj = type; type = undefined; } type = type || "fx"; while ( i-- ) { tmp = jQuery._data( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); } } resolve(); return defer.promise( obj ); } }); var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; var isHidden = function( elem, el ) { // isHidden might be called from jQuery#filter function; // in that case, element will be second argument elem = el || elem; return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); }; // Multifunctional method to get and set values of a collection // The value/s can optionally be executed if it's a function var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { var i = 0, length = elems.length, bulk = key == null; // Sets many values if ( jQuery.type( key ) === "object" ) { chainable = true; for ( i in key ) { jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); } // Sets one value } else if ( value !== undefined ) { chainable = true; if ( !jQuery.isFunction( value ) ) { raw = true; } if ( bulk ) { // Bulk operations run against the entire set if ( raw ) { fn.call( elems, value ); fn = null; // ...except when executing function values } else { bulk = fn; fn = function( elem, key, value ) { return bulk.call( jQuery( elem ), value ); }; } } if ( fn ) { for ( ; i < length; i++ ) { fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); } } } return chainable ? elems : // Gets bulk ? fn.call( elems ) : length ? fn( elems[0], key ) : emptyGet; }; var rcheckableType = (/^(?:checkbox|radio)$/i); (function() { var fragment = document.createDocumentFragment(), div = document.createElement("div"), input = document.createElement("input"); // Setup div.setAttribute( "className", "t" ); div.innerHTML = " <link/><table></table><a href='/a'>a</a>"; // IE strips leading whitespace when .innerHTML is used support.leadingWhitespace = div.firstChild.nodeType === 3; // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables support.tbody = !div.getElementsByTagName( "tbody" ).length; // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE support.htmlSerialize = !!div.getElementsByTagName( "link" ).length; // Makes sure cloning an html5 element does not cause problems // Where outerHTML is undefined, this still works support.html5Clone = document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav></:nav>"; // Check if a disconnected checkbox will retain its checked // value of true after appended to the DOM (IE6/7) input.type = "checkbox"; input.checked = true; fragment.appendChild( input ); support.appendChecked = input.checked; // Make sure textarea (and checkbox) defaultValue is properly cloned // Support: IE6-IE11+ div.innerHTML = "<textarea>x</textarea>"; support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; // #11217 - WebKit loses check when the name is after the checked attribute fragment.appendChild( div ); div.innerHTML = "<input type='radio' checked='checked' name='t'/>"; // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 // old WebKit doesn't clone checked state correctly in fragments support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; // Support: IE<9 // Opera does not clone events (and typeof div.attachEvent === undefined). // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() support.noCloneEvent = true; if ( div.attachEvent ) { div.attachEvent( "onclick", function() { support.noCloneEvent = false; }); div.cloneNode( true ).click(); } // Execute the test only if not already executed in another module. if (support.deleteExpando == null) { // Support: IE<9 support.deleteExpando = true; try { delete div.test; } catch( e ) { support.deleteExpando = false; } } // Null elements to avoid leaks in IE. fragment = div = input = null; })(); (function() { var i, eventName, div = document.createElement( "div" ); // Support: IE<9 (lack submit/change bubble), Firefox 23+ (lack focusin event) for ( i in { submit: true, change: true, focusin: true }) { eventName = "on" + i; if ( !(support[ i + "Bubbles" ] = eventName in window) ) { // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) div.setAttribute( eventName, "t" ); support[ i + "Bubbles" ] = div.attributes[ eventName ].expando === false; } } // Null elements to avoid leaks in IE. div = null; })(); var rformElems = /^(?:input|select|textarea)$/i, rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|contextmenu)|click/, rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; function returnTrue() { return true; } function returnFalse() { return false; } function safeActiveElement() { try { return document.activeElement; } catch ( err ) { } } /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { global: {}, add: function( elem, types, handler, data, selector ) { var tmp, events, t, handleObjIn, special, eventHandle, handleObj, handlers, type, namespaces, origType, elemData = jQuery._data( elem ); // Don't attach events to noData or text/comment nodes (but allow plain objects) if ( !elemData ) { return; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure and main handler, if this is the first if ( !(events = elemData.events) ) { events = elemData.events = {}; } if ( !(eventHandle = elemData.handle) ) { eventHandle = elemData.handle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events eventHandle.elem = elem; } // Handle multiple events separated by a space types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[t] ) || []; type = origType = tmp[1]; namespaces = ( tmp[2] || "" ).split( "." ).sort(); // There *must* be a type, no attaching namespace-only handlers if ( !type ) { continue; } // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend({ type: type, origType: origType, data: data, handler: handler, guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), namespace: namespaces.join(".") }, handleObjIn ); // Init the event handler queue if we're the first if ( !(handlers = events[ type ]) ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE elem = null; }, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { var j, handleObj, tmp, origCount, t, events, special, handlers, type, namespaces, origType, elemData = jQuery.hasData( elem ) && jQuery._data( elem ); if ( !elemData || !(events = elemData.events) ) { return; } // Once for each type.namespace in types; type may be omitted types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[t] ) || []; type = origType = tmp[1]; namespaces = ( tmp[2] || "" ).split( "." ).sort(); // Unbind all events (on this namespace, if provided) for the element if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector ? special.delegateType : special.bindType ) || type; handlers = events[ type ] || []; tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); // Remove matching events origCount = j = handlers.length; while ( j-- ) { handleObj = handlers[ j ]; if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !tmp || tmp.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { handlers.splice( j, 1 ); if ( handleObj.selector ) { handlers.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( origCount && !handlers.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { delete elemData.handle; // removeData also checks for emptiness and clears the expando if empty // so use it instead of delete jQuery._removeData( elem, "events" ); } }, trigger: function( event, data, elem, onlyHandlers ) { var handle, ontype, cur, bubbleType, special, tmp, i, eventPath = [ elem || document ], type = hasOwn.call( event, "type" ) ? event.type : event, namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; cur = tmp = elem = elem || document; // Don't do events on text and comment nodes if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } // focus/blur morphs to focusin/out; ensure we're not firing them right now if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } if ( type.indexOf(".") >= 0 ) { // Namespaced trigger; create a regexp to match event type in handle() namespaces = type.split("."); type = namespaces.shift(); namespaces.sort(); } ontype = type.indexOf(":") < 0 && "on" + type; // Caller can pass in a jQuery.Event object, Object, or just an event type string event = event[ jQuery.expando ] ? event : new jQuery.Event( type, typeof event === "object" && event ); // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) event.isTrigger = onlyHandlers ? 2 : 3; event.namespace = namespaces.join("."); event.namespace_re = event.namespace ? new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : null; // Clean up the event in case it is being reused event.result = undefined; if ( !event.target ) { event.target = elem; } // Clone any incoming data and prepend the event, creating the handler arg list data = data == null ? [ event ] : jQuery.makeArray( data, [ event ] ); // Allow special events to draw outside the lines special = jQuery.event.special[ type ] || {}; if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { bubbleType = special.delegateType || type; if ( !rfocusMorph.test( bubbleType + type ) ) { cur = cur.parentNode; } for ( ; cur; cur = cur.parentNode ) { eventPath.push( cur ); tmp = cur; } // Only add window if we got to document (e.g., not plain obj or detached DOM) if ( tmp === (elem.ownerDocument || document) ) { eventPath.push( tmp.defaultView || tmp.parentWindow || window ); } } // Fire handlers on the event path i = 0; while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { event.type = i > 1 ? bubbleType : special.bindType || type; // jQuery handler handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // Native handler handle = ontype && cur[ ontype ]; if ( handle && handle.apply && jQuery.acceptData( cur ) ) { event.result = handle.apply( cur, data ); if ( event.result === false ) { event.preventDefault(); } } } event.type = type; // If nobody prevented the default action, do it now if ( !onlyHandlers && !event.isDefaultPrevented() ) { if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && jQuery.acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. // Can't use an .isFunction() check here because IE6/7 fails that test. // Don't do default actions on window, that's where global variables be (#6170) if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method tmp = elem[ ontype ]; if ( tmp ) { elem[ ontype ] = null; } // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; try { elem[ type ](); } catch ( e ) { // IE<9 dies on focus/blur to hidden element (#1486,#12518) // only reproducible on winXP IE8 native, not IE9 in IE8 mode } jQuery.event.triggered = undefined; if ( tmp ) { elem[ ontype ] = tmp; } } } } return event.result; }, dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event ); var i, ret, handleObj, matched, j, handlerQueue = [], args = slice.call( arguments ), handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; event.delegateTarget = this; // Call the preDispatch hook for the mapped type, and let it bail if desired if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // Determine handlers handlerQueue = jQuery.event.handlers.call( this, event, handlers ); // Run delegates first; they may want to stop propagation beneath us i = 0; while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { // Triggered event must either 1) have no namespace, or // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { if ( (event.result = ret) === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // Call the postDispatch hook for the mapped type if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; }, handlers: function( event, handlers ) { var sel, handleObj, matches, i, handlerQueue = [], delegateCount = handlers.delegateCount, cur = event.target; // Find delegate handlers // Black-hole SVG <use> instance trees (#13180) // Avoid non-left-click bubbling in Firefox (#3861) if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { /* jshint eqeqeq: false */ for ( ; cur != this; cur = cur.parentNode || this ) { /* jshint eqeqeq: true */ // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; // Don't conflict with Object.prototype properties (#13203) sel = handleObj.selector + " "; if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) >= 0 : jQuery.find( sel, this, null, [ cur ] ).length; } if ( matches[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push({ elem: cur, handlers: matches }); } } } } // Add the remaining (directly-bound) handlers if ( delegateCount < handlers.length ) { handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); } return handlerQueue; }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, copy, type = event.type, originalEvent = event, fixHook = this.fixHooks[ type ]; if ( !fixHook ) { this.fixHooks[ type ] = fixHook = rmouseEvent.test( type ) ? this.mouseHooks : rkeyEvent.test( type ) ? this.keyHooks : {}; } copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; event = new jQuery.Event( originalEvent ); i = copy.length; while ( i-- ) { prop = copy[ i ]; event[ prop ] = originalEvent[ prop ]; } // Support: IE<9 // Fix target property (#1925) if ( !event.target ) { event.target = originalEvent.srcElement || document; } // Support: Chrome 23+, Safari? // Target should not be a text node (#504, #13143) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // Support: IE<9 // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) event.metaKey = !!event.metaKey; return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; }, // Includes some event props shared by KeyEvent and MouseEvent props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split(" "), filter: function( event, original ) { // Add which for key events if ( event.which == null ) { event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, mouseHooks: { props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function( event, original ) { var body, eventDoc, doc, button = original.button, fromElement = original.fromElement; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add relatedTarget, if necessary if ( !event.relatedTarget && fromElement ) { event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, special: { load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { // Fire native event if possible so blur/focus sequence is correct trigger: function() { if ( this !== safeActiveElement() && this.focus ) { try { this.focus(); return false; } catch ( e ) { // Support: IE<9 // If we error on focus to hidden element (#1486, #12518), // let .trigger() run the handlers } } }, delegateType: "focusin" }, blur: { trigger: function() { if ( this === safeActiveElement() && this.blur ) { this.blur(); return false; } }, delegateType: "focusout" }, click: { // For checkbox, fire native event so checked state will be right trigger: function() { if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { this.click(); return false; } }, // For cross-browser consistency, don't fire native .click() on links _default: function( event ) { return jQuery.nodeName( event.target, "a" ); } }, beforeunload: { postDispatch: function( event ) { // Even when returnValue equals to undefined Firefox will still show alert if ( event.result !== undefined ) { event.originalEvent.returnValue = event.result; } } } }, simulate: function( type, elem, event, bubble ) { // Piggyback on a donor event to simulate a different one. // Fake originalEvent to avoid donor's stopPropagation, but if the // simulated event prevents default then we do the same on the donor. var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true, originalEvent: {} } ); if ( bubble ) { jQuery.event.trigger( e, null, elem ); } else { jQuery.event.dispatch.call( elem, e ); } if ( e.isDefaultPrevented() ) { event.preventDefault(); } } }; jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, handle, false ); } } : function( elem, type, handle ) { var name = "on" + type; if ( elem.detachEvent ) { // #8545, #7054, preventing memory leaks for custom events in IE6-8 // detachEvent needed property on element, by name of that event, to properly expose it to GC if ( typeof elem[ name ] === strundefined ) { elem[ name ] = null; } elem.detachEvent( name, handle ); } }; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword if ( !(this instanceof jQuery.Event) ) { return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = src.defaultPrevented || src.defaultPrevented === undefined && ( // Support: IE < 9 src.returnValue === false || // Support: Android < 4.0 src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; // Event type } else { this.type = src; } // Put explicitly provided properties onto the event object if ( props ) { jQuery.extend( this, props ); } // Create a timestamp if incoming event doesn't have one this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; }; // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse, preventDefault: function() { var e = this.originalEvent; this.isDefaultPrevented = returnTrue; if ( !e ) { return; } // If preventDefault exists, run it on the original event if ( e.preventDefault ) { e.preventDefault(); // Support: IE // Otherwise set the returnValue property of the original event to false } else { e.returnValue = false; } }, stopPropagation: function() { var e = this.originalEvent; this.isPropagationStopped = returnTrue; if ( !e ) { return; } // If stopPropagation exists, run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // Support: IE // Set the cancelBubble property of the original event to true e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); } }; // Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var ret, target = this, related = event.relatedTarget, handleObj = event.handleObj; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !jQuery.contains( target, related )) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; }); // IE submit delegation if ( !support.submitBubbles ) { jQuery.event.special.submit = { setup: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Lazy-add a submit handler when a descendant form may potentially be submitted jQuery.event.add( this, "click._submit keypress._submit", function( e ) { // Node name check avoids a VML-related crash in IE (#9807) var elem = e.target, form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; if ( form && !jQuery._data( form, "submitBubbles" ) ) { jQuery.event.add( form, "submit._submit", function( event ) { event._submit_bubble = true; }); jQuery._data( form, "submitBubbles", true ); } }); // return undefined since we don't need an event listener }, postDispatch: function( event ) { // If form was submitted by the user, bubble the event up the tree if ( event._submit_bubble ) { delete event._submit_bubble; if ( this.parentNode && !event.isTrigger ) { jQuery.event.simulate( "submit", this.parentNode, event, true ); } } }, teardown: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Remove delegated handlers; cleanData eventually reaps submit handlers attached above jQuery.event.remove( this, "._submit" ); } }; } // IE change delegation and checkbox/radio fix if ( !support.changeBubbles ) { jQuery.event.special.change = { setup: function() { if ( rformElems.test( this.nodeName ) ) { // IE doesn't fire change on a check/radio until blur; trigger it on click // after a propertychange. Eat the blur-change in special.change.handle. // This still fires onchange a second time for check/radio after blur. if ( this.type === "checkbox" || this.type === "radio" ) { jQuery.event.add( this, "propertychange._change", function( event ) { if ( event.originalEvent.propertyName === "checked" ) { this._just_changed = true; } }); jQuery.event.add( this, "click._change", function( event ) { if ( this._just_changed && !event.isTrigger ) { this._just_changed = false; } // Allow triggered, simulated change events (#11500) jQuery.event.simulate( "change", this, event, true ); }); } return false; } // Delegated event; lazy-add a change handler on descendant inputs jQuery.event.add( this, "beforeactivate._change", function( e ) { var elem = e.target; if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { jQuery.event.add( elem, "change._change", function( event ) { if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { jQuery.event.simulate( "change", this.parentNode, event, true ); } }); jQuery._data( elem, "changeBubbles", true ); } }); }, handle: function( event ) { var elem = event.target; // Swallow native change events from checkbox/radio, we already triggered them above if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { return event.handleObj.handler.apply( this, arguments ); } }, teardown: function() { jQuery.event.remove( this, "._change" ); return !rformElems.test( this.nodeName ); } }; } // Create "bubbling" focus and blur events if ( !support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { // Attach a single capturing handler on the document while someone wants focusin/focusout var handler = function( event ) { jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); }; jQuery.event.special[ fix ] = { setup: function() { var doc = this.ownerDocument || this, attaches = jQuery._data( doc, fix ); if ( !attaches ) { doc.addEventListener( orig, handler, true ); } jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); }, teardown: function() { var doc = this.ownerDocument || this, attaches = jQuery._data( doc, fix ) - 1; if ( !attaches ) { doc.removeEventListener( orig, handler, true ); jQuery._removeData( doc, fix ); } else { jQuery._data( doc, fix, attaches ); } } }; }); } jQuery.fn.extend({ on: function( types, selector, data, fn, /*INTERNAL*/ one ) { var type, origFn; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // ( types-Object, data ) data = data || selector; selector = undefined; } for ( type in types ) { this.on( type, selector, data, types[ type ], one ); } return this; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return this; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return this.each( function() { jQuery.event.add( this, types, fn, data, selector ); }); }, one: function( types, selector, data, fn ) { return this.on( types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { var handleObj, type; if ( types && types.preventDefault && types.handleObj ) { // ( event ) dispatched jQuery.Event handleObj = types.handleObj; jQuery( types.delegateTarget ).off( handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { // ( types-object [, selector] ) for ( type in types ) { this.off( type, selector, types[ type ] ); } return this; } if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) fn = selector; selector = undefined; } if ( fn === false ) { fn = returnFalse; } return this.each(function() { jQuery.event.remove( this, types, fn, selector ); }); }, trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, triggerHandler: function( type, data ) { var elem = this[0]; if ( elem ) { return jQuery.event.trigger( type, data, elem, true ); } } }); function createSafeFragment( document ) { var list = nodeNames.split( "|" ), safeFrag = document.createDocumentFragment(); if ( safeFrag.createElement ) { while ( list.length ) { safeFrag.createElement( list.pop() ); } } return safeFrag; } var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, rtagName = /<([\w:]+)/, rtbody = /<tbody/i, rhtml = /<|&#?\w+;/, rnoInnerhtml = /<(?:script|style|link)/i, // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rscriptType = /^$|\/(?:java|ecma)script/i, rscriptTypeMasked = /^true\/(.*)/, rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, // We have to close these tags to support XHTML (#13200) wrapMap = { option: [ 1, "<select multiple='multiple'>", "</select>" ], legend: [ 1, "<fieldset>", "</fieldset>" ], area: [ 1, "<map>", "</map>" ], param: [ 1, "<object>", "</object>" ], thead: [ 1, "<table>", "</table>" ], tr: [ 2, "<table><tbody>", "</tbody></table>" ], col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, // unless wrapped in a div with non-breaking characters in front of it. _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ] }, safeFragment = createSafeFragment( document ), fragmentDiv = safeFragment.appendChild( document.createElement("div") ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; function getAll( context, tag ) { var elems, elem, i = 0, found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) : typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) : undefined; if ( !found ) { for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { if ( !tag || jQuery.nodeName( elem, tag ) ) { found.push( elem ); } else { jQuery.merge( found, getAll( elem, tag ) ); } } } return tag === undefined || tag && jQuery.nodeName( context, tag ) ? jQuery.merge( [ context ], found ) : found; } // Used in buildFragment, fixes the defaultChecked property function fixDefaultChecked( elem ) { if ( rcheckableType.test( elem.type ) ) { elem.defaultChecked = elem.checked; } } // Support: IE<8 // Manipulating tables requires a tbody function manipulationTarget( elem, content ) { return jQuery.nodeName( elem, "table" ) && jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? elem.getElementsByTagName("tbody")[0] || elem.appendChild( elem.ownerDocument.createElement("tbody") ) : elem; } // Replace/restore the type attribute of script elements for safe DOM manipulation function disableScript( elem ) { elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; return elem; } function restoreScript( elem ) { var match = rscriptTypeMasked.exec( elem.type ); if ( match ) { elem.type = match[1]; } else { elem.removeAttribute("type"); } return elem; } // Mark scripts as having already been evaluated function setGlobalEval( elems, refElements ) { var elem, i = 0; for ( ; (elem = elems[i]) != null; i++ ) { jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); } } function cloneCopyEvent( src, dest ) { if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { return; } var type, i, l, oldData = jQuery._data( src ), curData = jQuery._data( dest, oldData ), events = oldData.events; if ( events ) { delete curData.handle; curData.events = {}; for ( type in events ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) { jQuery.event.add( dest, type, events[ type ][ i ] ); } } } // make the cloned public data object a copy from the original if ( curData.data ) { curData.data = jQuery.extend( {}, curData.data ); } } function fixCloneNodeIssues( src, dest ) { var nodeName, e, data; // We do not need to do anything for non-Elements if ( dest.nodeType !== 1 ) { return; } nodeName = dest.nodeName.toLowerCase(); // IE6-8 copies events bound via attachEvent when using cloneNode. if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { data = jQuery._data( dest ); for ( e in data.events ) { jQuery.removeEvent( dest, e, data.handle ); } // Event data gets referenced instead of copied if the expando gets copied too dest.removeAttribute( jQuery.expando ); } // IE blanks contents when cloning scripts, and tries to evaluate newly-set text if ( nodeName === "script" && dest.text !== src.text ) { disableScript( dest ).text = src.text; restoreScript( dest ); // IE6-10 improperly clones children of object elements using classid. // IE10 throws NoModificationAllowedError if parent is null, #12132. } else if ( nodeName === "object" ) { if ( dest.parentNode ) { dest.outerHTML = src.outerHTML; } // This path appears unavoidable for IE9. When cloning an object // element in IE9, the outerHTML strategy above is not sufficient. // If the src has innerHTML and the destination does not, // copy the src.innerHTML into the dest.innerHTML. #10324 if ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { dest.innerHTML = src.innerHTML; } } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { // IE6-8 fails to persist the checked state of a cloned checkbox // or radio button. Worse, IE6-7 fail to give the cloned element // a checked appearance if the defaultChecked value isn't also set dest.defaultChecked = dest.checked = src.checked; // IE6-7 get confused and end up setting the value of a cloned // checkbox/radio button to an empty string instead of "on" if ( dest.value !== src.value ) { dest.value = src.value; } // IE6-8 fails to return the selected option to the default selected // state when cloning options } else if ( nodeName === "option" ) { dest.defaultSelected = dest.selected = src.defaultSelected; // IE6-8 fails to set the defaultValue to the correct value when // cloning other types of input fields } else if ( nodeName === "input" || nodeName === "textarea" ) { dest.defaultValue = src.defaultValue; } } jQuery.extend({ clone: function( elem, dataAndEvents, deepDataAndEvents ) { var destElements, node, clone, i, srcElements, inPage = jQuery.contains( elem.ownerDocument, elem ); if ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { clone = elem.cloneNode( true ); // IE<=8 does not properly clone detached, unknown element nodes } else { fragmentDiv.innerHTML = elem.outerHTML; fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); } if ( (!support.noCloneEvent || !support.noCloneChecked) && (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 destElements = getAll( clone ); srcElements = getAll( elem ); // Fix all IE cloning issues for ( i = 0; (node = srcElements[i]) != null; ++i ) { // Ensure that the destination node is not null; Fixes #9587 if ( destElements[i] ) { fixCloneNodeIssues( node, destElements[i] ); } } } // Copy the events from the original to the clone if ( dataAndEvents ) { if ( deepDataAndEvents ) { srcElements = srcElements || getAll( elem ); destElements = destElements || getAll( clone ); for ( i = 0; (node = srcElements[i]) != null; i++ ) { cloneCopyEvent( node, destElements[i] ); } } else { cloneCopyEvent( elem, clone ); } } // Preserve script evaluation history destElements = getAll( clone, "script" ); if ( destElements.length > 0 ) { setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); } destElements = srcElements = node = null; // Return the cloned set return clone; }, buildFragment: function( elems, context, scripts, selection ) { var j, elem, contains, tmp, tag, tbody, wrap, l = elems.length, // Ensure a safe fragment safe = createSafeFragment( context ), nodes = [], i = 0; for ( ; i < l; i++ ) { elem = elems[ i ]; if ( elem || elem === 0 ) { // Add nodes directly if ( jQuery.type( elem ) === "object" ) { jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); // Convert non-html into a text node } else if ( !rhtml.test( elem ) ) { nodes.push( context.createTextNode( elem ) ); // Convert html into DOM nodes } else { tmp = tmp || safe.appendChild( context.createElement("div") ); // Deserialize a standard representation tag = (rtagName.exec( elem ) || [ "", "" ])[ 1 ].toLowerCase(); wrap = wrapMap[ tag ] || wrapMap._default; tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2]; // Descend through wrappers to the right content j = wrap[0]; while ( j-- ) { tmp = tmp.lastChild; } // Manually add leading whitespace removed by IE if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); } // Remove IE's autoinserted <tbody> from table fragments if ( !support.tbody ) { // String was a <table>, *may* have spurious <tbody> elem = tag === "table" && !rtbody.test( elem ) ? tmp.firstChild : // String was a bare <thead> or <tfoot> wrap[1] === "<table>" && !rtbody.test( elem ) ? tmp : 0; j = elem && elem.childNodes.length; while ( j-- ) { if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { elem.removeChild( tbody ); } } } jQuery.merge( nodes, tmp.childNodes ); // Fix #12392 for WebKit and IE > 9 tmp.textContent = ""; // Fix #12392 for oldIE while ( tmp.firstChild ) { tmp.removeChild( tmp.firstChild ); } // Remember the top-level container for proper cleanup tmp = safe.lastChild; } } } // Fix #11356: Clear elements from fragment if ( tmp ) { safe.removeChild( tmp ); } // Reset defaultChecked for any radios and checkboxes // about to be appended to the DOM in IE 6/7 (#8060) if ( !support.appendChecked ) { jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); } i = 0; while ( (elem = nodes[ i++ ]) ) { // #4087 - If origin and destination elements are the same, and this is // that element, do not do anything if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { continue; } contains = jQuery.contains( elem.ownerDocument, elem ); // Append to fragment tmp = getAll( safe.appendChild( elem ), "script" ); // Preserve script evaluation history if ( contains ) { setGlobalEval( tmp ); } // Capture executables if ( scripts ) { j = 0; while ( (elem = tmp[ j++ ]) ) { if ( rscriptType.test( elem.type || "" ) ) { scripts.push( elem ); } } } } tmp = null; return safe; }, cleanData: function( elems, /* internal */ acceptData ) { var elem, type, id, data, i = 0, internalKey = jQuery.expando, cache = jQuery.cache, deleteExpando = support.deleteExpando, special = jQuery.event.special; for ( ; (elem = elems[i]) != null; i++ ) { if ( acceptData || jQuery.acceptData( elem ) ) { id = elem[ internalKey ]; data = id && cache[ id ]; if ( data ) { if ( data.events ) { for ( type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } } } // Remove cache only if it was not already removed by jQuery.event.remove if ( cache[ id ] ) { delete cache[ id ]; // IE does not allow us to delete expando properties from nodes, // nor does it have a removeAttribute function on Document nodes; // we must handle all of these cases if ( deleteExpando ) { delete elem[ internalKey ]; } else if ( typeof elem.removeAttribute !== strundefined ) { elem.removeAttribute( internalKey ); } else { elem[ internalKey ] = null; } deletedIds.push( id ); } } } } } }); jQuery.fn.extend({ text: function( value ) { return access( this, function( value ) { return value === undefined ? jQuery.text( this ) : this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); }, null, value, arguments.length ); }, append: function() { return this.domManip( arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.appendChild( elem ); } }); }, prepend: function() { return this.domManip( arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.insertBefore( elem, target.firstChild ); } }); }, before: function() { return this.domManip( arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this ); } }); }, after: function() { return this.domManip( arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this.nextSibling ); } }); }, remove: function( selector, keepData /* Internal Use Only */ ) { var elem, elems = selector ? jQuery.filter( selector, this ) : this, i = 0; for ( ; (elem = elems[i]) != null; i++ ) { if ( !keepData && elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem ) ); } if ( elem.parentNode ) { if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { setGlobalEval( getAll( elem, "script" ) ); } elem.parentNode.removeChild( elem ); } } return this; }, empty: function() { var elem, i = 0; for ( ; (elem = this[i]) != null; i++ ) { // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); } // Remove any remaining nodes while ( elem.firstChild ) { elem.removeChild( elem.firstChild ); } // If this is a select, ensure that it displays empty (#12336) // Support: IE<9 if ( elem.options && jQuery.nodeName( elem, "select" ) ) { elem.options.length = 0; } } return this; }, clone: function( dataAndEvents, deepDataAndEvents ) { dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map(function() { return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); }); }, html: function( value ) { return access( this, function( value ) { var elem = this[ 0 ] || {}, i = 0, l = this.length; if ( value === undefined ) { return elem.nodeType === 1 ? elem.innerHTML.replace( rinlinejQuery, "" ) : undefined; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && ( support.htmlSerialize || !rnoshimcache.test( value ) ) && ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && !wrapMap[ (rtagName.exec( value ) || [ "", "" ])[ 1 ].toLowerCase() ] ) { value = value.replace( rxhtmlTag, "<$1></$2>" ); try { for (; i < l; i++ ) { // Remove element nodes and prevent memory leaks elem = this[i] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch(e) {} } if ( elem ) { this.empty().append( value ); } }, null, value, arguments.length ); }, replaceWith: function() { var arg = arguments[ 0 ]; // Make the changes, replacing each context element with the new content this.domManip( arguments, function( elem ) { arg = this.parentNode; jQuery.cleanData( getAll( this ) ); if ( arg ) { arg.replaceChild( elem, this ); } }); // Force removal if there was no new content (e.g., from empty arguments) return arg && (arg.length || arg.nodeType) ? this : this.remove(); }, detach: function( selector ) { return this.remove( selector, true ); }, domManip: function( args, callback ) { // Flatten any nested arrays args = concat.apply( [], args ); var first, node, hasScripts, scripts, doc, fragment, i = 0, l = this.length, set = this, iNoClone = l - 1, value = args[0], isFunction = jQuery.isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit if ( isFunction || ( l > 1 && typeof value === "string" && !support.checkClone && rchecked.test( value ) ) ) { return this.each(function( index ) { var self = set.eq( index ); if ( isFunction ) { args[0] = value.call( this, index, self.html() ); } self.domManip( args, callback ); }); } if ( l ) { fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { fragment = first; } if ( first ) { scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); hasScripts = scripts.length; // Use the original fragment for the last item instead of the first because it can end up // being emptied incorrectly in certain situations (#8070). for ( ; i < l; i++ ) { node = fragment; if ( i !== iNoClone ) { node = jQuery.clone( node, true, true ); // Keep references to cloned scripts for later restoration if ( hasScripts ) { jQuery.merge( scripts, getAll( node, "script" ) ); } } callback.call( this[i], node, i ); } if ( hasScripts ) { doc = scripts[ scripts.length - 1 ].ownerDocument; // Reenable scripts jQuery.map( scripts, restoreScript ); // Evaluate executable scripts on first document insertion for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl ) { jQuery._evalUrl( node.src ); } } else { jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); } } } } // Fix #11809: Avoid leaking memory fragment = first = null; } } return this; } }); jQuery.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var elems, i = 0, ret = [], insert = jQuery( selector ), last = insert.length - 1; for ( ; i <= last; i++ ) { elems = i === last ? this : this.clone(true); jQuery( insert[i] )[ original ]( elems ); // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() push.apply( ret, elems.get() ); } return this.pushStack( ret ); }; }); var iframe, elemdisplay = {}; /** * Retrieve the actual display of a element * @param {String} name nodeName of the element * @param {Object} doc Document object */ // Called only from within defaultDisplay function actualDisplay( name, doc ) { var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), // getDefaultComputedStyle might be reliably used only on attached element display = window.getDefaultComputedStyle ? // Use of this method is a temporary fix (more like optmization) until something better comes along, // since it was removed from specification and supported only in FF window.getDefaultComputedStyle( elem[ 0 ] ).display : jQuery.css( elem[ 0 ], "display" ); // We don't have any data stored on the element, // so use "detach" method as fast way to get rid of the element elem.detach(); return display; } /** * Try to determine the default display value of an element * @param {String} nodeName */ function defaultDisplay( nodeName ) { var doc = document, display = elemdisplay[ nodeName ]; if ( !display ) { display = actualDisplay( nodeName, doc ); // If the simple way fails, read from inside an iframe if ( display === "none" || !display ) { // Use the already-created iframe if possible iframe = (iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" )).appendTo( doc.documentElement ); // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse doc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document; // Support: IE doc.write(); doc.close(); display = actualDisplay( nodeName, doc ); iframe.detach(); } // Store the correct default display elemdisplay[ nodeName ] = display; } return display; } (function() { var a, shrinkWrapBlocksVal, div = document.createElement( "div" ), divReset = "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;" + "display:block;padding:0;margin:0;border:0"; // Setup div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; a = div.getElementsByTagName( "a" )[ 0 ]; a.style.cssText = "float:left;opacity:.5"; // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 support.opacity = /^0.5/.test( a.style.opacity ); // Verify style float existence // (IE uses styleFloat instead of cssFloat) support.cssFloat = !!a.style.cssFloat; div.style.backgroundClip = "content-box"; div.cloneNode( true ).style.backgroundClip = ""; support.clearCloneStyle = div.style.backgroundClip === "content-box"; // Null elements to avoid leaks in IE. a = div = null; support.shrinkWrapBlocks = function() { var body, container, div, containerStyles; if ( shrinkWrapBlocksVal == null ) { body = document.getElementsByTagName( "body" )[ 0 ]; if ( !body ) { // Test fired too early or in an unsupported environment, exit. return; } containerStyles = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px"; container = document.createElement( "div" ); div = document.createElement( "div" ); body.appendChild( container ).appendChild( div ); // Will be changed later if needed. shrinkWrapBlocksVal = false; if ( typeof div.style.zoom !== strundefined ) { // Support: IE6 // Check if elements with layout shrink-wrap their children div.style.cssText = divReset + ";width:1px;padding:1px;zoom:1"; div.innerHTML = "<div></div>"; div.firstChild.style.width = "5px"; shrinkWrapBlocksVal = div.offsetWidth !== 3; } body.removeChild( container ); // Null elements to avoid leaks in IE. body = container = div = null; } return shrinkWrapBlocksVal; }; })(); var rmargin = (/^margin/); var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); var getStyles, curCSS, rposition = /^(top|right|bottom|left)$/; if ( window.getComputedStyle ) { getStyles = function( elem ) { return elem.ownerDocument.defaultView.getComputedStyle( elem, null ); }; curCSS = function( elem, name, computed ) { var width, minWidth, maxWidth, ret, style = elem.style; computed = computed || getStyles( elem ); // getPropertyValue is only needed for .css('filter') in IE9, see #12537 ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined; if ( computed ) { if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { ret = jQuery.style( elem, name ); } // A tribute to the "awesome hack by Dean Edwards" // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { // Remember the original values width = style.width; minWidth = style.minWidth; maxWidth = style.maxWidth; // Put in the new values to get a computed value out style.minWidth = style.maxWidth = style.width = ret; ret = computed.width; // Revert the changed values style.width = width; style.minWidth = minWidth; style.maxWidth = maxWidth; } } // Support: IE // IE returns zIndex value as an integer. return ret === undefined ? ret : ret + ""; }; } else if ( document.documentElement.currentStyle ) { getStyles = function( elem ) { return elem.currentStyle; }; curCSS = function( elem, name, computed ) { var left, rs, rsLeft, ret, style = elem.style; computed = computed || getStyles( elem ); ret = computed ? computed[ name ] : undefined; // Avoid setting ret to empty string here // so we don't default to auto if ( ret == null && style && style[ name ] ) { ret = style[ name ]; } // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels // but not position css attributes, as those are proportional to the parent element instead // and we can't measure the parent instead because it might trigger a "stacking dolls" problem if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { // Remember the original values left = style.left; rs = elem.runtimeStyle; rsLeft = rs && rs.left; // Put in the new values to get a computed value out if ( rsLeft ) { rs.left = elem.currentStyle.left; } style.left = name === "fontSize" ? "1em" : ret; ret = style.pixelLeft + "px"; // Revert the changed values style.left = left; if ( rsLeft ) { rs.left = rsLeft; } } // Support: IE // IE returns zIndex value as an integer. return ret === undefined ? ret : ret + "" || "auto"; }; } function addGetHookIf( conditionFn, hookFn ) { // Define the hook, we'll check on the first run if it's really needed. return { get: function() { var condition = conditionFn(); if ( condition == null ) { // The test was not ready at this point; screw the hook this time // but check again when needed next time. return; } if ( condition ) { // Hook not needed (or it's not possible to use it due to missing dependency), // remove it. // Since there are no other hooks for marginRight, remove the whole object. delete this.get; return; } // Hook needed; redefine it so that the support test is not executed again. return (this.get = hookFn).apply( this, arguments ); } }; } (function() { var a, reliableHiddenOffsetsVal, boxSizingVal, boxSizingReliableVal, pixelPositionVal, reliableMarginRightVal, div = document.createElement( "div" ), containerStyles = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px", divReset = "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;" + "display:block;padding:0;margin:0;border:0"; // Setup div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; a = div.getElementsByTagName( "a" )[ 0 ]; a.style.cssText = "float:left;opacity:.5"; // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 support.opacity = /^0.5/.test( a.style.opacity ); // Verify style float existence // (IE uses styleFloat instead of cssFloat) support.cssFloat = !!a.style.cssFloat; div.style.backgroundClip = "content-box"; div.cloneNode( true ).style.backgroundClip = ""; support.clearCloneStyle = div.style.backgroundClip === "content-box"; // Null elements to avoid leaks in IE. a = div = null; jQuery.extend(support, { reliableHiddenOffsets: function() { if ( reliableHiddenOffsetsVal != null ) { return reliableHiddenOffsetsVal; } var container, tds, isSupported, div = document.createElement( "div" ), body = document.getElementsByTagName( "body" )[ 0 ]; if ( !body ) { // Return for frameset docs that don't have a body return; } // Setup div.setAttribute( "className", "t" ); div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; container = document.createElement( "div" ); container.style.cssText = containerStyles; body.appendChild( container ).appendChild( div ); // Support: IE8 // Check if table cells still have offsetWidth/Height when they are set // to display:none and there are still other visible table cells in a // table row; if so, offsetWidth/Height are not reliable for use when // determining if an element has been hidden directly using // display:none (it is still safe to use offsets if a parent element is // hidden; don safety goggles and see bug #4512 for more information). div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>"; tds = div.getElementsByTagName( "td" ); tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; isSupported = ( tds[ 0 ].offsetHeight === 0 ); tds[ 0 ].style.display = ""; tds[ 1 ].style.display = "none"; // Support: IE8 // Check if empty table cells still have offsetWidth/Height reliableHiddenOffsetsVal = isSupported && ( tds[ 0 ].offsetHeight === 0 ); body.removeChild( container ); // Null elements to avoid leaks in IE. div = body = null; return reliableHiddenOffsetsVal; }, boxSizing: function() { if ( boxSizingVal == null ) { computeStyleTests(); } return boxSizingVal; }, boxSizingReliable: function() { if ( boxSizingReliableVal == null ) { computeStyleTests(); } return boxSizingReliableVal; }, pixelPosition: function() { if ( pixelPositionVal == null ) { computeStyleTests(); } return pixelPositionVal; }, reliableMarginRight: function() { var body, container, div, marginDiv; // Use window.getComputedStyle because jsdom on node.js will break without it. if ( reliableMarginRightVal == null && window.getComputedStyle ) { body = document.getElementsByTagName( "body" )[ 0 ]; if ( !body ) { // Test fired too early or in an unsupported environment, exit. return; } container = document.createElement( "div" ); div = document.createElement( "div" ); container.style.cssText = containerStyles; body.appendChild( container ).appendChild( div ); // Check if div with explicit width and no margin-right incorrectly // gets computed margin-right based on width of container. (#3333) // Fails in WebKit before Feb 2011 nightlies // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right marginDiv = div.appendChild( document.createElement( "div" ) ); marginDiv.style.cssText = div.style.cssText = divReset; marginDiv.style.marginRight = marginDiv.style.width = "0"; div.style.width = "1px"; reliableMarginRightVal = !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); body.removeChild( container ); } return reliableMarginRightVal; } }); function computeStyleTests() { var container, div, body = document.getElementsByTagName( "body" )[ 0 ]; if ( !body ) { // Test fired too early or in an unsupported environment, exit. return; } container = document.createElement( "div" ); div = document.createElement( "div" ); container.style.cssText = containerStyles; body.appendChild( container ).appendChild( div ); div.style.cssText = "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;" + "position:absolute;display:block;padding:1px;border:1px;width:4px;" + "margin-top:1%;top:1%"; // Workaround failing boxSizing test due to offsetWidth returning wrong value // with some non-1 values of body zoom, ticket #13543 jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { boxSizingVal = div.offsetWidth === 4; }); // Will be changed later if needed. boxSizingReliableVal = true; pixelPositionVal = false; reliableMarginRightVal = true; // Use window.getComputedStyle because jsdom on node.js will break without it. if ( window.getComputedStyle ) { pixelPositionVal = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; boxSizingReliableVal = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; } body.removeChild( container ); // Null elements to avoid leaks in IE. div = body = null; } })(); // A method for quickly swapping in/out CSS properties to get correct calculations. jQuery.swap = function( elem, options, callback, args ) { var ret, name, old = {}; // Remember the old values, and insert the new ones for ( name in options ) { old[ name ] = elem.style[ name ]; elem.style[ name ] = options[ name ]; } ret = callback.apply( elem, args || [] ); // Revert the old values for ( name in options ) { elem.style[ name ] = old[ name ]; } return ret; }; var ralpha = /alpha\([^)]*\)/i, ropacity = /opacity\s*=\s*([^)]*)/, // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display rdisplayswap = /^(none|table(?!-c[ea]).+)/, rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ), rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ), cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssNormalTransform = { letterSpacing: 0, fontWeight: 400 }, cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; // return a css property mapped to a potentially vendor prefixed property function vendorPropName( style, name ) { // shortcut for names that are not vendor prefixed if ( name in style ) { return name; } // check for vendor prefixed names var capName = name.charAt(0).toUpperCase() + name.slice(1), origName = name, i = cssPrefixes.length; while ( i-- ) { name = cssPrefixes[ i ] + capName; if ( name in style ) { return name; } } return origName; } function showHide( elements, show ) { var display, elem, hidden, values = [], index = 0, length = elements.length; for ( ; index < length; index++ ) { elem = elements[ index ]; if ( !elem.style ) { continue; } values[ index ] = jQuery._data( elem, "olddisplay" ); display = elem.style.display; if ( show ) { // Reset the inline display of this element to learn if it is // being hidden by cascaded rules or not if ( !values[ index ] && display === "none" ) { elem.style.display = ""; } // Set elements which have been overridden with display: none // in a stylesheet to whatever the default browser style is // for such an element if ( elem.style.display === "" && isHidden( elem ) ) { values[ index ] = jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); } } else { if ( !values[ index ] ) { hidden = isHidden( elem ); if ( display && display !== "none" || !hidden ) { jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); } } } } // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( index = 0; index < length; index++ ) { elem = elements[ index ]; if ( !elem.style ) { continue; } if ( !show || elem.style.display === "none" || elem.style.display === "" ) { elem.style.display = show ? values[ index ] || "" : "none"; } } return elements; } function setPositiveNumber( elem, value, subtract ) { var matches = rnumsplit.exec( value ); return matches ? // Guard against undefined "subtract", e.g., when used as in cssHooks Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : value; } function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { var i = extra === ( isBorderBox ? "border" : "content" ) ? // If we already have the right measurement, avoid augmentation 4 : // Otherwise initialize for horizontal or vertical properties name === "width" ? 1 : 0, val = 0; for ( ; i < 4; i += 2 ) { // both box models exclude margin, so add it if we want it if ( extra === "margin" ) { val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); } if ( isBorderBox ) { // border-box includes padding, so remove it if we want content if ( extra === "content" ) { val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); } // at this point, extra isn't border nor margin, so remove border if ( extra !== "margin" ) { val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } else { // at this point, extra isn't content, so add padding val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); // at this point, extra isn't content nor padding, so add border if ( extra !== "padding" ) { val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } } return val; } function getWidthOrHeight( elem, name, extra ) { // Start with offset property, which is equivalent to the border-box value var valueIsBorderBox = true, val = name === "width" ? elem.offsetWidth : elem.offsetHeight, styles = getStyles( elem ), isBorderBox = support.boxSizing() && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; // some non-html elements return undefined for offsetWidth, so check for null/undefined // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 if ( val <= 0 || val == null ) { // Fall back to computed then uncomputed css if necessary val = curCSS( elem, name, styles ); if ( val < 0 || val == null ) { val = elem.style[ name ]; } // Computed unit is not pixels. Stop here and return. if ( rnumnonpx.test(val) ) { return val; } // we need the check for style in case a browser which returns unreliable values // for getComputedStyle silently falls back to the reliable elem.style valueIsBorderBox = isBorderBox && ( support.boxSizingReliable() || val === elem.style[ name ] ); // Normalize "", auto, and prepare for extra val = parseFloat( val ) || 0; } // use the active box-sizing model to add/subtract irrelevant styles return ( val + augmentWidthOrHeight( elem, name, extra || ( isBorderBox ? "border" : "content" ), valueIsBorderBox, styles ) ) + "px"; } jQuery.extend({ // Add in style property hooks for overriding the default // behavior of getting and setting a style property cssHooks: { opacity: { get: function( elem, computed ) { if ( computed ) { // We should always get a number back from opacity var ret = curCSS( elem, "opacity" ); return ret === "" ? "1" : ret; } } } }, // Don't automatically add "px" to these possibly-unitless properties cssNumber: { "columnCount": true, "fillOpacity": true, "fontWeight": true, "lineHeight": true, "opacity": true, "order": true, "orphans": true, "widows": true, "zIndex": true, "zoom": true }, // Add in properties whose names you wish to fix before // setting or getting the value cssProps: { // normalize float css property "float": support.cssFloat ? "cssFloat" : "styleFloat" }, // Get and set the style property on a DOM Node style: function( elem, name, value, extra ) { // Don't set styles on text and comment nodes if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { return; } // Make sure that we're working with the right name var ret, type, hooks, origName = jQuery.camelCase( name ), style = elem.style; name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); // gets hook for the prefixed version // followed by the unprefixed version hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; // Check if we're setting a value if ( value !== undefined ) { type = typeof value; // convert relative number strings (+= or -=) to relative numbers. #7345 if ( type === "string" && (ret = rrelNum.exec( value )) ) { value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); // Fixes bug #9237 type = "number"; } // Make sure that null and NaN values aren't set. See: #7116 if ( value == null || value !== value ) { return; } // If a number was passed in, add 'px' to the (except for certain CSS properties) if ( type === "number" && !jQuery.cssNumber[ origName ] ) { value += "px"; } // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, // but it would mean to define eight (for every problematic property) identical functions if ( !support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { style[ name ] = "inherit"; } // If a hook was provided, use that value, otherwise just set the specified value if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { // Support: IE // Swallow errors from 'invalid' CSS values (#5509) try { // Support: Chrome, Safari // Setting style to blank string required to delete "style: x !important;" style[ name ] = ""; style[ name ] = value; } catch(e) {} } } else { // If a hook was provided get the non-computed value from there if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { return ret; } // Otherwise just get the value from the style object return style[ name ]; } }, css: function( elem, name, extra, styles ) { var num, val, hooks, origName = jQuery.camelCase( name ); // Make sure that we're working with the right name name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); // gets hook for the prefixed version // followed by the unprefixed version hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; // If a hook was provided get the computed value from there if ( hooks && "get" in hooks ) { val = hooks.get( elem, true, extra ); } // Otherwise, if a way to get the computed value exists, use that if ( val === undefined ) { val = curCSS( elem, name, styles ); } //convert "normal" to computed value if ( val === "normal" && name in cssNormalTransform ) { val = cssNormalTransform[ name ]; } // Return, converting to number if forced or a qualifier was provided and val looks numeric if ( extra === "" || extra ) { num = parseFloat( val ); return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; } return val; } }); jQuery.each([ "height", "width" ], function( i, name ) { jQuery.cssHooks[ name ] = { get: function( elem, computed, extra ) { if ( computed ) { // certain elements can have dimension info if we invisibly show them // however, it must have a current display style that would benefit from this return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ? jQuery.swap( elem, cssShow, function() { return getWidthOrHeight( elem, name, extra ); }) : getWidthOrHeight( elem, name, extra ); } }, set: function( elem, value, extra ) { var styles = extra && getStyles( elem ); return setPositiveNumber( elem, value, extra ? augmentWidthOrHeight( elem, name, extra, support.boxSizing() && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", styles ) : 0 ); } }; }); if ( !support.opacity ) { jQuery.cssHooks.opacity = { get: function( elem, computed ) { // IE uses filters for opacity return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : computed ? "1" : ""; }, set: function( elem, value ) { var style = elem.style, currentStyle = elem.currentStyle, opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level style.zoom = 1; // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 // if value === "", then remove inline opacity #12685 if ( ( value >= 1 || value === "" ) && jQuery.trim( filter.replace( ralpha, "" ) ) === "" && style.removeAttribute ) { // Setting style.filter to null, "" & " " still leave "filter:" in the cssText // if "filter:" is present at all, clearType is disabled, we want to avoid this // style.removeAttribute is IE Only, but so apparently is this code path... style.removeAttribute( "filter" ); // if there is no filter style applied in a css rule or unset inline opacity, we are done if ( value === "" || currentStyle && !currentStyle.filter ) { return; } } // otherwise, set new filter values style.filter = ralpha.test( filter ) ? filter.replace( ralpha, opacity ) : filter + " " + opacity; } }; } jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight, function( elem, computed ) { if ( computed ) { // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right // Work around by temporarily setting element display to inline-block return jQuery.swap( elem, { "display": "inline-block" }, curCSS, [ elem, "marginRight" ] ); } } ); // These hooks are used by animate to expand properties jQuery.each({ margin: "", padding: "", border: "Width" }, function( prefix, suffix ) { jQuery.cssHooks[ prefix + suffix ] = { expand: function( value ) { var i = 0, expanded = {}, // assumes a single number if not a string parts = typeof value === "string" ? value.split(" ") : [ value ]; for ( ; i < 4; i++ ) { expanded[ prefix + cssExpand[ i ] + suffix ] = parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; } return expanded; } }; if ( !rmargin.test( prefix ) ) { jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; } }); jQuery.fn.extend({ css: function( name, value ) { return access( this, function( elem, name, value ) { var styles, len, map = {}, i = 0; if ( jQuery.isArray( name ) ) { styles = getStyles( elem ); len = name.length; for ( ; i < len; i++ ) { map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); } return map; } return value !== undefined ? jQuery.style( elem, name, value ) : jQuery.css( elem, name ); }, name, value, arguments.length > 1 ); }, show: function() { return showHide( this, true ); }, hide: function() { return showHide( this ); }, toggle: function( state ) { if ( typeof state === "boolean" ) { return state ? this.show() : this.hide(); } return this.each(function() { if ( isHidden( this ) ) { jQuery( this ).show(); } else { jQuery( this ).hide(); } }); } }); function Tween( elem, options, prop, end, easing ) { return new Tween.prototype.init( elem, options, prop, end, easing ); } jQuery.Tween = Tween; Tween.prototype = { constructor: Tween, init: function( elem, options, prop, end, easing, unit ) { this.elem = elem; this.prop = prop; this.easing = easing || "swing"; this.options = options; this.start = this.now = this.cur(); this.end = end; this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); }, cur: function() { var hooks = Tween.propHooks[ this.prop ]; return hooks && hooks.get ? hooks.get( this ) : Tween.propHooks._default.get( this ); }, run: function( percent ) { var eased, hooks = Tween.propHooks[ this.prop ]; if ( this.options.duration ) { this.pos = eased = jQuery.easing[ this.easing ]( percent, this.options.duration * percent, 0, 1, this.options.duration ); } else { this.pos = eased = percent; } this.now = ( this.end - this.start ) * eased + this.start; if ( this.options.step ) { this.options.step.call( this.elem, this.now, this ); } if ( hooks && hooks.set ) { hooks.set( this ); } else { Tween.propHooks._default.set( this ); } return this; } }; Tween.prototype.init.prototype = Tween.prototype; Tween.propHooks = { _default: { get: function( tween ) { var result; if ( tween.elem[ tween.prop ] != null && (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { return tween.elem[ tween.prop ]; } // passing an empty string as a 3rd parameter to .css will automatically // attempt a parseFloat and fallback to a string if the parse fails // so, simple values such as "10px" are parsed to Float. // complex values such as "rotate(1rad)" are returned as is. result = jQuery.css( tween.elem, tween.prop, "" ); // Empty strings, null, undefined and "auto" are converted to 0. return !result || result === "auto" ? 0 : result; }, set: function( tween ) { // use step hook for back compat - use cssHook if its there - use .style if its // available and use plain properties where available if ( jQuery.fx.step[ tween.prop ] ) { jQuery.fx.step[ tween.prop ]( tween ); } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); } else { tween.elem[ tween.prop ] = tween.now; } } } }; // Support: IE <=9 // Panic based approach to setting things on disconnected nodes Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { set: function( tween ) { if ( tween.elem.nodeType && tween.elem.parentNode ) { tween.elem[ tween.prop ] = tween.now; } } }; jQuery.easing = { linear: function( p ) { return p; }, swing: function( p ) { return 0.5 - Math.cos( p * Math.PI ) / 2; } }; jQuery.fx = Tween.prototype.init; // Back Compat <1.8 extension point jQuery.fx.step = {}; var fxNow, timerId, rfxtypes = /^(?:toggle|show|hide)$/, rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ), rrun = /queueHooks$/, animationPrefilters = [ defaultPrefilter ], tweeners = { "*": [ function( prop, value ) { var tween = this.createTween( prop, value ), target = tween.cur(), parts = rfxnum.exec( value ), unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), // Starting value computation is required for potential unit mismatches start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && rfxnum.exec( jQuery.css( tween.elem, prop ) ), scale = 1, maxIterations = 20; if ( start && start[ 3 ] !== unit ) { // Trust units reported by jQuery.css unit = unit || start[ 3 ]; // Make sure we update the tween properties later on parts = parts || []; // Iteratively approximate from a nonzero starting point start = +target || 1; do { // If previous iteration zeroed out, double until we get *something* // Use a string for doubling factor so we don't accidentally see scale as unchanged below scale = scale || ".5"; // Adjust and apply start = start / scale; jQuery.style( tween.elem, prop, start + unit ); // Update scale, tolerating zero or NaN from tween.cur() // And breaking the loop if scale is unchanged or perfect, or if we've just had enough } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); } // Update tween properties if ( parts ) { start = tween.start = +start || +target || 0; tween.unit = unit; // If a +=/-= token was provided, we're doing a relative animation tween.end = parts[ 1 ] ? start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : +parts[ 2 ]; } return tween; } ] }; // Animations created synchronously will run synchronously function createFxNow() { setTimeout(function() { fxNow = undefined; }); return ( fxNow = jQuery.now() ); } // Generate parameters to create a standard animation function genFx( type, includeWidth ) { var which, attrs = { height: type }, i = 0; // if we include width, step value is 1 to do all cssExpand values, // if we don't include width, step value is 2 to skip over Left and Right includeWidth = includeWidth ? 1 : 0; for ( ; i < 4 ; i += 2 - includeWidth ) { which = cssExpand[ i ]; attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; } if ( includeWidth ) { attrs.opacity = attrs.width = type; } return attrs; } function createTween( value, prop, animation ) { var tween, collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), index = 0, length = collection.length; for ( ; index < length; index++ ) { if ( (tween = collection[ index ].call( animation, prop, value )) ) { // we're done with this property return tween; } } } function defaultPrefilter( elem, props, opts ) { /* jshint validthis: true */ var prop, value, toggle, tween, hooks, oldfire, display, dDisplay, anim = this, orig = {}, style = elem.style, hidden = elem.nodeType && isHidden( elem ), dataShow = jQuery._data( elem, "fxshow" ); // handle queue: false promises if ( !opts.queue ) { hooks = jQuery._queueHooks( elem, "fx" ); if ( hooks.unqueued == null ) { hooks.unqueued = 0; oldfire = hooks.empty.fire; hooks.empty.fire = function() { if ( !hooks.unqueued ) { oldfire(); } }; } hooks.unqueued++; anim.always(function() { // doing this makes sure that the complete handler will be called // before this completes anim.always(function() { hooks.unqueued--; if ( !jQuery.queue( elem, "fx" ).length ) { hooks.empty.fire(); } }); }); } // height/width overflow pass if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and // overflowY are set to the same value opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; // Set display property to inline-block for height/width // animations on inline elements that are having width/height animated display = jQuery.css( elem, "display" ); dDisplay = defaultDisplay( elem.nodeName ); if ( display === "none" ) { display = dDisplay; } if ( display === "inline" && jQuery.css( elem, "float" ) === "none" ) { // inline-level elements accept inline-block; // block-level elements need to be inline with layout if ( !support.inlineBlockNeedsLayout || dDisplay === "inline" ) { style.display = "inline-block"; } else { style.zoom = 1; } } } if ( opts.overflow ) { style.overflow = "hidden"; if ( !support.shrinkWrapBlocks() ) { anim.always(function() { style.overflow = opts.overflow[ 0 ]; style.overflowX = opts.overflow[ 1 ]; style.overflowY = opts.overflow[ 2 ]; }); } } // show/hide pass for ( prop in props ) { value = props[ prop ]; if ( rfxtypes.exec( value ) ) { delete props[ prop ]; toggle = toggle || value === "toggle"; if ( value === ( hidden ? "hide" : "show" ) ) { // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { hidden = true; } else { continue; } } orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); } } if ( !jQuery.isEmptyObject( orig ) ) { if ( dataShow ) { if ( "hidden" in dataShow ) { hidden = dataShow.hidden; } } else { dataShow = jQuery._data( elem, "fxshow", {} ); } // store state if its toggle - enables .stop().toggle() to "reverse" if ( toggle ) { dataShow.hidden = !hidden; } if ( hidden ) { jQuery( elem ).show(); } else { anim.done(function() { jQuery( elem ).hide(); }); } anim.done(function() { var prop; jQuery._removeData( elem, "fxshow" ); for ( prop in orig ) { jQuery.style( elem, prop, orig[ prop ] ); } }); for ( prop in orig ) { tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); if ( !( prop in dataShow ) ) { dataShow[ prop ] = tween.start; if ( hidden ) { tween.end = tween.start; tween.start = prop === "width" || prop === "height" ? 1 : 0; } } } } } function propFilter( props, specialEasing ) { var index, name, easing, value, hooks; // camelCase, specialEasing and expand cssHook pass for ( index in props ) { name = jQuery.camelCase( index ); easing = specialEasing[ name ]; value = props[ index ]; if ( jQuery.isArray( value ) ) { easing = value[ 1 ]; value = props[ index ] = value[ 0 ]; } if ( index !== name ) { props[ name ] = value; delete props[ index ]; } hooks = jQuery.cssHooks[ name ]; if ( hooks && "expand" in hooks ) { value = hooks.expand( value ); delete props[ name ]; // not quite $.extend, this wont overwrite keys already present. // also - reusing 'index' from above because we have the correct "name" for ( index in value ) { if ( !( index in props ) ) { props[ index ] = value[ index ]; specialEasing[ index ] = easing; } } } else { specialEasing[ name ] = easing; } } } function Animation( elem, properties, options ) { var result, stopped, index = 0, length = animationPrefilters.length, deferred = jQuery.Deferred().always( function() { // don't match elem in the :animated selector delete tick.elem; }), tick = function() { if ( stopped ) { return false; } var currentTime = fxNow || createFxNow(), remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) temp = remaining / animation.duration || 0, percent = 1 - temp, index = 0, length = animation.tweens.length; for ( ; index < length ; index++ ) { animation.tweens[ index ].run( percent ); } deferred.notifyWith( elem, [ animation, percent, remaining ]); if ( percent < 1 && length ) { return remaining; } else { deferred.resolveWith( elem, [ animation ] ); return false; } }, animation = deferred.promise({ elem: elem, props: jQuery.extend( {}, properties ), opts: jQuery.extend( true, { specialEasing: {} }, options ), originalProperties: properties, originalOptions: options, startTime: fxNow || createFxNow(), duration: options.duration, tweens: [], createTween: function( prop, end ) { var tween = jQuery.Tween( elem, animation.opts, prop, end, animation.opts.specialEasing[ prop ] || animation.opts.easing ); animation.tweens.push( tween ); return tween; }, stop: function( gotoEnd ) { var index = 0, // if we are going to the end, we want to run all the tweens // otherwise we skip this part length = gotoEnd ? animation.tweens.length : 0; if ( stopped ) { return this; } stopped = true; for ( ; index < length ; index++ ) { animation.tweens[ index ].run( 1 ); } // resolve when we played the last frame // otherwise, reject if ( gotoEnd ) { deferred.resolveWith( elem, [ animation, gotoEnd ] ); } else { deferred.rejectWith( elem, [ animation, gotoEnd ] ); } return this; } }), props = animation.props; propFilter( props, animation.opts.specialEasing ); for ( ; index < length ; index++ ) { result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); if ( result ) { return result; } } jQuery.map( props, createTween, animation ); if ( jQuery.isFunction( animation.opts.start ) ) { animation.opts.start.call( elem, animation ); } jQuery.fx.timer( jQuery.extend( tick, { elem: elem, anim: animation, queue: animation.opts.queue }) ); // attach callbacks from options return animation.progress( animation.opts.progress ) .done( animation.opts.done, animation.opts.complete ) .fail( animation.opts.fail ) .always( animation.opts.always ); } jQuery.Animation = jQuery.extend( Animation, { tweener: function( props, callback ) { if ( jQuery.isFunction( props ) ) { callback = props; props = [ "*" ]; } else { props = props.split(" "); } var prop, index = 0, length = props.length; for ( ; index < length ; index++ ) { prop = props[ index ]; tweeners[ prop ] = tweeners[ prop ] || []; tweeners[ prop ].unshift( callback ); } }, prefilter: function( callback, prepend ) { if ( prepend ) { animationPrefilters.unshift( callback ); } else { animationPrefilters.push( callback ); } } }); jQuery.speed = function( speed, easing, fn ) { var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing }; opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; // normalize opt.queue - true/undefined/null -> "fx" if ( opt.queue == null || opt.queue === true ) { opt.queue = "fx"; } // Queueing opt.old = opt.complete; opt.complete = function() { if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } if ( opt.queue ) { jQuery.dequeue( this, opt.queue ); } }; return opt; }; jQuery.fn.extend({ fadeTo: function( speed, to, easing, callback ) { // show any hidden elements after setting opacity to 0 return this.filter( isHidden ).css( "opacity", 0 ).show() // animate to the value specified .end().animate({ opacity: to }, speed, easing, callback ); }, animate: function( prop, speed, easing, callback ) { var empty = jQuery.isEmptyObject( prop ), optall = jQuery.speed( speed, easing, callback ), doAnimation = function() { // Operate on a copy of prop so per-property easing won't be lost var anim = Animation( this, jQuery.extend( {}, prop ), optall ); // Empty animations, or finishing resolves immediately if ( empty || jQuery._data( this, "finish" ) ) { anim.stop( true ); } }; doAnimation.finish = doAnimation; return empty || optall.queue === false ? this.each( doAnimation ) : this.queue( optall.queue, doAnimation ); }, stop: function( type, clearQueue, gotoEnd ) { var stopQueue = function( hooks ) { var stop = hooks.stop; delete hooks.stop; stop( gotoEnd ); }; if ( typeof type !== "string" ) { gotoEnd = clearQueue; clearQueue = type; type = undefined; } if ( clearQueue && type !== false ) { this.queue( type || "fx", [] ); } return this.each(function() { var dequeue = true, index = type != null && type + "queueHooks", timers = jQuery.timers, data = jQuery._data( this ); if ( index ) { if ( data[ index ] && data[ index ].stop ) { stopQueue( data[ index ] ); } } else { for ( index in data ) { if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { stopQueue( data[ index ] ); } } } for ( index = timers.length; index--; ) { if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { timers[ index ].anim.stop( gotoEnd ); dequeue = false; timers.splice( index, 1 ); } } // start the next in the queue if the last step wasn't forced // timers currently will call their complete callbacks, which will dequeue // but only if they were gotoEnd if ( dequeue || !gotoEnd ) { jQuery.dequeue( this, type ); } }); }, finish: function( type ) { if ( type !== false ) { type = type || "fx"; } return this.each(function() { var index, data = jQuery._data( this ), queue = data[ type + "queue" ], hooks = data[ type + "queueHooks" ], timers = jQuery.timers, length = queue ? queue.length : 0; // enable finishing flag on private data data.finish = true; // empty the queue first jQuery.queue( this, type, [] ); if ( hooks && hooks.stop ) { hooks.stop.call( this, true ); } // look for any active animations, and finish them for ( index = timers.length; index--; ) { if ( timers[ index ].elem === this && timers[ index ].queue === type ) { timers[ index ].anim.stop( true ); timers.splice( index, 1 ); } } // look for any animations in the old queue and finish them for ( index = 0; index < length; index++ ) { if ( queue[ index ] && queue[ index ].finish ) { queue[ index ].finish.call( this ); } } // turn off finishing flag delete data.finish; }); } }); jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { var cssFn = jQuery.fn[ name ]; jQuery.fn[ name ] = function( speed, easing, callback ) { return speed == null || typeof speed === "boolean" ? cssFn.apply( this, arguments ) : this.animate( genFx( name, true ), speed, easing, callback ); }; }); // Generate shortcuts for custom animations jQuery.each({ slideDown: genFx("show"), slideUp: genFx("hide"), slideToggle: genFx("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function( name, props ) { jQuery.fn[ name ] = function( speed, easing, callback ) { return this.animate( props, speed, easing, callback ); }; }); jQuery.timers = []; jQuery.fx.tick = function() { var timer, timers = jQuery.timers, i = 0; fxNow = jQuery.now(); for ( ; i < timers.length; i++ ) { timer = timers[ i ]; // Checks the timer has not already been removed if ( !timer() && timers[ i ] === timer ) { timers.splice( i--, 1 ); } } if ( !timers.length ) { jQuery.fx.stop(); } fxNow = undefined; }; jQuery.fx.timer = function( timer ) { jQuery.timers.push( timer ); if ( timer() ) { jQuery.fx.start(); } else { jQuery.timers.pop(); } }; jQuery.fx.interval = 13; jQuery.fx.start = function() { if ( !timerId ) { timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); } }; jQuery.fx.stop = function() { clearInterval( timerId ); timerId = null; }; jQuery.fx.speeds = { slow: 600, fast: 200, // Default speed _default: 400 }; // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ jQuery.fn.delay = function( time, type ) { time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; return this.queue( type, function( next, hooks ) { var timeout = setTimeout( next, time ); hooks.stop = function() { clearTimeout( timeout ); }; }); }; (function() { var a, input, select, opt, div = document.createElement("div" ); // Setup div.setAttribute( "className", "t" ); div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; a = div.getElementsByTagName("a")[ 0 ]; // First batch of tests. select = document.createElement("select"); opt = select.appendChild( document.createElement("option") ); input = div.getElementsByTagName("input")[ 0 ]; a.style.cssText = "top:1px"; // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) support.getSetAttribute = div.className !== "t"; // Get the style information from getAttribute // (IE uses .cssText instead) support.style = /top/.test( a.getAttribute("style") ); // Make sure that URLs aren't manipulated // (IE normalizes it by default) support.hrefNormalized = a.getAttribute("href") === "/a"; // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) support.checkOn = !!input.value; // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) support.optSelected = opt.selected; // Tests for enctype support on a form (#6743) support.enctype = !!document.createElement("form").enctype; // Make sure that the options inside disabled selects aren't marked as disabled // (WebKit marks them as disabled) select.disabled = true; support.optDisabled = !opt.disabled; // Support: IE8 only // Check if we can trust getAttribute("value") input = document.createElement( "input" ); input.setAttribute( "value", "" ); support.input = input.getAttribute( "value" ) === ""; // Check if an input maintains its value after becoming a radio input.value = "t"; input.setAttribute( "type", "radio" ); support.radioValue = input.value === "t"; // Null elements to avoid leaks in IE. a = input = select = opt = div = null; })(); var rreturn = /\r/g; jQuery.fn.extend({ val: function( value ) { var hooks, ret, isFunction, elem = this[0]; if ( !arguments.length ) { if ( elem ) { hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; } ret = elem.value; return typeof ret === "string" ? // handle most common string cases ret.replace(rreturn, "") : // handle cases where value is null/undef or number ret == null ? "" : ret; } return; } isFunction = jQuery.isFunction( value ); return this.each(function( i ) { var val; if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { val = value.call( this, i, jQuery( this ).val() ); } else { val = value; } // Treat null/undefined as ""; convert numbers to string if ( val == null ) { val = ""; } else if ( typeof val === "number" ) { val += ""; } else if ( jQuery.isArray( val ) ) { val = jQuery.map( val, function( value ) { return value == null ? "" : value + ""; }); } hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; // If set returns undefined, fall back to normal setting if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); } }); jQuery.extend({ valHooks: { option: { get: function( elem ) { var val = jQuery.find.attr( elem, "value" ); return val != null ? val : jQuery.text( elem ); } }, select: { get: function( elem ) { var value, option, options = elem.options, index = elem.selectedIndex, one = elem.type === "select-one" || index < 0, values = one ? null : [], max = one ? index + 1 : options.length, i = index < 0 ? max : one ? index : 0; // Loop through all the selected options for ( ; i < max; i++ ) { option = options[ i ]; // oldIE doesn't update selected after form reset (#2551) if ( ( option.selected || i === index ) && // Don't return options that are disabled or in a disabled optgroup ( support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { // Get the specific value for the option value = jQuery( option ).val(); // We don't need an array for one selects if ( one ) { return value; } // Multi-Selects return an array values.push( value ); } } return values; }, set: function( elem, value ) { var optionSet, option, options = elem.options, values = jQuery.makeArray( value ), i = options.length; while ( i-- ) { option = options[ i ]; if ( jQuery.inArray( jQuery.valHooks.option.get( option ), values ) >= 0 ) { // Support: IE6 // When new option element is added to select box we need to // force reflow of newly added node in order to workaround delay // of initialization properties try { option.selected = optionSet = true; } catch ( _ ) { // Will be executed only in IE6 option.scrollHeight; } } else { option.selected = false; } } // Force browsers to behave consistently when non-matching value is set if ( !optionSet ) { elem.selectedIndex = -1; } return options; } } } }); // Radios and checkboxes getter/setter jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } }; if ( !support.checkOn ) { jQuery.valHooks[ this ].get = function( elem ) { // Support: Webkit // "" is returned instead of "on" if a value isn't specified return elem.getAttribute("value") === null ? "on" : elem.value; }; } }); var nodeHook, boolHook, attrHandle = jQuery.expr.attrHandle, ruseDefault = /^(?:checked|selected)$/i, getSetAttribute = support.getSetAttribute, getSetInput = support.input; jQuery.fn.extend({ attr: function( name, value ) { return access( this, jQuery.attr, name, value, arguments.length > 1 ); }, removeAttr: function( name ) { return this.each(function() { jQuery.removeAttr( this, name ); }); } }); jQuery.extend({ attr: function( elem, name, value ) { var hooks, ret, nType = elem.nodeType; // don't get/set attributes on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } // Fallback to prop when attributes are not supported if ( typeof elem.getAttribute === strundefined ) { return jQuery.prop( elem, name, value ); } // All attributes are lowercase // Grab necessary hook if one is defined if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { name = name.toLowerCase(); hooks = jQuery.attrHooks[ name ] || ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { elem.setAttribute( name, value + "" ); return value; } } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { ret = jQuery.find.attr( elem, name ); // Non-existent attributes return null, we normalize to undefined return ret == null ? undefined : ret; } }, removeAttr: function( elem, value ) { var name, propName, i = 0, attrNames = value && value.match( rnotwhite ); if ( attrNames && elem.nodeType === 1 ) { while ( (name = attrNames[i++]) ) { propName = jQuery.propFix[ name ] || name; // Boolean attributes get special treatment (#10870) if ( jQuery.expr.match.bool.test( name ) ) { // Set corresponding property to false if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { elem[ propName ] = false; // Support: IE<9 // Also clear defaultChecked/defaultSelected (if appropriate) } else { elem[ jQuery.camelCase( "default-" + name ) ] = elem[ propName ] = false; } // See #9699 for explanation of this approach (setting first, then removal) } else { jQuery.attr( elem, name, "" ); } elem.removeAttribute( getSetAttribute ? name : propName ); } } }, attrHooks: { type: { set: function( elem, value ) { if ( !support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { // Setting the type on a radio button after the value resets the value in IE6-9 // Reset value to default in case type is set after value during creation var val = elem.value; elem.setAttribute( "type", value ); if ( val ) { elem.value = val; } return value; } } } } }); // Hook for boolean attributes boolHook = { set: function( elem, value, name ) { if ( value === false ) { // Remove boolean attributes when set to false jQuery.removeAttr( elem, name ); } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { // IE<8 needs the *property* name elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); // Use defaultChecked and defaultSelected for oldIE } else { elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; } return name; } }; // Retrieve booleans specially jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { var getter = attrHandle[ name ] || jQuery.find.attr; attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? function( elem, name, isXML ) { var ret, handle; if ( !isXML ) { // Avoid an infinite loop by temporarily removing this function from the getter handle = attrHandle[ name ]; attrHandle[ name ] = ret; ret = getter( elem, name, isXML ) != null ? name.toLowerCase() : null; attrHandle[ name ] = handle; } return ret; } : function( elem, name, isXML ) { if ( !isXML ) { return elem[ jQuery.camelCase( "default-" + name ) ] ? name.toLowerCase() : null; } }; }); // fix oldIE attroperties if ( !getSetInput || !getSetAttribute ) { jQuery.attrHooks.value = { set: function( elem, value, name ) { if ( jQuery.nodeName( elem, "input" ) ) { // Does not return so that setAttribute is also used elem.defaultValue = value; } else { // Use nodeHook if defined (#1954); otherwise setAttribute is fine return nodeHook && nodeHook.set( elem, value, name ); } } }; } // IE6/7 do not support getting/setting some attributes with get/setAttribute if ( !getSetAttribute ) { // Use this for any attribute in IE6/7 // This fixes almost every IE6/7 issue nodeHook = { set: function( elem, value, name ) { // Set the existing or create a new attribute node var ret = elem.getAttributeNode( name ); if ( !ret ) { elem.setAttributeNode( (ret = elem.ownerDocument.createAttribute( name )) ); } ret.value = value += ""; // Break association with cloned elements by also using setAttribute (#9646) if ( name === "value" || value === elem.getAttribute( name ) ) { return value; } } }; // Some attributes are constructed with empty-string values when not defined attrHandle.id = attrHandle.name = attrHandle.coords = function( elem, name, isXML ) { var ret; if ( !isXML ) { return (ret = elem.getAttributeNode( name )) && ret.value !== "" ? ret.value : null; } }; // Fixing value retrieval on a button requires this module jQuery.valHooks.button = { get: function( elem, name ) { var ret = elem.getAttributeNode( name ); if ( ret && ret.specified ) { return ret.value; } }, set: nodeHook.set }; // Set contenteditable to false on removals(#10429) // Setting to empty string throws an error as an invalid value jQuery.attrHooks.contenteditable = { set: function( elem, value, name ) { nodeHook.set( elem, value === "" ? false : value, name ); } }; // Set width and height to auto instead of 0 on empty string( Bug #8150 ) // This is for removals jQuery.each([ "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = { set: function( elem, value ) { if ( value === "" ) { elem.setAttribute( name, "auto" ); return value; } } }; }); } if ( !support.style ) { jQuery.attrHooks.style = { get: function( elem ) { // Return undefined in the case of empty string // Note: IE uppercases css property names, but if we were to .toLowerCase() // .cssText, that would destroy case senstitivity in URL's, like in "background" return elem.style.cssText || undefined; }, set: function( elem, value ) { return ( elem.style.cssText = value + "" ); } }; } var rfocusable = /^(?:input|select|textarea|button|object)$/i, rclickable = /^(?:a|area)$/i; jQuery.fn.extend({ prop: function( name, value ) { return access( this, jQuery.prop, name, value, arguments.length > 1 ); }, removeProp: function( name ) { name = jQuery.propFix[ name ] || name; return this.each(function() { // try/catch handles cases where IE balks (such as removing a property on window) try { this[ name ] = undefined; delete this[ name ]; } catch( e ) {} }); } }); jQuery.extend({ propFix: { "for": "htmlFor", "class": "className" }, prop: function( elem, name, value ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set properties on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); if ( notxml ) { // Fix name and attach hooks name = jQuery.propFix[ name ] || name; hooks = jQuery.propHooks[ name ]; } if ( value !== undefined ) { return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? ret : ( elem[ name ] = value ); } else { return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? ret : elem[ name ]; } }, propHooks: { tabIndex: { get: function( elem ) { // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ // Use proper attribute retrieval(#12072) var tabindex = jQuery.find.attr( elem, "tabindex" ); return tabindex ? parseInt( tabindex, 10 ) : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : -1; } } } }); // Some attributes require a special call on IE // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx if ( !support.hrefNormalized ) { // href/src property should get the full normalized URL (#10299/#12915) jQuery.each([ "href", "src" ], function( i, name ) { jQuery.propHooks[ name ] = { get: function( elem ) { return elem.getAttribute( name, 4 ); } }; }); } // Support: Safari, IE9+ // mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it if ( !support.optSelected ) { jQuery.propHooks.selected = { get: function( elem ) { var parent = elem.parentNode; if ( parent ) { parent.selectedIndex; // Make sure that it also works with optgroups, see #5701 if ( parent.parentNode ) { parent.parentNode.selectedIndex; } } return null; } }; } jQuery.each([ "tabIndex", "readOnly", "maxLength", "cellSpacing", "cellPadding", "rowSpan", "colSpan", "useMap", "frameBorder", "contentEditable" ], function() { jQuery.propFix[ this.toLowerCase() ] = this; }); // IE6/7 call enctype encoding if ( !support.enctype ) { jQuery.propFix.enctype = "encoding"; } var rclass = /[\t\r\n\f]/g; jQuery.fn.extend({ addClass: function( value ) { var classes, elem, cur, clazz, j, finalValue, i = 0, len = this.length, proceed = typeof value === "string" && value; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).addClass( value.call( this, j, this.className ) ); }); } if ( proceed ) { // The disjunction here is for better compressibility (see removeClass) classes = ( value || "" ).match( rnotwhite ) || []; for ( ; i < len; i++ ) { elem = this[ i ]; cur = elem.nodeType === 1 && ( elem.className ? ( " " + elem.className + " " ).replace( rclass, " " ) : " " ); if ( cur ) { j = 0; while ( (clazz = classes[j++]) ) { if ( cur.indexOf( " " + clazz + " " ) < 0 ) { cur += clazz + " "; } } // only assign if different to avoid unneeded rendering. finalValue = jQuery.trim( cur ); if ( elem.className !== finalValue ) { elem.className = finalValue; } } } } return this; }, removeClass: function( value ) { var classes, elem, cur, clazz, j, finalValue, i = 0, len = this.length, proceed = arguments.length === 0 || typeof value === "string" && value; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).removeClass( value.call( this, j, this.className ) ); }); } if ( proceed ) { classes = ( value || "" ).match( rnotwhite ) || []; for ( ; i < len; i++ ) { elem = this[ i ]; // This expression is here for better compressibility (see addClass) cur = elem.nodeType === 1 && ( elem.className ? ( " " + elem.className + " " ).replace( rclass, " " ) : "" ); if ( cur ) { j = 0; while ( (clazz = classes[j++]) ) { // Remove *all* instances while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { cur = cur.replace( " " + clazz + " ", " " ); } } // only assign if different to avoid unneeded rendering. finalValue = value ? jQuery.trim( cur ) : ""; if ( elem.className !== finalValue ) { elem.className = finalValue; } } } } return this; }, toggleClass: function( value, stateVal ) { var type = typeof value; if ( typeof stateVal === "boolean" && type === "string" ) { return stateVal ? this.addClass( value ) : this.removeClass( value ); } if ( jQuery.isFunction( value ) ) { return this.each(function( i ) { jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names var className, i = 0, self = jQuery( this ), classNames = value.match( rnotwhite ) || []; while ( (className = classNames[ i++ ]) ) { // check each className given, space separated list if ( self.hasClass( className ) ) { self.removeClass( className ); } else { self.addClass( className ); } } // Toggle whole class name } else if ( type === strundefined || type === "boolean" ) { if ( this.className ) { // store className if set jQuery._data( this, "__className__", this.className ); } // If the element has a class name or if we're passed "false", // then remove the whole classname (if there was one, the above saved it). // Otherwise bring back whatever was previously saved (if anything), // falling back to the empty string if nothing was stored. this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { var className = " " + selector + " ", i = 0, l = this.length; for ( ; i < l; i++ ) { if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { return true; } } return false; } }); // Return jQuery for attributes-only inclusion jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { return arguments.length > 0 ? this.on( name, null, data, fn ) : this.trigger( name ); }; }); jQuery.fn.extend({ hover: function( fnOver, fnOut ) { return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); }, bind: function( types, data, fn ) { return this.on( types, null, data, fn ); }, unbind: function( types, fn ) { return this.off( types, null, fn ); }, delegate: function( selector, types, data, fn ) { return this.on( types, selector, data, fn ); }, undelegate: function( selector, types, fn ) { // ( namespace ) or ( selector, types [, fn] ) return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); } }); var nonce = jQuery.now(); var rquery = (/\?/); var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g; jQuery.parseJSON = function( data ) { // Attempt to parse using the native JSON parser first if ( window.JSON && window.JSON.parse ) { // Support: Android 2.3 // Workaround failure to string-cast null input return window.JSON.parse( data + "" ); } var requireNonComma, depth = null, str = jQuery.trim( data + "" ); // Guard against invalid (and possibly dangerous) input by ensuring that nothing remains // after removing valid tokens return str && !jQuery.trim( str.replace( rvalidtokens, function( token, comma, open, close ) { // Force termination if we see a misplaced comma if ( requireNonComma && comma ) { depth = 0; } // Perform no more replacements after returning to outermost depth if ( depth === 0 ) { return token; } // Commas must not follow "[", "{", or "," requireNonComma = open || comma; // Determine new depth // array/object open ("[" or "{"): depth += true - false (increment) // array/object close ("]" or "}"): depth += false - true (decrement) // other cases ("," or primitive): depth += true - true (numeric cast) depth += !close - !open; // Remove this token return ""; }) ) ? ( Function( "return " + str ) )() : jQuery.error( "Invalid JSON: " + data ); }; // Cross-browser xml parsing jQuery.parseXML = function( data ) { var xml, tmp; if ( !data || typeof data !== "string" ) { return null; } try { if ( window.DOMParser ) { // Standard tmp = new DOMParser(); xml = tmp.parseFromString( data, "text/xml" ); } else { // IE xml = new ActiveXObject( "Microsoft.XMLDOM" ); xml.async = "false"; xml.loadXML( data ); } } catch( e ) { xml = undefined; } if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + data ); } return xml; }; var // Document location ajaxLocParts, ajaxLocation, rhash = /#.*$/, rts = /([?&])_=[^&]*/, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL // #7653, #8125, #8152: local protocol detection rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, rnoContent = /^(?:GET|HEAD)$/, rprotocol = /^\/\//, rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/, /* Prefilters * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) * 2) These are called: * - BEFORE asking for a transport * - AFTER param serialization (s.data is a string if s.processData is true) * 3) key is the dataType * 4) the catchall symbol "*" can be used * 5) execution will start with transport dataType and THEN continue down to "*" if needed */ prefilters = {}, /* Transports bindings * 1) key is the dataType * 2) the catchall symbol "*" can be used * 3) selection will start with transport dataType and THEN go to "*" if needed */ transports = {}, // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression allTypes = "*/".concat("*"); // #8138, IE may throw an exception when accessing // a field from window.location if document.domain has been set try { ajaxLocation = location.href; } catch( e ) { // Use the href attribute of an A element // since IE will modify it given document.location ajaxLocation = document.createElement( "a" ); ajaxLocation.href = ""; ajaxLocation = ajaxLocation.href; } // Segment location into parts ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport function addToPrefiltersOrTransports( structure ) { // dataTypeExpression is optional and defaults to "*" return function( dataTypeExpression, func ) { if ( typeof dataTypeExpression !== "string" ) { func = dataTypeExpression; dataTypeExpression = "*"; } var dataType, i = 0, dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || []; if ( jQuery.isFunction( func ) ) { // For each dataType in the dataTypeExpression while ( (dataType = dataTypes[i++]) ) { // Prepend if requested if ( dataType.charAt( 0 ) === "+" ) { dataType = dataType.slice( 1 ) || "*"; (structure[ dataType ] = structure[ dataType ] || []).unshift( func ); // Otherwise append } else { (structure[ dataType ] = structure[ dataType ] || []).push( func ); } } } }; } // Base inspection function for prefilters and transports function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { var inspected = {}, seekingTransport = ( structure === transports ); function inspect( dataType ) { var selected; inspected[ dataType ] = true; jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { options.dataTypes.unshift( dataTypeOrTransport ); inspect( dataTypeOrTransport ); return false; } else if ( seekingTransport ) { return !( selected = dataTypeOrTransport ); } }); return selected; } return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); } // A special extend for ajax options // that takes "flat" options (not to be deep extended) // Fixes #9887 function ajaxExtend( target, src ) { var deep, key, flatOptions = jQuery.ajaxSettings.flatOptions || {}; for ( key in src ) { if ( src[ key ] !== undefined ) { ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ]; } } if ( deep ) { jQuery.extend( true, target, deep ); } return target; } /* Handles responses to an ajax request: * - finds the right dataType (mediates between content-type and expected dataType) * - returns the corresponding response */ function ajaxHandleResponses( s, jqXHR, responses ) { var firstDataType, ct, finalDataType, type, contents = s.contents, dataTypes = s.dataTypes; // Remove auto dataType and get content-type in the process while ( dataTypes[ 0 ] === "*" ) { dataTypes.shift(); if ( ct === undefined ) { ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); } } // Check if we're dealing with a known content-type if ( ct ) { for ( type in contents ) { if ( contents[ type ] && contents[ type ].test( ct ) ) { dataTypes.unshift( type ); break; } } } // Check to see if we have a response for the expected dataType if ( dataTypes[ 0 ] in responses ) { finalDataType = dataTypes[ 0 ]; } else { // Try convertible dataTypes for ( type in responses ) { if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { finalDataType = type; break; } if ( !firstDataType ) { firstDataType = type; } } // Or just use first one finalDataType = finalDataType || firstDataType; } // If we found a dataType // We add the dataType to the list if needed // and return the corresponding response if ( finalDataType ) { if ( finalDataType !== dataTypes[ 0 ] ) { dataTypes.unshift( finalDataType ); } return responses[ finalDataType ]; } } /* Chain conversions given the request and the original response * Also sets the responseXXX fields on the jqXHR instance */ function ajaxConvert( s, response, jqXHR, isSuccess ) { var conv2, current, conv, tmp, prev, converters = {}, // Work with a copy of dataTypes in case we need to modify it for conversion dataTypes = s.dataTypes.slice(); // Create converters map with lowercased keys if ( dataTypes[ 1 ] ) { for ( conv in s.converters ) { converters[ conv.toLowerCase() ] = s.converters[ conv ]; } } current = dataTypes.shift(); // Convert to each sequential dataType while ( current ) { if ( s.responseFields[ current ] ) { jqXHR[ s.responseFields[ current ] ] = response; } // Apply the dataFilter if provided if ( !prev && isSuccess && s.dataFilter ) { response = s.dataFilter( response, s.dataType ); } prev = current; current = dataTypes.shift(); if ( current ) { // There's only work to do if current dataType is non-auto if ( current === "*" ) { current = prev; // Convert response if prev dataType is non-auto and differs from current } else if ( prev !== "*" && prev !== current ) { // Seek a direct converter conv = converters[ prev + " " + current ] || converters[ "* " + current ]; // If none found, seek a pair if ( !conv ) { for ( conv2 in converters ) { // If conv2 outputs current tmp = conv2.split( " " ); if ( tmp[ 1 ] === current ) { // If prev can be converted to accepted input conv = converters[ prev + " " + tmp[ 0 ] ] || converters[ "* " + tmp[ 0 ] ]; if ( conv ) { // Condense equivalence converters if ( conv === true ) { conv = converters[ conv2 ]; // Otherwise, insert the intermediate dataType } else if ( converters[ conv2 ] !== true ) { current = tmp[ 0 ]; dataTypes.unshift( tmp[ 1 ] ); } break; } } } } // Apply converter (if not an equivalence) if ( conv !== true ) { // Unless errors are allowed to bubble, catch and return them if ( conv && s[ "throws" ] ) { response = conv( response ); } else { try { response = conv( response ); } catch ( e ) { return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; } } } } } } return { state: "success", data: response }; } jQuery.extend({ // Counter for holding the number of active queries active: 0, // Last-Modified header cache for next request lastModified: {}, etag: {}, ajaxSettings: { url: ajaxLocation, type: "GET", isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), global: true, processData: true, async: true, contentType: "application/x-www-form-urlencoded; charset=UTF-8", /* timeout: 0, data: null, dataType: null, username: null, password: null, cache: null, throws: false, traditional: false, headers: {}, */ accepts: { "*": allTypes, text: "text/plain", html: "text/html", xml: "application/xml, text/xml", json: "application/json, text/javascript" }, contents: { xml: /xml/, html: /html/, json: /json/ }, responseFields: { xml: "responseXML", text: "responseText", json: "responseJSON" }, // Data converters // Keys separate source (or catchall "*") and destination types with a single space converters: { // Convert anything to text "* text": String, // Text to html (true = no transformation) "text html": true, // Evaluate text as a json expression "text json": jQuery.parseJSON, // Parse text as xml "text xml": jQuery.parseXML }, // For options that shouldn't be deep extended: // you can add your own custom options here if // and when you create one that shouldn't be // deep extended (see ajaxExtend) flatOptions: { url: true, context: true } }, // Creates a full fledged settings object into target // with both ajaxSettings and settings fields. // If target is omitted, writes into ajaxSettings. ajaxSetup: function( target, settings ) { return settings ? // Building a settings object ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : // Extending ajaxSettings ajaxExtend( jQuery.ajaxSettings, target ); }, ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), ajaxTransport: addToPrefiltersOrTransports( transports ), // Main method ajax: function( url, options ) { // If url is an object, simulate pre-1.5 signature if ( typeof url === "object" ) { options = url; url = undefined; } // Force options to be an object options = options || {}; var // Cross-domain detection vars parts, // Loop variable i, // URL without anti-cache param cacheURL, // Response headers as string responseHeadersString, // timeout handle timeoutTimer, // To know if global events are to be dispatched fireGlobals, transport, // Response headers responseHeaders, // Create the final options object s = jQuery.ajaxSetup( {}, options ), // Callbacks context callbackContext = s.context || s, // Context for global events is callbackContext if it is a DOM node or jQuery collection globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? jQuery( callbackContext ) : jQuery.event, // Deferreds deferred = jQuery.Deferred(), completeDeferred = jQuery.Callbacks("once memory"), // Status-dependent callbacks statusCode = s.statusCode || {}, // Headers (they are sent all at once) requestHeaders = {}, requestHeadersNames = {}, // The jqXHR state state = 0, // Default abort message strAbort = "canceled", // Fake xhr jqXHR = { readyState: 0, // Builds headers hashtable if needed getResponseHeader: function( key ) { var match; if ( state === 2 ) { if ( !responseHeaders ) { responseHeaders = {}; while ( (match = rheaders.exec( responseHeadersString )) ) { responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; } } match = responseHeaders[ key.toLowerCase() ]; } return match == null ? null : match; }, // Raw string getAllResponseHeaders: function() { return state === 2 ? responseHeadersString : null; }, // Caches the header setRequestHeader: function( name, value ) { var lname = name.toLowerCase(); if ( !state ) { name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; requestHeaders[ name ] = value; } return this; }, // Overrides response content-type header overrideMimeType: function( type ) { if ( !state ) { s.mimeType = type; } return this; }, // Status-dependent callbacks statusCode: function( map ) { var code; if ( map ) { if ( state < 2 ) { for ( code in map ) { // Lazy-add the new callback in a way that preserves old ones statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; } } else { // Execute the appropriate callbacks jqXHR.always( map[ jqXHR.status ] ); } } return this; }, // Cancel the request abort: function( statusText ) { var finalText = statusText || strAbort; if ( transport ) { transport.abort( finalText ); } done( 0, finalText ); return this; } }; // Attach deferreds deferred.promise( jqXHR ).complete = completeDeferred.add; jqXHR.success = jqXHR.done; jqXHR.error = jqXHR.fail; // Remove hash character (#7531: and string promotion) // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) // Handle falsy url in the settings object (#10093: consistency with old signature) // We also use the url parameter if available s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); // Alias method option to type as per ticket #12004 s.type = options.method || options.type || s.method || s.type; // Extract dataTypes list s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ]; // A cross-domain request is in order when we have a protocol:host:port mismatch if ( s.crossDomain == null ) { parts = rurl.exec( s.url.toLowerCase() ); s.crossDomain = !!( parts && ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !== ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) ) ); } // Convert data if not already a string if ( s.data && s.processData && typeof s.data !== "string" ) { s.data = jQuery.param( s.data, s.traditional ); } // Apply prefilters inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); // If request was aborted inside a prefilter, stop there if ( state === 2 ) { return jqXHR; } // We can fire global events as of now if asked to fireGlobals = s.global; // Watch for a new set of requests if ( fireGlobals && jQuery.active++ === 0 ) { jQuery.event.trigger("ajaxStart"); } // Uppercase the type s.type = s.type.toUpperCase(); // Determine if request has content s.hasContent = !rnoContent.test( s.type ); // Save the URL in case we're toying with the If-Modified-Since // and/or If-None-Match header later on cacheURL = s.url; // More options handling for requests with no content if ( !s.hasContent ) { // If data is available, append data to url if ( s.data ) { cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); // #9682: remove data so that it's not used in an eventual retry delete s.data; } // Add anti-cache in url if needed if ( s.cache === false ) { s.url = rts.test( cacheURL ) ? // If there is already a '_' parameter, set its value cacheURL.replace( rts, "$1_=" + nonce++ ) : // Otherwise add one to the end cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++; } } // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { if ( jQuery.lastModified[ cacheURL ] ) { jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); } if ( jQuery.etag[ cacheURL ] ) { jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); } } // Set the correct header, if data is being sent if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { jqXHR.setRequestHeader( "Content-Type", s.contentType ); } // Set the Accepts header for the server, depending on the dataType jqXHR.setRequestHeader( "Accept", s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : s.accepts[ "*" ] ); // Check for headers option for ( i in s.headers ) { jqXHR.setRequestHeader( i, s.headers[ i ] ); } // Allow custom headers/mimetypes and early abort if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { // Abort if not done already and return return jqXHR.abort(); } // aborting is no longer a cancellation strAbort = "abort"; // Install callbacks on deferreds for ( i in { success: 1, error: 1, complete: 1 } ) { jqXHR[ i ]( s[ i ] ); } // Get transport transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); // If no transport, we auto-abort if ( !transport ) { done( -1, "No Transport" ); } else { jqXHR.readyState = 1; // Send global event if ( fireGlobals ) { globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); } // Timeout if ( s.async && s.timeout > 0 ) { timeoutTimer = setTimeout(function() { jqXHR.abort("timeout"); }, s.timeout ); } try { state = 1; transport.send( requestHeaders, done ); } catch ( e ) { // Propagate exception as error if not done if ( state < 2 ) { done( -1, e ); // Simply rethrow otherwise } else { throw e; } } } // Callback for when everything is done function done( status, nativeStatusText, responses, headers ) { var isSuccess, success, error, response, modified, statusText = nativeStatusText; // Called once if ( state === 2 ) { return; } // State is "done" now state = 2; // Clear timeout if it exists if ( timeoutTimer ) { clearTimeout( timeoutTimer ); } // Dereference transport for early garbage collection // (no matter how long the jqXHR object will be used) transport = undefined; // Cache response headers responseHeadersString = headers || ""; // Set readyState jqXHR.readyState = status > 0 ? 4 : 0; // Determine if successful isSuccess = status >= 200 && status < 300 || status === 304; // Get response data if ( responses ) { response = ajaxHandleResponses( s, jqXHR, responses ); } // Convert no matter what (that way responseXXX fields are always set) response = ajaxConvert( s, response, jqXHR, isSuccess ); // If successful, handle type chaining if ( isSuccess ) { // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { modified = jqXHR.getResponseHeader("Last-Modified"); if ( modified ) { jQuery.lastModified[ cacheURL ] = modified; } modified = jqXHR.getResponseHeader("etag"); if ( modified ) { jQuery.etag[ cacheURL ] = modified; } } // if no content if ( status === 204 || s.type === "HEAD" ) { statusText = "nocontent"; // if not modified } else if ( status === 304 ) { statusText = "notmodified"; // If we have data, let's convert it } else { statusText = response.state; success = response.data; error = response.error; isSuccess = !error; } } else { // We extract error from statusText // then normalize statusText and status for non-aborts error = statusText; if ( status || !statusText ) { statusText = "error"; if ( status < 0 ) { status = 0; } } } // Set data for the fake xhr object jqXHR.status = status; jqXHR.statusText = ( nativeStatusText || statusText ) + ""; // Success/Error if ( isSuccess ) { deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); } else { deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); } // Status-dependent callbacks jqXHR.statusCode( statusCode ); statusCode = undefined; if ( fireGlobals ) { globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", [ jqXHR, s, isSuccess ? success : error ] ); } // Complete completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); if ( fireGlobals ) { globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); // Handle the global AJAX counter if ( !( --jQuery.active ) ) { jQuery.event.trigger("ajaxStop"); } } } return jqXHR; }, getJSON: function( url, data, callback ) { return jQuery.get( url, data, callback, "json" ); }, getScript: function( url, callback ) { return jQuery.get( url, undefined, callback, "script" ); } }); jQuery.each( [ "get", "post" ], function( i, method ) { jQuery[ method ] = function( url, data, callback, type ) { // shift arguments if data argument was omitted if ( jQuery.isFunction( data ) ) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ url: url, type: method, dataType: type, data: data, success: callback }); }; }); // Attach a bunch of functions for handling common AJAX events jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) { jQuery.fn[ type ] = function( fn ) { return this.on( type, fn ); }; }); jQuery._evalUrl = function( url ) { return jQuery.ajax({ url: url, type: "GET", dataType: "script", async: false, global: false, "throws": true }); }; jQuery.fn.extend({ wrapAll: function( html ) { if ( jQuery.isFunction( html ) ) { return this.each(function(i) { jQuery(this).wrapAll( html.call(this, i) ); }); } if ( this[0] ) { // The elements to wrap the target around var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); if ( this[0].parentNode ) { wrap.insertBefore( this[0] ); } wrap.map(function() { var elem = this; while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { elem = elem.firstChild; } return elem; }).append( this ); } return this; }, wrapInner: function( html ) { if ( jQuery.isFunction( html ) ) { return this.each(function(i) { jQuery(this).wrapInner( html.call(this, i) ); }); } return this.each(function() { var self = jQuery( this ), contents = self.contents(); if ( contents.length ) { contents.wrapAll( html ); } else { self.append( html ); } }); }, wrap: function( html ) { var isFunction = jQuery.isFunction( html ); return this.each(function(i) { jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); }); }, unwrap: function() { return this.parent().each(function() { if ( !jQuery.nodeName( this, "body" ) ) { jQuery( this ).replaceWith( this.childNodes ); } }).end(); } }); jQuery.expr.filters.hidden = function( elem ) { // Support: Opera <= 12.12 // Opera reports offsetWidths and offsetHeights less than zero on some elements return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 || (!support.reliableHiddenOffsets() && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); }; jQuery.expr.filters.visible = function( elem ) { return !jQuery.expr.filters.hidden( elem ); }; var r20 = /%20/g, rbracket = /\[\]$/, rCRLF = /\r?\n/g, rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, rsubmittable = /^(?:input|select|textarea|keygen)/i; function buildParams( prefix, obj, traditional, add ) { var name; if ( jQuery.isArray( obj ) ) { // Serialize array item. jQuery.each( obj, function( i, v ) { if ( traditional || rbracket.test( prefix ) ) { // Treat each array item as a scalar. add( prefix, v ); } else { // Item is non-scalar (array or object), encode its numeric index. buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); } }); } else if ( !traditional && jQuery.type( obj ) === "object" ) { // Serialize object item. for ( name in obj ) { buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); } } else { // Serialize scalar item. add( prefix, obj ); } } // Serialize an array of form elements or a set of // key/values into a query string jQuery.param = function( a, traditional ) { var prefix, s = [], add = function( key, value ) { // If value is a function, invoke it and return its value value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); }; // Set traditional to true for jQuery <= 1.3.2 behavior. if ( traditional === undefined ) { traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; } // If an array was passed in, assume that it is an array of form elements. if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { // Serialize the form elements jQuery.each( a, function() { add( this.name, this.value ); }); } else { // If traditional, encode the "old" way (the way 1.3.2 or older // did it), otherwise encode params recursively. for ( prefix in a ) { buildParams( prefix, a[ prefix ], traditional, add ); } } // Return the resulting serialization return s.join( "&" ).replace( r20, "+" ); }; jQuery.fn.extend({ serialize: function() { return jQuery.param( this.serializeArray() ); }, serializeArray: function() { return this.map(function() { // Can add propHook for "elements" to filter or add form elements var elements = jQuery.prop( this, "elements" ); return elements ? jQuery.makeArray( elements ) : this; }) .filter(function() { var type = this.type; // Use .is(":disabled") so that fieldset[disabled] works return this.name && !jQuery( this ).is( ":disabled" ) && rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && ( this.checked || !rcheckableType.test( type ) ); }) .map(function( i, elem ) { var val = jQuery( this ).val(); return val == null ? null : jQuery.isArray( val ) ? jQuery.map( val, function( val ) { return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }) : { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }).get(); } }); // Create the request object // (This is still attached to ajaxSettings for backward compatibility) jQuery.ajaxSettings.xhr = window.ActiveXObject !== undefined ? // Support: IE6+ function() { // XHR cannot access local files, always use ActiveX for that case return !this.isLocal && // Support: IE7-8 // oldIE XHR does not support non-RFC2616 methods (#13240) // See http://msdn.microsoft.com/en-us/library/ie/ms536648(v=vs.85).aspx // and http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9 // Although this check for six methods instead of eight // since IE also does not support "trace" and "connect" /^(get|post|head|put|delete|options)$/i.test( this.type ) && createStandardXHR() || createActiveXHR(); } : // For all other browsers, use the standard XMLHttpRequest object createStandardXHR; var xhrId = 0, xhrCallbacks = {}, xhrSupported = jQuery.ajaxSettings.xhr(); // Support: IE<10 // Open requests must be manually aborted on unload (#5280) if ( window.ActiveXObject ) { jQuery( window ).on( "unload", function() { for ( var key in xhrCallbacks ) { xhrCallbacks[ key ]( undefined, true ); } }); } // Determine support properties support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); xhrSupported = support.ajax = !!xhrSupported; // Create transport if the browser can provide an xhr if ( xhrSupported ) { jQuery.ajaxTransport(function( options ) { // Cross domain only allowed if supported through XMLHttpRequest if ( !options.crossDomain || support.cors ) { var callback; return { send: function( headers, complete ) { var i, xhr = options.xhr(), id = ++xhrId; // Open the socket xhr.open( options.type, options.url, options.async, options.username, options.password ); // Apply custom fields if provided if ( options.xhrFields ) { for ( i in options.xhrFields ) { xhr[ i ] = options.xhrFields[ i ]; } } // Override mime type if needed if ( options.mimeType && xhr.overrideMimeType ) { xhr.overrideMimeType( options.mimeType ); } // X-Requested-With header // For cross-domain requests, seeing as conditions for a preflight are // akin to a jigsaw puzzle, we simply never set it to be sure. // (it can always be set on a per-request basis or even using ajaxSetup) // For same-domain requests, won't change header if already provided. if ( !options.crossDomain && !headers["X-Requested-With"] ) { headers["X-Requested-With"] = "XMLHttpRequest"; } // Set headers for ( i in headers ) { // Support: IE<9 // IE's ActiveXObject throws a 'Type Mismatch' exception when setting // request header to a null-value. // // To keep consistent with other XHR implementations, cast the value // to string and ignore `undefined`. if ( headers[ i ] !== undefined ) { xhr.setRequestHeader( i, headers[ i ] + "" ); } } // Do send the request // This may raise an exception which is actually // handled in jQuery.ajax (so no try/catch here) xhr.send( ( options.hasContent && options.data ) || null ); // Listener callback = function( _, isAbort ) { var status, statusText, responses; // Was never called and is aborted or complete if ( callback && ( isAbort || xhr.readyState === 4 ) ) { // Clean up delete xhrCallbacks[ id ]; callback = undefined; xhr.onreadystatechange = jQuery.noop; // Abort manually if needed if ( isAbort ) { if ( xhr.readyState !== 4 ) { xhr.abort(); } } else { responses = {}; status = xhr.status; // Support: IE<10 // Accessing binary-data responseText throws an exception // (#11426) if ( typeof xhr.responseText === "string" ) { responses.text = xhr.responseText; } // Firefox throws an exception when accessing // statusText for faulty cross-domain requests try { statusText = xhr.statusText; } catch( e ) { // We normalize with Webkit giving an empty statusText statusText = ""; } // Filter status for non standard behaviors // If the request is local and we have data: assume a success // (success with no data won't get notified, that's the best we // can do given current implementations) if ( !status && options.isLocal && !options.crossDomain ) { status = responses.text ? 200 : 404; // IE - #1450: sometimes returns 1223 when it should be 204 } else if ( status === 1223 ) { status = 204; } } } // Call complete if needed if ( responses ) { complete( status, statusText, responses, xhr.getAllResponseHeaders() ); } }; if ( !options.async ) { // if we're in sync mode we fire the callback callback(); } else if ( xhr.readyState === 4 ) { // (IE6 & IE7) if it's in cache and has been // retrieved directly we need to fire the callback setTimeout( callback ); } else { // Add to the list of active xhr callbacks xhr.onreadystatechange = xhrCallbacks[ id ] = callback; } }, abort: function() { if ( callback ) { callback( undefined, true ); } } }; } }); } // Functions to create xhrs function createStandardXHR() { try { return new window.XMLHttpRequest(); } catch( e ) {} } function createActiveXHR() { try { return new window.ActiveXObject( "Microsoft.XMLHTTP" ); } catch( e ) {} } // Install script dataType jQuery.ajaxSetup({ accepts: { script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { script: /(?:java|ecma)script/ }, converters: { "text script": function( text ) { jQuery.globalEval( text ); return text; } } }); // Handle cache's special case and global jQuery.ajaxPrefilter( "script", function( s ) { if ( s.cache === undefined ) { s.cache = false; } if ( s.crossDomain ) { s.type = "GET"; s.global = false; } }); // Bind script tag hack transport jQuery.ajaxTransport( "script", function(s) { // This transport only deals with cross domain requests if ( s.crossDomain ) { var script, head = document.head || jQuery("head")[0] || document.documentElement; return { send: function( _, callback ) { script = document.createElement("script"); script.async = true; if ( s.scriptCharset ) { script.charset = s.scriptCharset; } script.src = s.url; // Attach handlers for all browsers script.onload = script.onreadystatechange = function( _, isAbort ) { if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { // Handle memory leak in IE script.onload = script.onreadystatechange = null; // Remove the script if ( script.parentNode ) { script.parentNode.removeChild( script ); } // Dereference the script script = null; // Callback if not abort if ( !isAbort ) { callback( 200, "success" ); } } }; // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending // Use native DOM manipulation to avoid our domManip AJAX trickery head.insertBefore( script, head.firstChild ); }, abort: function() { if ( script ) { script.onload( undefined, true ); } } }; } }); var oldCallbacks = [], rjsonp = /(=)\?(?=&|$)|\?\?/; // Default jsonp settings jQuery.ajaxSetup({ jsonp: "callback", jsonpCallback: function() { var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); this[ callback ] = true; return callback; } }); // Detect, normalize options and install callbacks for jsonp requests jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { var callbackName, overwritten, responseContainer, jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? "url" : typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data" ); // Handle iff the expected data type is "jsonp" or we have a parameter to set if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { // Get callback name, remembering preexisting value associated with it callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback; // Insert callback into url or form data if ( jsonProp ) { s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); } else if ( s.jsonp !== false ) { s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; } // Use data converter to retrieve json after script execution s.converters["script json"] = function() { if ( !responseContainer ) { jQuery.error( callbackName + " was not called" ); } return responseContainer[ 0 ]; }; // force json dataType s.dataTypes[ 0 ] = "json"; // Install callback overwritten = window[ callbackName ]; window[ callbackName ] = function() { responseContainer = arguments; }; // Clean-up function (fires after converters) jqXHR.always(function() { // Restore preexisting value window[ callbackName ] = overwritten; // Save back as free if ( s[ callbackName ] ) { // make sure that re-using the options doesn't screw things around s.jsonpCallback = originalSettings.jsonpCallback; // save the callback name for future use oldCallbacks.push( callbackName ); } // Call if it was a function and we have a response if ( responseContainer && jQuery.isFunction( overwritten ) ) { overwritten( responseContainer[ 0 ] ); } responseContainer = overwritten = undefined; }); // Delegate to script return "script"; } }); // data: string of html // context (optional): If specified, the fragment will be created in this context, defaults to document // keepScripts (optional): If true, will include scripts passed in the html string jQuery.parseHTML = function( data, context, keepScripts ) { if ( !data || typeof data !== "string" ) { return null; } if ( typeof context === "boolean" ) { keepScripts = context; context = false; } context = context || document; var parsed = rsingleTag.exec( data ), scripts = !keepScripts && []; // Single tag if ( parsed ) { return [ context.createElement( parsed[1] ) ]; } parsed = jQuery.buildFragment( [ data ], context, scripts ); if ( scripts && scripts.length ) { jQuery( scripts ).remove(); } return jQuery.merge( [], parsed.childNodes ); }; // Keep a copy of the old load method var _load = jQuery.fn.load; /** * Load a url into a page */ jQuery.fn.load = function( url, params, callback ) { if ( typeof url !== "string" && _load ) { return _load.apply( this, arguments ); } var selector, response, type, self = this, off = url.indexOf(" "); if ( off >= 0 ) { selector = url.slice( off, url.length ); url = url.slice( 0, off ); } // If it's a function if ( jQuery.isFunction( params ) ) { // We assume that it's the callback callback = params; params = undefined; // Otherwise, build a param string } else if ( params && typeof params === "object" ) { type = "POST"; } // If we have elements to modify, make the request if ( self.length > 0 ) { jQuery.ajax({ url: url, // if "type" variable is undefined, then "GET" method will be used type: type, dataType: "html", data: params }).done(function( responseText ) { // Save response for use in complete callback response = arguments; self.html( selector ? // If a selector was specified, locate the right elements in a dummy div // Exclude scripts to avoid IE 'Permission Denied' errors jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : // Otherwise use the full result responseText ); }).complete( callback && function( jqXHR, status ) { self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); }); } return this; }; jQuery.expr.filters.animated = function( elem ) { return jQuery.grep(jQuery.timers, function( fn ) { return elem === fn.elem; }).length; }; var docElem = window.document.documentElement; /** * Gets a window from an element */ function getWindow( elem ) { return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 ? elem.defaultView || elem.parentWindow : false; } jQuery.offset = { setOffset: function( elem, options, i ) { var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, position = jQuery.css( elem, "position" ), curElem = jQuery( elem ), props = {}; // set position first, in-case top/left are set even on static elem if ( position === "static" ) { elem.style.position = "relative"; } curOffset = curElem.offset(); curCSSTop = jQuery.css( elem, "top" ); curCSSLeft = jQuery.css( elem, "left" ); calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [ curCSSTop, curCSSLeft ] ) > -1; // need to be able to calculate position if either top or left is auto and position is either absolute or fixed if ( calculatePosition ) { curPosition = curElem.position(); curTop = curPosition.top; curLeft = curPosition.left; } else { curTop = parseFloat( curCSSTop ) || 0; curLeft = parseFloat( curCSSLeft ) || 0; } if ( jQuery.isFunction( options ) ) { options = options.call( elem, i, curOffset ); } if ( options.top != null ) { props.top = ( options.top - curOffset.top ) + curTop; } if ( options.left != null ) { props.left = ( options.left - curOffset.left ) + curLeft; } if ( "using" in options ) { options.using.call( elem, props ); } else { curElem.css( props ); } } }; jQuery.fn.extend({ offset: function( options ) { if ( arguments.length ) { return options === undefined ? this : this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); } var docElem, win, box = { top: 0, left: 0 }, elem = this[ 0 ], doc = elem && elem.ownerDocument; if ( !doc ) { return; } docElem = doc.documentElement; // Make sure it's not a disconnected DOM node if ( !jQuery.contains( docElem, elem ) ) { return box; } // If we don't have gBCR, just use 0,0 rather than error // BlackBerry 5, iOS 3 (original iPhone) if ( typeof elem.getBoundingClientRect !== strundefined ) { box = elem.getBoundingClientRect(); } win = getWindow( doc ); return { top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) }; }, position: function() { if ( !this[ 0 ] ) { return; } var offsetParent, offset, parentOffset = { top: 0, left: 0 }, elem = this[ 0 ]; // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent if ( jQuery.css( elem, "position" ) === "fixed" ) { // we assume that getBoundingClientRect is available when computed position is fixed offset = elem.getBoundingClientRect(); } else { // Get *real* offsetParent offsetParent = this.offsetParent(); // Get correct offsets offset = this.offset(); if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) { parentOffset = offsetParent.offset(); } // Add offsetParent borders parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ); } // Subtract parent offsets and element margins // note: when an element has margin: auto the offsetLeft and marginLeft // are the same in Safari causing offset.left to incorrectly be 0 return { top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true) }; }, offsetParent: function() { return this.map(function() { var offsetParent = this.offsetParent || docElem; while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) { offsetParent = offsetParent.offsetParent; } return offsetParent || docElem; }); } }); // Create scrollLeft and scrollTop methods jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { var top = /Y/.test( prop ); jQuery.fn[ method ] = function( val ) { return access( this, function( elem, method, val ) { var win = getWindow( elem ); if ( val === undefined ) { return win ? (prop in win) ? win[ prop ] : win.document.documentElement[ method ] : elem[ method ]; } if ( win ) { win.scrollTo( !top ? val : jQuery( win ).scrollLeft(), top ? val : jQuery( win ).scrollTop() ); } else { elem[ method ] = val; } }, method, val, arguments.length, null ); }; }); // Add the top/left cssHooks using jQuery.fn.position // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 // getComputedStyle returns percent when specified for top/left/bottom/right // rather than make the css module depend on the offset module, we just check for it here jQuery.each( [ "top", "left" ], function( i, prop ) { jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, function( elem, computed ) { if ( computed ) { computed = curCSS( elem, prop ); // if curCSS returns percentage, fallback to offset return rnumnonpx.test( computed ) ? jQuery( elem ).position()[ prop ] + "px" : computed; } } ); }); // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { // margin is only for outerHeight, outerWidth jQuery.fn[ funcName ] = function( margin, value ) { var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); return access( this, function( elem, type, value ) { var doc; if ( jQuery.isWindow( elem ) ) { // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there // isn't a whole lot we can do. See pull request at this URL for discussion: // https://github.com/jquery/jquery/pull/764 return elem.document.documentElement[ "client" + name ]; } // Get document width or height if ( elem.nodeType === 9 ) { doc = elem.documentElement; // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. return Math.max( elem.body[ "scroll" + name ], doc[ "scroll" + name ], elem.body[ "offset" + name ], doc[ "offset" + name ], doc[ "client" + name ] ); } return value === undefined ? // Get width or height on the element, requesting but not forcing parseFloat jQuery.css( elem, type, extra ) : // Set width or height on the element jQuery.style( elem, type, value, extra ); }, type, chainable ? margin : undefined, chainable, null ); }; }); }); // The number of elements contained in the matched element set jQuery.fn.size = function() { return this.length; }; jQuery.fn.andSelf = jQuery.fn.addBack; // Register as a named AMD module, since jQuery can be concatenated with other // files that may use define, but not via a proper concatenation script that // understands anonymous AMD modules. A named AMD is safest and most robust // way to register. Lowercase jquery is used because AMD module names are // derived from file names, and jQuery is normally delivered in a lowercase // file name. Do this after creating the global so that if an AMD module wants // to call noConflict to hide this version of jQuery, it will work. if ( typeof define === "function" && define.amd ) { define( "jquery", [], function() { return jQuery; }); } var // Map over jQuery in case of overwrite _jQuery = window.jQuery, // Map over the $ in case of overwrite _$ = window.$; jQuery.noConflict = function( deep ) { if ( window.$ === jQuery ) { window.$ = _$; } if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }; // Expose jQuery and $ identifiers, even in // AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557) // and CommonJS for browser emulators (#13566) if ( typeof noGlobal === strundefined ) { window.jQuery = window.$ = jQuery; } return jQuery; })); ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/thumb.thtml������������������������������������������������������������0000644�0001750�0001750�00000001217�12301071671�020211� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" class="media media_$media.type"> <a href="$media.link"><img class="media media_$media.type" src="$media.thumb" width="$media.thumb_width" height="$media.thumb_height" alt="$media.thumb_name thumb" /></a> <a py:if="media.type == 'video'" href="$media.link"><img class="video_arrow" src="${rel_root}shared/video_arrow.svg" alt="video arrow overlay" /><span class="video_length" py:content="media.length" /></a> </div> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/SHARED_video_arrow.svg�������������������������������������������������0000644�0001750�0001750�00000011305�12301071671�022106� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="478.2052" height="478.2052" id="svg2" version="1.1" inkscape:version="0.48.2 r9819" sodipodi:docname="video_arrow.svg"> <defs id="defs4"> <filter inkscape:collect="always" id="filter3799" color-interpolation-filters="sRGB"> <feGaussianBlur inkscape:collect="always" stdDeviation="10.227795" id="feGaussianBlur3801" /> </filter> <filter inkscape:collect="always" id="filter3828" color-interpolation-filters="sRGB"> <feGaussianBlur inkscape:collect="always" stdDeviation="4.1097545" id="feGaussianBlur3830" /> </filter> </defs> <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.98994949" inkscape:cx="106.34645" inkscape:cy="105.03316" inkscape:document-units="px" inkscape:current-layer="layer2" showgrid="false" inkscape:window-width="1920" inkscape:window-height="1061" inkscape:window-x="1596" inkscape:window-y="-4" inkscape:window-maximized="1" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" borderlayer="false" showborder="true" inkscape:showpageshadow="false" /> <metadata id="metadata7"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g inkscape:label="Circle" inkscape:groupmode="layer" id="layer1" transform="translate(-138.18936,-45.038574)"> <path sodipodi:type="arc" style="fill:none;stroke:#000000;stroke-width:20;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3799)" id="path3765" sodipodi:cx="377.29196" sodipodi:cy="284.14117" sodipodi:rx="204.55589" sodipodi:ry="204.55589" d="m 581.84785,284.14117 c 0,112.9731 -91.58279,204.5559 -204.55589,204.5559 -112.9731,0 -204.55589,-91.5828 -204.55589,-204.5559 0,-112.9731 91.58279,-204.555889 204.55589,-204.555889 112.9731,0 204.55589,91.582789 204.55589,204.555889 z" /> <path sodipodi:type="arc" style="opacity:0.26582277;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" id="path2985" sodipodi:cx="377.29196" sodipodi:cy="284.14117" sodipodi:rx="204.55589" sodipodi:ry="204.55589" d="m 581.84785,284.14117 c 0,112.9731 -91.58279,204.5559 -204.55589,204.5559 -112.9731,0 -204.55589,-91.5828 -204.55589,-204.5559 0,-112.9731 91.58279,-204.555889 204.55589,-204.555889 112.9731,0 204.55589,91.582789 204.55589,204.555889 z" /> <path d="m 581.84785,284.14117 c 0,112.9731 -91.58279,204.5559 -204.55589,204.5559 -112.9731,0 -204.55589,-91.5828 -204.55589,-204.5559 0,-112.9731 91.58279,-204.555889 204.55589,-204.555889 112.9731,0 204.55589,91.582789 204.55589,204.555889 z" sodipodi:ry="204.55589" sodipodi:rx="204.55589" sodipodi:cy="284.14117" sodipodi:cx="377.29196" id="path3763" style="fill:none;stroke:#ffffff;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" sodipodi:type="arc" /> </g> <g inkscape:groupmode="layer" id="layer2" inkscape:label="Arrow" transform="translate(-138.18936,-45.038574)"> <path inkscape:connector-curvature="0" id="path3826" d="m 313.79521,195.24775 0,177.78685 150.99351,-87.17615 z" style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3828)" /> <path style="fill:#ffffff;fill-opacity:1;stroke:none" d="m 313.79521,195.24775 0,177.78685 150.99351,-87.17615 z" id="path3805" inkscape:connector-curvature="0" /> </g> </svg> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/feeditem.thtml���������������������������������������������������������0000644�0001750�0001750�00000000531�12301071671�020652� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/"> <p py:if="desc" py:content="desc"></p> <p><img src="$album_pic_path" alt="album picture"/></p> <p> <py:if test="subgal_count > 0">$subgal_count ${_('sub-galleries')},</py:if> $picture_count ${_('photos')} </p> </div> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/SHARED_purple.css������������������������������������������������������0000644�0001750�0001750�00000003572�12301071671�021075� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������@import url("basic.css"); p{ text-align:center; } a{ text-decoration:none; color:purple; } #osize_links{ text-align:center; } #onum_links{ text-align:center; } h1{ display: none; } .inline_enum ul{ margin-left: 0; padding-left: 0; display: inline; } .inline_enum ul li { margin-left: 0; padding-left: 2px; border: none; list-style: none; display: inline; } #breadcrumbs { padding: 3px; margin-bottom: 25px; font-size: small; } #breadcrumbs ul li:after { content: "\0020 \0020 \0020 \00BB \0020"; } #breadcrumbs ul li.bc_current:after { content: " "; } #osize_links ul li:after { content: " |"; } #osize_links ul li:last-child:after { content: " "; } #onum_links ul li:after { content: " |"; } #onum_links ul li:last-child:after { content: " "; } div.subgal_link { float: left; padding: 1em; width: 50em; } div.subgal_image { float: left; margin-right: 2em; margin-left: 4em; width:180px; } div.subgal_image img { margin: 0; border: none; } div.subgal_description p{ text-align:justify; } div.subgal_stats{ font-size: x-small; } #subgal_links li { margin: 0.5em; } .media_links p{ text-align:justify; } .media_links{ padding-left:7em; padding-right:7em; } .prevnext_text { display: none; } #prev_link{ position:absolute; top:50%; left:0.1em; } #next_link{ position:absolute; top:50%; right:0.1em; } img { border:solid black 1px; margin:1em; } .image_caption li{ list-style-type:none; text-align:center; } .image_caption_tech{ display:none; } .footer{ padding-top:3em; clear:both; font-size:x-small; } .footer p{ text-align:justify; } #lazygalfooter{ padding-top:0em; margin-top:0.2em; border-top: solid black 1px; } /* * vim: ts=4 sw=4 expandtab */ ��������������������������������������������������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/image.thtml������������������������������������������������������������0000644�0001750�0001750�00000002641�12301071671�020156� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<div xmlns:py="http://genshi.edgewall.org/" id="image"> <div id="image_img"> <img src="$img_src" width="$img_width" height="$img_height" alt="Image $image_name" /> </div> <div id="image_caption"> <div class="image_comment" py:if="comment">$comment</div> <div class="image_date" py:if="image_datetime"> ${_('Taken')} ${image_datetime.strftime(_('on %d/%m/%Y at %H:%M'))} </div> <div class="authorship" py:if="authorship">${_('Author')} : $authorship</div> <div class="keywords" py:if="keywords">${_('Keywords')} : $keywords</div> <div py:if="original_link" class="original_link"><a href="$original_link">${_('Original picture')}</a></div> <div class="image_caption_tech"> <ul> <li>$image_name</li> <li py:if="camera_name">${_('Camera:')} $camera_name <py:if test="lens_name"> ${_('with')} $lens_name</py:if> </li> <li py:if="exposure">${_('Exposure')} $exposure</li> <li py:if="iso">${_('Sensitivity ISO')} $iso</li> <li py:if="fnumber">${_('Aperture')} $fnumber</li> <li py:if="flash">${_('Flash')} $flash</li> <li py:if="focal_length">${_('Focal length')} $focal_length</li> </ul> </div> </div> </div> <!--! vim: set fenc=utf-8 ts=4 sw=4 expandtab: --> �����������������������������������������������������������������������������������������������lazygal-0.8.2/themes/default/browse.thtml�����������������������������������������������������������0000644�0001750�0001750�00000004101�12301071671�020366� 0����������������������������������������������������������������������������������������������������ustar �niol����������������������������niol����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE HTML> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude"> <head> <title py:content="name">