set_time_limit(0);
ini_set('memory_limit', '1024M');
$baseDir = __DIR__ . '/har_tasks';
if (!is_dir($baseDir)) {
mkdir($baseDir, 0777, true);
}
function json_response($data) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function safe_task_id($id) {
return preg_replace('/^a-zA-Z0-9_-/', '', $id);
}
function task_path($taskId) {
global $baseDir;
return $baseDir . '/' . safe_task_id($taskId);
}
function progress_file($taskId) {
return task_path($taskId) . '/progress.json';
}
function save_progress($taskId, $data) {
file_put_contents(progress_file($taskId), json_encode($data, JSON_UNESCAPED_UNICODE
JSON_PRETTY_PRINT));
}
function read_progress($taskId) {
$file = progress_file($taskId);
if (!file_exists($file)) return null;
return json_decode(file_get_contents($file), true);
}
function extract_svg_links_from_har($file) {
$json = file_get_contents($file);
$data = json_decode($json, true);
if (!$data
empty($data'log''entries')) {
return [];
}
$links = [];
foreach ($data'log''entries' as $entry) {
$url = $entry'request''url' ?? '';
if (!$url) continue;
$path = parse_url($url, PHP_URL_PATH);
if (
strpos($url, 'https://cdn.file.mixinnet.cn/icon') === 0 &&
stripos($path, '.svg') !== false
) {
$links[] = $url;
}
}
return array_values(array_unique($links));
}
function curl_download($url, $savePath, $maxRetries = 3) {
$retryCount = 0;
while ($retryCount < $maxRetries) {
$fp = fopen($savePath, 'w');
if (!$fp) {
return false, '无法创建文件';
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36',
CURLOPT_HTTPHEADER => [
'Accept: image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language: zh-CN,zh;q=0.9',
'Referer: https://sc.mixinnet.cn/',
],
]);
$ok = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
fclose($fp);
if ($ok && $code < 400 && file_exists($savePath) && filesize($savePath) > 0) {
// 验证 SVG 内容
$content = file_get_contents($savePath, false, null, 0, 1000);
if (stripos($content, '
return true, null;
} else {
@unlink($savePath);
return false, '下载的内容不是有效的 SVG 格式';
}
}
@unlink($savePath);
$retryCount++;
if ($retryCount < $maxRetries) {
usleep(500000); // 等待 0.5 秒后重试
}
}
return false, "下载失败,已重试 {$maxRetries} 次。HTTP: {$code}, Error: {$err}";
}
// 方法1: 使用 rsvg-convert (推荐)
function svg_to_png_rsvg($svgPath, $pngPath, $size = 256) {
// 检查 rsvg-convert 是否可用
$checkCmd = "command -v rsvg-convert";
exec($checkCmd, $checkOutput, $checkRet);
if ($checkRet !== 0) {
throw new Exception('rsvg-convert 未安装');
}
// 使用 rsvg-convert 转换
$cmd = sprintf(
'rsvg-convert -w %d -h %d --keep-aspect-ratio --background-color=transparent %s -o %s 2>&1',
intval($size),
intval($size),
escapeshellarg($svgPath),
escapeshellarg($pngPath)
);
exec($cmd, $output, $ret);
if ($ret !== 0
!file_exists($pngPath)
filesize($pngPath) <= 0) {
$errorMsg = implode("n", $output);
throw new Exception("rsvg-convert 转换失败: {$errorMsg}");
}
// 验证 PNG 文件
$pngContent = file_get_contents($pngPath, false, null, 0, 100);
if (strpos($pngContent, 'PNG') === false) {
throw new Exception('生成的文件不是有效的 PNG 格式');
}
return true;
}
// 方法2: 使用 ImageMagick
function svg_to_png_imagick($svgPath, $pngPath, $size = 256) {
if (!extension_loaded('imagick')) {
throw new Exception('ImageMagick 扩展未加载');
}
try {
$imagick = new Imagick();
$imagick->setResolution(300, 300);
$imagick->readImage($svgPath);
$imagick->setImageFormat('png');
$imagick->resizeImage($size, $size, Imagick::FILTER_LANCZOS, 1);
$imagick->writeImage($pngPath);
$imagick->clear();
if (!file_exists($pngPath)
filesize($pngPath) <= 0) {
throw new Exception('ImageMagick 生成 PNG 失败');
}
return true;
} catch (Exception $e) {
throw new Exception('ImageMagick 转换失败: ' . $e->getMessage());
}
}
// 方法3: 使用 GD + 外部工具(备选)
function svg_to_png_gd($svgPath, $pngPath, $size = 256) {
// 创建一个临时 PNG 文件
$tempPng = dirname($pngPath) . '/temp_' . uniqid() . '.png';
// 尝试使用 rsvg-convert
$cmd = sprintf(
'rsvg-convert -w %d -h %d %s -o %s 2>&1',
intval($size),
intval($size),
escapeshellarg($svgPath),
escapeshellarg($tempPng)
);
exec($cmd, $output, $ret);
if ($ret === 0 && file_exists($tempPng) && filesize($tempPng) > 0) {
rename($tempPng, $pngPath);
return true;
}
if (file_exists($tempPng)) {
@unlink($tempPng);
}
throw new Exception('GD 转换失败,请安装 rsvg-convert 或 ImageMagick');
}
// 主转换函数(自动选择最佳方法)
function svg_to_png_hd($svgPath, $pngPath, $size = 256) {
$errors = [];
// 尝试方法1: rsvg-convert
try {
return svg_to_png_rsvg($svgPath, $pngPath, $size);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
// 尝试方法2: ImageMagick
try {
return svg_to_png_imagick($svgPath, $pngPath, $size);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
// 尝试方法3: GD (备选)
try {
return svg_to_png_gd($svgPath, $pngPath, $size);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
// 所有方法都失败
throw new Exception('所有转换方法都失败了: ' . implode('; ', $errors));
}
function clean_filename_from_url($url, $index) {
$path = parse_url($url, PHP_URL_PATH);
$name = basename($path);
$name = preg_replace('/.svg$/i', '', $name);
$name = preg_replace('/^a-zA-Z0-9_-/', '_', $name);
if ($name === '') {
$name = 'svg_' . str_pad((string)$index, 4, '0', STR_PAD_LEFT);
}
return str_pad((string)$index, 4, '0', STR_PAD_LEFT) . '_' . $name;
}
function process_task($taskId, $pngSize) {
$dir = task_path($taskId);
$harFile = $dir . '/input.har';
$svgDir = $dir . '/svg';
$pngDir = $dir . '/png';
$zipFile = $dir . '/result.zip';
if (!is_dir($svgDir)) mkdir($svgDir, 0777, true);
if (!is_dir($pngDir)) mkdir($pngDir, 0777, true);
$links = extract_svg_links_from_har($harFile);
$total = count($links);
if ($total === 0) {
save_progress($taskId, [
'status' => 'error',
'message' => 'HAR 文件中未找到 https://cdn.file.mixinnet.cn/icon 下的 SVG 链接',
'total' => 0,
'downloaded' => 0,
'converted' => 0,
'failed' => 0,
'zip_ready' => false,
]);
return;
}
$progress = [
'status' => 'running',
'message' => '开始下载 SVG',
'total' => $total,
'downloaded' => 0,
'converted' => 0,
'failed' => 0,
'zip_ready' => false,
'download_url' => '',
];
save_progress($taskId, $progress);
$downloadedFiles = [];
foreach ($links as $i => $url) {
$index = $i + 1;
$name = clean_filename_from_url($url, $index);
$svgPath = $svgDir . '/' . $name . '.svg';
$ok, $err = curl_download($url, $svgPath);
if (!$ok) {
$progress'failed'++;
$progress'message' = "下载失败:{$url} - {$err}";
save_progress($taskId, $progress);
continue;
}
$downloadedFiles[] = [
'name' => $name,
'svg' => $svgPath,
'url' => $url,
];
$progress'downloaded'++;
$progress'message' = "正在下载 SVG:{$progress'downloaded'} / {$total}";
save_progress($taskId, $progress);
}
$progress'message' = '开始高清转换 PNG (256x256)';
save_progress($taskId, $progress);
foreach ($downloadedFiles as $item) {
try {
$pngPath = $pngDir . '/' . $item'name' . '.png';
svg_to_png_hd($item'svg', $pngPath, 256); // 固定使用 256x256
$progress'converted'++;
$progress'message' = "正在转换 PNG:{$progress'converted'} / " . count($downloadedFiles);
save_progress($taskId, $progress);
} catch (Throwable $e) {
$progress'failed'++;
$progress'message' = '转换失败:' . $e->getMessage();
save_progress($taskId, $progress);
// 记录详细错误日志
$errorLog = $dir . '/error.log';
$logEntry = date('Y-m-d H:i:s') . " - {$item'name'}: " . $e->getMessage() . "n";
file_put_contents($errorLog, $logEntry, FILE_APPEND);
}
}
$progress'message' = '正在打包 ZIP';
save_progress($taskId, $progress);
$zip = new ZipArchive();
if ($zip->open($zipFile, ZipArchive::CREATE
ZipArchive::OVERWRITE) !== true) {
$progress'status' = 'error';
$progress'message' = '创建 ZIP 失败';
save_progress($taskId, $progress);
return;
}
// 添加 PNG 文件
$pngFiles = glob($pngDir . '/*.png');
if (count($pngFiles) > 0) {
foreach ($pngFiles as $file) {
$zip->addFile($file, 'png/' . basename($file));
}
}
// 添加 SVG 文件
$svgFiles = glob($svgDir . '/*.svg');
if (count($svgFiles) > 0) {
foreach ($svgFiles as $file) {
$zip->addFile($file, 'svg/' . basename($file));
}
}
$info = "总 SVG 链接数:{$total}n";
$info .= "下载成功:{$progress'downloaded'}n";
$info .= "转换成功:{$progress'converted'}n";
$info .= "失败数量:{$progress'failed'}n";
$info .= "PNG 输出尺寸:256x256n";
$info .= "转换时间:" . date('Y-m-d H:i:s') . "nn";
$info .= "SVG 链接列表:n" . implode("n", $links) . "n";
$zip->addFromString('readme.txt', $info);
$zip->close();
$progress'status' = 'done';
$progress'message' = '处理完成,共 ' . $progress'converted' . ' 个 PNG 文件';
$progress'zip_ready' = true;
$progress'download_url' = '?action=download&task_id=' . urlencode($taskId);
save_progress($taskId, $progress);
}
$action = $_GET'action' ?? '';
if ($action === 'upload') {
if (empty($_FILES'har''tmp_name')) {
json_response('success' => false, 'message' => '请选择 HAR 文件');
}
$taskId = date('YmdHis') . '_' . mt_rand(1000, 9999);
$dir = task_path($taskId);
mkdir($dir, 0777, true);
move_uploaded_file($_FILES'har''tmp_name', $dir . '/input.har');
save_progress($taskId, [
'status' => 'queued',
'message' => 'HAR 上传成功,等待处理',
'total' => 0,
'downloaded' => 0,
'converted' => 0,
'failed' => 0,
'zip_ready' => false,
]);
json_response('success' => true, 'task_id' => $taskId);
}
if ($action === 'process') {
$taskId = $_POST'task_id' ?? '';
$dir = task_path($taskId);
if (!$taskId
!is_dir($dir)) {
json_response('success' => false, 'message' => '任务不存在');
}
process_task($taskId, 256);
json_response('success' => true);
}
if ($action === 'progress') {
$taskId = $_GET'task_id' ?? '';
$progress = read_progress($taskId);
if (!$progress) {
json_response('success' => false, 'message' => '进度不存在');
}
$progress'success' = true;
json_response($progress);
}
if ($action === 'check_deps') {
$deps = [
'rsvg_convert' => false,
'imagick' => false,
'gd' => false,
];
// 检查 rsvg-convert
exec("command -v rsvg-convert", $output, $ret);
$deps'rsvg_convert' = ($ret === 0);
// 检查 imagick
$deps'imagick' = extension_loaded('imagick');
// 检查 gd
$deps'gd' = extension_loaded('gd');
json_response('success' => true, 'dependencies' => $deps);
}
if ($action === 'download') {
$taskId = $_GET'task_id' ?? '';
$zipFile = task_path($taskId) . '/result.zip';
if (!file_exists($zipFile)) {
die('ZIP 文件不存在');
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="svg_png_' . safe_task_id($taskId) . '.zip"');
header('Content-Length: ' . filesize($zipFile));
readfile($zipFile);
exit;
}
?>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 800px;
max-width: 100%;
background: #fff;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
margin: 0 0 10px 0;
text-align: center;
font-size: 28px;
color: #333;
}
.desc {
text-align: center;
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
.upload-box {
border: 2px dashed #667eea;
background: #f8f9ff;
border-radius: 16px;
padding: 40px 20px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
}
.upload-box.dragover {
background: #eef2ff;
border-color: #764ba2;
}
.upload-icon {
font-size: 48px;
margin-bottom: 10px;
}
.upload-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 10px;
padding: 12px 30px;
color: white;
font-size: 16px;
cursor: pointer;
margin-top: 10px;
transition: transform 0.2s;
}
.upload-btn:hover {
transform: translateY(-2px);
}
.file-name {
margin-top: 15px;
color: #666;
font-size: 14px;
}
.start-btn {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 10px;
padding: 14px;
color: white;
font-size: 16px;
font-weight: bold;
cursor: pointer;
margin-top: 20px;
transition: transform 0.2s;
}
.start-btn:hover:not(:disabled) {
transform: translateY(-2px);
}
.start-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress-area {
display: none;
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
}
.status-text {
color: #333;
margin-bottom: 15px;
font-size: 14px;
font-weight: bold;
}
.progress-row {
margin-bottom: 15px;
}
.progress-label {
display: flex;
justify-content: space-between;
color: #666;
margin-bottom: 5px;
font-size: 13px;
}
.progress-bar {
height: 8px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.progress-inner {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s ease;
}
.progress-inner.convert {
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 20px;
}
.stat {
background: white;
border-radius: 10px;
padding: 12px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat strong {
display: block;
color: #333;
font-size: 24px;
}
.stat span {
color: #999;
font-size: 12px;
}
.download-btn {
display: none;
width: 100%;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border: none;
border-radius: 10px;
padding: 14px;
color: white;
font-size: 16px;
font-weight: bold;
text-decoration: none;
text-align: center;
margin-top: 20px;
transition: transform 0.2s;
}
.download-btn:hover {
transform: translateY(-2px);
}
.deps-check {
margin-top: 20px;
padding: 15px;
background: #fef3c7;
border-radius: 10px;
font-size: 13px;
}
.deps-check h4 {
margin: 0 0 10px 0;
color: #92400e;
}
.deps-check ul {
margin: 5px 0;
padding-left: 20px;
}
.deps-check li {
color: #78350f;
margin: 5px 0;
}
.deps-check .ok {
color: #10b981;
}
.deps-check .error {
color: #ef4444;
}
.tips {
margin-top: 20px;
padding: 15px;
background: #e0e7ff;
border-radius: 10px;
font-size: 13px;
line-height: 1.6;
}
🎨 HAR 提取 SVG → PNG
上传 HAR 文件,自动提取 SVG 图标并转换为 256x256 PNG 高清图片
📥 下载进度
0%
🖼️ 转换进度
0%
🔧 系统依赖检查
💡 提示:
• 转换后的 PNG 尺寸固定为 256x256 像素
• ZIP 包含原始 SVG 和转换后的 PNG 文件
• 支持 rsvg-convert、ImageMagick 多种转换方式
let selectedFile = null;
let taskId = null;
let polling = null;
const fileInput = document.getElementById('harFile');
const fileName = document.getElementById('fileName');
const startBtn = document.getElementById('startBtn');
const progressArea = document.getElementById('progressArea');
const statusText = document.getElementById('statusText');
const dropBox = document.getElementById('dropBox');
// 检查依赖
async function checkDependencies() {
try {
const res = await fetch('?action=check_deps');
const data = await res.json();
if (data.success) {
const deps = data.dependencies;
let html = '
- ';
html += </p><li>${deps.rsvg_convert ? '✅' : '❌'} rsvg-convert ${deps.rsvg_convert ? '(已安装)' : '(未安装 - 推荐安装)'}</li>;
html += </p><li>${deps.imagick ? '✅' : '⚠️'} ImageMagick ${deps.imagick ? '(已安装)' : '(未安装 - 可选)'}</li>;
html += </p><li>${deps.gd ? '✅' : '⚠️'} GD ${deps.gd ? '(已安装)' : '(未安装 - 可选)'}</li>;
html += '';
if (!deps.rsvg_convert) {
html += '
⚠️ 建议安装 rsvg-convert 以获得最佳转换效果:
';html += '
Ubuntu/Debian: sudo apt-get install librsvg2-bin
';
html += 'CentOS/RHEL: sudo yum install librsvg2-tools
';} else {
html += '
✅ 系统配置正常,可以开始转换
';}
document.getElementById('depsStatus').innerHTML = html;
}
} catch (e) {
document.getElementById('depsStatus').innerHTML = '
无法检查依赖状态
';}
}
fileInput.addEventListener('change', function () {
selectedFile = this.files0;
if (selectedFile) {
fileName.textContent = selectedFile.name;
startBtn.disabled = false;
}
});
dropBox.addEventListener('dragover', function (e) {
e.preventDefault();
dropBox.classList.add('dragover');
});
dropBox.addEventListener('dragleave', function () {
dropBox.classList.remove('dragover');
});
dropBox.addEventListener('drop', function (e) {
e.preventDefault();
dropBox.classList.remove('dragover');
selectedFile = e.dataTransfer.files0;
if (selectedFile && selectedFile.name.endsWith('.har')) {
fileName.textContent = selectedFile.name;
startBtn.disabled = false;
} else {
alert('请上传 .har 格式的文件');
}
});
startBtn.addEventListener('click', async function () {
if (!selectedFile) return;
startBtn.disabled = true;
progressArea.style.display = 'block';
statusText.textContent = '📤 正在上传 HAR 文件...';
const formData = new FormData();
formData.append('har', selectedFile);
try {
const uploadRes = await fetch('?action=upload', {
method: 'POST',
body: formData
});
const uploadData = await uploadRes.json();
if (!uploadData.success) {
alert(uploadData.message
'上传失败');
startBtn.disabled = false;
return;
}
taskId = uploadData.task_id;
// 开始轮询进度
if (polling) clearInterval(polling);
polling = setInterval(loadProgress, 1000);
// 启动后台处理
const processRes = await fetch('?action=process', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'task_id=' + encodeURIComponent(taskId)
});
} catch (error) {
alert('上传失败: ' + error.message);
startBtn.disabled = false;
}
});
async function loadProgress() {
if (!taskId) return;
try {
const res = await fetch('?action=progress&task_id=' + encodeURIComponent(taskId));
const data = await res.json();
if (!data.success) return;
statusText.textContent = data.message
'处理中...';
const total = data.total
0;
const downloaded = data.downloaded
0;
const converted = data.converted
0;
const failed = data.failed
0;
const downloadPercent = total > 0 ? Math.round(downloaded / total * 100) : 0;
const convertPercent = downloaded > 0 ? Math.round(converted / downloaded * 100) : 0;
document.getElementById('downloadBar').style.width = downloadPercent + '%';
document.getElementById('convertBar').style.width = convertPercent + '%';
document.getElementById('downloadPercent').textContent = downloadPercent + '%';
document.getElementById('convertPercent').textContent = convertPercent + '%';
document.getElementById('totalNum').textContent = total;
document.getElementById('downloadNum').textContent = downloaded;
document.getElementById('convertNum').textContent = converted;
document.getElementById('failNum').textContent = failed;
if (data.status === 'done') {
clearInterval(polling);
const btn = document.getElementById('downloadBtn');
btn.style.display = 'block';
btn.href = data.download_url;
statusText.textContent = '✅ ' + data.message;
}
if (data.status === 'error') {
clearInterval(polling);
alert('处理失败: ' + data.message);
startBtn.disabled = false;
}
} catch (error) {
console.error('获取进度失败:', error);
}
}
// 页面加载时检查依赖
checkDependencies();
