基于 PHP 实现的缩略图 API 在 Github 上有现成的,但过于老旧,遂用 DeepSeek 写了一个,基于 PHP 的 GD 扩展实现,支持域名白名单,本地缓存,临时文件隔离,过期缓存文件清理,需要在配置中修改域名白名单及缓存文件存放目录。
考虑到所使用服务器存储空间有限,故加入了缓存清理机制,缓存逻辑:在调用 api 时,有 1% 的概率触发缓存清理流程,会自动清理 /cache/
目录下留存时间大于 30 天的文件,同时加入了容错机制,如果当前请求传入的图片链接被清除,则会重新生成。
<?php
// 配置部分
define('ALLOWED_DOMAINS', ['carefu.link', 'static.carefu.link']); // 允许的域名白名单
define('CACHE_DIR', __DIR__ . '/cache/'); // 缓存目录
define('TMP_DIR', __DIR__ . '/temp/'); // 临时文件目录
define('MAX_CACHE_AGE', 2592000); // 30天缓存有效期(秒)
define('CACHE_CLEAN_PROBABILITY', 1); // 1% 的清理概率
define('MAX_IMAGE_SIZE', 5242880); // 最大图片尺寸5MB
// 初始化目录
@mkdir(CACHE_DIR, 0755, true);
@mkdir(TMP_DIR, 0755, true);
try {
// 验证参数
$url = $_GET['url'] ?? null;
$width = isset($_GET['w']) ? intval($_GET['w']) : null;
$height = isset($_GET['h']) ? intval($_GET['h']) : null;
// 基础参数验证
if (!$url || !$width || !$height) {
throw new Exception('Missing parameters', 400);
}
// 验证尺寸参数
if ($width <= 0 || $height <= 0 || $width > 4096 || $height > 4096) {
throw new Exception('Invalid dimensions', 400);
}
// 验证URL合法性
$parsedUrl = parse_url($url);
if (!$parsedUrl || !isset($parsedUrl['host'])) {
throw new Exception('Invalid URL', 400);
}
// 域名白名单验证
if (!in_array($parsedUrl['host'], ALLOWED_DOMAINS)) {
throw new Exception('Domain not allowed', 403);
}
// 生成缓存文件名
$cacheKey = md5($url . $width . $height);
$extension = pathinfo($parsedUrl['path'], PATHINFO_EXTENSION);
$cacheFile = CACHE_DIR . $cacheKey . '.' . ($extension ?: 'jpg');
if (file_exists($cacheFile)) {
// 概率性触发缓存清理(不影响当前请求)
if (rand(1, 100) <= CACHE_CLEAN_PROBABILITY) {
cleanCache($cacheKey); // 修改清理函数避免删除当前文件
}
// 再次检查缓存文件是否存在
if (file_exists($cacheFile)) {
sendImage($cacheFile);
exit;
}
// 如果文件被清理,继续生成流程
}
// 下载远程文件到临时目录
$tmpFile = downloadImage($url);
// 生成缩略图
generateThumbnail($tmpFile, $cacheFile, $width, $height);
// 清理临时文件
@unlink($tmpFile);
// 发送生成的图片
sendImage($cacheFile);
} catch (Exception $e) {
http_response_code($e->getCode() ?: 500);
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
exit;
}
// 辅助函数
function downloadImage($url) {
$context = stream_context_create([
'http' => [
'timeout' => 15,
'header' => "User-Agent: ThumbnailGenerator/1.0\r\n"
]
]);
$data = file_get_contents($url, false, $context);
if (!$data) {
throw new Exception('Failed to download image', 500);
}
if (strlen($data) > MAX_IMAGE_SIZE) {
throw new Exception('Image too large', 413);
}
$tmpFile = tempnam(TMP_DIR, 'img_');
file_put_contents($tmpFile, $data);
return $tmpFile;
}
function generateThumbnail($srcPath, $destPath, $width, $height) {
list($srcWidth, $srcHeight, $type) = getimagesize($srcPath);
// 创建图像资源
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($srcPath);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($srcPath);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($srcPath);
break;
default:
throw new Exception('Unsupported image type', 415);
}
// 计算比例并进行居中裁剪
$ratio = max($width/$srcWidth, $height/$srcHeight);
$cropWidth = $width / $ratio;
$cropHeight = $height / $ratio;
$src_x = ($srcWidth - $cropWidth) / 2;
$src_y = ($srcHeight - $cropHeight) / 2;
$thumb = imagecreatetruecolor($width, $height);
// 处理透明背景
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF) {
imagecolortransparent($thumb, imagecolorallocatealpha($thumb, 0, 0, 0, 127));
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
}
imagecopyresampled(
$thumb, $image,
0, 0,
$src_x, $src_y,
$width, $height,
$cropWidth, $cropHeight
);
// 保存图像
imagejpeg($thumb, $destPath, 100);
imagedestroy($image);
imagedestroy($thumb);
}
function sendImage($path) {
if (!file_exists($path)) {
throw new Exception('Image not found', 404);
}
$mime = mime_content_type($path);
$lastModified = filemtime($path);
$etag = md5_file($path);
header('Content-Type: ' . $mime);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
header('ETag: ' . $etag);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + MAX_CACHE_AGE) . ' GMT');
readfile($path);
exit;
}
// 缓存清理函数
function cleanCache($excludeKey = null) {
$now = time();
foreach (glob(CACHE_DIR . '*') as $file) {
// 排除当前正在使用的缓存文件
if ($excludeKey && strpos($file, $excludeKey) !== false) {
continue;
}
if (is_file($file) && ($now - filemtime($file)) > MAX_CACHE_AGE) {
@unlink($file);
}
}
}
?>
调用格式:[api地址]?url=[图片地址]&w=[宽度]&h=[宽度]
,示例:
https://api.carefu.link/thumbnail.php?url=https://carefu.link/upload/images/thumbnail.jpg&w=840&h=420
评论(0)