<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SVG to PNG - canvg</title>
</head>
<body>
<div class="container">
<h1>🎨 SVG to PNG 转换器</h1>
<div class="subtitle">基于 canvg 库 | 支持自定义尺寸缩放</div>
<div class="input-section">
<label for="svgInput">📝 输入 SVG 代码:</label>
<textarea id="svgInput" placeholder="在此粘贴 SVG 代码..."><svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="300" height="300" fill="url(#grad1)" rx="20"/>
<circle cx="150" cy="150" r="80" fill="white" opacity="0.3"/>
<text x="150" y="160" font-size="28" font-weight="bold" text-anchor="middle" fill="white">SVG to PNG</text>
</svg></textarea>
</div>
<div class="size-controls">
<div class="input-group">
<label for="outputWidth">📏 输出宽度 (px):</label>
<input type="number" id="outputWidth" min="1" placeholder="留空使用原始宽度">
</div>
<div class="input-group">
<label for="outputHeight">📏 输出高度 (px):</label>
<input type="number" id="outputHeight" min="1" placeholder="留空使用原始高度">
</div>
<div class="input-group">
<div class="checkbox-group">
<input type="checkbox" id="keepRatio" checked>
<label for="keepRatio">🔒 保持宽高比</label>
</div>
</div>
</div>
<div class="button-group">
<button class="btn-render" id="renderBtn">
<span>🖼️</span>
<span>渲染预览</span>
</button>
<button class="btn-download" id="downloadBtn" disabled>
<span>⬇️</span>
<span>下载 PNG</span>
</button>
<button class="btn-clear" id="clearBtn">
<span>🗑️</span>
<span>清空</span>
</button>
</div>
<div class="output-section">
<label>🖼️ 输出预览:</label>
<div class="canvas-container" id="canvasContainer">
<p class="placeholder">等待渲染...</p>
</div>
<div id="info" class="info"></div>
</div>
</div>
<!-- 引入 canvg 库 -->
<script src="https://netnr.eu.org/canvg@3.0.11/lib/umd.js"></script>
<script>
let canvas = null;
async function renderSVG() {
const svgInput = document.getElementById('svgInput').value.trim();
const outputWidth = document.getElementById('outputWidth').value;
const outputHeight = document.getElementById('outputHeight').value;
const keepRatio = document.getElementById('keepRatio').checked;
const canvasContainer = document.getElementById('canvasContainer');
const info = document.getElementById('info');
const downloadBtn = document.getElementById('downloadBtn');
// 清除之前的内容
info.style.display = 'none';
if (!svgInput) {
showInfo('⚠️ 请输入 SVG 代码', 'error');
return;
}
try {
// 解析 SVG 获取原始尺寸
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgInput, 'image/svg+xml');
const svgElement = svgDoc.querySelector('svg');
if (!svgElement) {
throw new Error('无效的 SVG 代码');
}
// 检查解析错误
const parseError = svgDoc.querySelector('parsererror');
if (parseError) {
throw new Error('SVG 解析错误: ' + parseError.textContent);
}
// 获取原始尺寸
let originalWidth = parseFloat(svgElement.getAttribute('width')) || 0;
let originalHeight = parseFloat(svgElement.getAttribute('height')) || 0;
// 如果 SVG 没有指定宽高,尝试从 viewBox 获取
if ((!originalWidth || !originalHeight) && svgElement.getAttribute('viewBox')) {
const viewBox = svgElement.getAttribute('viewBox').split(/\s+|,/);
originalWidth = parseFloat(viewBox[2]) || 300;
originalHeight = parseFloat(viewBox[3]) || 300;
}
// 如果还是没有尺寸,使用默认值
if (!originalWidth) originalWidth = 300;
if (!originalHeight) originalHeight = 300;
// 计算输出尺寸
let canvasWidth = outputWidth ? parseFloat(outputWidth) : originalWidth;
let canvasHeight = outputHeight ? parseFloat(outputHeight) : originalHeight;
// 保持宽高比
if (keepRatio && (outputWidth || outputHeight)) {
const aspectRatio = originalWidth / originalHeight;
if (outputWidth && !outputHeight) {
canvasHeight = canvasWidth / aspectRatio;
} else if (outputHeight && !outputWidth) {
canvasWidth = canvasHeight * aspectRatio;
} else if (outputWidth && outputHeight) {
const targetRatio = canvasWidth / canvasHeight;
if (targetRatio > aspectRatio) {
canvasWidth = canvasHeight * aspectRatio;
} else {
canvasHeight = canvasWidth / aspectRatio;
}
}
}
// 创建新的 canvas
if (canvas) {
canvas.remove();
}
// 创建目标尺寸的 canvas
canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// 修改 SVG 内容,设置目标尺寸
const modifiedSvgDoc = parser.parseFromString(svgInput, 'image/svg+xml');
const modifiedSvgElement = modifiedSvgDoc.querySelector('svg');
// 设置新的尺寸
modifiedSvgElement.setAttribute('width', canvasWidth);
modifiedSvgElement.setAttribute('height', canvasHeight);
// 如果有 viewBox,保持原样以保证内容缩放
if (!modifiedSvgElement.getAttribute('viewBox')) {
modifiedSvgElement.setAttribute('viewBox', `0 0 ${originalWidth} ${originalHeight}`);
}
// 序列化修改后的 SVG
const serializer = new XMLSerializer();
const modifiedSvgString = serializer.serializeToString(modifiedSvgElement);
// 渲染修改后的 SVG
const v = await canvg.Canvg.from(ctx, modifiedSvgString);
await v.render();
// 清空容器并添加 canvas
canvasContainer.innerHTML = '';
canvasContainer.appendChild(canvas);
// 启用下载按钮
downloadBtn.disabled = false;
// 计算缩放比例用于显示
const scaleX = canvasWidth / originalWidth;
const scaleY = canvasHeight / originalHeight;
const scaleInfo = (scaleX !== 1 || scaleY !== 1)
? ` | 缩放: ${scaleX.toFixed(2)}x${scaleY.toFixed(2)}`
: '';
showInfo(
`✅ 渲染成功!原始: ${originalWidth.toFixed(0)}×${originalHeight.toFixed(0)}px | 输出: ${canvasWidth.toFixed(0)}×${canvasHeight.toFixed(0)}px${scaleInfo}`,
'success'
);
} catch (error) {
console.error('渲染错误:', error);
showInfo('❌ 渲染失败: ' + error.message, 'error');
downloadBtn.disabled = true;
}
}
function downloadImage() {
if (!canvas) {
showInfo('⚠️ 请先渲染 SVG', 'error');
return;
}
try {
const link = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
link.download = `svg-to-png-${timestamp}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
showInfo('✅ PNG 图片下载成功!', 'success');
} catch (error) {
showInfo('❌ 下载失败: ' + error.message, 'error');
}
}
function clearCanvas() {
const canvasContainer = document.getElementById('canvasContainer');
canvasContainer.innerHTML = '<p class="placeholder">等待渲染...</p>';
canvas = null;
document.getElementById('info').style.display = 'none';
document.getElementById('downloadBtn').disabled = true;
document.getElementById('outputWidth').value = '';
document.getElementById('outputHeight').value = '';
}
function showInfo(message, type = 'success') {
const info = document.getElementById('info');
info.textContent = message;
info.className = 'info ' + (type === 'error' ? 'error' : '');
info.style.display = 'block';
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
// Ctrl/Cmd + Enter 渲染
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
renderSVG();
}
// Ctrl/Cmd + S 下载
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (canvas) downloadImage();
}
});
</script>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
h1 {
color: #333;
margin-bottom: 10px;
text-align: center;
font-size: 32px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.input-section {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
font-size: 14px;
}
textarea {
width: 100%;
height: 200px;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
resize: vertical;
transition: border-color 0.3s;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
.size-controls {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 15px;
margin-bottom: 25px;
align-items: end;
}
.input-group {
display: flex;
flex-direction: column;
}
.input-group label {
margin-bottom: 5px;
font-size: 13px;
}
input[type="number"] {
padding: 10px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="number"]:focus {
outline: none;
border-color: #667eea;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 0;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.checkbox-group label {
margin: 0;
font-weight: 500;
cursor: pointer;
}
.button-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 30px;
}
button {
padding: 14px 24px;
font-size: 15px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-render {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-render:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-download {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-download:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(245, 87, 108, 0.4);
}
.btn-download:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-clear {
background: #f0f0f0;
color: #666;
}
.btn-clear:hover {
background: #e0e0e0;
}
.output-section {
border-top: 2px solid #f0f0f0;
padding-top: 25px;
}
.canvas-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
background:
repeating-conic-gradient(#f0f0f0 0% 25%, white 0% 50%) 50% / 20px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
overflow: auto;
}
canvas {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: white;
}
.placeholder {
color: #999;
text-align: center;
font-size: 16px;
}
.info {
margin-top: 15px;
padding: 12px 15px;
background: #e8f5e9;
border-left: 4px solid #4caf50;
border-radius: 6px;
font-size: 13px;
color: #2e7d32;
display: none;
}
.info.error {
background: #ffebee;
border-left-color: #f44336;
color: #c62828;
}
@media (max-width: 768px) {
.size-controls {
grid-template-columns: 1fr;
}
.button-group {
grid-template-columns: 1fr;
}
}
let canvas = null;
async function renderSVG() {
const svgInput = document.getElementById('svgInput').value.trim();
const outputWidth = document.getElementById('outputWidth').value;
const outputHeight = document.getElementById('outputHeight').value;
const keepRatio = document.getElementById('keepRatio').checked;
const canvasContainer = document.getElementById('canvasContainer');
const info = document.getElementById('info');
const downloadBtn = document.getElementById('downloadBtn');
// 清除之前的内容
info.style.display = 'none';
if (!svgInput) {
showInfo('⚠️ 请输入 SVG 代码', 'error');
return;
}
try {
// 解析 SVG 获取原始尺寸
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgInput, 'image/svg+xml');
const svgElement = svgDoc.querySelector('svg');
if (!svgElement) {
throw new Error('无效的 SVG 代码');
}
// 检查解析错误
const parseError = svgDoc.querySelector('parsererror');
if (parseError) {
throw new Error('SVG 解析错误: ' + parseError.textContent);
}
// 获取原始尺寸
let originalWidth = parseFloat(svgElement.getAttribute('width')) || 0;
let originalHeight = parseFloat(svgElement.getAttribute('height')) || 0;
// 如果 SVG 没有指定宽高,尝试从 viewBox 获取
if ((!originalWidth || !originalHeight) && svgElement.getAttribute('viewBox')) {
const viewBox = svgElement.getAttribute('viewBox').split(/\s+|,/);
originalWidth = parseFloat(viewBox[2]) || 300;
originalHeight = parseFloat(viewBox[3]) || 300;
}
// 如果还是没有尺寸,使用默认值
if (!originalWidth) originalWidth = 300;
if (!originalHeight) originalHeight = 300;
// 计算输出尺寸
let canvasWidth = outputWidth ? parseFloat(outputWidth) : originalWidth;
let canvasHeight = outputHeight ? parseFloat(outputHeight) : originalHeight;
// 保持宽高比
if (keepRatio && (outputWidth || outputHeight)) {
const aspectRatio = originalWidth / originalHeight;
if (outputWidth && !outputHeight) {
canvasHeight = canvasWidth / aspectRatio;
} else if (outputHeight && !outputWidth) {
canvasWidth = canvasHeight * aspectRatio;
} else if (outputWidth && outputHeight) {
const targetRatio = canvasWidth / canvasHeight;
if (targetRatio > aspectRatio) {
canvasWidth = canvasHeight * aspectRatio;
} else {
canvasHeight = canvasWidth / aspectRatio;
}
}
}
// 创建新的 canvas
if (canvas) {
canvas.remove();
}
// 创建目标尺寸的 canvas
canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// 修改 SVG 内容,设置目标尺寸
const modifiedSvgDoc = parser.parseFromString(svgInput, 'image/svg+xml');
const modifiedSvgElement = modifiedSvgDoc.querySelector('svg');
// 设置新的尺寸
modifiedSvgElement.setAttribute('width', canvasWidth);
modifiedSvgElement.setAttribute('height', canvasHeight);
// 如果有 viewBox,保持原样以保证内容缩放
if (!modifiedSvgElement.getAttribute('viewBox')) {
modifiedSvgElement.setAttribute('viewBox', `0 0 ${originalWidth} ${originalHeight}`);
}
// 序列化修改后的 SVG
const serializer = new XMLSerializer();
const modifiedSvgString = serializer.serializeToString(modifiedSvgElement);
// 渲染修改后的 SVG
const v = await canvg.Canvg.from(ctx, modifiedSvgString);
await v.render();
// 清空容器并添加 canvas
canvasContainer.innerHTML = '';
canvasContainer.appendChild(canvas);
// 启用下载按钮
downloadBtn.disabled = false;
// 计算缩放比例用于显示
const scaleX = canvasWidth / originalWidth;
const scaleY = canvasHeight / originalHeight;
const scaleInfo = (scaleX !== 1 || scaleY !== 1)
? ` | 缩放: ${scaleX.toFixed(2)}x${scaleY.toFixed(2)}`
: '';
showInfo(
`✅ 渲染成功!原始: ${originalWidth.toFixed(0)}×${originalHeight.toFixed(0)}px | 输出: ${canvasWidth.toFixed(0)}×${canvasHeight.toFixed(0)}px${scaleInfo}`,
'success'
);
} catch (error) {
console.error('渲染错误:', error);
showInfo('❌ 渲染失败: ' + error.message, 'error');
downloadBtn.disabled = true;
}
}
function downloadImage() {
if (!canvas) {
showInfo('⚠️ 请先渲染 SVG', 'error');
return;
}
try {
const link = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
link.download = `svg-to-png-${timestamp}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
showInfo('✅ PNG 图片下载成功!', 'success');
} catch (error) {
showInfo('❌ 下载失败: ' + error.message, 'error');
}
}
function clearCanvas() {
const canvasContainer = document.getElementById('canvasContainer');
canvasContainer.innerHTML = '<p class="placeholder">等待渲染...</p>';
canvas = null;
document.getElementById('info').style.display = 'none';
document.getElementById('downloadBtn').disabled = true;
document.getElementById('outputWidth').value = '';
document.getElementById('outputHeight').value = '';
}
function showInfo(message, type = 'success') {
const info = document.getElementById('info');
info.textContent = message;
info.className = 'info ' + (type === 'error' ? 'error' : '');
info.style.display = 'block';
}
// 快捷键支持
document.addEventListener('keydown', function (e) {
// Ctrl/Cmd + Enter 渲染
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
renderSVG();
}
// Ctrl/Cmd + S 下载
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (canvas) downloadImage();
}
});
// 绑定按钮事件
document.getElementById('renderBtn').addEventListener('click', renderSVG);
document.getElementById('downloadBtn').addEventListener('click', downloadImage);
document.getElementById('clearBtn').addEventListener('click', clearCanvas);