first commit
This commit is contained in:
658
xml_analyzer.py
Normal file
658
xml_analyzer.py
Normal file
@@ -0,0 +1,658 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
XML布局分析器模块
|
||||
用于解析和分析Android UI XML布局文件,以及SVG设计图的布局结构
|
||||
"""
|
||||
|
||||
from lxml import etree
|
||||
import re
|
||||
from pathlib import Path
|
||||
from config import Config
|
||||
|
||||
|
||||
class XMLAnalyzer:
|
||||
"""XML布局分析器"""
|
||||
|
||||
def __init__(self):
|
||||
self.elements_data = []
|
||||
self.issues = []
|
||||
self.svg_elements = []
|
||||
self.layout_comparison_result = None
|
||||
self.config = Config.XML_ANALYSIS
|
||||
|
||||
def parse_xml(self, xml_content):
|
||||
"""解析XML内容"""
|
||||
try:
|
||||
# 如果xml_content是字符串且包含编码声明,需要特殊处理
|
||||
if isinstance(xml_content, str):
|
||||
# 移除XML声明,因为etree.fromstring不支持带编码声明的Unicode字符串
|
||||
if xml_content.strip().startswith('<?xml'):
|
||||
# 找到XML声明的结束位置
|
||||
declaration_end = xml_content.find('?>') + 2
|
||||
xml_content = xml_content[declaration_end:].strip()
|
||||
|
||||
root = etree.fromstring(xml_content)
|
||||
self.elements_data = []
|
||||
self.issues = []
|
||||
self._parse_element(root, 0)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ XML解析失败: {e}")
|
||||
return False
|
||||
|
||||
def _parse_element(self, element, depth):
|
||||
"""递归解析XML元素"""
|
||||
# 提取元素信息
|
||||
element_info = {
|
||||
'tag': element.tag,
|
||||
'depth': depth,
|
||||
'attributes': dict(element.attrib),
|
||||
'text': element.text.strip() if element.text else '',
|
||||
'children_count': len(element)
|
||||
}
|
||||
|
||||
# 解析bounds属性
|
||||
bounds = element.get('bounds', '')
|
||||
if bounds:
|
||||
try:
|
||||
# bounds格式: [x1,y1][x2,y2]
|
||||
coords = bounds.replace('[', '').replace(']', ',').split(',')
|
||||
if len(coords) >= 4:
|
||||
x1, y1, x2, y2 = map(int, coords[:4])
|
||||
element_info['bounds'] = {
|
||||
'x1': x1, 'y1': y1, 'x2': x2, 'y2': y2,
|
||||
'width': x2 - x1, 'height': y2 - y1,
|
||||
'center_x': (x1 + x2) // 2, 'center_y': (y1 + y2) // 2
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
# 检查常见问题
|
||||
if self.config['check_accessibility'] or self.config['check_duplicates']:
|
||||
self._check_element_issues(element_info)
|
||||
|
||||
self.elements_data.append(element_info)
|
||||
|
||||
# 递归处理子元素
|
||||
for child in element:
|
||||
self._parse_element(child, depth + 1)
|
||||
|
||||
def _check_element_issues(self, element_info):
|
||||
"""检查元素问题"""
|
||||
issues = []
|
||||
|
||||
# 检查可访问性
|
||||
if self.config['check_accessibility']:
|
||||
if element_info.get('attributes', {}).get('clickable') == 'true':
|
||||
if not element_info.get('attributes', {}).get('content-desc'):
|
||||
issues.append("可点击元素缺少content-desc")
|
||||
|
||||
# 检查文本大小
|
||||
if 'bounds' in element_info:
|
||||
bounds = element_info['bounds']
|
||||
min_size = self.config['min_clickable_size']
|
||||
if bounds['width'] < min_size or bounds['height'] < min_size:
|
||||
if element_info.get('attributes', {}).get('clickable') == 'true':
|
||||
issues.append(f"可点击元素尺寸过小: {bounds['width']}x{bounds['height']}")
|
||||
|
||||
# 检查文本内容
|
||||
text = element_info.get('text', '')
|
||||
if text:
|
||||
max_length = self.config['max_text_length']
|
||||
if len(text) > max_length:
|
||||
issues.append(f"文本过长: {len(text)}字符")
|
||||
if text.lower() in ['click here', 'button', 'text']:
|
||||
issues.append(f"文本描述不明确: '{text}'")
|
||||
|
||||
# 检查重叠元素
|
||||
if 'bounds' in element_info:
|
||||
bounds = element_info['bounds']
|
||||
if bounds['width'] <= 0 or bounds['height'] <= 0:
|
||||
issues.append("元素尺寸无效")
|
||||
|
||||
if issues:
|
||||
self.issues.extend([{
|
||||
'element': element_info['tag'],
|
||||
'issue': issue,
|
||||
'bounds': element_info.get('bounds', {}),
|
||||
'attributes': element_info.get('attributes', {})
|
||||
} for issue in issues])
|
||||
|
||||
def get_statistics(self):
|
||||
"""获取统计信息"""
|
||||
stats = {
|
||||
'total_elements': len(self.elements_data),
|
||||
'clickable_elements': 0,
|
||||
'text_elements': 0,
|
||||
'image_elements': 0,
|
||||
'max_depth': 0,
|
||||
'issues_count': len(self.issues)
|
||||
}
|
||||
|
||||
for element in self.elements_data:
|
||||
if element.get('attributes', {}).get('clickable') == 'true':
|
||||
stats['clickable_elements'] += 1
|
||||
if element.get('text'):
|
||||
stats['text_elements'] += 1
|
||||
if 'Image' in element.get('tag', ''):
|
||||
stats['image_elements'] += 1
|
||||
stats['max_depth'] = max(stats['max_depth'], element.get('depth', 0))
|
||||
|
||||
return stats
|
||||
|
||||
def find_duplicate_ids(self):
|
||||
"""查找重复的resource-id"""
|
||||
if not self.config['check_duplicates']:
|
||||
return []
|
||||
|
||||
id_counts = {}
|
||||
duplicates = []
|
||||
|
||||
for element in self.elements_data:
|
||||
resource_id = element.get('attributes', {}).get('resource-id')
|
||||
if resource_id:
|
||||
id_counts[resource_id] = id_counts.get(resource_id, 0) + 1
|
||||
|
||||
for resource_id, count in id_counts.items():
|
||||
if count > 1:
|
||||
duplicates.append({
|
||||
'resource_id': resource_id,
|
||||
'count': count
|
||||
})
|
||||
|
||||
return duplicates
|
||||
|
||||
def get_accessibility_issues(self):
|
||||
"""获取可访问性问题"""
|
||||
accessibility_issues = []
|
||||
|
||||
for issue in self.issues:
|
||||
if 'content-desc' in issue['issue'] or '可访问性' in issue['issue']:
|
||||
accessibility_issues.append(issue)
|
||||
|
||||
return accessibility_issues
|
||||
|
||||
def export_elements_data(self, format='json'):
|
||||
"""导出元素数据"""
|
||||
if format == 'json':
|
||||
import json
|
||||
return json.dumps(self.elements_data, ensure_ascii=False, indent=2)
|
||||
elif format == 'csv':
|
||||
import csv
|
||||
import io
|
||||
output = io.StringIO()
|
||||
if self.elements_data:
|
||||
fieldnames = ['tag', 'depth', 'text', 'clickable', 'resource-id']
|
||||
writer = csv.DictWriter(output, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for element in self.elements_data:
|
||||
row = {
|
||||
'tag': element.get('tag', ''),
|
||||
'depth': element.get('depth', 0),
|
||||
'text': element.get('text', ''),
|
||||
'clickable': element.get('attributes', {}).get('clickable', 'false'),
|
||||
'resource-id': element.get('attributes', {}).get('resource-id', '')
|
||||
}
|
||||
writer.writerow(row)
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
return str(self.elements_data)
|
||||
|
||||
def parse_svg(self, svg_path):
|
||||
"""解析SVG文件,提取布局元素信息"""
|
||||
try:
|
||||
svg_path = Path(svg_path)
|
||||
if not svg_path.exists():
|
||||
print(f"❌ SVG文件不存在: {svg_path}")
|
||||
return False
|
||||
|
||||
with open(svg_path, 'r', encoding='utf-8') as f:
|
||||
svg_content = f.read()
|
||||
|
||||
# 解析SVG XML
|
||||
root = etree.fromstring(svg_content.encode('utf-8'))
|
||||
|
||||
# 获取SVG的命名空间
|
||||
namespaces = {'svg': 'http://www.w3.org/2000/svg'}
|
||||
if root.nsmap:
|
||||
namespaces.update(root.nsmap)
|
||||
|
||||
self.svg_elements = []
|
||||
self._parse_svg_element(root, 0, namespaces)
|
||||
|
||||
print(f"✅ SVG解析完成,找到 {len(self.svg_elements)} 个元素")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ SVG解析失败: {e}")
|
||||
return False
|
||||
|
||||
def _parse_svg_element(self, element, depth, namespaces):
|
||||
"""递归解析SVG元素"""
|
||||
# 获取元素标签名(去除命名空间前缀)
|
||||
tag = element.tag
|
||||
if '}' in tag:
|
||||
tag = tag.split('}')[1]
|
||||
|
||||
# 提取元素信息
|
||||
element_info = {
|
||||
'tag': tag,
|
||||
'depth': depth,
|
||||
'attributes': dict(element.attrib),
|
||||
'text': (element.text or '').strip(),
|
||||
'children_count': len(element)
|
||||
}
|
||||
|
||||
# 解析位置和尺寸信息
|
||||
self._extract_svg_geometry(element_info)
|
||||
|
||||
# 解析样式信息
|
||||
self._extract_svg_styles(element_info)
|
||||
|
||||
# 识别UI元素类型
|
||||
self._classify_svg_element(element_info)
|
||||
|
||||
self.svg_elements.append(element_info)
|
||||
|
||||
# 递归处理子元素
|
||||
for child in element:
|
||||
self._parse_svg_element(child, depth + 1, namespaces)
|
||||
|
||||
def _extract_svg_geometry(self, element_info):
|
||||
"""提取SVG元素的几何信息"""
|
||||
attrs = element_info['attributes']
|
||||
tag = element_info['tag']
|
||||
|
||||
# 初始化几何信息
|
||||
geometry = {
|
||||
'x': 0, 'y': 0, 'width': 0, 'height': 0,
|
||||
'center_x': 0, 'center_y': 0
|
||||
}
|
||||
|
||||
try:
|
||||
if tag == 'rect':
|
||||
geometry['x'] = float(attrs.get('x', 0))
|
||||
geometry['y'] = float(attrs.get('y', 0))
|
||||
geometry['width'] = float(attrs.get('width', 0))
|
||||
geometry['height'] = float(attrs.get('height', 0))
|
||||
|
||||
elif tag == 'circle':
|
||||
cx = float(attrs.get('cx', 0))
|
||||
cy = float(attrs.get('cy', 0))
|
||||
r = float(attrs.get('r', 0))
|
||||
geometry['x'] = cx - r
|
||||
geometry['y'] = cy - r
|
||||
geometry['width'] = r * 2
|
||||
geometry['height'] = r * 2
|
||||
|
||||
elif tag == 'ellipse':
|
||||
cx = float(attrs.get('cx', 0))
|
||||
cy = float(attrs.get('cy', 0))
|
||||
rx = float(attrs.get('rx', 0))
|
||||
ry = float(attrs.get('ry', 0))
|
||||
geometry['x'] = cx - rx
|
||||
geometry['y'] = cy - ry
|
||||
geometry['width'] = rx * 2
|
||||
geometry['height'] = ry * 2
|
||||
|
||||
elif tag == 'line':
|
||||
x1 = float(attrs.get('x1', 0))
|
||||
y1 = float(attrs.get('y1', 0))
|
||||
x2 = float(attrs.get('x2', 0))
|
||||
y2 = float(attrs.get('y2', 0))
|
||||
geometry['x'] = min(x1, x2)
|
||||
geometry['y'] = min(y1, y2)
|
||||
geometry['width'] = abs(x2 - x1)
|
||||
geometry['height'] = abs(y2 - y1)
|
||||
|
||||
elif tag == 'text':
|
||||
geometry['x'] = float(attrs.get('x', 0))
|
||||
geometry['y'] = float(attrs.get('y', 0))
|
||||
# 文本的宽高需要根据字体大小估算
|
||||
font_size = self._extract_font_size(attrs)
|
||||
text_length = len(element_info.get('text', ''))
|
||||
geometry['width'] = text_length * font_size * 0.6 # 估算宽度
|
||||
geometry['height'] = font_size
|
||||
|
||||
elif tag == 'g': # 组元素
|
||||
# 对于组元素,尝试从transform属性获取位置
|
||||
transform = attrs.get('transform', '')
|
||||
translate_match = re.search(r'translate\(([^)]+)\)', transform)
|
||||
if translate_match:
|
||||
coords = translate_match.group(1).split(',')
|
||||
if len(coords) >= 2:
|
||||
geometry['x'] = float(coords[0].strip())
|
||||
geometry['y'] = float(coords[1].strip())
|
||||
|
||||
# 计算中心点
|
||||
geometry['center_x'] = geometry['x'] + geometry['width'] / 2
|
||||
geometry['center_y'] = geometry['y'] + geometry['height'] / 2
|
||||
|
||||
element_info['geometry'] = geometry
|
||||
|
||||
except (ValueError, TypeError):
|
||||
# 如果解析失败,使用默认值
|
||||
element_info['geometry'] = geometry
|
||||
|
||||
def _extract_font_size(self, attrs):
|
||||
"""提取字体大小"""
|
||||
# 从style属性中提取
|
||||
style = attrs.get('style', '')
|
||||
font_size_match = re.search(r'font-size:\s*(\d+(?:\.\d+)?)', style)
|
||||
if font_size_match:
|
||||
return float(font_size_match.group(1))
|
||||
|
||||
# 从font-size属性中提取
|
||||
font_size = attrs.get('font-size', '12')
|
||||
try:
|
||||
return float(re.sub(r'[^\d.]', '', font_size))
|
||||
except:
|
||||
return 12.0 # 默认字体大小
|
||||
|
||||
def _extract_svg_styles(self, element_info):
|
||||
"""提取SVG元素的样式信息"""
|
||||
attrs = element_info['attributes']
|
||||
styles = {}
|
||||
|
||||
# 解析style属性
|
||||
style_attr = attrs.get('style', '')
|
||||
if style_attr:
|
||||
for style_rule in style_attr.split(';'):
|
||||
if ':' in style_rule:
|
||||
key, value = style_rule.split(':', 1)
|
||||
styles[key.strip()] = value.strip()
|
||||
|
||||
# 直接的样式属性
|
||||
style_attrs = ['fill', 'stroke', 'stroke-width', 'opacity', 'font-family', 'font-size', 'color']
|
||||
for attr in style_attrs:
|
||||
if attr in attrs:
|
||||
styles[attr] = attrs[attr]
|
||||
|
||||
element_info['styles'] = styles
|
||||
|
||||
def _classify_svg_element(self, element_info):
|
||||
"""分类SVG元素,识别可能的UI组件类型"""
|
||||
tag = element_info['tag']
|
||||
attrs = element_info['attributes']
|
||||
styles = element_info.get('styles', {})
|
||||
text = element_info.get('text', '')
|
||||
|
||||
ui_type = 'unknown'
|
||||
|
||||
if tag == 'text' or text:
|
||||
ui_type = 'text'
|
||||
elif tag == 'rect':
|
||||
# 判断是否为按钮
|
||||
if styles.get('fill') and styles.get('stroke'):
|
||||
ui_type = 'button'
|
||||
else:
|
||||
ui_type = 'container'
|
||||
elif tag == 'circle' or tag == 'ellipse':
|
||||
ui_type = 'button' # 圆形通常是按钮
|
||||
elif tag == 'image':
|
||||
ui_type = 'image'
|
||||
elif tag == 'g':
|
||||
ui_type = 'group'
|
||||
elif tag == 'path':
|
||||
ui_type = 'icon' # path通常用于图标
|
||||
|
||||
element_info['ui_type'] = ui_type
|
||||
|
||||
def compare_layouts(self, svg_path):
|
||||
"""比较XML布局与SVG设计图的结构"""
|
||||
if not self.elements_data:
|
||||
print("❌ 请先解析XML布局")
|
||||
return None
|
||||
|
||||
if not self.parse_svg(svg_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
# 分析XML中的UI元素
|
||||
xml_ui_elements = self._extract_xml_ui_elements()
|
||||
|
||||
# 分析SVG中的UI元素
|
||||
svg_ui_elements = self._extract_svg_ui_elements()
|
||||
|
||||
# 进行布局比对
|
||||
comparison_result = {
|
||||
'xml_elements_count': len(xml_ui_elements),
|
||||
'svg_elements_count': len(svg_ui_elements),
|
||||
'matched_elements': [],
|
||||
'unmatched_xml': [],
|
||||
'unmatched_svg': [],
|
||||
'layout_similarity': 0.0,
|
||||
'position_differences': [],
|
||||
'size_differences': []
|
||||
}
|
||||
|
||||
# 匹配相似的元素
|
||||
self._match_ui_elements(xml_ui_elements, svg_ui_elements, comparison_result)
|
||||
|
||||
# 计算布局相似度
|
||||
self._calculate_layout_similarity(comparison_result)
|
||||
|
||||
self.layout_comparison_result = comparison_result
|
||||
|
||||
print(f"📊 布局比对完成:")
|
||||
print(f" XML元素: {comparison_result['xml_elements_count']}个")
|
||||
print(f" SVG元素: {comparison_result['svg_elements_count']}个")
|
||||
print(f" 匹配元素: {len(comparison_result['matched_elements'])}个")
|
||||
print(f" 布局相似度: {comparison_result['layout_similarity']:.1%}")
|
||||
|
||||
return comparison_result
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 布局比对失败: {e}")
|
||||
return None
|
||||
|
||||
def _extract_xml_ui_elements(self):
|
||||
"""从XML数据中提取UI元素"""
|
||||
ui_elements = []
|
||||
|
||||
for element in self.elements_data:
|
||||
# 过滤掉容器元素,只保留实际的UI组件
|
||||
if self._is_ui_component(element):
|
||||
ui_element = {
|
||||
'type': self._classify_xml_element(element),
|
||||
'text': element.get('text', ''),
|
||||
'bounds': element.get('bounds', {}),
|
||||
'attributes': element.get('attributes', {}),
|
||||
'source': 'xml'
|
||||
}
|
||||
ui_elements.append(ui_element)
|
||||
|
||||
return ui_elements
|
||||
|
||||
def _extract_svg_ui_elements(self):
|
||||
"""从SVG数据中提取UI元素"""
|
||||
ui_elements = []
|
||||
|
||||
for element in self.svg_elements:
|
||||
if element['ui_type'] != 'unknown':
|
||||
ui_element = {
|
||||
'type': element['ui_type'],
|
||||
'text': element.get('text', ''),
|
||||
'geometry': element.get('geometry', {}),
|
||||
'styles': element.get('styles', {}),
|
||||
'source': 'svg'
|
||||
}
|
||||
ui_elements.append(ui_element)
|
||||
|
||||
return ui_elements
|
||||
|
||||
def _is_ui_component(self, element):
|
||||
"""判断XML元素是否为UI组件"""
|
||||
tag = element.get('tag', '')
|
||||
attrs = element.get('attributes', {})
|
||||
|
||||
# 排除纯容器元素
|
||||
container_tags = ['LinearLayout', 'RelativeLayout', 'FrameLayout', 'ConstraintLayout']
|
||||
if any(container in tag for container in container_tags):
|
||||
return False
|
||||
|
||||
# 包含文本或可点击的元素
|
||||
if element.get('text') or attrs.get('clickable') == 'true':
|
||||
return True
|
||||
|
||||
# 特定的UI组件
|
||||
ui_tags = ['Button', 'TextView', 'ImageView', 'EditText', 'CheckBox', 'RadioButton']
|
||||
return any(ui_tag in tag for ui_tag in ui_tags)
|
||||
|
||||
def _classify_xml_element(self, element):
|
||||
"""分类XML元素"""
|
||||
tag = element.get('tag', '')
|
||||
attrs = element.get('attributes', {})
|
||||
|
||||
if 'Button' in tag:
|
||||
return 'button'
|
||||
elif 'TextView' in tag or element.get('text'):
|
||||
return 'text'
|
||||
elif 'ImageView' in tag:
|
||||
return 'image'
|
||||
elif 'EditText' in tag:
|
||||
return 'input'
|
||||
elif attrs.get('clickable') == 'true':
|
||||
return 'button'
|
||||
else:
|
||||
return 'container'
|
||||
|
||||
def _match_ui_elements(self, xml_elements, svg_elements, result):
|
||||
"""匹配XML和SVG中的UI元素"""
|
||||
matched_xml = set()
|
||||
matched_svg = set()
|
||||
|
||||
for i, xml_elem in enumerate(xml_elements):
|
||||
best_match = None
|
||||
best_score = 0
|
||||
|
||||
for j, svg_elem in enumerate(svg_elements):
|
||||
if j in matched_svg:
|
||||
continue
|
||||
|
||||
score = self._calculate_element_similarity(xml_elem, svg_elem)
|
||||
if score > best_score and score > 0.5: # 相似度阈值
|
||||
best_score = score
|
||||
best_match = j
|
||||
|
||||
if best_match is not None:
|
||||
matched_xml.add(i)
|
||||
matched_svg.add(best_match)
|
||||
|
||||
match_info = {
|
||||
'xml_element': xml_elements[i],
|
||||
'svg_element': svg_elements[best_match],
|
||||
'similarity': best_score,
|
||||
'position_diff': self._calculate_position_difference(
|
||||
xml_elements[i], svg_elements[best_match]
|
||||
),
|
||||
'size_diff': self._calculate_size_difference(
|
||||
xml_elements[i], svg_elements[best_match]
|
||||
)
|
||||
}
|
||||
result['matched_elements'].append(match_info)
|
||||
|
||||
# 记录未匹配的元素
|
||||
result['unmatched_xml'] = [xml_elements[i] for i in range(len(xml_elements)) if i not in matched_xml]
|
||||
result['unmatched_svg'] = [svg_elements[j] for j in range(len(svg_elements)) if j not in matched_svg]
|
||||
|
||||
def _calculate_element_similarity(self, xml_elem, svg_elem):
|
||||
"""计算两个元素的相似度"""
|
||||
score = 0
|
||||
|
||||
# 类型匹配
|
||||
if xml_elem['type'] == svg_elem['type']:
|
||||
score += 0.4
|
||||
|
||||
# 文本匹配
|
||||
xml_text = xml_elem.get('text', '').strip().lower()
|
||||
svg_text = svg_elem.get('text', '').strip().lower()
|
||||
|
||||
if xml_text and svg_text:
|
||||
if xml_text == svg_text:
|
||||
score += 0.4
|
||||
elif xml_text in svg_text or svg_text in xml_text:
|
||||
score += 0.2
|
||||
elif not xml_text and not svg_text:
|
||||
score += 0.2
|
||||
|
||||
# 位置相似度(相对位置)
|
||||
position_score = self._calculate_relative_position_similarity(xml_elem, svg_elem)
|
||||
score += position_score * 0.2
|
||||
|
||||
return min(score, 1.0)
|
||||
|
||||
def _calculate_relative_position_similarity(self, xml_elem, svg_elem):
|
||||
"""计算相对位置相似度"""
|
||||
# 这里简化处理,实际应该考虑屏幕尺寸的缩放
|
||||
return 0.5 # 暂时返回中等相似度
|
||||
|
||||
def _calculate_position_difference(self, xml_elem, svg_elem):
|
||||
"""计算位置差异"""
|
||||
xml_bounds = xml_elem.get('bounds', {})
|
||||
svg_geometry = svg_elem.get('geometry', {})
|
||||
|
||||
if not xml_bounds or not svg_geometry:
|
||||
return {'x': 0, 'y': 0}
|
||||
|
||||
return {
|
||||
'x': abs(xml_bounds.get('center_x', 0) - svg_geometry.get('center_x', 0)),
|
||||
'y': abs(xml_bounds.get('center_y', 0) - svg_geometry.get('center_y', 0))
|
||||
}
|
||||
|
||||
def _calculate_size_difference(self, xml_elem, svg_elem):
|
||||
"""计算尺寸差异"""
|
||||
xml_bounds = xml_elem.get('bounds', {})
|
||||
svg_geometry = svg_elem.get('geometry', {})
|
||||
|
||||
if not xml_bounds or not svg_geometry:
|
||||
return {'width': 0, 'height': 0}
|
||||
|
||||
return {
|
||||
'width': abs(xml_bounds.get('width', 0) - svg_geometry.get('width', 0)),
|
||||
'height': abs(xml_bounds.get('height', 0) - svg_geometry.get('height', 0))
|
||||
}
|
||||
|
||||
def _calculate_layout_similarity(self, result):
|
||||
"""计算整体布局相似度"""
|
||||
total_elements = max(result['xml_elements_count'], result['svg_elements_count'])
|
||||
if total_elements == 0:
|
||||
result['layout_similarity'] = 0.0
|
||||
return
|
||||
|
||||
matched_count = len(result['matched_elements'])
|
||||
base_similarity = matched_count / total_elements
|
||||
|
||||
# 考虑匹配质量
|
||||
if result['matched_elements']:
|
||||
avg_match_quality = sum(match['similarity'] for match in result['matched_elements']) / len(result['matched_elements'])
|
||||
result['layout_similarity'] = base_similarity * avg_match_quality
|
||||
else:
|
||||
result['layout_similarity'] = 0.0
|
||||
|
||||
def get_layout_comparison_summary(self):
|
||||
"""获取布局比对摘要"""
|
||||
if not self.layout_comparison_result:
|
||||
return "未进行布局比对"
|
||||
|
||||
result = self.layout_comparison_result
|
||||
summary = f"""
|
||||
📊 布局比对摘要:
|
||||
• XML元素数量: {result['xml_elements_count']}
|
||||
• SVG元素数量: {result['svg_elements_count']}
|
||||
• 匹配元素数量: {len(result['matched_elements'])}
|
||||
• 布局相似度: {result['layout_similarity']:.1%}
|
||||
• 未匹配XML元素: {len(result['unmatched_xml'])}
|
||||
• 未匹配SVG元素: {len(result['unmatched_svg'])}
|
||||
"""
|
||||
|
||||
if result['layout_similarity'] >= 0.8:
|
||||
summary += "\n✅ 布局高度一致"
|
||||
elif result['layout_similarity'] >= 0.6:
|
||||
summary += "\n⚠️ 布局基本一致,有少量差异"
|
||||
else:
|
||||
summary += "\n❌ 布局差异较大,建议检查"
|
||||
|
||||
return summary
|
||||
Reference in New Issue
Block a user