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.
241 lines
9.7 KiB
Python
241 lines
9.7 KiB
Python
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() |