pax_global_header00006660000000000000000000000064137600203200014503gustar00rootroot0000000000000052 comment=b746ca600b60098fa09d414a09a70a76fb198956 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/000077500000000000000000000000001376002032000206325ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/.gitignore000066400000000000000000000000711376002032000226200ustar00rootroot00000000000000cache/* workdir/* !.gitkeep coverage vendor composer.lockphp-arthurhoaro-web-thumbnailer-2.0.3+dfsg/.travis.yml000066400000000000000000000012521376002032000227430ustar00rootroot00000000000000matrix: include: - language: php php: nightly install: - composer self-update --2 - composer update --ignore-platform-req=php - language: php php: 7.4 - language: php php: 7.3 - language: php php: 7.2 - language: php php: 7.1 cache: directories: - $HOME/.composer/cache install: - composer self-update - composer install --prefer-dist script: - ./vendor/bin/phpcs src - ./vendor/bin/phpunit --bootstrap tests/bootstrap.php --configuration phpunit.xml.dist tests after_success: - travis_retry php vendor/bin/php-coveralls -v -x coverage/logs/clover.xml -o coverage/logs/coveralls-upload.json php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/CHANGELOG.md000066400000000000000000000043361376002032000224510ustar00rootroot00000000000000# Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## v1.3.1 - 2018-08-11 ### Removed * Remove PHP extension dependecies in `composer.json` to prevent docker multi stage build failure ## v1.3.0 - 2018-08-11 ### Added * Add a setting to force Apache version for htaccess syntax ### Fixed * PHPDocs improvements ### Removed * Parameter `PATH_TYPE` has been removed * WebThumbnailer no longer try to resolve relative path to thumbnails, it now relies on provided `path.cache` setting ## v1.2.1 - 2018-07-17 ### Fixed * Fix a issue where download_mode from JSON config has no effect ## v1.2.0 - 2018-06-30 ### Added * Path type parameter, to retrieve either a relative or an absolute path to the thumbnail cache file * `.htaccess` files are now created in cache folders (denied for `finder` and granted for `thumb`) ### Changed * The relative path to the thumbnail cache file is now retrieved using `SCRIPT_FILENAME`. ## v1.1.3 - 2018-06-13 ### Fixed * Fix an issue with thumbs path with Apache alias where DOCUMENT_ROOT is not set properly ## v1.1.2 - 2018-05-05 ### Added * Support redirection in cURL download callback ### Fixed * Fix an issue preventing the relative path to work properly in a subfolder * Decode HTML entities on thumb urls (e.g. &) * Fixed an issue where an empty cache folder where created ## v1.1.1 - 2018-05-01 ### Fixed * Fixed dev dependency ## v1.1.0 - 2018-05-01 > **Warning**: this release will invalidates existing cache. ### Added * An exception is now thrown if PHP extension requirements are not satisfied. * CI: - Coverall PR check added - Scrutinizer PR check added - PHP CodeSniffer PSR-2 syntax is now run along unit tests ### Changed * Image cache files are now stored as JPEG instead of PNG to save disk space. * Image cache domain folders are now stored using a hash instead of the raw domain name. ### Fixed * Make Github ignore HTML test files for language detection. ## v1.0.1 - 2017-11-27 First public release. ## v1.0.0 - 2017-11-27 First release.php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/LICENSE.md000066400000000000000000000020521376002032000222350ustar00rootroot00000000000000MIT LICENSE Copyright 2016 Arthur Hoareau Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/README.md000066400000000000000000000162701376002032000221170ustar00rootroot00000000000000# Web Thumbnailer ![](https://travis-ci.org/ArthurHoaro/web-thumbnailer.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/ArthurHoaro/web-thumbnailer/badge.svg?branch=master)](https://coveralls.io/github/ArthurHoaro/web-thumbnailer?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ArthurHoaro/web-thumbnailer/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ArthurHoaro/web-thumbnailer/?branch=master) PHP library which will retrieve a thumbnail for any given URL, if available. ## Features - Support various specific website features: Imgur, FlickR, Youtube, XKCD, etc. - Work with any website supporting [OpenGraph](http://ogp.me/) (tag meta `og:image`) - Or use direct link to images - Local cache - Resizing and/or cropping according to given settings ## Requirements Mandatory: - PHP 7.1 (`v2.0.0`+) - PHP 5.6 (`v1.x.x`) - PHP GD extension (Highly) Recommended: - PHP cURL extension: it let you retrieve thumbnails without downloading the whole remote page ## Installation Using [Composer](https://getcomposer.org/): ```bash composer require arthurhoaro/web-thumbnailer ``` ## Usage Using WebThumbnailer is pretty straight forward: ```php require_once 'vendor/autoload.php'; $wt = new WebThumbnailer(); // Very basic usage $thumb = $wt->thumbnail('https://github.com/ArthurHoaro'); // Using a bit of configuration $thumb2 = $wt->maxHeight(180) ->maxWidth(320) ->crop(true) ->thumbnail('https://github.com/ArthurHoaro'); echo ''; echo ''; // returns false $wt->thumbnail('bad url'); ``` Result: > ![](https://cloud.githubusercontent.com/assets/1962678/19929568/37f6b796-a104-11e6-85fc-b039eb64bd97.png) > ![](https://cloud.githubusercontent.com/assets/1962678/19929823/a26fde9e-a105-11e6-915c-ce0db1ffe6b0.png) ## Thumbnail configuration There are 2 ways to set up thumbnail configuration: * using `WebThumbnailer` helper functions as shown in *Usage* section. * passing an array of settings to `thumbnail()` while getting a thumbnail. This will override any setting setup with the helper functions. Example: ```php $conf = [ WebThumbnailer::MAX_HEIGHT => 320, WebThumbnailer::MAX_WIDTH => 320, WebThumbnailer::CROP => true ]; $wt->thumbnail('https://github.com/ArthurHoaro', $conf); ``` ### Download mode There are 3 download modes, only one can be used at once: * Download (default): it will download thumbnail, resize it and save it in the cache folder. * Hotlink: it will use [image hotlinking](https://en.wikipedia.org/wiki/Inline_linking) if the domain authorize it, download it otherwise. * Hotlink strict: it will use image hotlinking if the domain authorize it, fail otherwise. Usage: ```php // Download (default value) $wt = $wt->modeDownload(); $conf = [WebThumbnailer::DOWNLOAD]; // Hotlink $wt = $wt->modeHotlink(); $conf = [WebThumbnailer::HOTLINK]; // Hotlink Strict $wt = $wt->modeHotlinkStrict(); $conf = [WebThumbnailer::HOTLINK_STRICT]; ``` > **Warning**: hotlinking means that thumbnails won't be resized, and have to be downloaded as their original size. ### Image Size In download mode, thumbnails size can be defined using max width/height settings: * with max height and max width, the thumbnail will be resized to match the first reached limit. * with max height only, the thumbnail will be resized to the given height no matter its width. * with max width only, the thumbnail will be resized to the given width no matter its height. * if no size is provided, the default settings will apply (see Settings section). Usage: ```php // Sizes are given in number of pixels as an integer $wt = $wt->maxHeight(180); $conf = [WebThumbnailer::MAX_HEIGHT => 180]; $wt = $wt->maxWidth(320); $conf = [WebThumbnailer::MAX_WIDTH => 180]; ``` > **Bonus feature**: for websites which support an open API regarding their thumbnail size (e.g. Imgur, FlickR), WebThumbnailer makes sure to download the smallest image matching size requirements. ### Image Crop Image resizing might not be enough, and thumbnails might have to have a fixed size. This option will crop the image (from its center) to match given dimensions. > Note: max width AND height **must** be provided to use image crop. Usage: ```php // Sizes are given in number of pixels as an integer $wt = $wt->crop(true); $conf = [WebThumbnailer::CROP => true]; ``` ### Miscellaneous * **NOCACHE**: Force the thumbnail to be resolved and downloaded instead of using cache files. * **DEBUG**: Will throw an exception if an error occurred or if no thumbnail is found, instead of returning `false`. * **VERBOSE**: Will log an entry in error log if a thumbnail could not be retrieved. * **DOWNLOAD_TIMEOUT**: Override download timeout setting (in seconds). * **DOWNLOAD_MAX_SIZE**: Override download max size setting (in bytes). Usage: ```php $wt = $wt ->noCache(true) ->debug(true) ->verbose(true) ->downloadTimeout(30) ->downloadMaxSize(4194304) ; $conf = [ WebThumbnailer::NOCACHE => true, WebThumbnailer::DEBUG => true, WebThumbnailer::VERBOSE => true, WebThumbnailer::DOWNLOAD_TIMEOUT => 30, WebThumbnailer::DOWNLOAD_MAX_SIZE => 4194304, ]; ``` ## Settings Settings are stored in JSON, and can be overrode using a custom JSON file: ```php use WebThumbnailer\Application\ConfigManager; ConfigManager::addFile('conf/mysettings.json'); ``` Available settings: * `default`: * `download_mode`: default download mode (`DOWNLOAD`, `HOTLINK` or `HOTLINK_STRICT`). * `timeout`: default download timeout, in seconds. * `max_img_dl`: default download max size, in bytes. * `max_width`: default max width if no size requirement is provided. * `max_height`: default max height if no size requirement is provided. * `cache_duration`: cache validity duration, in seconds (use a negative value for infinite cache). * `path`: * `cache`: cache path. * `apache_version`: force `.htaccess` syntax depending on Apache's version, otherwise it uses `mod_version` (allowed values: `2.2` or `2.4`). ## Thumbnails path In download mode, the path to the thumbnail returned by WebThumbnailer library will depend on what's provided to the `path.cache` setting. If an absolute path is set, thumbnails will be attached to an absolute path, same for relative. Relative path will depend on the entry point of the execution. For example, if your entry point for all request is an `index.php` file in your project root directory, the default `cache/` setting will create a `cache/` folder in the root directory. Another example, for Symfony, the cache folder will be relative to the `web/` directory, which is the entry point with `app.php`. If you don't have a single entry point in your project folder structure, you should provide an absolute path and process the path yourself. ## Contributing WebThumbnailer can easily support more website by adding new rules in `rules.json` using one of the default Finders, or by writing a new Finder for specific cases. Please report any issue you might encounter. Also, feel free to correct any horrible English mistake I may have made in this README. ## License MIT license, see LICENSE.md php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/cache/000077500000000000000000000000001376002032000216755ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/cache/.gitkeep000066400000000000000000000000001376002032000233140ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/composer.json000066400000000000000000000012341376002032000233540ustar00rootroot00000000000000{ "name": "arthurhoaro/web-thumbnailer", "description": "PHP library which will retrieve a thumbnail for any given URL", "type": "library", "license": "MIT", "authors": [ { "name": "Arthur Hoaro", "homepage": "http://hoa.ro" } ], "require": { "php": ">=7.1", "phpunit/php-text-template": "^1.2 || ^2.0" }, "require-dev": { "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", "php-coveralls/php-coveralls": "^2.0", "squizlabs/php_codesniffer": "^3.0", "gskema/phpcs-type-sniff": "^0.13.1", "phpstan/phpstan": "^0.12.9" }, "autoload": { "psr-0": { "WebThumbnailer\\": ["src/", "tests/"] } } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/phpcs.xml.dist000066400000000000000000000012431376002032000234330ustar00rootroot00000000000000 src tests php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/phpunit.xml.dist000066400000000000000000000017321376002032000240100ustar00rootroot00000000000000 src/WebThumbnailer/ tests/WebThumbnailer/ tests php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/000077500000000000000000000000001376002032000214215ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/000077500000000000000000000000001376002032000243315ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/000077500000000000000000000000001376002032000265745ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/CacheManager.php000066400000000000000000000203411376002032000316030ustar00rootroot00000000000000thumbnail url resolution is also cached. * Cache files are organized by domains name, and have a unique name * based on their URL, max-width and max-height. * * Cache duration is defined in JSON settings. */ class CacheManager { /** Thumbnails image cache. */ public const TYPE_THUMB = 'thumb'; /** Finder cache. */ public const TYPE_FINDER = 'finder'; /** @var string Clean filename, used to clean directories periodically. */ protected static $CLEAN_FILE = '.clean'; /** * Returns the cache path according to the given type. * * @param string $type Type of cache. * @param bool $rebuilt Flag to tell if a rebuild tentative has been done. * * @return string Cache path. * * @throws IOException Type not found. * @throws CacheException * @throws BadRulesException */ public static function getCachePath(string $type, bool $rebuilt = false): string { static::checkCacheType($type); $cache = ConfigManager::get('settings.path.cache', 'cache/'); $path = FileUtils::getPath($cache, $type); if (!$path && !$rebuilt) { static::rebuildCacheFolders(); return static::getCachePath($type, true); } elseif (!$path) { throw new IOException('Cache folders are not writable: ' . $cache); } return $path; } /** * Get a thumb cache file absolute path. * * @param string $url URL of the thumbnail (unique file per URL). * @param string $domain Domain concerned. * @param string $type Type of cache. * @param int|string $width User setting for image width. * @param int|string $height User setting for image height. * @param bool|null $crop Crop enabled or not. * * @return string Absolute file path. * * @throws IOException * @throws CacheException * @throws BadRulesException */ public static function getCacheFilePath( string $url, string $domain, string $type, $width = 0, $height = 0, ?bool $crop = false ): string { $domainHash = static::getDomainHash($domain); static::createDomainThumbCacheFolder($domainHash, $type); $domainFolder = FileUtils::getPath(static::getCachePath($type), $domainHash); if ($domainFolder === false) { throw new CacheException(sprintf( 'Could not find cache path for type %s and domain hash %s', $type, $domainHash )); } if ($type === static::TYPE_THUMB) { $suffix = $width . $height . ($crop ? '1' : '0') . '.jpg'; } else { $suffix = $width . $height; } return $domainFolder . static::getThumbFilename($url) . $suffix; } /** * Check whether a valid cache file exists or not. * Also check that that file is still valid. * * Support endless cache using a negative value. * * @param string $cacheFile Cache file path. * @param string $domain Domain concerned. * @param string $type Type of cache. * * @return bool true if valid cache exists, false otherwise. * * @throws CacheException * @throws IOException * @throws BadRulesException */ public static function isCacheValid(string $cacheFile, string $domain, string $type): bool { $out = false; $cacheDuration = ConfigManager::get('settings.cache_duration', 3600 * 24 * 31); if ( is_readable($cacheFile) && ($cacheDuration < 0 || (time() - filemtime($cacheFile)) < $cacheDuration) ) { $out = true; } else { static::createDomainThumbCacheFolder(static::getDomainHash($domain), $type); } return $out; } /** * Create the domains folder for thumb cache if it doesn't exists. * * @param string $domain Domain used. * @param string $type Type of cache. * * @throws CacheException * @throws IOException * @throws BadRulesException */ protected static function createDomainThumbCacheFolder(string $domain, string $type): void { $cachePath = static::getCachePath($type); $domainFolder = $cachePath . $domain; if (!file_exists($domainFolder)) { mkdir($domainFolder, 0775, false); touch($domainFolder . '/' . static::$CLEAN_FILE); } static::createHtaccessFile($cachePath, $type === static::TYPE_THUMB); } /** * Create a .htaccess file for Apache webserver if it doesn't exists. * The folder should be allowed for thumbs, and denied for finder's cache. * * @param string $path Cache directory path * @param bool $allowed Weather the access is allowed or not * * @throws BadRulesException * @throws IOException */ protected static function createHtaccessFile(string $path, bool $allowed = false): void { $apacheVersion = ConfigManager::get('settings.apache_version', ''); $htaccessFile = $path . '.htaccess'; if (file_exists($htaccessFile)) { return; } $templateFile = file_exists(FileUtils::RESOURCES_PATH . 'htaccess' . $apacheVersion . '_template') ? FileUtils::RESOURCES_PATH . 'htaccess' . $apacheVersion . '_template' : FileUtils::RESOURCES_PATH . 'htaccess_template'; $template = TemplatePolyfill::get($templateFile); $template->setVar([ 'new_all' => $allowed ? 'granted' : 'denied', 'old_allow' => $allowed ? 'all' : 'none', 'old_deny' => $allowed ? 'none' : 'all', ]); file_put_contents($htaccessFile, $template->render()); } /** * Get the cache filename according to the given URL. * Using a sha1 hash to get unique valid filenames. * * @param string $url Thumbnail URL. * * @return string Thumb filename. */ protected static function getThumbFilename(string $url): string { return hash('sha1', $url); } /** * Make sure that the cache type exists. * * @param string $type Cache type. * * @return bool True if the check was successful. * * @throws CacheException Cache type doesn't exists. */ protected static function checkCacheType(string $type): bool { if ($type != static::TYPE_THUMB && $type != static::TYPE_FINDER) { throw new CacheException('Unknown cache type ' . $type); } return true; } /** * Recreates cache folders just in case the user delete them. * * @throws BadRulesException * @throws IOException */ protected static function rebuildCacheFolders(): void { $mainFolder = ConfigManager::get('settings.path.cache', 'cache/'); if (! is_dir($mainFolder)) { mkdir($mainFolder, 0755); } if (! is_dir($mainFolder . static::TYPE_THUMB)) { mkdir($mainFolder . static::TYPE_THUMB, 0755); } if (! is_readable($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep')) { touch($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep'); } if (! is_dir($mainFolder . static::TYPE_FINDER)) { mkdir($mainFolder . static::TYPE_FINDER, 0755); } if (! is_readable($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep')) { touch($mainFolder . static::TYPE_FINDER . DIRECTORY_SEPARATOR . '.gitkeep'); } } /** * Return the hashed folder name for a given domain. * * @param string $domain name * * @return string hash */ protected static function getDomainHash(string $domain): string { return md5($domain); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/ConfigManager.php000066400000000000000000000067241376002032000320160ustar00rootroot00000000000000 0) { return static::getConfig($settings, $config[$setting]); } return $config[$setting]; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/Thumbnailer.php000066400000000000000000000303771376002032000315710ustar00rootroot00000000000000url = $url; $this->server = $server; $this->finder = FinderFactory::getFinder($url); $this->finder->setUserOptions($options); $this->setOptions($options); } /** * Set Thumbnailer options from user input. * * @param mixed[] $options User options array. * * @throws BadRulesException * @throws IOException */ protected function setOptions(array $options): void { static::checkOptions($options); $this->options[static::DL_OPTION] = ConfigManager::get('settings.default.download_mode', 'DOWNLOAD'); foreach ($options as $key => $value) { // Download option. if ( $value === WebThumbnailer::DOWNLOAD || $value === WebThumbnailer::HOTLINK || $value === WebThumbnailer::HOTLINK_STRICT ) { $this->options[static::DL_OPTION] = $value; break; } } // DL size option if ( isset($options[WebThumbnailer::DOWNLOAD_MAX_SIZE]) && is_int($options[WebThumbnailer::DOWNLOAD_MAX_SIZE]) ) { $this->options[WebThumbnailer::DOWNLOAD_MAX_SIZE] = $options[WebThumbnailer::DOWNLOAD_MAX_SIZE]; } else { $maxdl = ConfigManager::get('settings.default.max_img_dl', 4194304); $this->options[WebThumbnailer::DOWNLOAD_MAX_SIZE] = $maxdl; } // DL timeout option if ( isset($options[WebThumbnailer::DOWNLOAD_TIMEOUT]) && is_int($options[WebThumbnailer::DOWNLOAD_TIMEOUT]) ) { $this->options[WebThumbnailer::DOWNLOAD_TIMEOUT] = $options[WebThumbnailer::DOWNLOAD_TIMEOUT]; } else { $timeout = ConfigManager::get('settings.default.timeout', 30); $this->options[WebThumbnailer::DOWNLOAD_TIMEOUT] = $timeout; } if (isset($options[WebThumbnailer::NOCACHE])) { $this->options[WebThumbnailer::NOCACHE] = $options[WebThumbnailer::NOCACHE]; } if (isset($options[WebThumbnailer::CROP])) { $this->options[WebThumbnailer::CROP] = $options[WebThumbnailer::CROP]; } else { $this->options[WebThumbnailer::CROP] = false; } if (isset($options[WebThumbnailer::DEBUG])) { $this->options[WebThumbnailer::DEBUG] = $options[WebThumbnailer::DEBUG]; } else { $this->options[WebThumbnailer::DEBUG] = false; } // Image size $this->setSizeOptions($options); } /** * Make sure user options are coherent. * - Only one thumb mode can be defined. * * @param mixed[] $options User options array. * * @return bool True if the check is successful. * * @throws BadRulesException Invalid options. */ protected static function checkOptions(array $options): bool { $incompatibleFlagsList = [ [WebThumbnailer::DOWNLOAD, WebThumbnailer::HOTLINK, WebThumbnailer::HOTLINK_STRICT], ]; foreach ($incompatibleFlagsList as $incompatibleFlags) { if (count(array_intersect($incompatibleFlags, $options)) > 1) { $error = 'Only one of these flags can be set between: '; foreach ($incompatibleFlags as $flag) { $error .= $flag . ' '; } throw new BadRulesException($error); } } return true; } /** * Set specific size option, allowing 'meta' size SMALL, MEDIUM, etc. * * @param mixed[] $options User options array. * * @throws BadRulesException * @throws IOException */ protected function setSizeOptions(array $options): void { foreach ([WebThumbnailer::MAX_WIDTH, WebThumbnailer::MAX_HEIGHT] as $parameter) { $value = 0; if (!empty($options[$parameter])) { if (SizeUtils::isMetaSize((string) $options[$parameter])) { $value = SizeUtils::getMetaSize((string) $options[$parameter]); } elseif (is_int($options[$parameter]) || ctype_digit($options[$parameter])) { $value = $options[$parameter]; } } $this->options[$parameter] = $value; } if ($this->options[WebThumbnailer::MAX_WIDTH] == 0 && $this->options[WebThumbnailer::MAX_HEIGHT] == 0) { $maxwidth = ConfigManager::get('settings.default.max_width', 160); $this->options[WebThumbnailer::MAX_WIDTH] = $maxwidth; $maxheight = ConfigManager::get('settings.default.max_height', 160); $this->options[WebThumbnailer::MAX_HEIGHT] = $maxheight; } } /** * Get the thumbnail according to download mode: * - HOTLINK_STRICT: will only try to get hotlink thumb. * - HOTLINK: will retrieve hotlink if available, or download otherwise. * - DOWNLOAD: will download the thumb, resize it, and store it in cache. * * Default mode: DOWNLOAD. * * @return string|false The thumbnail URL (relative if downloaded), or false if no thumb found. * * @throws DownloadException * @throws ImageConvertException * @throws NotAnImageException * @throws ThumbnailNotFoundException * @throws IOException * @throws CacheException * @throws BadRulesException */ public function getThumbnail() { $cache = CacheManager::getCacheFilePath( $this->url, $this->finder->getDomain(), CacheManager::TYPE_FINDER, $this->options[WebThumbnailer::MAX_WIDTH], $this->options[WebThumbnailer::MAX_HEIGHT] ); // Loading Finder result from cache if enabled and valid to prevent useless requests. if ( empty($this->options[WebThumbnailer::NOCACHE]) && CacheManager::isCacheValid($cache, $this->finder->getDomain(), CacheManager::TYPE_FINDER) ) { $thumbUrl = file_get_contents($cache); } else { $thumbUrl = $this->finder->find(); $thumbUrl = $thumbUrl !== false ? html_entity_decode($thumbUrl) : $thumbUrl; file_put_contents($cache, $thumbUrl); } if (empty($thumbUrl)) { $error = 'No thumbnail could be found using ' . $this->finder->getName() . ' finder: ' . $this->url; throw new ThumbnailNotFoundException($error); } // Only hotlink, find() is enough. if ($this->options[static::DL_OPTION] === WebThumbnailer::HOTLINK_STRICT) { return $this->thumbnailStrictHotlink($thumbUrl); } // Hotlink if available, download otherwise. if ($this->options[static::DL_OPTION] === WebThumbnailer::HOTLINK) { return $this->thumbnailHotlink($thumbUrl); } else { // Download return $this->thumbnailDownload($thumbUrl); } } /** * Get thumbnails in HOTLINK_STRICT mode. * Won't work for domains which doesn't allow hotlinking. * * @param string $thumbUrl Thumbnail URL, generated by the Finder. * * @return string The thumbnail URL. * * @throws ThumbnailNotFoundException Hotlink is disabled for this domains. */ protected function thumbnailStrictHotlink(string $thumbUrl): string { if (!$this->finder->isHotlinkAllowed()) { throw new ThumbnailNotFoundException('Hotlink is not supported for this URL.'); } return $thumbUrl; } /** * Get thumbnails in HOTLINK mode. * * @param string $thumbUrl Thumbnail URL, generated by the Finder. * * @return string|false The thumbnail URL, or false if no thumb found. * * @throws DownloadException * @throws ImageConvertException * @throws NotAnImageException * @throws IOException * @throws CacheException * @throws BadRulesException */ protected function thumbnailHotlink(string $thumbUrl) { if (!$this->finder->isHotlinkAllowed()) { return $this->thumbnailDownload($thumbUrl); } return $thumbUrl; } /** * Get thumbnails in HOTLINK mode. * * @param string $thumbUrl Thumbnail URL, generated by the Finder. * * @return string|false The thumbnail URL, or false if no thumb found. * * @throws DownloadException Couldn't download the image * @throws ImageConvertException Thumbnail not generated * @throws NotAnImageException * @throws IOException * @throws CacheException * @throws BadRulesException */ protected function thumbnailDownload(string $thumbUrl) { // Cache file path. $thumbPath = CacheManager::getCacheFilePath( $thumbUrl, $this->finder->getDomain(), CacheManager::TYPE_THUMB, $this->options[WebThumbnailer::MAX_WIDTH], $this->options[WebThumbnailer::MAX_HEIGHT], $this->options[WebThumbnailer::CROP] ); // If the cache is valid, serve it. if ( empty($this->options[WebThumbnailer::NOCACHE]) && CacheManager::isCacheValid( $thumbPath, $this->finder->getDomain(), CacheManager::TYPE_THUMB ) ) { return $thumbPath; } $webaccess = WebAccessFactory::getWebAccess($thumbUrl); // Download the thumb. list($headers, $data) = $webaccess->getContent( $thumbUrl, $this->options[WebThumbnailer::DOWNLOAD_TIMEOUT], $this->options[WebThumbnailer::DOWNLOAD_MAX_SIZE] ); if (strpos($headers[0], '200') === false) { throw new DownloadException( 'Unreachable thumbnail URL. HTTP ' . $headers[0] . '.' . PHP_EOL . ' - thumbnail URL: ' . $thumbUrl ); } if (empty($data)) { throw new DownloadException('Couldn\'t download the thumbnail at ' . $thumbUrl); } // Resize and save it locally. ImageUtils::generateThumbnail( $data, $thumbPath, $this->options[WebThumbnailer::MAX_WIDTH], $this->options[WebThumbnailer::MAX_HEIGHT], $this->options[WebThumbnailer::CROP] ); if (!is_file($thumbPath)) { throw new ImageConvertException('Thumbnail was not generated.'); } return $thumbPath; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/WebAccess/000077500000000000000000000000001376002032000304335ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/WebAccess/WebAccess.php000066400000000000000000000030131376002032000330000ustar00rootroot00000000000000 'curl_init() error'], false]; } // General cURL settings curl_setopt($ch, CURLOPT_AUTOREFERER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt( $ch, CURLOPT_HTTPHEADER, ['Accept-Language: ' . $acceptLanguage] ); curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); curl_setopt($ch, CURLOPT_COOKIESESSION, true); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie); // Max download size management curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16); curl_setopt($ch, CURLOPT_NOPROGRESS, false); curl_setopt( $ch, CURLOPT_PROGRESSFUNCTION, function ($arg0, $arg1, $arg2) use ($maxBytes) { $downloaded = $arg2; // Non-zero return stops downloading return ($downloaded > $maxBytes) ? 1 : 0; } ); if (is_callable($dlCallback)) { curl_setopt($ch, CURLOPT_WRITEFUNCTION, $dlCallback); curl_exec($ch); $response = $dlContent; } else { $response = curl_exec($ch); } $errorNo = curl_errno($ch); $errorStr = curl_error($ch); $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); curl_close($ch); if (!is_string($response)) { return [[0 => 'curl_exec() error #' . $errorNo . ': ' . $errorStr], false]; } // Formatting output like the fallback method $rawHeaders = substr($response, 0, $headSize); // Keep only headers from latest redirection $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders)); $rawHeadersLastRedir = end($rawHeadersArrayRedirs); $content = substr($response, $headSize); $headers = []; foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir ?: '') ?: [] as $line) { if (empty($line) or ctype_space($line)) { continue; } $splitLine = explode(': ', $line, 2); if (count($splitLine) > 1) { $key = $splitLine[0]; $value = $splitLine[1]; if (array_key_exists($key, $headers)) { if (!is_array($headers[$key])) { $headers[$key] = array(0 => $headers[$key]); } $headers[$key][] = $value; } else { $headers[$key] = $value; } } else { $headers[] = $splitLine[0]; } } return [$headers, $content]; } } WebAccessFactory.php000066400000000000000000000014701376002032000342560ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/WebAccessgetContext($timeout, false); stream_context_set_default($context); list($headers, $finalUrl) = $this->getRedirectedHeaders($url, $timeout, $maxRedr); if (! $headers || strpos($headers[0], '200 OK') === false) { $context = $this->getContext($timeout, true); stream_context_set_default($context); list($headers, $finalUrl) = $this->getRedirectedHeaders($url, $timeout, $maxRedr); } if (! $headers) { return array($headers, false); } $context = stream_context_create($context); $content = file_get_contents($finalUrl, false, $context, 0, $maxBytes); return array($headers, $content); } /** * Download URL HTTP headers and follow redirections (HTTP 30x) if necessary. * * @param string $url URL to download. * @param int $timeout network timeout (in seconds) * @param int $redirectionLimit Stop trying to follow redrection if this number is reached. * * @return mixed[] containing HTTP headers. */ protected function getRedirectedHeaders(string $url, int $timeout, int $redirectionLimit = 3): array { stream_context_set_default($this->getContext($timeout)); $headers = @get_headers($url, 1); // Some hosts don't like fulluri request, some requires it... if ($headers === false) { stream_context_set_default($this->getContext($timeout, false)); $headers = @get_headers($url, 1); } // Headers found, redirection found, and limit not reached. if ( $redirectionLimit-- > 0 && !empty($headers) && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false) && !empty($headers['Location']) ) { $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location']; if ($redirection != $url) { return $this->getRedirectedHeaders($redirection, $timeout, $redirectionLimit); } } return [$headers, $url]; } /** * Create a valid context for PHP HTTP functions. * * @param int $timeout network timeout (in seconds) * @param bool $fulluri this is required by some hosts, rejected by others, so option. * * @return mixed[] context. */ protected function getContext(int $timeout, bool $fulluri = true): array { return [ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:45.0; WebThumbnailer) Gecko/20100101 Firefox/45.0', 'request_fulluri' => $fulluri, ] ]; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Exception/000077500000000000000000000000001376002032000262675ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Exception/BadRegexException.php000066400000000000000000000002031376002032000323330ustar00rootroot00000000000000webAccess = WebAccessFactory::getWebAccess($url); $this->url = $url; $this->domain = $domain; } /** * Generic finder. * * @inheritdoc */ public function find() { if (ImageUtils::isImageExtension(UrlUtils::getUrlFileExtension($this->url))) { return $this->url; } $content = $thumbnail = null; $callback = $this->webAccess instanceof WebAccessCUrl ? $this->getCurlCallback($content, $thumbnail) : null; list($headers, $content) = $this->webAccess->getContent( $this->url, (int) ConfigManager::get('settings.default.timeout', 30), (int) ConfigManager::get('settings.default.max_img_dl', 16777216), $callback, $content ); if (empty($thumbnail) && ImageUtils::isImageString($content)) { return $this->url; } if (empty($thumbnail) && ! empty($headers) && strpos($headers[0], '200') === false) { return false; } // With curl, the thumb is extracted during the download if ($this->webAccess instanceof WebAccessCUrl && ! empty($thumbnail)) { return $thumbnail; } return ! empty($content) ? static::extractMetaTag($content) : false; } /** * Get a callback for curl write function. * * @param string|null $content A variable reference in which the downloaded content should be stored. * @param string|null $thumbnail A variable reference in which extracted thumb URL should be stored. * * @return callable CURLOPT_WRITEFUNCTION callback */ protected function getCurlCallback(?string &$content, ?string &$thumbnail): callable { $url = $this->url; $isRedirected = false; /** * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). * * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text' * Then we extract the title and the charset and stop the download when it's done. * * Note that when using CURLOPT_WRITEFUNCTION, we have to manually handle the content retrieved, * hence the $content reference variable. * * @param resource $ch cURL resource * @param string $data chunk of data being downloaded * * @return int|false length of $data or false if we need to stop the download */ return function ($ch, $data) use ($url, &$content, &$thumbnail, &$isRedirected) { $content .= $data; $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if (!empty($responseCode) && in_array($responseCode, [301, 302])) { $isRedirected = true; return strlen($data); } if (!empty($responseCode) && $responseCode !== 200) { return false; } // After a redirection, the content type will keep the previous request value // until it finds the next content-type header. if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); } // we look for image, and ignore application/octet-stream, // which is a the default content type for any binary // @see https://developer.mozilla.org/fr/docs/Web/HTTP/Basics_of_HTTP/MIME_types if ( !empty($contentType) && strpos($contentType, 'image/') !== false && strpos($contentType, 'application/octet-stream') === false ) { $thumbnail = $url; return false; } elseif ( !empty($contentType) && strpos($contentType, 'text/html') === false && strpos($contentType, 'application/octet-stream') === false ) { return false; } if (empty($thumbnail)) { $thumbnail = DefaultFinder::extractMetaTag($data); } // We got everything we want, stop the download. if (!empty($responseCode) && !empty($contentType) && !empty($thumbnail)) { return false; } return strlen($data); }; } /** * Applies the regexp on the HTML $content to extract the thumb URL. * * @param string $content Downloaded HTML content * * @return string|false Extracted thumb URL or false if not found. */ public static function extractMetaTag(string $content) { $propertiesKey = ['property', 'name', 'itemprop']; $properties = implode('|', $propertiesKey); // Try to retrieve OpenGraph image. $ogRegex = '#]+(?:' . $properties . ')=["\']?og:image["\'\s][^>]*content=["\']?(.*?)["\'\s>]#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. $ogRegexReverse = '#]+content=["\']?([^"\'\s]+)[^>]+(?:' . $properties . ')=["\']?og:image["\'\s/>]#'; if ( preg_match($ogRegex, $content, $matches) > 0 || preg_match($ogRegexReverse, $content, $matches) > 0 ) { return $matches[1]; } return false; } /** @inheritdoc */ public function isHotlinkAllowed(): bool { return true; } /** @inheritdoc */ public function checkRules(?array $rules): bool { return true; } /** @inheritdoc */ public function loadRules(?array $rules): void { } /** @inheritdoc */ public function getName(): string { return 'default'; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/Finder.php000066400000000000000000000047041376002032000274650ustar00rootroot00000000000000getOptionValue($option); return str_replace('${' . $option . '}', $chosenOption, $thumbnailUrl); } /** * @param string $option to retrieve * * @return mixed Found option value * * @throws BadRulesException */ protected function getOptionValue(string $option) { // If the provided option is not defined in the Finder rules. if (empty($this->finderOptions) || ! in_array($option, array_keys($this->finderOptions))) { throw new BadRulesException('Unknown option "' . $option . '" for the finder "' . $this->getName() . '"'); } // User option is defined. // Any defined option must provide a replacement value in rules under the `param` key. if ( ! empty($this->userOptions[$option]) && is_string($this->userOptions[$option]) && isset($this->finderOptions[$option][$this->userOptions[$option]]['param']) ) { return $this->finderOptions[$option][$this->userOptions[$option]]['param']; } // If no user option has been found, and no default value is provided: error. if (! isset($this->finderOptions[$option]['default'])) { $error = 'No default set for option "' . $option . '" for the finder "' . $this->getName() . '"'; throw new BadRulesException($error); } // Use default option replacement. $default = $this->finderOptions[$option]['default']; if (!isset($this->finderOptions[$option][$default]['param'])) { $error = 'No default parameter set for option "' . $option . '" for the finder "' . $this->getName() . '"'; throw new BadRulesException($error); } return $this->finderOptions[$option][$default]['param']; } /** @inheritdoc */ public function isHotlinkAllowed(): bool { if (! isset($this->finderOptions['hotlink_allowed']) || $this->finderOptions['hotlink_allowed'] === true) { return true; } return false; } /** @inheritdoc */ public function setUserOptions(?array $userOptions): void { $this->userOptions = $userOptions; $this->setSizeOption(); } /** * Set size parameter properly. * * If something goes wrong, we just ignore it. * The size user setting can be set to small, medium, etc. or a pixel value (int). * * We retrieve the thumbnail size bigger than the minimal size asked. */ protected function setSizeOption(): void { // If no option has been provided, we'll use default values. if ( empty($this->userOptions[WebThumbnailer::MAX_HEIGHT]) && empty($this->userOptions[WebThumbnailer::MAX_WIDTH]) ) { return; } // If the rules doesn't specify anything about size, abort. if (empty($this->finderOptions[static::SIZE_KEY])) { return; } // Load height user option. if (!empty($this->userOptions[WebThumbnailer::MAX_HEIGHT])) { $height = $this->userOptions[WebThumbnailer::MAX_HEIGHT]; if (SizeUtils::isMetaSize((string) $height)) { $height = SizeUtils::getMetaSize((string) $height); } } // Load width user option. if (!empty($this->userOptions[WebThumbnailer::MAX_WIDTH])) { $width = $this->userOptions[WebThumbnailer::MAX_WIDTH]; if (SizeUtils::isMetaSize((string) $width)) { $width = SizeUtils::getMetaSize((string) $width); } } // Trying to find a resolution higher than the one asked. foreach ($this->finderOptions[static::SIZE_KEY] as $sizeOption => $value) { if ($sizeOption == 'default') { continue; } if ( (empty($value['maxwidth']) || empty($width) || $value['maxwidth'] >= $width) && (empty($value['maxheight']) || empty($height) || $value['maxheight'] >= $height) ) { $this->userOptions[static::SIZE_KEY] = $sizeOption; break; } } // If the resolution asked hasn't been reached, take the highest resolution we have. if ((!empty($width) || !empty($height)) && empty($this->userOptions[static::SIZE_KEY])) { $ref = array_keys($this->finderOptions[static::SIZE_KEY]); $this->userOptions[static::SIZE_KEY] = end($ref); } } /** @inheritdoc */ public function getDomain(): string { return $this->domain; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/FinderFactory.php000066400000000000000000000077631376002032000310250ustar00rootroot00000000000000getOptionValue('size'); // One size is actually no suffix... $size = ! empty($size) ? '_' . $size : ''; $thumb = preg_replace('#(.*)_\w(\.\w+)$#i', '$1' . $size . '$2', $thumb); return $thumb ?? false; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/QueryRegexFinder.php000066400000000000000000000154771376002032000315170ustar00rootroot00000000000000webAccess = WebAccessFactory::getWebAccess($url); $this->url = $url; $this->domain = $domain; $this->loadRules($rules); $this->finderOptions = $options; } /** * This finder downloads target URL page, and apply the regex given in rules on its content * to extract the thumbnail image. * The thumb URL must include ${number} to be replaced from the regex match. * Also replace eventual URL options. * * @inheritdoc * * @throws BadRulesException */ public function find() { $thumbnail = $content = null; $callback = $this->webAccess instanceof WebAccessCUrl ? $this->getCurlCallback($content, $thumbnail) : null; list($headers, $content) = $this->webAccess->getContent( $this->url, (int) ConfigManager::get('settings.default.timeout', 30), (int) ConfigManager::get('settings.default.max_img_dl', 16777216), $callback, $content ); if ( empty($content) || empty($headers) || (empty($thumbnail) && strpos($headers[0], '200') === false) ) { return false; } // With curl, the thumb is extracted during the download if ($this->webAccess instanceof WebAccessCUrl && ! empty($thumbnail)) { return $thumbnail; } return $this->extractThumbContent($content); } /** * Get a callback for curl write function. * * @param string|null $content A variable reference in which the downloaded content should be stored. * @param string|null $thumbnail A variable reference in which extracted thumb URL should be stored. * * @return callable CURLOPT_WRITEFUNCTION callback */ protected function getCurlCallback(?string &$content, ?string &$thumbnail): callable { $isRedirected = false; /** * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). * * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text' * Then we extract the title and the charset and stop the download when it's done. * * Note that when using CURLOPT_WRITEFUNCTION, we have to manually handle the content retrieved, * hence the $content reference variable. * * @param resource $ch cURL resource * @param string $data chunk of data being downloaded * * @return int|false length of $data or false if we need to stop the download */ return function ($ch, $data) use (&$content, &$thumbnail, &$isRedirected) { $content .= $data; $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if (!empty($responseCode) && in_array($responseCode, [301, 302])) { $isRedirected = true; return strlen($data); } if (!empty($responseCode) && $responseCode !== 200) { return false; } // After a redirection, the content type will keep the previous request value // until it finds the next content-type header. if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); } if (!empty($contentType) && strpos($contentType, 'text/html') === false) { return false; } if (empty($thumbnail)) { $thumbnail = $this->extractThumbContent($data); } // We got everything we want, stop the download. if (!empty($responseCode) && !empty($contentType) && !empty($thumbnail)) { return false; } return strlen($data); }; } /** * @param string $content to extract thumb from * * @return string|false Thumbnail URL or false if not found * * @throws BadRulesException */ public function extractThumbContent(string $content) { $thumbnailUrl = $this->thumbnailUrlFormat; if (preg_match($this->urlRegex, $content, $matches) !== 0) { $total = count($matches); for ($i = 1; $i < $total; $i++) { $thumbnailUrl = str_replace('${' . $i . '}', $matches[$i], $thumbnailUrl); } // Match only options (not ${number}) if (preg_match_all('/\${((?!\d)\w+?)}/', $thumbnailUrl, $optionsMatch, PREG_PATTERN_ORDER)) { foreach ($optionsMatch[1] as $value) { $thumbnailUrl = $this->replaceOption($thumbnailUrl, $value); } } return $thumbnailUrl; } return false; } /** @inheritdoc */ public function checkRules(?array $rules): bool { if (count($rules ?? []) > 0 && !FinderUtils::checkMandatoryRules($rules, ['image_regex', 'thumbnail_url'])) { throw new BadRulesException(); } return true; } /** * @inheritdoc * * @throws BadRulesException */ public function loadRules(?array $rules): void { $this->checkRules($rules); $this->urlRegex = FinderUtils::buildRegex($rules['image_regex'], 'im'); $this->thumbnailUrlFormat = $rules['thumbnail_url']; } /** @inheritdoc */ public function getName(): string { return 'Query Regex'; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/UrlRegexFinder.php000066400000000000000000000056661376002032000311530ustar00rootroot00000000000000url = $url; $this->domain = $domain; $this->loadRules($rules); $this->finderOptions = $options; } /** * Will replace ${number} in URL format to regex match. * Also replace eventual URL options. * * {@inheritdoc} * * @throws BadRulesException */ public function find() { $this->thumbnailUrl = $this->thumbnailUrlFormat; if (preg_match($this->urlRegex, $this->url, $matches) !== 0) { $total = count($matches); for ($i = 1; $i < $total; $i++) { $this->thumbnailUrl = str_replace('${' . $i . '}', $matches[$i], $this->thumbnailUrl); } // Match only options (not ${number}) if (preg_match_all('/\${((?!\d)\w+?)}/', $this->thumbnailUrl, $optionsMatch, PREG_PATTERN_ORDER)) { foreach ($optionsMatch[1] as $value) { $this->thumbnailUrl = $this->replaceOption($this->thumbnailUrl, $value); } } return $this->thumbnailUrl; } return false; } /** * Mandatory rules: * - url_regex * - thumbnail_url * * {@inheritdoc} */ public function checkRules(?array $rules): bool { $mandatoryRules = [ 'url_regex', 'thumbnail_url', ]; foreach ($mandatoryRules as $mandatoryRule) { if (empty($rules[$mandatoryRule])) { throw new BadRulesException(); } } return true; } /** @inheritdoc */ public function loadRules(?array $rules): void { $this->checkRules($rules); $this->urlRegex = FinderUtils::buildRegex($rules['url_regex'], 'i'); $this->thumbnailUrlFormat = $rules['thumbnail_url']; } /** @inheritdoc */ public function getName(): string { return 'URL regex'; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/000077500000000000000000000000001376002032000254315ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/ApplicationUtils.php000066400000000000000000000027171376002032000314350ustar00rootroot00000000000000isDir() ? rmdir($value->getRealPath()) : unlink($value->getRealPath()); } return rmdir($path); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/FinderUtils.php000066400000000000000000000026631376002032000304010ustar00rootroot00000000000000 $value) { if (is_array($value)) { if (isset($rules[$key])) { return static::checkMandatoryRules($rules[$key], $value); } else { return false; } } else { if (! isset($rules[$value])) { return false; } } } return true; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/ImageUtils.php000066400000000000000000000155541376002032000302170ustar00rootroot00000000000000 $originalWidth) { $maxWidth = $originalWidth; } if ($maxHeight > $originalHeight) { $maxHeight = $originalHeight; } list($finalWidth, $finalHeight) = static::calcNewSize( $originalWidth, $originalHeight, $maxWidth, $maxHeight, $crop ); $targetImg = imagecreatetruecolor($finalWidth, $finalHeight); if ($targetImg === false) { throw new ImageConvertException('Could not generate the thumbnail from source image.'); } if ( !imagecopyresized( $targetImg, $sourceImg, 0, 0, 0, 0, $finalWidth, $finalHeight, $originalWidth, $originalHeight ) ) { static::imageDestroy($sourceImg); static::imageDestroy($targetImg); throw new ImageConvertException('Could not generate the thumbnail from source image.'); } if ($crop) { $targetImg = imagecrop($targetImg, [ 'x' => $finalWidth >= $finalHeight ? (int) floor(($finalWidth - $maxWidth) / 2) : 0, 'y' => $finalHeight <= $finalWidth ? (int) floor(($finalHeight - $maxHeight) / 2) : 0, 'width' => $maxWidth, 'height' => $maxHeight ]); } if (false === $targetImg) { throw new ImageConvertException('Could not generate the thumbnail.'); } imagedestroy($sourceImg); imagejpeg($targetImg, $target); imagedestroy($targetImg); } /** * Calculate image new size to keep proportions depending on actual image size * and max width/height settings. * * @param int $originalWidth Image original width * @param int $originalHeight Image original height * @param int $maxWidth Target image maximum width * @param int $maxHeight Target image maximum height * @param bool $crop Is cropping enabled * * @return int[] [final width, final height] * * @throws ImageConvertException At least maxwidth or maxheight needs to be defined */ public static function calcNewSize( int $originalWidth, int $originalHeight, int $maxWidth, int $maxHeight, bool $crop ): array { if (empty($maxHeight) && empty($maxWidth)) { throw new ImageConvertException('At least maxwidth or maxheight needs to be defined.'); } $diffWidth = !empty($maxWidth) ? $originalWidth - $maxWidth : false; $diffHeight = !empty($maxHeight) ? $originalHeight - $maxHeight : false; if ( ($diffHeight === false && $diffWidth !== false) || ($diffWidth > $diffHeight && !$crop) || ($diffWidth < $diffHeight && $crop) ) { $finalWidth = $maxWidth; $finalHeight = $originalHeight * ($finalWidth / $originalWidth); } else { $finalHeight = $maxHeight; $finalWidth = $originalWidth * ($finalHeight / $originalHeight); } return [(int) floor($finalWidth), (int) floor($finalHeight)]; } /** * Check if a file extension is an image. * * @param string $ext file extension. * * @return bool true if it's an image extension, false otherwise. */ public static function isImageExtension(string $ext): bool { $supportedImageFormats = ['png', 'jpg', 'jpeg', 'svg']; return in_array($ext, $supportedImageFormats); } /** * Check if a string is an image. * * @param string $content String to check. * * @return bool True if the content is image, false otherwise. */ public static function isImageString(string $content): bool { return static::imageCreateFromString($content) !== false; } /** * With custom error handlers, @ does not stop the warning to being thrown. * * @param string $content * * @return resource|false */ protected static function imageCreateFromString(string $content) { try { return @imagecreatefromstring($content); } catch (\Throwable $e) { // Avoid raising PHP exceptions here with custom error handler, we want to raise our own. } return false; } /** * With custom error handlers, @ does not stop the warning to being thrown. * * @param resource $image * * @return bool */ // resource can't be type hinted: // phpcs:ignore Gskema.Sniffs.CompositeCodeElement.FqcnMethodSniff protected static function imageDestroy($image): bool { try { return @imagedestroy($image); } catch (\Throwable $e) { // Avoid raising PHP exceptions here with custom error handler, we want to raise our own. } return false; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/SizeUtils.php000066400000000000000000000030531376002032000300760ustar00rootroot00000000000000 0) { return strtolower($match[1]); } return ''; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/WebThumbnailer.php000066400000000000000000000143761376002032000277650ustar00rootroot00000000000000 * * @param string $url User URL. * @param mixed[] $options Options array. See the documentation for more infos. * * @return string|false Thumbnail URL, false if not found. * * @throws WebThumbnailerException Only throw exception in debug mode. */ public function thumbnail(string $url, array $options = []) { $url = trim($url); if (empty($url)) { return false; } $options = array_merge( [ static::DEBUG => $this->debug, static::VERBOSE => $this->verbose, static::NOCACHE => $this->nocache, static::MAX_WIDTH => $this->maxWidth, static::MAX_HEIGHT => $this->maxHeight, static::DOWNLOAD_TIMEOUT => $this->downloadTimeout, static::DOWNLOAD_MAX_SIZE => $this->downloadMaxSize, static::CROP => $this->crop, $this->downloadMode ], $options ); try { $downloader = new Thumbnailer($url, $options, $_SERVER); return $downloader->getThumbnail(); } catch (MissingRequirementException $e) { throw $e; } catch (WebThumbnailerException $e) { if (isset($options[static::VERBOSE]) && $options[static::VERBOSE] === true) { error_log($e->getMessage()); } if (isset($options[static::DEBUG]) && $options[static::DEBUG] === true) { throw $e; } return false; } } /** * @param int|string $maxWidth Either number of pixels or SIZE_SMALL|SIZE_MEDIUM|SIZE_LARGE. * * @return WebThumbnailer self instance. */ public function maxWidth($maxWidth): self { $this->maxWidth = (int) $maxWidth; return $this; } /** * @param int|string $maxHeight Either number of pixels or SIZE_SMALL|SIZE_MEDIUM|SIZE_LARGE. * * @return WebThumbnailer self instance. */ public function maxHeight($maxHeight): self { $this->maxHeight = (int) $maxHeight; return $this; } /** * @param bool $debug * * @return WebThumbnailer self instance. */ public function debug(bool $debug): self { $this->debug = $debug; return $this; } /** * @param bool $verbose * * @return WebThumbnailer self instance. */ public function verbose(bool $verbose): self { $this->verbose = $verbose; return $this; } /** * @param bool $nocache * * @return WebThumbnailer self instance. */ public function noCache(bool $nocache): self { $this->nocache = $nocache; return $this; } /** * @param bool $crop * * @return WebThumbnailer $this */ public function crop(bool $crop): self { $this->crop = $crop; return $this; } /** * @param int $downloadTimeout in seconds * * @return WebThumbnailer $this */ public function downloadTimeout(int $downloadTimeout): self { $this->downloadTimeout = $downloadTimeout; return $this; } /** * @param int $downloadMaxSize in bytes * * @return WebThumbnailer $this */ public function downloadMaxSize(int $downloadMaxSize): self { $this->downloadMaxSize = $downloadMaxSize; return $this; } /** * Enable download mode * It will download thumbnail, resize it and save it in the cache folder. * * @return WebThumbnailer $this */ public function modeDownload(): self { $this->downloadMode = static::DOWNLOAD; return $this; } /** * Enable hotlink mode * It will use image hotlinking if the domain authorize it, download it otherwise. * * @return WebThumbnailer $this */ public function modeHotlink(): self { $this->downloadMode = static::HOTLINK; return $this; } /** * Enable strict hotlink mode * It will use image hotlinking if the domain authorize it, fail otherwise. * * @return WebThumbnailer $this */ public function modeHotlinkStrict(): self { $this->downloadMode = static::HOTLINK_STRICT; return $this; } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/000077500000000000000000000000001376002032000253745ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/finder/000077500000000000000000000000001376002032000266435ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/finder/.gitkeep000066400000000000000000000000001376002032000302620ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/thumb/000077500000000000000000000000001376002032000265135ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/thumb/.gitkeep000066400000000000000000000000001376002032000301320ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/000077500000000000000000000000001376002032000263435ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/htaccess2.2_template000066400000000000000000000000541376002032000321770ustar00rootroot00000000000000Allow from {old_allow} Deny from {old_deny} php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/htaccess2.4_template000066400000000000000000000000261376002032000322000ustar00rootroot00000000000000Require all {new_all} php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/htaccess_template000066400000000000000000000004001376002032000317500ustar00rootroot00000000000000 = 2.4> Require all {new_all} Allow from {old_allow} Deny from {old_deny} Require all {new_all} php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/rules.json000066400000000000000000000200661376002032000303740ustar00rootroot00000000000000{ "flickR": { "domains": ["flickr.com"], "finder": "FlickR", "options": { "hotlink_allowed": true, "size": { "default": "medium", "thumb": { "param": "t", "maxwidth": "100", "maxheight": "100" }, "small": { "param": "m", "maxwidth": "240", "maxheight": "240" }, "smallplus": { "param": "n", "maxwidth": "320", "maxheight": "320" }, "medium": { "param": "", "maxwidth": "500", "maxheight": "500" }, "mediumplus": { "param": "z", "maxwidth": "640", "maxheight": "640" }, "large": { "param": "c", "maxwidth": "800", "maxheight": "800" }, "xlarge": { "param": "b", "maxwidth": "1024", "maxheight": "1024" }, "xxlarge": { "param": "h", "maxwidth": "1600", "maxheight": "1600" }, "xxxlarge": { "param": "k", "maxwidth": "2048", "maxheight": "2048" }, "_comment": "Original size is disabled because users may deny access to original size..." } }, "rules": { "image_regex": "assertTrue(is_dir($path)); $this->assertStringContainsString(self::$cache . CacheManager::TYPE_THUMB . '/', $path); $path = CacheManager::getCachePath(CacheManager::TYPE_FINDER); $this->assertTrue(is_dir($path)); $this->assertStringContainsString(self::$cache . CacheManager::TYPE_FINDER . '/', $path); } /** * Test getCachePath() with an invalid type. */ public function testGetCachePathInvalidType() { $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/Unknown cache type/'); CacheManager::getCachePath('nope'); } /** * Test getCachePath() without cache folder. */ public function testGetCachePathNoFolder() { $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/Cache folders are not writable/'); CacheManager::getCachePath(CacheManager::TYPE_THUMB, true); } /** * Test getCacheFilePath */ public function testGetCacheFilePathValid() { $url = 'http://whatever.io'; $domain = 'whatever.io'; $type = CacheManager::TYPE_THUMB; $width = 512; $height = 0; $cacheFile = CacheManager::getCacheFilePath($url, $domain, $type, $width, $height, false); $whateverDir = self::$cache . 'thumb/' . md5($domain) . '/'; $this->assertTrue(is_dir($whateverDir)); $this->assertStringContainsString($whateverDir, $cacheFile); // sha1 sum + dimensions $this->assertStringContainsString('0a35602901944a0c6d853da2a5364665c2bda069' . '51200' . '.jpg', $cacheFile); } /** * Test isCacheValid() with an existing file. */ public function testIsCacheValidExisting() { $domain = 'whatever.io'; $filename = '0a35602901944a0c6d853da2a5364665c2bda06951200.jpg'; mkdir(self::$cache . '/thumb/' . $domain, 0755, true); $cacheFile = self::$cache . '/thumb/' . $domain . '/' . $filename; touch($cacheFile); $this->assertTrue(CacheManager::isCacheValid($cacheFile, $domain, CacheManager::TYPE_THUMB)); } /** * Test isCacheValid() with an outdated file. */ public function testIsCacheValidExpired() { $domain = 'whatever.io'; $filename = '0a35602901944a0c6d853da2a5364665c2bda0695120.jpg'; mkdir(self::$cache . '/thumb/' . $domain, 0755, true); $cacheFile = self::$cache . '/thumb/' . $domain . '/' . $filename; touch($cacheFile, time() - ConfigManager::get('settings.cache_duration') - 1); $this->assertFalse(CacheManager::isCacheValid($cacheFile, $domain, CacheManager::TYPE_THUMB)); } /** * Test isCacheValid() without any file. */ public function testIsCacheValidNotExistent() { $domain = 'whatever.io'; $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertTrue(is_dir(self::$cache . '/thumb/' . md5($domain))); } /** * Test isCacheValid() without any file and infinite cache setting. */ public function testIsCacheValidInfiniteNotExistent() { $domain = 'whatever.io'; ConfigManager::addFile('tests/WebThumbnailer/resources/settings-infinite-cache.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertTrue(is_dir(self::$cache . '/thumb/' . md5($domain))); } /** * Test isCacheValid() with an existing file and infinite cache setting. */ public function testIsCacheValidInfiniteExisting() { $domain = 'whatever.io'; $filename = '0a35602901944a0c6d853da2a5364665c2bda06951200.jpg'; mkdir(self::$cache . '/thumb/' . $domain, 0755, true); $cacheFile = self::$cache . '/thumb/' . $domain . '/' . $filename; touch($cacheFile); ConfigManager::addFile('tests/WebThumbnailer/resources/settings-infinite-cache.json'); $this->assertTrue(CacheManager::isCacheValid($cacheFile, $domain, CacheManager::TYPE_THUMB)); } /** * Test isCacheValid() with an existing file and infinite cache setting. */ public function testIsCacheValidInfiniteExistingOneYear() { $domain = 'whatever.io'; $filename = '0a35602901944a0c6d853da2a5364665c2bda06951200.jpg'; mkdir(self::$cache . '/thumb/' . $domain, 0755, true); $cacheFile = self::$cache . '/thumb/' . $domain . '/' . $filename; touch($cacheFile, time() - 3600 * 24 * 31 * 12); ConfigManager::addFile('tests/WebThumbnailer/resources/settings-infinite-cache.json'); $this->assertTrue(CacheManager::isCacheValid($cacheFile, $domain, CacheManager::TYPE_THUMB)); } /** * Check that htaccess file is properly created (finder -> denied). */ public function testHtaccessCreationDenied() { $domain = 'whatever.io'; $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_FINDER)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess_denied', self::$cache . '/finder/.htaccess'); } /** * Check that htaccess file is properly created (thumb -> granted). */ public function testHtaccessCreationGranted() { $domain = 'whatever.io'; $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess_granted', self::$cache . '/thumb/.htaccess'); } /** * Check that htaccess file is properly created with Apache 2.2 forced (finder -> granted). */ public function testHtaccess22CreationDenied() { $domain = 'whatever.io'; ConfigManager::addFile(__DIR__ . '/../resources/settings-apache22.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_FINDER)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess22_denied', self::$cache . '/finder/.htaccess'); } /** * Check that htaccess file is properly created with Apache 2.2 forced (finder -> denied). */ public function testHtaccess22CreationGranted() { $domain = 'whatever.io'; ConfigManager::addFile(__DIR__ . '/../resources/settings-apache22.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess22_granted', self::$cache . '/thumb/.htaccess'); } /** * Check that htaccess file is properly created with Apache 2.4 forced (finder -> granted). */ public function testHtaccess24CreationDenied() { $domain = 'whatever.io'; ConfigManager::addFile(__DIR__ . '/../resources/settings-apache24.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_FINDER)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess24_denied', self::$cache . '/finder/.htaccess'); } /** * Check that htaccess file is properly created with Apache 2.4 forced (finder -> denied). */ public function testHtaccess24CreationGranted() { $domain = 'whatever.io'; ConfigManager::addFile(__DIR__ . '/../resources/settings-apache24.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess24_granted', self::$cache . '/thumb/.htaccess'); } /** * Check that htaccess file is properly created with Apache invalid version forced (finder -> granted). */ public function testHtaccessInvalidCreationDenied() { $domain = 'whatever.io'; ConfigManager::addFile(__DIR__ . '/../resources/settings-apache-ko.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_FINDER)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess_denied', self::$cache . '/finder/.htaccess'); } /** * Check that htaccess file is properly created with Apache invalid version forced (finder -> denied). */ public function testHtaccessInvalidCreationGranted() { $domain = 'whatever.io'; ConfigManager::addFile(__DIR__ . '/../resources/settings-apache-ko.json'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertFileEquals(__DIR__ . '/../resources/htaccess_granted', self::$cache . '/thumb/.htaccess'); } /** * Check that htaccess file is not overridden if it already exists */ public function testHtaccessDontOverride() { $domain = 'whatever.io'; $htaccessFile = self::$cache . '/thumb/.htaccess'; mkdir(self::$cache . '/thumb/', 0755, true); file_put_contents($htaccessFile, $content = 'kek'); $this->assertFalse(CacheManager::isCacheValid('nope', $domain, CacheManager::TYPE_THUMB)); $this->assertEquals($content, file_get_contents($htaccessFile)); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Application/ConfigManagerTest.php000066400000000000000000000063411376002032000332240ustar00rootroot00000000000000assertEquals('value', $value); $value = ConfigManager::get('nested.setting'); $this->assertEquals(2, count($value)); } /** * Load config file and read non existing keys. */ public function testLoadConfigNotFound() { ConfigManager::$configFiles = [FileUtils::getPath(__DIR__, '..', 'resources') . 'empty.json']; $value = ConfigManager::get(''); $this->assertEmpty($value); $value = ConfigManager::get('nope'); $this->assertEmpty($value); $value = ConfigManager::get('nope.nope'); $this->assertEmpty($value); $value = ConfigManager::get('nope', false); $this->assertEquals(false, $value); } /** * Load multiple config files with overriding value. */ public function testLoadConfigMultiFiles() { ConfigManager::$configFiles = [ FileUtils::getPath(__DIR__, '..', 'resources') . 'settingsok.json', FileUtils::getPath(__DIR__, '..', 'resources') . 'settings-multiple.json', ]; $value = ConfigManager::get('nested.setting.1.top'); $this->assertEquals('value', $value); $value = ConfigManager::get('key'); $this->assertEquals('value2', $value); } /** * Add a second config file, with overriding setting. */ public function testLoadConfigMultiFilesReloaded() { ConfigManager::$configFiles = [FileUtils::getPath(__DIR__, '..', 'resources') . 'settingsok.json']; $value = ConfigManager::get('nested.setting.1.top'); $this->assertEquals('value', $value); $this->assertEquals('foo', ConfigManager::get('key')); ConfigManager::$configFiles[] = FileUtils::getPath(__DIR__, '..', 'resources') . 'settings-multiple.json'; ConfigManager::reload(); $value = ConfigManager::get('key'); $this->assertEquals('value2', $value); } /** * Add a second config file, with overriding setting (using addFile()). */ public function testLoadConfigAddFile() { ConfigManager::$configFiles = [FileUtils::getPath(__DIR__, '..', 'resources') . 'settingsok.json']; $value = ConfigManager::get('nested.setting.1.top'); $this->assertEquals('value', $value); $this->assertEquals('foo', ConfigManager::get('key')); ConfigManager::addFile(FileUtils::getPath(__DIR__, '..', 'resources') . 'settings-multiple.json'); $value = ConfigManager::get('key'); $this->assertEquals('value2', $value); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Application/ThumbnailerTest.php000066400000000000000000000157131376002032000330010ustar00rootroot00000000000000getThumbnail(); $this->assertEquals(self::$gravatarThumb, $thumburl); } /** * Test strictHotlinkThumbnail() with a domains which doesn't allow hotlink. * * @expectedException \Exception * @expectedExceptionMessage Hotlink is not supported for this URL. */ public function testStrictHotlinkThumbnailInvalid() { // I don't know any website where hotlink is disabled. // FIXME! Use test rule. $this->markTestSkipped(); $options = [WebThumbnailer::HOTLINK_STRICT]; $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); //$this->assertEquals(self::$gravatarThumb, $thumburl); } /** * Test hotlinkThumbnail(). */ public function testHotlinkThumbnail() { $options = [WebThumbnailer::HOTLINK]; $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); $this->assertEquals(self::$gravatarThumb, $thumburl); } /** * Test hotlinkThumbnail() with a domains which doesn't allow hotlink => download mode. */ public function testHotlinkThumbnailDownload() { // I don't know any website where hotlink is disabled. // FIXME! Use test rule. $this->markTestSkipped(); $options = [WebThumbnailer::HOTLINK_STRICT]; $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); //$this->assertEquals(self::$gravatarThumb, $thumburl); } /** *Test downloadThumbnail(). */ public function testDownloadThumbnailValid() { $options = [WebThumbnailer::DOWNLOAD]; $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); $fileHash = hash('sha1', self::$gravatarThumb); $this->assertEquals('cache/thumb/' . md5('gravatar.com') . '/' . $fileHash . '1601600.jpg', $thumburl); unlink($thumburl); } /** *Test downloadThumbnail() with both width and height defined. */ public function testDownloadSizedThumbnailBoth() { $options = [ WebThumbnailer::DOWNLOAD, WebThumbnailer::MAX_WIDTH => 205, WebThumbnailer::MAX_HEIGHT => 205, ]; $fileHash = hash('sha1', self::$gravatarThumb); $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); $this->assertEquals('cache/thumb/' . md5('gravatar.com') . '/' . $fileHash . '2052050.jpg', $thumburl); $img = imagecreatefromjpeg($thumburl); $this->assertEquals(205, imagesx($img)); $this->assertEquals(205, imagesy($img)); imagedestroy($img); unlink($thumburl); } /** *Test downloadThumbnail() with both width and height defined with preset values. */ public function testDownloadSizedThumbnailBothPreset() { $options = [ WebThumbnailer::DOWNLOAD, WebThumbnailer::MAX_HEIGHT => WebThumbnailer::SIZE_SMALL, WebThumbnailer::MAX_WIDTH => WebThumbnailer::SIZE_SMALL, ]; $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); $fileHash = hash('sha1', 'http://gravatar.com/avatar/69ae657aa40c6c777aa2f391a63f327f?s=160'); $this->assertEquals('cache/thumb/' . md5('gravatar.com') . '/' . $fileHash . '1601600.jpg', $thumburl); $img = imagecreatefromjpeg($thumburl); $this->assertEquals(SizeUtils::getMetaSize(WebThumbnailer::SIZE_SMALL), imagesx($img)); $this->assertEquals(SizeUtils::getMetaSize(WebThumbnailer::SIZE_SMALL), imagesy($img)); imagedestroy($img); unlink($thumburl); } /** *Test downloadThumbnail() with height defined. */ public function testDownloadSizedThumbnailHeight() { $options = [ WebThumbnailer::DOWNLOAD, WebThumbnailer::MAX_HEIGHT => 205, ]; $fileHash = hash('sha1', self::$gravatarThumb); $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); $this->assertEquals('cache/thumb/' . md5('gravatar.com') . '/' . $fileHash . '02050.jpg', $thumburl); $img = imagecreatefromjpeg($thumburl); $this->assertEquals(205, imagesx($img)); $this->assertEquals(205, imagesy($img)); imagedestroy($img); unlink($thumburl); } /** *Test downloadThumbnail() with width defined. */ public function testDownloadSizedThumbnailWidth() { $options = [ WebThumbnailer::DOWNLOAD, WebThumbnailer::MAX_WIDTH => 205, ]; $fileHash = hash('sha1', self::$gravatarThumb); $thumbnailer = new Thumbnailer(self::$gravatarLink, $options, null); $thumburl = $thumbnailer->getThumbnail(); $this->assertEquals('cache/thumb/' . md5('gravatar.com') . '/' . $fileHash . '20500.jpg', $thumburl); $img = imagecreatefromjpeg($thumburl); $this->assertEquals(205, imagesx($img)); $this->assertEquals(205, imagesy($img)); imagedestroy($img); unlink($thumburl); } /** * Try to create an instance of Thumbnailer with incompatible settings. */ public function testDownloadBadConfigurationDownload() { $this->expectException(BadRulesException::class); $this->expectExceptionMessageRegExp( '/Only one of these flags can be set between: DOWNLOAD HOTLINK HOTLINK_STRICT/' ); $options = [ WebThumbnailer::DOWNLOAD, WebThumbnailer::HOTLINK_STRICT, ]; new Thumbnailer(self::$gravatarLink, $options, null); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Finder/000077500000000000000000000000001376002032000261135ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Finder/DefaultFinderTest.php000066400000000000000000000071711376002032000322060ustar00rootroot00000000000000assertEquals($url, $finder->find()); $url = 'http://domains.tld/image.JPG'; $finder = new DefaultFinder('', $url, [], []); $this->assertEquals($url, $finder->find()); $url = 'http://domains.tld/image.svg'; $finder = new DefaultFinder('', $url, [], []); $this->assertEquals($url, $finder->find()); } /** * Test the default finder with URL which does NOT match an image. */ public function testDefaultFinderNotImage() { $file = __DIR__ . '/../workdir/nope'; touch($file); $finder = new DefaultFinder('', $file, [], []); $this->assertFalse($finder->find()); @unlink($file); } /** * Test the default finder downloading an image without extension. */ public function testDefautFinderRemoteImage() { $file = __DIR__ . '/../workdir/image'; // From http://php.net/imagecreatefromstring $data = 'iVBORw0KGgoAAAANSUhEUgAAABwAAAASCAMAAAB/2U7WAAAABl' . 'BMVEUAAAD///+l2Z/dAAAASUlEQVR4XqWQUQoAIAxC2/0vXZDr' . 'EX4IJTRkb7lobNUStXsB0jIXIAMSsQnWlsV+wULF4Avk9fLq2r' . '8a5HSE35Q3eO2XP1A1wQkZSgETvDtKdQAAAABJRU5ErkJggg=='; file_put_contents($file, base64_decode($data)); $finder = new DefaultFinder('', $file, null, null); $this->assertEquals($file, $finder->find()); @unlink($file); } /** * Test the default finder trying to find an open graph link. */ public function testDefaultFinderOpenGraph() { $url = __DIR__ . '/../resources/default/le-monde.html'; $expected = 'https://img.lemde.fr/2016/10/21/107/0/1132/566/1440/720/60/0/fe3b107_3522-d2olbw.y93o25u3di.jpg'; $finder = new DefaultFinder('', $url, null, null); $this->assertEquals($expected, $finder->find()); } /** * Test the default finder trying to find an open graph link. */ public function testDefaultFinderOpenGraphRemote() { $url = self::LOCAL_SERVER . 'default/le-monde.html'; $expected = 'https://img.lemde.fr/2016/10/21/107/0/1132/566/1440/720/60/0/fe3b107_3522-d2olbw.y93o25u3di.jpg'; $finder = new DefaultFinder('', $url, null, null); $this->assertEquals($expected, $finder->find()); } /** * Test the default finder trying to find an image mime-type. */ public function testDefaultFinderImageMimetype() { $url = self::LOCAL_SERVER . 'default/image-mimetype.php'; $expected = $url; $finder = new DefaultFinder('', $url, null, null); $this->assertEquals($expected, $finder->find()); } /** * Test the default finder finding a non 200 status code. */ public function testDefaultFinderStatusError() { $url = self::LOCAL_SERVER . 'default/status-ko.php'; $finder = new DefaultFinder('', $url, null, null); $this->assertFalse($finder->find()); } /** * Test getName(). */ public function testGetName() { $finder = new DefaultFinder('', '', [], []); $this->assertEquals('default', $finder->getName()); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Finder/FinderFactoryTest.php000066400000000000000000000167001376002032000322270ustar00rootroot00000000000000assertEquals(UrlRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('http://youtube.com'); $this->assertEquals(UrlRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('https://youtube.com/stuff/bla.aspx?foo=bar#foobar'); $this->assertEquals(UrlRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('imgur.com/fds'); $this->assertEquals(UrlRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('i.imgur.com/fds'); $this->assertEquals(UrlRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('i.imgur.com/gallery/fds'); $this->assertEquals(QueryRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('gravatar.com/avatar/'); $this->assertEquals(UrlRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('twitter.com/status/'); $this->assertEquals(QueryRegexFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('instagram.com/p/stuff'); $this->assertEquals(QueryRegexFinder::class, get_class($finder)); } /** * Test getFinder() with an unsupported domain: it should return DefaultFinder. */ public function testGetFinderNotSupportedDomain() { $finder = FinderFactory::getFinder('somewhere.io'); $this->assertEquals(DefaultFinder::class, get_class($finder)); $finder = FinderFactory::getFinder('https://somewhere.io/stuff/index.php?foo=bar#foobar'); $this->assertEquals(DefaultFinder::class, get_class($finder)); } /** * Test getFinder() with support domains, but not valid URL: fallback to DefaultFinder. */ public function testGetFinderUrlRequirementInvalid() { $finder = FinderFactory::getFinder('gravatar.com'); $this->assertEquals(DefaultFinder::class, get_class($finder)); } /** * Test getThumbnailMeta() for Youtube. */ public function testGetThumbnailMetaYoutube() { // imgur single $data = FinderFactory::getThumbnailMeta('youtube.com', 'http://youtube.com/bla/bla'); $this->assertEquals('youtube.com', $data[0]); $this->assertEquals('UrlRegex', $data[1]); $this->assertEquals('v=([^&]+)', $data[2]['url_regex']); $this->assertEquals('https://img.youtube.com/vi/${1}/${size}.jpg', $data[2]['thumbnail_url']); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals('medium', $data[3]['size']['default']); // random size value $this->assertEquals('mqdefault', $data[3]['size']['medium']['param']); } /** * Test getThumbnailMeta() for Youtube short URL. */ public function testGetThumbnailMetaYoutubeShort() { // imgur single $data = FinderFactory::getThumbnailMeta('youtu.be', 'http://youtube.com/bla/bla'); $this->assertEquals('youtu.be', $data[0]); $this->assertEquals('UrlRegex', $data[1]); $this->assertEquals('youtu.be/([^&]+)', $data[2]['url_regex']); $this->assertEquals('https://img.youtube.com/vi/${1}/${size}.jpg', $data[2]['thumbnail_url']); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals('medium', $data[3]['size']['default']); // random size value $this->assertEquals('mqdefault', $data[3]['size']['medium']['param']); } /** * Test getThumbnailMeta() for Imgur single image. */ public function testGetThumbnailMetaImgur() { // imgur single $data = FinderFactory::getThumbnailMeta('i.imgur.com', 'http://imgur.com/bla/bla'); $this->assertEquals('imgur.com', $data[0]); $this->assertEquals('UrlRegex', $data[1]); $this->assertEquals('\.com/([\w\d]+)', $data[2]['url_regex']); $this->assertEquals('https://i.imgur.com/${1}${size}.jpg', $data[2]['thumbnail_url']); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals('medium', $data[3]['size']['default']); // random size value $this->assertEquals('m', $data[3]['size']['medium']['param']); } /** * Test getThumbnailMeta() for Imgur albums. */ public function testGetThumbnailMetaImgurAlbum() { // imgur single $data = FinderFactory::getThumbnailMeta('i.imgur.com', 'http://imgur.com/gallery/bla/bla'); $this->assertEquals('imgur.com', $data[0]); $this->assertEquals('QueryRegex', $data[1]); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals('medium', $data[3]['size']['default']); // random size value $this->assertEquals('m', $data[3]['size']['medium']['param']); } /** * Test getThumbnailMeta() for Imgur albums. */ public function testGetThumbnailMetaInstagram() { // imgur single $data = FinderFactory::getThumbnailMeta('instagram.com', 'http://instagram.com/p/bla/bla'); $this->assertEquals('instagram.com', $data[0]); $this->assertEquals('QueryRegex', $data[1]); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals(1, count($data[3])); } /** * Test getThumbnailMeta() for Twitter. */ public function testGetThumbnailMetaTwitter() { // imgur single $data = FinderFactory::getThumbnailMeta('twitter.com', 'http://twitter.com/status/bla/bla'); $this->assertEquals('twitter.com', $data[0]); $this->assertEquals('QueryRegex', $data[1]); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals(1, count($data[3])); } /** * Test getThumbnailMeta() for Twitter. */ public function testGetThumbnailMetaGravatar() { // imgur single $data = FinderFactory::getThumbnailMeta('gravatar.com', 'http://gravatar.com/avatar/bla/bla'); $this->assertEquals('gravatar.com', $data[0]); $this->assertEquals('UrlRegex', $data[1]); $this->assertEquals('(https?)://gravatar\\.com/avatar/(\\w+)', $data[2]['url_regex']); $this->assertEquals('${1}://gravatar.com/avatar/${2}?s=${size}', $data[2]['thumbnail_url']); $this->assertTrue($data[3]['hotlink_allowed']); $this->assertEquals('medium', $data[3]['size']['default']); // random size value $this->assertEquals('320', $data[3]['size']['medium']['param']); } /** * Test checkMetaFormat() with valid info. */ public function testCheckMetaFormatValid() { $this->addToAssertionCount(1); $meta = [ 'finder' => 'foo', 'domains' => ['bar'] ]; FinderFactory::checkMetaFormat($meta); $meta['foo'] = ['bar']; FinderFactory::checkMetaFormat($meta); } /** * Test checkMetaFormat() with invalid info. */ public function testCheckMetaFormatBadRules() { $this->expectException(BadRulesException::class); $meta = array('finder' => 'test'); FinderFactory::checkMetaFormat($meta); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Finder/FlickRFinderTest.php000066400000000000000000000060571376002032000317760ustar00rootroot00000000000000 ' '${1}', ]; self::$params = [ 'size' => [ 'default' => 'large', 'large' => [ 'param' => 'c', 'maxwidth' => 800, 'maxheight' => 800, ] ] ]; } /** * Test find() with an existing FlickR page loaded locally. */ public function testFlickRFinderExistingImage() { $url = __DIR__ . '/../resources/flickr/flickr-image.html'; $expected = 'https://c1.staticflickr.com/9/8657/29903845474_7d23197890_c.jpg'; $finder = new FlickRFinder('flickr.com', $url, self::$rules, self::$params); $this->assertEquals($expected, $finder->find()); } /** * Test find() with an existing FlickR page loaded locally. * We use the empty size to make sure it works: * one of thumb size doesn't have a suffix. */ public function testFlickRFinderEmptySuffix() { $url = __DIR__ . '/../resources/flickr/flickr-image.html'; $expected = 'https://c1.staticflickr.com/9/8657/29903845474_7d23197890.jpg'; self::$params['size']['large']['param'] = ''; $finder = new FlickRFinder('flickr.com', $url, self::$rules, self::$params); $this->assertEquals($expected, $finder->find()); } /** * Test find() with an existing FlickR page loaded locally. */ public function testFlickRFinderProfile() { $url = __DIR__ . '/../resources/flickr/flickr-profile.html'; $expected = 'https://c7.staticflickr.com/9/8562/29912456894_b3e6ddfe28_c.jpg'; $finder = new FlickRFinder('flickr.com', $url, self::$rules, self::$params); $this->assertEquals($expected, $finder->find()); } /** * Test find() with an existing FlickR page loaded locally. */ public function testFlickRFinderHomepage() { $url = __DIR__ . '/../resources/flickr/flickr-homepage.html'; $expected = 'https://farm4.staticflickr.com/3914/15118079089_489aa62638_c.jpg'; $finder = new FlickRFinder('flickr.com', $url, self::$rules, self::$params); $this->assertEquals($expected, $finder->find()); } /** * Load FlickR homepage, no image found. */ public function testFlickRFinderNoImage() { $url = __DIR__ . '/../resources/flickr/flickr-doc.html'; $finder = new FlickRFinder('flickr.com', $url, self::$rules, self::$params); $this->assertFalse($finder->find()); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Finder/QueryRegexFinderTest.php000066400000000000000000000364011376002032000327200ustar00rootroot00000000000000 '(.*?)', 'thumbnail_url' => 'https://domain.tld/pics/${1}?name=${2}', ]; } /** * Test find() with a valid thumb found. */ public function testQueryRegexFinderValid() { $url = __DIR__ . '/../resources/queryregex/one-thumb.html'; $expected = 'https://domain.tld/pics/thumb.png?name=text'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertEquals($expected, $finder->find()); } /** * Test find() with 2 valid thumbs matching the regex, we use the first one. */ public function testQueryRegexFinderTwoThumbs() { $url = __DIR__ . '/../resources/queryregex/two-thumb.html'; $expected = 'https://domain.tld/pics/thumb.png?name=text'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertEquals($expected, $finder->find()); } /** * Test find() with parameter. */ public function testQueryRegexFinderWithParameter() { $url = __DIR__ . '/../resources/queryregex/one-thumb.html'; $expected = 'https://domain.tld/pics/thumb.png?param=foobar-other'; self::$rules['thumbnail_url'] = 'https://domain.tld/pics/${1}?param=${option1}-${option2}'; $params = [ 'option1' => [ 'default' => 'name', 'name' => [ 'param' => 'foobar', ] ], 'option2' => [ 'default' => 'name', 'name' => [ 'param' => 'other', ] ] ]; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, $params); $this->assertEquals($expected, $finder->find()); } /** * Test find() with a valid thumb found. */ public function testQueryRegexFinderCurlValid() { $url = self::LOCAL_SERVER . 'queryregex/one-thumb.html'; $expected = 'https://domain.tld/pics/thumb.png?name=text'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertEquals($expected, $finder->find()); } /** * Test find() with 2 valid thumbs matching the regex, we use the first one. */ public function testQueryRegexFinderCurlTwoThumbs() { $url = self::LOCAL_SERVER . 'queryregex/two-thumb.html'; $expected = 'https://domain.tld/pics/thumb.png?name=text'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertEquals($expected, $finder->find()); } /** * Test find() with parameter. */ public function testQueryRegexFinderCurlWithParameter() { $url = self::LOCAL_SERVER . 'queryregex/one-thumb.html'; $expected = 'https://domain.tld/pics/thumb.png?param=foobar-other'; self::$rules['thumbnail_url'] = 'https://domain.tld/pics/${1}?param=${option1}-${option2}'; $params = [ 'option1' => [ 'default' => 'name', 'name' => [ 'param' => 'foobar', ] ], 'option2' => [ 'default' => 'name', 'name' => [ 'param' => 'other', ] ] ]; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, $params); $this->assertEquals($expected, $finder->find()); } /** * Test the default finder trying to find an image mime-type. */ public function testQueryRegexFinderImageMimetype() { $url = self::LOCAL_SERVER . 'default/image-mimetype.php'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertFalse($finder->find()); } /** * Test the default finder finding a non 200 status code. */ public function testQueryRegexFinderStatusError() { $url = self::LOCAL_SERVER . 'default/status-ko.php'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertFalse($finder->find()); } /** * Test getName(). */ public function testGetName() { $rules = [ 'image_regex' => 'foo', 'thumbnail_url' => 'bar', ]; $finder = new QueryRegexFinder('', '', $rules, []); $this->assertEquals('Query Regex', $finder->getName()); } /** * Test loading the finder with bad rules (`thumbnail_url`). */ public function testQueryRegexFinderBadRulesThumbUrl() { $this->expectException(BadRulesException::class); unset(self::$rules['thumbnail_url']); new QueryRegexFinder('domain.tld', '', self::$rules, null); } /** * Test loading the finder with bad rules (`image_regex`). */ public function testQueryRegexFinderBadRulesImageRegex() { $this->expectException(BadRulesException::class); unset(self::$rules['image_regex']); new QueryRegexFinder('domain.tld', '', self::$rules, null); } /** * Test downloading an inaccessible remote content (empty content). */ public function testQueryRegexFinderResourceNotReachable() { $finder = new QueryRegexFinder('domain.tld', '', self::$rules, null); $this->assertFalse($finder->find()); } /** * A page without thumbnails, return false. */ public function testQueryRegexFinderNoMatch() { $url = __DIR__ . '/../resources/queryregex/no-thumb.html'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertFalse($finder->find()); } /** * Not matching placeholder are ignored. */ public function testQueryRegexNoEnoughMatch() { $url = __DIR__ . '/../resources/queryregex/one-thumb.html'; $expected = 'thumb.png text ${3}'; self::$rules['thumbnail_url'] = '${1} ${2} ${3}'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $this->assertEquals($expected, $finder->find()); } /** * Use an unknown option in the URL. */ public function testQueryRegexUnknownOption() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Unknown option "option" for the finder "Query Regex"'); $url = __DIR__ . '/../resources/queryregex/one-thumb.html'; self::$rules['thumbnail_url'] = '${option}'; $finder = new QueryRegexFinder('domain.tld', $url, self::$rules, null); $finder->find(); } /** * Test Giphy. */ public function testQueryRegexGiphy() { $expected = 'https://media.giphy.com/media/8JQqAqsxNDUXu/giphy-facebook_s.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['giphy']['rules']; $options = $allRules['giphy']['options']; $url = __DIR__ . '/../resources/giphy/giphy-gif.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Imgur Album: multiple images on a single page, we take the first (OpenGraph choice). */ public function testQueryRegexImgurAlbum() { $expected = 'https://i.imgur.com/iQxE4BHm.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['imgur_album']['rules']; $options = $allRules['imgur_album']['options']; $url = __DIR__ . '/../resources/imgur/imgur-album.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Imgur Gallery: multiple images on a single page, we take the first (OpenGraph choice). * The difference between albums (/a/) and galleries (/gallery/), is that * a gallery has been published to the community and includes votes and comments. */ public function testQueryRegexImgurGallery() { $expected = 'https://i.imgur.com/iQxE4BHm.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['imgur_album']['rules']; $options = $allRules['imgur_album']['options']; $url = __DIR__ . '/../resources/imgur/imgur-gallery.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Instagram thumb: one picture */ public function testQueryRegexInstagramPicture() { $expected = 'https://scontent-cdg2-1.cdninstagram.com/t51.2885-15/sh0.08/e35/p750x750/' . '14719286_1129421600429160_916728922148700160_n.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['instagram']['rules']; $options = $allRules['instagram']['options']; $url = __DIR__ . '/../resources/instagram/instagram-picture.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Instagram thumb: profile, get the avatar */ public function testQueryRegexInstagramProfile() { $expected = 'https://scontent-cdg2-1.cdninstagram.com/t51.2885-19/s150x150/' . '11351823_506089142881765_717664936_a.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['instagram']['rules']; $options = $allRules['instagram']['options']; $url = __DIR__ . '/../resources/instagram/instagram-profile.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Pinterest thumb: single picture */ public function testQueryRegexPinterestPicture() { $expected = 'https://s-media-cache-ak0.pinimg.com/600x315/e0/7d/c0/e07dc09f93e12170fae7caa09329d815.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['pinterest']['rules']; $options = $allRules['pinterest']['options']; $url = __DIR__ . '/../resources/pinterest/pinterest-picture.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Pinterest thumb: profile picture */ public function testQueryRegexPinterestProfile() { $expected = 'https://s-media-cache-ak0.pinimg.com/avatars/sjoshua1_1367516806_140.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['pinterest']['rules']; $options = $allRules['pinterest']['options']; $url = __DIR__ . '/../resources/pinterest/pinterest-profile.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test The Oatmeal comic. */ public function testQueryRegexTheOatmealComic() { $expected = 'http://s3.amazonaws.com/theoatmeal-img/thumbnails/unhappy_big.png'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['theoatmeal']['rules']; $options = $allRules['theoatmeal']['options']; $url = __DIR__ . '/../resources/theoatmeal/theoatmeal-comic.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Twitter rules: no media, should use the avatar. */ public function testQueryRegexTwitterNoMedia() { $expected = 'https://pbs.twimg.com/profile_images/737009192758870016/I_p72JBK_400x400.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['twitter']['rules']; $options = $allRules['twitter']['options']; $url = __DIR__ . '/../resources/twitter/twitter-no-media.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Twitter rules: one media, should use it. */ public function testQueryRegexTwitterOneMedia() { $expected = 'https://pbs.twimg.com/media/CvilUtwWgAAQ46n.jpg:large'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['twitter']['rules']; $options = $allRules['twitter']['options']; $url = __DIR__ . '/../resources/twitter/twitter-single-media.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Twitter rules: multiple medias, should use the first one. */ public function testQueryRegexTwitterMultipleMedia() { $expected = 'https://pbs.twimg.com/media/CuKCNVBVUAU332-.jpg:large'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['twitter']['rules']; $options = $allRules['twitter']['options']; $url = __DIR__ . '/../resources/twitter/twitter-multiple-media.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Youtube profile page: use the avatar. */ public function testQueryRegexYoutubeProfile() { $expected = 'https://yt3.ggpht.com/-KLL2Lp8Zqso/AAAAAAAAAAI/AAAAAAAAAAA/Y0qd6h5C_jQ/' . 's900-c-k-no-mo-rj-c0xffffff/photo.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['youtube_profile']['rules']; $options = $allRules['youtube_profile']['options']; $url = __DIR__ . '/../resources/youtube/youtube-profile.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test XKCD comic. */ public function testQueryRegexXkcdComic() { $expected = '//imgs.xkcd.com/comics/movie_folder.png'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['xkcd']['rules']; $options = $allRules['xkcd']['options']; $url = __DIR__ . '/../resources/xkcd/xkcd-comic.html'; $finder = new QueryRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Finder/UrlRegexFinderTest.php000066400000000000000000000330701376002032000323540ustar00rootroot00000000000000addToAssertionCount(1); $rules = array( 'url_regex' => 'str', 'thumbnail_url' => 'str', ); $finder = new UrlRegexFinder('', '', $rules, null); $finder->checkRules($rules); } /** * Test checkRules() with invalid data. */ public function testCheckRulesMissingThumbUrl() { $this->expectException(BadRulesException::class); $rules = [ 'url_regex' => 'str', ]; $finder = new UrlRegexFinder('', '', $rules, null); $finder->checkRules($rules); } /** * Test checkRules() with invalid data. */ public function testCheckRulesMissingUrlRegex() { $this->expectException(BadRulesException::class); $rules = [ 'thumbnail_url' => 'str', ]; $finder = new UrlRegexFinder('', '', $rules, null); $finder->checkRules($rules); } /** * Test find() with basic replacements. */ public function testFind() { $url = 'http://test.io/q=id1&id2¬important'; $thumburl = 'http://test.io/img/id1/id2.png'; $rules = array( 'url_regex' => 'q=([^&]+)&([^&]+)', 'thumbnail_url' => 'http://test.io/img/${1}/${2}.png', ); $finder = new UrlRegexFinder('', $url, $rules, null); $this->assertEquals($thumburl, $finder->find()); } /** * Test find() with basic replacements plus size replacement. */ public function testFindWithSizeOptions() { $url = 'http://test.io/?123'; $rules = array( 'url_regex' => '/\\?([^&]+)', 'thumbnail_url' => 'http://test.io/${1}/${size}.png', ); $options = array( 'size' => array( 'default' => 'small', 'small' => array( 'param' => 'a', 'maxwidth' => 200, 'maxheight' => 200, ), 'medium' => array( 'param' => 'b', 'maxwidth' => 320, 'maxheight' => 320, ), 'large' => array( 'param' => 'c', 'maxwidth' => 640, 'maxheight' => 640, ) ), ); $userOptions = array( WebThumbnailer::MAX_HEIGHT => 200, WebThumbnailer::MAX_WIDTH => 200, ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/a.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( WebThumbnailer::MAX_HEIGHT => 200, WebThumbnailer::MAX_WIDTH => 201, ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/b.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( WebThumbnailer::MAX_HEIGHT => WebThumbnailer::SIZE_MEDIUM, ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/b.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( WebThumbnailer::MAX_HEIGHT => 199, ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/a.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( WebThumbnailer::MAX_HEIGHT => WebThumbnailer::SIZE_MEDIUM, WebThumbnailer::MAX_WIDTH => WebThumbnailer::SIZE_MEDIUM, ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/b.png'; $this->assertEquals($thumburl, $finder->find()); } /** * Test find() with basic replacements, and default options. */ public function testFindWithDefaultOptions() { $url = 'http://test.io/?123'; $rules = array( 'url_regex' => '/\\?([^&]+)', 'thumbnail_url' => 'http://test.io/${1}/${setting}.png', ); $options = array( 'setting' => array( 'default' => 'first', 'first' => array( 'param' => 'a', ), 'second' => array( 'param' => 'b', ), 'third' => array( 'param' => 'c', ), ) ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions(array()); $thumburl = 'http://test.io/123/a.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( 'setting' => 'second' ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/b.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( 'setting' => 'third' ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/c.png'; $this->assertEquals($thumburl, $finder->find()); } /** * Test find() with basic replacements, and default options using bad values. */ public function testFindWithDefaultOptionsBadValues() { $url = 'http://test.io/?123'; $rules = array( 'url_regex' => '/\\?([^&]+)', 'thumbnail_url' => 'http://test.io/${1}/${setting}.png', ); $options = array( 'setting' => array( 'default' => 'first', 'first' => array( 'param' => 'a', ), 'second' => array( 'param' => 'b', ), ) ); $userOptions = array( 'setting' => 'nope' ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/a.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( 'setting' => '' ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/a.png'; $this->assertEquals($thumburl, $finder->find()); $userOptions = array( 'setting' => array('other' => 'stuff') ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions($userOptions); $thumburl = 'http://test.io/123/a.png'; $this->assertEquals($thumburl, $finder->find()); } /** * Test find() with basic replacements, and default options * but without defining default values. */ public function testFindWithDefaultOptionsNoDefault() { $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/No default set for option/'); $url = 'http://test.io/?123'; $rules = array( 'url_regex' => '/\\?([^&]+)', 'thumbnail_url' => 'http://test.io/${1}/${setting}.png', ); $options = array( 'setting' => array() ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions(array()); $finder->find(); } /** * Test find() with basic replacements, and default options * with an invalid default option. */ public function testFindWithDefaultOptionsNoDefaultParam() { $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/No default parameter set for option/'); $url = 'http://test.io/?123'; $rules = array( 'url_regex' => '/\\?([^&]+)', 'thumbnail_url' => 'http://test.io/${1}/${setting}.png', ); $options = array( 'setting' => array( 'default' => 'unknown' ) ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions(array()); $finder->find(); } /** * Test find() with basic replacements, and options not matching anything. */ public function testFindWithDefaultOptionsUnknownOption() { $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/Unknown option/'); $url = 'http://test.io/?123'; $rules = array( 'url_regex' => '/\\?([^&]+)', 'thumbnail_url' => 'http://test.io/${1}/${unkown}.png', ); $options = array( 'setting' => array() ); $finder = new UrlRegexFinder('', $url, $rules, $options); $finder->setUserOptions(array()); $finder->find(); } /** * Test getName(). */ public function testGetName() { $rules = [ 'url_regex' => 'foo', 'thumbnail_url' => 'bar', ]; $finder = new UrlRegexFinder('', '', $rules, []); $this->assertEquals('URL regex', $finder->getName()); } /** * Test Gfycat permalink */ public function testQueryRegexGfycatPermalink() { $url = 'https://gfycat.com/RigidJadedBirdofparadise'; $expected = 'https://thumbs.gfycat.com/RigidJadedBirdofparadise-mobile.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['gfycat']['rules']; $options = $allRules['gfycat']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Gfycat in navigation mode (/detail) */ public function testQueryRegexGfycatNavigation() { $url = 'https://gfycat.com/detail/HoarseJubilantHadrosaurus?tagname=battlefield_one&tvmode=1'; $expected = 'https://thumbs.gfycat.com/HoarseJubilantHadrosaurus-mobile.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['gfycat']['rules']; $options = $allRules['gfycat']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Imgur single image */ public function testQueryRegexImgurSingle() { $url = 'http://i.imgur.com/iQxE4BH'; $expected = 'https://i.imgur.com/iQxE4BHm.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['imgur_single']['rules']; $options = $allRules['imgur_single']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Imgur Homepage (no thumb) */ public function testQueryRegexImgurHomepage() { $url = 'https://imgur.com/'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['imgur_single']['rules']; $options = $allRules['imgur_single']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertFalse($finder->find()); } /** * Test Youtube video link */ public function testQueryRegexYoutubeVideo() { $url = 'https://youtube.com/watch?v=videoid'; $expected = 'https://img.youtube.com/vi/videoid/mqdefault.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['youtube']['rules']; $options = $allRules['youtube']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } /** * Test Youtube: not a video link */ public function testQueryRegexYoutubeNotVideo() { $url = 'https://youtube.com/about'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['youtube']['rules']; $options = $allRules['youtube']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertFalse($finder->find()); } /** * Test Youtube video link */ public function testQueryRegexYoutubeShort() { $url = 'https://youtu.be/videoid&stuff'; $expected = 'https://img.youtube.com/vi/videoid/mqdefault.jpg'; $allRules = DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'rules.json'); $rules = $allRules['youtube_short']['rules']; $options = $allRules['youtube_short']['options']; $finder = new UrlRegexFinder('domain.tld', $url, $rules, $options); $this->assertEquals($expected, $finder->find()); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/TestCase.php000066400000000000000000000006751376002032000271400ustar00rootroot00000000000000expectExceptionMessageMatches($regularExpression); } else { parent::expectExceptionMessageRegExp($regularExpression); } } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Utils/000077500000000000000000000000001376002032000260045ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Utils/DataUtilsTest.php000066400000000000000000000021531376002032000312500ustar00rootroot00000000000000assertEquals('medium', $rules['imgur_single']['options']['size']['default']); } public function testLoadJsonResourceNoFile() { $this->expectException(\Exception::class); $this->expectExceptionMessage('JSON resource file not found or not readable.'); DataUtils::loadJson(FileUtils::RESOURCES_PATH . 'nope.json'); } public function testLoadJsonResourceBadSyntax() { $this->expectException(\Exception::class); $this->expectExceptionMessageRegExp('/An error occured while parsing JSON file: error code #/'); $path = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR; DataUtils::loadJson($path . 'badsyntax.json'); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Utils/FileUtilsTests.php000066400000000000000000000026161376002032000314450ustar00rootroot00000000000000assertRegExp('#^/.*?/tests/WebThumbnailer/resources/$#', $path); } /** * Test getPath() with a non existent path. */ public function testGetPathNonExistent() { $this->assertFalse(FileUtils::getPath(__DIR__, 'nope')); } /** * Test getPath() with a non existent path. */ public function testGetPathEmpty() { $this->assertFalse(FileUtils::getPath()); } /** * Test rmdir with a valid path. */ public function testRmdirValid() { mkdir('tmp'); mkdir('tmp/tmp'); touch('tmp/file'); touch('tmp/tmp/file'); $this->assertTrue(is_dir('tmp')); $this->assertTrue(is_dir('tmp/tmp')); FileUtils::rmdir('tmp'); $this->assertFalse(is_dir('tmp')); } /** * Test rmdir with a invalid paths. */ public function testRmdirInvalid() { $this->assertFalse(FileUtils::rmdir('nope/')); $this->assertFalse(FileUtils::rmdir('/')); $this->assertFalse(FileUtils::rmdir('')); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Utils/FinderUtilsTest.php000066400000000000000000000051401376002032000316050ustar00rootroot00000000000000assertEquals($formatted . $flags, $res); $res = FinderUtils::buildRegex($regex, false); $this->assertEquals($formatted, $res); $res = FinderUtils::buildRegex(false, $flags); $this->assertEquals('{}' . $flags, $res); } /** * Test checkMandatoryRules() with valid data. */ public function testCheckMandatoryRulesSimple() { $this->assertTrue(FinderUtils::checkMandatoryRules([], [])); $this->assertTrue(FinderUtils::checkMandatoryRules(['data'], [])); $this->assertTrue(FinderUtils::checkMandatoryRules(['data' => 'value'], ['data'])); $this->assertTrue(FinderUtils::checkMandatoryRules(['data' => false], ['data'])); $this->assertTrue(FinderUtils::checkMandatoryRules(['data' => '', 'other' => 'value'], ['data'])); } /** * Test checkMandatoryRules() with valid data and nested mandatory rules. */ public function testCheckMandatoryRulesNested() { $rules = [ 'foo' => 'bar', 'foobar' => [ 'nested' => 'rule', ] ]; $mandatory = [ 'foo', 'foobar' => ['nested'] ]; $this->assertTrue(FinderUtils::checkMandatoryRules($rules, $mandatory)); } /** * Test checkMandatoryRules() with invalid data. */ public function testCheckMandatoryRulesInvalidSimple() { $this->assertFalse(FinderUtils::checkMandatoryRules([], ['rule'])); $this->assertFalse(FinderUtils::checkMandatoryRules(['rule' => ''], ['rule', 'other'])); $this->assertFalse(FinderUtils::checkMandatoryRules(['other' => 'value'], ['rule'])); } /** * Test checkMandatoryRules() with invalid data and nested mandatory rules. */ public function testCheckMandatoryRulesInvalidNested() { $rules = [ 'foo' => 'bar', 'foobar' => [ 'nested' => [ 'missing' => 'rule', ] ] ]; $mandatory = [ 'foo', 'foobar' => ['nested' => ['nope']] ]; $this->assertFalse(FinderUtils::checkMandatoryRules($rules, $mandatory)); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Utils/ImageUtilsTest.php000066400000000000000000000112261376002032000314220ustar00rootroot00000000000000assertEquals(28, imagesx($im)); $this->assertEquals(18, imagesy($im)); unlink($path); // Reduce size. ImageUtils::generateThumbnail(self::$imageSource, $path, 14, 9); $im = imagecreatefromjpeg($path); $this->assertEquals(14, imagesx($im)); $this->assertEquals(9, imagesy($im)); unlink($path); // Bigger size: must be changed to original size. ImageUtils::generateThumbnail(self::$imageSource, $path, 56, 36); $im = imagecreatefromjpeg($path); $this->assertEquals(28, imagesx($im)); $this->assertEquals(18, imagesy($im)); unlink($path); // Only specify width. ImageUtils::generateThumbnail(self::$imageSource, $path, 14, 0); $im = imagecreatefromjpeg($path); $this->assertEquals(14, imagesx($im)); $this->assertEquals(9, imagesy($im)); unlink($path); // Only specify heigth. ImageUtils::generateThumbnail(self::$imageSource, $path, 0, 9); $im = imagecreatefromjpeg($path); $this->assertEquals(14, imagesx($im)); $this->assertEquals(9, imagesy($im)); unlink($path); } /** * Generate a thumbnail into a non existent folder => Exception. */ public function testGenerateThumbnailUnwritable() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Target file is not writable.'); $path = self::$WORKDIR . 'nope' . DIRECTORY_SEPARATOR . 'file'; @ImageUtils::generateThumbnail(self::$imageSource, $path, 28, 18); } /** * Generate a thumbnail from a string which is not an image => NotAnImageException. */ public function testGenerateThumbnailNotAnImage() { $this->expectException(NotAnImageException::class); $path = self::$WORKDIR . 'file1.png'; ImageUtils::generateThumbnail('virus.exe', $path, 28, 18); } /** * Generate a thumbnail with bad sizes => Exception. */ public function testGenerateThumbnailBadSize() { $this->expectException(ImageConvertException::class); $this->expectExceptionMessage('Height and width must be zero or positive'); $path = self::$WORKDIR . 'file1.png'; @ImageUtils::generateThumbnail(self::$imageSource, $path, -1, -1); } /** * Generate a thumbnail with bad sizes (Double 0) => Exception. */ public function testGenerateThumbnailBadSizeDoubleZero() { $this->expectException(\Exception::class); $this->expectExceptionMessage('At least maxwidth or maxheight needs to be defined.'); $path = self::$WORKDIR . 'file1.png'; @ImageUtils::generateThumbnail(self::$imageSource, $path, 0, 0); } /** * Check that a string is an image. */ public function testIsStringImageTrue() { $this->assertTrue(ImageUtils::isImageString(self::$imageSource)); } /** * Check that a string is not an image. */ public function testIsStringImageFalse() { $this->assertFalse(ImageUtils::isImageString('string')); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/Utils/UrlUtilsTest.php000066400000000000000000000034261376002032000311450ustar00rootroot00000000000000assertEquals($expectedDomain, UrlUtils::getDomain('domain.tld')); $this->assertEquals($expectedDomain, UrlUtils::getDomain('https://domain.tld/blabla/file.php?foo=bar#foobar')); $this->assertEquals($expectedDomain, UrlUtils::getDomain('https://domain.tld:443/file.php?foo=bar#foobar')); $this->assertEquals($expectedDomain, UrlUtils::getDomain('ftp://DOMAIN.TLD/blabla/file.php?foo=bar#foobar')); $this->assertEquals('sub.' . $expectedDomain, UrlUtils::getDomain('sub.domain.tld')); $this->assertEquals('localhost', UrlUtils::getDomain('localhost')); } /** * Test getUrlFileExtension from various URL/file type. */ public function testGetUrlFileExtension() { $url = 'http://hostname.tld/path/index.php?arg=value#anchor'; $this->assertEquals('php', UrlUtils::getUrlFileExtension($url)); $url = 'http://hostname.tld/path/INDEX.PHP?arg=value#anchor'; $this->assertEquals('php', UrlUtils::getUrlFileExtension($url)); $url = 'http://hostname.tld/path/INDEX.tar.gz?arg=value#anchor'; $this->assertEquals('gz', UrlUtils::getUrlFileExtension($url)); $url = 'http://hostname.tld/path/?arg=value#anchor'; $this->assertEquals('', UrlUtils::getUrlFileExtension($url)); $url = 'http://hostname.tld/path/file.php/otherpath/?arg=value#anchor'; $this->assertEquals('', UrlUtils::getUrlFileExtension($url)); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/WebThumbnailerTest.php000066400000000000000000000306101376002032000311650ustar00rootroot00000000000000regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . $image; $wt = new WebThumbnailer(); $thumb = $wt->thumbnail($url); $this->assertEquals(base64_encode(file_get_contents($expected)), base64_encode(file_get_contents($thumb))); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL without extension. */ public function testDirectImageWithoutExtension() { $image = 'default/image'; $this->regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . $image; $wt = new WebThumbnailer(); $thumb = $wt->thumbnail($url); $this->assertFileEquals($expected, $thumb); } /** * URL which contains an opengraph image. */ public function testOpenGraphImage() { $image = 'default/le-monde.jpg'; $this->regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/le-monde.html'; $wt = new WebThumbnailer(); $thumb = $wt->thumbnail($url); $this->assertFileEquals($expected, $thumb); } /** * URL which contains an opengraph image with absolute path explicitly set. */ public function testOpenGraphImageAbsolute() { $image = 'default/le-monde.png'; $this->regenerate($image); mkdir(self::$tmp); file_put_contents( $conf = self::$tmp . 'tmp.json', json_encode([ 'settings' => [ 'path' => [ 'cache' => self::$cache, ], ], ]) ); ConfigManager::addFile($conf); $expected = self::$cache . 'thumb/421aa90e079fa326b6494f812ad13e79/8f72b887d2e3f64c3a1c719d8058823047d3ec031601600.jpg'; $url = self::LOCAL_SERVER . 'default/le-monde.html'; $wt = new WebThumbnailer(); $thumb = $wt->thumbnail($url); $this->assertEquals($expected, $thumb); $this->assertFileEquals($expected, $thumb); } /** * Get a file URL which isn't an image. */ public function testNotAnImage() { $oldlog = ini_get('error_log'); ini_set('error_log', '/dev/null'); $image = 'default/not-image.txt'; $url = self::LOCAL_SERVER . $image; $wt = new WebThumbnailer(); $this->assertFalse($wt->thumbnail($url)); ini_set('error_log', $oldlog); } /** * Simple image URL in download mode, resizing with max width. */ public function testDownloadDirectImageResizeWidth() { $image = 'default/image-width-341.png'; $this->regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/image.png'; $wt = new WebThumbnailer(); $wt = $wt->maxWidth(341); $thumb = $wt->thumbnail($url); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL in download mode, resizing with max height. */ public function testDownloadDirectImageResizeHeight() { $image = 'default/image-height-341.png'; $this->regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/image.png'; $wt = new WebThumbnailer(); $wt = $wt->maxHeight(341); $thumb = $wt->thumbnail($url); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL in download mode, resizing with max width and max height. */ public function testDownloadDirectImageResizeBothWidth() { $image = 'default/image-width-341.png'; $this->regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/image.png'; $wt = new WebThumbnailer(); $wt = $wt->maxWidth(341)->maxHeight(341); $thumb = $wt->thumbnail($url); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL in download mode, resizing with max height and max height, with vertical image. */ public function testDownloadDirectImageResizeBothHeight() { $image = 'default/image-vertical-height-341.png'; $this->regenerate($image); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/image-vertical.png'; $wt = new WebThumbnailer(); $wt = $wt->maxHeight(341)->maxWidth(341); $thumb = $wt->thumbnail($url); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL in download mode, crop enabled without both dimensions. */ public function testDownloadDirectImageResizeWidthCrop() { $oldlog = ini_get('error_log'); ini_set('error_log', '/dev/null'); $url = self::LOCAL_SERVER . 'default/image.png'; $wt = new WebThumbnailer(); $wt = $wt->maxWidth(341)->crop(true); $this->assertFalse(@$wt->thumbnail($url)); ini_set('error_log', $oldlog); } /** * Simple image URL in download mode, crop enabled without both dimensions. */ public function testDownloadDirectImageResizeHeightCrop() { $oldlog = ini_get('error_log'); ini_set('error_log', '/dev/null'); $url = self::LOCAL_SERVER . 'default/image.png'; $wt = new WebThumbnailer(); $wt = $wt->maxHeight(341)->crop(true); $this->assertFalse($wt->thumbnail($url)); ini_set('error_log', $oldlog); } /** * Simple image URL in download mode, resizing with max height/width + crop. */ public function testDownloadDirectImageResizeWidthHeightCrop() { $image = 'default/image-crop-341-341.png'; $this->regenerate($image, true); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/image-crop.png'; $wt = new WebThumbnailer(); $wt = $wt->maxHeight(341)->maxWidth(341)->crop(true); $thumb = $wt->thumbnail($url); $this->assertEquals(base64_encode(file_get_contents($expected)), base64_encode(file_get_contents($thumb))); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL in download mode, resizing with max height/width + crop. * Override max heigth/width using array settings. */ public function testDownloadDirectImageResizeWidthHeightCropOverride() { $image = 'default/image-crop-120-160.png'; $this->regenerate($image, true); $expected = self::$regenerated . $image; $url = self::LOCAL_SERVER . 'default/image-crop.png'; $wt = new WebThumbnailer(); $wt = $wt->maxHeight(341)->maxWidth(341)->crop(true); $thumb = $wt->thumbnail( $url, [ WebThumbnailer::MAX_WIDTH => 120, WebThumbnailer::MAX_HEIGHT => 160, ] ); $this->assertEquals(base64_encode(file_get_contents($expected)), base64_encode(file_get_contents($thumb))); $this->assertFileEquals($expected, $thumb); } /** * Simple image URL, in hotlink mode. */ public function testHotlinkSimpleImage() { $url = self::LOCAL_SERVER . 'default/image.png'; $wt = new WebThumbnailer(); $thumb = $wt->modeHotlink()->thumbnail($url); $this->assertEquals($url, $thumb); } /** * Simple image URL without extension, in hotlink mode. */ public function testHotlinkSimpleImageWithoutExtension() { $url = self::LOCAL_SERVER . 'default/image'; $wt = new WebThumbnailer(); $thumb = $wt->modeHotlink()->thumbnail($url); $this->assertEquals($url, $thumb); } /** * Simple opengraph URL, in hotlink mode. */ public function testHotlinkOpenGraph() { $expected = 'https://img.lemde.fr/2016/10/21/107/0/1132/566/1440/720/60/0/fe3b107_3522-d2olbw.y93o25u3di.jpg'; $url = self::LOCAL_SERVER . 'default/le-monde.html'; $wt = new WebThumbnailer(); $thumb = $wt->modeHotlink()->thumbnail($url); $this->assertEquals($expected, $thumb); } /** * Simple opengraph URL, in hotlink mode set by config file. */ public function testHotlinkOpenGraphJsonConfig() { $expected = 'https://img.lemde.fr/2016/10/21/107/0/1132/566/1440/720/60/0/fe3b107_3522-d2olbw.y93o25u3di.jpg'; $url = self::LOCAL_SERVER . 'default/le-monde.html'; $wt = new WebThumbnailer(); ConfigManager::addFile('tests/WebThumbnailer/resources/settings-hotlink.json'); $thumb = $wt->thumbnail($url); $this->assertEquals($expected, $thumb); } /** * Duplicate expected thumbnails using the current GD version. * * Different versions of GD will result in slightly different images, * which would make the comparaison test fail. By regenerating expected thumbs, * the expected and actual result should be the same. * * @param string $image relative path of the expected thumb inside the expected thumb directory. * @param bool $crop Set to true to apply the crop function. * @param int[] $cropParameters Crop parameters: x, y, width and height * * @throws \Exception couldn't create the image. */ public function regenerate($image, $crop = false, $cropParameters = []) { $targetFolder = dirname(self::$regenerated . $image); if (! is_dir($targetFolder)) { mkdir($targetFolder, 0755, true); } $content = file_get_contents(self::$expected . $image); $sourceImg = @imagecreatefromstring($content); $width = imagesx($sourceImg); $height = imagesy($sourceImg); $targetImg = imagecreatetruecolor($width, $height); if ( !imagecopyresized( $targetImg, $sourceImg, 0, 0, 0, 0, $width, $height, $width, $height ) ) { @imagedestroy($sourceImg); @imagedestroy($targetImg); throw new \Exception('Could not generate the thumbnail from source image.'); } if ($crop) { if (!empty($cropParameters)) { [$x, $y, $cropWidth, $croptHeight] = $cropParameters; } $targetImg = imagecrop($targetImg, [ 'x' => $x ?? 0, 'y' => $y ?? 0, 'width' => $cropWidth ?? $width, 'height' => $croptHeight ?? $height ]); } $target = self::$regenerated . $image; imagedestroy($sourceImg); imagejpeg($targetImg, $target); imagedestroy($targetImg); } } php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/000077500000000000000000000000001376002032000267165ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/badrules.json000066400000000000000000000002631376002032000314130ustar00rootroot00000000000000{ "domains": { "badrules.com": { "rules": { "url_regex": "v=([^&])", "thumbnail_url": "https://img.youtube.com/vi/${1}/default.jpg" } } } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/badsyntax.json000066400000000000000000000000241376002032000316020ustar00rootroot00000000000000[ "test": "test" ]php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/empty.json000066400000000000000000000000021376002032000307370ustar00rootroot00000000000000[]php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/000077500000000000000000000000001376002032000320175ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/default/000077500000000000000000000000001376002032000334435ustar00rootroot00000000000000image000066400000000000000000000005731376002032000343760ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultPNG  IHDRxn8 pHYs+-IDATxA @ӾBq`9tepqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpq~aeIENDB`image-crop-120-160.png000066400000000000000000000026671376002032000367340ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultPNG  IHDRxHk? pHYs+iIDATxMlu}v.6 A#$U=x$Fm!߉M< i4DDL5!R.ݷv<θysN|fggTM!Dp`hahahahahahahahahahahahahahahahahahahahahahahahahahӞ_P:5^=GWCwr/#p_S8.j<Z{yɚGLbBb\ūI$u^ {D`B#R7֙~H BS&o^s(z0of[#H֩77Lr7O|o[LhGΚ|Nc7B׸K|:c͆gW1I#<=4?ѷM7P+_rqIyW֞-ָn:O#Z\o(==y[o\)ԡ?{ zH:[r5}K|oŰ#&oyFZv=#4NкΡ_-ni~WJc#q}ycS# ICy3R)74$ Cߘ34@2?Mo:"@;tLǶ Γ# nƑĒjyZ YsBgwh2|}sy٨yr:Z=J~'חe ꋱ7jpg#Lp^h՗bkx{|ƍZ!Qhǩf< %Ʌ7/9Sy KV-ɶqjA(pjIɵ6`rP싻r5H0%zt [/Qz4,0&񡳯/z:?9¸24*^]Ͱߌ㽝FFKmzWW[7+&/NR vײo=xXq޿6.Bx8o7A&MOF/Ltx1SVY5q)T]mX_)J;lXz,3}13S3]y8&ngF{6 $ZyvU;oXп~{jS{bnn^ѿ:r{%38 Er"o}-8sO/hgolKܜq'RRĊT}o.ˍXWF?_GS/3|Z9{Sv>LпH OL;?KF0%##_/ܑG[7'9iM!^4,x鎫K2&L.<@V)l~o}:V gN_c"aS{ W7;oD[e)1f恥&s ߑ%;B~30,g z9ߑFƘ);6ώ.~^+'іU U6:+17gEKNi]+mg^ .Ȓ.L| |u$>Cc6. PWQxG4fqi?.t͵D/: g [[d"phDlK77@6]Vݑ[x|Y_pέ;^v?vl# .B o^|^tb wn 7 AwdUϚ^h]An0Ǘ^Ox=,,̺`~Ԧe?pX'm4߳~.XX1/11ymqt.u)^Nb•K0݊0ڧhF۽zqqtc8~eO?|\f{>soA !d$-?9o W86yźV_,i& O[v z⒄pCW^҅I̸M?paCޭ8IQ1/&'^7 @3iD3g6mrLٸ-g>&?pVGVrIۊ–. #.ýG%USHi!S7k.Gd EtF$(wD甙X̛y2(~K ϊ5qr-VI|˱x^|VN|Zy{CvghM?3e=^4݇:t?]E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E.t?]@E+?;`IENDB`image-height-341.png000066400000000000000000000023541376002032000367330ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultPNG  IHDRUؓ pHYs+IDATxԱ 0 :>zKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2 Td, cKX*@R2=m/IENDB`image-vertical-height-341.png000066400000000000000000000015541376002032000405430ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultPNG  IHDRU pHYs+IDATxA O k,ٚs;O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?4O?I=CjIENDB`image-width-341.png000066400000000000000000000015111376002032000365740ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultPNG  IHDRUΞE pHYs+IDATx1  >]sIw?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?t].C?tI I,IENDB`image.png000066400000000000000000000005731376002032000351610ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultPNG  IHDRxn8 pHYs+-IDATxA @ӾBq`9tepqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpqgpq~aeIENDB`le-monde.jpg000066400000000000000000000064571376002032000356020ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/expected-thumbs/defaultJFIF``>CREATOR: gd-jpeg v1.0 (using IJG JPEG v80), default quality C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222P" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?aR`b8dtlP }3`}=?yr<9Xsy#~ ;Y:m^1(fy+#%NAc#4` vD7*w'URVf1Ƞٰo劐prJGPuua woh+.Lɒ%8?)׷zT.w9>4pN=yzdha3sLOC '緿O\ӝG8 ~>G*qoA #U kBV;@9^jT:1A{! ϑH#:VKE'xoh^9ReP@8-@NtNzǏ֑o I JtMF[S<)L#;oʀ*+I,ǜNShr zz 7n8),0;u@Cg8_^sD"㌑D ӽ+if=GQ@ Ndw*lT'ȸ$9Jq~'ؐ瑅 2M\n?ZS*W',? "ć t/C@|:F7|I?n `tp?*Hޘ-<~9Ldʀ,>0ݓ;Mbۃ0jHG##!q^ǽW9FQ?1R˕) 8cI#=3v@%Xsޖe;x?ր,esRhF7gݺ߯CYOy G9,r$@n qndaq*G;Ho>~9Ϲe;x#<Wߎ gsը $v*vqsPpy$^Ґx3 0C98z=X̌籦FU< `tϯ\SAic`qhd 1Sl0ČqО\9UI#~#"+22s׵Èr݇CR,o'p*d'mcwt7,4*g-(%J W>A%szXq)m^zc{Lqf*@ώszS /y\ S A,@'9nS#ߚceXqVt5Y:s)(N ʞ9=8$ӯE/$c=<桗n$gބ1(˂H'߭K zR͹1?^O9BvUro@ ̤d@'S9u=oLn< AHvR7ls׭HrW)QQp2;xq!' z{{t8sYmJP:Q=ԊX Z&\>P֬ >z ߬buWEoݛKyBH?ǿJbv,JARe$R0rI5oYPw`>x:ELq}?׊e-9+8_u?_os{ +|˂ K$t8Goh x;wtw!6AX9 Ǯp}EX?Fɐ0%a<`^2;qd$rnR(#,K?d8 ۜAphp-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/htaccess22_denied000066400000000000000000000000361376002032000321110ustar00rootroot00000000000000Allow from none Deny from all php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/htaccess22_granted000066400000000000000000000000361376002032000323050ustar00rootroot00000000000000Allow from all Deny from none php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/htaccess24_denied000066400000000000000000000000231376002032000321070ustar00rootroot00000000000000Require all denied php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/htaccess24_granted000066400000000000000000000000241376002032000323040ustar00rootroot00000000000000Require all granted php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/htaccess_denied000066400000000000000000000003541376002032000317500ustar00rootroot00000000000000 = 2.4> Require all denied Allow from none Deny from all Require all denied php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/htaccess_granted000066400000000000000000000003561376002032000321460ustar00rootroot00000000000000 = 2.4> Require all granted Allow from all Deny from none Require all granted php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/queryregex/000077500000000000000000000000001376002032000311165ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/queryregex/no-thumb.html000066400000000000000000000000251376002032000335320ustar00rootroot00000000000000

No thumb here.

php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/queryregex/one-thumb.html000066400000000000000000000001051376002032000336760ustar00rootroot00000000000000

One thumb here.

textphp-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/queryregex/two-thumb.html000066400000000000000000000001651376002032000337340ustar00rootroot00000000000000

One thumb here.

text textphp-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settings-apache-ko.json000066400000000000000000000000641376002032000332770ustar00rootroot00000000000000{ "settings": { "apache_version": "3.25" } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settings-apache22.json000066400000000000000000000000631376002032000330330ustar00rootroot00000000000000{ "settings": { "apache_version": "2.2" } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settings-apache24.json000066400000000000000000000000631376002032000330350ustar00rootroot00000000000000{ "settings": { "apache_version": "2.4" } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settings-hotlink.json000066400000000000000000000002671376002032000331240ustar00rootroot00000000000000{ "settings": { "default": { "cache_duration": 3600, "download_mode": "HOTLINK" }, "path": { "cache": "tests/WebThumbnailer/workdir/cache/" } } }settings-infinite-cache.json000066400000000000000000000001721376002032000342360ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources{ "settings": { "cache_duration": -1, "path": { "cache": "tests/WebThumbnailer/workdir/cache/" } } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settings-multiple.json000066400000000000000000000000251376002032000332770ustar00rootroot00000000000000{ "key": "value2" }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settings-useful.json000066400000000000000000000001741376002032000327540ustar00rootroot00000000000000{ "settings": { "cache_duration": 3600, "path": { "cache": "tests/WebThumbnailer/workdir/cache/" } } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/resources/settingsok.json000066400000000000000000000001611376002032000320010ustar00rootroot00000000000000{ "key": "foo", "nested": { "setting": [ "nope", { "top": "value" } ] } }php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/workdir/000077500000000000000000000000001376002032000263655ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/WebThumbnailer/workdir/.gitkeep000066400000000000000000000000001376002032000300040ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/bootstrap.php000066400000000000000000000013151376002032000245220ustar00rootroot00000000000000/dev/null 2>&1 & echo $!', WEB_SERVER_HOST, WEB_SERVER_PORT, WEB_SERVER_DOCROOT ); // Execute the command and store the process ID $output = array(); exec($command, $output); $pid = (int) $output[0]; echo sprintf( '%s - Web server started on %s:%d with PID %d', date('r'), WEB_SERVER_HOST, WEB_SERVER_PORT, $pid ) . PHP_EOL; // Kill the web server when the process ends register_shutdown_function(function () use ($pid) { echo sprintf('%s - Killing process with ID %d', date('r'), $pid) . PHP_EOL; exec('kill ' . $pid); }); require_once __DIR__ . '/../vendor/autoload.php'; php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/000077500000000000000000000000001376002032000232525ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/default/000077500000000000000000000000001376002032000246765ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/default/image000066400000000000000000000062651376002032000257140ustar00rootroot00000000000000PNG  IHDR XvpsBIT|d lIDATxױ0?/j\еss@`a@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2 c@2B@}IENDB`php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/default/image-crop.png000066400000000000000000000234011376002032000274270ustar00rootroot00000000000000PNG  IHDR XvpsBIT|d IDATxyeừzMu6CPaE qAD {EfqFOsFgqgѰ":((> ,IN:^c[]SuNst}߶X0w d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@lioBSSJ뼇5[ϋ;!Np 1>""]hƪSoӗ%}p]C6}k^(M&V"@h_ڵ_'N\pZq6Y.,tN'ZMF0OqkJm,[o1Dj15zҺ^tO JKG7]i1o] V$@j5.<Z/o+o22umݯ mc V%@;Z6!iևNylD֐5 e'N5쵶_xz@Ʒ/ֻC<`6 mp,h쯒g> 5 @ ԧZVOֻTj\ @k ԥg`mva>"<JCO:S%/,$@(e/oI?l{Q/>(׳ @!=,xѱe0, ld\y²lJrs[&mֻa}מu曏92bj"L l(ޕq5`+5eOs /h+nm|G趌ĚyM@c^KD{zoM XDo>&@ϼ%;(oJ>  =Ϝ>?(OmF5{.vn=/6-a= cxxٹ $ G+b$:ZXuGy NcmO CQO* CKԯ-,[}{0}7.Йw}RQ?h\t-g0}ɱ8m`J=ɧEϦ`zWڰ>޸˼Bgt퇣ڵҿb\3:W음Xdp {ӳCls(F1O^%ʥb=S mxÍ}͇pn b\29Wحw7o]\_J7isv`Yy0S.t]^G3ѿ<H~= +j8%TS*1ڐ[`9s8dyDEXÓdQ* $ s_`_݉7sdD9<:0 Й;6D7~WQoxW 99N>OE]o=j_nM>gG l71WБqC-`YXsxd`~r(tq΋ZGіqHwiZ+ ٬ZKO)GD%)$s m Y{`]?Oޞֻҿ<>sX9! e//td||C|nvo87ǰcm``Z>8ZGOc|'4 L6ɸ}oOLڻϧ%a/>3J㛒,3xR_G37ҷc3;zu ,>H4.3;s"?Y<.by ng'ڨ~g_nXKO1D"G^ߜR:__O>3G6 ~P-:299>D MSL>fO;^`ch/Ѩv{ZzNkj93 U ;-u_NKޣy}&F@ qʅW•6joztv./I>0jq)ŮXuN ixo6'ϫQMq/Np;:qGKHPp$eKO^9sN~i񧆿{7"J?([#\|OcZN TFׇ&ҽ Risq?}1QMuge`W?<<2ƪ9Smr,JPKl hpT]q˓LTE>vymQ_ޕ݉sK$ 3Pxbwڴ%(3>cX/?vQ9sϼ" kxQn|E/vsaF6{,vju2y6ʬ+c{5.F0b  i-zJme%0d|K|/>?ᦃ,D ?N>O>`. 3NXQGw^k,FsӳC<|e9s)|V;`__H'V%0g`mس?jkDΞ8cxΙѱeC9syz[3Gbb~kFf~QN>T*;$0V79>]4~T[s޲ꈈyA?@N={StrONQygF] }5Nd\u²N?g]D[mTcG&9O@ ݰ>m/tfll]\wXZT=?/+W6ܚ|l&@ZGNYT̹ZGwmZo'bRK>y2:tU\'4𹿺=}osf+Т9G""~l33|[(;-'0[ V4:zǎrqc݉ET@ ڻKjoh%pncW&w=M~}%Wn3w@ZMcx㏎Eoo3h%Z|%ނmtS>fB:aͽgxT(oj3h!{EOWx]f0Ѷi(>ԅ^ss#?0[]&uXI䁛!@ZX\ufoA>s4{Cci3^{}w4{ o;fV㳧7{ O+d]ޡCk .KWotHtIt^`{Qܽr'fvԢܖlFg8nSq~f>DmÃ= ~`Y{~P1zj5L4 _Vύ4x7L%l3≃O>`& 2>_x~sbrޒ nS+ǓϹ'0 &Y6ůI8s;&fhklH>OtO93hT|~LNn^X%uŚ3f*St>?.F?Smr,H4O\T׹3nzq7{r\Gl,HdVڰ>ް˼F6:nJ\ԝ#<A@f=kpiDɓϧ-/~2sfӖMq 껅߹]3; Fd[htzλ;'hޘbg{FtK>`& LMUz7"WܘbR-I@& 6nUXS:2FsT&?؈sf Cg,-Nt{zvL>` cvg\Gk;\}g _U׹֢ ޘ]xP8xK93Hmt8yº^{G+ndsf@bl5Juz7" 9__p1 ؒV'@RWU߭wGn-yRA JxGߚsZHtCU칗1TYu1[VQL> TոU \Gbޕ _M> D:&6-Xx$_t1y';@+ U>Meqalí*@PuVqa ވGKէdt` < _h{L,j6l\7GP9H4nKe(k~! rG-yI4ScQG}?^'&6w؅(䊛\㎌J @ Te5,j \r]>ފ8dsZhuŝu?/o6ԥ-Vgx2zD9-zתnc ܆zs1 V"@q uWDXn ]ܥ곯9V7@ 꿕rO{i`Yf[7e/w^ vjBLT@T˳h۱)zmu 'dz2+^|ft2 tU&}ZEjB4]TjsZ c|YQ^h8?ZGi|SY$@'/ӿmB &u .Xb^7<hK⎱JYX(o2 Y4zP״"ڧ$Vcc0˨ef,NmÃq37 چ}K_yIQޚf uZo1s چ*/7Oeٹ >SYf4C?/\2K(1<#Jdw1Gȝ7@QɸuK28xl!HDD׿lr `0߮{ZqϺbomD[Y~R֙9&q~ri~!o\ߺF=~5[64䵘|W#yC2wF_Gc~lJv5c2y^Ӕ)GwK:#ǝ'_Q6uiּkvqzZvhf}{_C97~8e#J夳(1pE(LsXƆkb$z6:biw+K\MSw~m[j l-TGluŖRow.JϒxVXc~i"ʕX;bgű{0N_2j쌑ZwDע[a@ 61['cTl2>c2YjMVb|['1>UZWj1>UJD㕈jD5b1Q-T-bZJDLQTD9*mF)ʵJ~wTmDG){/n/EW{[tE'z:1=z~;c~W~O, d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F d#@l @6F?:ھIENDB`php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/default/image-mimetype.php000066400000000000000000000000731376002032000303200ustar00rootroot00000000000000 Bj.taC "0䈠)Ě%95 ~ ˏ L 35 !c[GWBQIp*HS0_Y(,g(d֝{GOPR i2L^ 008tA%LbA-؝46Ë Rshi t>YC1SrB,n>Z8zklx p4jEh)m,V 0o`0Ȁ 4 Loa0J  teJ~ 4xŜ5ToGC !Z9RrȪz's0eR] ͑E<>GR 3=gqz--j cUz>=Xװa_ɓLF@'IVx5(ˀY3xREݸyMJ[p\Bv\u7M|DѮ/ ?nN-Rۧ pk]ݜ]SSkrwAsq+K |JVkl|g' D%cXjz'#In,48:̋38. _7] !AJ__,Ra `!pm=k~5-n]^|uC5_@{5ЮXk#P[,t,k`(b !Bd<PTo&~9>݄@<=,<>u|=/0Z Z=Sgńɡ ,N-'?n n~ 1l <6h-!]{p2>Mz,-,f` d r i {Aue;ps3,  %Ds|GO-.<NzJ.Է9?,͚niGz`1B%L뎋,f7UktuE aEty#8?=`tVXFC&1WEt5K`d;RT|mby}6ߣ^ ѣqA}sIpe'2R Wv8]>v!!XGM A|{]XS3UtAfm`Ph@Qd/ZkVEPn\ci=jYK__x):zy Fr@ ` " !4#tfjoL}}w 5HaB F;& T-u_Sx~ {m CcvLJ>{ x8frLD`W=o[1$Cu@3Ր0:oM` 7"`hbd9έA|MVЮ%y32yކDwv[xVCT nR08rty|9cP9Kr88*a0&Nej 2 oBu-#% Y_ͱZk(kK0|"h |-'`E]u!gP()d3]ߏ6`/RgИI-7sY&խEу|D [3ހи>ڵxYݒ5"!4׮L*tX#P-0c0ɉtu6{щƏ.n=,]SVp|ݲY+cV:ŗPHX*%Q')]Gו"FJ8k-@eх`ogo֢vZ&SE2i 1߅jqCC90() 2G !`gj sg#MܛeūxQbhѺ`;hj8Mu))5!$HCM?G.vna0CzװnSP"PZay-P` "lݶ(FIQ1<1.V.oMxYrywM (̎ T kgqGX_aJ!қr+EP)ԴQ7j*Ë èc,NLʡb|t%|}V2Ƭ\`?DW|(sŸD5+ 5Gp:s8"x~S%n:*FbmLSoXY8`pp"tCLjkGLJȷP!6mFGoeUir>^Fj rP`U9knp!EH?H+9xXXEsUw;28GWO2}gpx4] 1; U2j3b2=r.ƴxQ M7_V-a/rk`#?1`&}BFb,c|$DTS,T Ț0segh ۈP:~2Hyb4/ϲ> 9 $ ! 98.XiAE$22b+)&Ҝ9U^2Z;b=/vՒT͡M>PLbIE"]ph]A9`_!RTvRF=}e }4Hr p=:;;K\BO/ ")68rёy)3IhJ00SiB "e368*7ы6Fry w߈>[v+$ Ib7CGhnS;4V#0>2N򜈖 QDģ9GpMlۤn ZrpA~<,'u|l <~do>Ӻ-VC{`\2nXRV́ V:ӣP6F*BHBR47/Z_&nː71]ɹ~TRξJH*}RlN޸Gvh{4J+>hpx]$E)O4s;PC"XxA| rG;W11! v ܉j+n8q+o--\ B6$3m Q߈M-3)we 63DB %38*| |ł,nνhW"t%U8pǹ=wxȵwNH@Gu?; l{4H}Uj7S]h!Kq M߽(Ll] ޸rŘv!Y s4 R28$ã%OרsxN cf1Φm!ĺ+J`߰Bbrs)|5o!H%t;M]NUX/]8"?"aUYR|93ߏ˷FTd>?)39ܖCK|׀]#=} |Q=`xXF b*&&3~rw^lͺlѮWxk.e &6ѕE%F"TKs UO7[ٜ#9KQipgC XkYܲ?0Y5 M8 fPmciZ#ƙ/>=L*qrrS>K<~ޝ#8_HloO*-ZxXɁDhM' RX -"]M kw8G| 'if,Ljz1ш\#?gwx*ӟ`Bt(<+l}/`[)j./&jzuE #8yH!!$Z)1KEI C9哻,z0o3}4yK&狧?DHI֜~o>b}so&O ,a_}@WHXB 0ل,d%Z)(%皩ԷhW7Ο^r9E$U̺=MM*S 8M2!R#@ v>߾)󏗸.&=1 p7yu~Ń %Y5Ae]vWW/a4(YVgq [)H2&Š̌3 1٥N|oٌ̯AL[^Ōx۱ u z_3EOOjm^{(W!LmHC!PBUY,dOޒ\]|,5{_(͞6?=ムs~]Uw9g; T!:㸮H!=TsMIosB3&x ǻ5UtN(KHoyO Mjyh@j0'$6V! b<"0kvjv M꟦QfvdrAйk-6c `4e9RJLf>Jpdd(d#vgk ڦiDPP{i"Gyql) w]Vm@RBqt&D DU>kRovt 4i$4!z6CznX-D2 ,3H)LA!6x=lYNy#6(eWx=:Ker89xQ 6<5$xxvM!^GO!^[oG$z'ay \-c6:Y XaA4ٮ $H¢DOU 3&9ڎl!TvY91DjC*(A/&'#Qa5`6"9.6߅)vՊT;qcBԂߢGh[X 'x}Tx9q0_iێ5}v[3dr\Cl[Txnvf#nt9NmT1%5vxE5p*bzP7@@!ʔ(T>8 J@q]N!ğK>m#Uj1g|Dxg) I-/e2hp?ޣSաD:yUtYׁ4Gn74tF0{9ГmV;h3@&&dP]#C3ݥ:f 2}$Qz/͈js-.MXNƊ>gr R!`* kUpq047#ULR]Op6h1i]+cEwêq_tȶ2Α:Cɐv e2`r0o`CkI![<8P4ۅ?/cA(WUG2 Z HIu|]]Y8E&6hA]¹mCI6+D"k4֪ؔ>}s|N5ߖ;E^0Vh#!k {t7Wsڎy~dX}F3m7 qY{:Thf7=|)SPu9}70'ݞޞb} BPTv*vlڠ!uӀsd8PiHO휰zF76kWWs{g_cmIRJfO?)Wu5J48ؓרPta6S*'FF'D1B,LZwB Oȏ  Vd|nmv*$2FT@mj~K0Cv䳳}lX1 P'*!(ƇY顟B@{zuXpCxK?_J@_ۏLR@_Q9F<^&WǕYr7O@#!- nalKn3] $DW ֒n2^^QK."%ZB~9Cs2]6 l`L8@_BZ֘lA&5=^\_cL qlՔ;O0(Ha" c qUƄ6Pz|=Gpf}K]Cɭ|vPFseFDQ⥣kՒ"?dj%%Jjn^*x80)uǠB 8:|޲ZeMT{7" - q `x(6Y?6DxBX 𯁶 m1U̘P_~Apa8 6`X6pxt@(!ec[|}6_ȵ&PYs+ẖ/o4Ƕ!39.ܝ2 Fgh!Є1.]ڲ(dj&ߦvy7 d4 ߾IDAT M8#Į\j+bnzzF@ bqQG]p =}R#Hᘿ*9>~'::w=>?Y 2G [ȇ\/:𮣭=_x>95޶yɐ"$] VQ Ξ^nds>{Uc hq+:IlW¯Ťf(*[KYwPY,Vk:Xo:7_|{xЦ:m4F'?<33TW5& SDO+yCgxchԄ "vLÝ* !V02]zFDc:Ejہ,!(&.L#Z!T$:>xlښ׼v__{~6k~Zk<x%Oʪ ۛ9_Qd/C k )^XGM eY:-8Wg09J8Rl$GJ,]!$' l^i:_|!B iuy{VR &ѦHca5|9mnc~8nEC a1S Bv~xg<rNߡ_-8 `+Tjuy)5EYkuwh 4#=hfL&9ÌQ#T}>Ɋ1ƌ𶍂 %7S^aL23iH,}.xbO?zۑJ#`8R/'*#4cQ >.HI'T+ɥ@Bq8tSf=y1`?.rByȘ=`jx`9z3:B(j^D"ZٛȌi6nwL3>KBզccVoyi 2;S|~Ȳ/XQ. {{F!hL^AGiS]c>)zx7ʲ* ( 1-2H4Viަ\6KF)R2.g^[0kMGqu}W_?K^)Ui߁W ='3wV5vU\Mw,#xr"H_ $HFeA94e9`@UU U{!zZI QZpi!E :'JE b>DZY׬/ϒ2riC۳nY9@c:1D8$+d+ ?}̳) e3:YP"t+T9Oj0,ԬN]ҒxRBft{TeD1*P B+B%QRrDbt/NyuyTr 7J2ۢ|44\j>BWչiiSzzGo=yÒ,hfj& qK-9ӟ_qt.^\]ZEldy!:p/n0ӯ֌ +8!jWhy2YIsu5x'9Wf "t|)a]<C8U}cZΦ%!!^vL;bF=zt!oO6Ql}>Xr\Lc ( O< omk{ؾ'xwo>=SG08dH{l޾mȌ!qmǏb]ǼY|bt`B[9ºx4R<{ >dw?P YQIMq4- LN~5o-~λ9i>'Y0yMS>>5qΦb>Re{ԊL;S; Z#>u\'Y`5Zng[zJϞam~n2hr)AD qҵ+z')GTcH7wAS/SB9Qq",Ǒ9Ulװ NZ"18%HEW=mBDHCR73 >8ByZeA9(b%"v=v5R2^!!+M`Y{&%}4k['-!{P qd*eýg{l(8G xE1|2Ā@"߭"I =ѫ(υ`î\v=|qŦL&h<`fKx.lj W= #_Z3j%BZ׶F(f?bxkvmpmBQU{ (Z@VVт>ͫ_ҧ+8J۶0^MqWA cUß}{OVyUWdigϞ75@b8ϒPZy{2V65^be|=tk=O{O#B|ra)-p>-H 輢[/o~SzA^bMhc2o-+*6V i6eN x͔7'oD *<{ gm$.:v&Qr6o9<*m3=Β()BBj&tT7B 2vMe*( <0̆΄]u\}״u]+NVc"Ze&VoM;lh[-<tA`9_nw )yHcRMTnB Gt;ͺ],"p!N9YQ$\dMG#|qHqs2N_Qd{ٞiumfMvٴؖcJ)Q{6.nVIJ6\)Jj?|{/opymkmzldfd(H*{ a!-푌F9@ @+]IXh]*}~mkjKA`XE/:Y49Bg霧qiGosi޳=UOS;KKvkG۸_yQbG-95&S&4Y2$&Shʊl(8}:Bx\G @ rF?A+mk7,bF"/Jv|v~&h!ԖuWqtniR!w@_jb}vM綳\bDi$ZZS!(^HaqMbN>3։[;!͝DK'@CGKX"MD]zf>n|-!; e/GJ824RRY˽yQ%݇sdGG6k+hm!-:JFٮKH^ BB߁QsQ٬ϟ \NBX1ts͠TZ"]OlȽ0K;SgPlCgQ1'mhBPCUPc6'uF`IENDB`php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/gravatar_thumb000066400000000000000000004602211376002032000262100ustar00rootroot00000000000000PNG  IHDR@@͐ pHYs+ IDATxlK,Iv#2}֣3= R@ V3/+6!(ь{efDfnuW;F_黿fTPf0P Ul3ՠ*_GULP{M6_P0T> h;^_h b[*\wDgQ* l @lMTo|?] xfůi|+w"x\4C\b>W'̀l5%{ob=5^Kl7Ul>7\4/`"g?<_Kgc_Ϧ P!P)v>5LD"=p.=|QH|Ͳ]*LPu\ "`ж3΀ K_ ojQA`mO0D6pp gߣ-qT<7J A!` *Pb4Q*ODAepkBUQ f7o,Uf`vBD1]bb|'UEӲHFX6ˊV8h@fA 0CD@z#h mjC)D n&3T,i~-\*+H?ж)bJ)Ï=T'[ooG@wri#ߗo| a57~^=3>N?PY'Ϧ  B @'@H[X#_Vd\6V`}JmP{Ql.x]0y)o<Ǚ~ C{e.Z Кb6R, xfP%(e!D)PR2A#x4(M(^7@A(i& {ӑTɀF^b'S@hdރ@?j5{e~ j%&Vh`(P0T[.T&G,(VP&P*XZW:@~BsCRD9'T. 5F[M8cmp  V)#8R$0TmE:i̓Af]lE>BTA#3=xo!LϷ@u ZSI@Nؘ^ i $$OHKuxiNȯDAG<~`@bsCkBD(h 3V(WUl" vr@Lzٌ@Rvފ&qR*ZkRز/†@he4 JDAX-ST׶-O"yӚz8`=t5 &\0£7bKSRU(3!{luA{_ e>4{ RE*i=j=.~=ͭ-Rګ@i*# ɠ2(ei.N?w = w -pN nP(P9Pg @D3܃.xmpbR*4M`G54<1T)bhjbP|[Pܶ&BmC,AXX4HGK).q U? pj _1ҭqa'9n"[_@u)boHk'h  Į-Nz^8 AA␼D}2@~F{޾T&N:,1Ҧ#=e(NSAxPT2j=Ԙ"neBe?l~A>NM )'=Zb}>-POLsu=I(-`bEH<.1_:ڤTT ֈjS) KP] }Ss % xՀ2 HHF ]Z$/r˛*j}QW266l(>WJ)G9C P^#eV@7bmsYYK1Xf#Swg`I:@Ja=ϵ3N&=pu[u=uY:KJjɥYPF/_O l &[/Cl&KE ]<Qi(gh'GHNy CPAu&VB/=ߠLW\\ki1:0&WTQ<6ou/^l"VԲH-[4v(J"YONO=eSlJKP$YGC)Efs\g. ՗8k|ƄH-@#"+m-r{ ~#;n ȎB?_^U&*N>Kfy KW 4!Jm#U <=+SSG?ٯ_϶Elo%tW'dMxuݡ/l A,ޣj 8$`W`u:kܸX~6Xc>*dbHAJ5YMK*x`"XwdݶM_bdN?1Oe0~p=p?kK4)r,=[(en mv1D5N`s.b8Ԃrtr\[\ghRoq~ZqYLʕКa2F9MnE lm8;_tu>Ԃ3&3`Ҫvr*:j{A |UBRH#+&:($࢐V+dkCDA7Hxj=2P(IRh,0Q-b~ide-QwzĢҤ3&RnE ӥ,x% 9Sb׭d)om]M5\u2fƘGM->#pN$-)DBQ,(}Z^ ?3pu刎ǡE%ulw([m n^e \$^G٢mC)$a.BYLӾ(Xg3؍Q:V*4@a-*08%CiZ rm<Up)#9!0LXP2#@(/lZȄ 6DvhC%S&3+zu9@O8/PQ&au@GjwFf.2QEkZ7L:Aڂv~`B"}~)ЃBG;eFݧԫϰL7:&;|gvl_*S/RLؖ'Ѣ^IrfُXLuB[?{r (\"E8'ª.zs-h-pk6sCflͺt(Z[\FU`cx?  <]e{ "f$+&g,ƅ> WtD(T6Ⱥ`;_< 7`ў?$ C`յ=M⟖>qZitMvtpz~gBd+7nĎ[]e^H[){2vi`i:^F~9Ib~Û#S"XcDny;0H:\x_wߟ_ҋգN"C1TeM^+8hTG86`L.IR4keUp2i{wuqh#<ǠrBlP hh7@ގg̒xpZ/lU\p/OG xNѻ/DL! 3\Ǧ16RK#v1f%4C耕 /PC ~#>ܡ: Ъu)B^Ċ.ƖziӣTᄂNHyϒc#:bw?Ɖ@%3c T9Jmv.0bpI5&Qg{)CYύG0TQxlf&O;yOD^vw}f:õ#Q]W-vg<>g.{]5ݖ,t77բb$S[@ji0|' bkGÈRY) !ꇸU>KhN'pa |B;s Jahz 'Ix_(y d5}BI.0E- هAt:*]"1gq&5AվmsHbmX6&4ܢ?ock`*f)"ɤoIՀ{g^ߌYݰ 73!v2,2'=ϰz |w !-BK#hva(cr)@HTOq0Փx2Dqi(`uTR !^܎݂|֯PS9y~x#gA͗?Jp'b.4!@OwovW/[<`g|:PRH7 X''?8&{\~G@as'3pG*u0ݩ'p"-Fc2 9"қNX26Їh/!4{(J5}NȘ _/3*D_$ *6b)^GhP~@DVS;~^Ul>. j r㏟X(&,(d.Fo =M՞.#h:϶HܦMxHڷ@SoFI97qǟR+Xܶx/iŃRxC@7YV8Q˓G&ẠSaWͤCi;"$iE+[xw.`Hdc#ɫZtayOW_@LBdކ7N/m= N A;:YL>mVz+޼{# *l~߫V6~LnP!_,[0t8uGʬȎ˭)r4}B,[@>}^r? ٍRwf:4T-N-$-Ё!%E޻ {0ZVz#2Uڊ=P=w7uPEטY ҺZk=8{ڀ⁊=yTtXt2IKSBr5pURǽڗy٨wWo>7ƫ!<>Ƃȩ/D2 IDAT-xAf,Dba|?L*4D.,YFvr&M{q{g ǺO$&XzV-hZ@ n2^AߡwOGﱵ`5LJvp7I`mڶX73lݕn}O+7.hm7sua6yWL^/Аh_m^euNն@uʆ who7wƀE!>Ne}m>A߿p?&SmnuZ+d #f?IXݺiݒWusQ8:6ShjGgW #e>h/X ?4L,"ȨogKr[}{Y,aDx&r%ZbSI˹!j~qod)m2wHk fZ޼s9|FnѥIt_ktì a(++\pkҏL""h[|Ijb@d))KܨWmfXyOQOiFp8g쉋SIʭ53Ѩ(T;mx<!#^ُ2sr> r{&w5IcbSq#LaJLX;?ݡcNJ7giL=dyϠ[>/\Esf!ܶޞf(eX"iN>Of@Bsp6Ux}H#=n INtt YZ㏏n^iꨧ'w YG%Л\=;*Ku`c/k&[8yer2:~Ө@%&tlm|m~Xv4ٽUgϓ1 O@g<=<>ݞK$RGw -þ͑RKZeʕeh,;&I@{_[y0wCjԝZ]C=gcr!#3EMkI9 YO#>{Kipծ.4K!.N}M7Ҫ\#\ud.*tG3bP[?_s߃ub^sIҤK mRӇxYnp=Ǿ;ÄPҏBrXd:!ۥ;n-fCP)p}њɑ=M"kj:#+C8[xoʬξ10ro0݋rk8оN&Cƿn{Dމ6]DhE"FCahKnҚ1plA_^<@D=f:usgc?d03|tY167"-M OW;T bd J Nm;EXQ4i] OˎB N%&)WJ֯oR-=.i$V˦8Y K6͢!XG|N~:GR஺w6SH?c'b/FJՂc ѐcG"/LTݯˡG{6 weK߶D< ML GG)1!guq fyTs-~56r$E)9l q8gk3>Ꮪ~dJCv~{&a;fhIaQ)[v&|ZFLnf Vi3kSJ/ t 6rsw13;Z@6; +Ro}|"]|~(h?HW#eP%MR$fOK{ %Jv1>"ڊǘd}1 jfHp3tHe !΢)x7tzTq_k>Oӻf/E]#c//!H\llf0__\Guř^,W/5` ..@N>갰/7Xi =3|gq|>wK殩 2ͨQ4Y E:40J`uSőeӜ+hq2qUNLfR{eO=fĞG=Dțς9=*>ғF[s*3MhmgtF[[5W?F$;ʘhDd7iLJ} ^Mx#zI샍d@6d/՘e0PKyCrq[N%cŰ8eeh/:++@\v:%"@ jݍ$ӖXUP NxޏK#,j5RtjHSc3p2Qei"j~w%C:"o[xpg_Rx \YX |N "ꗡqjӓ̺w47\Glᖣ,9D{LNDD T.ը8R~/I[Tό{A׉tĒt2j&Ui:u+,PM< n:HݿC}*G1;v'*ܐ/}%PKqř^A3z6Kj<-&]!.1 *+wHf"dݻi#/ Az{l Fi#HezC93aK5|L)ʼn^Ol7uRHǯ(4FQuȅz[alӝĵSsIJlaѝC~X͂ʢ웘M7Tq{Gz]0l X˄ fljlm4: R>"9@ڃk%.=8Yh${(FR&iJFp07*B|Q 1D*G>QR}pB[rn0@{^H0ʴ1>Ã'[2r0Mi|d x[2:-Zg2HErٞOF& ]j.6dkבyp}28YV] Y=:HYM& zamKz=u1Dk#gv;>{ywr<ؽy8Y2F?QBj̰vEOSuLֶ6DT3>|Gd>1vA;o Bhg(`Y0v/+yAPeafb𑙽L(sal|[T..LuK+>c˃xni,:!``cKJ`4s+ӗScѱ >$ F3b?=CvMOiְY%8pA->0LD lPOlaD1+8l%fwK\R. ykjϞejK'ݻpĦnaQC]tE]ݶI(B8yNV }GWI6[==K%Y/Ir1BbpLvBlUi2YH1qJ _7_wĻc4[c4/ -iNȖ } |8F0d&tmXݜ 眺%ֽ|-p|c[uĹ7G\LeF2%}iNsv=(1JM8iebr?d꽜ہEDUvfG$9^.Ir@xWDc47J Q?f7V/v.3'ǒWp<`\ИxE-ōDUAg(OYXq8`P%<җXʌ+\Y1C}va]4R}@=7⌖z01iI!}TT:X!9I`&8!4~2#6Mסd\2̱tMo^n>#=5xitnwg;oޅP+l{+3G/c*] \Zd.0atcP 3 ^'F9}nJC]BIa;whcLdN0Ix[/W`趺8f BA@Tm;Ep>FV&͋[77"Mm]A"LjHNcVBר<С*P!6}„G0TZ!t -nUSgu#tek{{w6@H0#E&VGYҼ ==%q='f6۠vBn{KrostPU~RYf!A>^~T俫5)浙4u\6?4^-鹉,FB{2B~uRHNܧGh.6S'"czvq4YMւTg##Y6xL^OԵdd"/PHj.LAS[ tRJXR.\ã}~CçPtT-if+\'szY7OnF>0n{RnX߰)cdD4yF*Rݳ7W)ְA `ʸ- 507l/RoJ&a:+[JP֏I^x¦Zpk|usfwN$8YIӲ~8{yt]/ 4'Y?M0Μў~>x}eEӕ85ے45ڣ 6~˖brNg3"!E bPGf=T)$ͷ %ωzkP\#pq5yk#4ώD(zefP/xNܡ4x6%|w{Γ\D I֭se]L;xha02Ӥ Pw'>z[K(oLP Άy5yGg^φ/w3h:9<%9ڍzƵӬ6@TAݠ^zt=;jbl.+޿|y\ ? !|V`~glY‘SK 4m "6>a{ Sl7Qmކ34ֆ,o+[S 4GږEwQDP%d ݫ5uڇ pn4OK; D.Qo쥎 RlT| ./ڭ IDATcF[ٔzc62AICnMDA'6Fv|WdkVA)"[D'av*Tvb#2GrL.:,ڑoG= |_.PRS-G nnLR_O,&ou+:9k~|֢A l[Cz; 1*)Yط|i]uw8`N"ie"tk$m(:d1Gg(6w lb ͎7hȎЁ8=ïw` 47'޽pVqg/:qOmr) 3@RCjOf78n.xf0M`q]3(+n,2jyzE©?'\L9tFdcL- nH` 2Xr9U:$Ovg*$?\OB|#2&qj 8Ig4)u_;MX{vӱ2)YL~[I`~ST7aȌCYv~zFt  B TGa~R+n޼ӣ+liҧAMcm mg芹PX,w YTÜ,[pdYQ;yvG |W5,mu{)3'x˟٢IRc{k^ +DLY1gpȞjS%Z&M!i]N{4=,N!:4$r} <|r֐j$ (B>X;NKksھ.M(y,["m?|~=\9C޸x We$isDx{2H>6E <qNqn[Bm|^17rtU'/q٬/3Z0Wa^LnpЫn~aS-!gQsj)yf?\u.)p>G3^GD bŰI׷o;4O|=ieԭK~l{t2X! Y9CjI y EeSܫ֏_O'WOmmW14/R6tyTl&! yHu|իB,A;N(nvu09I68W6zwӉR,Z;t׶-`K(TSpj)[ QYaH_61%¯ x=@Oe?|?2 I3be8%K^#fÐ*mTAΪv p#p:Pt?SgSWkH~vVSWs`Uf '2^Wu{ Q1۩LO!ZL!ߴ5˱ p^! 4mKg6OwW yq36N@YS zǚGCv2/Ԑː4GX}(O2YbHs7^ci({2\,_%IIT,'ejyvGl/1CJu Ӓ};`AN$Kwj, Lń)lɔAَ?@$[+ϻU7lҠ14ww4;ρ_yj2~ 5ڮj<[.D[rn@kb5ւ_?IҡAHH[ жCyid10݂^gW1x~3 )).iP&X h^[C@m}fugyҧݼ.IR`V쭓w^x{[X}v-ڔ6D5suꧪŵs[KT +?H:ӧϿ [K0ĥ2}|aOGk4 !M1** 9Zz$ziLH7ڼߓP7 ]x1֩OOdM=: )@GQ5 \%?4;} ׼`f35n Lh>Z١LiNB}MMgFF5>`mLRX̧5&@}a&]޶:D}&u2$frq۟@\c.  Tn(e m]mRӶP#H.WFAEeyJ|>"Z(y.Mpm(YtH@8OiNm财tfzߜ5 A8n 5E'ǜ@0d]ޡU7o-8Npg1 R,x&C"0~h2-|2ulϬ2C&2p}bK^K'DBJ7c&Ku^+gx%V16 *kH@ m!9jpڂv1 t@C+CkZ:CUlsYКt֛J$aY"⮹Ls!G(JA/G=@)P 9=.sM?NdPȪ{oD17[n7'l<0R@$Q ^Q!ؾwXN_䔜 O5#1,:>y:2u2 I10j%ku?=_utMBrcU[Mb b"u m[Qm^]eԈCIZ/#EqZcJ)9px6"\I=ߓ}$wjX(:/ޖ,]o_?/Z4)u27dvdB#ufQΗNΙƹyՌud .\۰N s9 F:ab.\Ô2M/J :=:]~2@օ B@u§%0qs)jh}#L<3R \, QC'bȥR -; 풗f%n[Q:EW* ȝjcAXtylJf8Mm2!jiISVLqnWJZ9n{gQ5%^i|丬?lǏu;DŽ,RB&4uJz\mRRG<|, 3c y֢Ty.KcsL*䨝bHrE{O<-.7e Ͽ *JO1F@>pךa,꫟(}-Y:i(n`aTMq]4b%~5ca ]d.ؤY;I !#]O*E2H0@i'<>y&Y*l@}"@jbܝz䂘,&񄞧ϸb ᴾqR0 ^ۇרHmk'Jpv2=|j/F\c,o4rv3gJ.20̤A;|rW&B;^v׊%޶ȃ?OZHB#֍=;`݀/Mi4MLE\z}?7q8Vy7q,]8L{Gq9 uecF8" !#pEv5[]" stNaQbĬ+)&[P3`excuJu^c"# RrM:^q ]=rWヌLJiؠ{ȥ8P2Ēq+](߿^x&d#5E6Gyo hhp-lo!}++}Mpذ5G9SUgjW,$7K83/4+ e, $h8"n(P I(ߢqq 98:a&Y0fqr=r[<uUu^!v=e9[+,g]ѕ9cFҫJQ-R:ZfR,beꈖz}nwU:܍[p׬ж r}XXms$%QXpuKvåVח,5I]kK@-^Ɋ 4^pO(^:%yYIyiGie 2H/:'k3e)½-Oc9j.Z '? Nn싘ZIE^R\r\E=mY~%ӂ!v]QUB\?u5@3%r. b,A 1)[eqEJV=4CCi0Κl^ c+3&ʓߗm~g.ؽ]|:9rp֥y1#zxx'_cE dNj"փ6^ºQ\v7[`wےR#ar+i} p|̘O\SwP4]#07y[v{zeBؓHP$LKIJa\9 ʧiip:oZ.R(ۖ?u.2[ZvYR}#psoO7m5oQPrd_;{6^+q<+;q^_>79Ϣ7njQ$ Z?3XQgҌt@"T'A?ov4@=E "UDJБ%|VUps{stc\T #0p~^i/򐄲 R`7& 7Wyƣ*k,viޏH\ICBjz̪ adm]I(3(g=x$eF]\"lEX[q]p)z&bw [2r3< Șmd|5k8H(/-"!Ġ2䅱?2F6Y9k{킨Er8vh(1Fe:+izq,>f1 V!{(աٶ̔u%)̝ӂ[v1<'`x*5gg(|> 4`"Sǽ:r,Ҕ޴篏pHLQrADC"-{,6>9Q)uכJh0 PYC΍GIjʛlfgJZe @tKyM\v#^vkԭO+8Z^m.?R!`G";-E ] Yyl OU^,6>(Кe_-H}BLC}@YĨHI!~!h](dJ.($zRAQnJ5 e%˗ia'7z˳(Q8^r8?f,69El'A&g  nwukv(aT*AF╬vXp0(!28r츦033 %]*^"l8>;'"[x9zW#t]N+&2$쯤..c>"N++7 1Ƽ(=+6o@t:[,RI@ Qb vr^(a\ \2(i' %TH4|9F̏EN[e)-\$q'݄jpu{ڹl3X{ f.CX:;>|,ștw66_S/n1+%AKBB}b y3#"\V!r1NV ,@@Re¹ŋF f62絚Rz0 b@pQ. ڨqL$ss;IIbj]a{. TօfQ$|!O"6 lD7Kp?v|YlOf睢@1XYc#Ҍ8rC3J֘K ܡP_FZXW A3 "[yI*պ**VAs7`qݓɑTx˲! Pjݛa~dSШx&NBC)#2]Iθ:SMNp>МjjEĤMb$6Z Y$GQj^qt1&!.b '/ƈC'oͲEǏPG.vMucn,jE}G-5YWh(Ɔ'=LڅP+LG+y}D\KQF,Sx5fT ;k;CEܳ-vM>~pXȢS-pre>ߧU{׊@7B*`:#\c8~cY74Ę(}LGR / acVÉQh3ba dY7f (#C=qb`*`B(gP9˄9l$pva;ϣ#Z^{"<7z@ 7;(+~}cjC/[)P 3Gz!? 7Ir,zVɅ̂~S IDATA bYIEqˣUEY]u"KIF!AEV;16V Lǀ\du4\;K?RBVI A>9 GQfJe{+c tZy'R>4,Pj݉RL)ٱ%-3vYI6i tlJnނF?V&5;kAJگ&R'8xalTv%¥*s[?AQMbGЋDoիr]vP"Cgf$@oR3gYB[c>=@E@^.U҅ }NxGeS;[XewmaYOGGb []Z ))e񞎏OQ33Xt3-u}1f8k?[ѾhB(F bW%ݵn-P/裬E#ȋР" K@MA{a9?aqNAQ":Dy!8'sFs1}X곋@0JU@^KN5lx߸u&bX&eENB;Wwj“dp7w%ihisaU m|dǸc4 4eIM>;U2_pF fo]1#'NS*(⊫WudTb}^mعbJ:ʢBYWeA(I^.TO ZYnP l)QRB8"y:" qg 9od$x569rn\Y x(2#siyˮkU|u5nw#p?aadMpK$wrB.T|ՅjQVyi^u[fFf]|s9A | 1S5'0&Yt,`bDb3(1Z7q.c=\C Giة.rSs$paXK)M1S5!Ш#nD ( PH1Y."$!PbnY)ߋ}r@PVp( xI D|DʩyBc:,Ejk=]n:ג₀de(cp-Pܺ8tE3sK~:86Z}sy y] Jp]kH՞?qyG yf0@"V:B]EG%rqMLlLIwItǽ,qF%P ^ELvQMfNeΝfxh: y{L܌rD59& Uwng KnqϞ^읚)4 Hqe\0!?Q(-܌ S@SR R/T>jhzcpD|FYN>-X9 yjb3H'pi"uqzD5U4jj#)Q9R,vYtVFQ,7ĸ8ߒ!jӂCz`rnӳ0v]w* ֙`7H4YP?]둎7]c,rET 0~M\엀/иs{>?$UyMr傢bpJֈ5QPr5FB;!UT[e$br \Ul঑j1SC9Ι0,AJ.1^>qms3.Mk 8:╩PW23-V3tB~gU/a#GWpfPTK$aU0{7x$sVI%=Ke*-&M%y2/;k}/K)˽F溰,&K/DfkŋXƟ{fVזy[7O dafc*]"/Gn3#b+`]G/hY E1tԸr.A&8 Yƣi[ͫo q^__h\Cxx+x7\{Tp>W .ΐ]x6w뇉fq9-qujFN X.!q.ˢf;ߵ΂'c6$C*.DN`(YCZ n`Y̡\]T~i݌CHT^Xӣ0%[xS'Og]!E +/4ݳ˃a&7OEHpQC'1.<+|k:.s~ VML`Ӊd$y#/G8vvB,2\SM]Н5Eم擝Lq5 Nh3e̖Ln$tFP֛Om QJn>[VL7=="R~:Fbg`nx5૟z#ըpj̥(Ŋr iG1TCTy#; 90?!CSNd V=[ ³,8: MQUyqn`i.%Ky{)IWD, ] ؝7;!K3gab\v\1>`&۞1t'G~YPA9Ĕ6DҌ[gK:`"poDg9%K6:s έY-~+Mp)ܨh0nb\e`ec"6Ÿb(j}vܻ~YMJFf|3Ǒ(0NMs!`̂Re7x̂F^]Yjqu;VZ %pTA"bEƤr r_w?RJs2-/A#>gOݟ.c[2(2yHA$O~oQ[/`K/( #ΏQc- gX1u[5X *7\~Ʀqh?<~AљgKv);v;5}pց7kWު-N:aLe&ƌ.f|sY zZQV&OQKrFQpәkG01&V^FΖsyA\.ccaegqܺKƬIt|>יF6SO2;Ղ$It1(DZ96WB}EEu2ͤFO Z1qO|ZB(bM3Fse zX7/o-Ygf0\Ώ|N2P(/?sĩ = >x|5؜cJ]E4_)No?heL(#,L!_Y!i$Du7FT Dv:4Ӵxm1)mr(0E5pt-~C7xx<}Pɠe.ь3pv|,v3 Mhmӂ2:ײ4M0n3J*}ۄ N",ӆ.iO$q"Lv.~me_m۶ˬ;P-7}-;Ȉ A}OuKʕqY_Y-PH-Z!4uYeow%]Εg95-E˧yHPYIx.7n6ReXfLWʞG[xbF[:e[@>j$l_ePY?<"P~@'i(PNߡo?qR2hu~@_owᶦ!MaHfV0]S1j18Ax5QwPCDabku{Q mj슰F[*$5:ZĤ |<$&xjyuVp:r:!򃌓*5_YL*s Aܠ]! ܡ2M06ۀw9Kd.ˆl2(j>jרItHr6O܎})~`e-/AEO^+h-1"eAh#)i1Ktj/D.{0F܊cHiuNI+]pF) vVܺ<҂frouI8Y/9|<9׍r[^]0?G\!D`#=-x;e߂mX Q)&lk#~Z43BxTW R!cw֛,֕U PUd9Fs9-;[بP~+<7I\mXGߩ\)AY BJ6nf_aʰSeha!)9:(%}~3"/x|BxE7@Qg6hAK;f|k4IjbGxs0lJ)L: Xܭ{IB,>KfoXּz[OۦTL_ ^v@:oدgAuwfk>rQwQLuT~< E[tes e_stWl%E/q}DO} Ex7粨.$lS,6NcQ^lB! « 8O,{\lQGqT)$,$nn12L!b@˝<I撁/^jαٜڬ]ׅj3vUE 9ά!8."\- #Vnm1)u[u)w9ִ{=h͂fD9&"Zm5Hl9IeIhx#> !B.51E7) ߂j)yAb T%@*޾' DB^?Up ?-zh  ݄֪v<"6e۾8(?;f26{J|vBѮn^]hFb]DTp9n忟bTf.X*:_V_((!aIW3\;,Eתx;:9]ds= Ji1`ـ򂔀ׯ&p"IJã]NM ca&RZ}9[k7/T`?.fT|%WƺNɼvVg#˗2m9k֯~EY6Ri^\8fQg/ hejc#[yr[g/l=I={]Np®E;$'C3b[wscgtA.@t- w5}Asuw2)ӌB{Vc]bKV8cEMo iM. K#л17u183Xqgkm4Tg%erFKFm8њ6Fq JR"Z^S ~YP+YyvJH s=qs%ݟF.:`Y0bSfja+3-}#%VHw\"O3 /4-:`8 y IDATkpbm]!&lh%;H:=ac~U&jY=pf )#&d u,mu>'י4dP`0ncP׼||xfP̮c4wFKWZNW-:inZ}Vу7/oquw[ȀΊcI;t˹z3J E(2A8T۪R M%IEOÀs-`ˈ^cvJsU $Ḙ+tUraJ˳D=% ,zR(,8*rZayY.#~ ݦ42# DŽӓ‡Tc.e$gZbjmRa#4f6mA:>+XْW"i..TY>}ND@JS*[b( (LЃ/`hrRRCwxӻ!%c?K-3IiqP`2$wr:.XpN-6̟, }-U΀؉z2_|reQ £{7oYHڃf]o^n˧@Q.-żn EOan!=PۮW|`:6~奅s"8~`qV?fnO r4:r4njse|RX#9;l6I`iD\\[qLyb5ICu`ߌP\()Hu]e>%H/ 'guZbcJ ;q<}׭{hTC*qJ~-SALp`|1/݋E K3BXQKbvJ@0'8_)BAUd Yia,[’)`{% /{#z5 j6]*0-#Y.ڲ+_~oHoaEtj~h5  /~S -aVwn-!r^-d8Ԕm$$0.g0GP`|e~n^\dG ywBXykuV 4777Wꬺ۱s+]b\/uîCRs1*?b6KeDD x| <5Y*h:nYXZ7?:7ޒס\ jSԋV\fj.3B4Kd}Ű ]&=;_";JXԍia޿1(0! چʮCf`O\N_,j5|(<;U8}&wѢ[)nd"s 6ttXMkdq9T% ?;b8=_klv1M->A\Lܘb2ءJdjZ7Q=(Ӡ sScY']fmi?}K2j(,h&HфDX#u"VNKѻ~x5IVu܃Z>K_vź 75~#_H= @Ѩ,YxQ ՔwH@t H e3hҩsz[Cw {F^r]u( mJk_H"O8hiշ/vz0 p9",oP*E\Ԧ\\/|^nRV课DBb,"2h]"yq%liKe8| s?| < ]QUmnbqx#REdyZ~DqsaFqAR!5Ms#=WHjn0nG]8Ϻad{bNzhbaIWՁ#F %+kʠרn@LQqz5Y ͤWA RG^1>"bFkZq3d; gg/evIte*#QlYeXaHW~~gsч4"so%ΒS B0nz SJ'&ٿL@7@Pb${\nX;_N6MVseɒcByɥ vbMA)(EBc.4?{Ň1L;0E#IZ?r~,{80YQY2=Q\G-eq8.'V۱57lRA2L U}JN>-tNt>pEZru7/A&'|^#P'q7Xɒyo ˴x\fz%E(%e^,)h0AB07psRFY.l9eFDw}zbaȾs4ѿYUN7eTf5 Nk/FtjGtN5scBJ(%b^6q@)sxbˌtnCݴ˅-Őuړ 5|PvLrxK{ .zFǷBk@Jʙ׳}!_ulmnFġFc.VcLAH!6^\wA[8L8`yZ >$' XDzPCyU #nfTsvkAI"gk8m;zx~xtܔ&kcWYmmsVs֠#ӸWTgINƌ"^G0"7#%Ouaju% Đ^}Ag,\b)Ɨ.8P @U'>|Xpƙ>nTQeL.hgk ؐiQ=M" f݌Ƶi܃4 C[P$ W ؃1uWx:.șp*:Nn@.Ay ]ofG\95%3.r6@ @yhkQ1r$ѥֹw߈֑uOp% ;b&!֟{#3[cD\VQќP,Q_yF>?'pب4鬛_{R*pؤOO1R>KuсL5踺eeK>573;5?bW\Gho]80ee dw*1??% ; ~p:1Ex9Cj+4 bFEbV_Bh QX8( D! NP(zK/(DdxoRQn#%jq}Tv:&ХuFMJgs^/]ZbW,hi)rكeMp5"I|D@z14sĴl\ ȂEM/D&tؕ1ݒV Eu S _H'W>h.489 .}<k<.d㍺5᳕ͭoeLM;eu`r:5gmQ!u(Jy"9Hc%-oQzQnd7nDdg$~BxO߄{HAն"Y]f99|4elz* ;|1D(A$둳Ç3,6^kfIfC8<)t=6$f×ClE\z { )4"P`Mz 33" KsVn[in(]W?_k[3;#i95H1/Gױ]hkd絜bQ;Dy]h1b"Y(bRvK<4(I0ʪB`u^Og]*||^aa+\L 󹍆/7@$BQnCUѫ[~]b/ !n2S=_:?3mZgiuǡ]"Jmk͡T@]efߡay?aP$p5ƔȌH ay7#xڋ-89{w[#IFH9i~\xON譙9eo9MH-8Ѷ~ ][-;дBY= >˂b$+\]#y9 Y3汔Xt& 1!s@?a.Rq-le,si/FlgÐtA;/@Eڝ ^v÷^t}^`&b.G+E7k"&*Jz֓>Tvb}@MU݁x5a%\PR%F@TR+p3;a&j9ӪĚtjJ׷d."cdιc~8os}3<5|nV=lZ^)!$14&Dr0m B~ F$OϏ"p8^*Dd9rys=# <t2f3W3N]yqY7˕-Ҥ %gt_}=qn6; 7ul,j %(FLTP~?I󦖖+ݫHƛm@=;eH8ǒpL9G0j,Q$ =5 -75A>ǺH-b*okʣR[4hAYyGwW]yCfU} .'ظ p !F]HGH0iXXu$c$gDZ@en,Qխ(ߍ 5>TQlj$t a-Z2y/6OLyZgicڨZ,nRvs,ü$1}K;@戻/^"u \5gI3bբƊ?EdD0 9ia.%0E0+geCLM* ԎbDglN890;[$c:5NHry,dž! 1H[qՈRC55I7`|ׯy Q9fsF. |MEUi0+2`wcV ͞XeبqRAEI[l^uBt zn_3j( HZ;y Z\4nU[rXm1! 4^kdN:lD*ZO.Er>#< RCc߂*<IAV&QZa^ *fyJB踢|1?-f# ubEqUMR#K#-NW_k=Y^\>uQC: ~`zJV ,H?N08a1hbDFcϥGOg5DXF7YR D%Ve<!C5G3^pfQ5egmo/'՗췷HC|LM9.R }BXk͋raCGjK T +|E#Fuˢ2!^u<(!/,?.Iڼ e*Mtx+(cEɤ+[_[?@̓O)2@%:5 aFE+JߘyFkJpDfj`T6 8蛫ңF!u}8^H:ÿ̳t gU>6uG%:yY y|#لH~?woO]'K bP5XTރo%lϮCrYR\(UU t"*O'ƥW?‚p~@9n2@aTDA Qpj̖!O*EsUCNRR>Ji^Z:-ADXt:JR {?v7>% 9wmp)}ؗ?F?6S3Y:04X.2`XVdH4VXgcw=L b7f1ro*b b6aN/U~ۜA~N[I/?SslJ^ʭ~&d?#cr> G}]MI7nvLb%Ad̜ZpKIu)}eD]?T$ju$7=^lۯah7>fߧwm1a~TS԰,6ywqK韍/F4Ph愍.3Ab9/=l ^i.B(LXRVRw|3ZQ-LH:p%EL/*aH\aX]8ևw5wru^׶C܌]g{NRT#L<M5R"Əp6<ݯ~[\vZI giB$dSUɚHθ\=-.6_C-VϷe5a ~~>'0+@\y^k9cP gz-fv1pX4GQc?:T^y)k!4ܾy~y -T7yF>0=?~2,elvY҉iV̘s@FsYv3D臤ߤ?[ocq==x2#hJ%܌PGŷ:2A(๮3]f1E_8O4TA [(:ώn9SXMSG]ַi&,9{{+[ܝpbo +R*=6pv.~uŝQb~O ﮲*Wj1UJ3xR ..Z;6-;-*!Sǹ`4I&***};7)N~:URv(H6AKֵR)g"lq}90`PVצecΑhTZe|Ih0JҗUL)y4xvM\edwTl%x|:*M ,~*[ќB ^sA[̘ \&52RcS"GN2X67fc9y"Iق&B31Mz%^`( ZEĐ%j}ۛ5K E9?rlky77;Aq4tIX|y|OPFDw6Z2#B )̥~+$)jhp[qfp!4&} g*^ayXSPyK>ݦZ8N>H|AFDC,i1rs _q2 )ص;G3T6(1@tT͈ы:ig%dmCS\6ɌqK L2ULwGM`fP)S ;)&o,-pyWK}H!<)zJ:g`ʲlVrrhVXcq]K &z!$Cb;򣼡WQ\ d;#G>PN VQ7T3Vl rQg|&rAl iı':oaGuYMv >t<|6M$kJqZqX֒ mrba 岄!(p&g5 \}hCS@BoӔ*L |w W7gjAޙrAy}-G%; YF\Uʊ //lBNakȵzJ1`mK@˸BB| ~HoM_\9u54lMbX&Di'}n.*gG/zR Z=vGU*Ln8Zrtv^& 9;۪u033;mGln/ gD)g*Ki3]$9Cʂ#R`0SL"hhR474$GA55v\ mmeMRk|rl..Kw8:ZД'Vřlˬ-{e c0 eY3պ;ўw5êwf#U襅u nw7]0q_zVcŠOűzj\0a/xƴ?cgmX[ qpsy )?G2Pu64:rj5E&`d$ ceJh*1!'idܔyN\Ґ aJlp =ES A{HJ{ y7W}iUVRAw3}_!x{7z w h3} ]EF섽5|j I4~}S$kS XCX|a'ݤTOg?Ѵ|Azch4>5pDc"m3#ŁaX!hTU~vKlZ@?[vfXB4̡7 ," tT;J7z6J>*;c/"app mnJw>$6+oau+Z,'B9ܴw&I[ldgqsfukUfL!"tcP;S$D?Le}ߌZS xC'8Ø|7ʭkE5TľJur'OW[0x56'CMp*]v|i7ЮHENf57`>idiŘ]B!ex@d9{c (_ZzˬQ0V2.bit7z]X 1NmǏyzPu621\{s<-/a!!FBO YzH Y]!{U>2ׇ:.luu0zT{\Jŷ^gz_ΟS74C67cn^p#uV4ڀps'GUxUU1>4iuwvs%9>xYv=XF}E-1 JJE80&blLUfV51sIeU.cH8iyjՇ!WF'ZZ8Ho`?;uw$0c,k}ű VUg5VmJ29y3u՛&~ZTzu<, <ԍJ&V*)+ tOc{sBY_zQxuM2?:i &1Vlܪq;{RD[;,-B*HB|+1ZX7:g5J14/O?ŏxL7k#q?Y2&z]P1 ~D/Ha1яX:gԇζvJ7[x)n-}^̦6:_WZ郚 NwaaB*q3 &x2;UQg7W(oWv)97P7rnO?'/3YjپDbd0];pCRjRf .h0$WZPRljEʡHR`J@f cVsP^׊?ؗԖo3)~c<P?x!sWku&ݝMYBS5VnKyxvJ?!Q9tb8;c ݖ۫|zwRpm4jY2 Fhږs%7'zGҞ e°3Ή/6Bjʀ+@cDh[`C$2hҔiU;hT@'xRƚ"kN5 :6*]idwD}L{q\Ӛ(3 '*A݌X!ߤb9g`smW{>G&C>gvV̿:HDi27&-meܟ×,C"M3m`S cE.;(o1~O$ _jS]hA6fX[&n;qHk41Q| '-w8f8Un"q\{V+qŜcat{|!&~cjM!ՈAu*GUtr!f21 iZi N];f  8\*(L=O>hFJ %d&)YTanҨ>W)A!V7fF`&!eK}#=l68T}n>o2*iMk1iqDN*BSE[POq"2iB_"b~ ttpֶWX~_<9U2JZUxsZB0S(J STT(hi3 S_]54Ch,r Ty@%?[}& O6[QTv0?ZɎ(u,k4O"\/~ ͭme[TTS"Yl.?;=㋝Ov<W]=d޵{"9nnnl=;lgonDu~} ![b$L<)b>@zӡx{UlgPqfużugSłRy1ZCD9d$Tw?>:/4A 3C\TPG) IH1R w hLeEY4xYQy&k)JG?9Ir \5sql噢jIJD12_4St_*oLB ";%GBCQPk:;y>V=`BISgfT'PYl0irNpvҴ1зr0:=R?O^nFovݡwAFg.t eYS|%_;^ O݋,-0j{O|%h XXyV;_#)2l'v!o^_[~Qljz&39W(5,O%-j&Sm5;UMam<\ 1Bɳ9WPsdz ھ%Uݐ#Sb1:3%oLBH  Ǭnuk9"Kgjp 6MՃS]<Kl8Ov=(:%Nb@ [0wz+P xGcǛ}‚*edfʒmY f'O;=`/|tL,I y!bpʯr{$,͜\t}C3!%l1  ^DU[e'̑].1]WD`0t?m7/}WՊp3 QX$ )]U}[gΣ~ƜlX"Ѷ?r4^c9>Y/3B`tn6AU哚ORFG%h)rJQV 74vbLKh{v$-{V$PV$ _ Xh,Mb9 /3~f>EQ V;}? a?-ѲU.v.$(M[.Xok>YjhiS9e=r2qL/68g\g6 7BNCgqb~gⲙCU c`!B0;}F3n e-PXF1q.AT#z߼#T"˘FLaZ>t}lD(-6.r\,iNZ{\7RU?ڃ)-Tܤ5,}{gf4g[||rȰ ~>?"qøflxkUA|gb5<f[Z+-h3~Xc ؎4rP-Gqܔk[ 1"N3-- [5J(xI1{g3K'ӶV'fAƒ3Td)[^UĢZ0:B#c.VW91_V1x}Y_/o2үYOp!|n'|Q WAf(s2a.,1pT:ݪݛCͳ [Mkf2 ;P inh lےP)Xs@2ܘ` _$>[Ãx2HhTpz[=(.=Hؗ5 cdwE95)e^KJZRQzTm*!WggTkl[iiwG0ڶաI Dd+n @,Y5|8-%S"IUu4q 1`bnhw6XhvED#897fKa~//k~&Yy_r `)E $UjsUN.$[{6"H7:1kBv*ߡP`bwSZV^ 셴}ʙf]padn.>;4z?V;oC2j>P]e#SKԿ8;mBo*HYE2RQ9oT*f]=H[|8 1ct?_k+3?Wn_@%n6>H9\ʙM(1?>X\z~Fv7Yܪ*pf| pjghg:e+oEoaid7p[|!NцR64M*x5Z:+"rfAҕ}T:A,Є4*e!a0yI{Yz\;A, #| GO&JS@!0! ߿$lbtں/?_e+`&4jK%x*0:<\^Jԏry׎1bKqf/4 q\a'TNK%Aklg0 9m\s~֗$9c-~4$mI>hI l &J?2^"c ,<lp)vry_~XÛp1u!u-0wb,{szˏQt#KłB,pS`?ڦx#, +X]H'WhF0n'};(c=4:ZSɌzN2x"|f%Rg g 9@Nf-Y}W&i=l) `Fq/yص)?4k$nkR+[BnL[T0TQq f_ .5'R7"렧\g?EeePT1N:;,eϞ\׃؛=f:d_4"Xpv rZ8r`)z*6h!ڎHTFH v1=H8!lKp~ePmCPI V}ֵ43jrg?x5mL[n{C̏J6DZF+:=(V[ZmY>4HO`#>N6x%SIcHW).Ak <pX, ;L\lllHD}$bU%1LT3@1"6Cކ}{2Ѱyzs%XUdJ+EF'ile)U`'mVTQ5"3ڰǸf]X[yȆWL++R8@VlX(-m^ľe;Tg[$HTZV.CW36+xUS’S)ɫw LWbGykHO+K~w`V!a{1Xގd{vem n79-#XSΚ%o0^VyµFA|vig41D#-ƬIш =s m(ob ;2'&_0)2=Ƭ eƓ`[C"Ħ83kIIbIRCf\F V:z0 #-<ׯ}åm`><~3UmuAHئݒ@9RQhh;3FX٢o{y^*:|m.hSvq.ذ? @ĹCї8Q*?cϚ}ĘQeV[0ЯMؤsi-β3–,uKL;qYz.8 cs ^ ͚~Ðʦ5ŪTܖٚlVn٣A7n0 ~RS&x).V+tU7W pv=q'D1HofخGD0-GG'|~|KF1hwܿhD>XcN`ʟ?J76|/~ĽX4̐T 4X}hgRqC+1.i"UorR;TEozBӵmu w]pU@_89КFt*.*{xxjߞއ~\&6nEgx-bw|`͎5Mw<8kjRFEUad]uRTqT{~S:Q s_ PY0V|xxZP%WtxqO?YKˌ5i!5FUV9(Xȴ,Toq`}ExYG}fI`%yOo)̤>t9RVhZf-j.)´,s7ӜoJ緓W1C1/m WNMٽQ?u2"[~\!Jv6U^7s%?Jk,)hl+aE#u60 $7O0~w7,Kf]1 W{Z=_ T򣇰nukwEǏIQ,P.d5]J o,m,5~-m0~Br8R"^fҾ1Y4\z>%~7؅ݐv- b>V\Dg0`Tkb#vsq`&w_Hw~" vП8{g\֍ q AbWeAжbK=q 9c9%0#e s+|;"jҨ-H5,sl0pBHf0\ZgUuNꔱ YHfa7E KWf],n6%LD?ȦʘEEGd Êq8uiFIϣ?}) nFuuz%rttc,m [חpubIG:T1й9Ge%+#1 aȢqNUҦ0qܤ9mɳc>W{7ੱF9[XGqMZc_t,;>~vʿΎ8c>;G?f ]OUG.'l&y,ܖQS6:WF1{H0noY?Id }bm[I7_x !<{yG_ oz2\}M#T!AҼ:`r vkfZ}vٓ N8k?ݰ#G [5GA h N@I CެA6*3F+Jɂr|xADFXo,6x+mt\#A`D_,A0 ~`w#pue9in9mcY? ?T:͊3pvzB4Jc>d#٬Fadv;4 D*3 mEVGqeNmWpGhLC}kQ;4@/dv_dltTm HNwDiw߿қ*Lc7coUQEMC"&T+&'H iUCQ-z#svnF Fn@839KSku|ɨV?b"^#~ 9yj7se0|XRkެ/]hNb:SRvM<" H4k!z!cl Nן|@i_茱Ť Q!zeY&Ю!OR&UbÍY 8+_wl~G OL>g_~jXB_4S~?!֤5?۫w8kl&t= Lp.w-s} /?fHWUYͮqz]V`sSEL>3y]U0UiX*PR@/,KkdU ѩU /~בR )2:_XCsz ۵V+ vv1Ά-R뵎i[I/IS+jU|u}QojEZ1%ԉ >k̞ EF n$?G-f "a/2&:'/^[v<>oٍACA3k88{'Q22ck^_B)ҵ-Ł~\y`LFCF9CobY^ !&{v4x97k%ֲ\j*8ML4L9k){Þ=l-g[r|3N(QL)TTVȪ.-ចo(4%jQ U*UzcUK&eU|Ƶ0vlѺ$wkb=?$sls~{͚ffX䄾m?_`-D ӁBDeI(WLjwR qԌ]ZCۍVfbYq}h}x]S*RtzF[sS3gm%˼4FiOُ_rs}ˇ߲ZEg?l. f%w1 #1' Mto[;ju}yPp3S '7D|Ndj<ཨتPXaHWJbΎX o.,R!"ElgA3'1$țN۶bUcTq(uo"9*fsXȈm[ķU9@2 %qV6E4E(Wq:[4iLPuԴ 3~2ܡ*$ɵo8<:XGJؠTDgc+-Z,/$ewԙώ~z%K#K쨚#2rBuc3/o3o34 %nnfU[n@ؐrዚѳuqa4;& ',=?__->}>@^f뗘N~z^}^`gcC91Ⱥ)l#^v2lcm[I1̅`^5S8Ie?"B/۾4orBaԬU :$a8s{Y--$b9 DXtT>6}yVn~q{O?uwZ7{?ފWDJ+D/ijA%N 1c<@O Iml ^Ywx @e%7…}@n+/O^ζ֍1Cl!p^yg.oT۔0،b$S9Q V؅wBP+pW3/>wHw*-RzRzfH=/$ʆ>v$Űlt9vΏ*!NxL.?Ni c`=^#{SB'Dfts!3~e ђ`&K<c#O{;ͩCajFTF$߽CAFGZ3=b^E*F=|RsP59(|\el/DrVr!K7yuIqK"bW]b x;)\K̘[EJ>̻" vmNc;nH5+fAmbӰƑv]&̗]0孻\m[tv &]vPGfwM0Utm<>Ǐx"ES!I%E*6u*V=h-iWRa`KfM/z-a,mu^$H5gCX9MuenܺøVrjo&+ȸE]\(:v%4XuUK;%C)mlESxݷ_gtxjyUuو<{|#afN8o3dv:lk8 챽yLJ="^1C[mS:IXf4^; 6_WW~Wʉ H ;ͳˇuMO^ nNHsGLk 3g3fL6a5.!F?N-H!*оޠ>OG>p qHTN.Nifd̷ eǂ#2  DE5ɱx壤jzËZua6k/ ϮSKNC$A9`k!ɞ,#B_pGWyi{6z4v2&W"5{|kX_ opR7OVǹ׍ol㥆]$oC\Ffp3nloiCyf_9~SfPGnxeٲE7&󹤫.xLJ?4V3{ !|X?5^}OT̛|>3v+Ej|;E%o ?OIvo=.@1o3Gi*?U LcHS Ľe9@QL(+w[K;D̜9)Rp@%df-<{?8i! ~B8WĒyM.iR,p+-S(ԛ}wbԘ)ZGĤҽ䅴ZZDur}c}?`4QJ$;#14F#]}l4Nuxj3' $YNjQLސ$0ECS݅gj;#υ)dPYN ; _sad2?Wg6n=Dh4E"WB0sS@$u54 2EZ`99I%Qÿnf A\<5>bx pQXeN v7pByl,CO;ҽ @`>A{_<WEfY PNh6-f$ wLy}i$#].Дv#v[+^v)GX<6Ql\- 5zxx9>b dtLC@qG]x0ll#׆٢ y> @Gₕ$h [9(v a")8? E.+aGE.G鱗cr91fh9\]m_X1=NecDsGP@k kX *Qن,]^>S.XwN٢f?.@v˙\`-uhspwX2f.e8<_ ¤ݖi>wحׯ;sMMH#Χ3\!F?u?R@d~x1Q4K8iFbA6n%V.Ņ_ \TVK0|cW}'Ɂi#DY"j  c<s !O!)=$rwDݖDfnn.Uh͗yX~臍VbVVp!rRBr2_7Nzld.mŐE4:FeTAru.>_yNZ*Re;#d zpB4_0酗FC-3f &vt [5f;)SMyc"ԙtv 1'IEbNj=^mHyO~@Zc١_n$-drɿ%",^%¨:|Dp;B?#{:R s-Yt{5-1BgFf**[b*؅s}3|SR#~㬎=g5LJˉ=EVU$!:GN WSQ8K,FgL= 'x8IwY\^r16+jj vw>("7{0)>Q02.yv-6vRLڶr﵃q>cJ]ڌ&W$HռS|> N#.fkV{p,8$5_H¶}0@@kq09 M-/! WH-̮ŊQ2yZ8% oR hb8B?ޥKGfY;&| ùҀ$C=Rȶt9Q7,78=|T-А9aL_ ~s;x#8ǏzebRu +t8* ӤsM@!3MK i/N˗&<өvi.gW"PG`l(_JQ͙d~J &{sX{%-rC IUy6: 9lB p|*Ź.If. MȪS%/4@ gQ,7$T3{;6whd@)-~v>`ki>[ /Xow\`u Ť,d5fG H磙 b"s5{_ri<+VT4׸J7#OH#/8~הRMdk 9}K emt 5YD )rM:I8dD^} Uxa(}F6X g"*W8My+ fu`7`&TIbqwi,a6D2êxIu5%nx8)xRhaΛky: rf{wԱ9CS_X$ЪW8PuA,oꢹ,WsgftO.؞c_ GVѯJ4R`y")g%#Qc [j[B믿-aZodeŖ8s򘛭rG(IGW_['،iѱ\7aZi q``Wx!̸ @r rlհB%TR59-@G_#B0}5 lC:dXST!&w#aKM&jQCϝ8KBвm(T^Sc ]nJ$Ë[b$c.Gf\TPYgTv$zw@ei24ne֌iJ{H_!b IDAT-B"8̸zM? #xO%%19.Xع wwhdץBg_NSS^.zȄ4P{SʰB1Ibd+ `8lt/D^3E`Dx y[9?VJOyA'%5oRtt-ۨLbg_n!Ȼ7VڛT_^?=?pb01 /K0%D=Zt ,(8_fzH:iMH)LrV P/blΫJ}RRQz c49G 9+sJ R\<ܔKO;#.ᆍLierlA(#D5栌g$ً.GԔ?հF.I(ac@GwUS#],kHqM Q*S~M&?b:K l."f+FB}G`? Ê(=1}^D[1r9#iUɠFpJN˩ Z%U diJ&pdNغF)EJOHsTf }i[Ҹ㝮OohӤ͹:Hu^/"R7*q|[hjf;SjR$lŮb屻~eK\EZf1!EHA 8bf ]oR* 2#iByy!MKHXX-HIKh]mkǚfAF y˶l_W0@S,"-7!wmն ?Xt ϔb#ƜI]#gHVnxV6"b8#')&-qgK`F3Q&bOH qNJ^~V^`ʋlWxg~\ߠbX%xL$6HRik|f8OF)V}Z ;O"\ss}1l/aӹGHfNG͛8eV7M`4CIbЃ.Oi,XO>m ! 0/&" jXŔ'E3F&hdx><חyC+.6Es[+1[=vcoIgz e74ͦWc5zhn-mζB]h)AŰ0,avC$@4G%e'rcd´ul4"]u wM1ԯW@x @lB Nڟ1e!=]t, 2 1}bC?"c8o1NJ}pz P[!\v$tx@ZmWՄ%*Q;r޾9ժy ;۬Ċ֒bl& {rz_i51#@S&{67& jG\jڽD aڂ'q{1|FCwdU.s/N:[[@¿ ,1EN՛ {6rS$&PeC F_*6OuDJcYXJvUnh8c ԹJSiiW6AUө#˩cB]hw-morX3řTLUEJ)lcvKZ$ %b"_ڃ}eu~rjc 1"0J kGKf+wj ƿ[6d{b ah(YPI 8- Ǖ7%y^!1ءndC#f}=az8F `O)=v 22\iŽ3;.ׂtxbgxvxq}י_MN !$VWEŷ&ع:vSd`j!WbQSЌD*,%[.39Jr_ n è"|kE`N /#$Fv34*bύ[t+,XWhn9JD˜O]e1ҸpgQXN {EX+9ijvx{0;t!զ[RB@/}f8c3:pW:Htz34;Pt̔LdOHecJ# Kig+`|7,IX"Y̮r؁^_e諰L&Ēp$dz)"ЄlKB#4yyxGOmR [^uYԯ'tf~w ~ppCDZo(SbcoŇ"%G-Q?Z|/s-+6KQx];vyLs)Һd.[i,-,JrgfDfZwWϿ?(@xegK.*w'(SMO͹y)ŭ%V7B 8κɋA< >hYnRf@zTIg Pxg r-/"zG0bܕ.[ʓ7?` VUa_ y)GV[c&t'B>X`R^&9ߗcf^C~QtWlz#ʒf6`a/jl[ˑ<@[wm4.Qغ*S*8>~FGJ +1G3xFL%"00KJj{ý>pXGg6bD"I0Zct9"^/ʴ +Tpu(HbTŠf뛍b}&Y+:\"ܴ֟׿ Ͼт0٬.Qs:') njr\sghj F"M!suE;qbC~j^ :7f hs)AM&BiI'Vv/j98{-!&t3t%"\>Pᣍ{H88b&C5Cd Q:M0ﭸ.rjҬ]ۗMOsG/`9,gt,F @|va#"2 B7r]:?x)A|QH 6Cfb)Fo1OG,H !2f,撢.$7&W$m @X.;+FeQ4o->ϟg!j2!frDTb&ݮI{/MT }֐yQ٬ Im #t2.R ec[8-;Г"Hmm -Ԯ" % . ,$ LsH MΤIi?ws*w=|bWy㨚`z]]f$ jiƉSEK?5HJFBmv}x4&viMb: M&paaj(kQmI&M'W(2RueA H=:ci ^6BOQpb2(l@ ^MhDb҄HFJvpT۪ˑ.-s `Kn[n[\& 6csq\j;7?e7]secP aL˰?Qb ñjbD 'D11G t6R)XT*co׹f0-'%e['L9ww?LvZU N1༿CA%$H)qUJsq)sa W{ 4DbT5/gM$ar تj0mJcXUe;N "YH2#]ųx-RRݫ7:TyOQq^' ֨%JdH|ӽ}tu|dc;l/#L1jL!q`.O'!zy_W%_c]_>p Ga(dv>nH&A9?xRL!AԨ8Dޘ.I)]XK"` fHavpࡢː qێf7k@eveƫd,;ἦTYc5'/kҌBBy%Q:W'W% | ϰ?`0ϓɴJ"ULWIy3N`baǫi^Ͷȁn;:6*1cvatxcN访de(j5;#YKYD^b›ުu&s5`'Ӈس9L\N~ VH/"@dB13+|gђ,$$JNHJX K,%'ռ*RDq2)F l0\`H. o7`j#aIqRk&VVe $FH\22G 퉅^>:wJ*$E}vI V*t%Jߔ2-s|jfca?_0vʩ4Ch*yۛ_߽F }MWϯ#`^-(Q u-e%|Oz8 HݚѨ/`L*Wxf(F9RG ^`h^ze&#-s7xvۢ;ƺE&2"q2o$IM=h߯ລ9Ġy5Lnb)BULHyQ›> )?c6tU#8+>g"}jSw )Qٹle厱 պW8¬AHs)-=44X ]no.EҤFj.;2RSunySV{\pB/ʐ43TsQVv=/ gdS1@BN[\~4;"\Vgh?hr+)ZHZy8V`4*ȝdxϯ3pUo0Эy"u9Wǯؒ@;*蠛N9xw6Z"  PEs8CΘsDkL ݻԹ!0PB".u@"'AD1:ƜDO46([56T:2؊BPΈ=X7ň‡|gQ#HmEvx 8}vd!AW [YDS8 l '6O8&be(W\E1x%it']ń\\bEWϕrʺ-ט7V&-Uh5~eT|@'%/~ų?jIm8]/"`vwՎ.#S0xWn ~Ѐj]_Kn (~<~htryrjpzw(v8ͦ){{"vA7ngY37e+PvfȨ㧇`03=ugeC, BfA ,,{0%T%$K H&Y41_lh]`ϓs)f_cϘG{ln ~X#k)d/MLj8 WB(6_J{!\l$n_6bAy/<կHqC{ɴ,]f\,% |6-a Cq5M\iZBt Q e&-)ϝnVXL+5=-pq5c,<`= w¤|m c6 YF%j`gMA]M(/6;|6iA5OiD0ńq::]d~zDۭ9m3¶+hmX.&AL<:h!!`\~:'qoH#gɤ#yr =Xn [ bnSw3 #\:ft{ _Gf̀_x,_/dc`$Jf\bD<Hpeh޸l @D퍩G|h_#6O-rLM=s(6iul)6k`18 `d/\䗥s2*KtlHTloL)ِ]_0$I2xn,:̇ɖ·YUHn o<2_Nӄ)fe ga@Iɮ. 26h؆:.Y)9KT6do}SeQz 2FRZ6SeR_AKJapSy:u *W>Ps(*"I-wAc26+49Dg4ӱ¹#FW4Mp|{C /OAAԴx,:rzΧzKbNAYK74xvj ꬨ~x+$g2'H(w4tNE Wgmw3n[$d)(vLޯAoe}!c"F[x\PhBGEigwKgW\c {-x<͸.W+,nZ(9f)@`:59M;{:l[Σuy^ 3euI}pBϪ#7A3pFa>)yKS@r$ϘϏX_b-l]}_[s!Q̑\#" g lXqtGHt蛭6 eBL*l:܌s@pJG%9 9p.\,<r2'R>T\`XlkQK\n}35Ţan>[֭&(:\Գ0%L߬?>ۡY"k aYG<Ē*Blf LgiP{Ku\/.[]j2ҫ6/7 RK'X!`X/;x\ǧZ{oƥК2Ui#8+Y޶b็@Nv]wwINl3:vq 93x=iQmW*?$B81`|є y\ `SWcB20GS;>!L#NGOO8o£BAIjs<p+^.2פ9uf[[ʈ<up^x?0e#Z!g_eGdGH gSRwsmR ̗&]f"}?s J`k ĺ;¸4(lIˢMCsj>_:19I'TBi(3zz¸d ^aWd4Tsvr0u$§pŅREJx+h*B6.vd\(8ڥ\mj/ [ l lcO:Kg /҇4yc魩;&PO '5>VǓ]u]15\R[b#!KrSv2#.I&s*PX*NGҮF,Z~?£H `eTQ 4Q/>)N!M==O0 ] $#-`MkG5{ܽ3XmZvL\Jڦbܛw&IԤ@={JsHJHa#w+9YȈ"i_؜WlOXz,Wb D>fFc?jF#ax;}$ˮG (G"ҙVU)DDDI8 (^ƶ<7 kl-e:@*S%Bw]T\B<:t~˂M\ &)VfC|ia]2 5  jIΨ,$V=:I6ɬ)WZ`s(&~Tj(V&TL6P {tFpݗrn-d԰м A bڈ.2LnkO;&Bb w`<]؎H8p̆Te eHnvl>vX.Eye!`d,6o S<f\h]sI@3!Clު>R<#wء۩;|3Z @SE=y^?onitZQY81EFbʖSLղ0NLR@ DD8| ovV?]ޭ:$ ]5bB5%-BmXxaT!h)~b?[i3o1ƜIj8,rsO[T\itx^**"Si91I{h\ im~)˦aqn2)&RTG#Ip}5 zLcti8/%e ޷{*-J2c|as˒) wǠMh 8Y &ؖuSQlgW_ h.`m1Nz_D:$yš+qzzǬ#h@KNvWpV@nG7єi1HDGK(FU:ɗ :o%-EH/ )WKGnz9:|uKbfTdjv BʞC֋< [[#RDӃ Q!d$9C]*:NocH \)\)E0\-O6m[_00zZeXϼ!c a=[lˋ|9%\ d0l*׺i_a0>Z*yod |%eqfn~m`>?#m øG#bZEv5Iq :b{i-Mpـ ,'&Kr7]=%Ol"l8f!)-}qyEiΈ-0MPcqxP{MqBfz$c7F|1t;*墡XH_:pÛ7u][ٺ*:.R'vjG  z[[H+pysRdb)=!V:P"!_ZdF(FQqlz-Krۀ[bU`xX-fˏBPEVRED"4 2a&JFnЖjb:dJ+UV /soC!>.vC 6Ǽcx> q|N>HRZ uLp LFjg嵆 [>=\B_NWT:Z\2r^1nϛų1mF٠ԦaB[7?-rI 4-]WTivr&}{ңUhZϟiz@#ܾ.?QdǷeWm"/uU/ܪLq;&U@M ]Y^5cF5bPYs\ (D=⑐1TejJy+ Z.e"Av(`I{/cGZ)=4:jkk\5JT:"l!bϻڱ6?GM2<%Erj7mۈdjJ?_3,:j_A@\%5@j .E =0H^8UC`q"J^5[ q`[Tmn6T0n7p8`^6l8 1ͩ-sס+"c* !iɡVw!5ɭ h2ڴݮ~ ¸ 6(Tc*}ʌG1[a-GARtt4 Wjy -۶ne4ǣb^ ~b'ȁ-[p= 3%.RÊ8uɟ+n Q *X4)֧TM!_Ϝ_7Ȁ]P*-_GQ_"ס_jz˗(%7'Hqg-l$y9'dIEtef&JrR4+ 3r8_&_,yEu]?$nКRًH,72iu5\ak*p nW,'_ͭv2-E: 6*b&ao ûM Z]NX+ ƫ9m<{5C3CuW5`,>pV;΃/:1t@\kJCTn`H2:GFTYaUhMGEup ESL tL xFU@&/Uk[O4c<&SꇱjsdEHtddmHqZ3 ttLD` LR=`8*.TrK5A*WFV9![U[n \}EPx/.PZ#֪P8(zemA@dC __"hNsiQA*j$شUT7: [&OȃQ6!!8;=C҂M8Zt&3e3M/F Zϋ9X ->oñI\н _)!v*UbB<_/2 J:Lo~,f3oW4Y*5xKe[z4A(h!47X\6y-B㍋>s-{z-Qe/m7tQ赗ߨ ^=Dgg㵪HgqX IDATkhꢕ8!X+]( {To%KBRڐDZ5.NcKP09#Z_wx}xWbkw jcjG_?tBJ^Ew B恼W/t`*B,W/raO<,iW(VDĻwr!` aHGm]^~ X3%5.$֑r*e֖q9\@Xrhw.q pHj?kE(jCB^]iݐ /7ޗq+ɍ3yؗL.1L/0NfDHVL#v +h}{^rY5+N@i@[YR*,i)po/%)?}_)3NWOǽAC]@N?_L[(KS#ՌkmѲ ! 8}CbCʌud!'liźl.yRYV +#VqKul[0Ee ϐl@>/޾[/b_H͞]a.߷Dᄇ2ZEs 9B@:JǦٞЖ4B>#ZA٫Q"P9mZ7i~o)~ ^?>]#Kx(L05T\Pd؋5ͥ_KVRC6CFʍؾ"7船'>s77c_e(q c"AE0[`+jisÄi*T!;%z*!NH+d{lg , ^G\nTWtǻ9GX7eLԜe[[A2Jy vatsgP+HA*ؒصZrV\fWc/ȕ@lٿθBghhvI[~UkŃq/5=_($w-殣g7Ɉ> 3yזx ]<>Ax$@+,i /'Pq1983Gv5! GYJQP!"y0}Bvu< tQ>70|qoB$?GVp=l`+ 8(-Ƿ=Z_je^"@ط=(cj ^3,Z]n3$er)p0Oh?_Zf~wxaeDQVIWIv\iG^pܝte󔜖-Lyf}$k;$oIRXP:G<\<N U ~nn +Z>7Þ uu:Q4CiXB"3YppV\bP!֗i.x#觏޽ݱ 5@8j ta]!! Y rZ %[0ҼݝcI>?/xshUK MO`H%0V5A5BN}|N4 _ Ud;J%0 lz Қ׆@pF|4M`c@3k{v&^{EIw~0D,F,㮳ʵw~I[n]9Rt) VՎ<.{lW FFPٳl0A4>P(ٓ Ga3I}KN6d|h-!~#}mW KMM1"'GK쭘Ue4: W37͏ϫ-'V@).v/F]|JUypUka`wwNАX}˝V.~8E"$B ! 6C!NLJ @{,2( ­U!+gRr}NE`.[z69k{<~8UT 2*1``}/ d:nm-hQ@-8O[F@=GT}[_Wly٠_!60 xEpo,.5Ҹ1$pd6/ι.#J-;M7^W ?ӵ4|~{TuZ 1@[`[NYAQ(X`|nZBΩ.1ӯ eOؙTk@ pomp8cHg,|E`QKs4+t NL8*3ķ>%3v܋I[\zble>6;tDϰmӼB>h(Wo5Lj{#awq3H؞ #)t!6c1iI#$9Pݿl-/Qe_>ynnq٪}*?vYض MlO>,gl"aara3rZ;2{E"@ײF$ZҀ$E/c,`( [ d"BRl9b6C+],|۰_1εov!"[!Ej&%N~`!h7-V ЖB;`sP5ʋ\0K9׬g"Fa@~yӻ>Scj_`'wjgݱGNZ\KF TԁlKIqŚ-nl{j:Q5D~M`DNbb,U_fG m*9CY꠾F?齔YvVn+?@{qpROt6RMLb[SO|)r툇%̒SŷNj5J9TT1/K،yx[fg@VpQ  8PFW譻U1ȗ Nm;?2 m6G-SF |IsNjAAsvo'b1D6_e!1/Vb'D͛pG YZ%uŲn+NUa@zoHu ^' %d@ H>ީJ #l]uB2 5ۖ3^%+8~P+ fypÕ4t wb[LUbN`̟ag}Dc|3b|sj=umfT>3X0ev#q#(b"=vJ`z,쾋u+WeFXӂOJv,{=(| ԢZ1.fjEz{[ ?<|d IH,Thq{x`,Nӵ: v (3@Hv 9QN]tIXbWprJ8G{T@aV"|<#^`ܽ{8'XiӲ}*{|i?VAa nG-rϩE`xQgܸn[SC9W}ߐ'|yn);j÷qz+bk^:Ѕ`Sqw}bfؒy6[WFf6ϧ 3GC IrwBtr r(Ј BP afw*(Y,kV4`[.=V{:= 1N-D:ejOP"a?B&I/v놛áuD~}oNlXjl]YZRQAfZPg-~('jpgCr Y!)3>l+ nbê(Fۢ{ -Nffb=xzԅV BU0:"X׮Cìk-#`yC}"u*H`P Hr.UEkGP+@4eU=[m UqYF4^[yNj IDAT@@4~3{%FFaQ7Z`l7 VmEBpa/ ]-3MB] G!LV6L.3a!Kj] unu:M^rqbxl㫩زw 4Tyo!ۋS<)툹^,Jq[~׏J1\ aˁ-^ڒl8&?\F įuhE^_ u#Hz"iV G$/ NjMk'U,Yre2$a 4/AާglZ[+ 0n dyvw@Mzɲͫ{WYI߿xoKeq*ca^LѲv,8@1@WV>1M q}bh`zb1 u Aut|^ .I3 o)P&)"a`O=[5CY5f4St%)?|skLW9eFh4Y[R>L =p}]-,mk$ Nkѣ]m,5^N/ۇ3֔;!mh@ƮvM w c~)8,@k|n#evD T\`XSVRV皻-,h60lGc6/Z i9gvjQ_J9⒃vB}qV C/M on{3PZlU3rJa0C$AOH4', aնU98+y]A}^ɐXSÆq#@ӯVq_À3nyÐ%a§b~OᾱXu{ mE<%PD̟2[E, }p%u=1ufp'7[OiEʄ3lT~_S$'M?f;58j\h/ejXjtҮNᚁK"$bl[n-P|X_}=b;)"jAG5C] P3 3I!yfɌ@UT{l9Dѡ}<'_cWAji]V!iT!fz5rIA&?!гs8N6:")E;l| 3\p I\ޓEXrڻbݲCAiT`ˊ!bEp-[#hqÄ-19@\#6nؒh]LFE&aZRJp%k].?`㒁K&n3V%dN@%ޯ"' ,cbwêpDC]UF=Ҳ@Zg(?EY؅S>/[lU'XodG`yѥ@n]E5"(zPr=T2_-]]2Otq3T";@Mr@O^iaL{.aXTp9 <""Qux%Pɫr5 Q%:P[F#ܯ6l<0%yцE d QZP-(W+@,*)ٸEE\CsޠTZ 'υ&MV\hݷhEX 3TT#;Mk!OsC+Sщ a_Ū`[ԬS x9'ߺ3q0LŃ~SBNPn@û WP hgDxp%l(S,P0Wq! i`JBn٪U`YX5-rc!&f1:6I{)o-a'G|D',[rFRwXQ=ځy d,J5" ?}]IJs'EVW |=ہ/a0WG.FWgfH2_}!H2Z".xn}u۶ =9p,9kŖTm.;В[a\]%+k83&_j4~Tq!η456lVMA@ i^VD,[r1 SfiXR7PKAȦ5A\,X*qqû߂rCl{@$+x\Fl:BD-y[nv"q]E,u;rY1o;78ܼ;/&KisoF E. w_f19۟-%/ģ`mqo!l՘0-z3@aB.W -o '`ӄFCۻ1`7imjcLV01v"G<=x~Zb!J$>iYqoxCqh֓ZѢ0\K͝>w8CM¿ovC7 .Z6- Amj 桊Ff {|V9X. Q֖Vu3C0VBGk\/~Em?#t__e`{ϯHeiʷD]^0iEFf{XRZe/mkˍSP=$gu -ꜰ$’1[sHi|7?O>8: *ZĊ5AeEVi"&nF=q㟐ËrJI&6'Cٓ9[--k[@v>Q%dGSevl  ,NO)@o0BV[ӽ0o/;vR4o`,hC݆VVxȪW'1HFDE-u84mX cl ޟN_" $H)sWY?~4c`ƼMX"ZY ׃6Si5%tD &N(%hAS ȱ MR"9!q`Dj*i%4eYr , \s ]U ;ݫ a$2Ddf3:arhŪc=aU?pq3 pR.㈑a"gj5],œ ލf(,'w+j{G]ȉ1CDq/ +'S;a1fvQ/Z[aHp>P]T}1#?|~k9=$d8k|tgm,֯v%Mư$1pBgLI,ȲVaȮ#%nh6 ߥ^qBaJ5~BW9/HQZLOPΝd,~ܤK!wL(2Ig? XvQYyy:ɪvJk2e1jd,˅Veof_Ŷ<#i>v,ӛO;Q:Vֺ2\3Vh)áRElힱnfۺ3͐4.7ͬ+yF@c]5!&}c86NP(XړSΟ?}Շ!fz@H8ƶ༝a:cwC cy[N%/Z'!uWҲZO,eۚ!)81všug=ў&]QFsK{![8puý8MG.Ƒ[9.;bZf|yOH#a냾Zݿ:˷ ۞3t5a ySilP ))O1){sKl@S'^d ;FySRӓ^&D:S;[L݄S76aKaGgꜰN?L0h<"b{fΟfEq)k(DrsgK6H ͦƚRtYa.1aqKu&=e)g4/¤Rɮf&a.WIJ_Vsf ((ILy& .^^9g~^yam+Be3G\2<,OlS+b+5~qHfu_d.\qJ#TQ /qxq+[~ķPLRZ%ʡяB%&L~~,]> &9 4a|i+1\s 10&w5 1%L㻏yβ^4WsnYv-ֲ^tÎ};]yy4p!p1o.њ*YtyuKڅ]8|+߷woYN8o~.C߇ [W+F Q"' pМ /emGbjjSj5>]B7)J`L/-ma8Lϐ9"Hak,fT ?pڑV5gbmjs5SxYgߘOdr1-dEfmE8JV)"WrycP/z,žkӔb<\1+V18z1(5喖m%TZkDm>rd -VRˋH7C"(˚]6բ?lVc/(sٚ7\IZ5LKc7jS@ҏ]FnrYqo+ƒ`o7V[nQi)Sv R9s8 QcLTm6i%_|~tGGKkrY֍ci"c 睦racYS=ݎ`OiTVH%ݢe쪥nurNw0R) g1ZEE=te|+|d|:vs83ybۿlx q`Nr8Z+q+899%F18k'@-GͫcCvioaHHvCwDSvLB62N^]&1j-剡k HZZ;N\Œ\8 } '}Fzrku0i3zՍ^NFy%D48%(L"W8Թe5T]^H5HQRt*VЁlgpN/FDc&\` v r1u>$O :/2k3vʲeVL 5fIJFpע"ǡs>`ZfjlVj]`9ʏ v+ qi(bٰ ݎRaX%} chgT1m1Pړ* p0d-sRRrr"^AJәmhnWx"N˯>e B9EVK_q_v.e̬? <2S Y*0j|1ʁ8]sS~ lGXwqbZTczs7AmIkaJu)-SAe^W! Uʝp0 _miU`א3<~~l]]86-UU {dTI6+2g51$wU[آC &.ϚzäBSZux^ITE;,2,,w WN@)|ⴺTً#Yq~ޛa qIi/xYc e{65!*|%e&lwrr1fl2cF` ˕x/(3y7M ^82.`1IPL:e|SOfԁ!b=xoo0|ےRdMH8H)Y>ԊH*V.'o/[<6.A奝SE1jH`4?dTïd ΦUO -X3oxz Ma~MQ\2 :ó혹=]l[Gr-V+r);qD+HZbakVCۈc{ռ,;X4Ef~5Ab)>gyn#uzL|ΤCoppҮ LBXF"Qi^A MԤ" x>'Uvf҈ 0a1\Ijqp62s9bIq$=1/!!Yn}#]WXJpx)I [QeBjrڋ'Ub1&iDI"S:5HSAnQp=1 Ft k͊).:+Qe.iK %@J}fBzM"c޽ Ƌu4 `,MYI07.,K4A 2AXՒ_PHENpWˆ9' __wVW10MO-Id$ˡ8% TlY71o$wVr8ck%$khN,;4i$AyыߞFH"U6a''(fi'/A7M5-}159[O3;-R]lcH1diVfCdb6$-$hX(8V`rгU|;эprX0ȸi;ij:FZS)<0;qC)9ů04÷/`=&Z.в\[%\NR@]&r#P\;Se):QסsSO n}8 )M9DD'Z:q6.+8xMbc&"ˎX4nV@溽 1;M%0O1j{X¨k=oGɔQ҈Mkz V%7|9+~Ben97#x8yws؜㛖grPo"jA8=$)aﳯhӓ O=֊Z3Qi@ Pf?3\|w.,F{'NQC|λ?:\i_wK5{ٺFP8`񆶑TVt2L,&l^x<מjۚsJM1rՍ7g<|@]$a7-AhVCm%s?~x{`%lrԯ(Dp9%7gK[.7Wgӥ=aGX5SݒXX\w2'(v݂\(2J_>"8pV1ƄYh:8`{{M2Q9bUZeɩ1@`lK̬pJ4d״~H8gH3g2.`%-[ؤbl}MOh{RsRK5w#!&(j=\sE_.E1tU[d=)&-'_;[9Y頾ѰOAEYEy K %@9as(0E 7qljkWI51 VUiYEnFE䕈-(dkYXgiJI-/_㵊l<=??]W/ޙT2EzŒa Dx%En8bE$ESqFʐ)VK]FaR~M®06 ''}N}O"zkRB)~)ygzmt3gXf}?_ a%2}SL15Kd H惹 *JŇDoXeo 0DBiy9Qh[5״>m+UUƧ);^~ۻ:D*{SǏ 33TѲe͒=g>Zq z5᫨vM_z|so]o}ܲ۲}g@c46?1/yp;d ~7~v$ ՟iu(3-̿G{;7O3g5k7 kN' mDQ˗ÇmKuK^%plvaO4mG:Wt,Vi֧gs<yZ;k5ۊ '"gg#DXYṗw{؜: i_ !j4A-]b5w-]=`y򄱿c}`<~6;d, M9 2mPo~-g,FMzXCcuJFo A+"ڕ.etmgȍd"eg.F5m89Yb GU9Ym!ʠ/TPPYqJ~=9HFyڅ@^%ٮ~]9E0R$~ŦP(t1W8»WDf.,'gV֍vVC5r$| {cXuF9酞^ACN_Q٥*j|%MZ4b<şP&Ĩseӷ:|ǐN9BF+u? U. r*2@ʉW7If$[cjuB 5hfO|;K+E8b~ }/pzY4 ŨjLQ* h, E#k)Bck$kK}NjB]#keIYìpjfqTL)¬ e^;Uz(gmiq#wq#&B0n:'O#_G8?q&)TMk9bQOVkVDK5< 1 -q<ϱjo)27s8v7-NAE8-NQJN3dNRIK|p7߾d ZRu$)A~g+r<^Z_\E/y-jD|,*'`3egOxm2WKth)hAi 0R;ߩ,FfZ3>/9xP̋PZ\&=vQH֘,GdY9wja8l)*Of[%MSd5 4J3FK?\IR5!ZP8rX$DQH4ա䴤3{*J;flх63@ޯe]ۻ~-V{ߦIePhZ1z>hC E.R8QW['[<ӭsbS;:?g@ ^A?U-bMlQp%z94p)b|J#8bL ΩݬW g~!7HIRK1yekXf EW5R8XB0xr:&2A4͒nyo8ר$#puSK:PVc+%{vWBmK%{~|Sïɚ-P NQT7l=ϗ?[_dMW}xy5,mO`9%)LG9Kchϟ?mtYGx δrb:EY7IVZQAI*A4݂ϿS>jYAVGٺB \ɣVc_an6Ct2$[qgjddt-)Y$d32ᘉT5H+&+޾:4b|WQeMU&f'_JyɡEp%W? cfM,WX֛L(ヲ() rwWo`? (]S.m0{}\mQBʌq3e@Y9Yg%C%r%xia)ÁNVֶlV Zxκj&-w/5g' 8W $7lW3ó?R6wHGR[|vEz:\0X ~:4h[/"ȫׯBKOZN':ޏ|PXZ )$p˷/w _qmj-ZzQi~JgO4o/ƀN3Ӷ7&khT~aր,C<_z~QpY1FPUQ! G,G!Q2]*P_ro[aqj#jFlDj] &c0NX+2Ni6"^!5}yn\fK!Ѩc&Wf@ߓ P*<+j,ZqQ}jF!~k=Ct 7CΆ>$IL*$r)\D5%N>W h72v Ͳ%WE ,ÂYT@,-cUI|fM XDG Ki[D?G%JYu>Ik "x$W/ fQ/s%7 s3㭺&"tD9_qA‰-g9q703UԊE)yG8U۴UhP9.0Sb6 S^VԔʴw4M9X= Kd~V:y*M[xʱ,D4ܼH2w?HL^ּ` azK$3 HawV Nx~<%OR]ƔeMy܌lf1oyQꊲ҅Pxb+keO1rv˯^ے4>>YygW,@>A`?FڦaZNT.EO_#Bʌ)10L`JK|2%dT0Mŗ|'BF>kD*2*EڵDàkq1Hsƣv-'Jgdh\hSу*ƶNa+ Cm$^3zU#Y&=m-8F~zjAsJqxz;X\`4+obk UB W܁~MOGaKxgX.asi[iͤ%Cp 8gZM>ߚőWoސV|q`N7 Ւ:ќ-# 1X+:B[BSxq?dIic9Aԟ(^d'ĢzAf[UeiIyӗcZ$ht5Đ]_RL42qs}wJ2Ӵz~`.О=ôk`vc* JMұ3*A $r-kTP]eDs<\mbam$>2k4dNkpd6_cF VkiRV[E 9fڌcn=)Abkh ɩ¹B͈Y;Y-nCuy;^~wwJ*Q{eB-cˌ^E^cTw}hlJ1w]ËK=]ycy !d[໅K!9oH+1׌d6&[͏2~Ac3ZƓ'fc%`)O, SG?}ou|SNIUN0brf˵^ wcVFJAh۫墄qGnF]$ZUG/ap,s—j럳< a֪ElIBOs8V,~= %K+[&&o{!Yhe]:cqER w޲ZeD8o0%wkړGЮ`R( o&01ՕQIH[_Y4-."ܱVV?*GpiP^#95@ԕq4;ˢ߿/N?NCG@#N`*ƭ+]^˘%A5X&USÂ"L2x5Hv=|V?\3Eoy" I𔝠ՂIo, y^-y|o{}U*dW9{ł!dO>4bJf)}l\ۓdr9g\Θ%bWnB-[ƣOs41Ii[FʡZ.$2;3dg #Yוּzjh,#kug[ybJ'P+TdQU9'%Wg\YezZՌ/VT1Zx'RK50gmubiH](јrrf^}rlcT0Qd?R ֔)Qa vZr',כ&< !suu?@ߏϳo47YH28FU@J)I$::eb-Ҳǁ8buQ)v'SVrFvƑR_\`N?v q_+c+42v%Gm4GS̉вK,OJS>Eg8?xsu:Ǔq+|izR#~_U`-؆na9i:`4Lc1YZ oWj$$-mŋGyI.UZxy^>nG.qM&UX&\#յYF8vc`˸z)^FqX}lڶśor"WQg%$’D-pI|枪z8evj,Fv I/+JYtY6-LF0|SC٨DL4R'A!gjg/6N # {g_FLY5>34B![ U5О) i,fqm׺oX=ew8/m-_}GO=Ϻyv{BJ,Q~ss\{%eQ±4jwЬ6,6tU.a{v-OA2`.ݚ4JĴYzhT^2[$Я|;i.t/ߐ mC( Κ'OhuW"mA.J[xZ6_)uT+ȪbRw6R#.k*|e!yW~+!9rc x>%mx8pc/DVцv刾ε!ĘhߵBJq9k 2BA\'>9ZSC K`9 ǖv i3EAV,~3TOW_2Vk)R࣮T5#:+N+tzJɑ%fxLq`)f-'ۈw6H)B= OTG>28Ue&KxedKZYXr4V%au.U5%wjb2E;ȭ4:rT}f97 F @+Zq*1˗01dWe}巶9\lW4@ @jb5҃"4(F_ePebЈH Dm:˥c쓘4*9{Z5DC`[/)Y9:eqtUOmNi[\yi&ۭmᚎ%?q-)[uWAc =JеEG`#S*K"AOZb4rja7c7I E {K"wxw3*~_ڤ:~uqmQx]d ɚ@(~F84spJc׬k1=rZxgS%e\]\/YՐ@­mJ(N`0[ph|SWk^DmgKNŀ)&!(=uEnzձEJi_\v-U_J‰ Q}8-J1u/-5 ja/5(3%Jzjf,CŲ'dCfAk-Zien:YHmjo|pBϿxdi+{aZ6 ZftKb.əZZ68"1mRWpvYnGw-0ƈix ܙ234͉q9n>8hVjXml}AL>.IUTV[۩Z?@CbK@JKU͕lY $So*9δJm.~FZB}/q9êh[lI)ob-4KiOĀJṮ $ %s?:bAGG)N"ִzyxG iPFCrIT*-Z,Kt%U\h5V_ IApҔ(C\9u k{,1aAFt'hWYAPDTEKDYZp%ĘL榔 "zaִGPBN[^ `ydz!pѡ/69z]2å5U\z]T K[RB3ζ~}~_0߬18L2SZy=|Eosou 8}qX,;<~\Մ0^m[XK̷*64(-nci 5 j$&P 40.:suڮEJ'(=;S@Z8kb˹qחv;l ﬥC77k4  *5[_K9eï8].]E,dSnD>L t3vd!Ƹ?G"YS)ŚEɔv ,Kl,Egc!5S\l1 ,_%5{גwa8d[P0*=%.+tGԸYtP9$ 8q^H=?.4iRg?~uDcRMK.~YSʌr,bJ`҂[4ۻ-Ύ+qpF'oZCEʅrJe%k̜=,8hE0jIK3:,VW6Hid I3#p"J:v2/z<6L%i£k# 3* F7;l=Pܒn51íEr&,f%ǧ.SĭleGc0?Oh拖fӥG ^(}Y{VFA[xrLma(AzRmI %w7VGGyӈ|Ouo3i]jsjm2h|jE>K;rH6n.љbL9A|A}&׹gC)2\*pݏ8,]}1mo,NѬ0+T(Fc&W: wpI e98D7ɀRrxJ_r1M [<}|VJm(un;ݐU[eMeYuYrIxy 0pô:13yQ)VW$ka{LIثcBj͟Ũۅ9DH-X,x(ّJW`,qA֙[ݠ3| ,! RmJkTm:WRP-h *h<p /qw1?y񣕤&Sƕ:!EJ Jgd\mp^i.( *מ؁'XFe$_jP孵1N:*!g7BPhՈ`E*Ó6\$Aڋ07T0 ..جZ]#XYJTEZ[U.YEUV/_Vŏ9Ίp" ~m˖@njAl~Ϝffӹ%Έ/,RuFsrׂoѝJ1x}?pT<:ZS%#M%\GRt'XNס0fK'ʉ92BH"gDz2o4 '8DD|4 fشEL#J4nQuJ#_V3`Rp݊TRhK&֖?p-NgpTJ(UR_rMUQeTftr=_?'[UiϤ(I%2·0 8 #9l0Z⪮f+FmX 8GuɅ5ZM $-=b0' k/hP(e0Y*MŲְqو)0dnãGuK"DlVakVqB m/u3QED^| WװD"+eˮjV0m,5Z ];-4<\fI I2 ۰nOェV3ap@6fӮQ$ `cu|Sfj`>a XgWr)L!!2b,87"f?\߶qجW8;9g,7:_+JCƀ}l0.sIgM2r@+hik; #pAY_xk^cw}w/p1?D4mvg2"*A*:9bY)OO:XbCU((:r.8ڬC:w˘%uh8z~D88=Fӥ_넷YTWCaȡMn\r$/\Q|[q"X|ksS$Lv#cDdoF|NF I(O=Ois3$TY; ڶeLS糆Db_|hlZ=w4#Lqꆵq,z%|}9a8}mKv=.)H\ay7X\RjCϨ2jeONx&S6Qw|L<[Wu]&7%3A',OW Y'-3 Tlǀ 3#җ⑋se8^*> ILC 4ZIJ6G;t:<8=QIqr \b#Da ܤ/^a Qe# LI䛎+ W%OYMԥ 3 ZǀEkw-W6t"A)N9p@)8]>AawȻk $^%de(N_dq6I<%!JdxX 'rj4()4/o~O>!'9f0UbɔKkpWXv<M6B] ߭㈜5\0hNtn㋗QFՇc1x{4XOXnV ;"uDgY T(ud Mw ]TYKbD .D84—a[)aZ.ȇPRxK+EjTU3{tGGX?ľ#Bh:K4M7.׶ꥶj(ʒ$R:Xd˞^N]Q&nvKi2ͺR+ATL_CT"IHbuz}\jeF/<΅HH r* 834pq)u$2U/G=!nr=+^U)Tmhb1sB#o8C)Lkd,[))zᅀ' C" 3-@`L#UjCH'BUﮮFSwwchжlQRsZVW@N.%bLɫkJa;w|yse Zިx )!:J\E3 5"材+h L-IlH/t1=BS˿ CJw;8ariud4N~߫ƢdF|?~×h ` {h" w~o+7qrή14{EGixOK e,\sR푙SfrRۡ6 sN$䌠[ū z:D8N{ΉrԌPdUb{NbXa9e@6^IW֘Qa  OD^gط̤P!kWVr ˄1CjX 񌽰:Z#gXy8Zam 2w;ˆ)Lb%WX)ϟ&>@Xֲ|Ҝ S<$f6J$Wm<w=Ơ$` nq;c) 4dL\< KوO'%*pƎI&)Uu% ?BwԞpazݱ[:d-UKxun|-őZ`kQ c?47 1Q2Xf Oe F0 VYZQFpmӹz[Dv@sbbZ +l/qttOnS-a@\w>rYdd]Bv88KO~~ : L?hEL9g|[kxQe!N@P6C o1Ma~1?ާ~{__p~%?yN}Ĵt ١&XjCR 2J.vEN1(+87+趪SdW0U=r,8 .>hQ*bڶE`貏LA i {LH6V;¸;`5Zx/gӘM̽eo4H$.}R8L0Xqp8WWEcltVtDq7Ce_p "}79labˍXAr|@PZ5xi̦`C?ݡÈ 3SH-j)Co! Ĉ1Sf׎^,p?klM㭱Vr+>S5-<0!D,~­%a}n}7cc*-S0փM* ;?~ =-e, NK`ATfE2  qg޼d %'ww$pqBEz0=ơN/a٤ ʸcGuAa8;q~z:oQa,8o0Op*d: fxwgprBzど5ձ#,HĹڤGV!YH+@ x Vx8bXa\W۟$IPQ&]lwUWʕ~1IG/m`2x$a vq2 k KQN ͽf4l 0KObuӈ<Cq0)jysN6Au[f*+Zmķ[iP=xdro0ts'8aiۖ3b*q8MarFyR+-6`Fssl[3`I9fP^!èV[z䜲VtH|k gC"aKئX2ϳrb:dDq0?o~5glat]蟎%d.TY!1Ӂ" rθ~ūk.>"&lG? !L(7hA!L7W|CJAa@qdysO>?GudL;TdÙqJYֱ8W{XĘwrD@PY^,iqv@顯8LT8c7'>7M !ʾ1tp(3'JIX4V @,iLbj9%'RizZH1e̽0Ym 0؜>h[pU -PzD矼?wo jmE1,q)eEi(\3pU,zXak4#2οx뿁'H՜R$*L `|(Woq5n09oo矿Ə^#LI .s^S տ3 D?YX4<+Ej~iKKk5VUaW#դc *!fE"󲂃a&*uY@aHq-~d8kXp/3)Fk{@X=v@6\k,|ɲt%m8mݞ1SB Vx)/)fѴM O,#ٯ\UnMj(qnPTZAaݒ]!Ye5/ k5W&nChgz=]/9^NTc~Nt[6 E_})>Ưpml@-x{4(9;)/aB0)FEGJpVT9q=]\!7'npHH+?\/7Xҋ=×/o*-̸C)81wo?~!jJ3}xӳ&'2]9Π_4, C ]n67윐9[[Î.k/|USł$E4[8&+i@Q!!F i|ÇusU)l,r9--¦mPL1Sڞ-Zxzr9FitDy#֌C&AOf F,o +(-ҋz5$,Ї}jc2!z@A0]g?}(_VY$К$,aͩY1 8Su^j}ͭH5oYM2؎iD F0ι*(<{Y7|c +J;\{G,lWL&&l0bGcsU|#0c BHL)&a"WX"4!)&LcBP4"0DjE\F"W(ma=c2\f/?{0׹J_oڢ(~&]'o=v"u].iM)s1T Y 3UA΀D)CL3-FR*\B%B芔oe>Β#4ƩF8k8ksNS w>HȜ%bDT,@Do/YG'%[\ɓgPs`\L̠S~-ݸ>>FJGJgys`-)w| P6Zv1ehN0NG(-R)RJ0Knsɣ)LLw!7RCY+PS-hj4;XKR4(bJg<tp\4!Ɓgmr{39q8UʖG.X,h,n)B3KnĬnbjs}58{XZ-U4,!%4T%J)-d9iAUZF'TUx:/0|}!x;r7/n݁1Ro &`8:n=LX%dS?h(3Jc9K?"4#4ekg8{x7k-=I2”L@`Î2seSP)mP/e 8REG<)\ŃO`S)̲G`bJn=r1FPИ*2Ty;YqJs{խYI4 -AH R~lБү8l 'c[%)p;Ip$Pz2 YXH9JJ]Ϊ?N͍J`3cxl/J2mY vIl g mp4іbf`!0H3Y(iq|bϯwrF/c YUWY 5_CvQ"gO{{2HuV5rPL) TjRJՏ[؍ˀ|0f_G"$}a!Ƅi2U|!Dp[_<2 bz>sHT[`yBֶXJ7d52=)@`:tw *FYCG0}8%Ua9EتFb  I@t)7R-o=_R(+GGC%?/)f\ɳmZ#qd܍!L {F&m)ZIpTg쿼`[o'iHa HBLY Jd%JVygog ?_6'eaR~璉6#Z11}-݆sFwS:F  ~[n +@fso 6'[ӼsiGJlcEIe1 EuP 9uJlAMsI;܌^١FAƗ50%Mu).&£<6mX"*]GNKn0p @h%TA?ްZ$r97"bxu,!ctي(YCumUi5C݄a0tyj\KR ' l(I"iAVY;aj;X%)> eY(FAX ?&U%: YqusN~iPc Y% D[[(k`\5:1Ś_|spΉ,,6B [<GO?‡_ALΑ+?M"˕]Yjv3&HȐ+?|⠲J%V'DkN q? oSf_gƇ?uwC嗮h^#tЙ|X?b}SDXཏ?7QcO :#^ed:~?=EL 3EgrB{,~rhVRILJe.k(3Q1v3z1p逜2_W@1DIzg,7k<~翂>Lݣ>xQ涷ZjKgnjmEře'E;]rp'/\Hv~ WxE)љgNquwHڞZǾv&5߫*} 4 )9I7rV@F:!{ܜ -"-i-auΰXku:[ M o[\+ 4PZׄaST޾N?N$Z x9)l\ Yrf~.5Zu*3"kú(u2#v>/SEȘT=N2~s pBS(64n0McY=C }6(jÈ1h9Lo 9g'70)ٶ ?|loIcCD*Y@QJf,>B6"]{LriF3hGYłAF(c s[?c[GVdČCWv!iW9Rwx`e?({/NR0gտ *;' UAwy$s7LCw[!jPCkӿoQ>^#v7*Qu*Z|I qxk9*9rz!|,<uVw!U1 ȕc(' $ 2C4ցL" 0 @G )i7:levn3tB,'r .zoP/Mc\vt!!$,. <>IaW+u6]RIDp%b<~M,wQx 5@ԑ')u m"'X*3]e$[).V@(tp]槭 K盨G9p6.g_(S']M.gb8Pjg XY~zgPk05MJhu4K@Z#"`_xu3>k [|`QQ\p.s!v6ۦ38NmtZg 3Y>cg$(zrCx/鱀ؾvd9L*taVv?3srIg6BO\A(խgt1 [ !wDۜM7eGstSP~ T7iy఍8f}*6 M|l߲>bJ;aH|ʝr}J4e\\< azGm(qFI05\.TD ۻ7?G"C#ϥRgY2&-ߊäw%vQIha>ΒΈzEcA3 6fI&ڽaӬC |,c똱.fVT{Y/4pKH;y1\ 1k KXd jf3rI(ӌ7Wrb83lۭwm94:n@eE.ˎG*b\#w~yϞ׈qBnцX 2#NeieyIsF Ilm PjBo`ޜWˬ>w*$E7xv_gcl5*,ZaŘT/ј~.MiôZwtVQ4v?+~,|~z[R! M;mI1#FG= )yu7;nlbDݤ5"ط\˂woCM MW3m?=i:kqs0R!!jۑأmVtVnG[4DPi69ʶD"Np1Z2X vۢ+MwHKFLEnRvx)u,ɟ_oBי‹U8!rEO8~ <>ci;o?u!!5d&éu2M'1»3) 1>*t+,V7$uP#1bJ38<:*ޟ1&Nb>B *{f T7%HK@vut8 \eU "(blKys>k ˺O:[ǝ]2cDmUl. lKcYZDm-e 11VvٓTJ xwq}R[\z')V20$+򨤘!?mX|D\܂B²|BcS1pzg/B̺|QBoʣ 1#DIfL*-1]0yBh |㧿CI`V&9#;e;.4 G7ͦdˇr ɃJ8`0W; i B6њd12Qn ! N'l ֚ScP Xp:f- 6]gZ)~@&P-OcoĒV&ljRVt#ۣWiÈ55C@yHnѦx|=G.MK&^:~?| KR5݃%<HN=&/훏jql`:<F5EQI3qwxHyX͠RErV)~̷ڦ.8 :? GPJ01#l̩:J64ffcm0l89t6Ժֳ¢ƠP5Z3j!0ZۼP4D k7(;,Bx=l ªmnI^ Zo 09NW>$z8=51FLtzB~2Ӆ9lْŻ6Px;3ޚ =ϛ$.ÞȻl-zsىF@ J .ާޜdF=Nr!̲u&@Q6ʮzmBYJ?Eq2ecm2UOD]io$#]QWHZ|<_ y~rg/EL@߶i2;+@ϟ1xH0'3TC.2&⪇'U}Y>2kW \Xh]mu;W}D]C!Y H>GFgLT{պ{v5b _Q![ cYܫqa/[/o~ _BM"f}߰xƲw#]F[W뽢g2b|r\r߿Q6RLpЃ)RBxŻO}3ma 4E$Ctd(TgZ@vq~!fi#uӟ-mF'`YHB>G.[v{@J?D*O-a|xt>E $%/*hիCo!1\~cn0k%j| \][ `^OqpMԃQC1Fb 9;oHz]Fck˽k}-:8yVt>. [H2ripvUedpӐ?+<_D'enQD :_Gl#nOH`Շ ?;Foyp2?F ÿ=k|d$\Ky:$FݏaLfC*7-@:awRۂfJ 2;d?JΚ |}yw *Sg~8Xc.%DU9:1ONhu$f³8#`%m?G˛[ŷ/? y6ݜ\ϼ]g@8ݜp{mhs#a[,;mclJ-J S]N'Hٟz{E6TvU #K"[Ŋۜ(@~' MA[^-mѻSBMɷyMb[~eУwT/2tc 4(#򢄗4GOf}ްj 50hHSĘN4|ʞٿQ]na%G? jE{ǶgCC )ƍ- x EhEP'lQh!6F'xϿL]FJ-Q Gz攐#a%,8Lc(qE⮰4; AF. dI}eu]O֤ݽի1=<=lu5}\ḟ'n#D:b4CboD8\+Vv׆@!\EYqM9;4DdW6Z'ԵW<c2UR`sq}_'̓Äd ѳ%__wZoT)3BMGKշjS&oYrupАE,{/8Nvo[Jpv"ְ?v*dV98UCr9pP@*M)!f0=RY_Cgt݆ob 'Ker*DSS&tj .-ӡ8tk7PGQ냏o rW_MyۍunN#C)Tg X]cyZ,Ā3~xxИ^ ? f!%iQ5N`R |3'L#C-wo<h"jX-}W5y>[suTR :5Oz.`^q!IUa2o:^M#K-,m](7 kEm8 pPAN} |߯5mޕV,CҖS$QeF$T>荠dr̸! ,Q;xo|O$.=وNp?G_ H~~Cw /hu,N%6;m}E{skKL$a;Cʹܙ-'Qv" 8fܷWӪRtqYQ}ŗX 9r]nXs!mnʼӄ; 1S.˅ƌËo=o7G7K®#bl,v>6sŽ %p<>!GQw t.EDki ?;š[T۷Qi0TC)x5iR"j,Uʠ-zD:Be^ËMCi@ZdCttXi-loTN^'-l«wy8z6-:Pa#czqo5XCwAH9NS=ˣY my|O8k:To߾zצ|:bb*q8mMo(J LY % ٳB)&"<$0j !x}<|z+  wo W,J8DsaJg[8E&C^4#]QL;ZvŸ̑b:[nOP rtuԑXA\RApwՊw-Jp:LI31b@Cx+on:VB3޽uA1Z 1ovjEs+6/Nz06,`Y  nNEuEe1ͳJz[B晬!v46ivN2SKstAHc.dsguOHpS. ޳u~1N("/o mZ>R_k4!e4@ZY~eak-t5\̚˒hV&~"aZ뽓|~Q]ZzR"r2p&(WU#(Rnf2KSsRU~6e E'pi=ew釳;Bew:v-䇿`&XXDubqx>oU 1 &BU36\ ߿y_+|ٓګ m[^2p% l^UD|nx5ey,=" `Tt0dhSr*fp "RІd&(:G v-0ñ*J>k.OX|x5<.!!, (@O4@'{{xE.' Mdb4]<00~UDE.ݷ=\-pgeki BW6)CZj:ʌ2.n^)&pN "A=?ow7"WJN!#Q*m߶Uz̠eWmۻm6x8tS"cMɌB Թkb"jW4CNW7T.gT 2aju+2z?QuC5"#gIXD` m ~oWx=QN`7Z 9JR1]PpPx+! tL>!;Īi˔8'JX,m%0f](YYH s˪edU#|!b`xN- 49k cW?@?::^X)Gq!nsICؿu0a] HjG ECc^.2rKas`rx3}ށdui(_qAԺxymiV'` Ƕw0G% 4Iy^Ag^!DzA:Q@Ț"&{|7_Ÿ+\_]=oCL:0cT\s4pD [|^gTJIф6 H]ZSD KMb,z? _3je%u fmCp{)d-SDg)XOxj78f|̶W1Vp*4A 8>=&,L&Cmq=1+FHVaflQ>X ocځL Zܩ!Ѯv4tU+y]*?HF,li VVŧ]etўOϐbt̕6ħ~~zYV1q!n#θ3p@ᤛO L"]I4tJm M1`6'VZAFvB|AYRāPD䬛j/J9-beR0 p[IfpaB kIJc}v] Q5b79u??7/sX 0% t D8t KOno`ޚVBf6{а5b晔!9}7ޤb%E!>w§^hUTv1 N(5߾Eb)c*z V24„Ȗ"K]lDNioH[ [:%sZU+ڽf̎ O.=XH8^j^qG:"|Ѩ)jnzHrq}fbbȞF!r訠nXAr4eF&MxmgJ+TEiQ]hLn\EwY5hVW/ 5|.9⎠v|IF]gukϳJKd.] fzG D$2*>&,_웯gP,V8UGE[/0+^۶;{"MIJ"5`h֐>"΢4_tM!䀽]U CDQbGq˔9xfŐE7PPY,K HD."K]b*5 $0.2Ѳ);P*Hxmw? v7t$hʤ]cg2 ^dL jMJ7<~` { cZfqNEVHmHh1\Ƿxw3OjW 1GtŎR@*c79#/ n~<%dp8r|8YGQ+z \ļ|dXԃ#0n2 ke_[T߶:=uvv)y/QLGQb*6kO*]Ŝ|+1 8$Ζ7<d@%> ta?7xgOBNۢe*Knmh6\E.R4Me2Y@JUpR?"8,TK7rhFv+ahH|w+yx;߆RNE492"ukEEFk +!(w18e״dd*PP{y:E h<$&ׇb,D{⍻biZg_m,`XM-Ta k:ֹny|zFm}~#? +uUB9d Mh&ՐSB5N"3#ĠUڶbgDWL`Ȕɭl6&@Eg '!euH51C: VRj5Vg!Z0| JH*̺"n@4)`mcbW[:e"ZX6SB87x{LPN؋޾O$κub'&3zVR)Y{h"f=`7 F*j{"2&D<pxt %\qm"3e$%AtmlЯ>b v,36 %{W1GR{k͗XnLX0#TT30 sm99"h 9:C{!#d\rp+_-n@ ``-SJAJHdt9fiEBqn4A6"Q?ۺ!D ;n19`l.I q"UM`Ǎm.wQq.wzlx%&FHZrR4`%<&x)I!&1]]XMJw(S0ͮ'u&;ߺK2lI>Y}Cތ\?UI4%LI7Ҍ0LGς1׊QtӁhEEc57t.πK}+Y#ո&Cv~{;Eȶc68,>lBј6q˭8E̲N\f7N1r#}=+5l3m$m{7kݵ`de()L Gu 3z'&5DŽi*2GXPU_RQi"3K:HwFV bDbGved>UN_oiEa,"&F'H@ *3V~n# ucĪ+/dߛ13Q0>$9VT1l40RxW5]`hoRA9H"vHZ Vw]ﭣՊmYkpht[h3mkrpa, VCTMd!1ځ*3eCJ%wLEM)oϓzU]xO@#}vֶ0)@yb`"; ב6RittБKD?oiUhX#)DD08A(Ux̒/o}}T{._7XM2K+7 !NN۸ tOyM)mmkFʌжV%IVnSDo~ e@iuQpy?UV.Y6` H@V<Mf&1$'xhڛ)>ga#4!g|pCL.Q6 59=et%&N!FVzG-Kׯ0_`:\#yZ:x f؇2y:GX?܂94tsHAb]SѴQ"nKa]O:b:&꺂zö|q}<+†/!ӂ|VXR䞘MrmuAȪeycon9c0!" >c/}!ɺu\^ 8/A/\;*vNcNis,/Ô~JD:޿68;- .6-L8^GV@VH偠K&9x4f)G5B.)耗SLݜg7I\1Do+[a_!k#F5mk1"@ԍp&Gx|֧kxE1"%U? {KGd34$ֺmTX uֺ>uk&!ESHHr|||r9!!&'T|!E7J6Zt_K.!}E\g9aAlٷcvGC',ˊ5mّ)Ou9 z-K p.gp^tbu̒r1RCC@.*@Om{{?Sa~L ۦ>,NΌ;c%hd% AfX_ʤTWEbvEdKO+~oP3}7oíث7<ʋU}vR*B39cQ $N+ϐw *̨D蔤HukضyIEA6.HTqPHD_n0DVJvT8eZi 1eB |BY)]vu,2#XxMeLÌ<*K\zu}Ulu%\T.rBIY}%uQR[]? 7INLebF9><p<_Q{xඩXy]v,kyP>->>LQSjǶ -%lY4Kó5ZWߠl̠$r<aۂu9#ČnJm[coM@!d"/ag'iG::kn '(8ˑA#wcؽ)Epg6CBE0 $W:MYOwWBJiUڵI:_ =DKwz[=ںF>|rҬZA.tOSVpd˩a1ɟD%!ub1 9z2<&| %VG:>~ǷIt~vI]HHrvn.iF* eF'벳b*Am̉jYZ'*޽"%:;cAcw;N^tYqm{ºlX/+=º@L%0' Ls2%1ME^ )E'tQ~AV= w@Gd+r٘Cw^oT ?" ozW V:V}kĨeIԕ9+LTmeD&Ab1 Ir m#D;&֞0W/hatz%,ϯ,+`.mc#6RPizwfdaw&?=˂)$l嫾x)в:%W'}|̄qk3QށQ([`ۢm˲r^p:ξw4**W)L-},);(Y` !"e99 1ѷKEjF A)v3j[lt(҇1ypu9bݚ kIDATlE``?3Hߣme<<EB[#GBָ ܱu#"$IDf ǂg'9"OY))$An.`JPAC%!FּE`l0ۈrjUtqLp*Y%u]PmޖzuZ?{ȱi@_]e`۵6=M Sq!(ߴ%ӗ4%=(0 jh!}nQNyYdV@ HK^!PPPRPc[2~يdFPkR6zYrBz۰(4 KiޅQu:Zh{G5ֱmgyz..rvP^]bQLѤ V(hF2b"&Dҙ uB˅qTFh5`Y*?mxRH#*`rRC= )O l*vmMu&qݴjGw]f H/iCBʅ^p2Z]AkC[+Zü&ս*uC*"DT$.,Ɂ0eɧe6i':!pX M]22]tA"M0o7&gR%Mc</ww|=Eb劉7/uu=Jyeٱ)f3QjXqҋMıҶzvΐvA+BTl2% [g1LO/tF=cL R)JNJ~"ڻUnvԲ4/fw;:W= fԪR9{~4NӹD `:pesG벵V,x) LS%9|n$` IF߲giJ*kG+c!Y1ta>^U]ವS@, )&LSO@4ϘO ,|Hȥ G5,݆M,Ma,[` bf=GB"Z[eI#qF'J(3жUj?/o26#k^݃lUN[Ufwp]Oe퀤϶T ~3>&(`:V|8'+f9ΠF)o?G+ ~7%%kAê誨k:<}~(O A5gj2c:.LKf0PsU@|:ӧ3RؖM7XnlEH5e>`:Pr8m|&Du4bl*1Z|j|TधĻQ%1ɡI!hylIgQum>HҊy)F¶x| K弢m\\:j-u< ZspmhһtŘWl3 arYRC"qw?+v)@ k$9$ޠsdJރrJD),3T03rY eȹa!NWRzh58b$^著!$ 0E23mBP (戮qZ6҄%fpC;RgQdTu1W*mB8X^ox{T2dĔm[seBm.O5EQI<"ѨݒZi؈.BTv޴5t;Y\ADQA$fgf@\LJtu ÚLL3㵈'>pVˊFa+HCd'.YfqTԻh2Rc4\ƺd@} SMfȂ$QjAk {kj16ViTvXMӮ#yS];٥#[iU9 `Wnڡe|. 8 3YC{ Ra<!K i| ;%X!.P{Iğt@A0MB-YLuP1&evȶ|?ӗ`YV,gmm#,B& @#ZĮtV2C4IT~i}M ~ZΦT&mk!ӈ?!uWbfcvWM,́BB`ܩQ4˻Gӄt쐆%wnE.M/bCWZƗHÕ>#ckHchi9AEpă<$3J#kMm 6@@.[h6%؝THf4]¥ Lmj,㠺Hܥ5TEi h+ S@]')+2YJ9 p~ơbH@D&2hK:#ȓ""WRdʼeB  ^zDbY`!'hwaB +U#b^2Z&syM4@7{[0g=)o 9J|Q4SI0h Yeu@ߦ)3GP2:2GW.3˔rP[l*͘0  GI|q<Ƿߡ}0n?ǨJ3մyd3# 7F8OşL=Gˉm< Nc8(p ^ s^6vѮxtF45sqxFvN J oO"q˿mvG3?HTizYN̘V?ےQ%V%=ҰaK7%Q0Wk6XL?, +éFqϥsUMB!d@;ufJʼK%JU%{%|58|#F0Pd 9r+/iT792DnJ7Lb(mDVeXL<|Q?e#7ɝOGzuHZjTȶ0guYs"|M>u0~<ֶQcdj F9^UKۗ6Z :○v"̉ث ά`^P$r,CUm@GwJ 5WOTi`&0m=F;[kjjE% 6bV"fDRNdBv\ʼ@[ADʍؔ ReN]yN1nfhFe4$"kF6Y"ml6A8@E05p-0% bp W|dYGHW#sbm?e3*+7S*;2Ϸrd]# O<Zq*oc(R<+D\A~EqAuQ/: 3=@fbwEVRʈt-.BQZ#ۮDF[\ľy?햇% N)MqPL/-@!r_ܫ\W\h-d[_tt1|bFUZ;ۅ?W PK^7]Xb"$Q⪸Tf*_ZHa.ըc)$Np{q8bItRal sbiA\ӡ:928rA0'ͩU%W)K>cinYH54)7P0zWhZR-?:CFky")h ]HMNw.<MC+."ͣ- nYK,4.3@Ze&:fW2 I U/zfEZ'Ex7vrJkwx#c Tl"!,aT-Pmu?e_WS`EUȂ[ɸ{ŗsNaӠSp~'xށ7qFS;]6ew')}zKz2Θr\0M[usl&7sBnnҪ\8¦gr^YַsT/#зC!j)Ÿ8A OiUf $ ,dsw91 UݭD9h|' ^rrq^i._bݼ Ӌk"J%!z*Zőv]v1ggz.\e_FC mc9fٶœ5-däU)?DUiӯm(1@Ԗ"gJ3N #𗍄uoG@zyE9|[2`Wlg0X 4O^0>0`+/_|#YamSxx'ws x=KDSfpw,}PtSګu30Ū(ma.'Ckaٚq(DRpTw[-X>(y e!YB,yL~JGhoF#ܻ$$IE+7Җ6pje% R'eqfUfy~qsIZTf#tmQq\%H Mebm[bg[WȑE+yٍ xޤH- Qiy\^c>[F #D$9HQoɗ?!IENDB`php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/queryregex/000077500000000000000000000000001376002032000254525ustar00rootroot00000000000000php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/queryregex/no-thumb.html000066400000000000000000000000251376002032000300660ustar00rootroot00000000000000

No thumb here.

php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/queryregex/one-thumb.html000066400000000000000000000001051376002032000302320ustar00rootroot00000000000000

One thumb here.

textphp-arthurhoaro-web-thumbnailer-2.0.3+dfsg/tests/public/queryregex/two-thumb.html000066400000000000000000000001651376002032000302700ustar00rootroot00000000000000

One thumb here.

text textphp-arthurhoaro-web-thumbnailer-2.0.3+dfsg/version.php000066400000000000000000000000221376002032000230220ustar00rootroot00000000000000