news 2026/10/1 6:58:29

Java实现 DWG 转 PDF 服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java实现 DWG 转 PDF 服务

手上有个需求,要把 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,想用还得自己打 jar;jdxf 之类的库更小众,也基本停更了。

所以路线只能改成: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

上传 DWG,format=pdf 返回 PDF 文件

Controller 本身没什么花活,就是把参数收下来、交给 Service,重点在异常统一收口,转换失败都抛 ConversionException,由 GlobalExceptionHandler 转成结构化 JSON,前端好处理:

@PostMapping(value = "/convert", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<byte[]> 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 用进程调用,不碰 JNI

LibreDWG 有 C 接口,理论上能用 JNI 或 JNA 直接调,但我选了最土的 ProcessBuilder。

原因很简单:JNI 一旦涉及跨平台编译、崩溃隔离这些事就很麻烦。进程调用崩了顶多这个请求失败,不会把整个 JVM 带走;而且换实现特别容易,想从 LibreDWG 换成 ODA File Converter,改个配置路径就行,一行 Java 代码都不用动。

4.2 每次转换开一个独立临时目录

每个请求一个临时目录,转完递归删掉,避免并发请求互相踩。调试的时候设 DWG_KEEP_TEMP_FILES=true 就能把中间文件留下来看。

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 (Stream<Path> 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,默认是 3(Type 3)
matplotlib.rcParams["svg.fonttype"] = "path" # 文字转轮廓,彻底摆脱字体依赖

五、Docker 部署

镜像用多阶段构建,编译工具链不进最终镜像,否则光一个 gcc 就能把镜像撑到 1G 以上:

# Stage 1:编译 LibreDWG,拿到 dwg2dxf
FROM debian:bookworm-slim AS libredwg
RUN sh ./autogen.sh && ./configure --enable-release --disable-bindings \
--disable-docs --disable-shared && make -j"$(nproc)" && make install

# Stage 2:构建 Spring Boot fat jar
FROM maven:3.9.9-eclipse-temurin-17 AS build
RUN mvn -B -DskipTests package

# Stage 3:运行时只留 JRE + Python + dwg2dxf
FROM eclipse-temurin:17-jre-jammy
RUN apt-get install -y python3 python3-venv \
&& python3 -m venv /opt/venv \
&& /opt/venv/bin/pip install ezdxf==1.3.5 matplotlib==3.9.2
COPY --from=libredwg /opt/libredwg/usr/local/bin/ /usr/local/bin/
COPY --from=build /build/target/dwg-converter.jar /app/app.jar
ENTRYPOINT ["/usr/bin/tini", "--", "sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]

端口这块,容器内用 SERVER_PORT 环境变量控制,端口映射两边跟着同一个变量走,改一处就够:

# 默认 8080
docker run -d -p 8080:8080 dwg-converter:1.0.0

# 换成 9000
docker run -d -e SERVER_PORT=9000 -p 9000:9000 dwg-converter:1.0.0

镜像本身是 Linux 容器,Windows 的 Docker Desktop 和 Linux 上都能跑。

六、拿真实图纸跑一遍

代码写完当然得测。除了 LibreDWG 官方的测试样本,我拿了一张真实的工程图纸 —— EM-DP01外观.dwg,276 KB,文件头是 AC1032,也就是 AutoCAD 2018 格式。

最终产出的 PDF 长这样:

图 2 EM-DP01外观.dwg 转换出的 PDF(渲染首页,矢量输出)

项

值

输入文件

EM-DP01外观.dwg,276,606 B,AutoCAD 2018(AC1032)

中间 DXF

963,037 B,model space 共 1050 个实体

输出 PDF

142,247 B,1 页,矢量,放大不失真

不过这个结果是修完坑之后的,中间过程挺曲折,下面说。

七、踩到的两个坑

坑一:给 dwg2dxf 加降级参数,实体全丢

这个是最坑的。我一开始为了「兼容性」,给 dwg2dxf 加了 --as r2000,想着输出老版本 DXF 兼容性更好。

结果这张图转出来,DXF 只有 27 KB,实体数是 0,渲染出来是一张纯白页,PDF 只有 1.2 KB。

要命的是,接口返回的是 HTTP 200。要不是我顺手看了一眼文件大小,这个问题可能就直接上线了。

图 3 同一张图纸,降级与不降级的输出对比

去掉 --as 让它沿用源版本,DXF 变成 963 KB,model space 里 1050 个实体:LINE 740 个、LWPOLYLINE 293 个、ARC 17 个。验证方法很简单,用 ezdxf 数一下就知道:

import collections, ezdxf
doc = 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, Configuration

config = Configuration(
color_policy=ColorPolicy.COLOR,
background_policy=BackgroundPolicy.WHITE) # 关键:别用 DEFAULT
Frontend(ctx, backend, config=config).draw_layout(layout, finalize=True)

我把各种策略组合都跑了一遍,结果挺说明问题的:

color policy

background policy

非白像素

COLOR

DEFAULT

0

COLOR

WHITE

47916

COLOR

MODELSPACE

0

MONOCHROME_LIGHT_BG

任意

47916

WHITE

任意

0

可以看到 BackgroundPolicy.DEFAULT 会走到 MODELSPACE(深色),只有 WHITE 才会把颜色 7 映射成黑色。这个默认值,说实话挺容易踩的。

八、小结

回过头看,这次的坑基本都不在「写代码」上,而在「默认值」上。dwg2dxf 的默认 DXF 版本、ezdxf 的默认背景策略,两个默认值叠在一起,产出的就是一张看起来完全正常、实际上一片空白的 PDF。

所以最想说的是:转换类服务一定要验证输出内容,不能只看 HTTP 200。像我这个场景,最简单的判据就是数一下非白像素,0 个就说明出问题了。

几个能直接抄走的结论:

  • dwg2dxf 不要加 --as 降级,沿用源版本
  • ezdxf 渲染白底图,必须显式设 BackgroundPolicy.WHITE
  • matplotlib 输出 PDF 记得设 pdf.fonttype=42,嵌入 TrueType 字体
  • 转换服务别信退出码和 HTTP 状态码,要验产物本身

完整工程整理好了,docker build -t dwg-converter:1.0.0 . 就能构建,端口用 SERVER_PORT 控制。有做类似需求的朋友,欢迎评论区交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 6:58:11

DEV 实战:ComboBoxEdit 与 barEditItem 配置 TaoToken 的 settings.json 骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 6:56:59

OpenAI Dot 是什么?2026 年 Dots 功能、使用方式与开放范围

发布日期&#xff1a;2026年9月30日 | 信息核验&#xff1a;OpenAI DevDay 2026 与 OpenAI 官方文档 OpenAI Dot 是 OpenAI 于 2026 年 9 月 29 日发布的常驻 AI 代理&#xff0c;官方将产品整体称为 Dots&#xff0c;单个代理称为 a dot。它由 GPT-6 Astra 驱动&#xff0c;拥…

作者头像 李华