├── .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 | [
](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
--------------------------------------------------------------------------------