Files
XC_VM/src/Core/Util/ImageUtils.php
T

209 lines
7.6 KiB
PHP
Raw Normal View History

<?php
use XcVm\Core\Config\SettingsManager;
/**
* ImageUtils — image utils
*
* @package XC_VM_Core_Util
* @author Divarion_D <https://github.com/Divarion-D>
* @copyright 2025-2026 Vateron Media
* @link https://github.com/Vateron-Media/XC_VM
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
class ImageUtils {
/**
* Resolve an image URL, expanding internal `s:<serverId>:` references.
*
* @param string $rURL Image URL or internal `s:` reference.
* @param string|null $rForceProtocol Force http/https when resolving server URLs.
* @return string Resolved public URL ('' if the server cannot be resolved).
*/
2026-03-07 21:53:45 +03:00
public static function validateURL($rURL, $rForceProtocol = null) {
if (substr($rURL, 0, 2) == 's:') {
$rSplit = explode(':', $rURL, 3);
2026-03-07 21:53:45 +03:00
$rServerURL = ServerRepository::getPublicURL(intval($rSplit[1]), $rForceProtocol);
if ($rServerURL) {
return $rServerURL . 'images/' . basename($rURL);
}
return '';
}
return $rURL;
}
/**
* Return the URL of a cached resized image, or the validated original.
*
* @param string $rURL Source image URL.
* @param int $rMaxW Max width.
* @param int $rMaxH Max height.
* @return string Public URL of the resized image, or the validated source.
*/
public static function resize($rURL, $rMaxW, $rMaxH) {
list($rExtension) = explode('.', strtolower(pathinfo($rURL)['extension']));
$rImagePath = IMAGES_PATH . 'admin/' . md5($rURL) . '_' . $rMaxW . '_' . $rMaxH . '.' . $rExtension;
if (file_exists($rImagePath)) {
2026-03-07 21:53:45 +03:00
$rServerInfo = ServerRepository::getAll()[SERVER_ID];
$rDomain = (empty($rServerInfo['domain_name']) ? $rServerInfo['server_ip'] : explode(',', $rServerInfo['domain_name'])[0]);
2026-03-07 21:53:45 +03:00
return $rServerInfo['server_protocol'] . '://' . $rDomain . ':' . $rServerInfo['request_port'] . '/images/admin/' . md5($rURL) . '_' . $rMaxW . '_' . $rMaxH . '.' . $rExtension;
}
2026-03-07 21:53:45 +03:00
return self::validateURL($rURL);
}
/**
* Generate and cache a thumbnail sized for the given stream type.
*
* @param string $rImage Source image path/URL.
* @param int $rType Stream type controlling target dimensions.
* @return bool True if the thumbnail exists/was created, false otherwise.
*/
public static function generateThumbnail($rImage, $rType) {
if ($rType == 1 || $rType == 5 || $rType == 4) {
$rMaxW = 96;
$rMaxH = 32;
} else {
if ($rType == 2) {
$rMaxW = 58;
$rMaxH = 32;
} else {
if ($rType == 5) {
$rMaxW = 32;
$rMaxH = 64;
} else {
return false;
}
}
}
list($rExtension) = explode('.', strtolower(pathinfo($rImage)['extension']));
if (!in_array($rExtension, array('png', 'jpg', 'jpeg'))) {
} else {
$rImagePath = IMAGES_PATH . 'admin/' . md5($rImage) . '_' . $rMaxW . '_' . $rMaxH . '.' . $rExtension;
if (file_exists($rImagePath)) {
} else {
2026-03-07 21:53:45 +03:00
if (self::isAbsoluteUrl($rImage)) {
$rActURL = $rImage;
} else {
$rActURL = IMAGES_PATH . basename($rImage);
}
list($rWidth, $rHeight) = getimagesize($rActURL);
2026-03-07 21:53:45 +03:00
$rImageSize = self::getImageSizeKeepAspectRatio($rWidth, $rHeight, $rMaxW, $rMaxH);
if (!($rImageSize['width'] && $rImageSize['height'])) {
} else {
$rImageP = imagecreatetruecolor($rImageSize['width'], $rImageSize['height']);
if ($rExtension == 'png') {
$rImage = imagecreatefrompng($rActURL);
} else {
$rImage = imagecreatefromjpeg($rActURL);
}
imagealphablending($rImageP, false);
imagesavealpha($rImageP, true);
imagecopyresampled($rImageP, $rImage, 0, 0, 0, 0, $rImageSize['width'], $rImageSize['height'], $rWidth, $rHeight);
imagepng($rImageP, $rImagePath);
}
}
if (!file_exists($rImagePath)) {
} else {
return true;
}
}
return false;
}
2026-03-07 21:53:45 +03:00
/**
* Download a remote image into the local image cache.
*
* Stores jpg/jpeg/png images and returns an internal `s:<serverId>:` reference;
* returns the original URL unchanged when not downloadable.
*
* @param string $rImage Remote image URL.
* @param int|null $rType Optional stream type (unused placeholder).
* @return string Internal `s:` reference, or the original URL.
*/
2026-03-07 21:53:45 +03:00
public static function downloadImage($rImage, $rType = null) {
if (0 < strlen($rImage) && substr(strtolower($rImage), 0, 4) == 'http') {
$rPathInfo = pathinfo(parse_url($rImage, PHP_URL_PATH) ?: $rImage);
$rExt = strtolower($rPathInfo['extension'] ?? '');
2026-03-07 21:53:45 +03:00
if (!$rExt) {
$rImageInfo = @getimagesize($rImage);
if (is_array($rImageInfo) && !empty($rImageInfo['mime']) && strpos($rImageInfo['mime'], '/') !== false) {
list(, $rExt) = explode('/', strtolower($rImageInfo['mime']), 2);
2026-03-07 21:53:45 +03:00
}
}
if (in_array(strtolower($rExt), array('jpg', 'jpeg', 'png'))) {
$rFilename = Encryption::encrypt($rImage, SettingsManager::getAll()['live_streaming_pass'], OPENSSL_EXTRA);
// A single filename component is capped at 255 bytes on ext4/most
// filesystems. Long source URLs produce an encrypted name that
// exceeds this, so file_put_contents fails with "File name too
// long" and the whole import (e.g. Plex Sync) stalls. Fall back to
// a deterministic hash for those — the tools-images self-heal can't
// reverse a hash, but the image still downloads and caches. The
// "h_" prefix marks these so the self-heal can skip them.
if (strlen($rFilename . '.' . $rExt) > 250) {
$rFilename = 'h_' . hash('sha256', $rImage);
}
2026-03-07 21:53:45 +03:00
$rPrevPath = IMAGES_PATH . $rFilename . '.' . $rExt;
if (file_exists($rPrevPath)) {
return 's:' . SERVER_ID . ':/images/' . $rFilename . '.' . $rExt;
}
$rCurl = curl_init();
curl_setopt($rCurl, CURLOPT_URL, $rImage);
curl_setopt($rCurl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($rCurl, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($rCurl, CURLOPT_TIMEOUT, 5);
$rData = curl_exec($rCurl);
if (strlen($rData) > 0) {
$rPath = IMAGES_PATH . $rFilename . '.' . $rExt;
file_put_contents($rPath, $rData);
if (file_exists($rPath)) {
return 's:' . SERVER_ID . ':/images/' . $rFilename . '.' . $rExt;
}
}
}
}
return $rImage;
}
/**
* Compute dimensions that fit within a bounding box, preserving aspect ratio.
*
* @param int $origWidth Original width.
* @param int $origHeight Original height.
* @param int $maxWidth Max width (0 = no limit).
* @param int $maxHeight Max height (0 = no limit).
* @return array ['width' => int, 'height' => int].
*/
2026-03-07 21:53:45 +03:00
public static function getImageSizeKeepAspectRatio($origWidth, $origHeight, $maxWidth, $maxHeight) {
if ($maxWidth == 0) {
$maxWidth = $origWidth;
}
if ($maxHeight == 0) {
$maxHeight = $origHeight;
}
$widthRatio = $maxWidth / (($origWidth ?: 1));
$heightRatio = $maxHeight / (($origHeight ?: 1));
$ratio = min($widthRatio, $heightRatio);
if ($ratio < 1) {
$newWidth = (int) $origWidth * $ratio;
$newHeight = (int) $origHeight * $ratio;
} else {
$newHeight = $origHeight;
$newWidth = $origWidth;
}
return array('height' => round($newHeight, 0), 'width' => round($newWidth, 0));
}
/**
* Determine whether a string is an absolute URL.
*
* @param string $rURL Candidate URL.
* @return bool True if absolute.
*/
2026-03-07 21:53:45 +03:00
public static function isAbsoluteUrl($rURL) {
$rPattern = "/^(?:ftp|https?|feed)?:?\\/\\/(?:(?:(?:[\\w\\.\\-\\+!\$&'\\(\\)*\\+,;=]|%[0-9a-f]{2})+:)*" . "\n" . " (?:[\\w\\.\\-\\+%!\$&'\\(\\)*\\+,;=]|%[0-9a-f]{2})+@)?(?:" . "\n" . ' (?:[a-z0-9\\-\\.]|%[0-9a-f]{2})+|(?:\\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\\]))(?::[0-9]+)?(?:[\\/|\\?]' . "\n" . " (?:[\\w#!:\\.\\?\\+\\|=&@\$'~*,;\\/\\(\\)\\[\\]\\-]|%[0-9a-f]{2})*)?\$/xi";
return (bool) preg_match($rPattern, $rURL);
}
}