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

基于PHP实现的缩略图API在Github上有现成的,但过于老旧,遂用DeepSeek写了一个,基于PHP的GD扩展实现,支持域名白名单,本地缓存,临时文件隔离,过期缓存文件清理,需要在配置中修改域名白名单及缓存文件存放目录。

考虑到所使用服务器存储空间有限,故加入了缓存清理机制,缓存逻辑:在调用api时,有1%的概率触发缓存清理流程,会自动清理/cache/目录下留存时间大于30天的文件,同时加入了容错机制,如果当前请求传入的图片链接被清除,则会重新生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?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=[宽度],示例:

1
https://api.carefu.link/thumbnail.php?url=https://carefu.link/upload/images/thumbnail.jpg&w=840&h=420

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

https://goldrun.fyi/posts/36866.html

作者

谨行

发布于

2026-02-04

更新于

2026-02-09

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.