├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── shortpixel ├── composer.json ├── examples └── integration.php ├── lib ├── ShortPixel.php ├── ShortPixel │ ├── Client.php │ ├── Commander.php │ ├── Exception.php │ ├── Lock.php │ ├── Persister.php │ ├── Result.php │ ├── SPCache.php │ ├── SPLog.php │ ├── SPTools.php │ ├── Settings.php │ ├── Source.php │ ├── notify │ │ ├── ProgressNotifier.php │ │ ├── ProgressNotifierFileQ.php │ │ └── ProgressNotifierMemcache.php │ └── persist │ │ ├── ExifPersister.php │ │ ├── PNGMetadataExtractor.php │ │ ├── PNGReader.php │ │ ├── TextMetaFile.php │ │ └── TextPersister.php ├── cmdShortpixelOptimize.php ├── cmdShortpixelOptimizeFile.php ├── data │ └── shortpixel.crt ├── no-composer.php └── shortpixel-php-req.php ├── phpunit.xml ├── test.php └── test └── cmdMoveNFiles.sh /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.lock 4 | lib/splog.txt 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - 7.1 9 | - nightly 10 | dist: precise 11 | matrix: 12 | allow_failures: 13 | - php: nightly 14 | before_script: composer install 15 | script: vendor/bin/phpunit 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016 Shortpixel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Build Status](https://travis-ci.org/short-pixel-optimizer/shortpixel-php) 2 | 3 | # ShortPixel SDK and API client for PHP 4 | 5 | PHP client for the ShortPixel API, used for [ShortPixel](https://shortpixel.com) ShortPixel optimizes your images and improves website performance by reducing images size. Read more at [http://shortpixel.com](http://shortpixel.com). 6 | 7 | ## Documentation 8 | 9 | [Go to the documentation for the PHP client](https://shortpixel.com/api-tools). 10 | 11 | ## Installation 12 | 13 | Install the API client with Composer. Add this to your `composer.json`: 14 | 15 | ```json 16 | { 17 | "require": { 18 | "shortpixel/shortpixel-php": "*" 19 | } 20 | } 21 | ``` 22 | 23 | Then install with: 24 | 25 | ``` 26 | composer install 27 | ``` 28 | 29 | Use autoloading to make the client available in PHP: 30 | 31 | ```php 32 | require_once("vendor/autoload.php"); 33 | ``` 34 | 35 | Alternatively, if you don't use Composer, add the following require to your PHP code: 36 | 37 | ```php 38 | require_once("lib/shortpixel-php-req.php");_ 39 | ``` 40 | 41 | Get your API Key from https://shortpixel.com/free-sign-up 42 | 43 | ## Usage 44 | 45 | ```php 46 | // Set up the API Key. 47 | ShortPixel\setKey("YOUR_API_KEY"); 48 | 49 | // Compress with default settings 50 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->toFiles("/path/to/save/to"); 51 | // Compress with default settings but specifying a different file name 52 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->toFiles("/path/to/save/to", "optimized.png"); 53 | 54 | // Compress with default settings from a local file 55 | ShortPixel\fromFile("/path/to/your/local/unoptimized.png")->toFiles("/path/to/save/to"); 56 | // Compress with default settings from several local files 57 | ShortPixel\fromFiles(array("/path/to/your/local/unoptimized1.png", "/path/to/your/local/unoptimized2.png"))->toFiles("/path/to/save/to"); 58 | //Compres and rename each file 59 | \ShortPixel\fromFiles(array("/path/to/your/local/unoptimized1.png", "/path/to/your/local/unoptimized2.png"))->toFiles("/path/to/save/to", ['renamed-one.png', 'renamed-two.png']); 60 | 61 | // Compress with a specific compression level: 0 - lossless, 1 - lossy (default), 2 - glossy 62 | ShortPixel\fromFile("/path/to/your/local/unoptimized.png")->optimize(2)->toFiles("/path/to/save/to"); 63 | 64 | // Compress and resize - image is resized to have the either width equal to specified or height equal to specified 65 | // but not LESS (with settings below, a 300x200 image will be resized to 150x100) 66 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->resize(100, 100)->toFiles("/path/to/save/to"); 67 | // Compress and resize - have the either width equal to specified or height equal to specified 68 | // but not MORE (with settings below, a 300x200 image will be resized to 100x66) 69 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->resize(100, 100, true)->toFiles("/path/to/save/to"); 70 | 71 | // Keep the exif when compressing 72 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->keepExif()->toFiles("/path/to/save/to"); 73 | 74 | // Also generate and save a WebP version of the file - the WebP file will be saved next to the optimized file, with same basename and .webp extension 75 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->generateWebP()->toFiles("/path/to/save/to"); 76 | 77 | //Compress from a folder - the status of the compressed images is saved in a text file named .shortpixel in each image folder 78 | \ShortPixel\ShortPixel::setOptions(array("persist_type" => "text")); 79 | //Each call will optimize up to 10 images from the specified folder and mark in the .shortpixel file. 80 | //It automatically recurses a subfolder when finds it 81 | //Save to the same folder, set wait time to 300 to allow enough time for the images to be processed 82 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder")->wait(300)->toFiles("/path/to/your/local/folder"); 83 | //Save to a different folder. CURRENT LIMITATION: When using the text persist type and saving to a different folder, you also need to specify the destination folder as the fourth parameter to fromFolder ( it indicates where the persistence files should be created) 84 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder", 0, array(), "/different/path/to/save/to")->wait(300)->toFiles("/different/path/to/save/to"); 85 | //use a URL to map the folder to a WEB path in order for our servers to download themselves the images instead of receiving them via POST - faster and less exposed to connection timeouts 86 | $ret = ShortPixel\fromWebFolder("/path/to/your/local/folder", "http://web.path/to/your/local/folder")->wait(300)->toFiles("/path/to/save/to"); 87 | //let ShortPixel back-up all your files, before overwriting them (third parameter of toFiles). 88 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder")->wait(300)->toFiles("/path/to/save/to", null, "/back-up/path"); 89 | //Recurse only $N levels down into the subfolders of the folder ( N == 0 means do not recurse ) 90 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder", 0, array(), false, ShortPixel::CLIENT_MAX_BODY_SIZE, $N)->wait(300)->toFiles("/path/to/save/to"); 91 | 92 | //Set custom cURL options (proxy) 93 | \ShortPixel\setCurlOptions(array(CURLOPT_PROXY => '66.96.200.39:80', CURLOPT_REFERER => 'https://shortpixel.com/')); 94 | 95 | 96 | //A simple loop to optimize all images from a folder 97 | \ShortPixel\ShortPixel::setOptions(array("persist_type" => "text")); 98 | $stop = false; 99 | while(!$stop) { 100 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder")->wait(300)->toFiles("/path/to/save/to"); 101 | if(count($ret->succeeded) + count($ret->failed) + count($ret->same) + count($ret->pending) == 0) { 102 | $stop = true; 103 | } 104 | } 105 | 106 | //Compress from an image in memory 107 | $myImage = file_get_contents($pathTo_shortpixel.png); 108 | $ret = \ShortPixel\fromBuffer('shortpixel.png', $myImage)->wait(300)->toFiles(self::$tempDir); 109 | 110 | //Compress to an image in memory 111 | $ret = \ShortPixel\fromFiles(array("/path/to/your/local/unoptimized1.png", "/path/to/your/local/unoptimized2.png"))->wait(300)->toBuffers(); 112 | file_put_contents($pathTo_optimized1.png, $ret->succeeded[0]->Buffer); //the optimized image in memory 113 | 114 | //Get account status and credits info: 115 | $ret = \ShortPixel\ShortPixel::getClient()->apiStatus(YOUR_API_KEY); 116 | 117 | ``` 118 | There are more code examples in the [examples/integration.php](https://github.com/short-pixel-optimizer/shortpixel-php/blob/master/examples/integration.php ) file. 119 | 120 | Alternatively, you might want to add the call to a cron job using the cmdShortPixelOptimizer.php script found in lib/. More details about its usage here: [ShortPixel CLI](https://shortpixel.com/cli-docs) 121 | 122 | ## Running tests 123 | 124 | ``` 125 | composer install 126 | vendor/bin/phpunit 127 | ``` 128 | 129 | ### Integration tests 130 | 131 | Currently the integration tests were taken out in a separate project, at github's request (contained more than 200Mb test images). If you want to run the integration tests by yourself, please [contact us](https://shortpixel.com/contact) and will provide the integration tests. 132 | 133 | ``` 134 | composer install 135 | SHORTPIXEL_KEY=$YOUR_API_KEY vendor/bin/phpunit --no-configuration test/integration.php 136 | ``` 137 | 138 | ## License 139 | 140 | This software is licensed under the MIT License. [View the license](LICENSE). 141 | -------------------------------------------------------------------------------- /bin/shortpixel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ $@ == *--file* ]]; then 3 | php ../lib/cmdShortpixelOptimizeFile.php $@ 4 | else 5 | php ../lib/cmdShortpixelOptimize.php $@ 6 | fi 7 | 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shortpixel/shortpixel-php", 3 | "description": "ShortPixel PHP SDK. Read more at https://shortpixel.com/api-tools", 4 | "keywords": [ 5 | "shortpixel", 6 | "optimize", 7 | "compress", 8 | "images", 9 | "api" 10 | ], 11 | 12 | "bin": ["bin/shortpixel"], 13 | 14 | "homepage": "https://shortpixel.com/api", 15 | "license": "MIT", 16 | 17 | "support": { 18 | "email": "support@shortpixel.com" 19 | }, 20 | 21 | "authors": [{ 22 | "name": "Simon Duduica", 23 | "email": "simon@shortpixel.com" 24 | }], 25 | 26 | "require": { 27 | "php": ">=5.3.0", 28 | "ext-curl": "*", 29 | "ext-json": "*", 30 | "lib-curl": ">=7.20.0" 31 | }, 32 | 33 | "require-dev": { 34 | "symfony/yaml": "~2.0", 35 | "phpunit/phpunit": "~4.0" 36 | }, 37 | 38 | "autoload": { 39 | "files": ["lib/ShortPixel.php", "lib/ShortPixel/Exception.php"], 40 | "psr-4": {"ShortPixel\\": "lib/ShortPixel/"} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/ShortPixel.php: -------------------------------------------------------------------------------- 1 | 1, // 1 - lossy, 2 - glossy, 0 - lossless 27 | "keep_exif" => 0, // 1 - EXIF is preserved, 0 - EXIF is removed 28 | "resize" => 0, // 0 - don't resize, 1 - outer resize, 3 - inner resize 29 | "resize_width" => null, // in pixels. null means no resize 30 | "resize_height" => null, // in pixels. null means no resize 31 | "cmyk2rgb" => 1, // convert CMYK to RGB: 1 yes, 0 no 32 | "convertto" => "", // if '+webp' then also the WebP version will be generated, if +avif then also the AVIF version will be generated. Specify both with +webp|+avif 33 | "user" => "", //set the user needed for HTTP AUTH of the base_url 34 | "pass" => "", //se the pass needed for HTTP AUTH of the base_url 35 | // **** return options **** 36 | "notify_me" => null, // should contain full URL of of notification script (notify.php) - to be implemented 37 | "wait" => 30, // seconds 38 | // **** local options **** 39 | "total_wait" => 30, //seconds 40 | "base_url" => null, // base url of the images - used to generate the path for toFile by extracting from original URL and using the remaining path as relative path to base_path 41 | "url_filter" => false, //the URL filter will be applied on the full inside base_url. Current available URL filters: encode (for base64_encode) 42 | "base_source_path" => "", // base path of the local files 43 | "base_path" => false, // base path to save the files 44 | "backup_path" => false, // backup path, relative to the optimization folder (base_source_path) 45 | // **** persist options **** 46 | "persist_type" => null, // null - don't persist, otherwise "text" (.shortpixel text file in each folder), "exif" (mark in the EXIF that the image has been optimized) or "mysql" (to be implemented) 47 | "persist_name" => ".shortpixel", 48 | "notify_progress" => false, 49 | "cache_time" => 0 // number of seconds to cache the folder results - the *Persister classes will cache the getTodo results and retrieve them from memcache if it's available. 50 | //"persist_user" => "user", // only for mysql 51 | //"persist_pass" => "pass" // only for mysql 52 | // "" => null, 53 | ); 54 | private static $curlOptions = array(); 55 | 56 | public static $PROCESSABLE_EXTENSIONS = array('jpg', 'jpeg', 'jpe', 'jfif', 'jif', 'gif', 'png', 'pdf'); 57 | 58 | private static $persistersRegistry = array(); 59 | 60 | /** 61 | * @param $key - the ShortPixel API Key 62 | */ 63 | public static function setKey($key) { 64 | self::$key = $key; 65 | self::$client = NULL; 66 | } 67 | 68 | /** 69 | * @param $apiDomain - the ShortPixel API Domain 70 | */ 71 | public static function setApiDomain($apiDomain) { 72 | self::$apiDomain = $apiDomain; 73 | } 74 | 75 | /** 76 | * @param $options - set the ShortPxiel options. Options defaults are the following: 77 | * "lossy" => 1, // 1 - lossy, 0 - lossless 78 | "keep_exif" => 0, // 1 - EXIF is preserved, 0 - EXIF is removed 79 | "resize_width" => null, // in pixels. null means no resize 80 | "resize_height" => null, 81 | "cmyk2rgb" => 1, 82 | "convertto" => "", // if '+webp' then also the WebP version will be generated, if +avif then also the AVIF version will be generated. Specify both with +webp|+avif 83 | "notify_me" => null, // should contain full URL of of notification script (notify.php)- TO BE IMPLEMENTED 84 | "wait" => 30, 85 | //local options 86 | "total_wait" => 30, 87 | "base_url" => null, // base url of the images - used to generate the path for toFile by extracting from original URL and using the remaining path as relative path to base_path 88 | "base_path" => "/tmp", // base path for the saved files 89 | */ 90 | public static function setOptions($options) { 91 | self::$options = array_merge(self::$options, $options); 92 | } 93 | 94 | /** 95 | * add custom cURL options. These provided options will take precedence to the default options that are passed to all cURL calls but not to others that are specific to each call (CURLOPT_TIMEOUT, CURLOPT_CUSTOMREQUEST) 96 | * or to library's user agent. 97 | * @param array $curlOptions Key-value pairs 98 | */ 99 | public static function setCurlOptions($curlOptions) { 100 | self::$curlOptions = $curlOptions + self::$curlOptions; 101 | } 102 | 103 | /** 104 | * @return the API Key in use 105 | */ 106 | public static function getKey() { 107 | return self::$key; 108 | } 109 | 110 | /** 111 | * @param $name - option name 112 | * @return the option value or false if not found 113 | */ 114 | public static function opt($name) { 115 | return isset(self::$options[$name]) ? self::$options[$name] : false; 116 | } 117 | 118 | /** 119 | * @return the current options array 120 | */ 121 | public static function options() { 122 | return self::$options; 123 | } 124 | 125 | /** 126 | * @return Client singleton 127 | * @throws AccountException 128 | */ 129 | public static function getClient() { 130 | if (!self::$key) { 131 | throw new AccountException("Provide an API key with ShortPixel\setKey(...)", -6); 132 | } 133 | 134 | if (!self::$client) { 135 | self::$client = new Client(self::$curlOptions); 136 | } 137 | 138 | return self::$client; 139 | } 140 | 141 | public static function getPersister($context = null) { 142 | if(!self::$options["persist_type"]) { 143 | return null; 144 | } 145 | if($context && isset(self::$persistersRegistry[self::$options["persist_type"] . $context])) { 146 | return self::$persistersRegistry[self::$options["persist_type"] . $context]; 147 | } 148 | 149 | $persistType = self::$options["persist_type"]; 150 | if(!is_string($persistType)) { 151 | throw new PersistException("Invalid persist type: " . var_export($persistType, true)); 152 | } 153 | switch($persistType) { 154 | case "exif": 155 | $persister = new persist\ExifPersister(self::$options); 156 | break; 157 | case "mysql": 158 | return null; 159 | case "text": 160 | $persister = new persist\TextPersister(self::$options); 161 | break; 162 | default: 163 | throw new PersistException("Unknown persist type: " . self::$options["persist_type"]); 164 | } 165 | 166 | if($context) { 167 | self::$persistersRegistry[self::$options["persist_type"] . $context] = $persister; 168 | } 169 | return $persister; 170 | } 171 | 172 | static public function isProcessable($path) { 173 | return in_array(strtolower(pathinfo($path, PATHINFO_EXTENSION)), \ShortPixel\ShortPixel::$PROCESSABLE_EXTENSIONS); 174 | } 175 | 176 | static public function log($msg) { 177 | if(ShortPixel::DEBUG_LOG) { 178 | @file_put_contents(__DIR__ . '/splog.txt', date("Y-m-d H:i:s") . " - " . $msg . " \n\n", FILE_APPEND); 179 | } 180 | } 181 | } 182 | 183 | ShortPixel::setOptions(array('base_path' => sys_get_temp_dir())); 184 | 185 | 186 | /** 187 | * stub for ShortPixel::setKey() 188 | * @param $key - the ShortPixel API Key 189 | */ 190 | function setKey($key) { 191 | return ShortPixel::setKey($key); 192 | } 193 | 194 | /** 195 | * stub for ShortPixel::setKey() 196 | * @param $apiDomain - the ShortPixel API Domain 197 | */ 198 | function setApiDomain($apiDomain) { 199 | return ShortPixel::setApiDomain($apiDomain); 200 | } 201 | 202 | /** 203 | * stub for ShortPixel::setOptions() 204 | * @return the current options array 205 | */ 206 | function setOptions($options) { 207 | return ShortPixel::setOptions($options); 208 | } 209 | 210 | /** 211 | * stub for ShortPixel::setOptions() 212 | * @return the current options array 213 | */ 214 | function setCurlOptions($options) { 215 | return ShortPixel::setCurlOptions($options); 216 | } 217 | 218 | /** 219 | * stub for ShortPixel::opt() 220 | * @param $name - name of the option 221 | * @return the option 222 | */ 223 | function opt($name) { 224 | return ShortPixel::opt($name); 225 | } 226 | 227 | /** 228 | * Stub for Source::fromFiles 229 | * @param $path - the file path on the local drive 230 | * @return Commander - the class that handles the optimization commands 231 | * @throws ClientException 232 | */ 233 | function fromFiles($path) { 234 | $source = new Source(); 235 | return $source->fromFiles($path); 236 | } 237 | 238 | function fromFile($path) { 239 | return fromFiles($path); 240 | } 241 | 242 | /** 243 | * Stub for Source::folderInfo 244 | * @param $path - the file path on the local drive 245 | * @param bool $recurse - boolean - go into subfolders or not 246 | * @param bool $fileList - return the list of files with optimization status (only current folder, not subfolders) 247 | * @param array $exclude - array of folder names that you want to exclude from the optimization 248 | * @param bool $persistPath - the path where to look for the metadata, if different from the $path 249 | * @param int $recurseDepth - how many subfolders deep to go. Defaults to PHP_INT_MAX 250 | * @param bool $retrySkipped - if true, all skipped files will be reset to pending with retries = 0 251 | * @return object|void (object)array('status', 'total', 'succeeded', 'pending', 'same', 'failed') 252 | * @throws PersistException 253 | */ 254 | function folderInfo($path, $recurse = true, $fileList = false, $exclude = array(), $persistPath = false, $recurseDepth = PHP_INT_MAX, $retrySkipped = false) { 255 | $source = new Source(); 256 | return $source->folderInfo($path, $recurse, $fileList, $exclude, $persistPath, $recurseDepth, $retrySkipped); 257 | } 258 | 259 | /** 260 | * Stub for Source::fromFolder 261 | * @param $path - the file path on the local drive 262 | * @param $maxFiles - maximum number of files to select from the folder 263 | * @param $exclude - exclude files based on regex patterns 264 | * @param $persistPath - the path where to store the metadata, if different from the $path (usually the target path) 265 | * @param $maxTotalFileSize - max summed up file size in MB 266 | * @return Commander - the class that handles the optimization commands 267 | * @throws ClientException 268 | */ 269 | function fromFolder($path, $maxFiles = ShortPixel::MAX_ALLOWED_FILES_PER_CALL, $exclude = array(), $persistPath = false, $maxTotalFileSize = ShortPixel::CLIENT_MAX_BODY_SIZE, $recurseDepth = PHP_INT_MAX) { 270 | $source = new Source(); 271 | return $source->fromFolder($path, $maxFiles, $exclude, $persistPath, $maxTotalFileSize, $recurseDepth); 272 | } 273 | 274 | /** 275 | * Stub for Source::fromWebFolder 276 | * @param $path - the file path on the local drive 277 | * @param $webPath - the corresponding web path for the file path 278 | * @param array $exclude - exclude files based on regex patterns 279 | * @param bool $persistFolder - the path where to store the metadata, if different from the $path (usually the target path) 280 | * @param int $recurseDepth - how many subfolders deep to go. Defaults to PHP_INT_MAX 281 | * @return Commander - the class that handles the optimization commands 282 | * @throws ClientException 283 | */ 284 | function fromWebFolder($path, $webPath, $exclude = array(), $persistFolder = false, $recurseDepth = PHP_INT_MAX) { 285 | $source = new Source(); 286 | return $source->fromWebFolder($path, $webPath, $exclude, $persistFolder, $recurseDepth); 287 | } 288 | 289 | /** 290 | * Stub for Source::fromBuffer. Creates a Commander object from a buffer. 291 | * @param $name - a unique name for the buffer that will be sent to the optimization cloud 292 | * @param $contents - the image contents of the buffer 293 | * @return Commander 294 | */ 295 | function fromBuffer($name, $contents) { 296 | $source = new Source(); 297 | return $source->fromBuffer($name, $contents); 298 | } 299 | 300 | /** 301 | * Stub for Source::fromUrls 302 | * @param $urls - the array of urls to be optimized 303 | * @return Commander - the class that handles the optimization commands 304 | * @throws ClientException 305 | */ 306 | function fromUrls($urls) { 307 | $source = new Source(); 308 | return $source->fromUrls($urls); 309 | } 310 | 311 | /** 312 | */ 313 | function isOptimized($path) { 314 | $persist = ShortPixel::getPersister($path); 315 | if($persist) { 316 | return $persist->isOptimized($path); 317 | } else { 318 | throw new Exception("No persister available"); 319 | } 320 | } 321 | 322 | function validate() { 323 | try { 324 | ShortPixel::getClient()->request("post"); 325 | } catch (ClientException $e) { 326 | return true; 327 | } 328 | } 329 | 330 | function recurseCopy($source, $dest) { 331 | foreach ( 332 | $iterator = new \RecursiveIteratorIterator( 333 | new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), 334 | \RecursiveIteratorIterator::SELF_FIRST) as $item 335 | ) { 336 | $target = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); 337 | if ($item->isDir()) { 338 | if(!@mkdir($target)) { 339 | throw new PersistException("Could not create directory $target. Please check rights."); 340 | } 341 | } else { 342 | if(!@copy($item, $target)) { 343 | throw new PersistException("Could not copy file $item to $target. Please check rights."); 344 | } 345 | } 346 | } 347 | } 348 | 349 | function delTree($dir, $keepBase = true) { 350 | $files = array_diff(scandir($dir), array('.','..')); 351 | foreach ($files as $file) { 352 | (is_dir("$dir/$file")) ? delTree("$dir/$file", false) : unlink("$dir/$file"); 353 | } 354 | return $keepBase ? true : rmdir($dir); 355 | } 356 | 357 | /** 358 | * a basename alternative that deals OK with multibyte charsets (e.g. Arabic) 359 | * @param string $Path 360 | * @return string 361 | */ 362 | function MB_basename($Path, $suffix = false){ 363 | $Separator = " qq "; 364 | $qqPath = preg_replace("/[^ ]/u", $Separator."\$0".$Separator, $Path); 365 | if(!$qqPath) { //this is not an UTF8 string!! 366 | $pathElements = explode('/', $Path); 367 | $fileName = end($pathElements); 368 | $pos = gettype($suffix) == 'string' ? strpos($fileName, $suffix) : false; 369 | if($pos !== false) { 370 | return substr($fileName, 0, $pos); 371 | } 372 | return $fileName; 373 | } 374 | $suffix = preg_replace("/[^ ]/u", $Separator."\$0".$Separator, $suffix); 375 | $Base = basename($qqPath, $suffix); 376 | $Base = str_replace($Separator, "", $Base); 377 | return $Base; 378 | } 379 | 380 | if(!function_exists('mb_detect_encoding')) { 381 | function mb_detect_encoding($string, $enc=null) { 382 | 383 | static $list = array('utf-8', 'iso-8859-1', 'windows-1251'); 384 | 385 | foreach ($list as $item) { 386 | $sample = @iconv($item, $item, $string); 387 | if (md5($sample) == md5($string)) { 388 | if ($enc == $item) { return true; } else { return $item; } 389 | } 390 | } 391 | return null; 392 | } 393 | } 394 | 395 | function spdbg($var, $msg) { 396 | echo("DEBUG $msg : "); var_dump($var); 397 | } 398 | 399 | function spdbgd($var, $msg) { 400 | die(spdbg($var, $msg)); 401 | } 402 | 403 | function normalizePath($path) { 404 | $abs = ($path[0] === '/') ? '/' : ''; 405 | $path = str_replace(array('/', '\\'), '/', $path); 406 | $parts = array_filter(explode('/', $path), 'strlen'); 407 | $absolutes = array(); 408 | foreach ($parts as $part) { 409 | if ('.' == $part) continue; 410 | if ('..' == $part) { 411 | array_pop($absolutes); 412 | } else { 413 | $absolutes[] = $part; 414 | } 415 | } 416 | return $abs . implode('/', $absolutes); 417 | } 418 | 419 | function getMemcache() { 420 | $mc = false; 421 | if(class_exists('\Memcached')) { 422 | $mc = new \Memcached(); 423 | $mc->addServer('127.0.0.1', '11211'); 424 | if(!@$mc->getStats()) { 425 | $mc = false; 426 | } 427 | } 428 | elseif(class_exists('\Memcache')) { 429 | $mc = new \Memcache(); 430 | $mc->addServer('127.0.0.1', '11211'); 431 | if(!@$mc->connect('127.0.0.1', '11211')) { 432 | $mc = false; 433 | } else { 434 | $mc->close(); 435 | } 436 | } 437 | return $mc; 438 | } 439 | 440 | if ( ! function_exists( 'json_last_error_msg' ) ) { 441 | /** 442 | * Retrieves the error string of the last json_encode() or json_decode() call. 443 | * 444 | * @since 4.4.0 445 | * 446 | * @internal This is a compatibility function for PHP <5.5 447 | * 448 | * @return bool|string Returns the error message on success, "No Error" if no error has occurred, 449 | * or false on failure. 450 | */ 451 | function json_last_error_msg() 452 | { 453 | // See https://core.trac.wordpress.org/ticket/27799. 454 | if (!function_exists('json_last_error')) { 455 | return false; 456 | } 457 | 458 | $last_error_code = json_last_error(); 459 | 460 | // Just in case JSON_ERROR_NONE is not defined. 461 | $error_code_none = defined('JSON_ERROR_NONE') ? JSON_ERROR_NONE : 0; 462 | 463 | switch (true) { 464 | case $last_error_code === $error_code_none: 465 | return 'No error'; 466 | 467 | case defined('JSON_ERROR_DEPTH') && JSON_ERROR_DEPTH === $last_error_code: 468 | return 'Maximum stack depth exceeded'; 469 | 470 | case defined('JSON_ERROR_STATE_MISMATCH') && JSON_ERROR_STATE_MISMATCH === $last_error_code: 471 | return 'State mismatch (invalid or malformed JSON)'; 472 | 473 | case defined('JSON_ERROR_CTRL_CHAR') && JSON_ERROR_CTRL_CHAR === $last_error_code: 474 | return 'Control character error, possibly incorrectly encoded'; 475 | 476 | case defined('JSON_ERROR_SYNTAX') && JSON_ERROR_SYNTAX === $last_error_code: 477 | return 'Syntax error'; 478 | 479 | case defined('JSON_ERROR_UTF8') && JSON_ERROR_UTF8 === $last_error_code: 480 | return 'Malformed UTF-8 characters, possibly incorrectly encoded'; 481 | 482 | case defined('JSON_ERROR_RECURSION') && JSON_ERROR_RECURSION === $last_error_code: 483 | return 'Recursion detected'; 484 | 485 | case defined('JSON_ERROR_INF_OR_NAN') && JSON_ERROR_INF_OR_NAN === $last_error_code: 486 | return 'Inf and NaN cannot be JSON encoded'; 487 | 488 | case defined('JSON_ERROR_UNSUPPORTED_TYPE') && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code: 489 | return 'Type is not supported'; 490 | 491 | default: 492 | return 'An unknown error occurred'; 493 | } 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /lib/ShortPixel/Client.php: -------------------------------------------------------------------------------- 1 | customOptions = $curlOptions; 49 | $this->logger = SPLog::Get(SPLog::PRODUCER_CLIENT); 50 | $this->options = $curlOptions + array( 51 | CURLOPT_RETURNTRANSFER => true, 52 | CURLOPT_BINARYTRANSFER => true, 53 | CURLOPT_HEADER => true, 54 | CURLOPT_TIMEOUT => 60, 55 | //CURLOPT_CAINFO => self::caBundle(), 56 | CURLOPT_SSL_VERIFYPEER => false, //TODO true 57 | CURLOPT_SSL_VERIFYHOST => false, //TODO remove 58 | CURLOPT_USERAGENT => self::userAgent(), 59 | ); 60 | } 61 | 62 | /** 63 | * Does the CURL request to the ShortPixel API 64 | * @param $method 'post' or 'get' 65 | * @param null $body - the POST fields 66 | * @param array $header - HTTP headers 67 | * @return array - metadata from the API 68 | * @throws ConnectionException 69 | */ 70 | function request($method, $body = NULL, $header = array()){ 71 | if ($body) { 72 | foreach($body as $key => $val) { 73 | if($val === null) { 74 | unset($body[$key]); 75 | } 76 | } 77 | } 78 | 79 | ShortPixel::log("REQUEST BODY: " . json_encode($body)); 80 | 81 | $retUrls = array("body" => array(), "headers" => array(), "fileMappings" => array()); 82 | $retPend = array("body" => array(), "headers" => array(), "fileMappings" => array()); 83 | $retFiles = array("body" => array(), "headers" => array(), "fileMappings" => array()); 84 | 85 | if(isset($body["urllist"])) { 86 | $retUrls = $this->requestInternal($method, $body, $header); 87 | } 88 | if(isset($body["pendingURLs"])) { 89 | unset($body["urllist"]); 90 | //some files might have already been processed as relaunches in the given max time 91 | foreach($retUrls["body"] as $url) { 92 | //first remove it from the files list as the file was uploaded properly 93 | if($url->Status->Code != -102 && $url->Status->Code != -106) { 94 | $notExpired[] = $url; 95 | if(!isset($body["pendingURLs"][$url->OriginalURL])) { 96 | $lala = "cucu"; 97 | } else 98 | $unsetPath = $body["pendingURLs"][$url->OriginalURL]; 99 | if(isset($body["files"]) && ($key = array_search($unsetPath, $body["files"])) !== false) { 100 | unset($body["files"][$key]); 101 | } 102 | } 103 | //now from the pendingURLs if we already have an answer with urllist 104 | if(isset($body["pendingURLs"][$url->OriginalURL])) { 105 | $retUrls["fileMappings"][$url->OriginalURL] = $body["pendingURLs"][$url->OriginalURL]; 106 | unset($body["pendingURLs"][$url->OriginalURL]); 107 | } 108 | } 109 | if(count($body["pendingURLs"])) { 110 | $retPend = $this->requestInternal($method, $body, $header); 111 | if(isset($retPend['body']->Status->Code) && $retPend['body']->Status->Code < 0) { //something's wrong (API key?) 112 | throw new ClientException($retPend['body']->Status->Message, $retPend['body']->Status->Code); 113 | 114 | } 115 | if(isset($body["files"])) { 116 | $notExpired = array(); 117 | foreach($retPend['body'] as $detail) { 118 | if($detail->Status->Code != -102) { // -102 is expired, means we need to resend the image through post 119 | $notExpired[] = $detail; 120 | $unsetPath = $body["pendingURLs"][$detail->OriginalURL]; 121 | if(($key = array_search($unsetPath, $body["files"])) !== false) { 122 | unset($body["files"][$key]); 123 | } 124 | } 125 | } 126 | $retPend['body'] = $notExpired; 127 | } 128 | } 129 | } 130 | if (isset($body["files"]) && count($body["files"]) || 131 | isset($body["buffers"]) && count($body["buffers"])) { 132 | unset($body["pendingURLs"]); 133 | $retFiles = $this->requestInternal($method, $body, $header); 134 | } 135 | 136 | if(!isset($retUrls["body"]->Status) && !isset($retPend["body"]->Status) && !isset($retFiles["body"]->Status) 137 | && (!is_array($retUrls["body"]) || !is_array($retPend["body"]) || !is_array($retFiles["body"]))) { 138 | throw new Exception("Request inconsistent status. Please contact support."); 139 | } 140 | 141 | $body = isset($retUrls["body"]->Status) 142 | ? $retUrls["body"] 143 | : (isset($retPend["body"]->Status) 144 | ? $retPend["body"] 145 | : (isset($retFiles["body"]->Status) 146 | ? $retFiles["body"] : 147 | array_merge($retUrls["body"], $retPend["body"], $retFiles["body"]))); 148 | 149 | $theReturn = (object) array("body" => $body, 150 | "headers" => array_unique(array_merge($retUrls["headers"], $retPend["headers"], $retFiles["headers"])), 151 | "fileMappings" => array_merge($retUrls["fileMappings"], $retPend["fileMappings"], $retFiles["fileMappings"])); 152 | ShortPixel::log("REQUEST RETURNS: " . json_encode($theReturn)); 153 | return $theReturn; 154 | } 155 | 156 | function requestInternal($method, $body = NULL, $header = array()){ 157 | $request = curl_init(); 158 | curl_setopt_array($request, $this->options); 159 | 160 | $files = $urls = false; 161 | 162 | if (isset($body["urllist"])) { //images are sent as a list of URLs 163 | $this->prepareJSONRequest(self::API_ENDPOINT(), $request, $body, $method, $header); 164 | } 165 | elseif(isset($body["pendingURLs"])) { 166 | //prepare the pending items request 167 | $urls = array(); 168 | $fileCount = 1; 169 | foreach($body["pendingURLs"] as $url => $path) { 170 | $urls["url" . $fileCount] = $url; 171 | $fileCount++; 172 | } 173 | $pendingURLs = $body["pendingURLs"]; 174 | unset($body["pendingURLs"]); 175 | $body["file_urls"] = $urls; 176 | $this->prepareJSONRequest(self::API_UPLOAD_ENDPOINT(), $request, $body, $method, $header); 177 | } 178 | elseif (isset($body["files"]) || isset($body["buffers"])) { 179 | $files = $this->prepareMultiPartRequest($request, $body, $header); 180 | } 181 | else { 182 | return array("body" => array(), "headers" => array(), "fileMappings" => array()); 183 | } 184 | 185 | //spdbgd(rawurldecode($body['urllist'][1]), "body"); 186 | 187 | list($details, $headers, $status, $response) = $this->sendRequest($request,6); 188 | 189 | if(getenv("SHORTPIXEL_DEBUG")) { 190 | $info = "DETAILS\n"; 191 | if(is_array($details)) { 192 | foreach($details as $det) { 193 | $info .= $det->Status->Code . " " . $det->OriginalURL . (isset($det->localPath) ? "({$det->localPath})" : "" ) . "\n"; 194 | } 195 | } else { 196 | $info = $response; 197 | } 198 | } 199 | 200 | $fileMappings = array(); 201 | if($files) { 202 | $fileMappings = array(); 203 | foreach($details as $detail) { 204 | if(isset($detail->Key) && isset($files[$detail->Key])){ 205 | $fileMappings[$detail->OriginalURL] = $files[$detail->Key]; 206 | } 207 | } 208 | } elseif($urls) { 209 | $fileMappings = $pendingURLs; 210 | } 211 | 212 | if(getenv("SHORTPIXEL_DEBUG")) { 213 | $info .= "FILE MAPPINGS\n"; 214 | foreach($fileMappings as $key => $val) { 215 | $info .= "$key -> $val\n"; 216 | } 217 | } 218 | 219 | if ($status >= 200 && $status <= 299) { 220 | return array("body" => $details, "headers" => $headers, "fileMappings" => $fileMappings); 221 | } 222 | 223 | throw Exception::create($details->message, $details->error, $status); 224 | } 225 | 226 | protected function sendRequest($request, $tries) { 227 | for($i = 0; $i < $tries; $i++) { //curl_setopt($request, CURLOPT_TIMEOUT, 120);curl_setopt($request, CURLOPT_VERBOSE, true); 228 | $response = curl_exec($request); 229 | $this->logger->log(SPLog::PRODUCER_CLIENT, "RAW RESPONSE: " . $response); 230 | if(!curl_errno($request)) { 231 | break; 232 | } else { 233 | ShortPixel::log("CURL ERROR: " . curl_error($request) . " (BODY: $response)"); 234 | } 235 | } 236 | 237 | if(curl_errno($request)) { 238 | throw new ConnectionException("Error while connecting: " . curl_error($request) . ""); 239 | } 240 | if (!is_string($response)) { 241 | $message = sprintf("%s (#%d)", curl_error($request), curl_errno($request)); 242 | curl_close($request); 243 | throw new ConnectionException("Error while connecting: " . $message); 244 | } 245 | 246 | $status = curl_getinfo($request, CURLINFO_HTTP_CODE); 247 | $headerSize = curl_getinfo($request, CURLINFO_HEADER_SIZE); 248 | curl_close($request); 249 | 250 | $headers = self::parseHeaders(substr($response, 0, $headerSize)); 251 | $body = substr($response, $headerSize); 252 | 253 | $details = json_decode($body); 254 | 255 | if (!$details) { 256 | $message = sprintf("Error while parsing response (Status: %s): %s (#%d)", $status, 257 | PHP_VERSION_ID >= 50500 ? json_last_error_msg() : "Error", 258 | json_last_error()); 259 | $details = (object) array( 260 | "raw" => $body, 261 | "error" => "ParseError", 262 | "message" => $message . "( " . $body . ")", 263 | "Status" => (object)array("Code" => -1, "Message" => "ParseError: " . $message) 264 | ); 265 | ShortPixel::log("JSON Error while parsing response: " . json_encode($details)); 266 | } 267 | return array($details, $headers, $status, $response); 268 | } 269 | 270 | protected function prepareJSONRequest($endpoint, $request, $body, $method, $header) { 271 | //to escape the + from "+webp" 272 | if(isset($body["convertto"]) && $body["convertto"]) { 273 | $converts = explode('|', $body["convertto"]); 274 | $body["convertto"] = implode('|', array_map('urlencode', $converts )); 275 | } 276 | // if(isset($body["urllist"])) { 277 | // aici folosim ceva de genul: parse_url si apoi pe partea de path: str_replace('%2F', '/', rawurlencode($this->filePath) 278 | // $body["urllist"] = array_map('rawurlencode', $body["urllist"]); 279 | // } 280 | if(isset($body["buffers"])) unset($body['buffers']); 281 | 282 | $body = json_encode($body); 283 | 284 | array_push($header, "Content-Type: application/json"); 285 | curl_setopt($request, CURLOPT_URL, $endpoint); 286 | curl_setopt($request, CURLOPT_CUSTOMREQUEST, strtoupper($method)); 287 | curl_setopt($request, CURLOPT_HTTPHEADER, $header); 288 | if ($body) { 289 | curl_setopt($request, CURLOPT_POSTFIELDS, $body); 290 | } 291 | } 292 | 293 | 294 | protected function prepareMultiPartRequest($request, $body, $header) { 295 | $files = array(); 296 | $fileCount = 1; 297 | //to escape the + from "+webp" 298 | if($body["convertto"]) { 299 | $body["convertto"] = urlencode($body["convertto"]); 300 | } 301 | if(isset($body["files"])) { 302 | foreach($body["files"] as $filePath) { 303 | $files["file" . $fileCount] = $filePath; 304 | $fileCount++; 305 | } 306 | } 307 | $buffers = array(); 308 | if(isset($body["buffers"])) { 309 | foreach($body["buffers"] as $name => $contents) { 310 | $files["file" . $fileCount] = $name; 311 | $buffers["file" . $fileCount] = $contents; 312 | $fileCount++; 313 | } 314 | unset($body["buffers"]); 315 | } 316 | $body["file_paths"] = json_encode($files); 317 | unset($body["files"]); 318 | curl_setopt($request, CURLOPT_URL, Client::API_UPLOAD_ENDPOINT()); 319 | $this->curl_custom_postfields($request, $body, $files, $header, $buffers); 320 | return $files; 321 | } 322 | 323 | function curl_custom_postfields($ch, array $assoc = array(), array $files = array(), $header = array(), $buffers = array()) { 324 | 325 | // invalid characters for "name" and "filename" 326 | static $disallow = array("\0", "\"", "\r", "\n"); 327 | 328 | // build normal parameters 329 | foreach ($assoc as $k => $v) { 330 | $k = str_replace($disallow, "_", $k); 331 | $body[] = implode("\r\n", array( 332 | "Content-Disposition: form-data; name=\"{$k}\"", 333 | "", 334 | filter_var($v), 335 | )); 336 | } 337 | 338 | // build file parameters 339 | $fileContents = array(); 340 | foreach ($files as $k => $v) { 341 | switch (true) { 342 | case true === $v = realpath(filter_var($v)): 343 | case is_file($v): 344 | case is_readable($v): 345 | $fileContents[$k] = file_get_contents($v); 346 | // continue; // or return false, throw new InvalidArgumentException 347 | } 348 | } 349 | $fileContents = array_merge($fileContents, $buffers); 350 | 351 | foreach ($fileContents as $k => $data) { 352 | $pp = explode(DIRECTORY_SEPARATOR, $files[$k]); 353 | $v = end($pp); 354 | $k = str_replace($disallow, "_", $k); 355 | $v = str_replace($disallow, "_", $v); 356 | $body[] = implode("\r\n", array( 357 | "Content-Disposition: form-data; name=\"{$k}\"; filename=\"{$v}\"", 358 | "Content-Type: application/octet-stream", 359 | "", 360 | $data, 361 | )); 362 | } 363 | 364 | // generate safe boundary 365 | do { 366 | $boundary = "---------------------" . md5(mt_rand() . microtime()); 367 | } while (preg_grep("/{$boundary}/", $body)); 368 | 369 | // add boundary for each parameters 370 | array_walk($body, function (&$part) use ($boundary) { 371 | $part = "--{$boundary}\r\n{$part}"; 372 | }); 373 | 374 | // add final boundary 375 | $body[] = "--{$boundary}--"; 376 | $body[] = ""; 377 | 378 | // set options 379 | return @curl_setopt_array($ch, array( 380 | CURLOPT_POST => true, 381 | CURLOPT_BINARYTRANSFER => true, 382 | CURLOPT_RETURNTRANSFER => true, 383 | CURLOPT_TIMEOUT => 300, //to be able to handle via post large files up to 48M which might take a long time to upload. 384 | CURLOPT_POSTFIELDS => implode("\r\n", $body), 385 | CURLOPT_HTTPHEADER => array_merge(array( 386 | "Expect: 100-continue", 387 | "Content-Type: multipart/form-data; boundary={$boundary}", // change Content-Type 388 | ), $header), 389 | )); 390 | } 391 | 392 | protected static function parseHeaders($headers) { 393 | if (!is_array($headers)) { 394 | $headers = explode("\r\n", $headers); 395 | } 396 | 397 | $res = array(); 398 | foreach ($headers as $header) { 399 | if (empty($header)) continue; 400 | $split = explode(":", $header, 2); 401 | if (count($split) === 2) { 402 | $res[strtolower($split[0])] = trim($split[1]); 403 | } 404 | } 405 | return $res; 406 | } 407 | 408 | function download($sourceURL, $target, $expectedSize = false) { 409 | $targetTemp = substr($target, 0, 245) . ".sptemp"; 410 | $fp = @fopen ($targetTemp, 'w+'); // open file handle 411 | if(!$fp) { 412 | //file cannot be opened, probably no rights or path disappeared 413 | if(!is_dir(dirname($target))) { 414 | throw new ClientException("The file path cannot be found.", -15); 415 | } else { 416 | throw new ClientException("Temp file cannot be created inside " . dirname($targetTemp) . ". Please check rights.", -16); 417 | } 418 | } 419 | 420 | $ch = curl_init($sourceURL); 421 | // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // enable if you want 422 | curl_setopt_array($ch, $this->customOptions); 423 | curl_setopt($ch, CURLOPT_FILE, $fp); // output to file 424 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); //previously 1. Changed it because it conflicts with some clients open_basedir (php.ini) settings (https://secure.helpscout.net/conversation/859529984/16086?folderId=1117588) 425 | curl_setopt($ch, CURLOPT_TIMEOUT, 10000); // some large value to allow curl to run for a long time 426 | curl_setopt($ch, CURLOPT_USERAGENT, $this->options[CURLOPT_USERAGENT]); 427 | // curl_setopt($ch, CURLOPT_VERBOSE, true); // Enable this line to see debug prints 428 | curl_exec($ch); 429 | 430 | curl_close($ch); // closing curl handle 431 | fclose($fp); // closing file handle 432 | $actualSize = filesize($targetTemp); 433 | if(!$expectedSize || $expectedSize == $actualSize) { 434 | if(!@rename($targetTemp, $target)) { 435 | @unlink($targetTemp); 436 | throw new ClientException("File cannot be renamed. Please check rights.", -16); 437 | } 438 | } else { 439 | $meta = ($actualSize < 200 ? json_decode(file_get_contents($targetTemp)) : false); 440 | if(isset($meta->Status->Code) && $meta->Status->Code === '-302') { 441 | $this->logger->log(SPLog::PRODUCER_CLIENT, "File is gone on the server, needs to be resent.", $meta); 442 | } 443 | // ATENTIE!!!!! daca s-a oprit aici e un caz de fisier cu dimensiunea diferita, de verificat 444 | @unlink($targetTemp); 445 | return -$actualSize; //will retry 446 | } 447 | return true; 448 | } 449 | 450 | function apiStatus($key, $domainToCheck = false, $imgCount = 0, $thumbsCount = 0) { 451 | $request = curl_init(); 452 | curl_setopt_array($request, $this->options); 453 | //$this->prepareJSONRequest(self::API_STATUS_ENDPOINT(), $request, array('key' => $key), 'post', array()); 454 | curl_setopt($request, CURLOPT_URL, self::API_STATUS_ENDPOINT()); 455 | curl_setopt($request, CURLOPT_CUSTOMREQUEST, 'POST'); 456 | curl_setopt($request, CURLOPT_HTTPHEADER, array()); 457 | $params = array('key' => $key); 458 | if($domainToCheck) { 459 | $params['DomainCheck'] = $domainToCheck; 460 | $params['ImagesCount'] = $imgCount; 461 | $params['ThumbsCount'] = $thumbsCount; 462 | 463 | } 464 | curl_setopt($request, CURLOPT_POSTFIELDS, $params); 465 | return $this->sendRequest($request, 1); 466 | } 467 | 468 | /** 469 | * Method that checks the status of an image being optimized 470 | * @param $key 471 | * @param $url 472 | * @return array 473 | * @throws ConnectionException 474 | */ 475 | function imageStatus($key, $url) { 476 | $request = curl_init(); 477 | curl_setopt_array($request, $this->options); 478 | //$this->prepareJSONRequest(self::API_STATUS_ENDPOINT(), $request, array('key' => $key), 'post', array()); 479 | curl_setopt($request, CURLOPT_URL, self::IMAGE_STATUS_ENDPOINT()); 480 | curl_setopt($request, CURLOPT_CUSTOMREQUEST, 'POST'); 481 | curl_setopt($request, CURLOPT_HTTPHEADER, array()); 482 | $params = array('key' => $key, 'url' => $url); 483 | curl_setopt($request, CURLOPT_POSTFIELDS, json_encode($params)); 484 | return $this->sendRequest($request, 1); 485 | } 486 | 487 | /** 488 | * method that dumps the image from the optimization queue so the optimized version isn't available any more. 489 | * Useful when you MIGHT need to optimize another image with the same URL - but with different contents - in the next 490 | * hour and you don't want to have to keep a status to tell you if you need to use refresh() or not... 491 | * @param $key 492 | * @param $urllist 493 | * @return array 494 | * @throws ConnectionException 495 | */ 496 | function imageCleanup($key, $urllist) { 497 | $request = curl_init(); 498 | curl_setopt_array($request, $this->options); 499 | //$this->prepareJSONRequest(self::API_STATUS_ENDPOINT(), $request, array('key' => $key), 'post', array()); 500 | curl_setopt($request, CURLOPT_URL, self::CLEANUP_ENDPOINT()); 501 | curl_setopt($request, CURLOPT_CUSTOMREQUEST, 'POST'); 502 | curl_setopt($request, CURLOPT_HTTPHEADER, array()); 503 | $params = array('key' => $key, 'urllist' => $urllist); 504 | curl_setopt($request, CURLOPT_POSTFIELDS, json_encode($params)); 505 | return $this->sendRequest($request, 1); 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /lib/ShortPixel/Commander.php: -------------------------------------------------------------------------------- 1 | source = $source; 19 | $this->data = $data; 20 | $this->logger = SPLog::Get(SPLog::PRODUCER_CTRL); 21 | //$options = ShortPixel::options(); 22 | $this->commands = array();//('lossy' => 0 + $options["lossy"]); 23 | if(isset($data['refresh']) && $data['refresh']) { 24 | $this->refresh(); 25 | } 26 | } 27 | 28 | /** 29 | * @param int $type 1 - lossy (default), 2 - glossy, 0 - lossless 30 | * @return $this 31 | */ 32 | public function optimize($type = 1) { 33 | $this->commands = array_merge($this->commands, array("lossy" => $type)); 34 | return $this; 35 | } 36 | 37 | /** 38 | * resize the image - performs an outer resize (meaning the image will preserve aspect ratio and have the smallest sizes that allow a rectangle with given width and height to fit inside the resized image) 39 | * @param $width 40 | * @param $height 41 | * @param bool $inner - default, false, true to resize to maximum width or height (both smaller or equal) 42 | * @return $this 43 | */ 44 | public function resize($width, $height, $inner = false) { 45 | $this->commands = array_merge($this->commands, array("resize" => ($inner ? ShortPixel::RESIZE_INNER : ShortPixel::RESIZE_OUTER), "resize_width" => $width, "resize_height" => $height)); 46 | return $this; 47 | } 48 | 49 | /** 50 | * @param bool|true $keep 51 | * @return $this 52 | */ 53 | public function keepExif($keep = true) { 54 | $this->commands = array_merge($this->commands, array("keep_exif" => $keep ? 1 : 0)); 55 | return $this; 56 | } 57 | 58 | 59 | 60 | /** 61 | * @param bool|true $generate - default true, meaning generates WebP. 62 | * @return $this 63 | */ 64 | public function generateWebP($generate = true) { 65 | $convertto = isset($this->commands['convertto']) ? explode('|', $this->commands['convertto']) : array(); 66 | $convertto[] = '+webp'; 67 | $this->commands = array_merge($this->commands, array("convertto" => implode('|', array_unique($convertto)))); 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param bool|true $generate - default true, meaning generates WebP. 73 | * @return $this 74 | */ 75 | public function generateAVIF($generate = true) { 76 | $convertto = isset($this->commands['convertto']) ? explode('|', $this->commands['convertto']) : array(); 77 | $convertto[] = '+avif'; 78 | $this->commands = array_merge($this->commands, array("convertto" => implode('|', array_unique($convertto)))); 79 | return $this; 80 | } 81 | 82 | /** 83 | * @param bool|true $refresh - if true, tells the server to discard the already optimized image and redo the optimization with the new settings. 84 | * @return $this 85 | */ 86 | public function refresh($refresh = true) { 87 | $this->commands = array_merge($this->commands, array("refresh" => $refresh ? 1 : 0)); 88 | return $this; 89 | } 90 | 91 | /** 92 | * will wait for the optimization to finish but not more than $seconds. The wait on the ShortPixel Server side can be a maximum of 30 seconds, for longer waits subsequent server requests will be sent. 93 | * @param int $seconds 94 | * @return $this 95 | */ 96 | public function wait($seconds = 30) { 97 | $seconds = max(0, intval($seconds)); 98 | $this->commands = array_merge($this->commands, array("wait" => min($seconds, 30), "total_wait" => $seconds)); 99 | return $this; 100 | } 101 | 102 | /** 103 | * Not yet implemented 104 | * @param $callbackURL the full url of the notify.php script that handles the notification postback 105 | * @return mixed 106 | */ 107 | public function notifyMe($callbackURL) { 108 | throw new ClientException("NotifyMe not yet implemented"); 109 | $this->commands = array_merge($this->commands, array("notify_me" => $callbackURL)); 110 | return $this->execute(); 111 | } 112 | 113 | /** 114 | * call forwarder to Result - when a command is not understood by the Commander it could be a Result method like toFiles or toBuffer 115 | * @param $method 116 | * @param $args 117 | * @return mixed 118 | * @throws ClientException 119 | */ 120 | public function __call($method, $args) { 121 | if (method_exists("ShortPixel\Result", $method)) { 122 | //execute the commands and forward to Result 123 | if(isset($this->data["files"]) && !count($this->data["files"]) || 124 | isset($this->data["urllist"]) && !count($this->data["urllist"]) || 125 | isset($this->data["buffers"]) && !count($this->data["buffers"])) { 126 | //empty data - no files, no need to send anything, just return an empty result 127 | return (object) array( 128 | 'status' => array('code' => 2, 'message' => 'success'), 129 | 'succeeded' => array(), 130 | 'pending' => array(), 131 | 'failed' => array(), 132 | 'same' => array()); 133 | } 134 | for($i = 0; $i < 6; $i++) { 135 | $return = $this->execute(true); 136 | $this->logger->log(SPLog::PRODUCER_CTRL, "EXECUTE RETURNED: ", $return); 137 | if(!isset($return->body->Status->Code) || !in_array($return->body->Status->Code, array(-305, -404, -500))) { 138 | break; 139 | } 140 | // error -404: The maximum number of URLs in the optimization queue reached, wait a bit and retry. 141 | // error -500: maintenance mode 142 | sleep((10 + 3 * $i) * ($return->body->Status->Code == -500 ? 6 : 1)); //sleep six times longer if maintenance mode. This gives about 15 minutes in total, then it will throw exception. 143 | } 144 | 145 | if(isset($return->body->Status->Code) && $return->body->Status->Code < 0) { 146 | ShortPixel::log("ERROR THROWN: " . $return->body->Status->Message . (isset($return->body->raw) ? "(Server sent: " . substr($return->body->raw, 0, 200) . "...)" : "") . " CODE: " . $return->body->Status->Code); 147 | throw new AccountException($return->body->Status->Message . (isset($return->body->raw) ? "(Server sent: " . substr($return->body->raw, 0, 200) . "...)" : ""), $return->body->Status->Code); 148 | } 149 | return call_user_func_array(array(new Result($this, $return), $method), $args); 150 | } 151 | else { 152 | throw new ClientException('Unknown function '.__CLASS__.':'.$method, E_USER_ERROR); 153 | } 154 | } 155 | 156 | /** 157 | * @internal 158 | * @param bool|false $wait 159 | * @return mixed 160 | * @throws AccountException 161 | */ 162 | public function execute($wait = false){ 163 | if($wait && !isset($this->commands['wait'])) { 164 | $this->commands = array_merge($this->commands, array("wait" => ShortPixel::opt("wait"), "total_wait" => ShortPixel::opt("total_wait"))); 165 | } 166 | ShortPixel::log("EXECUTE OPTIONS: " . json_encode(ShortPixel::options()) . " COMMANDS: " . json_encode($this->commands) . " DATA: " . json_encode($this->data)); 167 | return ShortPixel::getClient()->request("post", array_merge(ShortPixel::options(), $this->commands, $this->data)); 168 | } 169 | 170 | /** 171 | * @internal 172 | * @param $pending 173 | * @return bool|mixed 174 | * @throws ClientException 175 | */ 176 | public function relaunch($ctx) { 177 | ShortPixel::log("RELAUNCH CTX: " . json_encode($ctx) . " COMMANDS: " . json_encode($this->commands) . " DATA: " . json_encode($this->data)); 178 | if(!count($ctx->body) && 179 | (isset($this->data["files"]) && !count($this->data["files"]) || 180 | isset($this->data["urllist"]) && !count($this->data["urllist"]))) return false; //nothing to do 181 | 182 | //decrease the total wait and exit while if time expired 183 | $this->commands["total_wait"] = max(0, $this->commands["total_wait"] - min($this->commands["wait"], 30)); 184 | if($this->commands['total_wait'] == 0) return false; 185 | 186 | $pendingURLs = array(); 187 | //currently we relaunch only if we have the URLs that for posted files should be returned in the first pass. 188 | $type = isset($ctx->body[0]->OriginalURL) ? 'URL' : 'FILE'; 189 | foreach($ctx->body as $pend) { 190 | if($type == 'URL') { 191 | if($pend->OriginalURL && !in_array($pend->OriginalURL, $pendingURLs)) { 192 | $pendingURLs[$pend->OriginalURL] = $pend->OriginalFile; 193 | } 194 | } else { 195 | //for now 196 | throw new ClientException("Not implemented (Commander->execute())"); 197 | } 198 | } 199 | $this->commands["refresh"] = 0; 200 | if($type == 'URL' && count($pendingURLs)) { 201 | $this->data["pendingURLs"] = $pendingURLs; 202 | //$this->data["fileMappings"] = $ctx->fileMappings; 203 | } 204 | return $this->execute(); 205 | 206 | } 207 | 208 | public function getCommands() { 209 | return $this->commands; 210 | } 211 | 212 | public function getData() { 213 | return $this->data; 214 | } 215 | 216 | /* public function setCommand($key, $value) { 217 | return $this->commands[$key] = $value; 218 | } 219 | */ 220 | 221 | public function isDone($item) { 222 | //remove from local files list 223 | if(isset($this->data["files"]) && is_array($this->data["files"])) { 224 | if (isset($item->OriginalFile)) { 225 | $this->data["files"] = array_diff($this->data["files"], array($item->OriginalFile)); 226 | } 227 | elseif (isset($item->SavedFile)) { 228 | $this->data["files"] = array_diff($this->data["files"], array($item->SavedFile)); 229 | } 230 | elseif(isset($item->OriginalURL) && isset($this->data["pendingURLs"][$item->OriginalURL])) { 231 | $this->data["files"] = array_diff($this->data["files"], array($this->data["pendingURLs"][$item->OriginalURL])); 232 | } 233 | } 234 | //remove from pending URLs 235 | if(isset($item->OriginalURL)) { 236 | if(isset($this->data["pendingURLs"][$item->OriginalURL])) { 237 | unset($this->data["pendingURLs"][$item->OriginalURL]); 238 | } 239 | elseif(isset($this->data["urllist"]) && in_array($item->OriginalURL, $this->data["urllist"])) { 240 | $this->data["urllist"] = array_values(array_diff($this->data["urllist"], array($item->OriginalURL))); 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /lib/ShortPixel/Exception.php: -------------------------------------------------------------------------------- 1 | = 400 && $status <= 499) { 10 | $klass = "ShortPixel\ClientException"; 11 | } else if($status >= 500 && $status <= 599) { 12 | $klass = "ShortPixel\ServerException"; 13 | } else { 14 | $klass = "ShortPixel\Exception"; 15 | } 16 | 17 | if (empty($message)) $message = "No message was provided"; 18 | return new $klass($type . ": " . $message, $status); 19 | } 20 | 21 | function __construct($message, $code = 0, $parent = NULL, $type = NULL, $status = NULL) { 22 | if ($status) { 23 | parent::__construct($message . " (HTTP " . $status . "/" . $type . ")", $code, $parent); 24 | } else { 25 | parent::__construct($message, $code, $parent); 26 | } 27 | } 28 | } 29 | 30 | class AccountException extends Exception {} 31 | class ClientException extends Exception { 32 | const NO_FILE_FOUND = -1; 33 | } 34 | class ServerException extends Exception {} 35 | class ConnectionException extends Exception {} 36 | class PersistException extends Exception {} 37 | 38 | -------------------------------------------------------------------------------- /lib/ShortPixel/Lock.php: -------------------------------------------------------------------------------- 1 | processId = $processId; 23 | $this->targetFolder = $targetFolder; 24 | $this->clearLock = $clearLock; 25 | $this->releaseTo = $releaseTo; 26 | $this->timeout = $timeout; 27 | $this->logger = SPLog::Get(SPLog::PRODUCER_PERSISTER); 28 | } 29 | 30 | function setTimeout($timeout) { 31 | $this->timeout = $timeout; 32 | } 33 | 34 | function lockFile() { 35 | return $this->targetFolder . '/' . self::FOLDER_LOCK_FILE; 36 | } 37 | 38 | function readLock() { 39 | if(file_exists($this->lockFile())) { 40 | $lock = file_get_contents($this->targetFolder . '/' . self::FOLDER_LOCK_FILE); 41 | return explode("=", $lock); 42 | } 43 | return false; 44 | } 45 | 46 | function lock() { 47 | //check if the folder is not locked by another ShortPixel process 48 | if(!$this->clearLock && ($lock = $this->readLock()) !== false) { 49 | $time = explode('!', $lock[1]); 50 | if(count($lock) >= 2 && $lock[0] != $this->processId && $time[0] > time() - (isset($time[1]) ? $time[1] : $this->timeout)) { 51 | //a lock was placed on the file and it's not yet expired as per its set timeout 52 | throw new \Exception($this->getLockMsg($lock, $this->targetFolder), -19); 53 | } 54 | elseif(count($lock) >= 4 && $lock[2] == $this->releaseTo) { 55 | // a request to release the lock was received 56 | unlink($this->lockFile()); 57 | throw new \Exception("A lock release was requested by " . $this->releaseTo, -20); 58 | } 59 | } 60 | if(!file_exists($this->targetFolder)) { 61 | mkdir($this->targetFolder, 0755, true); 62 | } 63 | if(FALSE === @file_put_contents($this->lockFile(), $this->processId . "=" . time() . '!' . $this->timeout . (strlen($this->releaseTo) ? "=" . $this->releaseTo : ''))) { 64 | throw new ClientException("Could not write lock file " . $this->lockFile() . ". Please check rights.", -16); 65 | } 66 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "{$this->processId} locked " . dirname($this->lockFile()) . " for {$this->timeout} sec."); 67 | } 68 | 69 | function requestLock($requester) { 70 | if(($lock = $this->readLock()) !== false) { 71 | if(isset($lock[2]) && $lock[2] == $requester) { 72 | //the script that locked the folder will accept a request from $requester to give the lock 73 | //mark in the lock a request to release it 74 | if(FALSE === @file_put_contents($this->lockFile(), $lock[0] . "=" . $lock[1] . "=" . $requester . "=true")) { 75 | throw new ClientException("Could not update lock file " . $this->lockFile() . ". Please check rights.", -16); 76 | } 77 | } else { 78 | //the script will not accept a request from $requester, maybe the lock is old? 79 | $this->lock(); 80 | return; 81 | } 82 | //now wait for the other process to release the lock, a bit more than its expiry time - in case it was left there... 83 | $expiry = max(1, 365 - (time() - $lock[1])); 84 | for($i = 0; $i < $expiry; $i++) { 85 | if(file_exists($this->lockFile())) { 86 | sleep(1); 87 | } else { 88 | break; 89 | } 90 | } 91 | } 92 | $this->lock(); 93 | } 94 | 95 | function unlock() { 96 | if(($lock = $this->readLock()) !== false) { 97 | if($lock[0] == $this->processId) { 98 | unlink($this->lockFile()); 99 | } 100 | } 101 | } 102 | 103 | function getLockMsg($lock, $folder) { 104 | return SPLog::format("The folder is locked by a different ShortPixel process ({$lock[0]}). Exiting. \n\n\033[31mIf you're SURE no other ShortPixel process is running, you can remove the lock with \n\n >\033[34m rm " . $folder . '/' . self::FOLDER_LOCK_FILE . " \033[0m \n"); 105 | } 106 | } -------------------------------------------------------------------------------- /lib/ShortPixel/Persister.php: -------------------------------------------------------------------------------- 1 | time = \ShortPixel\opt("cache_time"); 22 | $this->logger = SPLog::Get(SPLog::PRODUCER_CACHE); 23 | $this->mc = \ShortPixel\getMemcache(); 24 | $this->logger->log(SPLog::PRODUCER_CACHE, "Cache initialized, Expiry Time: " . $this->time . ' SEC.'); 25 | if(!$this->mc) { 26 | $this->logger->log(SPLog::PRODUCER_CACHE, "Memcache not found, using local array."); 27 | $this->local = array(); 28 | } 29 | } 30 | 31 | public function fetch($key) { 32 | $ret = false; 33 | if($this->mc) { 34 | $ret = $this->mc->get($key); 35 | } elseif(isset($this->local[$key]) && time() - $this->local[$key]['time'] < $this->time) { 36 | $ret = $this->local[$key]['value']; 37 | } 38 | $this->logger->log(SPLog::PRODUCER_CACHE, 'FETCHED KEY: ' . $key . ($ret ? ' VALUE: ' . print_r($ret, true) : ' UNFOUND')); 39 | return $ret; 40 | } 41 | 42 | public function store($key, $value) { 43 | if($this->time) { 44 | $this->logger->log(SPLog::PRODUCER_CACHE, 'STORING KEY: ' . $key . ' VALUE: ' . print_r($value, true)); 45 | if($this->mc) { 46 | return $this->mc->set($key, $value, $this->time); 47 | } else { 48 | $this->local[$key] = array('value' => $value, 'time' => time()); 49 | } 50 | } 51 | return false; 52 | } 53 | 54 | public function delete($key) { 55 | $this->logger->log(SPLog::PRODUCER_CACHE, 'DELETING KEY: ' . $key); 56 | if($this->mc) { 57 | return $this->mc->delete($key); 58 | } elseif(isset($this->local[$key])) { 59 | unset($this->local[$key]); 60 | } 61 | return false; 62 | } 63 | 64 | /** 65 | * returns the current cache provider. 66 | * @return SPCache 67 | */ 68 | public static function Get() { 69 | if(!isset(self::$instance)) { 70 | self::$instance = new SPCache(); 71 | } 72 | return self::$instance; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /lib/ShortPixel/SPLog.php: -------------------------------------------------------------------------------- 1 | processId = $processId; 42 | $this->acceptedProducers = $acceptedProducers; 43 | $this->target = $target; 44 | $this->targetName = $targetName; 45 | $this->time = microtime(true); 46 | $this->loggedAlready = array(); 47 | $this->flags = $flags; 48 | } 49 | 50 | /** 51 | * formats a log message 52 | * @param $processId 53 | * @param $msg 54 | * @param $time 55 | * @return string 56 | */ 57 | public static function format($msg, $processId = false, $time = false, $flags = self::FLAG_NONE) { 58 | return "\n" . ($processId ? "$processId@" : "") 59 | . date("Y-m-d H:i:s") 60 | . ($time ? " (" . number_format(microtime(true) - $time, 2) . "s)" : "") 61 | . ($flags | self::FLAG_MEMORY ? " (M: " . number_format(memory_get_usage()) . ")" : ""). " > $msg\n"; 62 | } 63 | 64 | /** 65 | * Log the message if the logger is configured to log from this producer 66 | * @param $producer SPLog::PRODUCER_* - the source of logging ( one of the SPLog::PRODUCER_* values ) 67 | * @param $msg $string the actual message 68 | * @param bool $object 69 | */ 70 | public function log($producer, $msg, $object = false) { 71 | if(!($this->acceptedProducers & $producer)) { return; } 72 | 73 | $msgFmt = self::format($msg, $this->processId, $this->time, $this->flags); 74 | if($object) { 75 | $msgFmt .= " " . json_encode($object); 76 | } 77 | $this->logRaw($msgFmt); 78 | } 79 | 80 | /** 81 | * Log only the first call with that key 82 | * @param $key 83 | * @param $producer 84 | * @param $msg 85 | * @param $object 86 | */ 87 | public function logFirst($key, $producer, $msg, $object = false) { 88 | if(!($this->acceptedProducers & $producer)) { return; } 89 | 90 | if(!in_array($key, $this->loggedAlready)) { 91 | $this->loggedAlready[] = $key; 92 | $this->log($producer, $msg, $object); 93 | } 94 | 95 | } 96 | 97 | public function clearLogged($producer, $key) { 98 | if(!($this->acceptedProducers & $producer)) { return; } 99 | 100 | if (($idx = array_search($key, $this->loggedAlready)) !== false) { 101 | unset($this->loggedAlready[$idx]); 102 | } 103 | } 104 | 105 | /** 106 | * logs a message regardless of the producer setting and without formatting 107 | * @param $msg 108 | */ 109 | public function logRaw($msg){ 110 | switch($this->target) { 111 | case self::TARGET_CONSOLE: 112 | echo($msg); 113 | break; 114 | case self::TARGET_FILE: 115 | $ret = file_put_contents($this->targetName, $msg, FILE_APPEND); 116 | break; 117 | 118 | } 119 | } 120 | 121 | /** 122 | * Log the message if the logger is configured to log from this producer AND EXIT ANYWAY 123 | * @param $producer the source of logging ( one of the SPLog::PRODUCER_* values ) 124 | * @param $msg the actual message 125 | * @param bool $object 126 | */ 127 | public function bye($producer, $msg, $object = false) { 128 | $this->log($producer, $msg, $object); echo("\n");die(); 129 | } 130 | 131 | /** 132 | * init the logger singleton 133 | * @param $processId 134 | * @param $acceptedProducers - the producers from which the logger will log, ignoring gracefully the others 135 | * @param int $target - the log type 136 | * @param bool|false $targetName the log name if needed 137 | * @return SPLog the newly created logger instance 138 | */ 139 | public static function Init($processId, $acceptedProducers, $target = self::TARGET_CONSOLE, $targetName = false, $flags = SPLog::FLAG_NONE) { 140 | self::$instance = new SPLog($processId, $acceptedProducers, $target, $targetName, $flags); 141 | return self::$instance; 142 | } 143 | 144 | /** 145 | * returns the current logger. If the logger is not set to log from that producer or if the log is not initialized, will return a dummy logger which doesn't log. 146 | * @param $producer 147 | * @return SPLog 148 | */ 149 | public static function Get($producer) { 150 | if( !(self::$instance && ($producer & self::$instance->acceptedProducers)) ) { 151 | if(!isset(self::$dummy)) { 152 | self::$dummy = new SPLog(0, self::PRODUCER_NONE, self::TARGET_CONSOLE, false); 153 | } 154 | return self::$dummy; 155 | } 156 | return self::$instance; 157 | } 158 | 159 | /** 160 | * set the target - useful to change the target, for example in order to start logging in a file if a number of retries has been surpassed. 161 | * @param $target 162 | * @param false $targetName 163 | */ 164 | public function setTarget($target, $targetName = false) { 165 | $this->target = $target; 166 | if($targetName) { 167 | $this->targetName = $targetName; 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /lib/ShortPixel/SPTools.php: -------------------------------------------------------------------------------- 1 | INI_PATH = $iniPath; 13 | $this->settings = array(); 14 | if(file_exists($this->INI_PATH)) { 15 | $this->settings = parse_ini_file($this->INI_PATH); 16 | } 17 | } 18 | 19 | function get($key) { 20 | return(isset($this->settings[$key]) ? $this->settings[$key] : false); 21 | } 22 | 23 | function persistApiKeyAndSettings($data) { 24 | if(isset($data['API_KEY']) && strlen($data['API_KEY']) == 20) { 25 | if(file_exists($this->INI_PATH)) { 26 | unlink($this->INI_PATH); 27 | } 28 | $strSettings = "[SHORTPIXEL]\nAPI_KEY=" . $data['API_KEY'] . "\n"; 29 | $settings = $this->post2options($data); 30 | foreach($settings as $key => $val) { 31 | $strSettings .= $key . '=' . $val . "\n"; 32 | } 33 | $settings['API_KEY'] = $data['API_KEY']; 34 | 35 | if(!@file_put_contents($this->INI_PATH, $strSettings)) { 36 | return array("error" => "Could not write properties file " . $this->INI_PATH . ". Please check rights."); 37 | } 38 | $this->settings = $settings; 39 | return array("success" => "API Key set: " . $data['API_KEY']); 40 | } else { 41 | return array("error" => "API Key should be 20 characters long."); 42 | } 43 | } 44 | 45 | function post2options($post) { 46 | $data = array(); 47 | if(isset($post['type'])) $data['lossy'] = $post['type'] == 'lossy' ? 1 : ($post['type'] == 'glossy' ? 2 : 0); 48 | $data['keep_exif'] = isset($post['removeExif']) ? 0 : 1; 49 | $data['cmyk2rgb'] = isset($post['cmyk2rgb']) ? 1 : 0; 50 | $data['resize'] = isset($post['resize']) ? ($post['resize_type'] == 'outer' ? 1 : 3) : 0; 51 | if($data['resize'] && isset($post['width'])) $data['resize_width'] = $post['width']; 52 | if($data['resize'] && isset($post['height'])) $data['resize_height'] = $post['height']; 53 | 54 | $convertto = isset($post['webp']) ? '|+webp' : ''; 55 | $convertto .= isset($post['avif']) ? '|+avif' : ''; 56 | $data['convertto'] = '' . substr($convertto, 1); 57 | 58 | if(isset($post['backup_path'])) { 59 | $data['backup_path'] = $post['backup_path']; 60 | } 61 | if(isset($post['exclude'])) { 62 | $data['exclude'] = $post['exclude']; 63 | } 64 | if(isset($post['user']) && isset($post['pass'])) { 65 | $data['user'] = $post['user']; 66 | $data['pass'] = $post['pass']; 67 | } 68 | if(isset($post['base_url']) && strlen($post['base_url'])) { 69 | $data['base_url'] = rtrim($post['base_url'], '/'); 70 | } elseif (isset($post['change_base_url']) && strlen($post['change_base_url'])) { 71 | $data['base_url'] = rtrim($post['change_base_url'], '/'); 72 | } 73 | return $data; 74 | } 75 | 76 | static function pathToRelative($path, $reference) { 77 | $pa = explode('/', trim($path, '/')); 78 | $ra = explode('/', trim($reference, '/')); 79 | $res = array(); 80 | for($i = 0, $same = true; $i < max(count($pa), count($ra)); $i++) { 81 | if($same && isset($pa[$i]) && isset($ra[$i]) && $pa[$i] == $ra[$i]) continue; 82 | $same = false; 83 | if(isset($ra[$i])) array_unshift($res, '..'); 84 | if(isset($pa[$i])) $res[] = $pa[$i]; 85 | } 86 | return implode('/', $res); 87 | } 88 | 89 | function persistFolderSettings($data, $path) { 90 | $strSettings = "[SHORTPIXEL]\n"; 91 | foreach($this->post2options($data) as $key => $val) { 92 | if(!in_array($key, array("API_KEY", "folder", ""))) 93 | $strSettings .= $key . '=' . (is_numeric($val) ? $val : '"' . $val . '"') . "\n"; 94 | } 95 | return @file_put_contents($path . '/' . self::FOLDER_INI_NAME, $strSettings); 96 | } 97 | 98 | function addOptions($options) { 99 | array_merge($this->settings, $options); 100 | } 101 | 102 | function readOptions($path) { 103 | $options = $this->settings; 104 | if($path && file_exists($path . '/' . self::FOLDER_INI_NAME)) { 105 | $options = array_merge($options, parse_ini_file($path . '/' . self::FOLDER_INI_NAME)); 106 | } 107 | unset($options['API_KEY']); 108 | return $options; 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/ShortPixel/Source.php: -------------------------------------------------------------------------------- 1 | ShortPixel::MAX_ALLOWED_FILES_PER_CALL) { 22 | throw new ClientException("Maximum 10 local images allowed per call."); 23 | } 24 | $files = array(); 25 | foreach($paths as $path) { 26 | if (!file_exists($path)) throw new ClientException("File not found: " . $path); 27 | if (is_dir($path)) throw new ClientException("For folders use fromFolder: " . $path); 28 | $files[] = $path; 29 | } 30 | $data = array( 31 | "plugin_version" => ShortPixel::LIBRARY_CODE . " " . ShortPixel::VERSION, 32 | "key" => ShortPixel::getKey(), 33 | "files" => $files 34 | ); 35 | if($refresh) { //don't put it in the array above because false will overwrite the commands refresh. If only set when true, will just force a refresh when needed. 36 | $data["refresh"] = 1; 37 | } 38 | if($pending && count($pending)) { 39 | $data["pendingURLs"] = $pending; 40 | } 41 | 42 | return new Commander($data, $this); 43 | } 44 | 45 | /** 46 | * returns the optimization counters of the folder and subfolders 47 | * @param $path - the file path on the local drive 48 | * @param bool $recurse - boolean - go into subfolders or not 49 | * @param bool $fileList - return the list of files with optimization status (only current folder, not subfolders) 50 | * @param array $exclude - array of folder names that you want to exclude from the optimization 51 | * @param bool $persistPath - the path where to look for the metadata, if different from the $path 52 | * @param int $recurseDepth - how many subfolders deep to go. Defaults to PHP_INT_MAX 53 | * @param bool $retrySkipped - if true, all skipped files will be reset to pending with retries = 0 54 | * @return object|void (object)array('status', 'total', 'succeeded', 'pending', 'same', 'failed') 55 | * @throws PersistException 56 | */ 57 | public function folderInfo($path, $recurse = true, $fileList = false, $exclude = array(), $persistPath = false, $recurseDepth = PHP_INT_MAX, $retrySkipped = false){ 58 | $path = rtrim($path, '/\\'); 59 | $persistPath = $persistPath ? rtrim($persistPath, '/\\') : false; 60 | $persister = ShortPixel::getPersister($path); 61 | if(!$persister) { 62 | throw new PersistException("Persist is not enabled in options, needed for fetching folder info"); 63 | } 64 | return $persister->info($path, $recurse, $fileList, $exclude, $persistPath, $recurseDepth, $retrySkipped); 65 | } 66 | 67 | /** 68 | * processes a chunk of MAX_ALLOWED files from the folder, based on the persisted information about which images are processed and which not. This information is offered by the Persister object. 69 | * @param $path - the folder path on the local drive 70 | * @param int $maxFiles - maximum number of files to select from the folder 71 | * @param array $exclude - exclude files based on regex patterns 72 | * @param bool $persistFolder - the path where to store the metadata, if different from the $path (usually the target path) 73 | * @param int $maxTotalFileSize - max summed up file size in MB 74 | * @param int $recurseDepth - how many subfolders deep to go. Defaults to PHP_INT_MAX 75 | * @return Commander - the class that handles the optimization commands 76 | * @throws ClientException 77 | * @throws PersistException 78 | */ 79 | public function fromFolder($path, $maxFiles = 0, $exclude = array(), $persistFolder = false, $maxTotalFileSize = ShortPixel::CLIENT_MAX_BODY_SIZE, $recurseDepth = PHP_INT_MAX) { 80 | if($maxFiles == 0) { 81 | $maxFiles = ShortPixel::MAX_ALLOWED_FILES_PER_CALL; 82 | } 83 | //sanitize 84 | $maxFiles = max(1, min(ShortPixel::MAX_ALLOWED_FILES_PER_CALL, intval($maxFiles))); 85 | $path = rtrim($path, '/\\'); 86 | $persistFolder = $persistFolder ? rtrim($persistFolder, '/\\') : false; 87 | 88 | $persister = ShortPixel::getPersister($path); 89 | if(!$persister) { 90 | throw new PersistException("Persist_type is not enabled in options, needed for folder optimization"); 91 | } 92 | $paths = $persister->getTodo($path, $maxFiles, $exclude, $persistFolder, $maxTotalFileSize, $recurseDepth); 93 | if($paths) { 94 | ShortPixel::setOptions(array("base_source_path" => $path)); 95 | return $this->fromFiles($paths->files, null, $paths->filesPending); 96 | } 97 | throw new ClientException("Couldn't find any processable file at given path ($path).", 2); 98 | } 99 | 100 | /** 101 | * processes a chunk of MAX_ALLOWED URLs from a folder that is accessible via web at the $webPath location, 102 | * based on the persisted information about which images are processed and which not. This information is offered by the Persister object. 103 | * @param $path - the folder path on the local drive 104 | * @param $webPath - the web URL of the folder 105 | * @param array $exclude - exclude files based on regex patterns 106 | * @param bool $persistFolder - the path where to store the metadata, if different from the $path (usually the target path) 107 | * @param int $recurseDepth - how many subfolders deep to go. Defaults to PHP_INT_MAX 108 | * @return Commander - the class that handles the optimization commands 109 | * @throws ClientException 110 | * @throws PersistException 111 | */ 112 | public function fromWebFolder($path, $webPath, $exclude = array(), $persistFolder = false, $recurseDepth = PHP_INT_MAX) { 113 | 114 | $path = rtrim($path, '/'); 115 | $webPath = rtrim($webPath, '/'); 116 | $persister = ShortPixel::getPersister(); 117 | if($persister === null) { 118 | //cannot optimize from folder without persister. 119 | throw new PersistException("Persist_type is not enabled in options, needed for folder optimization"); 120 | } 121 | $paths = $persister->getTodo($path, ShortPixel::MAX_ALLOWED_FILES_PER_WEB_CALL, $exclude, $persistFolder, $recurseDepth); 122 | $repl = (object)array("path" => $path . '/', "web" => $webPath . '/'); 123 | if($paths && count($paths->files)) { 124 | $items = array_merge($paths->files, array_values($paths->filesPending)); //not impossible to have filesPending - for example optimized partially without webPath then added it 125 | array_walk( 126 | $items, 127 | function(&$item, $key, $repl){ 128 | $relPath = str_replace($repl->path, '', $item); 129 | $item = implode('/', array_map('rawurlencode', explode('/', $relPath))); 130 | $item = $repl->web . Source::filter($item); 131 | }, $repl); 132 | ShortPixel::setOptions(array("base_url" => $webPath, "base_source_path" => $path)); 133 | 134 | return $this->fromUrls($items); 135 | } 136 | //folder is either empty, either fully optimized, in both cases it's optimized :) 137 | throw new ClientException("Couldn't find any processable file at given path ($path).", 2); 138 | } 139 | 140 | public function fromBuffer($name, $contents) { 141 | return new Commander(array( 142 | "plugin_version" => ShortPixel::LIBRARY_CODE . " " . ShortPixel::VERSION, 143 | "key" => ShortPixel::getKey(), 144 | "buffers" => array($name => $contents), 145 | // don't add it if false, otherwise will overwrite the refresh command //"refresh" => false 146 | ), $this); 147 | } 148 | 149 | /** 150 | * @param $urls - the array of urls to be optimized 151 | * @return Commander - the class that handles the optimization commands 152 | * @throws ClientException 153 | */ 154 | public function fromUrls($urls) { 155 | if(!is_array($urls)) { 156 | $urls = array($urls); 157 | } 158 | if(count($urls) > ShortPixel::MAX_API_ALLOWED_FILES_PER_WEB_CALL) { 159 | throw new ClientException("Maximum 100 images allowed per call."); 160 | } 161 | 162 | $this->urls = array_map (array('ShortPixel\SPTools', 'convertToUtf8'), $urls); 163 | $data = array( 164 | "plugin_version" => ShortPixel::LIBRARY_CODE . " " . ShortPixel::VERSION, 165 | "key" => ShortPixel::getKey(), 166 | "urllist" => $this->urls, 167 | // don't add it if false, otherwise will overwrite the refresh command //"refresh" => false 168 | ); 169 | 170 | return new Commander($data, $this); 171 | } 172 | 173 | protected static function filter($item) { 174 | if(ShortPixel::opt('url_filter') == 'encode') { 175 | //TODO apply base64 or crypt on $item, whichone makes for a shorter string. 176 | $extPos = strripos($item,"."); 177 | $extension = substr($item,$extPos + 1); 178 | $item = substr($item, 0, $extPos); 179 | //$ExtensionContentType = ( $extension == "jpg" ) ? "jpeg" : $extension; 180 | $item = base64_encode($item).'.'.$extension; 181 | SPLog::Get(SPLog::PRODUCER_SOURCE)->log(SPLog::PRODUCER_SOURCE, "ENCODED URL PART: " . $item); 182 | } 183 | return $item; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/ShortPixel/notify/ProgressNotifier.php: -------------------------------------------------------------------------------- 1 | path = $path; 13 | } 14 | 15 | public function recordProgress($info, $replace = false) { 16 | $data = $this->getData(); 17 | $data = $data ? $data : new \stdClass(); 18 | if(isset($info->status)) { 19 | $data->status = $info->status; 20 | } 21 | if(isset($info->total)) { 22 | $data->total = 0 + $info->total; 23 | } 24 | if($replace) { 25 | $data->succeeded = $data->failed = $data->same = 0; 26 | } 27 | if(isset($info->failed)) { 28 | $data->failed = (isset($data->failed) ? $data->failed : 0) + (is_array($info->failed) ? count($info->failed) : 0 + $info->failed); 29 | } 30 | if(isset($info->same)) { 31 | $data->same = (isset($data->same) ? $data->same : 0) + (is_array($info->same) ? count($info->same) : 0 + $info->same); 32 | } 33 | $succeeded = array(); 34 | if(isset($info->succeeded)) { 35 | $data->succeeded = (isset($data->succeeded) ? $data->succeeded : 0) + (is_array($info->succeeded) ? count($info->succeeded) : 0 + $info->succeeded); 36 | if(is_array($info->succeeded)) { 37 | $succeeded = $info->succeeded; 38 | } 39 | } 40 | $data->succeededList = array_slice(array_merge($succeeded, (isset($data->succeededList) ? $data->succeededList : array())), 0, 20); 41 | if(!count($data->succeededList)) unset($data->succeededList); 42 | 43 | $same = array(); 44 | if(isset($info->same)) { 45 | $data->same = (isset($data->same) ? $data->same : 0) + (is_array($info->same) ? count($info->same) : 0 + $info->same); 46 | if(is_array($info->same)) { 47 | $same = $info->same; 48 | } 49 | } 50 | $data->sameList = array_slice(array_merge($same, (isset($data->sameList) ? $data->sameList : array())), 0, 20); 51 | if(!count($data->sameList)) unset($data->sameList); 52 | 53 | $failed = array(); 54 | if(isset($info->failed)) { 55 | $data->failed = (isset($data->failed) ? $data->failed : 0) + (is_array($info->failed) ? count($info->failed) : 0 + $info->failed); 56 | if(is_array($info->failed)) { 57 | for($i = 0; $i < count($info->failed); $i++) { 58 | $info->failed[$i]->TimeStamp = date("Y-m-d H:i:s"); 59 | } 60 | $failed = $info->failed; 61 | } 62 | } 63 | $data->failedList = array_slice(array_merge($failed, (isset($data->failedList) ? $data->failedList : array())), 0, 100); 64 | if(!count($data->failedList)) unset($data->failedList); 65 | 66 | $this->setData($data); 67 | } 68 | 69 | public abstract function getData(); 70 | public abstract function setData($data); 71 | public abstract function set($type, $data); 72 | public abstract function get($type); 73 | 74 | public function enqueueFailedImages(){ 75 | 76 | } 77 | public function getFailedImages(){ 78 | 79 | } 80 | 81 | /** 82 | * Add to a queue info about the last optimized images. The queue is limited to maximum 20 images - the newest 83 | * @return mixed 84 | */ 85 | public function enqueueDoneImages() { 86 | 87 | } 88 | 89 | /** 90 | * @return array - list of the last maximum 20 images optimized. 91 | */ 92 | public function getDoneImages() { 93 | 94 | } 95 | 96 | public static function constructNotifier($path) { 97 | $mc = \ShortPixel\getMemcache(); 98 | if($mc) { 99 | $notifier = new ProgressNotifierMemcache($path); 100 | $notifier->setMemcache($mc); 101 | return $notifier; 102 | } 103 | return new ProgressNotifierFileQ($path); 104 | } 105 | } -------------------------------------------------------------------------------- /lib/ShortPixel/notify/ProgressNotifierFileQ.php: -------------------------------------------------------------------------------- 1 | getData(); 13 | return isset($data[$type]) ? $data[$type] : false; 14 | } 15 | 16 | public function set($type, $value) 17 | { 18 | $data = $this->getData(); 19 | if(!is_array($data)) { 20 | $data = []; 21 | } 22 | $data[$type] = $value; 23 | $this->setData($data); 24 | } 25 | 26 | const PROGRESS_FILE_NAME = '.sp-progress'; 27 | 28 | public function getFilePath() { 29 | return rtrim( $this->path, '/\\' ) . '/' . self::PROGRESS_FILE_NAME; 30 | } 31 | 32 | public function getData() { 33 | $file = $this->getFilePath(); 34 | if(file_exists($file)) { 35 | return json_decode(file_get_contents($file)); 36 | } 37 | return false; 38 | } 39 | 40 | function setData($data) { 41 | file_put_contents($this->getFilePath(), json_encode($data)); 42 | } 43 | } -------------------------------------------------------------------------------- /lib/ShortPixel/notify/ProgressNotifierMemcache.php: -------------------------------------------------------------------------------- 1 | key = md5($path); 15 | } 16 | 17 | public function setMemcache($mc) { 18 | $this->mc = $mc; 19 | } 20 | 21 | public function set($type, $val) 22 | { 23 | $data = $this->mc->get($this->key); 24 | $data[$type] = $val; 25 | $this->mc->set($this->key, $data); 26 | } 27 | 28 | public function get($type) 29 | { 30 | $data = $this->mc->get($this->key); 31 | return isset($data[$type]) ? $data[$type] : false; 32 | } 33 | 34 | public function getData() 35 | { 36 | return $this->mc->get($this->key); 37 | } 38 | 39 | public function setData($data) 40 | { 41 | $this->mc->set($this->key, $data); 42 | } 43 | } -------------------------------------------------------------------------------- /lib/ShortPixel/persist/ExifPersister.php: -------------------------------------------------------------------------------- 1 | $section) { 33 | if($key == "EXIF"){ 34 | foreach ($section as $name => $val) { 35 | if($name === "UserComment") { 36 | $code = substr($val, -5); 37 | if($code === \ShortPixel\ShortPixel::LOSSLESS_EXIF_TAG || $code === \ShortPixel\ShortPixel::LOSSY_EXIF_TAG) { 38 | return true; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | break; 45 | case IMAGETYPE_PNG: 46 | $png = new PNGReader($path); 47 | $sections = $png->get_sections(); 48 | $png = PNGMetadataExtractor::getMetadata($path); 49 | if(isset($png["text"])) { 50 | if(is_array($png["text"])){ 51 | foreach($png["text"] as $key => $item) { 52 | if($key == "APP1_Profile" && isset($item["x-default"])) { 53 | $lines = explode("\n", $item["x-default"]); 54 | if(isset($lines[7])){ 55 | $val = trim(substr(hex2bin($lines[7]), -6)); 56 | if($val === \ShortPixel\ShortPixel::LOSSLESS_EXIF_TAG || $val === \ShortPixel\ShortPixel::LOSSY_EXIF_TAG) 57 | return true; 58 | } 59 | } 60 | } 61 | } else { 62 | 63 | } 64 | } 65 | /*if(isset($sections["COMMENT"]) && $sections["COMMENT"] == "SPXLL") { 66 | return true; 67 | }*/ 68 | } 69 | return false; 70 | } 71 | 72 | function info($path, $recurse = true, $fileList = false, $exclude = array(), $persistPath = false) { 73 | throw new Exception("Not implemented"); 74 | } 75 | 76 | function getTodo($path, $count, $exclude = array(), $persistFolder = false, $maxTotalFileSize = false, $recurseDepth = PHP_INT_MAX) 77 | { 78 | $results = array(); 79 | $this->getTodoRecursive($path, $count, array_values(array_merge($exclude, array('.','..'))), $results, $recurseDepth); 80 | return (object)array('files' => $results, 'filesPending' => array(), 'filesSkipped' => array(), 'refresh' => false); 81 | } 82 | 83 | private function getTodoRecursive($path, &$count, $ignore, &$results, $recurseDepth) { 84 | if($count <= 0) return; 85 | $files = scandir($path); 86 | foreach($files as $t) { 87 | if($count <= 0) return; 88 | if(in_array($t, $ignore)) continue; 89 | $tpath = rtrim($path, '/') . '/' . $t; 90 | if (is_dir($tpath)) { 91 | if($recurseDepth <= 0) continue; 92 | self::getTodoRecursive($tpath, $count, $ignore, $results, $recurseDepth -1); 93 | } elseif( \ShortPixel\ShortPixel::isProcessable($t) 94 | && !$this->isOptimized($tpath)) { 95 | $results[] = $tpath; 96 | $count--; 97 | } 98 | } 99 | } 100 | 101 | function getOptimizationData($path) 102 | { 103 | // TODO: Implement getOptimizationData() method. 104 | } 105 | 106 | function getNextTodo($path, $count) 107 | { 108 | // TODO: Implement getNextTodo() method. 109 | } 110 | 111 | function doneGet() 112 | { 113 | // TODO: Implement doneGet() method. 114 | } 115 | 116 | function setPending($path, $optData) 117 | { 118 | // TODO: Implement setPending() method. 119 | } 120 | 121 | function setOptimized($path, $optData) 122 | { 123 | // TODO: Implement setOptimized() method. 124 | } 125 | 126 | function setFailed($path, $optData) 127 | { 128 | // TODO: Implement setFailed() method. 129 | } 130 | 131 | function setSkipped($path, $optData) 132 | { 133 | // TODO: Implement setSkipped() method. 134 | } 135 | } -------------------------------------------------------------------------------- /lib/ShortPixel/persist/PNGMetadataExtractor.php: -------------------------------------------------------------------------------- 1 | 'xmp', 23 | # Artist is unofficial. Author is the recommended 24 | # keyword in the PNG spec. However some people output 25 | # Artist so support both. 26 | 'artist' => 'Artist', 27 | 'model' => 'Model', 28 | 'make' => 'Make', 29 | 'author' => 'Artist', 30 | 'comment' => 'PNGFileComment', 31 | 'description' => 'ImageDescription', 32 | 'title' => 'ObjectName', 33 | 'copyright' => 'Copyright', 34 | # Source as in original device used to make image 35 | # not as in who gave you the image 36 | 'source' => 'Model', 37 | 'software' => 'Software', 38 | 'disclaimer' => 'Disclaimer', 39 | 'warning' => 'ContentWarning', 40 | 'url' => 'Identifier', # Not sure if this is best mapping. Maybe WebStatement. 41 | 'label' => 'Label', 42 | 'creation time' => 'DateTimeDigitized', 43 | 'raw profile type app1' => 'APP1_Profile', 44 | /* Other potentially useful things - Document */ 45 | ); 46 | 47 | $frameCount = 0; 48 | $loopCount = 1; 49 | $text = array(); 50 | $duration = 0.0; 51 | $bitDepth = 0; 52 | $colorType = 'unknown'; 53 | 54 | if ( !$filename ) { 55 | throw new Exception( __METHOD__ . ": No file name specified" ); 56 | } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) { 57 | throw new Exception( __METHOD__ . ": File $filename does not exist" ); 58 | } 59 | 60 | $fh = fopen( $filename, 'rb' ); 61 | 62 | if ( !$fh ) { 63 | throw new Exception( __METHOD__ . ": Unable to open file $filename" ); 64 | } 65 | 66 | // Check for the PNG header 67 | $buf = fread( $fh, 8 ); 68 | if ( $buf != self::$pngSig ) { 69 | throw new Exception( __METHOD__ . ": Not a valid PNG file; header: $buf" ); 70 | } 71 | 72 | // Read chunks 73 | while ( !feof( $fh ) ) { 74 | $buf = fread( $fh, 4 ); 75 | if ( !$buf || strlen( $buf ) < 4 ) { 76 | throw new Exception( __METHOD__ . ": Read error" ); 77 | } 78 | $chunk = unpack( "N", $buf ); 79 | $chunk_size = $chunk[1]; 80 | 81 | if ( $chunk_size < 0 ) { 82 | throw new Exception( __METHOD__ . ": Chunk size too big for unpack" ); 83 | } 84 | 85 | $chunk_type = fread( $fh, 4 ); 86 | if ( !$chunk_type || strlen( $chunk_type ) < 4 ) { 87 | throw new Exception( __METHOD__ . ": Read error" ); 88 | } 89 | 90 | if ( $chunk_type == "IHDR" ) { 91 | $buf = self::read( $fh, $chunk_size ); 92 | if ( !$buf || strlen( $buf ) < $chunk_size ) { 93 | throw new Exception( __METHOD__ . ": Read error" ); 94 | } 95 | $bitDepth = ord( substr( $buf, 8, 1 ) ); 96 | // Detect the color type in British English as per the spec 97 | // http://www.w3.org/TR/PNG/#11IHDR 98 | switch ( ord( substr( $buf, 9, 1 ) ) ) { 99 | case 0: 100 | $colorType = 'greyscale'; 101 | break; 102 | case 2: 103 | $colorType = 'truecolour'; 104 | break; 105 | case 3: 106 | $colorType = 'index-coloured'; 107 | break; 108 | case 4: 109 | $colorType = 'greyscale-alpha'; 110 | break; 111 | case 6: 112 | $colorType = 'truecolour-alpha'; 113 | break; 114 | default: 115 | $colorType = 'unknown'; 116 | break; 117 | } 118 | } elseif ( $chunk_type == "acTL" ) { 119 | $buf = fread( $fh, $chunk_size ); 120 | if ( !$buf || strlen( $buf ) < $chunk_size || $chunk_size < 4 ) { 121 | throw new Exception( __METHOD__ . ": Read error" ); 122 | } 123 | 124 | $actl = unpack( "Nframes/Nplays", $buf ); 125 | $frameCount = $actl['frames']; 126 | $loopCount = $actl['plays']; 127 | } elseif ( $chunk_type == "fcTL" ) { 128 | $buf = self::read( $fh, $chunk_size ); 129 | if ( !$buf || strlen( $buf ) < $chunk_size ) { 130 | throw new Exception( __METHOD__ . ": Read error" ); 131 | } 132 | $buf = substr( $buf, 20 ); 133 | if ( strlen( $buf ) < 4 ) { 134 | throw new Exception( __METHOD__ . ": Read error" ); 135 | } 136 | 137 | $fctldur = unpack( "ndelay_num/ndelay_den", $buf ); 138 | if ( $fctldur['delay_den'] == 0 ) { 139 | $fctldur['delay_den'] = 100; 140 | } 141 | if ( $fctldur['delay_num'] ) { 142 | $duration += $fctldur['delay_num'] / $fctldur['delay_den']; 143 | } 144 | } elseif ( $chunk_type == "iTXt" ) { 145 | // Extracts iTXt chunks, uncompressing if necessary. 146 | $buf = self::read( $fh, $chunk_size ); 147 | $items = array(); 148 | if ( preg_match( 149 | '/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds', 150 | $buf, $items ) 151 | ) { 152 | /* $items[1] = text chunk name, $items[2] = compressed flag, 153 | * $items[3] = lang code (or ""), $items[4]= compression type. 154 | * $items[5] = content 155 | */ 156 | 157 | // Theoretically should be case-sensitive, but in practise... 158 | $items[1] = strtolower( $items[1] ); 159 | if ( !isset( self::$textChunks[$items[1]] ) ) { 160 | // Only extract textual chunks on our list. 161 | fseek( $fh, self::$crcSize, SEEK_CUR ); 162 | continue; 163 | } 164 | 165 | $items[3] = strtolower( $items[3] ); 166 | if ( $items[3] == '' ) { 167 | // if no lang specified use x-default like in xmp. 168 | $items[3] = 'x-default'; 169 | } 170 | 171 | // if compressed 172 | if ( $items[2] == "\x01" ) { 173 | if ( function_exists( 'gzuncompress' ) && $items[4] === "\x00" ) { 174 | $errLevel = error_reporting(E_ERROR | E_PARSE); 175 | $items[5] = gzuncompress( $items[5] ); 176 | error_reporting($errLevel); 177 | 178 | if ( $items[5] === false ) { 179 | // decompression failed 180 | wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n" ); 181 | fseek( $fh, self::$crcSize, SEEK_CUR ); 182 | continue; 183 | } 184 | } else { 185 | wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,' 186 | . " or potentially invalid compression method\n" ); 187 | fseek( $fh, self::$crcSize, SEEK_CUR ); 188 | continue; 189 | } 190 | } 191 | $finalKeyword = self::$textChunks[$items[1]]; 192 | $text[$finalKeyword][$items[3]] = $items[5]; 193 | $text[$finalKeyword]['_type'] = 'lang'; 194 | } else { 195 | // Error reading iTXt chunk 196 | throw new Exception( __METHOD__ . ": Read error on iTXt chunk" ); 197 | } 198 | } elseif ( $chunk_type == 'tEXt' ) { 199 | $buf = self::read( $fh, $chunk_size ); 200 | 201 | // In case there is no \x00 which will make explode fail. 202 | if ( strpos( $buf, "\x00" ) === false ) { 203 | throw new Exception( __METHOD__ . ": Read error on tEXt chunk" ); 204 | } 205 | 206 | list( $keyword, $content ) = explode( "\x00", $buf, 2 ); 207 | if ( $keyword === '' || $content === '' ) { 208 | throw new Exception( __METHOD__ . ": Read error on tEXt chunk" ); 209 | } 210 | 211 | // Theoretically should be case-sensitive, but in practise... 212 | $keyword = strtolower( $keyword ); 213 | if ( !isset( self::$textChunks[$keyword] ) ) { 214 | // Don't recognize chunk, so skip. 215 | fseek( $fh, self::$crcSize, SEEK_CUR ); 216 | continue; 217 | } 218 | $errLevel = error_reporting(E_ERROR | E_PARSE); 219 | $content = iconv( 'ISO-8859-1', 'UTF-8', $content ); 220 | error_reporting($errLevel); 221 | 222 | if ( $content === false ) { 223 | throw new Exception( __METHOD__ . ": Read error (error with iconv)" ); 224 | } 225 | 226 | $finalKeyword = self::$textChunks[$keyword]; 227 | $text[$finalKeyword]['x-default'] = $content; 228 | $text[$finalKeyword]['_type'] = 'lang'; 229 | } elseif ( $chunk_type == 'zTXt' ) { 230 | if ( function_exists( 'gzuncompress' ) ) { 231 | $buf = self::read( $fh, $chunk_size ); 232 | 233 | // In case there is no \x00 which will make explode fail. 234 | if ( strpos( $buf, "\x00" ) === false ) { 235 | throw new Exception( __METHOD__ . ": Read error on zTXt chunk" ); 236 | } 237 | 238 | list( $keyword, $postKeyword ) = explode( "\x00", $buf, 2 ); 239 | if ( $keyword === '' || $postKeyword === '' ) { 240 | throw new Exception( __METHOD__ . ": Read error on zTXt chunk" ); 241 | } 242 | // Theoretically should be case-sensitive, but in practise... 243 | $keyword = strtolower( $keyword ); 244 | 245 | if ( !isset( self::$textChunks[$keyword] ) ) { 246 | // Don't recognize chunk, so skip. 247 | fseek( $fh, self::$crcSize, SEEK_CUR ); 248 | continue; 249 | } 250 | $compression = substr( $postKeyword, 0, 1 ); 251 | $content = substr( $postKeyword, 1 ); 252 | if ( $compression !== "\x00" ) { 253 | wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping.\n" ); 254 | fseek( $fh, self::$crcSize, SEEK_CUR ); 255 | continue; 256 | } 257 | 258 | $errLevel = error_reporting(E_ERROR | E_PARSE); 259 | $content = gzuncompress( $content ); 260 | error_reporting($errLevel); 261 | 262 | if ( $content === false ) { 263 | // decompression failed 264 | wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n" ); 265 | fseek( $fh, self::$crcSize, SEEK_CUR ); 266 | continue; 267 | } 268 | 269 | $errLevel = error_reporting(E_ERROR | E_PARSE); 270 | $content = iconv( 'ISO-8859-1', 'UTF-8', $content ); 271 | error_reporting($errLevel); 272 | 273 | if ( $content === false ) { 274 | throw new Exception( __METHOD__ . ": Read error (error with iconv)" ); 275 | } 276 | 277 | $finalKeyword = self::$textChunks[$keyword]; 278 | $text[$finalKeyword]['x-default'] = $content; 279 | $text[$finalKeyword]['_type'] = 'lang'; 280 | } else { 281 | wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping.\n" ); 282 | fseek( $fh, $chunk_size, SEEK_CUR ); 283 | } 284 | } elseif ( $chunk_type == 'tIME' ) { 285 | // last mod timestamp. 286 | if ( $chunk_size !== 7 ) { 287 | throw new Exception( __METHOD__ . ": tIME wrong size" ); 288 | } 289 | $buf = self::read( $fh, $chunk_size ); 290 | if ( !$buf || strlen( $buf ) < $chunk_size ) { 291 | throw new Exception( __METHOD__ . ": Read error" ); 292 | } 293 | 294 | // Note: spec says this should be UTC. 295 | $t = unpack( "ny/Cm/Cd/Ch/Cmin/Cs", $buf ); 296 | $strTime = sprintf( "%04d%02d%02d%02d%02d%02d", 297 | $t['y'], $t['m'], $t['d'], $t['h'], 298 | $t['min'], $t['s'] ); 299 | 300 | $exifTime = wfTimestamp( TS_EXIF, $strTime ); 301 | 302 | if ( $exifTime ) { 303 | $text['DateTime'] = $exifTime; 304 | } 305 | } elseif ( $chunk_type == 'pHYs' ) { 306 | // how big pixels are (dots per meter). 307 | if ( $chunk_size !== 9 ) { 308 | throw new Exception( __METHOD__ . ": pHYs wrong size" ); 309 | } 310 | 311 | $buf = self::read( $fh, $chunk_size ); 312 | if ( !$buf || strlen( $buf ) < $chunk_size ) { 313 | throw new Exception( __METHOD__ . ": Read error" ); 314 | } 315 | 316 | $dim = unpack( "Nwidth/Nheight/Cunit", $buf ); 317 | if ( $dim['unit'] == 1 ) { 318 | // Need to check for negative because php 319 | // doesn't deal with super-large unsigned 32-bit ints well 320 | if ( $dim['width'] > 0 && $dim['height'] > 0 ) { 321 | // unit is meters 322 | // (as opposed to 0 = undefined ) 323 | $text['XResolution'] = $dim['width'] 324 | . '/100'; 325 | $text['YResolution'] = $dim['height'] 326 | . '/100'; 327 | $text['ResolutionUnit'] = 3; 328 | // 3 = dots per cm (from Exif). 329 | } 330 | } 331 | } elseif ( $chunk_type == "IEND" ) { 332 | break; 333 | } else { 334 | fseek( $fh, $chunk_size, SEEK_CUR ); 335 | } 336 | fseek( $fh, self::$crcSize, SEEK_CUR ); 337 | } 338 | fclose( $fh ); 339 | 340 | if ( $loopCount > 1 ) { 341 | $duration *= $loopCount; 342 | } 343 | 344 | if ( isset( $text['DateTimeDigitized'] ) ) { 345 | // Convert date format from rfc2822 to exif. 346 | foreach ( $text['DateTimeDigitized'] as $name => &$value ) { 347 | if ( $name === '_type' ) { 348 | continue; 349 | } 350 | 351 | // @todo FIXME: Currently timezones are ignored. 352 | // possibly should be wfTimestamp's 353 | // responsibility. (at least for numeric TZ) 354 | $formatted = wfTimestamp( TS_EXIF, $value ); 355 | if ( $formatted ) { 356 | // Only change if we could convert the 357 | // date. 358 | // The png standard says it should be 359 | // in rfc2822 format, but not required. 360 | // In general for the exif stuff we 361 | // prettify the date if we can, but we 362 | // display as-is if we cannot or if 363 | // it is invalid. 364 | // So do the same here. 365 | 366 | $value = $formatted; 367 | } 368 | } 369 | } 370 | 371 | return array( 372 | 'frameCount' => $frameCount, 373 | 'loopCount' => $loopCount, 374 | 'duration' => $duration, 375 | 'text' => $text, 376 | 'bitDepth' => $bitDepth, 377 | 'colorType' => $colorType, 378 | ); 379 | } 380 | 381 | private static function read( $fh, $size ) { 382 | if ( $size > self::MAX_CHUNK_SIZE ) { 383 | throw new Exception( __METHOD__ . ': Chunk size of ' . $size . 384 | ' too big. Max size is: ' . self::MAX_CHUNK_SIZE ); 385 | } 386 | 387 | return fread( $fh, $size ); 388 | } 389 | } -------------------------------------------------------------------------------- /lib/ShortPixel/persist/PNGReader.php: -------------------------------------------------------------------------------- 1 | _chunks = array (); 16 | 17 | // Open the file 18 | $this->_fp = fopen($file, 'r'); 19 | 20 | if (!$this->_fp) 21 | throw new Exception('Unable to open file'); 22 | 23 | // Read the magic bytes and verify 24 | $header = fread($this->_fp, 8); 25 | 26 | if ($header != "\x89PNG\x0d\x0a\x1a\x0a") 27 | throw new Exception('Is not a valid PNG image'); 28 | 29 | // Loop through the chunks. Byte 0-3 is length, Byte 4-7 is type 30 | $chunkHeader = fread($this->_fp, 8); 31 | 32 | while ($chunkHeader) { 33 | // Extract length and type from binary data 34 | $chunk = @unpack('Nsize/a4type', $chunkHeader); 35 | 36 | // Store position into internal array 37 | if (!isset($this->_chunks[$chunk['type']]) || $this->_chunks[$chunk['type']] === null) 38 | $this->_chunks[$chunk['type']] = array (); 39 | $this->_chunks[$chunk['type']][] = array ( 40 | 'offset' => ftell($this->_fp), 41 | 'size' => $chunk['size'] 42 | ); 43 | 44 | // Skip to next chunk (over body and CRC) 45 | fseek($this->_fp, $chunk['size'] + 4, SEEK_CUR); 46 | 47 | // Read next chunk header 48 | $chunkHeader = fread($this->_fp, 8); 49 | } 50 | } 51 | 52 | function __destruct() { fclose($this->_fp); } 53 | 54 | // Returns all chunks of said type 55 | public function get_chunks($type) { 56 | if (!isset($this->_chunks[$type]) || $this->_chunks[$type] === null) 57 | return null; 58 | 59 | $chunks = array (); 60 | 61 | foreach ($this->_chunks[$type] as $chunk) { 62 | if ($chunk['size'] > 0) { 63 | fseek($this->_fp, $chunk['offset'], SEEK_SET); 64 | $chunks[] = fread($this->_fp, $chunk['size']); 65 | } else { 66 | $chunks[] = ''; 67 | } 68 | } 69 | 70 | return $chunks; 71 | } 72 | 73 | public function get_sections() { 74 | $rawTextData = $this->get_chunks('tEXt'); 75 | if($rawTextData === null) { 76 | //$rawTextData = gzinflate($this->get_chunks('zTXt')); 77 | } 78 | 79 | $metadata = array(); 80 | 81 | if(!is_array($rawTextData)) return $metadata; 82 | 83 | foreach($rawTextData as $data) { 84 | $sections = explode("\0", $data); 85 | 86 | if($sections > 1) { 87 | $key = array_shift($sections); 88 | $metadata[$key] = implode("\0", $sections); 89 | } else { 90 | $metadata[] = $data; 91 | } 92 | } 93 | return $metadata; 94 | } 95 | } -------------------------------------------------------------------------------- /lib/ShortPixel/persist/TextMetaFile.php: -------------------------------------------------------------------------------- 1 | logger = SPLog::Get(SPLog::PRODUCER_PERSISTER); 49 | 50 | $metaFile = $path . '/' . ShortPixel::opt("persist_name"); 51 | if(!is_dir($path) && !@mkdir($path, 0777, true)) { //create the folder 52 | throw new ClientException("The metadata destination path cannot be found. Please check rights", -17); 53 | } 54 | $existing = file_exists($metaFile); 55 | $fp = @fopen($metaFile, $type == 'update' ? 'c+' : 'r'); 56 | if(!$fp) { 57 | if(is_dir($metaFile)) { //saw this for a client 58 | throw new ClientException("Could not open persistence file $metaFile. There's already a directory with this name.", -16); 59 | } else { 60 | throw new ClientException("Could not open persistence file $metaFile. Please check rights.", -16); 61 | } 62 | } 63 | $this->fp = $fp; 64 | $this->type = $type; 65 | $this->path = $path; 66 | if($existing) { 67 | while(true) { 68 | $line = fgets($this->fp); 69 | if($line === false) break; 70 | $length = strlen(rtrim($line, "\r\n")); 71 | if($length == (self::LINE_LENGTH_V2 - 2)) $this->lineLength = self::LINE_LENGTH_V2; 72 | elseif($length == (self::LINE_LENGTH - 2)) $this->lineLength = self::LINE_LENGTH; 73 | if($this->lineLength) break; 74 | } 75 | if(!$this->lineLength) { 76 | $this->lineLength = self::LINE_LENGTH_V2; 77 | } 78 | fseek($this->fp, 0); 79 | } else { 80 | $this->lineLength = self::LINE_LENGTH_V2; 81 | } 82 | } 83 | 84 | private static function unSanitizeFileName($fileName) { 85 | return $fileName; 86 | } 87 | 88 | public function close() { 89 | fclose($this->fp); 90 | unset(self::$REGISTRY[$this->type . ':' . $this->path]); 91 | } 92 | 93 | public static function find($path) { 94 | $metaFile = self::Get(dirname($path), 'read'); 95 | $fp = $metaFile->fp; 96 | fseek($fp, 0); 97 | 98 | $name = \ShortPixel\MB_basename($path); 99 | for ($i = 0; ($line = fgets($fp)) !== FALSE; $i++) { 100 | $data = $metaFile->parse($line); 101 | if(!$data || !property_exists($data, 'file')) { 102 | SPLog::Get(SPLog::PRODUCER_PERSISTER)->log(SPLog::PRODUCER_PERSISTER, 'META LINE CORRUPT: ' . $line); 103 | } 104 | if($data->file === $name) { 105 | $data->filePos = $i; 106 | return $data; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | /** 113 | * @return array 114 | */ 115 | public function readAll() { 116 | $fp = $this->fp; 117 | $dataArr = array(); $err = false; 118 | for ($i = 0; ($line = fgets($fp)) !== FALSE; $i++) { 119 | $data = $this->parse($line); 120 | if($data) { 121 | $data->filePos = $i; 122 | if(isset($dataArr[$data->file])) { 123 | $err = true; //found situations where a line was duplicated, will rewrite but take only the first 124 | } else { 125 | $dataArr[$data->file] = $data; 126 | } 127 | } else { 128 | $err = true; 129 | } 130 | } 131 | if($err) { //at least one error found in the .shortpixel file, rewrite it 132 | fseek($fp, 0); 133 | ftruncate($fp, 0); 134 | foreach($dataArr as $meta) { 135 | fwrite($fp, $this->assemble($meta)); 136 | fwrite($fp, $line . "\r\n"); 137 | } 138 | } 139 | return $dataArr; 140 | } 141 | 142 | /** 143 | * @param $meta 144 | * @param bool|false $returnPointer - set this to true if need to have the file pointer back afterwards, such as when updating while reading the file line by line 145 | */ 146 | public function update($meta, $returnPointer = false) { 147 | $fp = $this->fp; 148 | if($returnPointer) { 149 | $crt = ftell($fp); 150 | } 151 | fseek($fp, $this->lineLength * $meta->filePos); // +2 for the \r\n 152 | fwrite($fp, $this->assemble($meta)); 153 | fflush($fp); 154 | if($returnPointer) { 155 | fseek($fp, $crt); 156 | } 157 | } 158 | 159 | /** 160 | * @param $meta 161 | * @param bool|false $returnPointer - set this to true if need to have the file pointer back afterwards, such as when updating while reading the file line by line 162 | */ 163 | public function append($meta, $returnPointer = false) { 164 | $fp = $this->fp; 165 | if($returnPointer) { 166 | $crt = ftell($fp); 167 | } 168 | $fstat = fstat($fp); 169 | fseek($fp, 0, SEEK_END); 170 | $line = $this->assemble($meta); 171 | //$ob = $this->parse($line); 172 | fwrite($fp, $line . "\r\n"); 173 | fflush($fp); 174 | if($returnPointer) { 175 | fseek($fp, $crt); 176 | } 177 | return $fstat['size'] / $this->lineLength; 178 | } 179 | 180 | public static function newEntry($file, $options) { 181 | //$this->logger->log(SPLog::PRODUCER_PERSISTER, "newMeta: file $file exists? " . (file_exists($file) ? "Yes" : "No")); 182 | return (object) array( 183 | "type" => is_dir($file) ? 'D' : 'I', 184 | "status" => 'pending', 185 | "retries" => 0, 186 | "compressionType" => $options['lossy'] == 1 ? 'lossy' : ($options['lossy'] == 2 ? 'glossy' : 'lossless'), 187 | "keepExif" => $options['keep_exif'], 188 | "cmyk2rgb" => $options['cmyk2rgb'], 189 | "resize" => $options['resize_width'] ? 1 : 0, 190 | "resizeWidth" => 0 + $options['resize_width'], 191 | "resizeHeight" => 0 + $options['resize_height'], 192 | "convertto" => $options['convertto'], 193 | "percent" => 0, 194 | "optimizedSize" => 0, 195 | "changeDate" => time(), 196 | "file" => TextPersister::sanitize(\ShortPixel\MB_basename($file)), 197 | "message" => '', 198 | //file does not exist if source is a WebFolder and the optimized images are saved to a different target 199 | "originalSize" => is_dir($file) || !file_exists($file) ? 0 : filesize($file)); 200 | } 201 | 202 | protected function parse($line) { 203 | $length = strlen(rtrim($line, "\r\n")); 204 | if($length != ($this->lineLength - 2)) return false; 205 | $v2offset = $this->lineLength - self::LINE_LENGTH; 206 | $percent = trim(substr($line, 52, 6)); 207 | $optimizedSize = trim(substr($line, 58, 9)); 208 | $originalSize = trim(substr($line, 454 + $v2offset, 9)); 209 | 210 | $convertto = trim(substr($line, 42, 10)); 211 | if(is_numeric($convertto)) { 212 | //convert to string representation 213 | $conv = array(); 214 | if($convertto | TextPersister::FLAG_WEBP) $conv[] = '+webp'; 215 | if($convertto | TextPersister::FLAG_AVIF) $conv[] = '+avif'; 216 | $convertto = implode('|', $conv); 217 | //$this->logger->log(SPLog::PRODUCER_PERSISTER, "Convertto $convertto"); 218 | } 219 | 220 | $ret = (object) array( 221 | "type" => trim(substr($line, 0, 2)), 222 | "status" => trim(substr($line, 2, 11)), 223 | "retries" => trim(substr($line, 13, 2)), 224 | "compressionType" => trim(substr($line, 15, 9)), 225 | "keepExif" => trim(substr($line, 24, 2)), 226 | "cmyk2rgb" => trim(substr($line, 26, 2)), 227 | "resize" => trim(substr($line, 28, 2)), 228 | "resizeWidth" => trim(substr($line, 30, 6)), 229 | "resizeHeight" => trim(substr($line, 36, 6)), 230 | "convertto" => $convertto, 231 | "percent" => is_numeric($percent) ? floatval($percent) : 0.0, 232 | "optimizedSize" => is_numeric($optimizedSize) ? intval($optimizedSize) : 0, 233 | "changeDate" => strtotime(trim(substr($line, 67, 20))), 234 | "file" => rtrim(self::unSanitizeFileName(substr($line, 87, 256))), //rtrim because there could be file names starting with a blank!! (had that) 235 | "message" => trim(substr($line, 343, 111 + $v2offset)), 236 | "originalSize" => is_numeric($originalSize) ? intval($originalSize) : 0, 237 | ); 238 | if(!in_array($ret->status, self::$ALLOWED_STATUSES) || !$ret->changeDate) { 239 | return false; 240 | } 241 | return $ret; 242 | } 243 | 244 | 245 | protected function assemble($data) { 246 | $v2offset = $this->lineLength - self::LINE_LENGTH; 247 | $convertto = 1; 248 | if(strpos($data->convertto, '+webp') !== false) $convertto |= TextPersister::FLAG_WEBP; 249 | if(strpos($data->convertto, '+avif') !== false) $convertto |= TextPersister::FLAG_AVIF; 250 | 251 | if($data->message === null) $data->message = ''; 252 | 253 | $line = sprintf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", 254 | str_pad($data->type, 2), 255 | str_pad($data->status, 11), 256 | str_pad($data->retries % 100, 2), // for folders, retries can be > 100 so do a sanity check here - we're not actually interested in folder retries 257 | str_pad($data->compressionType, 9), 258 | str_pad($data->keepExif, 2), 259 | str_pad($data->cmyk2rgb, 2), 260 | str_pad($data->resize, 2), 261 | str_pad(substr($data->resizeWidth, 0 , 5), 6), 262 | str_pad(substr($data->resizeHeight, 0 , 5), 6), 263 | str_pad($convertto, 10), 264 | str_pad(substr(number_format($data->percent, 2, ".",""),0 , 5), 6), 265 | str_pad(substr(number_format($data->optimizedSize, 0, ".", ""),0 , 8), 9), 266 | str_pad(date("Y-m-d H:i:s", $data->changeDate), 20), 267 | str_pad(substr($data->file, 0, 255), 256), 268 | str_pad(substr($data->message, 0, 110 + $v2offset), 111 + $v2offset), 269 | str_pad(substr(number_format($data->originalSize, 0, ".", ""),0 , 8), 9) 270 | ); 271 | 272 | if(strlen($line) + 2 !== $this->lineLength) { 273 | echo("LINE LENGTH CORRUPT. DEBUGINFO: " . base64_encode($line));die(); 274 | } 275 | return $line; 276 | } 277 | } -------------------------------------------------------------------------------- /lib/ShortPixel/persist/TextPersister.php: -------------------------------------------------------------------------------- 1 | options = $options; 35 | $this->logger = SPLog::Get(SPLog::PRODUCER_PERSISTER); 36 | $this->cache = SPCache::Get(); 37 | } 38 | 39 | public static function IGNORED_BY_DEFAULT() { 40 | return array('.','..',ShortPixel::opt('persist_name'),Settings::FOLDER_INI_NAME,Lock::FOLDER_LOCK_FILE,ProgressNotifierFileQ::PROGRESS_FILE_NAME,'ShortPixelBackups'); 41 | } 42 | 43 | function isOptimized($path) 44 | { 45 | if(!file_exists($path)) { 46 | return false; 47 | } 48 | try { 49 | $toClose = !TextMetaFile::IsOpen(dirname($path), 'read'); 50 | $metaFile = TextMetaFile::Get(dirname($path)); 51 | $metaData = TextMetaFile::find($path); 52 | if($toClose) { 53 | $metaFile->close(); 54 | } 55 | return isset($metaData->file); 56 | } catch(ClientException $cx) { 57 | return false; 58 | } 59 | 60 | return false; 61 | } 62 | 63 | protected function ignored($exclude) { 64 | $optExclude = isset($this->options['exclude']) && $this->options['exclude'] 65 | ? (is_array($this->options['exclude']) 66 | ? $this->options['exclude'] 67 | : (is_string($this->options['exclude']) ? explode(',', $this->options['exclude']) : array())) 68 | : array(); 69 | $optExclude = array_map('trim', $optExclude); 70 | return array_values(array_merge(self::IGNORED_BY_DEFAULT(), is_array($exclude) ? $exclude : arrray(), $optExclude)); 71 | } 72 | 73 | static function sanitize($filename) { 74 | //print_r($filename);die(); 75 | // our list of "unsafe characters", add/remove characters if necessary 76 | $dangerousCharacters = array("\n", "\r", "\\", "\b"); 77 | // every forbidden character is replaced by a space 78 | $safe_filename = str_replace($dangerousCharacters, ' ', $filename, $count); 79 | 80 | return $safe_filename; 81 | } 82 | 83 | /** 84 | * @param $path - the file path on the local drive 85 | * @param bool $recurse - boolean - go into subfolders or not 86 | * @param bool $fileList - return the list of files with optimization status (only current folder, not subfolders) 87 | * @param array $exclude - array of folder names that you want to exclude from the optimization 88 | * @param bool $persistPath - the path where to look for the metadata, if different from the $path 89 | * @param int $recurseDepth - how many subfolders deep to go. Defaults to PHP_INT_MAX 90 | * @param bool $retrySkipped - if true, all skipped files will be reset to pending with retries = 0 91 | * @return object|void (object)array('status', 'total', 'succeeded', 'pending', 'same', 'failed') 92 | * @throws PersistException 93 | */ 94 | function info($path, $recurse = true, $fileList = false, $exclude = array(), $persistPath = false, $recurseDepth = PHP_INT_MAX, $retrySkipped = false) { 95 | if($persistPath === false) { 96 | $persistPath = $path; 97 | } 98 | $toClose = false; $persistFolder = false; $metaFile = null; 99 | $info = (object)array('status' => 'error', 'message' => "Unknown error, please contact support.", 'code' => -999); 100 | 101 | try { 102 | if(is_dir($path)) { 103 | 104 | try { 105 | $persistFolder = $persistPath; 106 | $toClose = !TextMetaFile::IsOpen($persistPath); 107 | $metaFile = TextMetaFile::Get($persistPath); 108 | $dataArr = $metaFile->readAll(); 109 | } catch(ClientException $e) { 110 | if(!isset($metaFile) || is_null($metaFile) && is_dir($persistPath) && file_exists($persistPath . '/' . ShortPixel::opt("persist_name"))) { 111 | throw $e; //rethrow, there's a problem with the meta file. 112 | } 113 | $dataArr = array(); //there's no problem if the metadata file is missing and cannot be created, for the info call 114 | } 115 | 116 | $info = (object)array('status' => 'pending', 'total' => 0, 'succeeded' => 0, 'pending' => 0, 'same' => 0, 'failed' => 0, 'totalSize'=> 0, 'totalOptimizedSize'=> 0, 'todo' => null); 117 | $files = scandir($path); 118 | $ignore = $this->ignored($exclude); 119 | 120 | foreach($files as $file) { 121 | $filePath = $path . '/' . $file; 122 | $targetFilePath = $persistPath . '/' . $file; 123 | if (in_array($file, $ignore) 124 | || (!ShortPixel::isProcessable($file) && !is_dir($filePath)) 125 | || isset($dataArr[$file]) && $dataArr[$file]->status == 'deleted' 126 | ) { 127 | continue; 128 | } 129 | if (is_dir($filePath)) { 130 | if(!$recurse || $recurseDepth <= 0) continue; 131 | $subInfo = $this->info($filePath, $recurse, $fileList, $exclude, $targetFilePath, $recurseDepth - 1); 132 | if($subInfo->status == 'error') { 133 | $info = $subInfo; 134 | break; 135 | } 136 | $info->total += $subInfo->total; 137 | $info->succeeded += $subInfo->succeeded; 138 | $info->pending += $subInfo->pending; 139 | $info->same += $subInfo->same; 140 | $info->failed += $subInfo->failed; 141 | $info->totalSize += $subInfo->totalSize; 142 | $info->totalOptimizedSize += $subInfo->totalOptimizedSize; 143 | } 144 | else { 145 | rename($path . '/' . $file, $path . '/' . self::sanitize($file)); 146 | $info->total++; 147 | if(!isset($dataArr[$file]) || $dataArr[$file]->status == 'pending') { 148 | $info->pending++; 149 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->info - PENDING STATUS: $path/$file"); 150 | } 151 | elseif( $dataArr[$file]->status == 'success' && $this->isChanged($dataArr[$file], $file, $persistPath, $path) 152 | // || ($dataArr[$file]->status == 'skip' && ($dataArr[$file]->retries <= ShortPixel::MAX_RETRIES || $retrySkipped))) { 153 | || ($dataArr[$file]->status == 'skip' && $retrySkipped)) { 154 | if($dataArr[$file]->status == 'skip' && $retrySkipped) { 155 | $dataArr[$file]->retries = 0; 156 | } elseif($persistPath !== $path) { 157 | //ORIGINAL image size is changed, also update the size 158 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->info - CHANGED (" 159 | . ($dataArr[$file]->originalSize > 0 ? " original size: " . filesize($path . '/' . $file) . ", persisted size: " . $dataArr[$file]->originalSize : '') . ") - REVERT TO PENDING: $path/$file"); 160 | $dataArr[$file]->originalSize = filesize($path . '/' . $file); 161 | } 162 | //file changed since last optimized, mark it as pending 163 | $dataArr[$file]->status = 'pending'; 164 | $metaFile->update($dataArr[$file]); 165 | $info->pending++; 166 | } 167 | elseif($dataArr[$file]->status == 'success') { 168 | if($dataArr[$file]->percent > 0) { 169 | $info->succeeded++; 170 | $info->totalOptimizedSize += $dataArr[$file]->optimizedSize; 171 | $info->totalSize += round(100.0 * $dataArr[$file]->optimizedSize / (100.0 - $dataArr[$file]->percent)); 172 | } else { 173 | $info->same++; 174 | } 175 | } 176 | elseif($dataArr[$file]->status == 'skip'){ 177 | $info->failed++; 178 | } 179 | } 180 | if($fileList) $info->fileList = $dataArr; 181 | } 182 | 183 | if(isset($info->pending) && $info->pending == 0 && $info->status !== 'error') { 184 | $info->status = 'success'; 185 | } 186 | if($info->status !== 'error') { 187 | $info->todo = $this->getTodoInternal($files, $dataArr, $metaFile, $path, 1, $exclude, $persistPath, ShortPixel::CLIENT_MAX_BODY_SIZE, $recurseDepth); 188 | } 189 | } 190 | else { 191 | if(!file_exists($persistPath)) { 192 | throw new ClientException("File not found: $persistPath", -15); 193 | } 194 | $persistFolder = dirname($persistPath); 195 | $meta = $toClose = false; 196 | try { 197 | $toClose = !TextMetaFile::IsOpen($persistFolder, 'read'); 198 | $meta = TextMetaFile::find($persistPath); 199 | } catch(ClientException $e) { 200 | if(is_dir($persistFolder) && file_exists($persistFolder . '/' . ShortPixel::opt("persist_name"))) { 201 | throw $e; 202 | } 203 | } 204 | 205 | if(!$meta) { 206 | $info = (object)array('status' => 'pending'); 207 | } else { 208 | $info = (object)array('status' => $meta->getStatus()); 209 | } 210 | 211 | } 212 | } 213 | catch(ClientException $e) { 214 | $info = (object)array('status' => 'error', 'message' => $e->getMessage(), 'code' => $e->getCode()); 215 | } 216 | catch(\Exception $e) { //that should've been a finally but we need to be PHP5.4 compatible... 217 | if($toClose) { 218 | $this->closeMetaFile($persistFolder); 219 | } 220 | throw $e; 221 | } 222 | if($toClose && !is_null($metaFile)) { 223 | $metaFile->close(); 224 | } 225 | return $info; 226 | } 227 | 228 | function getTodo($path, $count, $exclude = array(), $persistPath = false, $maxTotalFileSizeMb = ShortPixel::CLIENT_MAX_BODY_SIZE, $recurseDepth = PHP_INT_MAX) 229 | { 230 | if(!file_exists($path) || !is_dir($path)) { 231 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - file not found or not a directory: $path"); 232 | return false; 233 | } 234 | if(!$persistPath) {$persistPath = $path;} 235 | 236 | $toClose = !TextMetaFile::IsOpen($persistPath); 237 | $metaFile = TextMetaFile::Get($persistPath); 238 | 239 | $files = scandir($path); 240 | $dataArr = $metaFile->readAll(); 241 | 242 | $ret = $this->getTodoInternal($files, $dataArr, $metaFile, $path, $count, $exclude, $persistPath, $maxTotalFileSizeMb, $recurseDepth); 243 | 244 | if($toClose) { 245 | $metaFile->close(); 246 | } 247 | 248 | if(count($ret->files) + count($ret->filesPending) + $ret->filesWaiting == 0) { 249 | $this->logger->logFirst($path, SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - FOR $path RETURN NONE"); 250 | } else { 251 | $this->logger->clearLogged(SPLog::PRODUCER_PERSISTER, $path); 252 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - FOR $path RETURN", $ret); 253 | } 254 | return $ret; 255 | } 256 | 257 | 258 | /** 259 | * @param $files 260 | * @param $dataArr 261 | * @param TextMetaFile $metaFile 262 | * @param $path 263 | * @param $count 264 | * @param $exclude 265 | * @param $persistPath 266 | * @param $maxTotalFileSizeMb 267 | * @param $recurseDepth 268 | * @return object 269 | */ 270 | protected function getTodoInternal(&$files, &$dataArr, $metaFile, $path, $count, $exclude, $persistPath, $maxTotalFileSizeMb, $recurseDepth) 271 | { 272 | $results = array(); 273 | $pendingURLs = array(); 274 | $ignore = $this->ignored($exclude); 275 | $remain = $count; 276 | $maxTotalFileSize = $maxTotalFileSizeMb * pow(1024, 2); 277 | $totalFileSize = 0; 278 | $filesWaiting = 0; 279 | 280 | foreach($files as $file) { 281 | $filePath = $path . '/' . $file; 282 | $targetPath = $persistPath . '/' . $file; 283 | if(in_array($file, $ignore)) { 284 | continue; //and do not log 285 | } 286 | if(!file_exists($filePath)) { 287 | continue; // strange but found this for a client..., on windows: HS ID 711715228 288 | } 289 | if( !is_dir($filePath) //never skip folders whatever reason as they can have changes inside them 290 | //that's a file: 291 | &&( !ShortPixel::isProcessable($file) //either the file is not processable 292 | || isset($dataArr[$file]) && $dataArr[$file]->status == 'deleted' //or it's deleted 293 | || isset($dataArr[$file]) 294 | && ( $dataArr[$file]->status == 'success' && !$this->isChanged($dataArr[$file], $file, $persistPath, $path) //or changed 295 | || $dataArr[$file]->status == 'skip') ) ) //or skipped 296 | { 297 | if(!isset($dataArr[$file]) || $dataArr[$file]->status !== 'success') { 298 | $this->logger->logFirst($filePath, SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - SKIPPING $path/$file - status " . (isset($dataArr[$file]) ? $dataArr[$file]->status : "not processable")); 299 | } 300 | continue; 301 | } 302 | 303 | if(isset($dataArr[$file]) && $this->isChanged($dataArr[$file], $file, $persistPath, $path)) { 304 | //This means the image was externally changed, revert it to pending state and update the size. 305 | $currentSize = filesize($path . '/' . $file); 306 | $this->logger->log( SPLog::PRODUCER_PERSISTER, "FILE OPTIMIZED BUT CHANGED AFTERWARDS: $file - initial size: " . $dataArr[$file]->originalSize . " current: " . $currentSize); 307 | $dataArr[$file]->status = 'pending'; 308 | $dataArr[$file]->originalSize = $currentSize; 309 | $metaFile->update($dataArr[$file]); 310 | } 311 | 312 | //if retried too many times recently { 313 | if(isset($dataArr[$file]) && $dataArr[$file]->status == 'pending') { 314 | $retries = $dataArr[$file]->retries; 315 | //over 3 retries wait a minute for each, over 5 retries 2 min. for each, over 10 retries 5 min for each, over 10 retries, 10 min. for each. 316 | $delta = max(0, $retries - 2) * 60 + max(0, $retries - 5) * 60 + max(0, $retries - 10) * 180 + max(0, $retries - 20) * 450; 317 | if($dataArr[$file]->changeDate > time() - $delta) { 318 | $filesWaiting++; 319 | $this->logger->logFirst($filePath, SPLog::PRODUCER_PERSISTER | SPLog::PRODUCER_CMD, "TextPersister->getTodo - TOO MANY RETRIES for $file ($retries)"); 320 | continue; 321 | } 322 | } 323 | if(is_dir($filePath)) { 324 | if($recurseDepth <= 0) continue; 325 | if(!isset($dataArr[$file])) { 326 | $dataArr[$file] = TextMetaFile::newEntry($filePath, $this->options); 327 | $dataArr[$file]->filePos = $metaFile->append($dataArr[$file]); 328 | } 329 | $resultsSubfolder = $this->cache->fetch($filePath); 330 | if(!$resultsSubfolder) { 331 | $resultsSubfolder = $this->getTodo($filePath, $count, $exclude, $targetPath, $maxTotalFileSizeMb, $recurseDepth - 1); 332 | if(!count($resultsSubfolder->files)) { 333 | //cache the folders with nothing to do. 334 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - Nothing to do for: $filePath. Caching."); 335 | $this->cache->store($filePath, $resultsSubfolder); 336 | } 337 | } else { 338 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - Cache says nothing to do for: $filePath"); 339 | } 340 | if(count($resultsSubfolder->files)) { 341 | return $resultsSubfolder; 342 | } elseif($dataArr[$file]->status != 'success' && !$resultsSubfolder->filesWaiting) {//otherwise ignore the folder but mark it as succeeded; 343 | $dataArr[$file]->status = 'success'; 344 | $metaFile->update($dataArr[$file]); 345 | } 346 | } else { 347 | $toUpdate = false; //will defer updating the record only if we finally add the image (if the image is too large for this set will not add it in the end 348 | clearstatcache(true, $targetPath); 349 | if(isset($dataArr[$file])) { 350 | if( ($dataArr[$file]->status == 'success') 351 | && (filesize($targetPath) !== $dataArr[$file]->optimizedSize)) { 352 | // a file with the wrong size 353 | $dataArr[$file]->status = 'pending'; 354 | $dataArr[$file]->optimizedSize = 0; 355 | $dataArr[$file]->changeDate = time(); 356 | $toUpdate = true; 357 | if(time() - strtotime($dataArr[$file]->changeDate) < 1800) { //need to refresh the file processing on the server 358 | $metaFile->update($dataArr[$file]); 359 | return (object)array('files' => array($filePath), 'filesPending' => array(), 'filesWaiting' => 0, 'refresh' => true); 360 | } 361 | } 362 | elseif($dataArr[$file]->status == 'error') { 363 | if($dataArr[$file]->retries >= ShortPixel::MAX_RETRIES) { 364 | $dataArr[$file]->status = 'skip'; 365 | $metaFile->update($dataArr[$file]); 366 | continue; 367 | } else { 368 | $dataArr[$file]->retries += 1; 369 | $toUpdate = true; 370 | } 371 | } 372 | 373 | elseif($dataArr[$file]->status == 'pending' && preg_match("/http[s]{0,1}:\/\/" . Client::API_DOMAIN() . "/", $dataArr[$file]->message)) { 374 | //elseif($dataArr[$file]->status == 'pending' && strpos($dataArr[$file]->message, str_replace("https://", "http://",\ShortPixel\Client::API_URL())) === 0) { 375 | //the file is already uploaded and the call should be made with the existent URL on the optimization server 376 | $apiURL = $dataArr[$file]->message; 377 | $pendingURLs[$apiURL] = $filePath; 378 | } 379 | } 380 | elseif(!isset($dataArr[$file])) { 381 | $dataArr[$file] = TextMetaFile::newEntry($filePath, $this->options); 382 | $dataArr[$file]->filePos = $metaFile->append($dataArr[$file]); 383 | } 384 | 385 | clearstatcache(true, $filePath); 386 | $fsz = filesize($filePath); 387 | if($fsz + $totalFileSize > $maxTotalFileSize){ 388 | if($fsz > $maxTotalFileSize) { //skip this as it won't ever be selected with current settings 389 | $dataArr[$file]->status = 'skip'; 390 | if(filesize($filePath) > ShortPixel::CLIENT_MAX_BODY_SIZE * pow(1024, 2)) { 391 | $dataArr[$file]->retries = 99; 392 | } 393 | $dataArr[$file]->message = 'File larger than the set limit of ' . $maxTotalFileSizeMb . 'MBytes'; 394 | $this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - File too large: $path/$file - size: " . $fsz . "MBytes"); 395 | $metaFile->update($dataArr[$file]); //this one is too big, we skipped it, just continue with next. 396 | } else { 397 | //$this->logger->log(SPLog::PRODUCER_PERSISTER, "TextPersister->getTodo - File won't fit this round: $path/$file - size: " . $fsz . "MBytes"); 398 | } 399 | continue; //the total file size would exceed the limit so leave this image out for now. If it's not too large by itself, will take it in the next pass. 400 | } 401 | if($toUpdate) { 402 | $metaFile->update($dataArr[$file]); 403 | } 404 | $results[] = $filePath; 405 | $totalFileSize += filesize($filePath); 406 | $remain--; 407 | 408 | if($remain <= 0) { 409 | break; 410 | } 411 | } 412 | } 413 | 414 | return (object)array('files' => $results, 'filesPending' => $pendingURLs, 'filesWaiting' => $filesWaiting, 'refresh' => false); 415 | } 416 | /** 417 | * @param $data - the .shortpixel metadata 418 | * @param $file - the file basename 419 | * @param $persistPath - the target path for the optimized files and for the .shortpixel metadata 420 | * @param $sourcePath - the path of the original images 421 | * @return bool true if the image is optimized but needs to be reoptimized because it changed 422 | */ 423 | protected function isChanged($data, $file, $persistPath, $sourcePath ) { 424 | clearstatcache(true, $sourcePath); 425 | $fileSize = filesize($sourcePath . '/' . $file); 426 | return $persistPath === $sourcePath && $data->optimizedSize > 0 && $fileSize != $data->optimizedSize 427 | || $persistPath !== $sourcePath && $data->originalSize > 0 && $fileSize != $data->originalSize; 428 | } 429 | 430 | function getNextTodo($path, $count) 431 | { 432 | // TODO: Implement getNextTodo() method. 433 | } 434 | 435 | function doneGet() 436 | { 437 | // TODO: Implement doneGet() method. 438 | } 439 | 440 | function getOptimizationData($path) 441 | { 442 | // TODO: Implement getOptimizationData() method. 443 | } 444 | 445 | function setPending($path, $optData) { 446 | return $this->setStatus($path, $optData, 'pending'); 447 | } 448 | 449 | function setOptimized($path, $optData = array()) { 450 | return $this->setStatus($path, $optData, 'success'); 451 | } 452 | 453 | function setFailed($path, $optData) { 454 | return $this->setStatus($path, $optData, 'error'); 455 | } 456 | 457 | function setSkipped($path, $optData) { 458 | return $this->setStatus($path, $optData, 'skip'); 459 | } 460 | 461 | protected function setStatus($path, $optData, $status) { 462 | $folder = dirname($path); 463 | $toClose = !TextMetaFile::IsOpen($folder); 464 | $meta = TextMetaFile::Get($folder); 465 | 466 | $metaData = TextMetaFile::find($path, 'update'); 467 | if($metaData) { 468 | $metaData->retries++; 469 | $metaData->changeDate = time(); 470 | } else { 471 | $metaData = TextMetaFile::newEntry($path, $this->options); 472 | } 473 | $metaData->status = $status == 'error' ? $metaData->retries > ShortPixel::MAX_RETRIES ? 'skip' : 'pending' : $status; 474 | $metaArr = array_merge((array)$metaData, $optData); 475 | if(isset($metaData->filePos)) { 476 | $meta->update((object)$metaArr, false); 477 | } else { 478 | $meta->append((object)$metaArr, false); 479 | } 480 | 481 | if($toClose) { 482 | $meta->close(); 483 | } 484 | return $metaData->status; 485 | } 486 | 487 | } 488 | -------------------------------------------------------------------------------- /lib/cmdShortpixelOptimize.php: -------------------------------------------------------------------------------- 1 | --folder=/full/path/to/your/images 7 | * - add --compression=x : 1 for lossy, 2 for glossy and 0 for lossless 8 | * - add --resize=800x600/[type] where type can be 1 for outer resize (default) and 3 for inner resize 9 | * - add --backupBase=/full/path/to/your/backup/basedir 10 | * - add --targetFolder to specify a different destination for the optimized files. 11 | * - add --webPath=http://yoursites.address/img/folder/ to map the folder to a web URL and have our servers download the images instead of posting them (less heavy on memory for large files) 12 | * - add --keepExif to keep the EXIF data 13 | * - add --speeed=x x between 1 and 10 - default is 10 but if you have large images it will eat up a lot of memory when creating the post messages so sometimes you might need to lower it. Not needed when using the webPath mapping. 14 | * - add --verbose parameter for more info during optimization 15 | * - add --clearLock to clear a lock that's already placed on the folder. BE SURE you know what you're doing, files might get corrupted if the previous script is still running. The locks expire in 6 min. anyway. 16 | * - add --logLevel for different areas of logging - bitwise flags: 4 for metadata handling, 8 for server comm (add them up to log more areas) 17 | * - add --cacheTime=[seconds] to cache the folders which have no new image to process. Useful for large folders for which checking at each pass is slowing down the optimization. 18 | * - add --quiet for no output - TBD 19 | * - the backup path will be used as parent directory to the backup folder which, if the backup path is outside the optimized folder, will be the basename of the folder, otherwise will be ShortPixelBackup 20 | * The script will read the .sp-options configuration file and will honour the parameters set there, but the command line parameters take priority 21 | */ 22 | 23 | ini_set('memory_limit','256M'); 24 | //error_reporting(E_ALL); 25 | //ini_set('display_errors', 1); 26 | 27 | require_once("shortpixel-php-req.php"); 28 | 29 | use ShortPixel\Lock; 30 | use ShortPixel\ShortPixel; 31 | use \ShortPixel\SPLog; 32 | use ShortPixel\SPTools; 33 | 34 | $argvIsNull = ($argv === NULL); 35 | 36 | $processId = uniqid("CLI"); 37 | 38 | $options = getopt("", array("apiKey::", "folder::", "targetFolder::", "webPath::", "compression::", "resize::", "createWebP", "createAVIF", "keepExif", "speed::", "backupBase::", "verbose", "clearLock", "retrySkipped", 39 | "exclude::", "recurseDepth::", "logLevel::", "cacheTime::")); 40 | 41 | $verbose = isset($options["verbose"]) ? (isset($options["logLevel"]) ? $options["logLevel"] : 0) | SPLog::PRODUCER_CMD_VERBOSE : 0; 42 | $logger = SPLog::Init($processId, $verbose | SPLog::PRODUCER_CMD, SPLog::TARGET_CONSOLE, false, ($verbose ? SPLog::FLAG_MEMORY : SPLog::FLAG_NONE)); 43 | 44 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "ShortPixel CLI version " . ShortPixel::VERSION); 45 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "ShortPixel Logging VERBOSE" . ($verbose & SPLog::PRODUCER_PERSISTER ? ", PERSISTER" : "") . ($verbose & SPLog::PRODUCER_CLIENT ? ", CLIENT" : "")); 46 | if($argvIsNull) { 47 | $logger->bye(SPLog::PRODUCER_CMD, "THE $argv global is not set, please check the register_argc_argv setting in php.ini"); 48 | } 49 | 50 | $apiKey = isset($options["apiKey"]) ? $options["apiKey"] : false; 51 | $folder = isset($options["folder"]) ? verifyFolder($options["folder"]) : false; 52 | $targetFolder = isset($options["targetFolder"]) ? verifyFolder($options["targetFolder"], true) : $folder; 53 | $webPath = isset($options["webPath"]) ? filter_var($options["webPath"], FILTER_VALIDATE_URL) : false; 54 | $compression = isset($options["compression"]) ? intval($options["compression"]) : false; 55 | $resizeRaw = isset($options["resize"]) ? $options["resize"] : false; 56 | $createWebP = isset($options["createWebP"]); 57 | $createAVIF = isset($options["createAVIF"]); 58 | $keepExif = isset($options["keepExif"]); 59 | $speed = isset($options["speed"]) ? intval($options["speed"]) : false; 60 | $bkBase = isset($options["backupBase"]) ? verifyFolder($options["backupBase"]) : false; 61 | $clearLock = isset($options["clearLock"]); 62 | $retrySkipped = isset($options["retrySkipped"]); 63 | $exclude = isset($options["exclude"]) ? explode(",", $options["exclude"]) : array(); 64 | $recurseDepth = isset($options["recurseDepth"]) && is_numeric($options["recurseDepth"]) && $options["recurseDepth"] >= 0 ? $options["recurseDepth"] : PHP_INT_MAX; 65 | $cacheTime = isset($options["cacheTime"]) && is_numeric($options["cacheTime"]) && $options["cacheTime"] >= 0 ? $options["cacheTime"] : 0; 66 | 67 | if(!function_exists('curl_version')) { 68 | $logger->bye(SPLog::PRODUCER_CMD, "cURL is not enabled. ShortPixel needs Curl to send the images to optimization and retrieve the results. Please enable cURL and retry."); 69 | } elseif($verbose) { 70 | $ver = curl_version(); 71 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "cURL version: " . $ver['version']); 72 | } 73 | 74 | if($webPath === false && isset($options["webPath"])) { 75 | $logger->bye(SPLog::PRODUCER_CMD, "The specified Web Path is invalid - " . $options["webPath"]); 76 | } 77 | 78 | $bkFolder = $bkFolderRel = false; 79 | if($bkBase) { 80 | if(is_dir($bkBase)) { 81 | $bkBase = SPTools::trailingslashit($bkBase); 82 | $bkFolder = $bkBase . (strpos($bkBase, SPTools::trailingslashit($folder)) === 0 ? 'ShortPixelBackups' : basename($folder) . (strpos($bkBase, SPTools::trailingslashit(dirname($folder))) === 0 ? "_SP_BKP" : "" )); 83 | $bkFolderRel = \ShortPixel\Settings::pathToRelative($bkFolder, $targetFolder); 84 | } else { 85 | $logger->bye(SPLog::PRODUCER_CMD, "Backup path does not exist ($bkFolder)"); 86 | } 87 | } 88 | 89 | //handle the ctrl+C 90 | if (function_exists('pcntl_signal')) { 91 | declare(ticks=1); // PHP internal, make signal handling work 92 | pcntl_signal(SIGINT, 'spCmdSignalHandler'); 93 | } 94 | 95 | //sanity checks 96 | if(!$apiKey || strlen($apiKey) != 20 || !ctype_alnum($apiKey)) { 97 | $logger->bye(SPLog::PRODUCER_CMD, "Please provide a valid API Key"); 98 | } 99 | 100 | if(!$folder || strlen($folder) == 0) { 101 | $logger->bye(SPLog::PRODUCER_CMD, "Please specify a folder to optimize"); 102 | } 103 | 104 | if($targetFolder != $folder) { 105 | if(strpos($targetFolder, SPTools::trailingslashit($folder)) === 0) { 106 | $logger->bye(SPLog::PRODUCER_CMD, "Target folder cannot be a subfolder of the source folder. ( $targetFolder $folder)"); 107 | } elseif (strpos($folder, SPTools::trailingslashit($targetFolder)) === 0) { 108 | $logger->bye(SPLog::PRODUCER_CMD, "Target folder cannot be a parent folder of the source folder."); 109 | } else { 110 | @mkdir($targetFolder, 0777, true); 111 | } 112 | } 113 | 114 | $notifier = \ShortPixel\notify\ProgressNotifier::constructNotifier($folder); 115 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Using notifier: " . get_class($notifier)); 116 | 117 | try { 118 | //check if the folder is not locked by another ShortPixel process 119 | $splock = new Lock($processId, $targetFolder, $clearLock); 120 | try { 121 | $splock->lock(); 122 | } catch(\Exception $ex) { 123 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Waiting for lock..."); 124 | $splock->requestLock("CLI"); 125 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Lock aquired"); 126 | } 127 | 128 | $logger->log(SPLog::PRODUCER_CMD, "ShortPixel CLI " . ShortPixel::VERSION . " starting to optimize folder $folder using API Key $apiKey ..."); 129 | 130 | \ShortPixel\setKey($apiKey); 131 | 132 | //try to get optimization options from the folder .sp-options 133 | $optionsHandler = new \ShortPixel\Settings(); 134 | $sourceOptions = $optionsHandler->readOptions($folder); 135 | $targetOptions = $optionsHandler->readOptions($targetFolder); 136 | $folderOptions = array_merge(is_array($sourceOptions) ? $sourceOptions : [], is_array($targetOptions) ? $targetOptions : []); 137 | if(count($folderOptions)) { 138 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Options from .sp-options file: ", $folderOptions); 139 | } 140 | 141 | if((!isset($webPath) || !$webPath) && isset($folderOptions["base_url"]) && strlen($folderOptions["base_url"])) { 142 | $webPath = $folderOptions["base_url"]; 143 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Using Web Path from settings: $webPath"); 144 | } 145 | 146 | // ********************* OPTIMIZATION OPTIONS FROM COMMAND LINE TAKE PRECEDENCE ********************* 147 | $overrides = array(); 148 | if($compression !== false) { 149 | $overrides['lossy'] = $compression; 150 | } 151 | if($resizeRaw !== false) { 152 | $tmp = explode("/", $resizeRaw); 153 | $resizeType = (count($tmp) == 2) && ($tmp[1] == 3) ? 3 : 1; 154 | $sizes = explode("x", $tmp[0]); 155 | if(count($sizes) == 2 and is_numeric($sizes[0]) && is_numeric($sizes[1])) { 156 | $overrides['resize'] = $resizeType; 157 | $overrides['resize_width'] = $sizes[0]; 158 | $overrides['resize_height'] = $sizes[1]; 159 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Resize type: " . ($resizeType == 3 ? "inner" : "outer") . ", width: {$overrides['resize_width']}, height: {$overrides['resize_height']}"); 160 | } else { 161 | $splock->unlock(); 162 | $logger->bye(SPLog::PRODUCER_CMD, "Malformed parameter --resize, should be --resize=[width]x[height]/[type] type being 1 for outer and 3 for inner"); 163 | } 164 | } 165 | if($createWebP !== false) { 166 | $overrides['convertto'] = '+webp'; 167 | } 168 | if($createAVIF !== false) { 169 | $overrides['convertto'] = (strlen($overrides['convertto']) ? $overrides['convertto'] . '|' : '') . '+avif'; 170 | } 171 | if($keepExif !== false) { 172 | $overrides['keep_exif'] = 1; 173 | } 174 | 175 | if($bkFolderRel) { 176 | $overrides['backup_path'] = $bkFolderRel; 177 | } 178 | if(!count($exclude) && isset($folderOptions["exclude"]) && strlen($folderOptions["exclude"])) { 179 | $exclude = $folderOptions["exclude"]; 180 | } 181 | $optimizationOptions = array_merge($folderOptions, $overrides, array("persist_type" => "text", "notify_progress" => true, "cache_time" => $cacheTime)); 182 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Using OPTIONS: ", $optimizationOptions); 183 | ShortPixel::setOptions($optimizationOptions); 184 | 185 | $imageCount = $failedImageCount = $sameImageCount = 0; 186 | $tries = 0; 187 | $consecutiveExceptions = 0; 188 | $folderOptimized = false; 189 | $targetFolderParam = ($targetFolder !== $folder ? $targetFolder : false); 190 | 191 | $splock->setTimeout(7200); 192 | $splock->lock(); 193 | $info = \ShortPixel\folderInfo($folder, true, false, $exclude, $targetFolderParam, $recurseDepth, $retrySkipped); 194 | $splock->setTimeout(360); 195 | $splock->lock(); 196 | $notifier->recordProgress($info, true); 197 | 198 | if($info->status == 'error') { 199 | $splock->unlock(); 200 | $logger->bye(SPLog::PRODUCER_CMD, "Error: " . $info->message . " (Code: " . $info->code . ")"); 201 | } 202 | 203 | $logger->log(SPLog::PRODUCER_CMD, "Folder has " . $info->total . " files, " . $info->succeeded . " optimized, " . $info->pending . " pending, " . $info->same . " don't need optimization, " . $info->failed . " failed."); 204 | 205 | if($info->status == "success") { 206 | $logger->log(SPLog::PRODUCER_CMD, "Congratulations, the folder is optimized."); 207 | } 208 | else { 209 | $lockTimeout = 360; 210 | while ($tries < 100000) { 211 | $crtImageCount = 0; 212 | $tempus = time(); 213 | try { 214 | if ($webPath) { 215 | $result = \ShortPixel\fromWebFolder($folder, $webPath, $exclude, $targetFolderParam, $recurseDepth)->wait(300)->toFiles($targetFolder); 216 | } else { 217 | $speed = ($speed ? $speed : ShortPixel::MAX_ALLOWED_FILES_PER_CALL); 218 | $logger->log(SPLog::PRODUCER_CMD, "\n\n\nPASS $tries ...."); 219 | $result = \ShortPixel\fromFolder($folder, $speed, $exclude, $targetFolderParam, ShortPixel::CLIENT_MAX_BODY_SIZE, $recurseDepth)->wait(300)->toFiles($targetFolder); 220 | } 221 | if(time() - $tempus > $lockTimeout - 100) { 222 | //increase the timeout of the lock file if a pass takes too long (for large folders) 223 | $lockTimeout += time() - $tempus; 224 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Increasing lock timeout to: $lockTimeout"); 225 | $splock->setTimeout($lockTimeout); 226 | } 227 | } catch (\ShortPixel\ClientException $ex) { 228 | if ($ex->getCode() == \ShortPixel\ClientException::NO_FILE_FOUND || $ex->getCode() == 2) { 229 | break; 230 | } else { 231 | $logger->log(SPLog::PRODUCER_CMD, "ClientException: " . $ex->getMessage() . " (CODE: " . $ex->getCode() . ")"); 232 | $tries++; 233 | if(++$consecutiveExceptions > ShortPixel::MAX_RETRIES) { 234 | $logger->log(SPLog::PRODUCER_CMD, "Too many exceptions. Exiting."); 235 | break; 236 | } 237 | $splock->lock(); 238 | continue; 239 | } 240 | } 241 | catch (\ShortPixel\ServerException $ex) { 242 | if($ex->getCode() == 502) { 243 | $logger->log(SPLog::PRODUCER_CMD, "ServerException: " . $ex->getMessage() . " (CODE: " . $ex->getCode() . ")"); 244 | if(++$consecutiveExceptions > ShortPixel::MAX_RETRIES) { 245 | $logger->log(SPLog::PRODUCER_CMD, "Too many exceptions. Exiting."); 246 | break; 247 | } 248 | } else { 249 | throw $ex; 250 | } 251 | } 252 | $tries++; 253 | $consecutiveExceptions = 0; 254 | 255 | if (count($result->succeeded) > 0) { 256 | $crtImageCount += count($result->succeeded); 257 | $imageCount += $crtImageCount; 258 | } elseif (count($result->failed)) { 259 | $crtImageCount += count($result->failed); 260 | $failedImageCount += count($result->failed); 261 | } elseif (count($result->same)) { 262 | $crtImageCount += count($result->same); 263 | $sameImageCount += count($result->same); 264 | } elseif (count($result->pending)) { 265 | $crtImageCount += count($result->pending); 266 | } 267 | if ($verbose) { 268 | $msg = "\n" . date("Y-m-d H:i:s") . " PASS $tries : " . count($result->succeeded) . " succeeded, " . count($result->pending) . " pending, " . count($result->same) . " don't need optimization, " . count($result->failed) . " failed\n"; 269 | foreach ($result->succeeded as $item) { 270 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . " (" 271 | . ($item->PercentImprovement > 0 ? "Reduced by " . $item->PercentImprovement . "%" : "") . ($item->PercentImprovement < 5 ? " - Bonus processing" : ""). ")\n"; 272 | } 273 | foreach ($result->pending as $item) { 274 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . "\n"; 275 | } 276 | foreach ($result->same as $item) { 277 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . " (Bonus processing)\n"; 278 | } 279 | foreach ($result->failed as $item) { 280 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . "\n"; 281 | } 282 | $logger->logRaw($msg . "\n"); 283 | } else { 284 | $logger->logRaw(str_pad("", $crtImageCount, "#")); 285 | } 286 | //if no files were processed in this pass, the folder is done 287 | if ($crtImageCount == 0) { 288 | $folderOptimized = (!isset($item) || $item->Status->Code == 2); 289 | break; 290 | } 291 | //check & refresh the lock file 292 | $splock->lock(); 293 | } 294 | 295 | $logger->log(SPLog::PRODUCER_CMD, "This pass: $imageCount images optimized, $sameImageCount don't need optimization, $failedImageCount failed to optimize." . ($folderOptimized ? " Congratulations, the folder is optimized.":"")); 296 | if ($crtImageCount > 0) $logger->log(SPLog::PRODUCER_CMD, "Images still pending, please relaunch the script to continue."); 297 | echo("\n"); 298 | } 299 | } catch(\Exception $e) { 300 | //record progress only if it's not a lock exception. 301 | if($e->getCode() != -19) { 302 | $notifier->recordProgress((object)array("status" => (object)array("code" => $e->getCode(), "message" => $e->getMessage())), true); 303 | } 304 | $logger->log(SPLog::PRODUCER_CMD, "\n" . $e->getMessage() . "( code: " . $e->getCode() . " type: " . get_class($e) . " )" . "\n"); 305 | } 306 | 307 | //cleanup the lock file 308 | $splock->unlock(); 309 | 310 | function verifyFolder($folder, $create = false) 311 | { 312 | global $logger; 313 | $folder = rtrim($folder, '/'); 314 | $suffix = ''; 315 | if($create) { 316 | $suffix = '/' . basename($folder); 317 | $folder = dirname($folder); 318 | } 319 | $folder = (realpath($folder) ? realpath($folder) : $folder); 320 | if (!is_dir($folder)) { 321 | if (substr($folder, 0, 2) == "./") { 322 | $folder = str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . "/" . substr($folder, 2); 323 | } 324 | if (!is_dir($folder)) { 325 | if ((strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && preg_match('/^[a-zA-Z]:(\/|\\)/', $folder) === 0) //it's Windows and no drive letter X - relative path? 326 | || (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' && substr($folder, 0, 1) !== '/') 327 | ) { //linux and no / - relative path? 328 | $folder = str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . "/" . $folder; 329 | } 330 | } 331 | if (!is_dir($folder)) { 332 | $logger->log(SPLog::PRODUCER_CMD, "The folder $folder does not exist."); 333 | } 334 | } 335 | return str_replace(DIRECTORY_SEPARATOR, '/', $folder . $suffix); 336 | } 337 | 338 | function spCmdSignalHandler($signo) 339 | { 340 | global $splock, $logger; 341 | $splock->unlock(); 342 | $logger->bye(SPLog::PRODUCER_CMD, "Caught interrupt signal, exiting."); 343 | } -------------------------------------------------------------------------------- /lib/cmdShortpixelOptimizeFile.php: -------------------------------------------------------------------------------- 1 | --folder=/full/path/to/your/images 7 | * - add --compression=x : 1 for lossy, 2 for glossy and 0 for lossless 8 | * - add --resize=800x600/[type] where type can be 1 for outer resize (default) and 3 for inner resize 9 | * - add --backupBase=/full/path/to/your/backup/basedir 10 | * - add --targetFolder to specify a different destination for the optimized files. 11 | * - add --webPath=http://yoursites.address/img/folder/ to map the folder to a web URL and have our servers download the images instead of posting them (less heavy on memory for large files) 12 | * - add --keepExif to keep the EXIF data 13 | * - add --speeed=x x between 1 and 10 - default is 10 but if you have large images it will eat up a lot of memory when creating the post messages so sometimes you might need to lower it. Not needed when using the webPath mapping. 14 | * - add --verbose parameter for more info during optimization 15 | * - add --clearLock to clear a lock that's already placed on the folder. BE SURE you know what you're doing, files might get corrupted if the previous script is still running. The locks expire in 6 min. anyway. 16 | * - add --logLevel for different areas of logging - bitwise flags: 4 for metadata handling, 8 for server comm (add them up to log more areas) 17 | * - add --cacheTime=[seconds] to cache the folders which have no new image to process. Useful for large folders for which checking at each pass is slowing down the optimization. 18 | * - add --quiet for no output - TBD 19 | * - the backup path will be used as parent directory to the backup folder which, if the backup path is outside the optimized folder, will be the basename of the folder, otherwise will be ShortPixelBackup 20 | * The script will read the .sp-options configuration file and will honour the parameters set there, but the command line parameters take priority 21 | */ 22 | 23 | ini_set('memory_limit','256M'); 24 | //error_reporting(E_ALL); 25 | //ini_set('display_errors', 1); 26 | 27 | require_once("shortpixel-php-req.php"); 28 | 29 | use ShortPixel\Lock; 30 | use ShortPixel\ShortPixel; 31 | use \ShortPixel\SPLog; 32 | use ShortPixel\SPTools; 33 | 34 | $argvIsNull = ($argv === NULL); 35 | 36 | $processId = uniqid("CLI"); 37 | 38 | $options = getopt("", array("apiKey::", "file::", "targetFile::", "webPath::", "compression::", "resize::", "createWebP", "createAVIF", "keepExif", "speed::", "backupBase::", "verbose", "clearLock", "retrySkipped", 39 | "exclude::", "recurseDepth::", "logLevel::", "cacheTime::")); 40 | 41 | $verbose = isset($options["verbose"]) ? (isset($options["logLevel"]) ? $options["logLevel"] : 0) | SPLog::PRODUCER_CMD_VERBOSE : 0; 42 | $logger = SPLog::Init($processId, $verbose | SPLog::PRODUCER_CMD, SPLog::TARGET_CONSOLE, false, ($verbose ? SPLog::FLAG_MEMORY : SPLog::FLAG_NONE)); 43 | 44 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "ShortPixel CLI version " . ShortPixel::VERSION); 45 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "ShortPixel Logging VERBOSE" . ($verbose & SPLog::PRODUCER_PERSISTER ? ", PERSISTER" : "") . ($verbose & SPLog::PRODUCER_CLIENT ? ", CLIENT" : "")); 46 | if($argvIsNull) { 47 | $logger->bye(SPLog::PRODUCER_CMD, 'THE $argv global is not set, please check the register_argc_argv setting in php.ini'); 48 | } 49 | 50 | $apiKey = isset($options["apiKey"]) ? $options["apiKey"] : false; 51 | 52 | $file = isset($options["file"]) ? $options["file"] : false; 53 | $targetFile = isset($options["targetFile"]) ? verifyFolder($options["targetFile"], true) : $file; 54 | $totalFiles = 1; //this is for future extensions when we will alow more than one file to be processed at once. Just in case. 55 | 56 | $compression = isset($options["compression"]) ? intval($options["compression"]) : false; 57 | $resizeRaw = isset($options["resize"]) ? $options["resize"] : false; 58 | $createWebP = isset($options["createWebP"]); 59 | $createAVIF = isset($options["createAVIF"]); 60 | $keepExif = isset($options["keepExif"]); 61 | $speed = isset($options["speed"]) ? intval($options["speed"]) : false; 62 | $bkBase = isset($options["backupBase"]) ? verifyFolder($options["backupBase"]) : false; 63 | //$clearLock = isset($options["clearLock"]); 64 | $retrySkipped = isset($options["retrySkipped"]); 65 | //$exclude = isset($options["exclude"]) ? explode(",", $options["exclude"]) : array(); 66 | //$recurseDepth = isset($options["recurseDepth"]) && is_numeric($options["recurseDepth"]) && $options["recurseDepth"] >= 0 ? $options["recurseDepth"] : PHP_INT_MAX; 67 | $cacheTime = isset($options["cacheTime"]) && is_numeric($options["cacheTime"]) && $options["cacheTime"] >= 0 ? $options["cacheTime"] : 0; 68 | 69 | if(!function_exists('curl_version')) { 70 | $logger->bye(SPLog::PRODUCER_CMD, "cURL is not enabled. ShortPixel needs Curl to send the images to optimization and retrieve the results. Please enable cURL and retry."); 71 | } elseif($verbose) { 72 | $ver = curl_version(); 73 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "cURL version: " . $ver['version']); 74 | } 75 | 76 | $bkFolder = $bkFolderRel = false; 77 | if($bkBase) { 78 | if(is_dir($bkBase)) { 79 | $bkBase = SPTools::trailingslashit($bkBase); 80 | $folder = dirname($file); 81 | $bkFolder = $bkBase . (strpos($bkBase, SPTools::trailingslashit($folder)) === 0 ? 'ShortPixelBackups' : basename($folder) . (strpos($bkBase, SPTools::trailingslashit(dirname($folder))) === 0 ? "_SP_BKP" : "" )); 82 | $bkFolderRel = \ShortPixel\Settings::pathToRelative($bkFolder, dirname($targetFile)); 83 | } else { 84 | $logger->bye(SPLog::PRODUCER_CMD, "Backup path does not exist ($bkFolder)"); 85 | } 86 | } 87 | 88 | //sanity checks 89 | if(!$apiKey || strlen($apiKey) != 20 || !ctype_alnum($apiKey)) { 90 | $logger->bye(SPLog::PRODUCER_CMD, "Please provide a valid API Key"); 91 | } 92 | 93 | if(!$file || strlen($file) == 0) { 94 | $logger->bye(SPLog::PRODUCER_CMD, "Please specify a file to optimize"); 95 | } 96 | 97 | if(!file_exists($file)) { 98 | $logger->bye(SPLog::PRODUCER_CMD, "File not found: $file"); 99 | } 100 | 101 | try { 102 | $logger->log(SPLog::PRODUCER_CMD, "ShortPixel CLI " . ShortPixel::VERSION . " starting to optimize file $file using API Key $apiKey ..."); 103 | 104 | \ShortPixel\setKey($apiKey); 105 | 106 | //try to get optimization options from the folder .sp-options 107 | $optionsHandler = new \ShortPixel\Settings(); 108 | $fileOptions = []; 109 | if((!isset($webPath) || !$webPath) && isset($fileOptions["base_url"]) && strlen($fileOptions["base_url"])) { 110 | $webPath = $fileOptions["base_url"]; 111 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Using Web Path from settings: $webPath"); 112 | } 113 | 114 | // ********************* OPTIMIZATION OPTIONS FROM COMMAND LINE TAKE PRECEDENCE ********************* 115 | $overrides = array(); 116 | if($compression !== false) { 117 | $overrides['lossy'] = $compression; 118 | } 119 | if($resizeRaw !== false) { 120 | $tmp = explode("/", $resizeRaw); 121 | $resizeType = (count($tmp) == 2) && ($tmp[1] == 3) ? 3 : 1; 122 | $sizes = explode("x", $tmp[0]); 123 | if(count($sizes) == 2 and is_numeric($sizes[0]) && is_numeric($sizes[1])) { 124 | $overrides['resize'] = $resizeType; 125 | $overrides['resize_width'] = $sizes[0]; 126 | $overrides['resize_height'] = $sizes[1]; 127 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Resize type: " . ($resizeType == 3 ? "inner" : "outer") . ", width: {$overrides['resize_width']}, height: {$overrides['resize_height']}"); 128 | } else { 129 | $logger->bye(SPLog::PRODUCER_CMD, "Malformed parameter --resize, should be --resize=[width]x[height]/[type] type being 1 for outer and 3 for inner"); 130 | } 131 | } 132 | if($createWebP !== false) { 133 | $overrides['convertto'] = '+webp'; 134 | } 135 | if($createAVIF !== false) { 136 | $overrides['convertto'] = (strlen($overrides['convertto']) ? $overrides['convertto'] . '|' : '') . '+avif'; 137 | } 138 | if($keepExif !== false) { 139 | $overrides['keep_exif'] = 1; 140 | } 141 | 142 | if($bkFolderRel) { 143 | $overrides['backup_path'] = $bkFolderRel; 144 | } 145 | $optimizationOptions = $overrides; 146 | $logger->log(SPLog::PRODUCER_CMD_VERBOSE, "Using OPTIONS: ", $optimizationOptions); 147 | ShortPixel::setOptions($optimizationOptions); 148 | 149 | $imageCount = $failedImageCount = $sameImageCount = 0; 150 | $tries = 0; 151 | $consecutiveExceptions = 0; 152 | $folderOptimized = false; 153 | 154 | while ($tries < 100) { 155 | $crtImageCount = 0; 156 | $tempus = time(); 157 | try { 158 | $logger->log(SPLog::PRODUCER_CMD, "\n\n\nPASS $tries ...."); 159 | $result = \ShortPixel\fromFile($file)->wait(300)->toFiles(dirname($targetFile), basename($targetFile)); 160 | } catch (\ShortPixel\ClientException $ex) { 161 | if ($ex->getCode() == \ShortPixel\ClientException::NO_FILE_FOUND || $ex->getCode() == 2) { 162 | break; 163 | } else { 164 | $logger->log(SPLog::PRODUCER_CMD, "ClientException: " . $ex->getMessage() . " (CODE: " . $ex->getCode() . ")"); 165 | $tries++; 166 | if(++$consecutiveExceptions > ShortPixel::MAX_RETRIES) { 167 | $logger->log(SPLog::PRODUCER_CMD, "Too many exceptions. Exiting."); 168 | break; 169 | } 170 | continue; 171 | } 172 | } 173 | catch (\ShortPixel\ServerException $ex) { 174 | if($ex->getCode() == 502) { 175 | $logger->log(SPLog::PRODUCER_CMD, "ServerException: " . $ex->getMessage() . " (CODE: " . $ex->getCode() . ")"); 176 | if(++$consecutiveExceptions > ShortPixel::MAX_RETRIES) { 177 | $logger->log(SPLog::PRODUCER_CMD, "Too many exceptions. Exiting."); 178 | break; 179 | } 180 | } else { 181 | throw $ex; 182 | } 183 | } 184 | $tries++; 185 | $consecutiveExceptions = 0; 186 | 187 | if (count($result->succeeded) > 0) { 188 | $crtImageCount += count($result->succeeded); 189 | $imageCount += $crtImageCount; 190 | if(count($result->succeeded) == $totalFiles) { 191 | break; 192 | } 193 | } elseif (count($result->failed)) { 194 | $crtImageCount += count($result->failed); 195 | $failedImageCount += count($result->failed); 196 | } elseif (count($result->same)) { 197 | $crtImageCount += count($result->same); 198 | $sameImageCount += count($result->same); 199 | } elseif (count($result->pending)) { 200 | $crtImageCount += count($result->pending); 201 | } 202 | if ($verbose) { 203 | $msg = "\n" . date("Y-m-d H:i:s") . " PASS $tries : " . count($result->succeeded) . " succeeded, " . count($result->pending) . " pending, " . count($result->same) . " don't need optimization, " . count($result->failed) . " failed\n"; 204 | foreach ($result->succeeded as $item) { 205 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . " (" 206 | . ($item->PercentImprovement > 0 ? "Reduced by " . $item->PercentImprovement . "%" : "") . ($item->PercentImprovement < 5 ? " - Bonus processing" : ""). ")\n"; 207 | } 208 | foreach ($result->pending as $item) { 209 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . "\n"; 210 | } 211 | foreach ($result->same as $item) { 212 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . " (Bonus processing)\n"; 213 | } 214 | foreach ($result->failed as $item) { 215 | $msg .= " - " . $item->SavedFile . " " . $item->Status->Message . "\n"; 216 | } 217 | $logger->logRaw($msg . "\n"); 218 | } else { 219 | $logger->logRaw(str_pad("", $crtImageCount, "#")); 220 | } 221 | } 222 | 223 | $logger->log(SPLog::PRODUCER_CMD, "This pass: $imageCount images optimized, $sameImageCount don't need optimization, $failedImageCount failed to optimize." . ($folderOptimized ? " Congratulations, the folder is optimized.":"")); 224 | echo("\n"); 225 | } catch(\Exception $e) { 226 | //record progress only if it's not a lock exception. 227 | if($e->getCode() != -19) { 228 | } 229 | $logger->log(SPLog::PRODUCER_CMD, "\n" . $e->getMessage() . "( code: " . $e->getCode() . " type: " . get_class($e) . " )" . "\n"); 230 | } 231 | -------------------------------------------------------------------------------- /lib/data/shortpixel.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE/DCCA+SgAwIBAgIQTOYGEQsUbdJjOrgYq+G/QjANBgkqhkiG9w0BAQsFADB1 3 | MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEpMCcGA1UECxMg 4 | U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIzAhBgNVBAMTGlN0YXJ0 5 | Q29tIENsYXNzIDEgQ2xpZW50IENBMB4XDTE2MDQwNzEwNTkzOVoXDTE3MDQwNzEw 6 | NTkzOVowRDEdMBsGA1UEAwwUc2ltb25Ac2hvcnRwaXhlbC5jb20xIzAhBgkqhkiG 7 | 9w0BCQEWFHNpbW9uQHNob3J0cGl4ZWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC 8 | AQ8AMIIBCgKCAQEA6VymIV3QNQ67On0AgvkuntO2u20e7UIkk/WrG+jbf6mfQoyo 9 | vd18t8VHfGnXAHMLB1SDzu/JDCAe+xgk4Y2uoMnNTA0NkPmqHg1rUKehVhXaAuMb 10 | d4cKJhkmD3FtlUuAWZB//578nL+VZ2ufj3RON+vufNbePB8coexdkE0mMQPq9paK 11 | 399o8vQjULFuyRJPXXcVIkfiaS6hhfR2qbtG/0nivVIUi7GTjnqinBbsGUcOO53m 12 | dSvPvreL5yqDPXORFX/bGa8ssVKxetFnpwyfv4e9+52kZqwWULbCW341uuYq0PfU 13 | wZQ0HbXFUi08LExULq+5ZetWglcEdQSqSWFCEwIDAQABo4IBtzCCAbMwDgYDVR0P 14 | AQH/BAQDAgSwMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDAJBgNVHRME 15 | AjAAMB0GA1UdDgQWBBRWf5A1tltRHE/XkPbfyvqRQSUYLTAfBgNVHSMEGDAWgBQk 16 | gWw5Yb5JD4+3G0YrySi1J0htaDBvBggrBgEFBQcBAQRjMGEwJAYIKwYBBQUHMAGG 17 | GGh0dHA6Ly9vY3NwLnN0YXJ0c3NsLmNvbTA5BggrBgEFBQcwAoYtaHR0cDovL2Fp 18 | YS5zdGFydHNzbC5jb20vY2VydHMvc2NhLmNsaWVudDEuY3J0MDgGA1UdHwQxMC8w 19 | LaAroCmGJ2h0dHA6Ly9jcmwuc3RhcnRzc2wuY29tL3NjYS1jbGllbnQxLmNybDAf 20 | BgNVHREEGDAWgRRzaW1vbkBzaG9ydHBpeGVsLmNvbTAjBgNVHRIEHDAahhhodHRw 21 | Oi8vd3d3LnN0YXJ0c3NsLmNvbS8wRgYDVR0gBD8wPTA7BgsrBgEEAYG1NwECBTAs 22 | MCoGCCsGAQUFBwIBFh5odHRwOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kwDQYJ 23 | KoZIhvcNAQELBQADggEBAFbFDY2edKRLAG0sDjOiiGob9F11ImhhIDQLy5/M2djn 24 | 1D3x9z1sc+U5kTfS7DNdK4K8XDY+TndAE7h+mEB8hKh0+UYkX00VqhdnBWtDopHM 25 | 2C/3PCyKeCr9YSUIvvSKrOnNsmm7tuiybmd63iQKTe8SWIMJOSZ9i0E8jm03RmKw 26 | QYW+KzOXsq0hRy/CdOBuVxHvu97TeMqyeH5/S0ChxCPZozr8xxT8+sHgkFy4r11P 27 | +YADBHflgqrtbfmbcEoQwx3ucRmiBOtSm8IVQhfiPeYcsOIgPfRFfpu55Qa4kvpk 28 | lP6nlHlkln7XDJsg9i7jHIbyrh9aGGPSMU4mSiEd2Q4= 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /lib/no-composer.php: -------------------------------------------------------------------------------- 1 | "); 5 | $tmpFolder = tempnam(sys_get_temp_dir(), "shortpixel-php"); 6 | echo("Temp folder: " . $tmpFolder); 7 | if(file_exists($tmpFolder)) unlink($tmpFolder); 8 | mkdir($tmpFolder); 9 | \ShortPixel\fromUrls("https://shortpixel.com/img/tests/wrapper/shortpixel.png")->refresh()->wait(300)->toFiles($tmpFolder); 10 | echo("\nSuccessfully saved the optimized image from URL to temp folder.\n"); 11 | \ShortPixel\fromFile(__DIR__ . "/data/cc.jpg")->refresh()->wait(300)->toFiles($tmpFolder); 12 | echo("\nSuccessfully saved the optimized image from path to temp folder.\n\n"); 13 | -------------------------------------------------------------------------------- /lib/shortpixel-php-req.php: -------------------------------------------------------------------------------- 1 | --> 2 | 3 | 4 | 5 | test 6 | 7 | 8 | 9 | 10 | lib 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | toFiles("/path/to/save/to"); 14 | // Compress with default settings but specifying a different file name 15 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->toFiles("/path/to/save/to", "optimized.png"); 16 | 17 | // Compress with default settings from a local file 18 | ShortPixel\fromFile("/path/to/your/local/unoptimized.png")->toFiles("/path/to/save/to"); 19 | // Compress with default settings from several local files 20 | ShortPixel\fromFiles(array("/path/to/your/local/unoptimized1.png", "/path/to/your/local/unoptimized2.png"))->toFiles("/path/to/save/to"); 21 | //Compres and rename each file 22 | \ShortPixel\fromFiles(array("/path/to/your/local/unoptimized1.png", "/path/to/your/local/unoptimized2.png"))->toFiles("/path/to/save/to", ['renamed-one.png', 'renamed-two.png']); 23 | 24 | // Compress with a specific compression level: 0 - lossless, 1 - lossy (default), 2 - glossy 25 | ShortPixel\fromFile("/path/to/your/local/unoptimized.png")->optimize(2)->toFiles("/path/to/save/to"); 26 | 27 | // Compress and resize - image is resized to have the either width equal to specified or height equal to specified 28 | // but not LESS (with settings below, a 300x200 image will be resized to 150x100) 29 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->resize(100, 100)->toFiles("/path/to/save/to"); 30 | // Compress and resize - have the either width equal to specified or height equal to specified 31 | // but not MORE (with settings below, a 300x200 image will be resized to 100x66) 32 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->resize(100, 100, true)->toFiles("/path/to/save/to"); 33 | 34 | // Keep the exif when compressing 35 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->keepExif()->toFiles("/path/to/save/to"); 36 | 37 | // Also generate and save a WebP version of the file - the WebP file will be saved next to the optimized file, with same basename and .webp extension 38 | ShortPixel\fromUrls("https://your.site/img/unoptimized.png")->generateWebP()->toFiles("/path/to/save/to"); 39 | 40 | //Compress from a folder - the status of the compressed images is saved in a text file named .shortpixel in each image folder 41 | \ShortPixel\ShortPixel::setOptions(array("persist_type" => "text")); 42 | //Each call will optimize up to 10 images from the specified folder and mark in the .shortpixel file. 43 | //It automatically recurses a subfolder when finds it 44 | //Save to the same folder, set wait time to 300 to allow enough time for the images to be processed 45 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder")->wait(300)->toFiles("/path/to/your/local/folder"); 46 | //Save to a different folder. CURRENT LIMITATION: When using the text persist type and saving to a different folder, you also need to specify the destination folder as the fourth parameter to fromFolder ( it indicates where the persistence files should be created) 47 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder", 0, array(), "/different/path/to/save/to")->wait(300)->toFiles("/different/path/to/save/to"); 48 | //use a URL to map the folder to a WEB path in order for our servers to download themselves the images instead of receiving them via POST - faster and less exposed to connection timeouts 49 | $ret = ShortPixel\fromWebFolder("/path/to/your/local/folder", "http://web.path/to/your/local/folder")->wait(300)->toFiles("/path/to/save/to"); 50 | //let ShortPixel back-up all your files, before overwriting them (third parameter of toFiles). 51 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder")->wait(300)->toFiles("/path/to/save/to", null, "/back-up/path"); 52 | //Recurse only $N levels down into the subfolders of the folder ( N == 0 means do not recurse ) 53 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder", 0, array(), false, ShortPixel::CLIENT_MAX_BODY_SIZE, $N)->wait(300)->toFiles("/path/to/save/to"); 54 | 55 | //Set custom cURL options (proxy) 56 | \ShortPixel\setCurlOptions(array(CURLOPT_PROXY => '66.96.200.39:80', CURLOPT_REFERER => 'https://shortpixel.com/')); 57 | 58 | 59 | //A simple loop to optimize all images from a folder 60 | $stop = false; 61 | while(!$stop) { 62 | $ret = ShortPixel\fromFolder("/path/to/your/local/folder")->wait(300)->toFiles("/path/to/save/to"); 63 | if(count($ret->succeeded) + count($ret->failed) + count($ret->same) + count($ret->pending) == 0) { 64 | $stop = true; 65 | } 66 | } 67 | 68 | //Compress from an image in memory 69 | $myImage = file_get_contents($pathTo_shortpixel.png); 70 | $ret = \ShortPixel\fromBuffer('shortpixel.png', $myImage)->wait(300)->toFiles(self::$tempDir); 71 | 72 | //Get account status and credits info: 73 | $ret = \ShortPixel\ShortPixel::getClient()->apiStatus(YOUR_API_KEY); 74 | 75 | -------------------------------------------------------------------------------- /test/cmdMoveNFiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find $1 -maxdepth 1 -type f |head -1000|xargs mv -t $2 --------------------------------------------------------------------------------