cad2image 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cad2image/__init__.py +34 -0
- cad2image/__main__.py +6 -0
- cad2image/batch.py +158 -0
- cad2image/cli.py +152 -0
- cad2image/config.py +164 -0
- cad2image/dwg2dxf.py +175 -0
- cad2image/fonts/NOTICE +6 -0
- cad2image/fonts/NotoSansMono-Regular.ttf +0 -0
- cad2image/fonts/NotoSansSC-Regular.otf +0 -0
- cad2image/fonts/OFL.txt +93 -0
- cad2image/plotstyle.py +52 -0
- cad2image/py.typed +0 -0
- cad2image/render.py +358 -0
- cad2image-0.1.0.dist-info/LICENSE +21 -0
- cad2image-0.1.0.dist-info/METADATA +287 -0
- cad2image-0.1.0.dist-info/RECORD +19 -0
- cad2image-0.1.0.dist-info/WHEEL +5 -0
- cad2image-0.1.0.dist-info/entry_points.txt +2 -0
- cad2image-0.1.0.dist-info/top_level.txt +1 -0
cad2image/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""cad2image — DWG/DXF → PNG/SVG 渲染管线。
|
|
2
|
+
|
|
3
|
+
用 ODA File Converter(DWG→DXF)+ ezdxf(DXF→PNG/SVG)重建 Acme CAD Converter
|
|
4
|
+
的渲染能力,消除 GDI 栅格化路径产生的「残余杂线」问题。
|
|
5
|
+
|
|
6
|
+
公开接口:
|
|
7
|
+
- convert_dwg_to_dxf / convert_directory:DWG → DXF(ODA File Converter 封装)
|
|
8
|
+
- render_dxf / render_to_png / render_to_svg:DXF → PNG/SVG(ezdxf 渲染核心)
|
|
9
|
+
- process_dwg / process_directory:单文件 / 目录批处理
|
|
10
|
+
- RenderOptions:渲染参数
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from cad2image.batch import BatchResult, process_directory, process_dwg
|
|
14
|
+
from cad2image.config import RenderOptions, get_oda_converter_path
|
|
15
|
+
from cad2image.dwg2dxf import convert_directory, convert_dwg_to_dxf
|
|
16
|
+
from cad2image.plotstyle import get_lineweight, load_ctb
|
|
17
|
+
from cad2image.render import render_dxf, render_to_png, render_to_svg
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BatchResult",
|
|
21
|
+
"RenderOptions",
|
|
22
|
+
"convert_directory",
|
|
23
|
+
"convert_dwg_to_dxf",
|
|
24
|
+
"get_lineweight",
|
|
25
|
+
"get_oda_converter_path",
|
|
26
|
+
"load_ctb",
|
|
27
|
+
"process_directory",
|
|
28
|
+
"process_dwg",
|
|
29
|
+
"render_dxf",
|
|
30
|
+
"render_to_png",
|
|
31
|
+
"render_to_svg",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
cad2image/__main__.py
ADDED
cad2image/batch.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""批量处理:多文件转换与渲染,支持部分失败不中断整批。
|
|
2
|
+
|
|
3
|
+
批处理遵循「单条失败不中止整批」原则:每个文件独立转换与渲染,异常被捕获并记录,
|
|
4
|
+
最终返回包含 ``successes`` / ``failures`` 的结果对象。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tempfile
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from cad2image.config import RenderOptions
|
|
14
|
+
from cad2image.dwg2dxf import convert_directory, convert_dwg_to_dxf
|
|
15
|
+
from cad2image.render import render_dxf
|
|
16
|
+
|
|
17
|
+
# 支持输出的图片格式。
|
|
18
|
+
_SUPPORTED_FORMATS = {"png", "svg"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BatchFailure:
|
|
23
|
+
"""单个文件处理失败的信息。
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
source: 源 DWG 文件路径。
|
|
27
|
+
stage: 失败阶段,``convert``(DWG→DXF)或 ``render``(DXF→图片)。
|
|
28
|
+
error: 错误信息。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
source: Path
|
|
32
|
+
stage: str
|
|
33
|
+
error: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class BatchResult:
|
|
38
|
+
"""批处理结果。
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
successes: 成功生成的图片文件路径列表。
|
|
42
|
+
failures: 失败项列表。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
successes: list[Path] = field(default_factory=list)
|
|
46
|
+
failures: list[BatchFailure] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def success_count(self) -> int:
|
|
50
|
+
"""成功文件数。"""
|
|
51
|
+
return len(self.successes)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def failure_count(self) -> int:
|
|
55
|
+
"""失败文件数。"""
|
|
56
|
+
return len(self.failures)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def process_dwg(
|
|
60
|
+
dwg_path: str | Path,
|
|
61
|
+
output_dir: str | Path,
|
|
62
|
+
options: RenderOptions,
|
|
63
|
+
*,
|
|
64
|
+
output_format: str = "png",
|
|
65
|
+
oda_path: str | Path | None = None,
|
|
66
|
+
) -> Path:
|
|
67
|
+
"""处理单个 DWG:DWG→DXF→图片。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
dwg_path: 源 DWG 文件路径。
|
|
71
|
+
output_dir: 图片输出目录。
|
|
72
|
+
options: 渲染参数。
|
|
73
|
+
output_format: 输出格式,``png`` 或 ``svg``。
|
|
74
|
+
oda_path: ODA File Converter 可执行文件路径。
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
生成的图片文件路径。
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: 当 ``output_format`` 不受支持时。
|
|
81
|
+
FileNotFoundError: 当源文件或 ODA 可执行文件不存在时。
|
|
82
|
+
RuntimeError: 当转换或渲染失败时。
|
|
83
|
+
"""
|
|
84
|
+
_validate_format(output_format)
|
|
85
|
+
source = Path(dwg_path)
|
|
86
|
+
target_dir = Path(output_dir)
|
|
87
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
with tempfile.TemporaryDirectory(prefix="oda_single_") as dxf_dir:
|
|
90
|
+
dxf = convert_dwg_to_dxf(source, dxf_dir, oda_path=oda_path)
|
|
91
|
+
output = target_dir / f"{source.stem}.{output_format}"
|
|
92
|
+
render_dxf(dxf, output, options)
|
|
93
|
+
return output
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def process_directory(
|
|
97
|
+
input_dir: str | Path,
|
|
98
|
+
output_dir: str | Path,
|
|
99
|
+
options: RenderOptions,
|
|
100
|
+
*,
|
|
101
|
+
output_format: str = "png",
|
|
102
|
+
recursive: bool = False,
|
|
103
|
+
oda_path: str | Path | None = None,
|
|
104
|
+
) -> BatchResult:
|
|
105
|
+
"""批量处理目录内全部 DWG 文件。
|
|
106
|
+
|
|
107
|
+
先一次性目录级转换 DWG→DXF,再逐文件渲染。单文件失败不中断整批。
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
input_dir: 包含 DWG 的输入目录。
|
|
111
|
+
output_dir: 图片输出目录。
|
|
112
|
+
options: 渲染参数。
|
|
113
|
+
output_format: 输出格式,``png`` 或 ``svg``。
|
|
114
|
+
recursive: 是否递归处理子目录。
|
|
115
|
+
oda_path: ODA File Converter 可执行文件路径。
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
包含成功与失败明细的 ``BatchResult``。
|
|
119
|
+
"""
|
|
120
|
+
_validate_format(output_format)
|
|
121
|
+
source_dir = Path(input_dir)
|
|
122
|
+
target_dir = Path(output_dir)
|
|
123
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
dwg_files = sorted(source_dir.rglob("*.dwg") if recursive else source_dir.glob("*.dwg"))
|
|
126
|
+
result = BatchResult()
|
|
127
|
+
if not dwg_files:
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
with tempfile.TemporaryDirectory(prefix="oda_batch_") as dxf_dir:
|
|
131
|
+
try:
|
|
132
|
+
convert_directory(source_dir, dxf_dir, oda_path=oda_path, recursive=recursive)
|
|
133
|
+
except (FileNotFoundError, RuntimeError) as exc:
|
|
134
|
+
for dwg in dwg_files:
|
|
135
|
+
result.failures.append(BatchFailure(dwg, "convert", str(exc)))
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
for dwg in dwg_files:
|
|
139
|
+
relative = dwg.relative_to(source_dir)
|
|
140
|
+
dxf = Path(dxf_dir) / relative.with_suffix(".dxf")
|
|
141
|
+
if not dxf.is_file():
|
|
142
|
+
result.failures.append(BatchFailure(dwg, "convert", "未生成 DXF 文件"))
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
output = target_dir / relative.with_suffix(f".{output_format}")
|
|
146
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
render_dxf(dxf, output, options)
|
|
148
|
+
result.successes.append(output)
|
|
149
|
+
except (FileNotFoundError, ValueError, RuntimeError) as exc:
|
|
150
|
+
result.failures.append(BatchFailure(dwg, "render", str(exc)))
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _validate_format(output_format: str) -> None:
|
|
156
|
+
"""校验输出格式,非法时 fail-fast。"""
|
|
157
|
+
if output_format not in _SUPPORTED_FORMATS:
|
|
158
|
+
raise ValueError(f"不支持的输出格式 '{output_format}',仅支持:{', '.join(sorted(_SUPPORTED_FORMATS))}")
|
cad2image/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""命令行入口(typer)。
|
|
2
|
+
|
|
3
|
+
支持三种输入:
|
|
4
|
+
|
|
5
|
+
- 单个 DXF 文件 → 直接渲染为 PNG/SVG
|
|
6
|
+
- 单个 DWG 文件 → 先经 ODA 转 DXF,再渲染
|
|
7
|
+
- 目录 → 批量处理(部分失败不中断整批)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from cad2image.batch import BatchResult, process_directory
|
|
18
|
+
from cad2image.config import RenderOptions
|
|
19
|
+
from cad2image.dwg2dxf import convert_dwg_to_dxf
|
|
20
|
+
from cad2image.render import render_dxf
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="cad2image",
|
|
24
|
+
help="DWG/DXF → PNG/SVG 渲染管线(ODA File Converter + ezdxf)。",
|
|
25
|
+
add_completion=False,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 进程退出码语义(对齐 Acme 报告:0 成功 / 1 通用失败)。
|
|
29
|
+
_EXIT_OK = 0
|
|
30
|
+
_EXIT_ERROR = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
def render(
|
|
35
|
+
input_path: Path = typer.Argument(..., exists=True, help="输入:DWG/DXF 文件,或包含 DWG 的目录"),
|
|
36
|
+
output: Path = typer.Option(None, "--output", "-o", help="输出文件/目录;缺省与输入同目录"),
|
|
37
|
+
dpi: int = typer.Option(300, "--dpi", help="PNG 分辨率(DPI)"),
|
|
38
|
+
output_format: str = typer.Option("png", "--format", "-f", help="输出格式:png / svg"),
|
|
39
|
+
background: str = typer.Option("white", "--background", help="背景策略:default/white/black/off"),
|
|
40
|
+
color: str = typer.Option("color", "--color", help="颜色策略:color/monochrome/grayscale/black/white"),
|
|
41
|
+
lineweight: str = typer.Option("absolute", "--lineweight", help="线宽策略:absolute/relative"),
|
|
42
|
+
lineweight_scaling: float = typer.Option(1.0, "--lineweight-scaling", help="线宽整体缩放系数(仅绝对线宽生效)"),
|
|
43
|
+
min_lineweight: float = typer.Option(None, "--min-lineweight", help="最小打印线宽(mm)"),
|
|
44
|
+
relative_max_stroke_width: float = typer.Option(0.001, "--relative-max-stroke-width", help="相对线宽:最粗线宽占页面较小边比例(默认 0.1%)"),
|
|
45
|
+
relative_min_stroke_width: float = typer.Option(0.05, "--relative-min-stroke-width", help="相对线宽:最细线宽占最粗线宽比例(默认 5%)"),
|
|
46
|
+
ctb: str = typer.Option("", "--ctb", help="CTB 打印样式表路径"),
|
|
47
|
+
font_dir: str = typer.Option("", "--font-dir", help="附加的 SHX/TTF 字体目录"),
|
|
48
|
+
layout: str = typer.Option(None, "--layout", help="布局名;缺省渲染模型空间"),
|
|
49
|
+
width: float = typer.Option(None, "--width", help="页面宽度(mm)"),
|
|
50
|
+
height: float = typer.Option(None, "--height", help="页面高度(mm)"),
|
|
51
|
+
fit: bool = typer.Option(False, "--fit", help="按内容包围盒自适应页面尺寸"),
|
|
52
|
+
margin: float = typer.Option(3.0, "--margin", help="内容自适应页面时的四周余量(%,相对内容较小边)"),
|
|
53
|
+
recursive: bool = typer.Option(False, "--recursive", help="目录批量时递归处理子目录"),
|
|
54
|
+
oda_path: Path = typer.Option(None, "--oda-path", help="ODAFileConverter.exe 路径;缺省从环境变量/默认路径解析"),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""把 DWG/DXF 渲染为 PNG 或 SVG。"""
|
|
57
|
+
options = RenderOptions(
|
|
58
|
+
dpi=dpi,
|
|
59
|
+
background=background,
|
|
60
|
+
color_policy=color,
|
|
61
|
+
lineweight_policy=lineweight,
|
|
62
|
+
lineweight_scaling=lineweight_scaling,
|
|
63
|
+
min_lineweight=min_lineweight,
|
|
64
|
+
relative_max_stroke_width=relative_max_stroke_width,
|
|
65
|
+
relative_min_stroke_width=relative_min_stroke_width,
|
|
66
|
+
ctb=ctb,
|
|
67
|
+
font_dir=font_dir,
|
|
68
|
+
layout_name=layout,
|
|
69
|
+
width_mm=width,
|
|
70
|
+
height_mm=height,
|
|
71
|
+
fit_to_extents=fit,
|
|
72
|
+
margin=margin,
|
|
73
|
+
)
|
|
74
|
+
try:
|
|
75
|
+
_run(input_path, output, options, output_format, recursive, oda_path)
|
|
76
|
+
except (FileNotFoundError, ValueError, RuntimeError, NotImplementedError) as exc:
|
|
77
|
+
typer.echo(f"错误:{exc}", err=True)
|
|
78
|
+
raise typer.Exit(code=_EXIT_ERROR) from exc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run(
|
|
82
|
+
input_path: Path,
|
|
83
|
+
output: Path | None,
|
|
84
|
+
options: RenderOptions,
|
|
85
|
+
output_format: str,
|
|
86
|
+
recursive: bool,
|
|
87
|
+
oda_path: Path | None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""根据输入类型分发到单文件或目录批处理。"""
|
|
90
|
+
if input_path.is_dir():
|
|
91
|
+
_run_batch(input_path, output or input_path, options, output_format, recursive, oda_path)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if input_path.suffix.lower() == ".dxf":
|
|
95
|
+
target = output or input_path.with_suffix(f".{output_format}")
|
|
96
|
+
render_dxf(input_path, target, options)
|
|
97
|
+
typer.echo(f"已生成:{target}")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
if input_path.suffix.lower() == ".dwg":
|
|
101
|
+
_run_single_dwg(input_path, output, options, output_format, oda_path)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
raise ValueError(f"不支持的文件类型 '{input_path.suffix}',仅支持 .dwg / .dxf")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_single_dwg(
|
|
108
|
+
input_path: Path,
|
|
109
|
+
output: Path | None,
|
|
110
|
+
options: RenderOptions,
|
|
111
|
+
output_format: str,
|
|
112
|
+
oda_path: Path | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""处理单个 DWG:临时目录转换 + 渲染到指定输出。"""
|
|
115
|
+
target = output or input_path.with_suffix(f".{output_format}")
|
|
116
|
+
with tempfile.TemporaryDirectory(prefix="oda_cli_") as dxf_dir:
|
|
117
|
+
dxf = convert_dwg_to_dxf(input_path, dxf_dir, oda_path=oda_path)
|
|
118
|
+
render_dxf(dxf, target, options)
|
|
119
|
+
typer.echo(f"已生成:{target}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _run_batch(
|
|
123
|
+
input_dir: Path,
|
|
124
|
+
output_dir: Path,
|
|
125
|
+
options: RenderOptions,
|
|
126
|
+
output_format: str,
|
|
127
|
+
recursive: bool,
|
|
128
|
+
oda_path: Path | None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""批量处理目录,打印汇总报告;存在失败时返回非零退出码。"""
|
|
131
|
+
result = process_directory(
|
|
132
|
+
input_dir,
|
|
133
|
+
output_dir,
|
|
134
|
+
options,
|
|
135
|
+
output_format=output_format,
|
|
136
|
+
recursive=recursive,
|
|
137
|
+
oda_path=oda_path,
|
|
138
|
+
)
|
|
139
|
+
_print_report(result)
|
|
140
|
+
if result.failure_count > 0:
|
|
141
|
+
raise typer.Exit(code=_EXIT_ERROR)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _print_report(result: BatchResult) -> None:
|
|
145
|
+
"""打印批处理汇总,失败明细输出到 stderr。"""
|
|
146
|
+
typer.echo(f"完成:成功 {result.success_count},失败 {result.failure_count}")
|
|
147
|
+
for failure in result.failures:
|
|
148
|
+
typer.echo(f" 失败[{failure.stage}] {failure.source.name}: {failure.error}", err=True)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
app()
|
cad2image/config.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""配置解析与渲染参数映射。
|
|
2
|
+
|
|
3
|
+
提供两部分能力:
|
|
4
|
+
|
|
5
|
+
1. ODA File Converter 可执行文件路径的解析(环境变量优先,默认安装路径兜底)。
|
|
6
|
+
2. 将面向用户的 ``RenderOptions`` 映射为 ezdxf ``Configuration``,
|
|
7
|
+
集中处理 Acme CAD Converter 参数语义到 ezdxf 的对应关系。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ezdxf.addons.drawing import config as draw_config
|
|
17
|
+
|
|
18
|
+
# ODA File Converter 的默认安装路径(Windows)。允许通过环境变量覆盖。
|
|
19
|
+
_DEFAULT_ODA_PATH = Path(r"D:\ODA\ODAFileConverter_title 21.5.0\ODAFileConverter.exe")
|
|
20
|
+
|
|
21
|
+
# 覆盖 ODA 安装路径的环境变量名。
|
|
22
|
+
_ODA_PATH_ENV = "ODA_FILE_CONVERTER_PATH"
|
|
23
|
+
|
|
24
|
+
# 合法的背景、颜色与线宽策略取值,用于 fail-fast 校验。
|
|
25
|
+
_BACKGROUND_CHOICES = {"default", "white", "black", "off", "none"}
|
|
26
|
+
_COLOR_POLICY_CHOICES = {"color", "monochrome", "grayscale", "black", "white"}
|
|
27
|
+
_LINEWEIGHT_POLICY_CHOICES = {"absolute", "relative"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_oda_converter_path() -> Path:
|
|
31
|
+
"""返回 ODA File Converter 可执行文件的路径。
|
|
32
|
+
|
|
33
|
+
优先读取环境变量 ``ODA_FILE_CONVERTER_PATH``,未设置时回退到默认安装路径。
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
``ODAFileConverter.exe`` 的绝对路径。
|
|
37
|
+
"""
|
|
38
|
+
env_path = os.getenv(_ODA_PATH_ENV)
|
|
39
|
+
if env_path:
|
|
40
|
+
return Path(env_path).expanduser()
|
|
41
|
+
return _DEFAULT_ODA_PATH
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class RenderOptions:
|
|
46
|
+
"""面向用户的渲染参数。
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
dpi: 输出分辨率(PNG)。默认 300。
|
|
50
|
+
background: 背景策略,``default/white/black/off``。默认 ``white``。
|
|
51
|
+
color_policy: 颜色策略,``color/monochrome/grayscale/black/white``。默认 ``color``。
|
|
52
|
+
lineweight_policy: 线宽策略,``absolute/relative``。默认 ``absolute``。
|
|
53
|
+
lineweight_scaling: 线宽整体缩放系数,乘到每条线的线宽上(仅绝对线宽策略生效)。默认 1.0。
|
|
54
|
+
min_lineweight: 最小打印线宽(mm),``None`` 表示不设下限。
|
|
55
|
+
relative_max_stroke_width: 相对线宽策略下,最粗线宽(2.11mm)占页面较小边的比例。默认 0.001(0.1%)。
|
|
56
|
+
relative_min_stroke_width: 相对线宽策略下,最细线宽(0.05mm)占最粗线宽的比例。默认 0.05(5%)。
|
|
57
|
+
ctb: CTB 打印样式表路径,``""`` 表示不使用。
|
|
58
|
+
font_dir: 附加的 SHX/TTF 字体目录,``""`` 表示仅用系统字体。
|
|
59
|
+
layout_name: 要渲染的布局名,``None`` 表示模型空间。默认 ``None``。
|
|
60
|
+
width_mm: 显式页面宽度(mm),``None`` 表示按布局/范围自适应。
|
|
61
|
+
height_mm: 显式页面高度(mm),``None`` 表示按布局/范围自适应。
|
|
62
|
+
fit_to_extents: 是否按模型空间内容范围自适应页面尺寸。默认 ``False``。
|
|
63
|
+
margin: 内容范围自适应时的四周余量,按内容较小边的百分比(0–100)。默认 3.0。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
dpi: int = 300
|
|
67
|
+
background: str = "white"
|
|
68
|
+
color_policy: str = "color"
|
|
69
|
+
lineweight_policy: str = "absolute"
|
|
70
|
+
lineweight_scaling: float = 1.0
|
|
71
|
+
min_lineweight: float | None = None
|
|
72
|
+
relative_max_stroke_width: float = 0.001
|
|
73
|
+
relative_min_stroke_width: float = 0.05
|
|
74
|
+
ctb: str = ""
|
|
75
|
+
font_dir: str = ""
|
|
76
|
+
layout_name: str | None = None
|
|
77
|
+
width_mm: float | None = None
|
|
78
|
+
height_mm: float | None = None
|
|
79
|
+
fit_to_extents: bool = False
|
|
80
|
+
margin: float = 3.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_drawing_configuration(options: RenderOptions) -> draw_config.Configuration:
|
|
84
|
+
"""将 ``RenderOptions`` 映射为 ezdxf ``Configuration``。
|
|
85
|
+
|
|
86
|
+
在边界处完成校验(fail-fast),非法的策略取值抛出 ``ValueError``。
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
options: 面向用户的渲染参数。
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
可直接传给 ``ezdxf.addons.drawing.Frontend`` 的配置对象。
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: 当背景/颜色/线宽策略或 DPI 取值非法时。
|
|
96
|
+
"""
|
|
97
|
+
if options.dpi <= 0:
|
|
98
|
+
raise ValueError(f"DPI 必须为正整数,得到 {options.dpi}")
|
|
99
|
+
if options.lineweight_scaling <= 0:
|
|
100
|
+
raise ValueError(f"线宽缩放系数必须为正数,得到 {options.lineweight_scaling}")
|
|
101
|
+
if options.min_lineweight is not None and options.min_lineweight <= 0:
|
|
102
|
+
raise ValueError(f"最小线宽必须为正数,得到 {options.min_lineweight}")
|
|
103
|
+
if options.relative_max_stroke_width <= 0:
|
|
104
|
+
raise ValueError(f"相对线宽最粗比例必须为正数,得到 {options.relative_max_stroke_width}")
|
|
105
|
+
if options.relative_min_stroke_width <= 0:
|
|
106
|
+
raise ValueError(f"相对线宽最细比例必须为正数,得到 {options.relative_min_stroke_width}")
|
|
107
|
+
if options.margin < 0:
|
|
108
|
+
raise ValueError(f"页面余量百分比必须为非负数,得到 {options.margin}")
|
|
109
|
+
|
|
110
|
+
background = _map_background_policy(options.background)
|
|
111
|
+
color_policy = _map_color_policy(options.color_policy)
|
|
112
|
+
lineweight_policy = _map_lineweight_policy(options.lineweight_policy)
|
|
113
|
+
|
|
114
|
+
return draw_config.Configuration(
|
|
115
|
+
background_policy=background,
|
|
116
|
+
color_policy=color_policy,
|
|
117
|
+
lineweight_policy=lineweight_policy,
|
|
118
|
+
lineweight_scaling=options.lineweight_scaling,
|
|
119
|
+
min_lineweight=options.min_lineweight,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _map_background_policy(value: str) -> draw_config.BackgroundPolicy:
|
|
124
|
+
"""把背景策略字符串映射为 ``BackgroundPolicy`` 枚举。"""
|
|
125
|
+
normalized = value.strip().lower()
|
|
126
|
+
if normalized not in _BACKGROUND_CHOICES:
|
|
127
|
+
raise ValueError(f"非法的背景策略 '{value}',可选:{', '.join(sorted(_BACKGROUND_CHOICES))}")
|
|
128
|
+
return {
|
|
129
|
+
"default": draw_config.BackgroundPolicy.DEFAULT,
|
|
130
|
+
"white": draw_config.BackgroundPolicy.WHITE,
|
|
131
|
+
"black": draw_config.BackgroundPolicy.BLACK,
|
|
132
|
+
"off": draw_config.BackgroundPolicy.OFF,
|
|
133
|
+
"none": draw_config.BackgroundPolicy.OFF,
|
|
134
|
+
}[normalized]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _map_color_policy(value: str) -> draw_config.ColorPolicy:
|
|
138
|
+
"""把颜色策略字符串映射为 ``ColorPolicy`` 枚举。
|
|
139
|
+
|
|
140
|
+
Note:
|
|
141
|
+
ezdxf 1.1.x 无独立的灰度策略,``grayscale`` 暂以 ``MONOCHROME`` 近似,
|
|
142
|
+
待 Phase 2 通过像素级后处理实现真正的 256 级灰度。
|
|
143
|
+
"""
|
|
144
|
+
normalized = value.strip().lower()
|
|
145
|
+
if normalized not in _COLOR_POLICY_CHOICES:
|
|
146
|
+
raise ValueError(f"非法的颜色策略 '{value}',可选:{', '.join(sorted(_COLOR_POLICY_CHOICES))}")
|
|
147
|
+
return {
|
|
148
|
+
"color": draw_config.ColorPolicy.COLOR,
|
|
149
|
+
"monochrome": draw_config.ColorPolicy.MONOCHROME,
|
|
150
|
+
"grayscale": draw_config.ColorPolicy.MONOCHROME,
|
|
151
|
+
"black": draw_config.ColorPolicy.BLACK,
|
|
152
|
+
"white": draw_config.ColorPolicy.WHITE,
|
|
153
|
+
}[normalized]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _map_lineweight_policy(value: str) -> draw_config.LineweightPolicy:
|
|
157
|
+
"""把线宽策略字符串映射为 ``LineweightPolicy`` 枚举。"""
|
|
158
|
+
normalized = value.strip().lower()
|
|
159
|
+
if normalized not in _LINEWEIGHT_POLICY_CHOICES:
|
|
160
|
+
raise ValueError(f"非法的线宽策略 '{value}',可选:{', '.join(sorted(_LINEWEIGHT_POLICY_CHOICES))}")
|
|
161
|
+
return {
|
|
162
|
+
"absolute": draw_config.LineweightPolicy.ABSOLUTE,
|
|
163
|
+
"relative": draw_config.LineweightPolicy.RELATIVE,
|
|
164
|
+
}[normalized]
|
cad2image/dwg2dxf.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""DWG → DXF 转换:ODA File Converter 封装。
|
|
2
|
+
|
|
3
|
+
ODA File Converter 是**目录级**转换工具:一次调用把输入目录内全部文件转换为指定
|
|
4
|
+
格式输出到输出目录。本模块负责处理「单文件 → 临时目录 → 收集输出 → 清理」的编排,
|
|
5
|
+
以及目录级批量转换,全部用 ``tempfile`` + ``with`` 管理临时资源。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from cad2image.config import get_oda_converter_path
|
|
16
|
+
|
|
17
|
+
# ODA File Converter 的输入版本(对 DWG 而言通常自动识别,指定为近年版本即可)。
|
|
18
|
+
DEFAULT_INPUT_VERSION = "ACAD2018"
|
|
19
|
+
# 输出版本为 "DXF" 表示输出 ASCII DXF(与 Acme 迁移方案一致)。
|
|
20
|
+
DEFAULT_OUTPUT_VERSION = "DXF"
|
|
21
|
+
|
|
22
|
+
# 单次转换的超时(秒),避免复杂文件导致进程挂死。
|
|
23
|
+
_CONVERSION_TIMEOUT_SECONDS = 300
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def convert_dwg_to_dxf(
|
|
27
|
+
dwg_path: str | Path,
|
|
28
|
+
output_dir: str | Path,
|
|
29
|
+
*,
|
|
30
|
+
oda_path: str | Path | None = None,
|
|
31
|
+
input_version: str = DEFAULT_INPUT_VERSION,
|
|
32
|
+
output_version: str = DEFAULT_OUTPUT_VERSION,
|
|
33
|
+
) -> Path:
|
|
34
|
+
"""把单个 DWG 文件转换为 DXF。
|
|
35
|
+
|
|
36
|
+
由于 ODA FC 是目录级工具,本函数会把目标 DWG 复制到临时输入目录,转换后从
|
|
37
|
+
临时输出目录收集生成的 DXF 到 ``output_dir``,最后清理临时目录。
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
dwg_path: 源 DWG 文件路径。
|
|
41
|
+
output_dir: 生成的 DXF 的存放目录。
|
|
42
|
+
oda_path: ODA File Converter 可执行文件路径,缺省时从环境/默认路径解析。
|
|
43
|
+
input_version: 输入版本,默认 ``ACAD2018``。
|
|
44
|
+
output_version: 输出版本,默认 ``DXF``。
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
生成的 DXF 文件路径。
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
FileNotFoundError: 当源 DWG 或 ODA 可执行文件不存在时。
|
|
51
|
+
RuntimeError: 当转换失败(ODA 退出码非零或未生成 DXF)时。
|
|
52
|
+
"""
|
|
53
|
+
source = Path(dwg_path)
|
|
54
|
+
if not source.is_file():
|
|
55
|
+
raise FileNotFoundError(f"源 DWG 文件不存在:{source}")
|
|
56
|
+
|
|
57
|
+
destination = Path(output_dir)
|
|
58
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
oda_exe = Path(oda_path) if oda_path else get_oda_converter_path()
|
|
61
|
+
if not oda_exe.is_file():
|
|
62
|
+
raise FileNotFoundError(f"ODA File Converter 可执行文件不存在:{oda_exe}")
|
|
63
|
+
|
|
64
|
+
with tempfile.TemporaryDirectory(prefix="oda_in_") as in_dir, tempfile.TemporaryDirectory(
|
|
65
|
+
prefix="oda_out_"
|
|
66
|
+
) as out_dir:
|
|
67
|
+
_copy_source_to_input(source, Path(in_dir))
|
|
68
|
+
_run_converter(
|
|
69
|
+
oda_exe,
|
|
70
|
+
input_dir=Path(in_dir),
|
|
71
|
+
output_dir=Path(out_dir),
|
|
72
|
+
input_version=input_version,
|
|
73
|
+
output_version=output_version,
|
|
74
|
+
)
|
|
75
|
+
return _collect_single_dxf(Path(out_dir), source.stem, destination)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def convert_directory(
|
|
79
|
+
input_dir: str | Path,
|
|
80
|
+
output_dir: str | Path,
|
|
81
|
+
*,
|
|
82
|
+
oda_path: str | Path | None = None,
|
|
83
|
+
input_version: str = DEFAULT_INPUT_VERSION,
|
|
84
|
+
output_version: str = DEFAULT_OUTPUT_VERSION,
|
|
85
|
+
recursive: bool = False,
|
|
86
|
+
) -> list[Path]:
|
|
87
|
+
"""把输入目录内全部 DWG 转换为 DXF。
|
|
88
|
+
|
|
89
|
+
直接调用 ODA FC 的目录级能力,一次转换整个目录,效率优于逐文件转换。
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
input_dir: 包含 DWG 文件的输入目录。
|
|
93
|
+
output_dir: DXF 输出目录。
|
|
94
|
+
oda_path: ODA File Converter 可执行文件路径,缺省时从环境/默认路径解析。
|
|
95
|
+
input_version: 输入版本,默认 ``ACAD2018``。
|
|
96
|
+
output_version: 输出版本,默认 ``DXF``。
|
|
97
|
+
recursive: 是否递归处理子目录。
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
生成的 DXF 文件路径列表。
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
FileNotFoundError: 当输入目录或 ODA 可执行文件不存在时。
|
|
104
|
+
RuntimeError: 当 ODA 转换失败时。
|
|
105
|
+
"""
|
|
106
|
+
source_dir = Path(input_dir)
|
|
107
|
+
if not source_dir.is_dir():
|
|
108
|
+
raise FileNotFoundError(f"输入目录不存在:{source_dir}")
|
|
109
|
+
|
|
110
|
+
target_dir = Path(output_dir)
|
|
111
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
oda_exe = Path(oda_path) if oda_path else get_oda_converter_path()
|
|
114
|
+
if not oda_exe.is_file():
|
|
115
|
+
raise FileNotFoundError(f"ODA File Converter 可执行文件不存在:{oda_exe}")
|
|
116
|
+
|
|
117
|
+
_run_converter(
|
|
118
|
+
oda_exe,
|
|
119
|
+
input_dir=source_dir,
|
|
120
|
+
output_dir=target_dir,
|
|
121
|
+
input_version=input_version,
|
|
122
|
+
output_version=output_version,
|
|
123
|
+
recursive=recursive,
|
|
124
|
+
)
|
|
125
|
+
return sorted(target_dir.glob("**/*.dxf") if recursive else target_dir.glob("*.dxf"))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _copy_source_to_input(source: Path, input_dir: Path) -> None:
|
|
129
|
+
"""把单个源 DWG 复制到临时输入目录。"""
|
|
130
|
+
shutil.copy2(source, input_dir / source.name)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _run_converter(
|
|
134
|
+
oda_exe: Path,
|
|
135
|
+
*,
|
|
136
|
+
input_dir: Path,
|
|
137
|
+
output_dir: Path,
|
|
138
|
+
input_version: str,
|
|
139
|
+
output_version: str,
|
|
140
|
+
recursive: bool = False,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""调用 ODA File Converter 命令行执行转换。"""
|
|
143
|
+
command = [
|
|
144
|
+
str(oda_exe),
|
|
145
|
+
str(input_dir),
|
|
146
|
+
str(output_dir),
|
|
147
|
+
input_version,
|
|
148
|
+
output_version,
|
|
149
|
+
"1" if recursive else "0",
|
|
150
|
+
"1", # audit:转换前审计文件
|
|
151
|
+
]
|
|
152
|
+
try:
|
|
153
|
+
completed = subprocess.run(
|
|
154
|
+
command,
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
timeout=_CONVERSION_TIMEOUT_SECONDS,
|
|
158
|
+
check=False,
|
|
159
|
+
)
|
|
160
|
+
except subprocess.TimeoutExpired as exc:
|
|
161
|
+
raise RuntimeError(f"ODA File Converter 转换超时(>{_CONVERSION_TIMEOUT_SECONDS}s):{input_dir}") from exc
|
|
162
|
+
|
|
163
|
+
if completed.returncode != 0:
|
|
164
|
+
stderr = (completed.stderr or "").strip()
|
|
165
|
+
raise RuntimeError(f"ODA File Converter 转换失败(退出码 {completed.returncode}):{stderr or '未知错误'}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _collect_single_dxf(output_dir: Path, stem: str, destination: Path) -> Path:
|
|
169
|
+
"""从临时输出目录收集生成的 DXF 并移动到目标目录。"""
|
|
170
|
+
generated = output_dir / f"{stem}.dxf"
|
|
171
|
+
if not generated.is_file():
|
|
172
|
+
raise RuntimeError(f"ODA 未生成预期的 DXF 文件:{stem}.dxf")
|
|
173
|
+
target = destination / generated.name
|
|
174
|
+
shutil.move(str(generated), str(target))
|
|
175
|
+
return target
|