This commit includes several updates to the Python_OpenSource_DXF project: - The core script draw_table_from_template.py has been refactored for better logic and clarity. - New utility scripts such as diagnose_blocks.py, export_dxf.py, and plot_by_block_name.py have been added to enhance diagnostic and plotting capabilities. - The convert_dxf_to_pdf.py script was removed as its functionality is now covered by other modules. - README.md and .specstory documentation have been updated to reflect these changes.
385 lines
19 KiB
Python
385 lines
19 KiB
Python
import ezdxf
|
||
import matplotlib.pyplot as plt
|
||
import matplotlib.patches as patches
|
||
from pathlib import Path
|
||
import argparse
|
||
import os
|
||
import sys
|
||
|
||
from ezdxf.addons.drawing import RenderContext, Frontend
|
||
from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
|
||
from ezdxf.addons.drawing.properties import Configuration
|
||
from ezdxf import bbox
|
||
|
||
# 配置中文字体支持
|
||
import matplotlib.font_manager as fm
|
||
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] # 支持中文显示
|
||
plt.rcParams['axes.unicode_minus'] = False # 正常显示负号
|
||
|
||
# 字体目录配置
|
||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
FONT_DIR = os.path.join(SCRIPT_DIR, "fonts")
|
||
|
||
# 纸张尺寸定义 (mm)
|
||
PAPER_SIZES_MM = {
|
||
"A4": {"landscape": (297, 210), "portrait": (210, 297)},
|
||
"A3": {"landscape": (420, 297), "portrait": (297, 420)},
|
||
"A2": {"landscape": (594, 420), "portrait": (420, 594)},
|
||
"A1": {"landscape": (841, 594), "portrait": (594, 841)},
|
||
"A0": {"landscape": (1189, 841), "portrait": (841, 1189)},
|
||
}
|
||
|
||
PRINTABLE_BLOCKS = {
|
||
"A$C2EB80DB8": ("A1", "portrait"),
|
||
"A$C1CC9093B": ("A1", "landscape"),
|
||
"A$C6D564680": ("A2", "portrait"),
|
||
"新块": ("A3", "landscape"),
|
||
"新块1": ("A4", "portrait"),
|
||
}
|
||
|
||
def export_frame_to_pdf_matplotlib(doc, search_space, block_ref, settings, output_filename):
|
||
"""使用ezdxf专业渲染系统导出PDF,支持完整的CAD实体和中文字体"""
|
||
block_name = block_ref.dxf.name
|
||
paper_size_key, orientation = settings
|
||
|
||
try:
|
||
page_dimensions = PAPER_SIZES_MM[paper_size_key][orientation]
|
||
except KeyError:
|
||
print(f" - 错误:为块 '{block_name}' 提供的设置无效")
|
||
return
|
||
|
||
print(f" - 图框: '{block_name}', 设置: {paper_size_key} {orientation} ({page_dimensions[0]}x{page_dimensions[1]}mm)")
|
||
|
||
try:
|
||
# 配置字体支持
|
||
if FONT_DIR not in ezdxf.options.support_dirs:
|
||
ezdxf.options.support_dirs.append(FONT_DIR)
|
||
print(f" - 已添加字体目录: {FONT_DIR}")
|
||
|
||
# 获取块中的所有实体
|
||
entities = list(block_ref.virtual_entities())
|
||
if not entities:
|
||
print(f" - 警告:块 '{block_name}' 是空的,跳过")
|
||
return
|
||
|
||
print(f" - 找到 {len(entities)} 个实体")
|
||
|
||
# 计算边界框
|
||
bbox_result = bbox.extents(entities)
|
||
if bbox_result.extmin is None or bbox_result.extmax is None:
|
||
print(f" - 警告:块 '{block_name}' 的边界为空,跳过")
|
||
return
|
||
|
||
minx, miny = bbox_result.extmin.x, bbox_result.extmin.y
|
||
maxx, maxy = bbox_result.extmax.x, bbox_result.extmax.y
|
||
|
||
print(f" - 边界: ({minx:.2f}, {miny:.2f}) 到 ({maxx:.2f}, {maxy:.2f})")
|
||
|
||
# 配置渲染器 - 参考export_dxf.py的成功配置
|
||
config = Configuration.defaults().with_changes(
|
||
pdsize=0, # 点的大小设置为0,避免3D点处理问题
|
||
)
|
||
|
||
# 设置正确的PDF纸张尺寸(英寸)
|
||
fig_width_in = page_dimensions[0] / 25.4
|
||
fig_height_in = page_dimensions[1] / 25.4
|
||
|
||
print(f" - 设置PDF尺寸: {fig_width_in:.2f}x{fig_height_in:.2f}英寸")
|
||
|
||
# 创建matplotlib图形,固定尺寸
|
||
fig = plt.figure(figsize=(fig_width_in, fig_height_in))
|
||
ax = fig.add_axes([0, 0, 1, 1])
|
||
|
||
# 设置视图边界为块的边界
|
||
ax.set_xlim(minx, maxx)
|
||
ax.set_ylim(miny, maxy)
|
||
ax.set_aspect('equal')
|
||
ax.axis('off')
|
||
|
||
print(f" - 正确的方法:先渲染整个布局,再裁剪到块区域...")
|
||
|
||
# 正确的方法:渲染整个布局的所有内容,然后设置视图为块的边界
|
||
try:
|
||
ctx = RenderContext(doc)
|
||
backend = MatplotlibBackend(ax)
|
||
frontend = Frontend(ctx, backend, config=config)
|
||
|
||
print(f" - 渲染整个布局的内容(包含图纸的所有图形)...")
|
||
|
||
# 渲染整个布局
|
||
frontend.draw_layout(search_space, finalize=True)
|
||
|
||
print(f" - 布局渲染完成,视图已设置为块 '{block_name}' 的边界区域")
|
||
|
||
except Exception as layout_error:
|
||
print(f" - 完整布局渲染失败: {layout_error}")
|
||
print(f" - 尝试带过滤的布局渲染...")
|
||
|
||
try:
|
||
# 过滤掉3D坐标问题的实体
|
||
def safe_filter(entity):
|
||
entity_type = entity.dxftype()
|
||
# 跳过可能导致3D坐标问题的实体
|
||
problematic_types = {"HATCH", "SOLID", "REGION", "BODY", "3DSOLID", "MESH", "SURFACE"}
|
||
return entity_type not in problematic_types
|
||
|
||
ctx = RenderContext(doc)
|
||
backend = MatplotlibBackend(ax)
|
||
frontend = Frontend(ctx, backend, config=config)
|
||
|
||
frontend.draw_layout(search_space, finalize=True, filter_func=safe_filter)
|
||
print(f" - 过滤渲染成功")
|
||
|
||
except Exception as filter_error:
|
||
print(f" - 过滤渲染也失败: {filter_error}")
|
||
print(f" - 使用matplotlib直接渲染所有实体...")
|
||
|
||
# 最后备用方案:matplotlib直接渲染所有布局实体
|
||
ax.clear()
|
||
ax.set_xlim(minx, maxx)
|
||
ax.set_ylim(miny, maxy)
|
||
ax.set_aspect('equal')
|
||
ax.axis('off')
|
||
|
||
all_entities = list(search_space.query('*')) # 获取布局中的所有实体
|
||
drawn_count = 0
|
||
|
||
print(f" - 开始渲染布局中的 {len(all_entities)} 个实体...")
|
||
|
||
for entity in all_entities:
|
||
try:
|
||
entity_type = entity.dxftype()
|
||
|
||
# 检查实体是否在块的边界区域内
|
||
if hasattr(entity.dxf, 'start') and hasattr(entity.dxf, 'end'):
|
||
# 线条类实体
|
||
start = entity.dxf.start
|
||
end = entity.dxf.end
|
||
if not (minx <= start.x <= maxx and miny <= start.y <= maxy and
|
||
minx <= end.x <= maxx and miny <= end.y <= maxy):
|
||
continue # 跳过不在区域内的实体
|
||
elif hasattr(entity.dxf, 'center'):
|
||
# 圆形类实体
|
||
center = entity.dxf.center
|
||
if not (minx <= center.x <= maxx and miny <= center.y <= maxy):
|
||
continue
|
||
elif hasattr(entity.dxf, 'insert'):
|
||
# 文本类实体
|
||
insert = entity.dxf.insert
|
||
if not (minx <= insert.x <= maxx and miny <= insert.y <= maxy):
|
||
continue
|
||
|
||
# 渲染在区域内的实体
|
||
if entity_type == "LINE":
|
||
start = entity.dxf.start
|
||
end = entity.dxf.end
|
||
ax.plot([start.x, end.x], [start.y, end.y], 'k-', linewidth=0.5)
|
||
drawn_count += 1
|
||
|
||
elif entity_type == "CIRCLE":
|
||
center = entity.dxf.center
|
||
radius = entity.dxf.radius
|
||
circle = patches.Circle((center.x, center.y), radius, fill=False, edgecolor='black', linewidth=0.5)
|
||
ax.add_patch(circle)
|
||
drawn_count += 1
|
||
|
||
elif entity_type == "ARC":
|
||
center = entity.dxf.center
|
||
radius = entity.dxf.radius
|
||
start_angle = entity.dxf.start_angle
|
||
end_angle = entity.dxf.end_angle
|
||
arc = patches.Arc((center.x, center.y), 2*radius, 2*radius,
|
||
theta1=start_angle, theta2=end_angle,
|
||
edgecolor='black', linewidth=0.5)
|
||
ax.add_patch(arc)
|
||
drawn_count += 1
|
||
|
||
elif entity_type in ["TEXT", "MTEXT"]:
|
||
insert_point = entity.dxf.insert if hasattr(entity.dxf, 'insert') else (0, 0, 0)
|
||
text_content = entity.dxf.text if hasattr(entity.dxf, 'text') else ""
|
||
if text_content.strip():
|
||
ax.text(insert_point.x, insert_point.y, text_content,
|
||
fontsize=6, ha='left', va='bottom', color='black')
|
||
drawn_count += 1
|
||
|
||
elif entity_type == "LWPOLYLINE":
|
||
points = list(entity.get_points())
|
||
if len(points) > 1:
|
||
x_coords = [p[0] for p in points]
|
||
y_coords = [p[1] for p in points]
|
||
ax.plot(x_coords, y_coords, 'k-', linewidth=0.5)
|
||
drawn_count += 1
|
||
|
||
elif entity_type == "DIMENSION":
|
||
# 处理尺寸标注
|
||
try:
|
||
if hasattr(entity, 'get_measurement'):
|
||
# 简化:绘制尺寸线的基本线条
|
||
if hasattr(entity.dxf, 'defpoint') and hasattr(entity.dxf, 'defpoint2'):
|
||
p1 = entity.dxf.defpoint
|
||
p2 = entity.dxf.defpoint2 if hasattr(entity.dxf, 'defpoint2') else p1
|
||
ax.plot([p1.x, p2.x], [p1.y, p2.y], 'k-', linewidth=0.3)
|
||
drawn_count += 1
|
||
except:
|
||
pass
|
||
|
||
elif entity_type == "INSERT":
|
||
# 处理嵌套INSERT块 - 这是关键的缺失部分!
|
||
try:
|
||
nested_entities = list(entity.virtual_entities())
|
||
insert_drawn = 0
|
||
|
||
for nested_entity in nested_entities:
|
||
try:
|
||
nested_type = nested_entity.dxftype()
|
||
|
||
if nested_type == "LINE":
|
||
start = nested_entity.dxf.start
|
||
end = nested_entity.dxf.end
|
||
ax.plot([start.x, end.x], [start.y, end.y], 'k-', linewidth=0.5)
|
||
insert_drawn += 1
|
||
|
||
elif nested_type == "CIRCLE":
|
||
center = nested_entity.dxf.center
|
||
radius = nested_entity.dxf.radius
|
||
circle = patches.Circle((center.x, center.y), radius, fill=False, edgecolor='black', linewidth=0.5)
|
||
ax.add_patch(circle)
|
||
insert_drawn += 1
|
||
|
||
elif nested_type in ["TEXT", "MTEXT"]:
|
||
insert_point = nested_entity.dxf.insert if hasattr(nested_entity.dxf, 'insert') else (0, 0, 0)
|
||
text_content = nested_entity.dxf.text if hasattr(nested_entity.dxf, 'text') else ""
|
||
if text_content.strip():
|
||
ax.text(insert_point.x, insert_point.y, text_content,
|
||
fontsize=6, ha='left', va='bottom', color='blue') # 用蓝色区分嵌套块文字
|
||
insert_drawn += 1
|
||
|
||
elif nested_type == "LWPOLYLINE":
|
||
points = list(nested_entity.get_points())
|
||
if len(points) > 1:
|
||
x_coords = [p[0] for p in points]
|
||
y_coords = [p[1] for p in points]
|
||
ax.plot(x_coords, y_coords, 'k-', linewidth=0.5)
|
||
insert_drawn += 1
|
||
except:
|
||
continue
|
||
|
||
if insert_drawn > 0:
|
||
drawn_count += insert_drawn
|
||
|
||
except:
|
||
pass
|
||
|
||
except Exception as e:
|
||
continue
|
||
|
||
# 最后,确保渲染当前目标块本身的内容(框架和标签)
|
||
print(f" - 正在渲染目标块 '{block_name}' 本身的内容...")
|
||
block_drawn = 0
|
||
|
||
for block_entity in entities: # entities 是当前块的虚拟实体
|
||
try:
|
||
entity_type = block_entity.dxftype()
|
||
|
||
if entity_type == "LINE":
|
||
start = block_entity.dxf.start
|
||
end = block_entity.dxf.end
|
||
ax.plot([start.x, end.x], [start.y, end.y], 'r-', linewidth=1.0) # 用红色突出显示块框架
|
||
block_drawn += 1
|
||
|
||
elif entity_type in ["TEXT", "MTEXT"]:
|
||
insert_point = block_entity.dxf.insert if hasattr(block_entity.dxf, 'insert') else (0, 0, 0)
|
||
text_content = block_entity.dxf.text if hasattr(block_entity.dxf, 'text') else ""
|
||
if text_content.strip():
|
||
ax.text(insert_point.x, insert_point.y, text_content,
|
||
fontsize=8, ha='left', va='bottom', color='red', weight='bold') # 用红色粗体显示块标签
|
||
block_drawn += 1
|
||
|
||
elif entity_type == "LWPOLYLINE":
|
||
points = list(block_entity.get_points())
|
||
if len(points) > 1:
|
||
x_coords = [p[0] for p in points]
|
||
y_coords = [p[1] for p in points]
|
||
ax.plot(x_coords, y_coords, 'r-', linewidth=1.0) # 用红色显示块边框
|
||
block_drawn += 1
|
||
|
||
elif entity_type == "INSERT":
|
||
# 处理目标块内的嵌套块
|
||
nested_entities = list(block_entity.virtual_entities())
|
||
for nested in nested_entities:
|
||
try:
|
||
if nested.dxftype() == "LINE":
|
||
start = nested.dxf.start
|
||
end = nested.dxf.end
|
||
ax.plot([start.x, end.x], [start.y, end.y], 'r-', linewidth=0.8)
|
||
block_drawn += 1
|
||
except:
|
||
pass
|
||
|
||
except Exception as e:
|
||
continue
|
||
|
||
print(f" - 目标块本身渲染完成,绘制了 {block_drawn} 个块实体")
|
||
print(f" - matplotlib区域渲染总计完成,绘制了 {drawn_count + block_drawn} 个实体")
|
||
|
||
# 保存为PDF,使用固定尺寸,不自动调整边界
|
||
plt.savefig(str(output_filename), format='pdf', dpi=300,
|
||
bbox_inches=None, pad_inches=0)
|
||
plt.close()
|
||
|
||
print(f" - 已保存到: {output_filename}")
|
||
|
||
except Exception as e:
|
||
print(f" - 错误:处理块 '{block_name}' 时出现异常: {e}")
|
||
return
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="使用matplotlib渲染DXF块为PDF")
|
||
parser.add_argument("input_dxf", type=Path, help="DXF文件路径")
|
||
parser.add_argument("-o", "--output", type=Path, default=Path("plots_output"), help="输出目录")
|
||
parser.add_argument("-l", "--layout", type=str, default=None, help="布局名称")
|
||
args = parser.parse_args()
|
||
|
||
output_dir = args.output
|
||
output_dir.mkdir(exist_ok=True)
|
||
|
||
try:
|
||
doc = ezdxf.readfile(args.input_dxf)
|
||
except Exception as e:
|
||
print(f"错误:无法读取文件 '{args.input_dxf}': {e}")
|
||
return
|
||
|
||
if args.layout:
|
||
try:
|
||
search_space = doc.layouts.get(args.layout)
|
||
print(f"--- 目标空间: 布局 '{args.layout}' ---")
|
||
except KeyError:
|
||
available = ", ".join(doc.layouts.names())
|
||
print(f"错误: 找不到布局 '{args.layout}'。可用布局: [{available}]")
|
||
return
|
||
else:
|
||
search_space = doc.modelspace()
|
||
print("--- 目标空间: 模型空间 ---")
|
||
|
||
all_block_references = search_space.query('INSERT')
|
||
frame_references = [ref for ref in all_block_references if ref.dxf.name in PRINTABLE_BLOCKS]
|
||
|
||
if not frame_references:
|
||
print("未找到任何配置的可打印图框块")
|
||
return
|
||
|
||
print(f"共找到 {len(frame_references)} 个待处理的图框块")
|
||
|
||
for i, block_ref in enumerate(frame_references):
|
||
block_name = block_ref.dxf.name
|
||
settings = PRINTABLE_BLOCKS[block_name]
|
||
|
||
print(f"\n处理第 {i+1} 个图框: '{block_name}'")
|
||
output_filename = output_dir / f"{block_name}_{i+1}.pdf"
|
||
|
||
export_frame_to_pdf_matplotlib(doc, search_space, block_ref, settings, output_filename)
|
||
|
||
print("\n处理完毕!")
|
||
|
||
if __name__ == "__main__":
|
||
main() |