简介: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 或 11,Maven 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 Title,SCU 端必须与此一致才能关联 dicom.scp.aetitle=DICOMPRINT # 监听端口,1024 以下需要 root 权限,建议用 11112 dicom.scp.port=11112 # 打印输出目录,Film Box 完成后生成的位图或 PDF 落在这里 print.output.dir=./output # 默认胶片尺寸,常见 8x10 或 14x17,单位英寸 print.film.size=14x17 # 每个 Film Box 最大 Image Box 数量 print.max.imagebox=20dicom.scp.aetitle必须和 SCU 端配置的 Called AE Title 完全一致,大小写敏感,这是最常见的关联失败原因。print.output.dir建议用绝对路径,相对路径在不同启动方式下解析结果不一样,容易找不到输出文件。print.film.size要和实际打印机或后续排版逻辑匹配,设错了会导致图像被裁切或留白异常。改完配置后,启动命令通常是:
java -jar target/dicomprint-*.jar --spring.config.location=config/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 Class,SCU 通过 N-CREATE 创建上层实例,拿到 UID 后再创建下层。源码里处理这套逻辑的地方通常在PrintService或FilmSessionHandler类中。关键 UID 如下表:
| 层级 | SOP Class 名称 | UID 后缀 |
|---|---|---|
| Film Session | Basic Film Session | 1.2.840.10008.5.1.1.1 |
| Film Box | Basic Film Box | 1.2.840.10008.5.1.1.2 |
| Image Box | Basic Grayscale Image Box | 1.2.840.10008.5.1.1.4 |
| Image Box | Basic Color Image Box | 1.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.encoding=UTF-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 到新环境,都强制走一遍这个三步验证,没再出现过上线才发现关联不上的情况。希望帮到你。
本文还有配套的精品资源,点击获取