
手上有个需求要把 CAD 图纸DWG转成 PDF 做归档要求走接口调用、Docker 部署端口还得能配。刚拿到需求的时候觉得这活儿不难真做下来发现坑比想象中多而且基本都出在 DWG 这个格式本身上。这篇只说 DWG 转 PDF 这一条链路把实现过程和踩的坑记一下。一、需求拆开看就三条上传一个 DWG 文件接口返回 PDF打成 Docker 镜像Windows、Linux 上都能跑服务端口可配置技术栈定的 Java 17 Spring Boot 3。端口可配这点 Spring Boot 本身就支持用 server.port 或者环境变量 SERVER_PORT 都行后面讲 Docker 的时候会说怎么串起来。二、选型纯 Java 走不通我一开始想得很简单找个 Java 库解析 DWG 就完事了。结果查了一圈发现不是这么回事。DWG 是 Autodesk 的私有二进制格式官方从来没公开过完整规范Java 生态里确实没有能打的解析库。Kabeja 算是比较有名的一个但它最后一次更新停在 2008 年而且根本没发布到 Maven Central想用还得自己打 jarjdxf 之类的库更小众也基本停更了。所以路线只能改成Spring Boot 负责服务化CAD 的解析和渲染交给成熟的开源引擎。最后定下来是这样图 1 DWG → PDF 转换流水线DWG → DXF用 LibreDWG 的 dwg2dxf 命令。LibreDWG 是 GNU 项目GPLv3官方说覆盖约 90% 的 DWG 实体DXF → PDF用 Python 的 ezdxf matplotlib 渲染。这是目前最靠谱的开源 DXF 渲染方案R12 到 R2018 都支持选 PDF 这条路其实比转图片省事。PDF 是矢量格式不用纠结分辨率和 DPI放大多少倍都不糊正好适合图纸归档和打印。这也是这个需求里最省心的一个决定。三、代码框架工程结构不复杂核心类就几个各管一摊DWG转pdf-Java/├── pom.xml Spring Boot 3.3.5 / Java 17├── Dockerfile 三阶段构建├── scripts/│ └── dxf2pdf.py DXF → PDF 渲染器└── src/main/java/com/example/dwgconverter/├── DwgConverterApplication.java├── config/ConverterProperties.java 外部化配置├── controller/ConversionController.java REST 接口├── controller/GlobalExceptionHandler.java├── model/ConversionOptions.java└── service/├── DwgConversionService.java 主流水线├── ProcessRunner.java 带超时的外部命令执行器└── ToolchainInspector.java 工具链自检对外就两个接口够用了方法路径说明GET/api/v1/health工具链自检dwg2dxf / ezdxf 是否就绪POST/api/v1/convert上传 DWGformatpdf 返回 PDF 文件Controller 本身没什么花活就是把参数收下来、交给 Service重点在异常统一收口转换失败都抛 ConversionException由 GlobalExceptionHandler 转成结构化 JSON前端好处理PostMapping(value /convert, consumes MediaType.MULTIPART_FORM_DATA_VALUE)public ResponseEntitybyte[] convert(RequestParam(file) MultipartFile file,RequestParam(value format, defaultValue pdf) String format,RequestParam(value monochrome, defaultValue false) boolean monochrome) {validateUpload(file);ConversionOptions options new ConversionOptions(background, monochrome);ConversionResult result conversionService.convert(openStream(file), file.getOriginalFilename(), OutputFormat.from(format), options);return asDownload(result); // Content-Disposition 用 RFC 5987 编码中文名不乱码}四、几个设计上的取舍4.1 用进程调用不碰 JNILibreDWG 有 C 接口理论上能用 JNI 或 JNA 直接调但我选了最土的 ProcessBuilder。原因很简单JNI 一旦涉及跨平台编译、崩溃隔离这些事就很麻烦。进程调用崩了顶多这个请求失败不会把整个 JVM 带走而且换实现特别容易想从 LibreDWG 换成 ODA File Converter改个配置路径就行一行 Java 代码都不用动。4.2 每次转换开一个独立临时目录每个请求一个临时目录转完递归删掉避免并发请求互相踩。调试的时候设 DWG_KEEP_TEMP_FILEStrue 就能把中间文件留下来看。private Path createWorkDir() {Path base Path.of(properties.getWorkDir());Files.createDirectories(base);return Files.createTempDirectory(base, job-);}private void cleanup(Path workDir) {if (properties.isKeepTempFiles()) { log.info(已保留中间文件{}, workDir); return; }try (StreamPath paths Files.walk(workDir)) {paths.sorted(Comparator.reverseOrder()).forEach(p - {try { Files.deleteIfExists(p); } catch (IOException e) { log.warn(删除失败 {}, p); }});}}4.3 外部命令必须带超时这点我觉得挺关键。matplotlib 渲染大图纸很吃 CPU万一张图有几十万个实体进程卡死请求就跟着一起挂。所以超时后直接 destroyForcibly()宁可这次转换失败也不能把线程池耗光boolean finished process.waitFor(timeoutSeconds, TimeUnit.SECONDS);if (!finished) {process.destroyForcibly();process.waitFor(5, TimeUnit.SECONDS);throw new ConversionException(外部命令执行超时 timeoutSeconds 秒);}4.4 成功判据看文件不看退出码这里有个小细节。dwg2dxf 遇到可恢复的告警也会返回非 0 退出码要是按退出码判断就会误判成失败。所以我改成以「DXF 文件到底有没有真的生成、大小是不是 0」作为判据非 0 但文件正常生成的就打个 warn 放行ProcessRunner.CommandResult result processRunner.run(command, workDir, properties.getTimeoutSeconds());if (!Files.isRegularFile(dxf) || sizeOf(dxf) 0) {throw new ConversionException(DWG 转 DXF 失败dwg2dxf 退出码 result.exitCode() tail(result.output()));}if (!result.isSuccess()) {log.warn(dwg2dxf 退出码为 {}但已生成 DXF继续处理, result.exitCode());}4.5 PDF 的字体嵌入这个是转 PDF 特有的坑。matplotlib 输出 PDF 时默认用 Type 3 字体这种字体在某些 PDF 阅读器里显示会发虚印刷厂那边也经常不认。改成嵌入 TrueType 就好了matplotlib.rcParams[pdf.fonttype] 42 # 42 嵌入 TrueType默认是 3Type 3matplotlib.rcParams[svg.fonttype] path # 文字转轮廓彻底摆脱字体依赖五、Docker 部署镜像用多阶段构建编译工具链不进最终镜像否则光一个 gcc 就能把镜像撑到 1G 以上# Stage 1编译 LibreDWG拿到 dwg2dxfFROM debian:bookworm-slim AS libredwgRUN sh ./autogen.sh ./configure --enable-release --disable-bindings \--disable-docs --disable-shared make -j$(nproc) make install# Stage 2构建 Spring Boot fat jarFROM maven:3.9.9-eclipse-temurin-17 AS buildRUN mvn -B -DskipTests package# Stage 3运行时只留 JRE Python dwg2dxfFROM eclipse-temurin:17-jre-jammyRUN apt-get install -y python3 python3-venv \ python3 -m venv /opt/venv \ /opt/venv/bin/pip install ezdxf1.3.5 matplotlib3.9.2COPY --fromlibredwg /opt/libredwg/usr/local/bin/ /usr/local/bin/COPY --frombuild /build/target/dwg-converter.jar /app/app.jarENTRYPOINT [/usr/bin/tini, --, sh, -c, exec java $JAVA_OPTS -jar /app/app.jar]端口这块容器内用 SERVER_PORT 环境变量控制端口映射两边跟着同一个变量走改一处就够# 默认 8080docker run -d -p 8080:8080 dwg-converter:1.0.0# 换成 9000docker run -d -e SERVER_PORT9000 -p 9000:9000 dwg-converter:1.0.0镜像本身是 Linux 容器Windows 的 Docker Desktop 和 Linux 上都能跑。六、拿真实图纸跑一遍代码写完当然得测。除了 LibreDWG 官方的测试样本我拿了一张真实的工程图纸 —— EM-DP01外观.dwg276 KB文件头是 AC1032也就是 AutoCAD 2018 格式。最终产出的 PDF 长这样图 2 EM-DP01外观.dwg 转换出的 PDF渲染首页矢量输出项值输入文件EM-DP01外观.dwg276,606 BAutoCAD 2018AC1032中间 DXF963,037 Bmodel space 共 1050 个实体输出 PDF142,247 B1 页矢量放大不失真不过这个结果是修完坑之后的中间过程挺曲折下面说。七、踩到的两个坑坑一给 dwg2dxf 加降级参数实体全丢这个是最坑的。我一开始为了「兼容性」给 dwg2dxf 加了 --as r2000想着输出老版本 DXF 兼容性更好。结果这张图转出来DXF 只有 27 KB实体数是 0渲染出来是一张纯白页PDF 只有 1.2 KB。要命的是接口返回的是 HTTP 200。要不是我顺手看了一眼文件大小这个问题可能就直接上线了。图 3 同一张图纸降级与不降级的输出对比去掉 --as 让它沿用源版本DXF 变成 963 KBmodel space 里 1050 个实体LINE 740 个、LWPOLYLINE 293 个、ARC 17 个。验证方法很简单用 ezdxf 数一下就知道import collections, ezdxfdoc ezdxf.readfile(EM-DP01.dxf)c collections.Counter(e.dxftype() for e in doc.modelspace())print(sum(c.values()), dict(c.most_common(3)))# 降级版0 {}# 源版本1050 {LINE: 740, LWPOLYLINE: 293, ARC: 17}所以 DWG_DXF_VERSION 的默认值我改成了空沿用源版本。降级这个选项还留着但默认不启用。坑二颜色 7 的实体在白底上直接隐形修完第一个坑PDF 从 1.2 KB 涨到 131 KB看着像是对了。但生成的图片还是很小我又去数了一下像素 —— 非白像素 0 个还是白页。PDF 有内容、位图没有这就很奇怪了。查下来是 ezdxf 的默认配色策略问题。AutoCAD 的模型空间背景是黑色的颜色 7 代表「白/黑」跟随背景变化。ezdxf 默认的 background policy 也遵循这个约定于是把颜色 7 的实体渲染成了白色。而我的画布是白底 —— 白线画在白纸上可不就看不见了。这张图正好 1050 个实体全是 BYLAYER所有图层颜色都是 7所以全军覆没。解决办法是显式指定策略别用默认值from ezdxf.addons.drawing.config import BackgroundPolicy, ColorPolicy, Configurationconfig Configuration(color_policyColorPolicy.COLOR,background_policyBackgroundPolicy.WHITE) # 关键别用 DEFAULTFrontend(ctx, backend, configconfig).draw_layout(layout, finalizeTrue)我把各种策略组合都跑了一遍结果挺说明问题的color policybackground policy非白像素COLORDEFAULT0COLORWHITE47916COLORMODELSPACE0MONOCHROME_LIGHT_BG任意47916WHITE任意0可以看到 BackgroundPolicy.DEFAULT 会走到 MODELSPACE深色只有 WHITE 才会把颜色 7 映射成黑色。这个默认值说实话挺容易踩的。八、小结回过头看这次的坑基本都不在「写代码」上而在「默认值」上。dwg2dxf 的默认 DXF 版本、ezdxf 的默认背景策略两个默认值叠在一起产出的就是一张看起来完全正常、实际上一片空白的 PDF。所以最想说的是转换类服务一定要验证输出内容不能只看 HTTP 200。像我这个场景最简单的判据就是数一下非白像素0 个就说明出问题了。几个能直接抄走的结论dwg2dxf 不要加 --as 降级沿用源版本ezdxf 渲染白底图必须显式设 BackgroundPolicy.WHITEmatplotlib 输出 PDF 记得设 pdf.fonttype42嵌入 TrueType 字体转换服务别信退出码和 HTTP 状态码要验产物本身完整工程整理好了docker build -t dwg-converter:1.0.0 . 就能构建端口用 SERVER_PORT 控制。有做类似需求的朋友欢迎评论区交流。