基于 PHP-GD 实现的单文件图片缩略图 API

技术 发布于 10 个月前

基于 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)

发布评论

相关文章