
简介DicomPrint-master 是一套面向医疗影像开发者的 DICOM 打印工具源码聚焦医学影像的胶片打印、格式定制与尺寸调整适合具备一定 C# 与 DICOM 协议基础的技术人员研究或二次开发。资源包共 57 个文件约 15.21MB以 cs 源码、dcm 样例影像、txt 说明、jpg 截图、csproj 工程文件为主另含 docx/doc 文档、uml 图、dll 库、sln 解决方案及 pdf 一致性声明覆盖 PrintSCU、PrintSCP 服务、公共库与示例 Demo 等模块。已有 598 人学习下载。读者可从中获取图像解析、胶片布局设置、尺寸调整、质量控制、元数据处理、预览与批处理等完整实现思路并借助样例 DICOM 文件与说明文档快速理解打印工作流为定制化开发或排错提供参考。1. 从一台老式激光相机说起DicomPrint-master 到底能干什么如果你在医院 PACS 运维或者影像设备对接的岗位上待过大概率遇到过这种场景一台服役十年的老式激光相机只认 DICOM Print SCU 协议而新上的 PACS 系统只提供 DICOM Storage 和 Worklist 服务两边就是握不上手。找厂商升级报价够买半台新设备自己写一个打印服务端又卡在 DIMSE 消息构造和 N-CREATE/N-SET 的状态机里出不来。DicomPrint-master 这个源码包就是冲着这个场景来的——它用一套相对完整的 DICOM Print 服务端实现把 Film Session、Film Box、Image Box 三层管理模型跑通让 PACS 或任意 SCU 端能把影像页发过来再由它转成可打印的位图或 PDF 落到本地。这个包适合三类人一是做 PACS 集成、需要快速验证打印链路的工程师二是维护老旧影像设备、想用软件方案替代专用打印机的运维三是学 DICOM 协议、想找一个能跑起来的 Print SCU/SCP 对照代码的开发者。它不解决图像后处理也不做排版美化核心价值就是把 DICOM Print 那套 SOP Class 的交互流程用可读的代码摊开给你看。下面我按“先跑通、再拆解、后避坑”的顺序把这份源码包从部署到调参到排错完整走一遍。2. 把 DicomPrint-master 跑起来环境、依赖与最小验证链路2.1 先看清目录结构和入口在哪拿到源码包后别急着敲命令先花两分钟把目录扫一遍。DicomPrint-master 的典型结构是根目录下分src、config、lib、scripts几块src里按 DIMSE 服务、SOP 类处理、打印渲染三层分包。入口通常是一个继承自BasicServiceClassProvider或类似基类的 Print SCP 类它注册了BasicFilmSessionSOPClass、BasicFilmBoxSOPClass、BasicGrayscaleImageBoxSOPClass这几个 UID。你要找的就是那个在main里启动DicomServer并绑定端口的文件常见命名是PrintSCP.java或DicomPrintServer.py取决于原始实现语言。确认入口后再看config下的application.properties或dicom.properties里面会有 AE Title、端口、打印输出目录、默认胶片尺寸这几个关键项。这一步不做后面报错你连改哪个文件都不知道。2.2 依赖安装与编译JDK、DCM4CHE 与构建工具这类 DICOM 打印服务端底层多半依赖 dcm4che 或 fo-dicom 这类库来处理 PDU 和 DIMSE。以 Java 版为例你需要 JDK 8 或 11Maven 3.6 以上。先确认pom.xml里 dcm4che 的版本常见是 5.x 系列它对应 DICOM 标准 2020 版左右。如果包内自带lib目录放了 jar那就省去联网拉依赖的麻烦直接把lib下所有 jar 加入 classpath 即可。编译命令我一般这样走# 进入项目根目录 cd DicomPrint-master # 如果有 Maven 包装器优先用它避免本机 Maven 版本差异 ./mvnw clean package -DskipTests # 如果没有包装器用本机 Maven mvn clean package -DskipTests # 编译完成后target 目录下会生成可执行 jar 或 classes ls target/这里-DskipTests不是偷懒而是这类源码包里的单元测试经常依赖真实 DICOM 节点没配好测试环境会直接卡住编译流程。编译成功后target下应该有一个dicomprint-*.jar或者classes目录。如果报package org.dcm4che3 does not exist说明依赖没拉全检查pom.xml里的仓库地址是否可达或者手动把lib下的 jar 通过mvn install:install-file装进本地仓库。2.3 配置 AE Title、端口与输出目录配置文件是跑通链路的关键。打开config/application.properties你会看到类似下面的条目# DICOM 服务端 AE TitleSCU 端必须与此一致才能关联 dicom.scp.aetitleDICOMPRINT # 监听端口1024 以下需要 root 权限建议用 11112 dicom.scp.port11112 # 打印输出目录Film Box 完成后生成的位图或 PDF 落在这里 print.output.dir./output # 默认胶片尺寸常见 8x10 或 14x17单位英寸 print.film.size14x17 # 每个 Film Box 最大 Image Box 数量 print.max.imagebox20dicom.scp.aetitle必须和 SCU 端配置的 Called AE Title 完全一致大小写敏感这是最常见的关联失败原因。print.output.dir建议用绝对路径相对路径在不同启动方式下解析结果不一样容易找不到输出文件。print.film.size要和实际打印机或后续排版逻辑匹配设错了会导致图像被裁切或留白异常。改完配置后启动命令通常是java -jar target/dicomprint-*.jar --spring.config.locationconfig/application.properties如果包不是 Spring Boot 结构那就用java -cp target/classes:lib/* com.xxx.PrintSCP这种形式具体主类名从入口文件里找。2.4 用 DCM4CHE 工具或 dcmtk 发一页测试图像服务端起来后别急着接 PACS先用命令行工具发一页图验证链路。dcmtk 的dcmsend或storescu可以模拟 SCU但打印 SOP Class 需要专门的printscu工具dcmtk 里对应的是dcmpssnd或自己用echoscu先测关联。更直接的办法是用 dcm4che 的dcm4che-tool-printscu命令大致如下# 先测关联确认 AE Title 和端口通 echoscu -v -aet TESTSCU -aec DICOMPRINT 127.0.0.1 11112 # 关联成功后用 printscu 发送打印请求 printscu -v -aet TESTSCU -aec DICOMPRINT 127.0.0.1 11112 \ -f 14x17 -i ./test.dcmechoscu返回Association Accepted说明网络层和 AE Title 没问题。printscu执行后会依次触发 N-CREATE Film Session、N-CREATE Film Box、N-SET Image Box、N-ACTION Print 这一串操作。如果服务端日志里能看到Film Session created、Image Box set、Print action received并且output目录下出现了文件那最小链路就算通了。这一步跑不通后面所有调参都是空谈。3. 拆开 DICOM Print 状态机Film Session、Film Box 与 Image Box 怎么串3.1 三层管理模型与 SOP Class UID 对照DICOM Print 管理模型是三层嵌套一个 Film Session 下挂多个 Film Box一个 Film Box 下挂多个 Image Box。每个层级对应一个 SOP ClassSCU 通过 N-CREATE 创建上层实例拿到 UID 后再创建下层。源码里处理这套逻辑的地方通常在PrintService或FilmSessionHandler类中。关键 UID 如下表层级SOP Class 名称UID 后缀Film SessionBasic Film Session1.2.840.10008.5.1.1.1Film BoxBasic Film Box1.2.840.10008.5.1.1.2Image BoxBasic Grayscale Image Box1.2.840.10008.5.1.1.4Image BoxBasic Color Image Box1.2.840.10008.5.1.1.4.1源码里如果只实现了 Grayscale Image Box那彩色图像发过来会直接被拒。检查supportedSOPClasses列表里有没有注册 Color Image Box没有的话要么补上要么在 SCU 端强制转灰度。Film Box 创建时会带一批属性Film Size ID、Magnification Type、Smoothing Type、Border Density、Trim、Configuration Information。这些属性决定了后续 Image Box 怎么排布源码里一般有个FilmBoxAttributeHandler来解析它们。3.2 N-CREATE 与 N-SET 的消息构造细节N-CREATE 请求里SCU 会带一个 Attribute List服务端解析后返回一个带新 UID 的响应。源码里构造响应的代码通常长这样// 创建 Film Session 响应分配 UID 并回填属性 Attributes filmSession new Attributes(); filmSession.setString(Tag.SOPInstanceUID, VR.UI, UIDUtils.createUID()); filmSession.setString(Tag.SOPClassUID, VR.UI, UID.BasicFilmSessionSOPClass); filmSession.setInt(Tag.NumberOfCopies, VR.IS, 1); filmSession.setString(Tag.PrintPriority, VR.CS, MED); // 构造 N-CREATE 响应 DimseRSP rsp new DimseRSP(CommandStatus.Success, filmSession);UIDUtils.createUID()生成的是根为1.2.840.10008的实例 UID必须全局唯一否则 SCU 端可能拒绝后续 N-SET。NumberOfCopies控制打印份数PrintPriority影响队列调度这些属性在源码里如果写死实际使用时会不够灵活建议改成从配置读。N-SET 用于往 Image Box 里塞像素数据请求里带PixelData和PhotometricInterpretation服务端收到后要按 Film Box 的排版参数把图像缩放、旋转、拼接到胶片画布上。这一步的渲染逻辑是源码里最值得细看的部分通常涉及BufferedImage的Graphics2D操作。3.3 打印触发与输出文件生成所有 Image Box 都 N-SET 完成后SCU 发 N-ACTION 请求Action Type ID 为 1表示 Print。服务端收到后要把当前 Film Box 对应的画布落盘。源码里一般有个PrintActionHandler核心逻辑是// 收到 N-ACTION Print 后把 Film Box 画布输出为 PNG 或 PDF public void onPrintAction(String filmBoxUID) { FilmBox box filmBoxMap.get(filmBoxUID); BufferedImage canvas box.getCanvas(); File output new File(outputDir, filmBoxUID .png); ImageIO.write(canvas, png, output); // 如果配置了 PDF 输出再走一遍 PDF 渲染 if (pdfEnabled) { PDFRenderer.render(canvas, new File(outputDir, filmBoxUID .pdf)); } }filmBoxUID作为文件名可以避免并发打印时互相覆盖。如果源码里用的是时间戳高并发下同一秒内多个 Film Box 会撞名这是实际部署中容易翻车的地方。输出格式支持 PNG 还是 PDF取决于源码里引了哪些库常见的是ImageIO加pdfbox。落盘后你可以用ls -lh output/确认文件大小是否合理一张 14x17 的灰度胶片PNG 大概在 2 到 5 MB太小说明画布没画上东西。4. 避坑与排查关联失败、图像错位、内存泄漏这三类问题最要命4.1 关联被拒AE Title 大小写与 PDU 长度协商现象是echoscu返回Association Rejected日志里写Called AE Title not recognized。原因通常是 SCU 端配的 Called AE Title 和服务端dicom.scp.aetitle不一致或者服务端启动时读的配置文件不是你以为的那个。解决方法是先用netstat -tlnp | grep 11112确认端口在听再用echoscu -aec逐个试大小写组合。另一个隐蔽原因是 PDU 长度协商老设备可能只支持 16KB而服务端默认 64KB需要在配置里把dicom.max.pdu.length调到 16384。4.2 图像错位或裁切Film Size 与 Magnification Type 不匹配现象是输出的胶片上图像偏到一角或者边缘被切掉。原因是 Film Box 创建时 SCU 传的 Film Size ID 是14x17而服务端配置的默认画布是8x10渲染时按小画布裁剪导致。解决方法是让服务端以 SCU 传入的 Film Size ID 为准动态创建画布而不是用固定配置。源码里如果写死了画布尺寸找到createCanvas方法把尺寸参数改成从 Film Box 属性读取。Magnification Type 设为REPLICATE时图像不缩放直接平铺设为BILINEAR才做插值缩放设错了也会导致视觉上的错位。4.3 内存泄漏Film Box 对象没释放导致 OOM现象是服务跑几天后OutOfMemoryError堆转储里全是FilmBox和BufferedImage对象。原因是 N-ACTION Print 完成后filmBoxMap里的条目没移除画布 BufferedImage 一直占着内存。解决方法是打印完成后立即filmBoxMap.remove(filmBoxUID)并把画布引用置空。如果源码里用静态 Map 存 Film Box那泄漏是必然的改成ConcurrentHashMap并在 finally 块里清理。另外Image Box 的像素数据在 N-SET 后如果没及时释放也会累积检查imageBoxMap的清理逻辑。4.4 并发打印时文件覆盖与 UID 冲突现象是两台 SCU 同时打印输出目录里只有一个文件另一个被覆盖。原因是文件名用了固定前缀加序号序号在并发下重复。解决方法是文件名直接用 Film Box 的 SOP Instance UID这个 UID 全局唯一不会撞。如果源码里用System.currentTimeMillis()做文件名同一毫秒内两个请求就会覆盖改成 UID 或加随机后缀。UID 冲突还可能导致 SCU 端 N-SET 找不到对应的 Image Box日志里会出现Unknown SOP Instance UID这时候要检查 UID 生成逻辑是否线程安全。4.5 中文路径与编码问题导致输出失败现象是print.output.dir设成含中文的路径后文件写不出来日志报FileNotFoundException。原因是 JVM 默认编码和文件系统编码不一致尤其在 Windows 上。解决方法是在启动命令里加-Dfile.encodingUTF-8并且路径尽量用英文。如果必须用中文路径用Paths.get(dir, filename)代替字符串拼接让 NIO 处理编码。这个坑在测试环境用英文路径时不会暴露一上生产就翻车血泪经验是部署前先用中文路径跑一遍。5. 进阶把打印输出接到实际工作流与自动化验证5.1 用 DCM4CHE 的 printscu 做回归测试每次改完源码别手动发图验证写一个 shell 脚本把printscu调用包起来跑完检查输出目录文件数和大小。脚本大概这样#!/bin/bash # 回归测试发一页图检查输出文件是否生成且大小合理 OUTPUT_DIR./output rm -f $OUTPUT_DIR/*.png printscu -v -aet TESTSCU -aec DICOMPRINT 127.0.0.1 11112 \ -f 14x17 -i ./test.dcm # 检查是否生成了文件 FILE_COUNT$(ls $OUTPUT_DIR/*.png 2/dev/null | wc -l) if [ $FILE_COUNT -ne 1 ]; then echo FAIL: expected 1 output file, got $FILE_COUNT exit 1 fi # 检查文件大小是否在合理范围2MB 到 10MB FILE_SIZE$(stat -c%s $OUTPUT_DIR/*.png) if [ $FILE_SIZE -lt 2000000 ] || [ $FILE_SIZE -gt 10000000 ]; then echo FAIL: output file size $FILE_SIZE out of range exit 1 fi echo PASS这个脚本能挡住大部分低级错误没输出、输出为空、输出被裁切导致文件过小。把它挂到 CI 里每次提交自动跑一遍比人工点强得多。5.2 把输出 PDF 接入 PACS 归档或本地打印队列生成的 PDF 如果只是躺在output目录里价值有限。常见做法是写一个监听器用WatchService监控目录新文件一出现就调lp或lpr送到本地打印队列或者用storescu把 PDF 转成 Secondary Capture 存回 PACS。下面是一个简单的文件监听片段// 监控输出目录新 PDF 出现后送到打印队列 WatchService watchService FileSystems.getDefault().newWatchService(); Paths.get(outputDir).register(watchService, StandardWatchEventKinds.ENTRY_CREATE); while (true) { WatchKey key watchService.take(); for (WatchEvent? event : key.pollEvents()) { Path newFile outputDir.resolve((Path) event.context()); if (newFile.toString().endsWith(.pdf)) { // 调用系统打印命令注意路径空格转义 Runtime.getRuntime().exec(new String[]{lp, -d, printer1, newFile.toString()}); } } key.reset(); }lp -d printer1里的printer1要换成实际队列名用lpstat -p查。如果打印队列在远程把lp换成lpr -H remotehost。这个监听器要处理文件写入未完成的情况PDF 可能还在写就被监听到了加一个Thread.sleep(500)或者检查文件锁。5.3 参数调优并发数、超时与日志级别生产环境要把dicom.scp.max.associations调到 10 以上否则多个 SCU 同时连会被拒。dicom.scp.idle.timeout设 300 秒避免空闲关联一直占着。日志级别从 DEBUG 调到 INFO不然高频打印时日志文件一天能涨几个 GB。如果源码里用的是 log4j 或 logback改配置文件里的root level即可。调完这些参数后用ab或jmeter模拟 5 个并发 SCU 同时发打印请求观察内存和输出文件是否正常。我自己的习惯是每次改完配置先用echoscu测关联再用printscu发一页最后看输出目录和日志三步都过了才认为这次改动是安全的。从那以后我每次部署 DicomPrint 到新环境都强制走一遍这个三步验证没再出现过上线才发现关联不上的情况。希望帮到你。本文还有配套的精品资源点击获取