| 1 |
<?php |
| 2 |
|
| 3 |
namespace VisualComposer\Helpers; |
| 4 |
|
| 5 |
if (!defined('ABSPATH')) { |
| 6 |
header('Status: 403 Forbidden'); |
| 7 |
header('HTTP/1.1 403 Forbidden'); |
| 8 |
exit; |
| 9 |
} |
| 10 |
|
| 11 |
use VisualComposer\Framework\Illuminate\Support\Helper; |
| 12 |
|
| 13 |
class Image implements Helper |
| 14 |
{ |
| 15 |
public function parseImage($matches) |
| 16 |
{ |
| 17 |
if (isset($matches[1])) { |
| 18 |
preg_match('(\ssrc=["|\'](.*?)["|\'])', $matches[1], $matchesUrl); |
| 19 |
if (isset($matchesUrl[1])) { |
| 20 |
$src = $matchesUrl[1]; |
| 21 |
} |
| 22 |
|
| 23 |
preg_match('(data-height=["|\']([0-9]{0,4})["|\'])', $matches[1], $matchesHeight); |
| 24 |
if (isset($matchesHeight[1])) { |
| 25 |
$height = (int)$matchesHeight[1]; |
| 26 |
} |
| 27 |
|
| 28 |
preg_match('(data-width=["|\']([0-9]{0,4})["|\'])', $matches[1], $matchesWidth); |
| 29 |
if (isset($matchesWidth[1])) { |
| 30 |
$width = (int)$matchesWidth[1]; |
| 31 |
} |
| 32 |
|
| 33 |
$dynamic = false; |
| 34 |
if (vcvenv('VCV_JS_FT_DYNAMIC_FIELDS')) { |
| 35 |
$isMatches = preg_match('(\sdata-dynamic=["|\'](.*?)["|\'])', $matches[1], $matchesDynamic); |
| 36 |
if ($isMatches) { |
| 37 |
$dynamic = $matchesDynamic[1]; |
| 38 |
} |
| 39 |
} |
| 40 |
|
| 41 |
$attachmentId = 0; |
| 42 |
preg_match('(data-attachment-id=["|\'](\d+)["|\'])', $matches[1], $matchesAttachmentId); |
| 43 |
if (isset($matchesAttachmentId[1])) { |
| 44 |
$attachmentId = (int)$matchesAttachmentId[1]; |
| 45 |
} |
| 46 |
|
| 47 |
if (isset($src, $width, $height)) { |
| 48 |
return $this->generateImage($matches, $src, $width, $height, $dynamic, $attachmentId); |
| 49 |
} |
| 50 |
} |
| 51 |
|
| 52 |
return $matches[0]; |
| 53 |
} |
| 54 |
|
| 55 |
public function getImageData($url) |
| 56 |
{ |
| 57 |
$contentUrl = content_url(); |
| 58 |
$contentUrl = str_replace(['http://', 'https://'], '', $contentUrl); |
| 59 |
if (strpos($url, $contentUrl) !== false) { |
| 60 |
$path = str_replace(['http://', 'https://'], '', $url); |
| 61 |
$path = str_replace($contentUrl, basename($contentUrl), $path); |
| 62 |
} else { |
| 63 |
$path = wp_make_link_relative($url); |
| 64 |
} |
| 65 |
|
| 66 |
$pathinfo = pathinfo($path); |
| 67 |
|
| 68 |
$imageData = [ |
| 69 |
'path' => rtrim(ABSPATH, '/\\') . '/' . ltrim($path, '/\\'), |
| 70 |
'filename' => $pathinfo['filename'], |
| 71 |
'extension' => $pathinfo['extension'], |
| 72 |
]; |
| 73 |
|
| 74 |
return $imageData; |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* Generate <img> with provided attributes |
| 79 |
* |
| 80 |
* @param array $content |
| 81 |
* @param string $src |
| 82 |
* @param int|bool $width |
| 83 |
* @param int|bool $height |
| 84 |
* @param bool $dynamic |
| 85 |
* @param int $attachmentId |
| 86 |
* |
| 87 |
* @return string |
| 88 |
*/ |
| 89 |
protected function generateImage($content, $src, $width = false, $height = false, $dynamic = false, $attachmentId = 0) |
| 90 |
{ |
| 91 |
$src = $this->getLazyLoadSrc($content[1], $src); |
| 92 |
$imageData = $this->getImageData($src); |
| 93 |
|
| 94 |
// Add some additional data for the image |
| 95 |
$imageData['width'] = $width; |
| 96 |
$imageData['height'] = $height; |
| 97 |
$imageData['attachmentId'] = $attachmentId; |
| 98 |
|
| 99 |
$image = wp_get_image_editor($imageData['path']); |
| 100 |
|
| 101 |
$newSrc = ''; |
| 102 |
if (!$dynamic && $width && $height && !is_wp_error($image)) { |
| 103 |
$originalSize = $image->get_size(); |
| 104 |
$imageData['originalWidth'] = $originalSize['width']; |
| 105 |
$imageData['originalHeight'] = $originalSize['height']; |
| 106 |
|
| 107 |
$sizes = $this->getSizes($imageData); |
| 108 |
$images = $this->getImages($sizes, $image, $imageData); |
| 109 |
|
| 110 |
// Update the src with a resized image (with provided $width and $height). |
| 111 |
// Should be already in a list of $images, as we tried to resize images |
| 112 |
// of all provided sizes in $sizes list. |
| 113 |
$resizedImageSrc = $this->getImageSrc($images, $width, $image, $imageData); |
| 114 |
if (!empty($resizedImageSrc)) { |
| 115 |
$src = $resizedImageSrc; |
| 116 |
} |
| 117 |
unset($resizedImageSrc); |
| 118 |
|
| 119 |
// Create the srcset |
| 120 |
$srcset = $this->getImageSrcset($images); |
| 121 |
if (!empty($srcset)) { |
| 122 |
// Note the whitespace before `srcset` attribute |
| 123 |
$newSrc .= sprintf(' srcset="%s"', $srcset); |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
$attributes = $content[1]; |
| 128 |
$isLazyload = strpos($content[1], 'data-src=') !== false; |
| 129 |
$attributes = $this->getImageAttributes($isLazyload, $newSrc, $src, $attributes, $dynamic); |
| 130 |
|
| 131 |
return '<img ' . $attributes . '/>'; |
| 132 |
} |
| 133 |
|
| 134 |
/** |
| 135 |
* @param string $attributes |
| 136 |
* @param $src |
| 137 |
* |
| 138 |
* @return string |
| 139 |
*/ |
| 140 |
public function getLazyLoadSrc($attributes, $src) |
| 141 |
{ |
| 142 |
$isLazyload = strpos($attributes, 'data-src=') !== false; |
| 143 |
if ($isLazyload) { |
| 144 |
preg_match('(\sdata-src=["|\'](.*?)["|\'])', $attributes, $matchesUrl); |
| 145 |
if (isset($matchesUrl[1])) { |
| 146 |
$src = set_url_scheme($matchesUrl[1]); |
| 147 |
} |
| 148 |
} |
| 149 |
|
| 150 |
return $src; |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Get a value for image `src` attribute |
| 155 |
* |
| 156 |
* @param array $images A list of resized images |
| 157 |
* @param int $width A width we are looking for |
| 158 |
* @param \WP_Image_Editor $image |
| 159 |
* @param array $imageData Image data |
| 160 |
* |
| 161 |
* @return string |
| 162 |
*/ |
| 163 |
public function getImageSrc($images, $width, $image = null, array $imageData = []) |
| 164 |
{ |
| 165 |
$key = "{$width}w"; |
| 166 |
if (!empty($images) && is_array($images) && array_key_exists($key, $images)) { |
| 167 |
return $images[ $key ]; |
| 168 |
} |
| 169 |
|
| 170 |
if (!$image instanceof \WP_Image_Editor || empty($imageData)) { |
| 171 |
return ''; |
| 172 |
} |
| 173 |
|
| 174 |
// Oops, image is not resized yet. Try to get an image again. |
| 175 |
$images = $this->getImages([$key => $width], $image, $imageData); |
| 176 |
if (!empty($images) && array_key_exists($key, $images)) { |
| 177 |
return $images[ $key ]; |
| 178 |
} |
| 179 |
|
| 180 |
// Something definitely goes wrong |
| 181 |
return ''; |
| 182 |
} |
| 183 |
|
| 184 |
/** |
| 185 |
* Get a value for image `srcset` attribute |
| 186 |
* |
| 187 |
* @param array $images |
| 188 |
* |
| 189 |
* @return string |
| 190 |
*/ |
| 191 |
public function getImageSrcset($images) |
| 192 |
{ |
| 193 |
if (empty($images)) { |
| 194 |
return ''; |
| 195 |
} |
| 196 |
|
| 197 |
$srcset = []; |
| 198 |
foreach ($images as $widthAttr => $imageSrc) { |
| 199 |
$srcset[] = sprintf('%s %s', esc_url($imageSrc), esc_attr($widthAttr)); |
| 200 |
} |
| 201 |
|
| 202 |
return implode(', ', $srcset); |
| 203 |
} |
| 204 |
|
| 205 |
/** |
| 206 |
* @param $isLazyload |
| 207 |
* @param $newSrc |
| 208 |
* @param $src |
| 209 |
* @param $attributes |
| 210 |
* @param $dynamic |
| 211 |
* |
| 212 |
* @return string |
| 213 |
*/ |
| 214 |
protected function getImageAttributes($isLazyload, $newSrc, $src, $attributes, $dynamic) |
| 215 |
{ |
| 216 |
if ($isLazyload) { |
| 217 |
$newSrc .= ' src=""'; |
| 218 |
$newDataSrc = ' data-src="' . set_url_scheme($src) . '"'; |
| 219 |
$attributes = preg_replace('(\sdata-src=["|\'](.*?)["|\'])', $newDataSrc, $attributes); |
| 220 |
} else { |
| 221 |
$urlSchemeSrc = set_url_scheme($src); |
| 222 |
$newSrc .= ' src="' . $urlSchemeSrc . '"'; |
| 223 |
} |
| 224 |
$attributes = preg_replace('(\ssrc=["|\'](.*?)["|\'])', $newSrc, $attributes); |
| 225 |
$attributes = preg_replace('(data-default-image=["|\'](true|false)["|\'])', '', $attributes); |
| 226 |
if (!$dynamic) { |
| 227 |
$attributes = str_replace(['data-height', 'data-width'], ['height', 'width'], $attributes); |
| 228 |
} |
| 229 |
|
| 230 |
return $attributes; |
| 231 |
} |
| 232 |
|
| 233 |
/** |
| 234 |
* Calculate aspect ratio |
| 235 |
* |
| 236 |
* @param int $width |
| 237 |
* @param int $height |
| 238 |
* |
| 239 |
* @return float |
| 240 |
*/ |
| 241 |
public function getAspectRatio($width, $height) |
| 242 |
{ |
| 243 |
return $width / $height; |
| 244 |
} |
| 245 |
|
| 246 |
/** |
| 247 |
* Get base URL |
| 248 |
* |
| 249 |
* @return string |
| 250 |
*/ |
| 251 |
public function getBaseUrl() |
| 252 |
{ |
| 253 |
$uploadDir = wp_get_upload_dir(); |
| 254 |
$baseUrl = trailingslashit($uploadDir['baseurl']); |
| 255 |
|
| 256 |
/* |
| 257 |
* If currently on HTTPS, prefer HTTPS URLs when we know they're supported by the domain |
| 258 |
* (which is to say, when they share the domain name of the current request). |
| 259 |
*/ |
| 260 |
if ( |
| 261 |
is_ssl() |
| 262 |
&& strpos($baseUrl, 'https') !== 0 |
| 263 |
&& wp_parse_url($baseUrl, PHP_URL_HOST) === $_SERVER['HTTP_HOST'] |
| 264 |
) { |
| 265 |
$baseUrl = set_url_scheme($baseUrl, 'https'); |
| 266 |
} |
| 267 |
|
| 268 |
return $baseUrl; |
| 269 |
} |
| 270 |
|
| 271 |
/** |
| 272 |
* Get sizes |
| 273 |
* |
| 274 |
* @param array $imageData Image data. Keys "width" and "originalWidth" are required. |
| 275 |
* |
| 276 |
* @return array |
| 277 |
*/ |
| 278 |
public function getSizes(array $imageData) |
| 279 |
{ |
| 280 |
if (empty($imageData['originalWidth']) || empty($imageData['width'])) { |
| 281 |
return []; |
| 282 |
} |
| 283 |
|
| 284 |
$originalWidth = $imageData['originalWidth']; |
| 285 |
$resizedWidth = $imageData['width']; |
| 286 |
|
| 287 |
// Pre-defined sizes |
| 288 |
$sizes = [ |
| 289 |
'320w' => 320, |
| 290 |
'480w' => 480, |
| 291 |
'800w' => 800, |
| 292 |
]; |
| 293 |
|
| 294 |
// Calculate a size, based on a $width that come from the editor |
| 295 |
$sizes[ $resizedWidth . 'w' ] = (int)$resizedWidth; |
| 296 |
|
| 297 |
// Calculate retina size |
| 298 |
if ($resizedWidth * 2 <= $originalWidth) { |
| 299 |
$retinaImage = $resizedWidth * 2; |
| 300 |
$sizes['2x'] = (int)$retinaImage; |
| 301 |
} |
| 302 |
|
| 303 |
return $sizes; |
| 304 |
} |
| 305 |
|
| 306 |
/** |
| 307 |
* Get images by provided list of sizes |
| 308 |
* |
| 309 |
* If an attachment id is provided this method first checks images |
| 310 |
* already resized by WordPress. |
| 311 |
* |
| 312 |
* Performs resize for the rest of the images (or all if an attachment ID |
| 313 |
* is not provided or required sizes are not found in a list of WordPress |
| 314 |
* images). |
| 315 |
* |
| 316 |
* Returns a list of resized image, where key is a width attribute |
| 317 |
* (for srcset) and value is a relative path to image, for example: |
| 318 |
* |
| 319 |
* ``` |
| 320 |
* [ |
| 321 |
* '320w' => '2022/06/Portfolio-4-320x213.jpg', |
| 322 |
* '480w' => '2022/06/Portfolio-4-480x320.jpg', |
| 323 |
* '2x' => '2022/06/Portfolio-4-2048x1366.jpg', |
| 324 |
* ] |
| 325 |
* ``` |
| 326 |
* |
| 327 |
* @param array $sizes A list of sizes in format [320w => 320, ...] |
| 328 |
* @param \WP_Image_Editor $image Image |
| 329 |
* @param array $imageData Image data (width, height, attachmentId, etc) |
| 330 |
* |
| 331 |
* @return array |
| 332 |
* |
| 333 |
*/ |
| 334 |
public function getImages($sizes, $image, $imageData) |
| 335 |
{ |
| 336 |
if (empty($sizes)) { |
| 337 |
return []; |
| 338 |
} |
| 339 |
|
| 340 |
// TODO: maybe check for cached images |
| 341 |
|
| 342 |
if (!empty($imageData['attachmentId'])) { |
| 343 |
$images = $this->getImagesFromAttachmentSizes($sizes, $imageData['attachmentId']); |
| 344 |
|
| 345 |
// Make sure we have all sizes |
| 346 |
$restOfSizes = array_diff_key($sizes, $images); |
| 347 |
if (!empty($restOfSizes)) { |
| 348 |
$resizedImages = $this->resizeImageBySizes($restOfSizes, $image, $imageData); |
| 349 |
$images = array_merge($images, $resizedImages); |
| 350 |
} |
| 351 |
} else { |
| 352 |
// Resize all images |
| 353 |
$images = $this->resizeImageBySizes($sizes, $image, $imageData); |
| 354 |
} |
| 355 |
|
| 356 |
// TODO: this is a good place to cache generated images |
| 357 |
|
| 358 |
// Images should be with a relative path without leading slash, |
| 359 |
// e.g. "2022/06/Image.jpg", so prepend them with a base URL |
| 360 |
$baseUrl = $this->getBaseUrl(); |
| 361 |
foreach ($images as &$relativePathToImage) { |
| 362 |
$relativePathToImage = $baseUrl . $relativePathToImage; |
| 363 |
} |
| 364 |
unset($relativePathToImage); |
| 365 |
|
| 366 |
return $images; |
| 367 |
} |
| 368 |
|
| 369 |
/** |
| 370 |
* Get images from attachment sizes |
| 371 |
* |
| 372 |
* Try to look through a list of already resized images. |
| 373 |
* There may already be what we are looking for. |
| 374 |
* |
| 375 |
* @param array $sizes A list of sizes to search |
| 376 |
* @param int $attachmentId Attachment ID |
| 377 |
* |
| 378 |
* @return array |
| 379 |
*/ |
| 380 |
public function getImagesFromAttachmentSizes($sizes, $attachmentId) |
| 381 |
{ |
| 382 |
$attachmentMeta = wp_get_attachment_metadata($attachmentId); |
| 383 |
if (empty($attachmentMeta) || empty($attachmentMeta['sizes'])) { |
| 384 |
return []; |
| 385 |
} |
| 386 |
|
| 387 |
$dirname = _wp_get_attachment_relative_path($attachmentMeta['file']); |
| 388 |
if ($dirname) { |
| 389 |
$dirname = trailingslashit($dirname); |
| 390 |
} |
| 391 |
|
| 392 |
$attachmentSizes = $attachmentMeta['sizes']; |
| 393 |
$images = []; |
| 394 |
foreach ($attachmentSizes as $image) { |
| 395 |
// Check if image meta isn't corrupted. |
| 396 |
if (!is_array($image)) { |
| 397 |
continue; |
| 398 |
} |
| 399 |
|
| 400 |
// Loop through the list of already resized images and try to find |
| 401 |
// a required "width". Once found - we can use it! |
| 402 |
if (in_array($image['width'], $sizes, true)) { |
| 403 |
$key = array_search($image['width'], $sizes, true); |
| 404 |
$images[ $key ] = $dirname . $image['file']; |
| 405 |
} |
| 406 |
} |
| 407 |
|
| 408 |
return $images; |
| 409 |
} |
| 410 |
|
| 411 |
/** |
| 412 |
* Resize a single image by provided list of sizes |
| 413 |
* |
| 414 |
* @param array $sizes A list of sizes in format [320w => 320] |
| 415 |
* @param \WP_Image_Editor $image Image |
| 416 |
* @param array $imageData Image data (width, height, attachmentId, etc) |
| 417 |
* |
| 418 |
* @return array |
| 419 |
*/ |
| 420 |
public function resizeImageBySizes($sizes, $image, $imageData) |
| 421 |
{ |
| 422 |
$images = []; |
| 423 |
$uploadDir = wp_upload_dir(); |
| 424 |
|
| 425 |
$dirname = $uploadDir['subdir']; |
| 426 |
if ($dirname) { |
| 427 |
$dirname = trailingslashit($dirname); |
| 428 |
} |
| 429 |
|
| 430 |
$aspectRatio = $this->getAspectRatio($imageData['width'], $imageData['height']); |
| 431 |
$retinaImage = array_key_exists('2x', $sizes); |
| 432 |
$fileHelper = vchelper('File'); |
| 433 |
foreach ($sizes as $widthAttr => $width) { |
| 434 |
if ($width > $imageData['width'] && !$retinaImage) { |
| 435 |
continue; |
| 436 |
} |
| 437 |
|
| 438 |
$height = (int)round($width / $aspectRatio); |
| 439 |
$filename = "{$imageData['filename']}-{$width}x{$height}.{$imageData['extension']}"; |
| 440 |
$absolutePath = "{$uploadDir['path']}/{$filename}"; |
| 441 |
|
| 442 |
// Check if file already exists. No need to resize twice. |
| 443 |
if (!$fileHelper->exists($absolutePath)) { |
| 444 |
$resizedImage = $this->resizeImage($image, $imageData, $absolutePath, $width, $height, true); |
| 445 |
if (is_wp_error($resizedImage)) { |
| 446 |
continue; |
| 447 |
} |
| 448 |
|
| 449 |
$filename = $resizedImage['file']; |
| 450 |
} |
| 451 |
|
| 452 |
// For consistency with "getImagesFromAttachmentSizes" method |
| 453 |
// return the relative path to the filename. |
| 454 |
$images[ $widthAttr ] = ltrim($dirname . $filename, '/\\'); |
| 455 |
} |
| 456 |
|
| 457 |
return $images; |
| 458 |
} |
| 459 |
|
| 460 |
/** |
| 461 |
* Resize and save image |
| 462 |
* |
| 463 |
* @param \WP_Image_Editor $image Image editor object. |
| 464 |
* @param array $imageData Image data. |
| 465 |
* @param string $path Absolute path where to save an image. |
| 466 |
* @param int $width |
| 467 |
* @param int $height |
| 468 |
* @param bool $crop |
| 469 |
* |
| 470 |
* @return array|\WP_Error |
| 471 |
*/ |
| 472 |
public function resizeImage($image, $imageData, $path, $width = null, $height = null, $crop = false) |
| 473 |
{ |
| 474 |
// For GD need to create a new object, because GD has a reference to a resource. |
| 475 |
// In case of ImageMagick it is possible to clone the object. |
| 476 |
if ($image instanceof \WP_Image_Editor_GD) { |
| 477 |
$newImage = wp_get_image_editor($imageData['path']); |
| 478 |
} else { |
| 479 |
$newImage = clone $image; |
| 480 |
} |
| 481 |
|
| 482 |
$newImage->resize($width, $height, $crop); |
| 483 |
|
| 484 |
return $newImage->save($path); |
| 485 |
} |
| 486 |
} |
| 487 |
|