参考文献

环境说明

  • 依赖以及依赖版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <properties>
    <!-- DICOM相关 -->
    <dcm4che.version>5.23.3</dcm4che.version>
    </properties>
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.dcm4che</groupId>
    <artifactId>dcm4che-core</artifactId>
    <version>${dcm4che.version}</version>
    <exclusions>
    <exclusion>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>org.dcm4che</groupId>
    <artifactId>dcm4che-net</artifactId>
    <version>${dcm4che.version}</version>

    </dependency>
    <dependency>
    <groupId>org.dcm4che.tool</groupId>
    <artifactId>dcm4che-tool-common</artifactId>
    <version>${dcm4che.version}</version>

    </dependency>
    <dependency>
    <groupId>org.dcm4che</groupId>
    <artifactId>dcm4che-imageio</artifactId>
    <version>${dcm4che.version}</version>
    </dependency>
    </dependencies>
    </dependencyManagement>

PACS功能实现

抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package cn.holelin.dicom.util.pacs;

import cn.holelin.dicom.enums.InformationModelEnum;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.UID;
import org.dcm4che3.net.ApplicationEntity;
import org.dcm4che3.net.Association;
import org.dcm4che3.net.Connection;
import org.dcm4che3.net.Device;
import org.dcm4che3.net.IncompatibleConnectionException;
import org.dcm4che3.net.QueryOption;
import org.dcm4che3.net.TransferCapability;
import org.dcm4che3.net.pdu.AAssociateRQ;
import org.dcm4che3.net.pdu.ExtendedNegotiation;
import org.dcm4che3.net.pdu.PresentationContext;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.EnumSet;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

/**
* @Description:
* @Author: HoleLin
* @CreateDate: 2022/8/15 10:20
* @UpdateUser: HoleLin
* @UpdateDate: 2022/8/15 10:20
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
public abstract class AbstractDcm4cheTemplate {

/**
* 本地设备
*/
private Device device = new Device();

/**
* 本地pacs名称
*/
private ApplicationEntity ae = new ApplicationEntity();

/**
* 本地连接
*/
private Connection local = new Connection();

/**
* 远程连接 即Find操作的目标服务
*/
private Connection remote = new Connection();

/**
* 连接请求
*/
public AAssociateRQ aarq = new AAssociateRQ();

/**
* DICOM连接
*/
public Association association;

private final ExecutorService executorService = Executors.newSingleThreadExecutor();
final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();


public AbstractDcm4cheTemplate(String deviceName, String aeTitle, String remoteHostName,
Integer remotePort, String remoteAeTitle) {
device.setDeviceName(deviceName);
ae.setAETitle(aeTitle);
remote.setHostname(remoteHostName);
remote.setPort(remotePort);
aarq.setCalledAET(remoteAeTitle);

device.addConnection(local);
device.addApplicationEntity(ae);
ae.addConnection(local);
ae.addTransferCapability(new TransferCapability(null, "*", TransferCapability.Role.SCP, "*"));
device.setExecutor(executorService);
device.setScheduledExecutor(scheduledExecutorService);
}

private void connect() throws IncompatibleConnectionException, GeneralSecurityException, IOException, InterruptedException {
association = ae.connect(local, remote, aarq);
}

private void disconnect() {
if (Objects.nonNull(association) && association.isReadyForDataTransfer()) {
try {
association.waitForOutstandingRSP();
association.release();
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
}
}
}

public void template(InformationModelEnum model, Attributes conditions) throws IncompatibleConnectionException,
GeneralSecurityException, IOException, InterruptedException {
preConnect(model);
connect();
try {
execute(model, conditions);
} finally {
disconnect();
}
}

/**
* @param model
* @param conditions
*/
abstract void execute(InformationModelEnum model, Attributes conditions) throws IOException, InterruptedException;

/**
*
* @param model
*/
abstract void preConnect(InformationModelEnum model);
}

SCP实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package cn.holelin.dicom.pacs.support;

import cn.holelin.dicom.pacs.StoreScpDimseHandler;
import cn.holelin.dicom.pacs.param.PacsScpParam;
import lombok.extern.slf4j.Slf4j;
import org.dcm4che3.net.ApplicationEntity;
import org.dcm4che3.net.Connection;
import org.dcm4che3.net.Device;
import org.dcm4che3.net.TransferCapability;
import org.dcm4che3.net.service.BasicCEchoSCP;
import org.dcm4che3.net.service.BasicCStoreSCP;
import org.dcm4che3.net.service.DicomServiceRegistry;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* Pacs SCP 实现类
*
* @Description: 提供启动SCP, 停止SCP
* @Author: HoleLin
* @CreateDate: 2022/12/7 17:06
* @UpdateUser: HoleLin
* @UpdateDate: 2022/12/7 17:06
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
@Slf4j
public class PacsScpSupport {

private final Device device = new Device();
private final ApplicationEntity ae = new ApplicationEntity("*");
private final Connection conn = new Connection();
private final ExecutorService executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(), Executors.defaultThreadFactory());
/**
* 接收DICOM目录
*/
private String storageDir;




public PacsScpSupport(PacsScpParam param) {
// 设置工作目录
this.storageDir = param.getStorageDir();
final String remoteAeTitle = param.getRemoteAeTitle();
final Integer remotePort = param.getRemotePort();
final String remoteHostName = param.getRemoteHostName();
final String deviceName = param.getDeviceName();

// 设置DIMSE请求处理器
final StoreScpDimseHandler storeScpDimseHandler = new StoreScpDimseHandler(storageDir);
device.setDimseRQHandler(createServiceRegistry(storeScpDimseHandler));

device.setDeviceName(deviceName);
conn.setPort(remotePort);
conn.setHostname(remoteHostName);
ae.setAETitle(remoteAeTitle);

device.addConnection(conn);
device.addApplicationEntity(ae);
ae.setAssociationAcceptor(true);
// 设置SCP接收所有类型的SOP Class并且接受所有传输协议
ae.addTransferCapability(new TransferCapability(null, "*",
TransferCapability.Role.SCP, "*"));
ae.addConnection(conn);
}

/**
* 启动一个SCP
* @throws GeneralSecurityException
* @throws IOException
*/
public void start() throws GeneralSecurityException, IOException {
device.setExecutor(executorService);
device.bindConnections();
log.info("DICOM PACS SCP start SUCCESS,aet:{},host:{},port:{}", ae.getAETitle(), conn.getHostname(),
conn.getPort());
}

/**
* 关闭当前SCP
*/
public void shutdown() {
device.unbindConnections();
executorService.shutdown();
log.info("DICOM PACS SCP stop SUCCESS");
}


/**
* 配置DICOM服务
*
* @return DicomServiceRegistry
*/
private DicomServiceRegistry createServiceRegistry(BasicCStoreSCP handler) {
DicomServiceRegistry serviceRegistry = new DicomServiceRegistry();
// 支持C-ECHO操作
serviceRegistry.addDicomService(new BasicCEchoSCP());
if (Objects.nonNull(handler)) {
serviceRegistry.addDicomService(handler);
}
return serviceRegistry;
}
}

C-FIND实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package cn.holelin.dicom.util.pacs;

import cn.holelin.dicom.enums.InformationModelEnum;
import java.util.EnumSet;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.net.Association;
import org.dcm4che3.net.DimseRSPHandler;
import org.dcm4che3.net.Priority;
import org.dcm4che3.net.QueryOption;
import org.dcm4che3.net.Status;

import java.io.IOException;
import org.dcm4che3.net.pdu.ExtendedNegotiation;
import org.dcm4che3.net.pdu.PresentationContext;

/**
* @Description:
* @Author: HoleLin
* @CreateDate: 2022/8/15 15:24
* @UpdateUser: HoleLin
* @UpdateDate: 2022/8/15 15:24
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
public class FindScuHelper extends AbstractDcm4cheTemplate {

public FindScuHelper(String deviceName, String aeTitle, String remoteHostName,
Integer remotePort, String remoteAeTitle) {
super(deviceName, aeTitle, remoteHostName, remotePort, remoteAeTitle);
}

@Override
void execute(InformationModelEnum model, Attributes conditions) throws IOException, InterruptedException {
// 执行c-find
this.association.cfind(model.cuid, Priority.NORMAL, conditions,
null, new DimseRSPHandler(association.nextMessageID()) {

int cancelAfter;
int numMatches;

@Override
public void onDimseRSP(Association as, Attributes cmd, Attributes data) {
super.onDimseRSP(as, cmd, data);
int status = cmd.getInt(Tag.Status, -1);
System.out.println("patientName: "+data.getString(Tag.PatientName));
if (Status.isPending(status)) {
++numMatches;
if (cancelAfter != 0 && numMatches >= cancelAfter) {
try {
cancel(as);
cancelAfter = 0;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
});
}

@Override
void preConnect(InformationModelEnum model) {
aarq.addPresentationContext(new PresentationContext(
this.aarq.getNumberOfPresentationContexts() * 2 + 1, model.cuid,
UID.ImplicitVRLittleEndian,
UID.ExplicitVRLittleEndian,
UID.ExplicitVRBigEndian
));
aarq.addPresentationContextFor(model.cuid, UID.ImplicitVRLittleEndian);
this.aarq.addPresentationContext(new PresentationContext(1,
UID.Verification, UID.ImplicitVRLittleEndian));
final EnumSet<QueryOption> queryOptions = EnumSet.allOf(QueryOption.class);

aarq.addExtendedNegotiation(new ExtendedNegotiation(model.cuid, QueryOption.toExtendedNegotiationInformation(queryOptions)));
}

}

执行C-FIND请求实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package cn.holelin.dicom.util.pacs;

import lombok.Data;

import java.time.LocalDate;
import java.time.LocalTime;

/**
* @Description:
* @Author: HoleLin
* @CreateDate: 2022/8/15 15:39
* @UpdateUser: HoleLin
* @UpdateDate: 2022/8/15 15:39
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
@Data
public class PacsStudySearchRequest {
private String patientId;
private String patientName;
private String accessionNumber;
private String studyInstanceUid;
private LocalDate startDate;
private LocalDate endDate;
private LocalTime startTime;
private LocalTime endTime;
}

C-MOVE实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package cn.holelin.dicom.util.pacs;

import cn.holelin.dicom.enums.InformationModelEnum;
import java.io.IOException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.net.Association;
import org.dcm4che3.net.DimseRSPHandler;
import org.dcm4che3.net.pdu.PresentationContext;

/**
* @Description:
* @Author: HoleLin
* @CreateDate: 2022/11/9 14:41
* @UpdateUser: HoleLin
* @UpdateDate: 2022/11/9 14:41
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
public class MoveScuHelper extends AbstractDcm4cheTemplate {

private String desAeTitle;
private int priority;

private int cancelAfter;
private ScheduledFuture<?> scheduledCancel;
private boolean releaseEager;

public MoveScuHelper(String deviceName, String aeTitle, String remoteHostName, Integer remotePort,
String remoteAeTitle, String desAeTitle, int priority) {
super(deviceName, aeTitle, remoteHostName, remotePort, remoteAeTitle);
this.desAeTitle = desAeTitle;
this.priority = priority;
}




@Override
void execute(InformationModelEnum model, Attributes conditions) throws IOException, InterruptedException {
final DimseRSPHandler rspHandler = new DimseRSPHandler(this.association.nextMessageID()) {
@Override
public void onDimseRSP(Association as, Attributes cmd,
Attributes data) {
super.onDimseRSP(as, cmd, data);
}
};
this.association.cmove(model.cuid, priority, conditions, null, desAeTitle, rspHandler);
if (cancelAfter > 0) {
scheduledCancel = schedule(() -> {
try {
rspHandler.cancel(association);
if (releaseEager) {
association.release();
}
} catch (IOException e) {
e.printStackTrace();
}
}, cancelAfter, TimeUnit.MILLISECONDS);
}
}

private ScheduledFuture<?> schedule(Runnable command, int delay, TimeUnit milliseconds) {
if (this.scheduledExecutorService == null)
throw new IllegalStateException(
"scheduled executor service not initialized");

return scheduledExecutorService.schedule(command, delay, milliseconds);

}
private static String[] IVR_LE_FIRST = {
UID.ImplicitVRLittleEndian,
UID.ExplicitVRLittleEndian,
UID.ExplicitVRBigEndian
};
@Override
void preConnect(InformationModelEnum model) {
this.aarq.addPresentationContext(new PresentationContext(1,model.cuid,IVR_LE_FIRST));
}
}

C-STORE实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package cn.holelin.dicom.util.pacs;

import cn.holelin.dicom.enums.InformationModelEnum;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.io.DicomInputStream;
import org.dcm4che3.io.DicomInputStream.IncludeBulkData;
import org.dcm4che3.net.Association;
import org.dcm4che3.net.DimseRSPHandler;
import org.dcm4che3.net.InputStreamDataWriter;
import org.dcm4che3.net.Status;
import org.dcm4che3.net.pdu.PresentationContext;
import org.dcm4che3.tool.common.DicomFiles.Callback;
import org.dcm4che3.util.SafeClose;
import org.dcm4che3.util.StringUtils;
import org.dcm4che3.util.TagUtils;

/**
* @Description:
* @Author: HoleLin
* @CreateDate: 2022/11/8 14:56
* @UpdateUser: HoleLin
* @UpdateDate: 2022/11/8 14:56
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
public class StoreScuHelper extends AbstractDcm4cheTemplate {

private String filePath;
private List<ScuInfo> list = new ArrayList<>();

public StoreScuHelper(String deviceName, String aeTitle, String remoteHostName, Integer remotePort,
String remoteAeTitle, String filePath) {
super(deviceName, aeTitle, remoteHostName, remotePort, remoteAeTitle);
this.filePath = filePath;
}

@Override
void execute(InformationModelEnum model, Attributes conditions) throws IOException {
for (ScuInfo info : list) {
if (this.association.isReadyForDataTransfer()) {
try {
send(new File(info.getFilePath()), info.getEndFmi(), info.getCuid(), info.getIuid(),
info.getTs());
} catch (Exception e) {
e.printStackTrace();
}
try {
this.association.waitForOutstandingRSP();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
list.clear();
}

@Override
void preConnect(InformationModelEnum model) {
final Path path = Paths.get(filePath);
doScan(path, (file, fmi, dsPos, ds) -> {
if (!addFile(file, dsPos, fmi)) {
return false;
}
return true;
});
}


private void doScan(Path path, Callback callback) {
if (Files.isDirectory(path)) {
try {
Files.list(path).forEach(it -> doScan(it, callback));
} catch (IOException e) {
throw new RuntimeException(e);
}

} else {
final File file = path.toFile();
try (DicomInputStream in = new DicomInputStream(file)) {
in.setIncludeBulkData(IncludeBulkData.NO);
Attributes fmi = in.readFileMetaInformation();
long dsPos = in.getPosition();
Attributes ds = in.readDatasetUntilPixelData();
if (fmi == null || !fmi.containsValue(Tag.TransferSyntaxUID)
|| !fmi.containsValue(Tag.MediaStorageSOPClassUID)
|| !fmi.containsValue(Tag.MediaStorageSOPInstanceUID)) {
fmi = ds.createFileMetaInformation(in.getTransferSyntax());
}
boolean b = callback.dicomFile(file, fmi, dsPos, ds);
} catch (Exception e) {
throw new RuntimeException(e);
}

}
}

private boolean addFile(File f, long endFmi,
Attributes fmi) throws IOException {
String cuid = fmi.getString(Tag.MediaStorageSOPClassUID);
String iuid = fmi.getString(Tag.MediaStorageSOPInstanceUID);
String ts = fmi.getString(Tag.TransferSyntaxUID);
if (cuid == null || iuid == null) {
return false;
}
final ScuInfo scuInfo = ScuInfo.builder().cuid(cuid).ts(ts).iuid(iuid).endFmi(endFmi).filePath(f.getPath())
.build();

list.add(scuInfo);
if (this.aarq.containsPresentationContextFor(cuid, ts)) {
return true;
}

if (!this.aarq.containsPresentationContextFor(cuid)) {
if (!ts.equals(UID.ExplicitVRLittleEndian)) {
this.aarq.addPresentationContext(
new PresentationContext(this.aarq.getNumberOfPresentationContexts() * 2 + 1, cuid,
UID.ExplicitVRLittleEndian));
}
if (!ts.equals(UID.ImplicitVRLittleEndian)) {
this.aarq.addPresentationContext(
new PresentationContext(this.aarq.getNumberOfPresentationContexts() * 2 + 1, cuid,
UID.ImplicitVRLittleEndian));
}
}
this.aarq.addPresentationContext(
new PresentationContext(this.aarq.getNumberOfPresentationContexts() * 2 + 1, cuid, ts));
return true;
}

public void send(final File f, long fmiEndPos, String cuid, String iuid,
String filets) throws FileNotFoundException {
String ts = selectTransferSyntax(cuid, filets);

FileInputStream in = new FileInputStream(f);
try {
in.skip(fmiEndPos);
InputStreamDataWriter data = new InputStreamDataWriter(in);
this.association.cstore(cuid, iuid, 0, data, ts,
rspHandlerFactory.createDimseRSPHandler(this.association, f));
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
} finally {
SafeClose.close(in);
}
}

private String selectTransferSyntax(String cuid, String filets) {
Set<String> tss = this.association.getTransferSyntaxesFor(cuid);
if (tss.contains(filets)) {
return filets;
}

if (tss.contains(UID.ExplicitVRLittleEndian)) {
return UID.ExplicitVRLittleEndian;
}

return UID.ImplicitVRLittleEndian;
}

public interface RSPHandlerFactory {

DimseRSPHandler createDimseRSPHandler(Association as, File file);
}

private RSPHandlerFactory rspHandlerFactory = new RSPHandlerFactory() {

@Override
public DimseRSPHandler createDimseRSPHandler(Association as, final File file) {
return new DimseRSPHandler(as.nextMessageID()) {
@Override
public void onDimseRSP(Association as, Attributes cmd,
Attributes data) {
super.onDimseRSP(as, cmd, data);
onCStoreRSP(cmd, file);
}
};
}
};

private void onCStoreRSP(Attributes cmd, File f) {
int status = cmd.getInt(Tag.Status, -1);
switch (status) {
case Status.Success:

System.out.print('.');
break;
case Status.CoercionOfDataElements:
case Status.ElementsDiscarded:
case Status.DataSetDoesNotMatchSOPClassWarning:
System.err.println(MessageFormat.format(
TagUtils.shortToHexString(status), f));
System.err.println(cmd);
break;
default:
System.out.print('E');
System.err.println(cmd);
}
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ScuInfo {

private String cuid;
private String iuid;
private String ts;
private long endFmi;
private String filePath;

@Override
public String toString() {
return "ScuInfo{" +
"cuid='" + cuid + '\'' +
", iuid='" + iuid + '\'' +
", ts='" + ts + '\'' +
", endFmi=" + endFmi +
", filePath='" + filePath + '\'' +
'}';
}
}

操作工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package cn.holelin.dicom.util.pacs;

import cn.holelin.dicom.enums.InformationModelEnum;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.VR;
import org.dcm4che3.net.IncompatibleConnectionException;
import org.dcm4che3.tool.common.CLIUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

/**
* @Description:
* @Author: HoleLin
* @CreateDate: 2022/8/15 15:30
* @UpdateUser: HoleLin
* @UpdateDate: 2022/8/15 15:30
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
public class PacsUtil {

private static final String DEFAULT_DEVICE_NAME = "cjl-test";
public static final String DEFAULT_AE_TITLE = "cjl-test";
public static final String DEFAULT_REMOTE_HOST = "192.168.253.173";
public static final int DEFAULT_REMOTE_PORT = 11112;
public static final String DEFAULT_REMOTE_AE_TITLE = "DCM4CHEE";


public void moveScu(PacsStudySearchRequest request, String destAeTitle, int priority)
throws IncompatibleConnectionException, GeneralSecurityException, IOException, InterruptedException {
final MoveScuHelper moveScuHelper = new MoveScuHelper(DEFAULT_DEVICE_NAME, DEFAULT_AE_TITLE,
DEFAULT_REMOTE_HOST, DEFAULT_REMOTE_PORT, DEFAULT_REMOTE_AE_TITLE, destAeTitle, priority);
moveScuHelper.template(InformationModelEnum.STUDY_ROOT, buildAttributes(request));
}

public void storeScu(String filePath)
throws IncompatibleConnectionException, GeneralSecurityException, IOException, InterruptedException {
final StoreScuHelper storeScuHelper = new StoreScuHelper(DEFAULT_DEVICE_NAME, DEFAULT_AE_TITLE,
DEFAULT_REMOTE_HOST, DEFAULT_REMOTE_PORT, DEFAULT_REMOTE_AE_TITLE, filePath);
storeScuHelper.template(InformationModelEnum.STORE, null);
}

public void findScu(PacsStudySearchRequest request)
throws IncompatibleConnectionException, GeneralSecurityException, IOException, InterruptedException {
final FindScuHelper helper = new FindScuHelper(DEFAULT_DEVICE_NAME, DEFAULT_AE_TITLE,
DEFAULT_REMOTE_HOST, DEFAULT_REMOTE_PORT, DEFAULT_REMOTE_AE_TITLE);
helper.template(InformationModelEnum.FIND, buildAttributes(request));
}

private Attributes buildAttributes(PacsStudySearchRequest request) {
final Attributes attributes = new Attributes();
final LocalDate startDate = request.getStartDate();
final LocalDate endDate = request.getEndDate();
if (Objects.nonNull(startDate) && Objects.nonNull(endDate)) {
final String days = startDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + endDate.format(
DateTimeFormatter.ofPattern("yyyyMMdd"));
CLIUtils.addAttributes(attributes, CLIUtils.toTags(new String[]{"StudyDate"}), days);

}
final LocalTime startTime = request.getStartTime();
final LocalTime endTime = request.getEndTime();
if (Objects.nonNull(startTime) && Objects.nonNull(endTime)) {
final String times = startTime.format(DateTimeFormatter.ofPattern("hhmmss")) + "-" + endTime.format(
DateTimeFormatter.ofPattern("hhmmss"));
CLIUtils.addAttributes(attributes, CLIUtils.toTags(new String[]{"StudyTime"}), times);
}
attributes.setString(Tag.QueryRetrieveLevel, VR.CS, InformationModelEnum.FIND.level);
final String studyInstanceUid = request.getStudyInstanceUid();
if (StringUtils.hasText(studyInstanceUid)) {
attributes.setString(Tag.StudyInstanceUID, VR.CS, studyInstanceUid);
}

final String patientId = request.getPatientId();
if (StringUtils.hasText(patientId)) {
attributes.setString(Tag.PatientID, VR.CS, patientId);
}
final String patientName = request.getPatientName();
if (StringUtils.hasText(patientName)) {
attributes.setString(Tag.PatientName, VR.CS, patientName);
}
final String accessionNumber = request.getAccessionNumber();
if (StringUtils.hasText(accessionNumber)) {
attributes.setString(Tag.AccessionNumber, VR.CS, accessionNumber);
}
return attributes;
}

public static void main(String[] args)
throws IncompatibleConnectionException, GeneralSecurityException, IOException, InterruptedException {
final PacsStudySearchRequest request = new PacsStudySearchRequest();
final PacsUtil pacsUtil = new PacsUtil();
// -c test@192.168.30.46:11112 /Users/holelin/dcm/3569
// pacsUtil.storeScu("/Users/holelin/dcm/xxxx");

// -c DCM4CHEE@192.168.253.173:11112 -m StudyInstanceUID=1.2.840.86.755.86226839.811.1620479 --dest test
request.setStudyInstanceUid("1.2.840.78.75.7.5.2648675.15027116692");
pacsUtil.moveScu(request, "test", 0);
}
}