实现 DWG 转 PDF/PNG/JPG 服务(强化版)
一、需求拆解说白了就三件事上传一个 DWG 文件接口返回 PDF / PNG / JPG打成 Docker 镜像Windows 和 Linux 上都能跑服务端口可配置技术栈定的 Java 17 Spring Boot 3。端口可配置这点 Spring Boot 本身就支持用 server.port 或者环境变量 SERVER_PORT 都行后面会讲怎么跟 Docker 的端口映射串起来。二、选型纯 Java 这条路走不通一开始我想得挺简单找个 Java 库解析 DWG 不就完了。结果查了一圈发现不是这么回事。DWG 是 Autodesk 的私有二进制格式官方从来没公开过完整规范Java 生态里确实没有能打的解析库Kabeja 算是比较有名的 DXF 库但最后一次更新是 2008 年而且根本没发布到 Maven Central想用还得自己打 jarjdxf 之类的库更小众也基本停更了顺带说一句Kabeja 只能读 DXFDWG 它读不了。所以路线只能改成Spring Boot 负责服务化CAD 的解析和渲染交给成熟的开源引擎。这也算业内通行做法了。最后定下来的流水线是这样图 1 整体转换流水线DWG → DXF用 LibreDWG 的 dwg2dxf 命令。LibreDWG 是 GNU 项目GPLv3官方说覆盖约 90% 的 DWG 实体DXF → 图片用 Python 的 ezdxf matplotlib这是目前最靠谱的开源 DXF 渲染方案R12 到 R2018 都支持SVG → PNG/JPGApache Batik纯 Java用来按任意 DPI 重新光栅化可能有同学会问都 Spring Boot 了怎么还塞 Python 进来。实话说我也不想但 Java 侧确实没有能替代 ezdxf 的方案。而且 Python 那边我写得非常薄只干「DXF 出图」这一件事接口、参数校验、临时目录、超时这些全在 Java 里。三、代码框架工程结构大概是这样核心类没几个各管一摊DWG转pdf-png-jpg-Java/├── pom.xml Spring Boot 3.3.5 / Java 17├── Dockerfile 三阶段构建├── docker-compose.yml├── scripts/│ └── dxf2img.py DXF → SVG/PDF/PNG/JPG 渲染器└── src/main/java/com/example/dwgconverter/├── DwgConverterApplication.java├── config/ConverterProperties.java 外部化配置├── controller/ConversionController.java REST 接口├── controller/GlobalExceptionHandler.java├── model/{OutputFormat,ConversionOptions,ConversionResult}.java└── service/├── ConversionService.java 接口├── DwgConversionService.java 主流水线├── ProcessRunner.java 带超时的外部命令执行器├── SvgRasterizer.java Batik 纯 Java 光栅化└── ToolchainInspector.java 工具链自检对外提供的接口方法路径说明GET/api/v1/health工具链自检dwg2dxf / ezdxf 是否就绪GET/api/v1/formats支持的输入输出格式POST/api/v1/convert上传 DWG/DXF返回单个 PDF/PNG/JPG/SVGPOST/api/v1/convert/batch一次生成多格式打包 ZIPPOST/api/v1/convert/base64多格式返回 JSON Base64POST/api/v1/svg-to-imageSVG 转 PNG/JPG纯 JavaController 这块没什么花活重点是把异常统一收口。所有转换失败都抛 ConversionException由 GlobalExceptionHandler 转成结构化 JSON前端好处理ExceptionHandler(UnsupportedFormatException.class)public ResponseEntityMapString, Object handleUnsupportedFormat(UnsupportedFormatException e, HttpServletRequest request) {return build(HttpStatus.BAD_REQUEST, e.getMessage(), request);}ExceptionHandler(ConversionException.class)public ResponseEntityMapString, Object handleConversion(ConversionException e, HttpServletRequest request) {log.warn(转换失败: {}, e.getMessage());return build(HttpStatus.UNPROCESSABLE_ENTITY, e.getMessage(), request);}四、几个设计上的取舍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());}五、Docker 部署镜像用多阶段构建编译工具链不进最终镜像否则一个 gcc 就把镜像撑到 1G 以上图 2 三阶段镜像构建# 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.2 PillowCOPY --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 格式图层有 0、10000_FRONT、10500_FRONT_LABELING。结果这一测直接测出两个大坑。先看最终效果图 3 EM-DP01外观.dwg 转换效果200 DPI、白底 PNG2338×3340图上 1050 个实体全部正常渲染。不过这个结果是修完坑之后的中间过程相当曲折。七、踩到的三个坑坑一强制降级 DXF实体全丢这个是最坑的。我一开始为了「兼容性」给 dwg2dxf 加了 --as r2000想着输出老版本 DXF 兼容性更好。结果这张图转出来DXF 只有 27 KB实体数是 0渲染出来是一张纯白页PDF 只有 1.2 KB。要命的是接口返回的是 HTTP 200。要不是我顺手看了一眼文件大小这个问题可能就直接上线了。图 4 同一张图纸降级与不降级的输出对比去掉 --as让它沿用源版本DXF 变成 963 KBmodel space 里 1050 个实体LINE 740 个、LWPOLYLINE 293 个、ARC 17 个。验证方法很简单用 ezdxf 数一下就知道import collections, ezdxfdoc ezdxf.readfile(EM-DP01.dxf)msp doc.modelspace()c collections.Counter(e.dxftype() for e in msp)print(modelspace total:, sum(c.values()), dict(c.most_common(5)))# 降级版modelspace total: 0 {}# 源版本modelspace total: 1050 {LINE: 740, LWPOLYLINE: 293, ARC: 17}所以 DWG_DXF_VERSION 的默认值我改成了空沿用源版本。降级这个选项还留着但默认不启用。坑二颜色 7 的实体在白底上直接隐形修完第一个坑PDF 从 1.2 KB 涨到 131 KB看着像是对了。但 PNG 还是 6 KB我心里咯噔一下又去数了一下像素 —— 非白像素 0 个还是白页。PDF 有内容、PNG 没有这就很奇怪了。查下来发现是 ezdxf 的默认配色策略问题。AutoCAD 的模型空间背景是黑色的颜色 7 代表「白/黑」跟随背景变化。ezdxf 默认的 background policy 也遵循这个约定于是把颜色 7 的实体渲染成了白色。而我的画布是白底 —— 白线画在白纸上可不就看不见了。这张图正好 1050 个实体全是 BYLAYER所有图层颜色都是 7所以全军覆没。解决办法是显式指定策略别用默认值def build_config(background, monochrome):bg (background or white).strip().lower()if bg in TRANSPARENT:background_policy BackgroundPolicy.WHITE # 透明底也要用深色线条elif bg black:background_policy BackgroundPolicy.BLACKelse:background_policy BackgroundPolicy.WHITEif monochrome:color_policy (ColorPolicy.MONOCHROME_LIGHT_BGif background_policy is BackgroundPolicy.WHITEelse ColorPolicy.MONOCHROME_DARK_BG)else:color_policy ColorPolicy.COLORreturn Configuration(color_policycolor_policy, background_policybackground_policy)改完之后非白像素从 0 变成 47916 个。我把各种策略组合都跑了一遍结果挺说明问题的color policybackground policy非白像素COLORDEFAULT0COLORWHITE47916COLORMODELSPACE0MONOCHROME_LIGHT_BG任意47916BLACK任意47916WHITE任意0可以看到 BackgroundPolicy.DEFAULT 会走到 MODELSPACE深色只有 WHITE 才会把颜色 7 映射成黑色。这个默认值说实话挺容易踩的。坑三附带width 参数被静默忽略这个是修上面问题的时候顺手发现的。我传 width1200返回的图却是 800 px 宽。原因是 MatplotlibBackend 默认 adjust_figureTrue它会按图纸范围自己把 figure 缩放一遍我前面设的 figsize 就被覆盖了。改成 adjust_figureFalse再按图纸宽高比定画布尺寸尺寸才真正受控aspect drawing_aspect(layout) # 图纸的高宽比避免留白if width and height:fig_w, fig_h float(width) / dpi, float(height) / dpielif width:fig_w, fig_h float(width) / dpi, (float(width) / dpi) * aspectelif height:fig_h float(height) / dpifig_w fig_h / aspectelse:fig_w, fig_h DEFAULT_FIG_WIDTH, DEFAULT_FIG_WIDTH * aspectbackend MatplotlibBackend(ax, adjust_figureFalse) # 关键别让它自己缩放八、最终测试结果三个坑修完之后重新跑一遍全部 HTTP 200请求产物大小尺寸非白像素formatpdf142,247 B——formatpng232,674 B2338×3340146,880formatjpg287,698 B2338×3340328,454formatpng width1200106,529 B1200×171470,389formatpng dpi300 monochrometrue385,411 B3507×5010322,727convert/batch formatspdf,png,jpg398,361 B (ZIP)3 个条目—异常分支也都验过格式不支持返回 400文件后缀不对返回 422dpi 越界返回 422缺 file 参数返回 400。九、小结回过头看这次的坑基本都不在「写代码」上而在「默认值」上。dwg2dxf 的默认 DXF 版本、ezdxf 的默认背景策略、matplotlib 的默认 figure 调整三个默认值叠在一起产出的就是一张看起来完全正常、实际上一片空白的图。所以最想说的是转换类服务一定要验证输出内容不能只看 HTTP 200。像我这个场景最简单的判据就是数一下非白像素0 个就说明出问题了。这个校验我后来也加到了测试里。几个能直接抄走的结论dwg2dxf 不要加 --as 降级沿用源版本ezdxf 渲染白底图必须显式设 BackgroundPolicy.WHITEMatplotlibBackend 要用 adjust_figureFalse否则尺寸参数全失效转换服务别信退出码和 HTTP 状态码要验产物本身完整工程已经整理好了docker build -t dwg-converter:1.0.0 . 就能构建端口用 SERVER_PORT 控制。有做类似需求的朋友欢迎评论区交流。

相关新闻

DeepSeek私有化部署指南:硬件选型、LoRA微调与推理加速实战

DeepSeek私有化部署指南:硬件选型、LoRA微调与推理加速实战

简介:面向希望落地AI能力的中小型企业及技术决策者,这份PDF围绕DeepSeek的私有化部署与业务应用展开,系统讲解了从核心技术原理、部署架构到客户服务、市场营销、生产制造等场景的适配方案,并给出电商、制造、物流三类企业案例&am…

2026/10/5 8:20:08 阅读更多 →
企业数字化转型实战:场景选择、落地路径与避坑要点

企业数字化转型实战:场景选择、落地路径与避坑要点

1. 先搞清楚:数字化转型到底在转什么1.1 很多人把数字化当成“上系统”,这是个误解我见过太多企业老板,一开口就是“我们明年上套ERP,数字化就算搞定了”。你要是真照着这个思路去落,大概率会踩进一个深坑:…

2026/10/5 8:20:08 阅读更多 →
云上数字场景:公私联动数据中台与零售营销实战指南

云上数字场景:公私联动数据中台与零售营销实战指南

简介:一份围绕商业银行公私联动数字化转型的行业研究文档,聚焦云上数字场景如何赋能对公与零售业务深度融合。内容面向银行管理层、对公/零售条线从业者及金融科技研究者,系统梳理了公私联动的必要性、组织架构调整、产品设计、营销策略、数据…

2026/10/5 8:20:08 阅读更多 →

最新新闻

OpenRig 开放式机架主机,从选材到组装的完整 DIY 指南

OpenRig 开放式机架主机,从选材到组装的完整 DIY 指南

组装过几台 OpenRig 之后,我发现这个项目比想象中成熟得多。OpenRig 不是什么新概念,它是一套开源的开放式机架主机方案,简单说就是把传统机箱的侧板、前面板和顶盖全部去掉,用铝型材搭出一个开放测试平台,让主板、显卡…

2026/10/5 9:44:38 阅读更多 →
hindsight:面向工程师决策过程的认知镜像系统

hindsight:面向工程师决策过程的认知镜像系统

1. 项目概述:这不是一个工具,而是一种“事后清醒”的工程化实践你有没有过这种体验:代码上线前反复检查,自信满满;线上报错后翻日志,发现某个参数明明该设成500却写成了50——不是逻辑错误,不是…

2026/10/5 9:44:38 阅读更多 →
基于Node.js与React的AI Agent开发实战:paperclip架构解析与OpenClaw实践

基于Node.js与React的AI Agent开发实战:paperclip架构解析与OpenClaw实践

1. 从 paperclip 说起:一个把 AI Agent 装进 Node.js 与 React 世界的项目第一次看到paperclip这个标题,我脑子里蹦出来的不是办公用品,而是那个经典的“回形针”隐喻——一个看似不起眼、却能撬动整套流程的小工具。结合热搜词里的 Node.js、…

2026/10/5 9:44:38 阅读更多 →
电赛综合测评实战指南:Multisim仿真与硬件调试全链路解析

电赛综合测评实战指南:Multisim仿真与硬件调试全链路解析

1. 这不是普通练习题,是电赛综合测评的“压力测试现场”电赛综合测评——这三个字在电子类本科生和指导教师心里,分量比期末考试重十倍。它不考理论推导,不考公式默写,而是把一个真实、带约束、有容错边界的硬件系统摆在你面前&am…

2026/10/5 9:44:38 阅读更多 →
OpenRIG开源机架指南:从铝型材选型到3D打印DIY硬件平台搭建

OpenRIG开源机架指南:从铝型材选型到3D打印DIY硬件平台搭建

1. 项目概述:OpenRIG 是什么,又是为了解决什么问题说实话,第一次听到 OpenRIG 这个名字,我脑子里第一反应是“open rig”,也就是开源、开放的机架/硬件平台。当你跑在技术社区、DIY 硬件圈子里,RIG 这个词…

2026/10/5 9:44:38 阅读更多 →
电源轨道系统核心技术路线对比与选型要点

电源轨道系统核心技术路线对比与选型要点

在装修与商用空间配电改造中,电源轨道系统凭借取电点位可调的技术特性,逐步替代传统固定插座。当前市场上产品技术来源多样,部分产品因忽视核心导电结构设计,存在安全性与耐用性不达标的工程风险。从核心技术维度区分产品技术路线…

2026/10/5 9:43:38 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

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

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

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

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/5 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/5 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/4 20:14:29 阅读更多 →