
簡介DicomPrint-master 是一套面向醫(yī)療影像開發(fā)者的 DICOM 打印工具源碼聚焦醫(yī)學影像的膠片打印、格式定制與尺寸調(diào)整適合具備一定 C# 與 DICOM 協(xié)議基礎(chǔ)的技術(shù)人員研究或二次開發(fā)。資源包共 57 個文件約 15.21MB以 cs 源碼、dcm 樣例影像、txt 說明、jpg 截圖、csproj 工程文件為主另含 docx/doc 文檔、uml 圖、dll 庫、sln 解決方案及 pdf 一致性聲明覆蓋 PrintSCU、PrintSCP 服務、公共庫與示例 Demo 等模塊。已有 598 人學習下載。讀者可從中獲取圖像解析、膠片布局設置、尺寸調(diào)整、質(zhì)量控制、元數(shù)據(jù)處理、預覽與批處理等完整實現(xiàn)思路并借助樣例 DICOM 文件與說明文檔快速理解打印工作流為定制化開發(fā)或排錯提供參考。1. 從一臺老式激光相機說起DicomPrint-master 到底能干什么如果你在醫(yī)院 PACS 運維或者影像設備對接的崗位上待過大概率遇到過這種場景一臺服役十年的老式激光相機只認 DICOM Print SCU 協(xié)議而新上的 PACS 系統(tǒng)只提供 DICOM Storage 和 Worklist 服務兩邊就是握不上手。找廠商升級報價夠買半臺新設備自己寫一個打印服務端又卡在 DIMSE 消息構(gòu)造和 N-CREATE/N-SET 的狀態(tài)機里出不來。DicomPrint-master 這個源碼包就是沖著這個場景來的——它用一套相對完整的 DICOM Print 服務端實現(xiàn)把 Film Session、Film Box、Image Box 三層管理模型跑通讓 PACS 或任意 SCU 端能把影像頁發(fā)過來再由它轉(zhuǎn)成可打印的位圖或 PDF 落到本地。這個包適合三類人一是做 PACS 集成、需要快速驗證打印鏈路的工程師二是維護老舊影像設備、想用軟件方案替代專用打印機的運維三是學 DICOM 協(xié)議、想找一個能跑起來的 Print SCU/SCP 對照代碼的開發(fā)者。它不解決圖像后處理也不做排版美化核心價值就是把 DICOM Print 那套 SOP Class 的交互流程用可讀的代碼攤開給你看。下面我按“先跑通、再拆解、后避坑”的順序把這份源碼包從部署到調(diào)參到排錯完整走一遍。2. 把 DicomPrint-master 跑起來環(huán)境、依賴與最小驗證鏈路2.1 先看清目錄結(jié)構(gòu)和入口在哪拿到源碼包后別急著敲命令先花兩分鐘把目錄掃一遍。DicomPrint-master 的典型結(jié)構(gòu)是根目錄下分src、config、lib、scripts幾塊src里按 DIMSE 服務、SOP 類處理、打印渲染三層分包。入口通常是一個繼承自BasicServiceClassProvider或類似基類的 Print SCP 類它注冊了BasicFilmSessionSOPClass、BasicFilmBoxSOPClass、BasicGrayscaleImageBoxSOPClass這幾個 UID。你要找的就是那個在main里啟動DicomServer并綁定端口的文件常見命名是PrintSCP.java或DicomPrintServer.py取決于原始實現(xiàn)語言。確認入口后再看config下的application.properties或dicom.properties里面會有 AE Title、端口、打印輸出目錄、默認膠片尺寸這幾個關(guān)鍵項。這一步不做后面報錯你連改哪個文件都不知道。2.2 依賴安裝與編譯JDK、DCM4CHE 與構(gòu)建工具這類 DICOM 打印服務端底層多半依賴 dcm4che 或 fo-dicom 這類庫來處理 PDU 和 DIMSE。以 Java 版為例你需要 JDK 8 或 11Maven 3.6 以上。先確認pom.xml里 dcm4che 的版本常見是 5.x 系列它對應 DICOM 標準 2020 版左右。如果包內(nèi)自帶lib目錄放了 jar那就省去聯(lián)網(wǎng)拉依賴的麻煩直接把lib下所有 jar 加入 classpath 即可。編譯命令我一般這樣走# 進入項目根目錄 cd DicomPrint-master # 如果有 Maven 包裝器優(yōu)先用它避免本機 Maven 版本差異 ./mvnw clean package -DskipTests # 如果沒有包裝器用本機 Maven mvn clean package -DskipTests # 編譯完成后target 目錄下會生成可執(zhí)行 jar 或 classes ls target/這里-DskipTests不是偷懶而是這類源碼包里的單元測試經(jīng)常依賴真實 DICOM 節(jié)點沒配好測試環(huán)境會直接卡住編譯流程。編譯成功后target下應該有一個dicomprint-*.jar或者classes目錄。如果報package org.dcm4che3 does not exist說明依賴沒拉全檢查pom.xml里的倉庫地址是否可達或者手動把lib下的 jar 通過mvn install:install-file裝進本地倉庫。2.3 配置 AE Title、端口與輸出目錄配置文件是跑通鏈路的關(guān)鍵。打開config/application.properties你會看到類似下面的條目# DICOM 服務端 AE TitleSCU 端必須與此一致才能關(guān)聯(lián) dicom.scp.aetitleDICOMPRINT # 監(jiān)聽端口1024 以下需要 root 權(quán)限建議用 11112 dicom.scp.port11112 # 打印輸出目錄Film Box 完成后生成的位圖或 PDF 落在這里 print.output.dir./output # 默認膠片尺寸常見 8x10 或 14x17單位英寸 print.film.size14x17 # 每個 Film Box 最大 Image Box 數(shù)量 print.max.imagebox20dicom.scp.aetitle必須和 SCU 端配置的 Called AE Title 完全一致大小寫敏感這是最常見的關(guān)聯(lián)失敗原因。print.output.dir建議用絕對路徑相對路徑在不同啟動方式下解析結(jié)果不一樣容易找不到輸出文件。print.film.size要和實際打印機或后續(xù)排版邏輯匹配設錯了會導致圖像被裁切或留白異常。改完配置后啟動命令通常是java -jar target/dicomprint-*.jar --spring.config.locationconfig/application.properties如果包不是 Spring Boot 結(jié)構(gòu)那就用java -cp target/classes:lib/* com.xxx.PrintSCP這種形式具體主類名從入口文件里找。2.4 用 DCM4CHE 工具或 dcmtk 發(fā)一頁測試圖像服務端起來后別急著接 PACS先用命令行工具發(fā)一頁圖驗證鏈路。dcmtk 的dcmsend或storescu可以模擬 SCU但打印 SOP Class 需要專門的printscu工具dcmtk 里對應的是dcmpssnd或自己用echoscu先測關(guān)聯(lián)。更直接的辦法是用 dcm4che 的dcm4che-tool-printscu命令大致如下# 先測關(guān)聯(lián)確認 AE Title 和端口通 echoscu -v -aet TESTSCU -aec DICOMPRINT 127.0.0.1 11112 # 關(guān)聯(lián)成功后用 printscu 發(fā)送打印請求 printscu -v -aet TESTSCU -aec DICOMPRINT 127.0.0.1 11112 \ -f 14x17 -i ./test.dcmechoscu返回Association Accepted說明網(wǎng)絡層和 AE Title 沒問題。printscu執(zhí)行后會依次觸發(fā) 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目錄下出現(xiàn)了文件那最小鏈路就算通了。這一步跑不通后面所有調(diào)參都是空談。3. 拆開 DICOM Print 狀態(tài)機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 創(chuàng)建上層實例拿到 UID 后再創(chuàng)建下層。源碼里處理這套邏輯的地方通常在PrintService或FilmSessionHandler類中。關(guān)鍵 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源碼里如果只實現(xiàn)了 Grayscale Image Box那彩色圖像發(fā)過來會直接被拒。檢查supportedSOPClasses列表里有沒有注冊 Color Image Box沒有的話要么補上要么在 SCU 端強制轉(zhuǎn)灰度。Film Box 創(chuàng)建時會帶一批屬性Film Size ID、Magnification Type、Smoothing Type、Border Density、Trim、Configuration Information。這些屬性決定了后續(xù) Image Box 怎么排布源碼里一般有個FilmBoxAttributeHandler來解析它們。3.2 N-CREATE 與 N-SET 的消息構(gòu)造細節(jié)N-CREATE 請求里SCU 會帶一個 Attribute List服務端解析后返回一個帶新 UID 的響應。源碼里構(gòu)造響應的代碼通常長這樣// 創(chuàng)建 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); // 構(gòu)造 N-CREATE 響應 DimseRSP rsp new DimseRSP(CommandStatus.Success, filmSession);UIDUtils.createUID()生成的是根為1.2.840.10008的實例 UID必須全局唯一否則 SCU 端可能拒絕后續(xù) N-SET。NumberOfCopies控制打印份數(shù)PrintPriority影響隊列調(diào)度這些屬性在源碼里如果寫死實際使用時會不夠靈活建議改成從配置讀。N-SET 用于往 Image Box 里塞像素數(shù)據(jù)請求里帶PixelData和PhotometricInterpretation服務端收到后要按 Film Box 的排版參數(shù)把圖像縮放、旋轉(zhuǎn)、拼接到膠片畫布上。這一步的渲染邏輯是源碼里最值得細看的部分通常涉及BufferedImage的Graphics2D操作。3.3 打印觸發(fā)與輸出文件生成所有 Image Box 都 N-SET 完成后SCU 發(fā) 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作為文件名可以避免并發(fā)打印時互相覆蓋。如果源碼里用的是時間戳高并發(fā)下同一秒內(nèi)多個 Film Box 會撞名這是實際部署中容易翻車的地方。輸出格式支持 PNG 還是 PDF取決于源碼里引了哪些庫常見的是ImageIO加pdfbox。落盤后你可以用ls -lh output/確認文件大小是否合理一張 14x17 的灰度膠片PNG 大概在 2 到 5 MB太小說明畫布沒畫上東西。4. 避坑與排查關(guān)聯(lián)失敗、圖像錯位、內(nèi)存泄漏這三類問題最要命4.1 關(guān)聯(lián)被拒AE Title 大小寫與 PDU 長度協(xié)商現(xiàn)象是echoscu返回Association Rejected日志里寫Called AE Title not recognized。原因通常是 SCU 端配的 Called AE Title 和服務端dicom.scp.aetitle不一致或者服務端啟動時讀的配置文件不是你以為的那個。解決方法是先用netstat -tlnp | grep 11112確認端口在聽再用echoscu -aec逐個試大小寫組合。另一個隱蔽原因是 PDU 長度協(xié)商老設備可能只支持 16KB而服務端默認 64KB需要在配置里把dicom.max.pdu.length調(diào)到 16384。4.2 圖像錯位或裁切Film Size 與 Magnification Type 不匹配現(xiàn)象是輸出的膠片上圖像偏到一角或者邊緣被切掉。原因是 Film Box 創(chuàng)建時 SCU 傳的 Film Size ID 是14x17而服務端配置的默認畫布是8x10渲染時按小畫布裁剪導致。解決方法是讓服務端以 SCU 傳入的 Film Size ID 為準動態(tài)創(chuàng)建畫布而不是用固定配置。源碼里如果寫死了畫布尺寸找到createCanvas方法把尺寸參數(shù)改成從 Film Box 屬性讀取。Magnification Type 設為REPLICATE時圖像不縮放直接平鋪設為BILINEAR才做插值縮放設錯了也會導致視覺上的錯位。4.3 內(nèi)存泄漏Film Box 對象沒釋放導致 OOM現(xiàn)象是服務跑幾天后OutOfMemoryError堆轉(zhuǎn)儲里全是FilmBox和BufferedImage對象。原因是 N-ACTION Print 完成后filmBoxMap里的條目沒移除畫布 BufferedImage 一直占著內(nèi)存。解決方法是打印完成后立即filmBoxMap.remove(filmBoxUID)并把畫布引用置空。如果源碼里用靜態(tài) Map 存 Film Box那泄漏是必然的改成ConcurrentHashMap并在 finally 塊里清理。另外Image Box 的像素數(shù)據(jù)在 N-SET 后如果沒及時釋放也會累積檢查imageBoxMap的清理邏輯。4.4 并發(fā)打印時文件覆蓋與 UID 沖突現(xiàn)象是兩臺 SCU 同時打印輸出目錄里只有一個文件另一個被覆蓋。原因是文件名用了固定前綴加序號序號在并發(fā)下重復。解決方法是文件名直接用 Film Box 的 SOP Instance UID這個 UID 全局唯一不會撞。如果源碼里用System.currentTimeMillis()做文件名同一毫秒內(nèi)兩個請求就會覆蓋改成 UID 或加隨機后綴。UID 沖突還可能導致 SCU 端 N-SET 找不到對應的 Image Box日志里會出現(xiàn)Unknown SOP Instance UID這時候要檢查 UID 生成邏輯是否線程安全。4.5 中文路徑與編碼問題導致輸出失敗現(xiàn)象是print.output.dir設成含中文的路徑后文件寫不出來日志報FileNotFoundException。原因是 JVM 默認編碼和文件系統(tǒng)編碼不一致尤其在 Windows 上。解決方法是在啟動命令里加-Dfile.encodingUTF-8并且路徑盡量用英文。如果必須用中文路徑用Paths.get(dir, filename)代替字符串拼接讓 NIO 處理編碼。這個坑在測試環(huán)境用英文路徑時不會暴露一上生產(chǎn)就翻車血淚經(jīng)驗是部署前先用中文路徑跑一遍。5. 進階把打印輸出接到實際工作流與自動化驗證5.1 用 DCM4CHE 的 printscu 做回歸測試每次改完源碼別手動發(fā)圖驗證寫一個 shell 腳本把printscu調(diào)用包起來跑完檢查輸出目錄文件數(shù)和大小。腳本大概這樣#!/bin/bash # 回歸測試發(fā)一頁圖檢查輸出文件是否生成且大小合理 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目錄里價值有限。常見做法是寫一個監(jiān)聽器用WatchService監(jiān)控目錄新文件一出現(xiàn)就調(diào)lp或lpr送到本地打印隊列或者用storescu把 PDF 轉(zhuǎn)成 Secondary Capture 存回 PACS。下面是一個簡單的文件監(jiān)聽片段// 監(jiān)控輸出目錄新 PDF 出現(xiàn)后送到打印隊列 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)) { // 調(diào)用系統(tǒng)打印命令注意路徑空格轉(zhuǎn)義 Runtime.getRuntime().exec(new String[]{lp, -d, printer1, newFile.toString()}); } } key.reset(); }lp -d printer1里的printer1要換成實際隊列名用lpstat -p查。如果打印隊列在遠程把lp換成lpr -H remotehost。這個監(jiān)聽器要處理文件寫入未完成的情況PDF 可能還在寫就被監(jiān)聽到了加一個Thread.sleep(500)或者檢查文件鎖。5.3 參數(shù)調(diào)優(yōu)并發(fā)數(shù)、超時與日志級別生產(chǎn)環(huán)境要把dicom.scp.max.associations調(diào)到 10 以上否則多個 SCU 同時連會被拒。dicom.scp.idle.timeout設 300 秒避免空閑關(guān)聯(lián)一直占著。日志級別從 DEBUG 調(diào)到 INFO不然高頻打印時日志文件一天能漲幾個 GB。如果源碼里用的是 log4j 或 logback改配置文件里的root level即可。調(diào)完這些參數(shù)后用ab或jmeter模擬 5 個并發(fā) SCU 同時發(fā)打印請求觀察內(nèi)存和輸出文件是否正常。我自己的習慣是每次改完配置先用echoscu測關(guān)聯(lián)再用printscu發(fā)一頁最后看輸出目錄和日志三步都過了才認為這次改動是安全的。從那以后我每次部署 DicomPrint 到新環(huán)境都強制走一遍這個三步驗證沒再出現(xiàn)過上線才發(fā)現(xiàn)關(guān)聯(lián)不上的情況。希望幫到你。本文還有配套的精品資源點擊獲取