PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
26
28
29#include <chrono>
30#include <cstdlib>
31#include <iostream>
32#include <string>
33
34namespace {
35
37constexpr const char* default_calling_ae = "PRINT_SCU";
38
40constexpr auto default_timeout = std::chrono::milliseconds{30000};
41
45enum class print_command {
46 print, // Full print workflow
47 status // Printer status query
48};
49
53struct options {
54 // Connection
55 std::string host;
56 uint16_t port{0};
57 std::string called_ae;
58 std::string calling_ae{default_calling_ae};
59
60 // Command
61 print_command command{print_command::print};
62
63 // Film Session options
64 uint32_t copies{1};
65 std::string priority{"MED"};
66 std::string medium_type{"BLUE FILM"};
67 std::string film_destination{"MAGAZINE"};
68 std::string session_label;
69
70 // Film Box options
71 std::string display_format{"STANDARD\\1,1"};
72 std::string orientation{"PORTRAIT"};
73 std::string film_size{"8INX10IN"};
74 std::string magnification;
75
76 // Output options
77 bool verbose{false};
78};
79
83void print_usage(const char* program_name) {
84 std::cout << R"(
85Print SCU - DICOM Print Management Client
86
87Usage: )" << program_name << R"( <host> <port> <called_ae> <command> [options]
88
89Arguments:
90 host Remote host address (IP or hostname)
91 port Remote port number (typically 10400)
92 called_ae Called AE Title (remote Print SCP's AE title)
93 command 'print' or 'status'
94
95Commands:
96 print Execute full print workflow (session + film box + print + cleanup)
97 status Query printer status via N-GET
98
99Film Session Options (for 'print' command):
100 --copies <n> Number of copies [default: 1]
101 --priority <p> Print priority: HIGH, MED, LOW [default: MED]
102 --medium <type> Medium type: PAPER, CLEAR FILM, BLUE FILM [default: BLUE FILM]
103 --destination <dest> Film destination: MAGAZINE, PROCESSOR [default: MAGAZINE]
104 --label <text> Film session label
105
106Film Box Options (for 'print' command):
107 --format <fmt> Image display format [default: STANDARD\1,1]
108 --orientation <o> Film orientation: PORTRAIT, LANDSCAPE [default: PORTRAIT]
109 --film-size <size> Film size: 8INX10IN, 14INX17IN, etc. [default: 8INX10IN]
110 --magnification <m> Magnification type: REPLICATE, BILINEAR, CUBIC, NONE
111
112General Options:
113 --calling-ae <ae> Calling AE Title [default: PRINT_SCU]
114 --verbose, -v Show detailed progress
115 --help, -h Show this help message
116
117Examples:
118 # Print with default settings
119 )" << program_name << R"( localhost 10400 PRINTER print
120
121 # Print with custom options
122 )" << program_name << R"( localhost 10400 PRINTER print \
123 --copies 2 --priority HIGH --medium PAPER \
124 --format "STANDARD\2,2" --orientation LANDSCAPE
125
126 # Query printer status
127 )" << program_name << R"( localhost 10400 PRINTER status
128
129Exit Codes:
130 0 Success
131 1 Print operation failed
132 2 Connection or argument error
133)";
134}
135
139bool parse_arguments(int argc, char* argv[], options& opts) {
140 if (argc < 5) {
141 return false;
142 }
143
144 opts.host = argv[1];
145
146 // Parse port
147 try {
148 int port_int = std::stoi(argv[2]);
149 if (port_int < 1 || port_int > 65535) {
150 std::cerr << "Error: Port must be between 1 and 65535\n";
151 return false;
152 }
153 opts.port = static_cast<uint16_t>(port_int);
154 } catch (const std::exception&) {
155 std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
156 return false;
157 }
158
159 opts.called_ae = argv[3];
160 if (opts.called_ae.length() > 16) {
161 std::cerr << "Error: Called AE title exceeds 16 characters\n";
162 return false;
163 }
164
165 // Parse command
166 std::string cmd = argv[4];
167 if (cmd == "print") {
168 opts.command = print_command::print;
169 } else if (cmd == "status") {
170 opts.command = print_command::status;
171 } else if (cmd == "--help" || cmd == "-h") {
172 return false;
173 } else {
174 std::cerr << "Error: Unknown command '" << cmd << "'. Use 'print' or 'status'\n";
175 return false;
176 }
177
178 // Parse optional arguments
179 for (int i = 5; i < argc; ++i) {
180 std::string arg = argv[i];
181
182 if (arg == "--help" || arg == "-h") {
183 return false;
184 }
185 if (arg == "--verbose" || arg == "-v") {
186 opts.verbose = true;
187 } else if (arg == "--calling-ae" && i + 1 < argc) {
188 opts.calling_ae = argv[++i];
189 if (opts.calling_ae.length() > 16) {
190 std::cerr << "Error: Calling AE title exceeds 16 characters\n";
191 return false;
192 }
193 }
194 // Film Session options
195 else if (arg == "--copies" && i + 1 < argc) {
196 opts.copies = static_cast<uint32_t>(std::stoul(argv[++i]));
197 } else if (arg == "--priority" && i + 1 < argc) {
198 opts.priority = argv[++i];
199 } else if (arg == "--medium" && i + 1 < argc) {
200 opts.medium_type = argv[++i];
201 } else if (arg == "--destination" && i + 1 < argc) {
202 opts.film_destination = argv[++i];
203 } else if (arg == "--label" && i + 1 < argc) {
204 opts.session_label = argv[++i];
205 }
206 // Film Box options
207 else if (arg == "--format" && i + 1 < argc) {
208 opts.display_format = argv[++i];
209 } else if (arg == "--orientation" && i + 1 < argc) {
210 opts.orientation = argv[++i];
211 } else if (arg == "--film-size" && i + 1 < argc) {
212 opts.film_size = argv[++i];
213 } else if (arg == "--magnification" && i + 1 < argc) {
214 opts.magnification = argv[++i];
215 } else {
216 std::cerr << "Error: Unknown option '" << arg << "'\n";
217 return false;
218 }
219 }
220
221 return true;
222}
223
228 const options& opts) {
229
230 using namespace kcenon::pacs::network;
231 using namespace kcenon::pacs::services;
232
233 association_config config;
234 config.calling_ae_title = opts.calling_ae;
235 config.called_ae_title = opts.called_ae;
236 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.2";
237 config.implementation_version_name = "PRINT_SCU_001";
238
239 // Propose Basic Grayscale Print Management Meta SOP Class
240 // (bundles Film Session, Film Box, Image Box, and Printer)
241 config.proposed_contexts.push_back({
242 1,
243 std::string(basic_grayscale_print_meta_sop_class_uid),
244 {
245 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
246 "1.2.840.10008.1.2" // Implicit VR Little Endian
247 }
248 });
249
250 // Also propose individual SOP classes as fallback
251 config.proposed_contexts.push_back({
252 3,
253 std::string(basic_film_session_sop_class_uid),
254 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
255 });
256 config.proposed_contexts.push_back({
257 5,
258 std::string(basic_film_box_sop_class_uid),
259 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
260 });
261 config.proposed_contexts.push_back({
262 7,
263 std::string(basic_grayscale_image_box_sop_class_uid),
264 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
265 });
266 config.proposed_contexts.push_back({
267 9,
268 std::string(printer_sop_class_uid),
269 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
270 });
271
272 return association::connect(opts.host, opts.port, config, default_timeout);
273}
274
278int perform_print(const options& opts) {
279 using namespace kcenon::pacs::services;
280
281 if (opts.verbose) {
282 std::cout << "=== Print Workflow ===\n";
283 std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
284 std::cout << " Calling AE: " << opts.calling_ae << "\n";
285 std::cout << " Called AE: " << opts.called_ae << "\n";
286 std::cout << " Copies: " << opts.copies << "\n";
287 std::cout << " Priority: " << opts.priority << "\n";
288 std::cout << " Medium: " << opts.medium_type << "\n";
289 std::cout << " Format: " << opts.display_format << "\n";
290 std::cout << " Orientation: " << opts.orientation << "\n";
291 std::cout << " Film Size: " << opts.film_size << "\n\n";
292 }
293
294 auto start_time = std::chrono::steady_clock::now();
295
296 // Establish association
297 auto connect_result = create_print_association(opts);
298 if (connect_result.is_err()) {
299 std::cerr << "Failed to establish association: "
300 << connect_result.error().message << "\n";
301 return 2;
302 }
303 auto& assoc = connect_result.value();
304
305 if (opts.verbose) {
306 auto connect_time = std::chrono::steady_clock::now();
307 auto connect_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
308 connect_time - start_time);
309 std::cout << "Association established in " << connect_ms.count() << " ms\n";
310 }
311
312 print_scu scu;
313
314 // Step 1: Create Film Session
315 if (opts.verbose) {
316 std::cout << "\n[1/4] Creating Film Session...\n";
317 }
318
319 print_session_data session_data;
320 session_data.number_of_copies = opts.copies;
321 session_data.print_priority = opts.priority;
322 session_data.medium_type = opts.medium_type;
323 session_data.film_destination = opts.film_destination;
324 session_data.film_session_label = opts.session_label;
325
326 auto session_result = scu.create_film_session(assoc, session_data);
327 if (session_result.is_err()) {
328 std::cerr << "Failed to create Film Session: "
329 << session_result.error().message << "\n";
330 assoc.abort();
331 return 1;
332 }
333 if (!session_result.value().is_success()) {
334 std::cerr << "Film Session creation returned status: 0x"
335 << std::hex << session_result.value().status << std::dec << "\n";
336 (void)assoc.release(default_timeout);
337 return 1;
338 }
339
340 auto session_uid = session_result.value().sop_instance_uid;
341 if (opts.verbose) {
342 std::cout << " Film Session UID: " << session_uid << "\n";
343 }
344
345 // Step 2: Create Film Box
346 if (opts.verbose) {
347 std::cout << "\n[2/4] Creating Film Box...\n";
348 }
349
350 print_film_box_data box_data;
351 box_data.image_display_format = opts.display_format;
352 box_data.film_orientation = opts.orientation;
353 box_data.film_size_id = opts.film_size;
354 box_data.magnification_type = opts.magnification;
355 box_data.film_session_uid = session_uid;
356
357 auto box_result = scu.create_film_box(assoc, box_data);
358 if (box_result.is_err()) {
359 std::cerr << "Failed to create Film Box: "
360 << box_result.error().message << "\n";
361 (void)scu.delete_film_session(assoc, session_uid);
362 (void)assoc.release(default_timeout);
363 return 1;
364 }
365 if (!box_result.value().is_success()) {
366 std::cerr << "Film Box creation returned status: 0x"
367 << std::hex << box_result.value().status << std::dec << "\n";
368 (void)scu.delete_film_session(assoc, session_uid);
369 (void)assoc.release(default_timeout);
370 return 1;
371 }
372
373 auto film_box_uid = box_result.value().sop_instance_uid;
374 if (opts.verbose) {
375 std::cout << " Film Box UID: " << film_box_uid << "\n";
376 }
377
378 // Step 3: Print Film Box (N-ACTION)
379 if (opts.verbose) {
380 std::cout << "\n[3/4] Printing Film Box...\n";
381 }
382
383 auto print_result_val = scu.print_film_box(assoc, film_box_uid);
384 if (print_result_val.is_err()) {
385 std::cerr << "Failed to print Film Box: "
386 << print_result_val.error().message << "\n";
387 (void)scu.delete_film_session(assoc, session_uid);
388 (void)assoc.release(default_timeout);
389 return 1;
390 }
391 if (!print_result_val.value().is_success()) {
392 std::cerr << "Print action returned status: 0x"
393 << std::hex << print_result_val.value().status << std::dec << "\n";
394 if (!print_result_val.value().error_comment.empty()) {
395 std::cerr << " Error: " << print_result_val.value().error_comment << "\n";
396 }
397 (void)scu.delete_film_session(assoc, session_uid);
398 (void)assoc.release(default_timeout);
399 return 1;
400 }
401
402 // Step 4: Clean up - Delete Film Session
403 if (opts.verbose) {
404 std::cout << "\n[4/4] Cleaning up (deleting Film Session)...\n";
405 }
406
407 auto delete_result = scu.delete_film_session(assoc, session_uid);
408 if (delete_result.is_err() && opts.verbose) {
409 std::cerr << "Warning: Film Session delete failed: "
410 << delete_result.error().message << "\n";
411 }
412
413 // Release association
414 if (opts.verbose) {
415 std::cout << "\nReleasing association...\n";
416 }
417 auto release_result = assoc.release(default_timeout);
418 if (release_result.is_err() && opts.verbose) {
419 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
420 }
421
422 auto end_time = std::chrono::steady_clock::now();
423 auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
424 end_time - start_time);
425
426 // Success output
427 std::cout << "\n";
428 std::cout << "========================================\n";
429 std::cout << " Print Completed Successfully\n";
430 std::cout << "========================================\n";
431 std::cout << " Session UID: " << session_uid << "\n";
432 std::cout << " Film Box UID: " << film_box_uid << "\n";
433 std::cout << " Copies: " << opts.copies << "\n";
434 std::cout << " Priority: " << opts.priority << "\n";
435 std::cout << " Medium: " << opts.medium_type << "\n";
436 std::cout << " Film Size: " << opts.film_size << "\n";
437 std::cout << " Total time: " << total_ms.count() << " ms\n";
438 std::cout << "========================================\n";
439
440 return 0;
441}
442
446int perform_status_query(const options& opts) {
447 using namespace kcenon::pacs::services;
448
449 if (opts.verbose) {
450 std::cout << "=== Printer Status Query ===\n";
451 std::cout << "Connecting to " << opts.host << ":" << opts.port << "...\n";
452 std::cout << " Calling AE: " << opts.calling_ae << "\n";
453 std::cout << " Called AE: " << opts.called_ae << "\n\n";
454 }
455
456 auto start_time = std::chrono::steady_clock::now();
457
458 auto connect_result = create_print_association(opts);
459 if (connect_result.is_err()) {
460 std::cerr << "Failed to establish association: "
461 << connect_result.error().message << "\n";
462 return 2;
463 }
464 auto& assoc = connect_result.value();
465
466 if (opts.verbose) {
467 auto connect_time = std::chrono::steady_clock::now();
468 auto connect_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
469 connect_time - start_time);
470 std::cout << "Association established in " << connect_ms.count() << " ms\n";
471 std::cout << "Sending N-GET Printer Status...\n";
472 }
473
474 print_scu scu;
475
476 auto status_result = scu.query_printer_status(assoc);
477 if (status_result.is_err()) {
478 std::cerr << "Failed to query printer status: "
479 << status_result.error().message << "\n";
480 assoc.abort();
481 return 1;
482 }
483
484 if (!status_result.value().is_success()) {
485 std::cerr << "Printer status query returned status: 0x"
486 << std::hex << status_result.value().status << std::dec << "\n";
487 (void)assoc.release(default_timeout);
488 return 1;
489 }
490
491 // Extract printer status from response dataset
492 const auto& response = status_result.value().response_data;
493 std::string printer_status_str = "UNKNOWN";
494 std::string printer_status_info;
495 std::string printer_name_str;
496
497 if (response.contains(print_tags::printer_status_tag)) {
498 printer_status_str = response.get_string(print_tags::printer_status_tag);
499 }
500 if (response.contains(print_tags::printer_status_info)) {
501 printer_status_info = response.get_string(print_tags::printer_status_info);
502 }
503 if (response.contains(print_tags::printer_name)) {
504 printer_name_str = response.get_string(print_tags::printer_name);
505 }
506
507 // Release association
508 if (opts.verbose) {
509 std::cout << "Releasing association...\n";
510 }
511 auto release_result = assoc.release(default_timeout);
512 if (release_result.is_err() && opts.verbose) {
513 std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
514 }
515
516 auto end_time = std::chrono::steady_clock::now();
517 auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
518 end_time - start_time);
519
520 // Output
521 std::cout << "\n";
522 std::cout << "========================================\n";
523 std::cout << " Printer Status\n";
524 std::cout << "========================================\n";
525 std::cout << " Status: " << printer_status_str << "\n";
526 if (!printer_status_info.empty()) {
527 std::cout << " Status Info: " << printer_status_info << "\n";
528 }
529 if (!printer_name_str.empty()) {
530 std::cout << " Printer Name: " << printer_name_str << "\n";
531 }
532 std::cout << " Query time: " << total_ms.count() << " ms\n";
533 std::cout << "========================================\n";
534
535 return 0;
536}
537
538} // namespace
539
540int main(int argc, char* argv[]) {
541 std::cout << R"(
542 ____ _ _ ____ ____ _ _
543 | _ \ _ __(_)_ __ | |_ / ___| / ___| | | |
544 | |_) | '__| | '_ \| __| \___ \| | | | | |
545 | __/| | | | | | | |_ ___) | |___| |_| |
546 |_| |_| |_|_| |_|\__| |____/ \____|\___/
547
548 DICOM Print Management Client
549)" << "\n";
550
551 options opts;
552
553 if (!parse_arguments(argc, argv, opts)) {
554 print_usage(argv[0]);
555 return 2;
556 }
557
558 if (opts.command == print_command::print) {
559 return perform_print(opts);
560 } else {
561 return perform_status_query(opts);
562 }
563}
DICOM Association management per PS3.8.
network::Result< print_result > create_film_session(network::association &assoc, const print_session_data &data)
Create a new Film Session (N-CREATE)
network::Result< print_result > print_film_box(network::association &assoc, std::string_view film_box_uid)
Print a Film Box (N-ACTION)
network::Result< print_result > create_film_box(network::association &assoc, const print_film_box_data &data)
Create a new Film Box (N-CREATE)
network::Result< print_result > delete_film_session(network::association &assoc, std::string_view session_uid)
Delete a Film Session (N-DELETE)
network::Result< print_result > query_printer_status(network::association &assoc)
Query printer status (N-GET)
int main()
Definition main.cpp:84
constexpr dicom_tag priority
Priority.
constexpr dicom_tag status
Status.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
constexpr core::dicom_tag film_destination
Film Destination (2000,0040)
Definition print_scp.h:423
constexpr core::dicom_tag medium_type
Medium Type (2000,0030)
Definition print_scp.h:420
constexpr core::dicom_tag printer_status_info
Printer Status Info (2110,0020)
Definition print_scp.h:459
@ print
Print Management Service Class.
DICOM Print Management SCU service (PS3.4 Annex H)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
Data for creating a Film Box via N-CREATE.
Definition print_scu.h:105
std::string magnification_type
Magnification type (REPLICATE, BILINEAR, CUBIC, NONE)
Definition print_scu.h:116
std::string film_orientation
Film orientation (PORTRAIT, LANDSCAPE)
Definition print_scu.h:110
std::string film_size_id
Film size ID (8INX10IN, 14INX17IN, etc.)
Definition print_scu.h:113
std::string image_display_format
Image display format (e.g., "STANDARD\\1,1")
Definition print_scu.h:107
std::string film_session_uid
Parent film session SOP Instance UID.
Definition print_scu.h:119
Data for creating a Film Session via N-CREATE.
Definition print_scu.h:82
std::string medium_type
Medium type (PAPER, CLEAR FILM, BLUE FILM)
Definition print_scu.h:93
std::string film_session_label
Film session label.
Definition print_scu.h:99
std::string film_destination
Film destination (MAGAZINE, PROCESSOR)
Definition print_scu.h:96
uint32_t number_of_copies
Number of copies to print.
Definition print_scu.h:87
std::string print_priority
Print priority (HIGH, MED, LOW)
Definition print_scu.h:90