"""
SVG转换工具类
一个功能完整的SVG文件转换工具,支持将SVG转换为PNG、ICO、JPG等格式。
支持文件路径转换和内存转换两种方式,提供丰富的参数定制选项。
作者: Rusian Huu
版本: 1.0.0
"""
import os
import io
from typing import Optional, Union, Tuple
from pathlib import Path
import base64
try:
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
from reportlab.lib.units import inch
SVGLIB_AVAILABLE = True
except (ImportError, OSError):
# ImportError: 模块未安装
# OSError: Cairo C库未找到
SVGLIB_AVAILABLE = False
try:
from PIL import Image, ImageDraw
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
try:
import cairosvg
CAIROSVG_AVAILABLE = True
except (ImportError, OSError):
# ImportError: 模块未安装
# OSError: Cairo C库未找到
CAIROSVG_AVAILABLE = False
class SVGConverterError(Exception):
"""SVG转换器异常基类"""
pass
class DependencyError(SVGConverterError):
"""依赖库缺失异常"""
pass
class ConversionError(SVGConverterError):
"""转换过程异常"""
pass
class SVGConverter:
"""
SVG转换工具类
支持将SVG文件转换为PNG、ICO、JPG等格式,提供文件路径转换和内存转换两种方式。
"""
SUPPORTED_FORMATS = ['png', 'ico', 'jpg', 'jpeg']
DEFAULT_SIZE = (256, 256)
DEFAULT_QUALITY = 95
DEFAULT_BACKGROUND = 'white'
def __init__(self, prefer_engine: str = 'auto'):
"""
初始化SVG转换器
Args:
prefer_engine (str): 首选转换引擎 ('auto', 'svglib', 'cairosvg')
"""
self.prefer_engine = prefer_engine
self._check_dependencies()
self._select_engine()
def _check_dependencies(self):
"""检查依赖库是否可用"""
if not PIL_AVAILABLE:
raise DependencyError("PIL/Pillow库未安装,请运行: pip install Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple/")
if not PIL_AVAILABLE:
raise DependencyError("PIL/Pillow库未安装,请运行: pip install Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple/")
# 检查SVG处理库的可用性
if not CAIROSVG_AVAILABLE and not SVGLIB_AVAILABLE:
raise DependencyError("cairosvg 和 svglib 均未安装,请至少安装一个: pip install cairosvg -i https://pypi.tuna.tsinghua.edu.cn/simple/ 或 pip install svglib reportlab -i https://pypi.tuna.tsinghua.edu.cn/simple/")
# 如果没有专门的SVG库,我们将使用PIL的基本功能
# 这是一个后备方案,功能有限但可以处理简单的SVG
def _select_engine(self):
"""选择转换引擎"""
if self.prefer_engine == 'svglib' and SVGLIB_AVAILABLE:
self.engine = 'svglib'
elif self.prefer_engine == 'cairosvg' and CAIROSVG_AVAILABLE:
self.engine = 'cairosvg'
elif self.prefer_engine == 'pil':
self.engine = 'pil'
elif self.prefer_engine == 'auto':
# 自动选择最佳引擎 - 优先使用 Cairo C库
if CAIROSVG_AVAILABLE:
self.engine = 'cairosvg'
elif SVGLIB_AVAILABLE:
self.engine = 'svglib'
else:
self.engine = 'pil'
else:
# 默认后备引擎
self.engine = 'pil'
def get_engine_info(self) -> dict:
"""
获取当前引擎信息
Returns:
dict: 包含引擎信息的字典
"""
engine_info = {
'current_engine': self.engine,
'available_engines': {
'cairosvg': CAIROSVG_AVAILABLE,
'svglib': SVGLIB_AVAILABLE,
'pil': PIL_AVAILABLE
},
'engine_descriptions': {
'cairosvg': 'Cairo C库 - 高质量SVG渲染,推荐使用',
'svglib': 'ReportLab SVG库 - 良好的SVG支持',
'pil': 'PIL后备方案 - 基本SVG支持,功能有限'
}
}
return engine_info
def _get_available_chinese_fonts(self) -> dict:
"""
获取系统中可用的中文字体
Returns:
dict: 字体名称到字体路径的映射
"""
from pathlib import Path
font_dir = Path('C:/Windows/Fonts')
available_fonts = {}
# 定义中文字体文件映射
chinese_font_files = {
'Microsoft YaHei': ['msyh.ttc', 'msyh.ttf'],
'SimSun': ['simsun.ttc', 'simsun.ttf'],
'SimHei': ['simhei.ttf'],
'KaiTi': ['simkai.ttf'],
'FangSong': ['simfang.ttf'],
'Microsoft YaHei Bold': ['msyhbd.ttc'],
'Microsoft YaHei Light': ['msyhl.ttc'],
}
# 检查哪些字体文件存在
for font_name, file_names in chinese_font_files.items():
for file_name in file_names:
font_path = font_dir / file_name
if font_path.exists():
available_fonts[font_name] = str(font_path)
break
return available_fonts
def _preprocess_svg_for_fonts(self, svg_content: str) -> str:
"""
预处理SVG内容,改进中文字体支持
Args:
svg_content: 原始SVG内容
Returns:
str: 处理后的SVG内容
"""
import re
# 检查SVG内容是否包含中文字符
has_chinese = bool(re.search(r'[\u4e00-\u9fff]', svg_content))
if has_chinese:
# 获取系统中可用的中文字体
available_fonts = self._get_available_chinese_fonts()
# 构建更有效的字体回退链
if 'Microsoft YaHei' in available_fonts:
primary_font = 'Microsoft YaHei'
elif 'SimSun' in available_fonts:
primary_font = 'SimSun'
elif 'SimHei' in available_fonts:
primary_font = 'SimHei'
else:
primary_font = 'Arial Unicode MS'
# 构建完整的字体回退链
fallback_chain = f'{primary_font}, Arial Unicode MS, Segoe UI, Tahoma, Arial, sans-serif'
# 如果包含中文,使用支持中文的字体映射
font_mapping = {
'SimSun': fallback_chain,
'Microsoft YaHei': fallback_chain,
'SimHei': fallback_chain,
'KaiTi': fallback_chain,
'FangSong': fallback_chain,
'Arial': fallback_chain, # 也替换Arial以确保中文显示
# 添加常见的中文字体变体
'宋体': fallback_chain,
'微软雅黑': fallback_chain,
'黑体': fallback_chain,
'楷体': fallback_chain,
'仿宋': fallback_chain
}
else:
# 如果不包含中文,使用标准字体映射
font_mapping = {}
# 替换字体族为中文字体回退链
processed_content = svg_content
for original_font, fallback_font in font_mapping.items():
# 替换 font-family 属性(精确匹配,避免重复替换)
pattern1 = rf'font-family\s*=\s*["\']({re.escape(original_font)})["\']'
replacement1 = f'font-family="{fallback_font}"'
processed_content = re.sub(pattern1, replacement1, processed_content, flags=re.IGNORECASE)
# 替换 style 中的 font-family(精确匹配)
pattern2 = rf'font-family\s*:\s*["\']?({re.escape(original_font)})["\']?'
replacement2 = f'font-family: {fallback_font}'
processed_content = re.sub(pattern2, replacement2, processed_content, flags=re.IGNORECASE)
return processed_content
def _render_svg_to_image_with_text(self, svg_content: str, target_size: Tuple[int, int]) -> Image.Image:
"""
完全重新渲染SVG,确保中文字符正确显示
Args:
svg_content: 原始SVG内容
target_size: 目标尺寸
Returns:
PIL.Image: 渲染后的图像
"""
from PIL import Image, ImageDraw, ImageFont
import re
width, height = target_size
# 创建白色背景图像
img = Image.new('RGB', (width, height), 'white')
draw = ImageDraw.Draw(img)
# 提取SVG原始尺寸用于缩放计算
original_size = self._extract_svg_size(svg_content)
scale_x = width / original_size[0]
scale_y = height / original_size[1]
# 获取最佳中文字体
font_path = self._get_best_chinese_font()
# 1. 渲染矩形元素(包括渐变填充)
self._render_rectangles(draw, svg_content, scale_x, scale_y)
# 2. 渲染圆形元素
self._render_circles(draw, svg_content, scale_x, scale_y)
# 3. 渲染线条和多边形
self._render_lines_and_polygons(draw, svg_content, scale_x, scale_y)
# 4. 渲染文本元素 - 改进的文本解析
self._render_text_elements(draw, svg_content, scale_x, scale_y, font_path)
return img
def _render_svg_with_hybrid_method(self, svg_content: str, target_size: Tuple[int, int],
background: Optional[str] = None,
transparent: bool = False) -> Image.Image:
"""
混合渲染方法:使用标准库渲染基础图形,然后添加中文文字覆盖
Args:
svg_content: SVG内容
target_size: 目标尺寸
background: 背景颜色
transparent: 是否透明背景
Returns:
PIL.Image: 渲染后的图像
"""
import re
import tempfile
import os
from PIL import Image, ImageDraw, ImageFont
# 1. 创建没有文字的SVG版本
svg_without_text = re.sub(r'<text[^>]*>.*?</text>', '', svg_content,
flags=re.IGNORECASE | re.DOTALL)
# 2. 使用标准库渲染基础图形
try:
if self.engine == 'cairosvg' and CAIROSVG_AVAILABLE:
# 使用CairoSVG渲染基础图形
kwargs = {
'bytestring': svg_without_text.encode('utf-8'),
'output_width': target_size[0],
'output_height': target_size[1],
}
if not transparent and background:
kwargs['background_color'] = background
import cairosvg
import io
png_data = cairosvg.svg2png(**kwargs)
base_img = Image.open(io.BytesIO(png_data))
elif self.engine == 'svglib' and SVGLIB_AVAILABLE:
# 使用SVGLib渲染基础图形
svg_content_clean = re.sub(r'<\?xml[^>]*\?>\s*', '', svg_without_text)
processed_svg = self._preprocess_svg_for_fonts(svg_content_clean)
svg_io = io.StringIO(processed_svg)
drawing = svg2rlg(svg_io)
if drawing:
drawing.width = target_size[0]
drawing.height = target_size[1]
base_img = renderPM.drawToPIL(drawing, fmt='PNG')
else:
raise Exception("SVGLib无法解析SVG")
else:
# 回退到直接渲染
return self._render_svg_to_image_with_text(svg_content, target_size)
except Exception as e:
print(f"标准库渲染失败,回退到直接渲染: {e}")
return self._render_svg_to_image_with_text(svg_content, target_size)
# 3. 在基础图形上添加中文文字
draw = ImageDraw.Draw(base_img)
# 解析文本元素
text_elements = self._parse_text_elements(svg_content)
# 获取缩放比例
original_size = self._extract_svg_size(svg_content)
scale_x = target_size[0] / original_size[0]
scale_y = target_size[1] / original_size[1]
# 获取中文字体
font_path = self._get_best_chinese_font()
for text_elem in text_elements:
x = text_elem['x'] * scale_x
y = text_elem['y'] * scale_y
font_size = int(text_elem['font_size'] * min(scale_x, scale_y))
fill_color = text_elem['fill']
text_anchor = text_elem['text_anchor']
text_content = text_elem['content']
if not text_content.strip():
continue
# 加载字体
font = self._load_font(font_path, font_size)
# 解析颜色
color = self._parse_color(fill_color)
# 处理文本锚点
anchor = None
if text_anchor == 'middle':
anchor = 'mm'
elif text_anchor == 'end':
anchor = 'rm'
else:
anchor = 'lm'
# 绘制文本
try:
draw.text((x, y), text_content, fill=color, font=font, anchor=anchor)
print(f"添加中文文字: '{text_content}' at ({x:.1f}, {y:.1f})")
except Exception as e:
print(f"绘制文本失败: '{text_content}' - {e}")
# 标记使用了混合渲染(这里临时设置,会在调用处被正确设置)
print("混合渲染完成")
return base_img
def _render_rectangles(self, draw, svg_content: str, scale_x: float, scale_y: float):
"""渲染矩形元素"""
import re
# 查找所有rect元素
rect_elements = re.findall(r'<rect[^>]*>', svg_content, re.IGNORECASE)
for rect_element in rect_elements:
# 解析属性
attrs = self._parse_element_attributes(rect_element)
x = float(attrs.get('x', 0)) * scale_x
y = float(attrs.get('y', 0)) * scale_y
rect_width = float(attrs.get('width', 100)) * scale_x
rect_height = float(attrs.get('height', 100)) * scale_y
fill_color = attrs.get('fill', 'black')
stroke_color = attrs.get('stroke')
stroke_width = int(float(attrs.get('stroke-width', 1)))
opacity = float(attrs.get('opacity', 1.0))
print(f"渲染矩形: x={x:.1f}, y={y:.1f}, w={rect_width:.1f}, h={rect_height:.1f}, fill={fill_color}")
# 处理渐变填充
if fill_color and fill_color.startswith('url('):
# 简化处理:对于渐变,使用黄色到红色的近似
fill_color = 'orange' # 渐变的中间色
print(f"检测到渐变,使用近似颜色: {fill_color}")
# 绘制填充
if fill_color and fill_color != 'none':
color = self._parse_color(fill_color)
if color:
# 处理透明度
if opacity < 1.0:
# PIL不直接支持透明度,这里简化处理
pass
draw.rectangle([x, y, x + rect_width, y + rect_height], fill=color)
print(f"绘制矩形填充: {color}")
# 绘制边框
if stroke_color and stroke_color != 'none':
color = self._parse_color(stroke_color)
if color:
draw.rectangle([x, y, x + rect_width, y + rect_height], outline=color, width=stroke_width)
print(f"绘制矩形边框: {color}, 宽度: {stroke_width}")
def _parse_element_attributes(self, element_str: str) -> dict:
"""解析SVG元素的属性"""
import re
attrs = {}
# 匹配带引号的属性值
quoted_pattern = r'(\w+(?:-\w+)*)\s*=\s*["\']([^"\']*)["\']'
for match in re.finditer(quoted_pattern, element_str):
attr_name = match.group(1).lower()
attr_value = match.group(2)
attrs[attr_name] = attr_value
# 匹配不带引号的属性值(但要避免与已匹配的重复)
unquoted_pattern = r'(\w+(?:-\w+)*)\s*=\s*([^"\'>\s]+)'
for match in re.finditer(unquoted_pattern, element_str):
attr_name = match.group(1).lower()
if attr_name not in attrs: # 避免覆盖已解析的带引号属性
attr_value = match.group(2)
attrs[attr_name] = attr_value
return attrs
def _render_circles(self, draw, svg_content: str, scale_x: float, scale_y: float):
"""渲染圆形元素"""
import re
# 查找所有circle元素
circle_elements = re.findall(r'<circle[^>]*>', svg_content, re.IGNORECASE)
for circle_element in circle_elements:
# 解析属性
attrs = self._parse_element_attributes(circle_element)
cx = float(attrs.get('cx', 50)) * scale_x
cy = float(attrs.get('cy', 50)) * scale_y
r = float(attrs.get('r', 25)) * min(scale_x, scale_y)
fill_color = attrs.get('fill', 'black')
stroke_color = attrs.get('stroke')
stroke_width = int(float(attrs.get('stroke-width', 1)))
print(f"渲染圆形: cx={cx:.1f}, cy={cy:.1f}, r={r:.1f}, fill={fill_color}")
# 计算边界框
x1 = cx - r
y1 = cy - r
x2 = cx + r
y2 = cy + r
# 绘制填充
if fill_color and fill_color != 'none':
color = self._parse_color(fill_color)
if color:
draw.ellipse([x1, y1, x2, y2], fill=color)
print(f"绘制圆形填充: {color}")
# 绘制边框
if stroke_color and stroke_color != 'none':
color = self._parse_color(stroke_color)
if color:
draw.ellipse([x1, y1, x2, y2], outline=color, width=stroke_width)
print(f"绘制圆形边框: {color}, 宽度: {stroke_width}")
def _render_lines_and_polygons(self, draw, svg_content: str, scale_x: float, scale_y: float):
"""渲染线条和多边形元素"""
import re
# 渲染polyline元素
polyline_elements = re.findall(r'<polyline[^>]*>', svg_content, re.IGNORECASE)
for polyline_element in polyline_elements:
# 解析属性
attrs = self._parse_element_attributes(polyline_element)
points_str = attrs.get('points', '')
stroke_color = attrs.get('stroke', 'black')
stroke_width = int(float(attrs.get('stroke-width', 1)))
fill_color = attrs.get('fill', 'none')
print(f"渲染折线: points={points_str}, stroke={stroke_color}")
if points_str:
# 解析点坐标 - 改进的解析方法
points = []
# 首先尝试按空格分割点对
point_pairs = points_str.strip().split()
for point_pair in point_pairs:
if ',' in point_pair:
# 格式:x,y
try:
x_str, y_str = point_pair.split(',')
x = float(x_str) * scale_x
y = float(y_str) * scale_y
points.append((x, y))
except ValueError:
continue
# 如果上面的方法没有解析到点,尝试其他格式
if not points:
# 尝试连续的数字格式:x y x y
numbers = re.findall(r'(\d+(?:\.\d+)?)', points_str)
for i in range(0, len(numbers) - 1, 2):
x = float(numbers[i]) * scale_x
y = float(numbers[i + 1]) * scale_y
points.append((x, y))
print(f"解析到 {len(points)} 个点: {points}")
if len(points) >= 2:
stroke_color_parsed = self._parse_color(stroke_color)
if stroke_color_parsed:
# 绘制线条
for i in range(len(points) - 1):
draw.line([points[i], points[i + 1]], fill=stroke_color_parsed, width=stroke_width)
print(f"绘制折线: {len(points)-1} 条线段")
def _render_text_elements(self, draw, svg_content: str, scale_x: float, scale_y: float, font_path: str):
"""渲染文本元素"""
# 渲染简单文本元素
text_elements = self._parse_text_elements(svg_content)
for text_elem in text_elements:
x = text_elem['x'] * scale_x
y = text_elem['y'] * scale_y
font_size = int(text_elem['font_size'] * min(scale_x, scale_y))
fill_color = text_elem['fill']
font_weight = text_elem['font_weight']
text_anchor = text_elem['text_anchor']
text_content = text_elem['content']
if not text_content.strip():
continue
# 加载字体
font = self._load_font(font_path, font_size)
# 解析颜色
color = self._parse_color(fill_color)
# 处理文本锚点
text_x = x
if text_anchor == 'middle':
try:
bbox = draw.textbbox((0, 0), text_content, font=font)
text_width = bbox[2] - bbox[0]
text_x = x - text_width / 2
except:
pass
elif text_anchor == 'end':
try:
bbox = draw.textbbox((0, 0), text_content, font=font)
text_width = bbox[2] - bbox[0]
text_x = x - text_width
except:
pass
# 绘制文本 - 修复Y坐标计算
try:
# SVG的y坐标是文本基线,PIL的y坐标是文本顶部
# 需要根据字体大小调整,但不要过度调整
adjusted_y = y - font_size * 0.75 # 减少调整幅度
draw.text((text_x, adjusted_y), text_content, fill=color, font=font)
except Exception as e:
print(f"绘制文本失败: '{text_content}' - {e}")
# 处理tspan元素(多行文本)
tspan_elements = self._parse_tspan_elements(svg_content)
for tspan_group in tspan_elements:
base_x = tspan_group['base_x'] * scale_x
base_y = tspan_group['base_y'] * scale_y
font_size = int(tspan_group['font_size'] * min(scale_x, scale_y))
fill_color = tspan_group['fill']
font = self._load_font(font_path, font_size)
color = self._parse_color(fill_color)
current_y = base_y
for tspan in tspan_group['tspans']:
x = (tspan['x'] if tspan['x'] is not None else base_x / scale_x) * scale_x
dy = tspan['dy'] * scale_y
text_content = tspan['content']
if text_content.strip():
current_y += dy
try:
adjusted_y = current_y - font_size * 0.75 # 与上面保持一致
draw.text((x, adjusted_y), text_content, fill=color, font=font)
except Exception as e:
print(f"绘制tspan文本失败: '{text_content}' - {e}")
def _parse_text_elements(self, svg_content: str) -> list:
"""
解析SVG中的所有文本元素
Args:
svg_content: SVG内容
Returns:
list: 文本元素列表,每个元素包含位置、样式和内容信息
"""
import re
text_elements = []
# 查找所有text元素(不包含tspan的简单文本)
text_pattern = r'<text([^>]*?)>([^<]*?)</text>'
for match in re.finditer(text_pattern, svg_content, re.IGNORECASE | re.DOTALL):
attributes = match.group(1)
content = match.group(2).strip()
if not content:
continue
# 解析属性
element = {
'x': self._extract_attribute_value(attributes, 'x', 0),
'y': self._extract_attribute_value(attributes, 'y', 20),
'font_size': self._extract_attribute_value(attributes, 'font-size', 16),
'fill': self._extract_attribute_string(attributes, 'fill', 'black'),
'font_weight': self._extract_attribute_string(attributes, 'font-weight', ''),
'text_anchor': self._extract_attribute_string(attributes, 'text-anchor', ''),
'font_family': self._extract_attribute_string(attributes, 'font-family', ''),
'content': content
}
text_elements.append(element)
return text_elements
def _extract_attribute_value(self, attributes: str, attr_name: str, default_value: float) -> float:
"""从属性字符串中提取数值属性"""
import re
pattern = rf'{attr_name}\s*=\s*["\']?([0-9.]+)["\']?'
match = re.search(pattern, attributes, re.IGNORECASE)
if match:
try:
return float(match.group(1))
except ValueError:
pass
return default_value
def _extract_attribute_string(self, attributes: str, attr_name: str, default_value: str) -> str:
"""从属性字符串中提取字符串属性"""
import re
pattern = rf'{attr_name}\s*=\s*["\']?([^"\'>\s]*)["\']?'
match = re.search(pattern, attributes, re.IGNORECASE)
if match:
return match.group(1)
return default_value
def _parse_tspan_elements(self, svg_content: str) -> list:
"""
解析SVG中包含tspan的文本元素
Args:
svg_content: SVG内容
Returns:
list: tspan组列表,每个组包含基础属性和tspan列表
"""
import re
tspan_groups = []
# 查找包含tspan的text元素
text_with_tspan_pattern = r'<text([^>]*?)>(.*?)</text>'
for match in re.finditer(text_with_tspan_pattern, svg_content, re.IGNORECASE | re.DOTALL):
attributes = match.group(1)
content = match.group(2)
# 检查是否包含tspan
if '<tspan' not in content.lower():
continue
# 解析text元素的基础属性
group = {
'base_x': self._extract_attribute_value(attributes, 'x', 20),
'base_y': self._extract_attribute_value(attributes, 'y', 260),
'font_size': self._extract_attribute_value(attributes, 'font-size', 14),
'fill': self._extract_attribute_string(attributes, 'fill', '#666666'),
'tspans': []
}
# 解析tspan元素
tspan_pattern = r'<tspan([^>]*?)>([^<]*?)</tspan>'
for tspan_match in re.finditer(tspan_pattern, content, re.IGNORECASE):
tspan_attrs = tspan_match.group(1)
tspan_content = tspan_match.group(2).strip()
if tspan_content:
tspan = {
'x': self._extract_attribute_value_nullable(tspan_attrs, 'x'),
'dy': self._extract_attribute_value(tspan_attrs, 'dy', 0),
'content': tspan_content
}
group['tspans'].append(tspan)
if group['tspans']:
tspan_groups.append(group)
return tspan_groups
def _extract_attribute_value_nullable(self, attributes: str, attr_name: str):
"""从属性字符串中提取数值属性,可能返回None"""
import re
pattern = rf'{attr_name}\s*=\s*["\']?([0-9.]+)["\']?'
match = re.search(pattern, attributes, re.IGNORECASE)
if match:
try:
return float(match.group(1))
except ValueError:
pass
return None
def _get_best_chinese_font(self) -> str:
"""获取最佳的中文字体路径"""
font_paths = [
r'C:\Windows\Fonts\msyh.ttc', # 微软雅黑
r'C:\Windows\Fonts\simsun.ttc', # 宋体
r'C:\Windows\Fonts\simhei.ttf', # 黑体
]
for font_path in font_paths:
if Path(font_path).exists():
return font_path
return None # 使用默认字体
def _load_font(self, font_path: str, size: int):
"""加载字体"""
from PIL import ImageFont
if font_path and Path(font_path).exists():
try:
return ImageFont.truetype(font_path, size)
except:
pass
try:
return ImageFont.load_default()
except:
return None
def _convert_chinese_text_to_embedded_image(self, svg_content: str) -> str:
"""
将包含中文的SVG文本转换为嵌入图像
这是解决中文字体渲染问题的备用方案
Args:
svg_content: 原始SVG内容
Returns:
str: 包含嵌入图像的SVG内容
"""
# 创建文本覆盖图像
text_overlay = self._create_text_overlay_image(svg_content)
if text_overlay is None:
return svg_content
try:
import re
# 提取SVG尺寸
width_match = re.search(r'width\s*=\s*["\']?(\d+(?:\.\d+)?)', svg_content)
height_match = re.search(r'height\s*=\s*["\']?(\d+(?:\.\d+)?)', svg_content)
svg_width = int(float(width_match.group(1))) if width_match else 400
svg_height = int(float(height_match.group(1))) if height_match else 300
# 移除原始文本元素
processed_svg = re.sub(r'<text[^>]*?>.*?</text>', '', svg_content, flags=re.IGNORECASE | re.DOTALL)
# 在SVG中添加嵌入的文本图像
image_element = f'<image x="0" y="0" width="{svg_width}" height="{svg_height}" xlink:href="data:image/png;base64,{text_overlay}" style="mix-blend-mode: normal;"/>'
# 在</svg>之前插入图像
processed_svg = processed_svg.replace('</svg>', f'{image_element}</svg>')
# 确保有xlink命名空间
if 'xmlns:xlink' not in processed_svg:
processed_svg = processed_svg.replace('<svg', '<svg xmlns:xlink="http://www.w3.org/1999/xlink"')
return processed_svg
except Exception as e:
# 如果嵌入图像失败,返回原始内容
return svg_content
def convert_file(self,
svg_path: Union[str, Path],
output_path: Optional[Union[str, Path]] = None,
output_format: str = 'png',
width: Optional[int] = None,
height: Optional[int] = None,
scale: Optional[float] = None,
quality: int = DEFAULT_QUALITY,
background: Optional[str] = None,
transparent: bool = False) -> str:
"""
从文件路径转换SVG
Args:
svg_path: SVG文件路径
output_path: 输出文件路径,如果为None则自动生成
output_format: 输出格式 ('png', 'ico', 'jpg', 'jpeg')
width: 输出宽度(像素)
height: 输出高度(像素)
scale: 缩放比例,与width/height互斥
quality: JPG质量 (1-100)
background: 背景颜色,如'white', '#FFFFFF', 'transparent'
transparent: 是否透明背景(仅PNG/ICO支持)
Returns:
str: 输出文件路径
Raises:
SVGConverterError: 转换失败时抛出
"""
svg_path = Path(svg_path)
if not svg_path.exists():
raise ConversionError(f"SVG文件不存在: {svg_path}")
# 读取SVG内容
try:
with open(svg_path, 'r', encoding='utf-8') as f:
svg_content = f.read()
except Exception as e:
raise ConversionError(f"读取SVG文件失败: {e}")
# 生成输出路径
if output_path is None:
output_path = svg_path.with_suffix(f'.{output_format.lower()}')
else:
output_path = Path(output_path)
# 调用内存转换
return self.convert_string(
svg_content=svg_content,
output_path=output_path,
output_format=output_format,
width=width,
height=height,
scale=scale,
quality=quality,
background=background,
transparent=transparent
)
def convert_string(self,
svg_content: str,
output_path: Union[str, Path],
output_format: str = 'png',
width: Optional[int] = None,
height: Optional[int] = None,
scale: Optional[float] = None,
quality: int = DEFAULT_QUALITY,
background: Optional[str] = None,
transparent: bool = False) -> str:
"""
从SVG字符串转换
Args:
svg_content: SVG字符串内容
output_path: 输出文件路径
output_format: 输出格式 ('png', 'ico', 'jpg', 'jpeg')
width: 输出宽度(像素)
height: 输出高度(像素)
scale: 缩放比例,与width/height互斥
quality: JPG质量 (1-100)
background: 背景颜色
transparent: 是否透明背景(仅PNG/ICO支持)
Returns:
str: 输出文件路径
Raises:
SVGConverterError: 转换失败时抛出
"""
# 参数验证
output_format = output_format.lower()
if output_format not in self.SUPPORTED_FORMATS:
raise ConversionError(f"不支持的输出格式: {output_format}")
output_path = Path(output_path)
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 检查是否需要使用直接渲染方法
import re
has_chinese = bool(re.search(r'[\u4e00-\u9fff]', svg_content))
has_gradients = bool(re.search(r'<linearGradient|<radialGradient|url\(#', svg_content, re.IGNORECASE))
has_complex_shapes = bool(re.search(r'<polyline|<polygon|<path', svg_content, re.IGNORECASE))
original_engine = self.engine
use_direct_rendering = False
# 智能转换策略:
# 1. 如果只有中文字符,使用混合方法(标准库+中文覆盖)
# 2. 如果有复杂图形或渐变但无中文,使用直接渲染
# 3. 如果既有中文又有复杂图形,使用直接渲染
use_hybrid_method = has_chinese and not (has_gradients or has_complex_shapes)
if use_hybrid_method:
print("检测到中文字符,使用混合渲染方法(标准库+中文覆盖)...")
elif has_chinese or has_gradients or has_complex_shapes:
reasons = []
if has_chinese:
reasons.append("中文字符")
if has_gradients:
reasons.append("渐变效果")
if has_complex_shapes:
reasons.append("复杂图形")
print(f"检测到{', '.join(reasons)},使用直接渲染方法...")
use_direct_rendering = True
# 处理尺寸参数
target_size = self._calculate_size(width, height, scale, svg_content)
# 处理背景参数
if transparent and output_format in ['jpg', 'jpeg']:
transparent = False # JPG不支持透明
if background is None:
background = self.DEFAULT_BACKGROUND
try:
if use_hybrid_method:
# 使用混合渲染方法:标准库+中文文字覆盖
img = self._render_svg_with_hybrid_method(svg_content, target_size,
background, transparent)
self._post_process_image(img, output_path, output_format,
quality, background, transparent)
# 标记使用了混合渲染
self.engine = "hybrid_rendering"
elif use_direct_rendering:
# 使用直接渲染方法
img = self._render_svg_to_image_with_text(svg_content, target_size)
self._post_process_image(img, output_path, output_format,
quality, background, transparent)
# 标记使用了直接渲染
self.engine = "direct_rendering"
else:
# 对于非中文内容,使用标准转换引擎
if self.engine == 'svglib':
self._convert_with_svglib(svg_content, output_path, output_format,
target_size, quality, background, transparent)
elif self.engine == 'cairosvg':
self._convert_with_cairosvg(svg_content, output_path, output_format,
target_size, quality, background, transparent)
elif self.engine == 'pil':
self._convert_with_pil(svg_content, output_path, output_format,
target_size, quality, background, transparent)
else:
raise ConversionError(f"未知的转换引擎: {self.engine}")
except Exception as e:
# 如果转换失败且包含中文,尝试使用直接渲染备用方案
if has_chinese and not use_direct_rendering:
try:
print(f"主要转换方法失败,尝试使用直接渲染备用方案...")
img = self._render_svg_to_image_with_text(svg_content, target_size)
self._post_process_image(img, output_path, output_format,
quality, background, transparent)
except Exception as backup_e:
# 恢复原始引擎
self.engine = original_engine
raise ConversionError(f"转换失败,备用方案也失败: 主要错误={e}, 备用错误={backup_e}")
else:
# 恢复原始引擎
self.engine = original_engine
raise ConversionError(f"转换失败: {e}")
# 只有在没有使用特殊渲染方法时才恢复原始引擎
if not use_direct_rendering and not use_hybrid_method and original_engine != self.engine:
self.engine = original_engine
return str(output_path)
def _calculate_size(self, width: Optional[int], height: Optional[int],
scale: Optional[float], svg_content: str) -> Tuple[int, int]:
"""计算目标尺寸"""
if scale is not None:
if width is not None or height is not None:
raise ConversionError("scale参数与width/height参数不能同时使用")
# 尝试从SVG中提取原始尺寸
original_size = self._extract_svg_size(svg_content)
return (int(original_size[0] * scale), int(original_size[1] * scale))
if width is not None and height is not None:
return (width, height)
elif width is not None:
# 保持宽高比,根据宽度计算高度
original_size = self._extract_svg_size(svg_content)
ratio = original_size[1] / original_size[0]
return (width, int(width * ratio))
elif height is not None:
# 保持宽高比,根据高度计算宽度
original_size = self._extract_svg_size(svg_content)
ratio = original_size[0] / original_size[1]
return (int(height * ratio), height)
else:
return self.DEFAULT_SIZE
def _extract_svg_size(self, svg_content: str) -> Tuple[int, int]:
"""从SVG内容中提取尺寸信息"""
import re
# 尝试提取width和height属性
width_match = re.search(r'width\s*=\s*["\']?(\d+(?:\.\d+)?)', svg_content)
height_match = re.search(r'height\s*=\s*["\']?(\d+(?:\.\d+)?)', svg_content)
if width_match and height_match:
return (int(float(width_match.group(1))), int(float(height_match.group(1))))
# 尝试提取viewBox
viewbox_match = re.search(r'viewBox\s*=\s*["\']?[\d\s]*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)', svg_content)
if viewbox_match:
return (int(float(viewbox_match.group(1))), int(float(viewbox_match.group(2))))
# 默认尺寸
return self.DEFAULT_SIZE
def _convert_with_svglib(self, svg_content: str, output_path: Path,
output_format: str, target_size: Tuple[int, int],
quality: int, background: Optional[str],
transparent: bool):
"""使用svglib进行转换"""
# svglib不支持带XML声明的Unicode字符串,需要移除XML声明或转换为字节
import re
# 移除XML声明
svg_content_clean = re.sub(r'<\?xml[^>]*\?>\s*', '', svg_content)
# 预处理SVG内容,改进中文字体支持
processed_svg = self._preprocess_svg_for_fonts(svg_content_clean)
# 将SVG字符串转换为ReportLab Drawing对象
svg_io = io.StringIO(processed_svg)
drawing = svg2rlg(svg_io)
if drawing is None:
raise ConversionError("svglib无法解析SVG内容")
# 设置尺寸
drawing.width = target_size[0]
drawing.height = target_size[1]
if output_format in ['png', 'ico']:
# PNG格式支持透明背景
if transparent:
# 对于透明PNG,不设置背景色,让reportlab使用默认透明背景
img_data = renderPM.drawToPIL(drawing, fmt='PNG')
# 确保图像有alpha通道
if img_data.mode != 'RGBA':
img_data = img_data.convert('RGBA')
else:
bg_color_255 = self._parse_color(background or self.DEFAULT_BACKGROUND)
bg_color_01 = self._convert_color_for_reportlab(bg_color_255)
img_data = renderPM.drawToPIL(drawing, fmt='PNG', bg=bg_color_01)
else:
# JPG格式
img_data = renderPM.drawToPIL(drawing, fmt='PNG')
# 后处理图像
self._post_process_image(img_data, output_path, output_format,
quality, background, transparent)
def _setup_cairo_font_config(self) -> str:
"""
为Cairo设置字体配置
Returns:
str: 字体配置XML内容
"""
available_fonts = self._get_available_chinese_fonts()
# 创建fontconfig配置
font_config = '''<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
'''
# 为每个可用的中文字体添加配置
for font_name, font_path in available_fonts.items():
font_config += f'''
<dir>{Path(font_path).parent}</dir>
<alias>
<family>{font_name}</family>
<prefer>
<family>{font_name}</family>
</prefer>
</alias>
'''
# 添加通用中文字体别名
font_config += '''
<alias>
<family>sans-serif</family>
<prefer>
<family>Microsoft YaHei</family>
<family>SimSun</family>
<family>SimHei</family>
<family>Arial Unicode MS</family>
<family>Arial</family>
</prefer>
</alias>
<alias>
<family>serif</family>
<prefer>
<family>SimSun</family>
<family>Microsoft YaHei</family>
<family>Times New Roman</family>
</prefer>
</alias>
</fontconfig>'''
return font_config
def _convert_with_cairosvg(self, svg_content: str, output_path: Path,
output_format: str, target_size: Tuple[int, int],
quality: int, background: Optional[str],
transparent: bool):
"""使用cairosvg进行转换"""
import tempfile
import os
# 预处理SVG内容,改进中文字体支持
processed_svg = self._preprocess_svg_for_fonts(svg_content)
# 设置字体配置
font_config = self._setup_cairo_font_config()
# 创建临时字体配置文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False, encoding='utf-8') as f:
f.write(font_config)
font_config_path = f.name
try:
# 设置环境变量指向字体配置
old_fontconfig = os.environ.get('FONTCONFIG_FILE')
os.environ['FONTCONFIG_FILE'] = font_config_path
# 准备cairosvg参数
kwargs = {
'bytestring': processed_svg.encode('utf-8'),
'output_width': target_size[0],
'output_height': target_size[1],
}
if not transparent and background:
kwargs['background_color'] = background
# 转换为PNG(cairosvg的中间格式)
png_data = cairosvg.svg2png(**kwargs)
# 使用PIL进行后处理
img = Image.open(io.BytesIO(png_data))
self._post_process_image(img, output_path, output_format,
quality, background, transparent)
finally:
# 恢复环境变量
if old_fontconfig is not None:
os.environ['FONTCONFIG_FILE'] = old_fontconfig
elif 'FONTCONFIG_FILE' in os.environ:
del os.environ['FONTCONFIG_FILE']
# 删除临时文件
try:
os.unlink(font_config_path)
except:
pass
def _convert_with_pil(self, svg_content: str, output_path: Path,
output_format: str, target_size: Tuple[int, int],
quality: int, background: Optional[str],
transparent: bool):
"""使用PIL进行简单SVG转换(后备方案)"""
# 这是一个简化的SVG渲染器,只能处理基本的SVG元素
# 对于复杂的SVG,建议安装svglib或cairosvg
# 创建一个基础图像
if transparent and output_format in ['png', 'ico']:
img = Image.new('RGBA', target_size, (0, 0, 0, 0))
else:
bg_color = self._parse_color(background or self.DEFAULT_BACKGROUND)
img = Image.new('RGB', target_size, bg_color)
draw = ImageDraw.Draw(img)
# 简单的SVG解析和渲染
self._render_simple_svg(svg_content, draw, target_size)
# 后处理图像
self._post_process_image(img, output_path, output_format,
quality, background, transparent)
def _render_simple_svg(self, svg_content: str, draw: ImageDraw.Draw, target_size: Tuple[int, int]):
"""简单的SVG渲染(支持基本形状和文本)"""
import re
# 提取SVG尺寸
original_size = self._extract_svg_size(svg_content)
scale_x = target_size[0] / original_size[0]
scale_y = target_size[1] / original_size[1]
# 渲染矩形
rect_pattern = r'<rect[^>]*?(?:x\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:y\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:width\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:height\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:style\s*=\s*["\']?([^"\']*)["\']?)?[^>]*?(?:fill\s*=\s*["\']?([^"\'>\s]+)["\']?)?'
for match in re.finditer(rect_pattern, svg_content, re.IGNORECASE):
groups = match.groups()
x = float(groups[0]) if groups[0] else 0
y = float(groups[1]) if groups[1] else 0
width = float(groups[2]) if groups[2] else 100
height = float(groups[3]) if groups[3] else 100
style = groups[4] or ''
fill_color = groups[5] or self._extract_fill_from_style(style) or 'black'
# 缩放坐标
x1 = int(x * scale_x)
y1 = int(y * scale_y)
x2 = int((x + width) * scale_x)
y2 = int((y + height) * scale_y)
# 解析颜色
color = self._parse_color(fill_color)
if color:
draw.rectangle([x1, y1, x2, y2], fill=color)
# 渲染圆形
circle_pattern = r'<circle[^>]*?(?:cx\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:cy\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:r\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:fill\s*=\s*["\']?([^"\'>\s]+)["\']?)?'
for match in re.finditer(circle_pattern, svg_content, re.IGNORECASE):
groups = match.groups()
cx = float(groups[0]) if groups[0] else 50
cy = float(groups[1]) if groups[1] else 50
r = float(groups[2]) if groups[2] else 25
fill_color = groups[3] or 'black'
# 缩放坐标
x1 = int((cx - r) * scale_x)
y1 = int((cy - r) * scale_y)
x2 = int((cx + r) * scale_x)
y2 = int((cy + r) * scale_y)
# 解析颜色
color = self._parse_color(fill_color)
if color:
draw.ellipse([x1, y1, x2, y2], fill=color)
# 渲染文本
text_pattern = r'<text[^>]*?(?:x\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:y\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:font-size\s*=\s*["\']?(\d+(?:\.\d+)?)["\']?)?[^>]*?(?:fill\s*=\s*["\']?([^"\'>\s]+)["\']?)?[^>]*?(?:font-family\s*=\s*["\']?([^"\']*)["\']?)?[^>]*?>([^<]*)</text>'
for match in re.finditer(text_pattern, svg_content, re.IGNORECASE):
groups = match.groups()
x = float(groups[0]) if groups[0] else 0
y = float(groups[1]) if groups[1] else 20
font_size = float(groups[2]) if groups[2] else 16
fill_color = groups[3] or 'black'
font_family = groups[4] or 'Arial'
text_content = groups[5] or ''
# 缩放坐标和字体大小
x_scaled = int(x * scale_x)
y_scaled = int(y * scale_y)
font_size_scaled = int(font_size * min(scale_x, scale_y))
# 解析颜色
color = self._parse_color(fill_color)
if color and text_content.strip():
try:
# 尝试加载字体
from PIL import ImageFont
try:
font = ImageFont.truetype(font_family, font_size_scaled)
except (OSError, IOError):
# 如果指定字体不可用,使用默认字体
try:
font = ImageFont.load_default()
except:
font = None
# 处理文本编码问题
try:
# 确保文本是UTF-8编码
if isinstance(text_content, bytes):
text_content = text_content.decode('utf-8')
if font:
draw.text((x_scaled, y_scaled), text_content, fill=color, font=font)
else:
draw.text((x_scaled, y_scaled), text_content, fill=color)
except UnicodeError:
# 如果有编码问题,尝试替换不支持的字符
safe_text = text_content.encode('ascii', 'replace').decode('ascii')
if font:
draw.text((x_scaled, y_scaled), safe_text, fill=color, font=font)
else:
draw.text((x_scaled, y_scaled), safe_text, fill=color)
except ImportError:
# 如果没有字体支持,使用默认字体
try:
draw.text((x_scaled, y_scaled), text_content, fill=color)
except UnicodeError:
# 处理编码问题
safe_text = text_content.encode('ascii', 'replace').decode('ascii')
draw.text((x_scaled, y_scaled), safe_text, fill=color)
def _extract_fill_from_style(self, style: str) -> Optional[str]:
"""从style属性中提取fill颜色"""
if not style:
return None
import re
# 查找 fill: 颜色值
fill_match = re.search(r'fill\s*:\s*([^;]+)', style, re.IGNORECASE)
if fill_match:
return fill_match.group(1).strip()
return None
def _post_process_image(self, img: Image.Image, output_path: Path,
output_format: str, quality: int,
background: Optional[str], transparent: bool):
"""图像后处理和保存"""
# 处理透明背景和格式转换
if output_format in ['jpg', 'jpeg']:
# JPG不支持透明,需要添加背景
if img.mode in ('RGBA', 'LA'):
bg_color = self._parse_color(background or self.DEFAULT_BACKGROUND)
background_img = Image.new('RGB', img.size, bg_color)
if img.mode == 'RGBA':
background_img.paste(img, mask=img.split()[-1]) # 使用alpha通道作为mask
else:
background_img.paste(img)
img = background_img
elif img.mode != 'RGB':
img = img.convert('RGB')
# 保存JPG
img.save(output_path, 'JPEG', quality=quality, optimize=True)
elif output_format == 'ico':
# ICO格式处理
if not transparent and background and img.mode in ('RGBA', 'LA'):
bg_color = self._parse_color(background)
background_img = Image.new('RGB', img.size, bg_color)
if img.mode == 'RGBA':
background_img.paste(img, mask=img.split()[-1])
else:
background_img.paste(img)
img = background_img
# ICO支持多种尺寸,这里生成常用尺寸
sizes = [(16, 16), (32, 32), (48, 48), (64, 64)]
if img.size not in sizes:
sizes.append(img.size)
# 创建多尺寸ICO
ico_images = []
for size in sizes:
resized = img.resize(size, Image.Resampling.LANCZOS)
ico_images.append(resized)
# 保存ICO文件
ico_images[0].save(output_path, 'ICO', sizes=[(img.width, img.height) for img in ico_images])
else: # PNG
if not transparent and background and img.mode in ('RGBA', 'LA'):
bg_color = self._parse_color(background)
background_img = Image.new('RGB', img.size, bg_color)
if img.mode == 'RGBA':
background_img.paste(img, mask=img.split()[-1])
else:
background_img.paste(img)
img = background_img
# 保存PNG
img.save(output_path, 'PNG', optimize=True)
def _parse_color(self, color: str) -> Union[str, Tuple[int, int, int]]:
"""解析颜色字符串,返回0-255范围的RGB元组"""
if not color:
return (255, 255, 255) # 默认白色
color = color.strip().lower()
# 处理常见颜色名称
color_map = {
'white': (255, 255, 255),
'black': (0, 0, 0),
'red': (255, 0, 0),
'green': (0, 255, 0),
'blue': (0, 0, 255),
'yellow': (255, 255, 0),
'orange': (255, 165, 0),
'purple': (128, 0, 128),
'pink': (255, 192, 203),
'gray': (128, 128, 128),
'grey': (128, 128, 128),
'transparent': None,
'none': None
}
if color in color_map:
return color_map[color]
# 处理十六进制颜色
if color.startswith('#'):
color = color[1:]
if len(color) == 3:
color = ''.join([c*2 for c in color])
if len(color) == 6:
try:
return tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
except ValueError:
pass
# 处理RGB格式
if color.startswith('rgb(') and color.endswith(')'):
try:
rgb_str = color[4:-1]
rgb_values = [int(x.strip()) for x in rgb_str.split(',')]
if len(rgb_values) == 3:
return tuple(rgb_values)
except ValueError:
pass
# 默认返回白色
return (255, 255, 255)
def _convert_color_for_reportlab(self, color_255: Tuple[int, int, int]) -> Tuple[float, float, float]:
"""
将0-255范围的RGB颜色转换为ReportLab需要的0-1范围
ReportLab遵循PDF/PostScript标准,要求颜色值在0-1范围内。
虽然0-255是更普遍的颜色表示方法,但我们必须转换以兼容ReportLab。
Args:
color_255: 0-255范围的RGB元组
Returns:
0-1范围的RGB元组,适用于ReportLab
Note:
这种转换是必要的,因为:
1. ReportLab严格要求0-1范围,否则会抛出AssertionError
2. 0-255是更普遍的标准,我们在其他地方保持这个标准
3. 只在与ReportLab交互时进行转换,保持接口一致性
"""
if color_255 is None:
return (1.0, 1.0, 1.0) # 默认白色
if isinstance(color_255, tuple) and len(color_255) == 3:
# 确保值在有效范围内,然后转换
normalized = tuple(max(0, min(255, c)) / 255.0 for c in color_255)
return normalized
return (1.0, 1.0, 1.0) # 默认白色
def get_svg_info(self, svg_path: Union[str, Path]) -> dict:
"""
获取SVG文件信息
Args:
svg_path: SVG文件路径
Returns:
dict: 包含SVG信息的字典
"""
svg_path = Path(svg_path)
if not svg_path.exists():
raise ConversionError(f"SVG文件不存在: {svg_path}")
try:
with open(svg_path, 'r', encoding='utf-8') as f:
svg_content = f.read()
except Exception as e:
raise ConversionError(f"读取SVG文件失败: {e}")
return self.get_svg_info_from_string(svg_content)
def get_svg_info_from_string(self, svg_content: str) -> dict:
"""
从SVG字符串获取信息
Args:
svg_content: SVG字符串内容
Returns:
dict: 包含SVG信息的字典
"""
import re
info = {
'width': None,
'height': None,
'viewbox': None,
'has_text': False,
'has_images': False,
'estimated_complexity': 'simple'
}
# 提取尺寸信息
width_match = re.search(r'width\s*=\s*["\']?(\d+(?:\.\d+)?)', svg_content)
height_match = re.search(r'height\s*=\s*["\']?(\d+(?:\.\d+)?)', svg_content)
viewbox_match = re.search(r'viewBox\s*=\s*["\']?([\d\s\.\-]+)["\']?', svg_content)
if width_match:
info['width'] = float(width_match.group(1))
if height_match:
info['height'] = float(height_match.group(1))
if viewbox_match:
info['viewbox'] = viewbox_match.group(1).strip()
# 检查内容复杂度
if '<text' in svg_content or '<tspan' in svg_content:
info['has_text'] = True
if '<image' in svg_content:
info['has_images'] = True
# 估算复杂度
element_count = len(re.findall(r'<[^/][^>]*>', svg_content))
if element_count > 50:
info['estimated_complexity'] = 'complex'
elif element_count > 20:
info['estimated_complexity'] = 'medium'
return info
@classmethod
def batch_convert(cls,
svg_files: list,
output_dir: Union[str, Path],
output_format: str = 'png',
**kwargs) -> list:
"""
批量转换SVG文件
Args:
svg_files: SVG文件路径列表
output_dir: 输出目录
output_format: 输出格式
**kwargs: 其他转换参数
Returns:
list: 转换结果列表,每个元素为(输入文件, 输出文件, 是否成功, 错误信息)
"""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
converter = cls()
results = []
for svg_file in svg_files:
svg_path = Path(svg_file)
output_path = output_dir / f"{svg_path.stem}.{output_format}"
try:
result_path = converter.convert_file(
svg_path=svg_path,
output_path=output_path,
output_format=output_format,
**kwargs
)
results.append((str(svg_path), result_path, True, None))
except Exception as e:
results.append((str(svg_path), str(output_path), False, str(e)))
return results