dxfedit/03_Python_OpenSource_DXF/plot_by_block_name.py
puzzlesion 5e87bd7c33 refactor(dxf): optimize table drawing and add diagnostic scripts
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.
2025-09-12 17:21:08 +08:00

241 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ezdxf
from ezdxf.addons.drawing import Frontend, RenderContext, pymupdf
from ezdxf import bbox # 修正导入方式
import fitz # PyMuPDF用于二次裁剪 PDF 页面
from pathlib import Path
import argparse
# --- 配置区 (您需要修改的全部内容都在这里) ---
# 移除了 DXF_FILE 和 OUTPUT_DIR 的硬编码
# 3. 定义标准纸张尺寸库 (宽, 高) in mm.
# 这里我们为每种尺寸都定义了横向(landscape)和纵向(portrait)
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)},
}
# 4. 【核心配置】指定哪些块需要打印,以及如何打印
# 格式: "您的块名": ("纸张代号", "方向"),
# - "纸张代号" 必须是上面 PAPER_SIZES_MM 中定义的键 (例如 "A3")
# - "方向" 必须是 "landscape" (横向) 或 "portrait" (纵向)
PRINTABLE_BLOCKS = {
# !! 注意:您需要为它们指定正确的纸张尺寸和方向 !!
# 我在这里先做了假设,请根据实际情况修改
"A$C2EB80DB8": ("A1", "portrait"), # A1 竖向
"A$C1CC9093B": ("A1", "landscape"), # A1 横向
"A$C6D564680": ("A2", "portrait"), # A2 竖向
"新块": ("A3", "landscape"), # A3 横向
"新块1": ("A4", "portrait"), # A4 竖向
}
# --- 代码正文 (通常无需修改) ---
def export_frame_to_pdf(doc, search_space, block_ref, settings, output_filename):
"""
根据精确的设置将单个图框块导出为PDF。
"""
layout_name = None # 初始化变量,增加代码健壮性
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}' 提供的设置 ('{paper_size_key}', '{orientation}') 无效。请检查配置。")
return
print(f" - 图框: '{block_name}', 设置: {paper_size_key} {orientation} ({page_dimensions[0]}x{page_dimensions[1]}mm)")
try:
entities = list(block_ref.virtual_entities())
if not entities:
print(f" - 警告:块 '{block_name}' 是空的,跳过。")
return
# 修正:直接将实体列表传递给 extents() 函数,
# 它会自动处理不同类型的实体,包括 LWPolyline。
bbox_result = bbox.extents(entities)
if bbox_result.extmin is None or bbox_result.extmax is None:
print(f" - 警告:块 '{block_name}' 的边界为空,跳过。")
return
# 页面尺寸mm
page_width, page_height = page_dimensions
print(f" - 正在生成 PDF...")
ctx = RenderContext(doc)
backend = pymupdf.PyMuPdfBackend()
# 绘制整个布局,但跳过已知容易引发异常的实体类型
def _filter(e):
t = e.dxftype()
# 跳过容易引发坐标错误的实体类型
problematic_types = {"HATCH", "TRACE", "SOLID", "REGION", "BODY", "3DSOLID", "ACIS", "MESH"}
if t in problematic_types:
return False
# 检查实体是否有有效的坐标
try:
if hasattr(e, 'dxf'):
# 检查基本坐标属性
for attr in ['start', 'end', 'center', 'insert']:
if hasattr(e.dxf, attr):
point = getattr(e.dxf, attr, None)
if point is not None:
# 检查坐标值是否有效
if any(coord is None or not isinstance(coord, (int, float)) for coord in [point.x, point.y]):
return False
except (AttributeError, TypeError):
# 如果检查过程中出错,跳过该实体
return False
return True
try:
# 只绘制该块中的实体,而不是整个布局
entities = list(block_ref.virtual_entities())
filtered_entities = [e for e in entities if _filter(e)]
print(f" - 过滤后剩余实体数量: {len(filtered_entities)}/{len(entities)}")
# 直接绘制过滤后的实体
for entity in filtered_entities:
try:
ctx.push_state()
backend.set_background('#FFFFFF') # 设置白色背景
entity_type = entity.dxftype()
# 只处理最安全的实体类型
if entity_type in {"LINE", "CIRCLE", "ARC", "TEXT", "MTEXT"}:
from ezdxf.addons.drawing.frontend import Frontend
temp_frontend = Frontend(ctx, backend)
temp_frontend.draw_entity(entity, ctx.current_entity_properties())
except Exception as entity_error:
print(f" - 跳过有问题的实体 {entity.dxftype()}: {entity_error}")
continue
finally:
ctx.pop_state()
except Exception as draw_error:
print(f" - 警告:绘制过程中出现错误: {draw_error}")
print(f" - 尝试只绘制基本线条和文字...")
# 最后的备用方案:只绘制最基本的实体
try:
entities = list(block_ref.virtual_entities())
for entity in entities:
entity_type = entity.dxftype()
if entity_type == "LINE":
try:
from ezdxf.addons.drawing.frontend import Frontend
temp_frontend = Frontend(ctx, backend)
temp_frontend.draw_entity(entity, ctx.current_entity_properties())
except:
continue
except Exception as final_error:
print(f" - 错误:无法绘制任何实体,跳过此块: {final_error}")
return
from ezdxf.addons.drawing import layout as draw_layout
page_def = draw_layout.Page(page_width, page_height, units=draw_layout.Units.mm)
pdf_bytes = backend.get_pdf_bytes(page_def)
# 用 PyMuPDF 对生成的 PDF 进行页面级裁剪,裁到块的范围
mm_to_pt = 72.0 / 25.4
minx_mm, miny_mm = bbox_result.extmin.x, bbox_result.extmin.y
maxx_mm, maxy_mm = bbox_result.extmax.x, bbox_result.extmax.y
x0 = max(0.0, minx_mm) * mm_to_pt
x1 = max(0.0, maxx_mm) * mm_to_pt
top = max(0.0, page_height - maxy_mm) * mm_to_pt
bottom = max(0.0, page_height - miny_mm) * mm_to_pt
pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
page = pdf_doc[0]
crop_rect = fitz.Rect(x0, top, x1, bottom)
page.set_cropbox(crop_rect)
pdf_doc.save(str(output_filename))
print(f" - 已保存到: {output_filename}")
finally:
pass
def main():
parser = argparse.ArgumentParser(
description="根据DXF文件中的块名自动批量打印为带正确纸张尺寸的PDF。"
)
parser.add_argument("input_dxf", type=Path, help="要处理的源DXF文件的路径。")
parser.add_argument(
"-o", "--output",
type=Path,
default=Path("plots_output"),
help="指定PDF文件的输出目录 (默认为: plots_output)"
)
parser.add_argument(
"-l", "--layout",
type=str,
default=None,
help="指定要在哪个图纸布局(Layout)中查找块。如果未提供,则在模型空间(Modelspace)中查找。"
)
args = parser.parse_args()
output_dir = args.output
dxf_file = args.input_dxf
output_dir.mkdir(exist_ok=True)
try:
doc = ezdxf.readfile(dxf_file)
except IOError:
print(f"错误:无法读取文件 '{dxf_file}'")
return
except ezdxf.DXFStructureError as e:
print(f"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("--- 目标空间: 模型空间 ---")
# 查找目标空间中所有的块参照 (INSERT entities)
all_block_references = search_space.query('INSERT')
# 根据 PRINTABLE_BLOCKS 字典的键来筛选需要处理的块
frame_references = [
ref for ref in all_block_references
if ref.dxf.name in PRINTABLE_BLOCKS
]
if not frame_references:
print("未找到任何在配置字典 PRINTABLE_BLOCKS 中定义的可打印图框块。")
return
print(f"'{dxf_file}' 中共找到 {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}' (句柄: {block_ref.dxf.handle})")
output_filename = output_dir / f"{block_name}_{i+1}.pdf"
export_frame_to_pdf(doc, search_space, block_ref, settings, output_filename)
print("\n所有指定的图框处理完毕!")
if __name__ == "__main__":
main()