PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
metadata_service.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
15
22
23#include <algorithm>
24#include <cmath>
25#include <filesystem>
26#include <sstream>
27
28namespace kcenon::pacs::web {
29
30// =============================================================================
31// Preset and Sort Order String Conversion
32// =============================================================================
33
34std::string_view preset_to_string(metadata_preset preset) {
35 switch (preset) {
37 return "image_display";
39 return "window_level";
41 return "patient_info";
43 return "acquisition";
45 return "positioning";
47 return "multiframe";
48 }
49 return "unknown";
50}
51
52std::optional<metadata_preset> preset_from_string(std::string_view str) {
53 if (str == "image_display") {
55 }
56 if (str == "window_level") {
58 }
59 if (str == "patient_info") {
61 }
62 if (str == "acquisition") {
64 }
65 if (str == "positioning") {
67 }
68 if (str == "multiframe") {
70 }
71 return std::nullopt;
72}
73
74std::string_view sort_order_to_string(sort_order order) {
75 switch (order) {
77 return "position";
79 return "instance_number";
81 return "acquisition_time";
82 }
83 return "unknown";
84}
85
86std::optional<sort_order> sort_order_from_string(std::string_view str) {
87 if (str == "position") {
89 }
90 if (str == "instance_number") {
92 }
93 if (str == "acquisition_time") {
95 }
96 return std::nullopt;
97}
98
99// =============================================================================
100// Helper Functions
101// =============================================================================
102
103namespace {
104
108std::string tag_to_hex(kcenon::pacs::core::dicom_tag tag) {
109 std::ostringstream oss;
110 oss << std::hex << std::uppercase << std::setfill('0') << std::setw(4)
111 << tag.group() << std::setw(4) << tag.element();
112 return oss.str();
113}
114
118std::optional<kcenon::pacs::core::dicom_tag> hex_to_tag(std::string_view hex) {
119 if (hex.size() != 8) {
120 return std::nullopt;
121 }
122
123 try {
124 uint16_t group =
125 static_cast<uint16_t>(std::stoul(std::string(hex.substr(0, 4)), nullptr, 16));
126 uint16_t element =
127 static_cast<uint16_t>(std::stoul(std::string(hex.substr(4, 4)), nullptr, 16));
128 return kcenon::pacs::core::dicom_tag{group, element};
129 } catch (...) {
130 return std::nullopt;
131 }
132}
133
137std::vector<double> parse_numeric_list(std::string_view str) {
138 std::vector<double> result;
139 std::string s(str);
140 std::istringstream iss(s);
141 std::string token;
142 while (std::getline(iss, token, '\\')) {
143 try {
144 result.push_back(std::stod(token));
145 } catch (...) {
146 // Skip invalid values
147 }
148 }
149 return result;
150}
151
155std::vector<std::string> parse_string_list(std::string_view str) {
156 std::vector<std::string> result;
157 std::string s(str);
158 std::istringstream iss(s);
159 std::string token;
160 while (std::getline(iss, token, '\\')) {
161 result.push_back(token);
162 }
163 return result;
164}
165
166} // namespace
167
168// =============================================================================
169// Construction / Destruction
170// =============================================================================
171
173 std::shared_ptr<storage::index_database> database)
174 : database_(std::move(database)) {}
175
177
178// =============================================================================
179// Preset Tag Definitions
180// =============================================================================
181
182std::unordered_set<std::string> metadata_service::get_preset_tags(
183 metadata_preset preset) {
184 using namespace kcenon::pacs::core::tags;
185
186 std::unordered_set<std::string> tags;
187
188 switch (preset) {
190 // Rows, Columns, BitsAllocated, BitsStored, HighBit,
191 // PixelRepresentation, PhotometricInterpretation, SamplesPerPixel
192 tags.insert(tag_to_hex(rows));
193 tags.insert(tag_to_hex(columns));
194 tags.insert(tag_to_hex(bits_allocated));
195 tags.insert(tag_to_hex(bits_stored));
196 tags.insert(tag_to_hex(high_bit));
197 tags.insert(tag_to_hex(pixel_representation));
198 tags.insert(tag_to_hex(photometric_interpretation));
199 tags.insert(tag_to_hex(samples_per_pixel));
200 break;
201
203 // WindowCenter, WindowWidth, RescaleSlope, RescaleIntercept
204 tags.insert(tag_to_hex(window_center));
205 tags.insert(tag_to_hex(window_width));
206 tags.insert(tag_to_hex(rescale_slope));
207 tags.insert(tag_to_hex(rescale_intercept));
208 // Window Explanation (0028,1055)
209 tags.insert("00281055");
210 // VOI LUT Sequence (0028,3010)
211 tags.insert("00283010");
212 break;
213
215 // PatientName, PatientID, PatientBirthDate, PatientSex, PatientAge
216 tags.insert(tag_to_hex(patient_name));
217 tags.insert(tag_to_hex(patient_id));
218 tags.insert(tag_to_hex(patient_birth_date));
219 tags.insert(tag_to_hex(patient_sex));
220 tags.insert(tag_to_hex(patient_age));
221 break;
222
224 // KVP (0018,0060), ExposureTime (0018,1150), XRayTubeCurrent
225 // (0018,1151) SliceThickness (0018,0050), SpacingBetweenSlices
226 // (0018,0088)
227 tags.insert("00180060"); // KVP
228 tags.insert("00181150"); // ExposureTime
229 tags.insert("00181151"); // XRayTubeCurrent
230 tags.insert("00180050"); // SliceThickness
231 tags.insert("00180088"); // SpacingBetweenSlices
232 break;
233
235 // ImagePositionPatient, ImageOrientationPatient, SliceLocation,
236 // PixelSpacing
237 tags.insert(tag_to_hex(image_position_patient));
238 tags.insert(tag_to_hex(image_orientation_patient));
239 tags.insert(tag_to_hex(slice_location));
240 tags.insert(tag_to_hex(pixel_spacing));
241 break;
242
244 // NumberOfFrames (0028,0008), FrameIncrementPointer (0028,0009),
245 // FrameTime (0018,1063)
246 tags.insert("00280008"); // NumberOfFrames
247 tags.insert("00280009"); // FrameIncrementPointer
248 tags.insert("00181063"); // FrameTime
249 break;
250 }
251
252 return tags;
253}
254
255// =============================================================================
256// Selective Metadata Retrieval
257// =============================================================================
258
260 std::string_view sop_instance_uid, const metadata_request& request) {
261 if (database_ == nullptr) {
262 return metadata_response::error("Database not configured");
263 }
264
265 // Find instance
266 auto instance = database_->find_instance(sop_instance_uid);
267 if (!instance) {
268 return metadata_response::error("Instance not found");
269 }
270
271 // Check file exists
272 if (!std::filesystem::exists(instance->file_path)) {
273 return metadata_response::error("DICOM file not found");
274 }
275
276 // Build set of requested tags
277 std::unordered_set<std::string> requested_tags;
278
279 // Add preset tags if specified
280 if (request.preset.has_value()) {
281 auto preset_tags = get_preset_tags(request.preset.value());
282 requested_tags.insert(preset_tags.begin(), preset_tags.end());
283 }
284
285 // Add explicitly requested tags
286 for (const auto& tag : request.tags) {
287 requested_tags.insert(tag);
288 }
289
290 // If no tags specified, return error
291 if (requested_tags.empty()) {
293 "No tags specified: provide 'tags' or 'preset' parameter");
294 }
295
296 // Read and filter DICOM tags
297 auto tag_values =
298 read_dicom_tags(instance->file_path, requested_tags, request.include_private);
299
300 return metadata_response::ok(std::move(tag_values));
301}
302
303std::unordered_map<std::string, std::string> metadata_service::read_dicom_tags(
304 std::string_view file_path,
305 const std::unordered_set<std::string>& requested_tags,
306 bool include_private) {
307 std::unordered_map<std::string, std::string> result;
308
309 // Open DICOM file
310 auto file_result = kcenon::pacs::core::dicom_file::open(std::filesystem::path(file_path));
311 if (file_result.is_err()) {
312 return result; // Return empty map on failure
313 }
314
315 const auto& dataset = file_result.value().dataset();
316
317 // Convert requested tags to dicom_tag and extract values
318 for (const auto& tag_hex : requested_tags) {
319 auto tag_opt = hex_to_tag(tag_hex);
320 if (!tag_opt.has_value()) {
321 continue;
322 }
323
324 auto tag = tag_opt.value();
325
326 // Skip private tags if not requested
327 if (tag.is_private() && !include_private) {
328 continue;
329 }
330
331 // Get element value
332 const auto* elem = dataset.get(tag);
333 if (elem != nullptr) {
334 auto str_result = elem->as_string();
335 if (str_result.is_ok()) {
336 result[tag_hex] = str_result.value();
337 }
338 }
339 }
340
341 return result;
342}
343
344// =============================================================================
345// Series Navigation
346// =============================================================================
347
348std::optional<std::string> metadata_service::get_series_uid(
349 std::string_view sop_instance_uid) {
350 if (database_ == nullptr) {
351 return std::nullopt;
352 }
353
354 auto instance = database_->find_instance(sop_instance_uid);
355 if (!instance) {
356 return std::nullopt;
357 }
358
359 // Get series from series_pk
360 auto series = database_->find_series_by_pk(instance->series_pk);
361 if (!series) {
362 return std::nullopt;
363 }
364
365 return series->series_uid;
366}
367
369 std::string_view series_uid, sort_order order, bool ascending) {
370 if (database_ == nullptr) {
371 return sorted_instances_response::error("Database not configured");
372 }
373
374 // Get all instances in the series
375 auto instances_result = database_->list_instances(series_uid);
376 if (instances_result.is_err()) {
377 return sorted_instances_response::error("Failed to list instances");
378 }
379
380 auto& instances = instances_result.value();
381 if (instances.empty()) {
382 return sorted_instances_response::error("Series not found or empty");
383 }
384
385 // Build sorted_instance list with additional DICOM metadata
386 std::vector<sorted_instance> sorted;
387 sorted.reserve(instances.size());
388
389 for (const auto& inst : instances) {
391 si.sop_instance_uid = inst.sop_uid;
392 si.instance_number = inst.instance_number;
393
394 // Read additional positioning data from DICOM file if exists
395 if (std::filesystem::exists(inst.file_path)) {
396 auto file_result =
397 kcenon::pacs::core::dicom_file::open(std::filesystem::path(inst.file_path));
398 if (file_result.is_ok()) {
399 const auto& ds = file_result.value().dataset();
400
401 // Slice location
402 auto slice_str = ds.get_string(kcenon::pacs::core::tags::slice_location);
403 if (!slice_str.empty()) {
404 try {
405 si.slice_location = std::stod(slice_str);
406 } catch (...) {
407 }
408 }
409
410 // Image position patient
411 auto pos_str =
413 if (!pos_str.empty()) {
414 si.image_position_patient = parse_numeric_list(pos_str);
415 }
416
417 // Acquisition time
418 auto time_str = ds.get_string(kcenon::pacs::core::tags::acquisition_time);
419 if (!time_str.empty()) {
420 si.acquisition_time = time_str;
421 }
422 }
423 }
424
425 sorted.push_back(std::move(si));
426 }
427
428 // Sort based on order
429 auto compare = [order, ascending](const sorted_instance& a,
430 const sorted_instance& b) {
431 bool result = false;
432
433 switch (order) {
435 // Sort by slice location or Z position
436 double a_pos = 0.0;
437 double b_pos = 0.0;
438
439 if (a.slice_location.has_value()) {
440 a_pos = a.slice_location.value();
441 } else if (a.image_position_patient.has_value() &&
442 a.image_position_patient->size() >= 3) {
443 a_pos = (*a.image_position_patient)[2]; // Z position
444 }
445
446 if (b.slice_location.has_value()) {
447 b_pos = b.slice_location.value();
448 } else if (b.image_position_patient.has_value() &&
449 b.image_position_patient->size() >= 3) {
450 b_pos = (*b.image_position_patient)[2];
451 }
452
453 result = a_pos < b_pos;
454 break;
455 }
456
458 int a_num = a.instance_number.value_or(0);
459 int b_num = b.instance_number.value_or(0);
460 result = a_num < b_num;
461 break;
462 }
463
465 std::string a_time = a.acquisition_time.value_or("");
466 std::string b_time = b.acquisition_time.value_or("");
467 result = a_time < b_time;
468 break;
469 }
470 }
471
472 return ascending ? result : !result;
473 };
474
475 std::sort(sorted.begin(), sorted.end(), compare);
476
477 return sorted_instances_response::ok(std::move(sorted), instances.size());
478}
479
481 std::string_view sop_instance_uid) {
482 if (database_ == nullptr) {
483 return navigation_info::error("Database not configured");
484 }
485
486 // Get series UID for this instance
487 auto series_uid_opt = get_series_uid(sop_instance_uid);
488 if (!series_uid_opt.has_value()) {
489 return navigation_info::error("Instance not found");
490 }
491
492 // Get sorted instances
493 auto sorted_result =
494 get_sorted_instances(series_uid_opt.value(), sort_order::position, true);
495 if (!sorted_result.success) {
496 return navigation_info::error(sorted_result.error_message);
497 }
498
499 const auto& instances = sorted_result.instances;
500 if (instances.empty()) {
501 return navigation_info::error("Series is empty");
502 }
503
504 // Find current instance index
505 size_t current_index = 0;
506 bool found = false;
507 for (size_t i = 0; i < instances.size(); ++i) {
508 if (instances[i].sop_instance_uid == sop_instance_uid) {
509 current_index = i;
510 found = true;
511 break;
512 }
513 }
514
515 if (!found) {
516 return navigation_info::error("Instance not found in series");
517 }
518
519 // Build navigation info
521 nav.index = current_index;
522 nav.total = instances.size();
523 nav.first = instances.front().sop_instance_uid;
524 nav.last = instances.back().sop_instance_uid;
525
526 if (current_index > 0) {
527 nav.previous = instances[current_index - 1].sop_instance_uid;
528 }
529
530 if (current_index < instances.size() - 1) {
531 nav.next = instances[current_index + 1].sop_instance_uid;
532 }
533
534 return nav;
535}
536
537// =============================================================================
538// Window/Level Presets
539// =============================================================================
540
541std::vector<window_level_preset> metadata_service::get_window_level_presets(
542 std::string_view modality) {
543 std::vector<window_level_preset> presets;
544
545 if (modality == "CT") {
546 presets.push_back({"Lung", -600, 1500});
547 presets.push_back({"Bone", 300, 1500});
548 presets.push_back({"Soft Tissue", 40, 400});
549 presets.push_back({"Brain", 40, 80});
550 presets.push_back({"Liver", 60, 150});
551 presets.push_back({"Mediastinum", 50, 350});
552 } else if (modality == "MR") {
553 presets.push_back({"T1 Brain", 600, 1200});
554 presets.push_back({"T2 Brain", 700, 1400});
555 presets.push_back({"Spine", 500, 1000});
556 } else if (modality == "CR" || modality == "DX") {
557 presets.push_back({"Default", 2048, 4096});
558 presets.push_back({"Bone", 1500, 3000});
559 presets.push_back({"Soft Tissue", 1800, 3600});
560 } else if (modality == "US") {
561 presets.push_back({"Default", 128, 256});
562 } else {
563 // Generic presets
564 presets.push_back({"Default", 128, 256});
565 }
566
567 return presets;
568}
569
570voi_lut_info metadata_service::get_voi_lut(std::string_view sop_instance_uid) {
571 if (database_ == nullptr) {
572 return voi_lut_info::error("Database not configured");
573 }
574
575 auto instance = database_->find_instance(sop_instance_uid);
576 if (!instance) {
577 return voi_lut_info::error("Instance not found");
578 }
579
580 if (!std::filesystem::exists(instance->file_path)) {
581 return voi_lut_info::error("DICOM file not found");
582 }
583
584 auto file_result =
585 kcenon::pacs::core::dicom_file::open(std::filesystem::path(instance->file_path));
586 if (file_result.is_err()) {
587 return voi_lut_info::error("Failed to open DICOM file");
588 }
589
590 const auto& ds = file_result.value().dataset();
591
593
594 // Window Center
595 auto wc_str = ds.get_string(kcenon::pacs::core::tags::window_center);
596 if (!wc_str.empty()) {
597 info.window_center = parse_numeric_list(wc_str);
598 }
599
600 // Window Width
601 auto ww_str = ds.get_string(kcenon::pacs::core::tags::window_width);
602 if (!ww_str.empty()) {
603 info.window_width = parse_numeric_list(ww_str);
604 }
605
606 // Window Explanation (0028,1055)
607 const auto* we_elem =
608 ds.get(kcenon::pacs::core::dicom_tag{0x0028, 0x1055});
609 if (we_elem != nullptr) {
610 auto we_result = we_elem->as_string();
611 if (we_result.is_ok()) {
612 info.window_explanations = parse_string_list(we_result.value());
613 }
614 }
615
616 // Rescale Slope
617 auto rs_str = ds.get_string(kcenon::pacs::core::tags::rescale_slope);
618 if (!rs_str.empty()) {
619 try {
620 info.rescale_slope = std::stod(rs_str);
621 } catch (...) {
622 }
623 }
624
625 // Rescale Intercept
626 auto ri_str = ds.get_string(kcenon::pacs::core::tags::rescale_intercept);
627 if (!ri_str.empty()) {
628 try {
629 info.rescale_intercept = std::stod(ri_str);
630 } catch (...) {
631 }
632 }
633
634 return info;
635}
636
637// =============================================================================
638// Multi-frame Support
639// =============================================================================
640
641frame_info metadata_service::get_frame_info(std::string_view sop_instance_uid) {
642 if (database_ == nullptr) {
643 return frame_info::error("Database not configured");
644 }
645
646 auto instance = database_->find_instance(sop_instance_uid);
647 if (!instance) {
648 return frame_info::error("Instance not found");
649 }
650
651 if (!std::filesystem::exists(instance->file_path)) {
652 return frame_info::error("DICOM file not found");
653 }
654
655 auto file_result =
656 kcenon::pacs::core::dicom_file::open(std::filesystem::path(instance->file_path));
657 if (file_result.is_err()) {
658 return frame_info::error("Failed to open DICOM file");
659 }
660
661 const auto& ds = file_result.value().dataset();
662
663 frame_info info = frame_info::ok();
664
665 // Number of Frames (0028,0008)
666 const auto* nf_elem = ds.get(kcenon::pacs::core::dicom_tag{0x0028, 0x0008});
667 if (nf_elem != nullptr) {
668 auto nf_result = nf_elem->as_string();
669 if (nf_result.is_ok()) {
670 try {
671 info.total_frames = static_cast<uint32_t>(std::stoul(nf_result.value()));
672 } catch (...) {
673 info.total_frames = 1;
674 }
675 }
676 }
677
678 // Frame Time (0018,1063) - in milliseconds
679 const auto* ft_elem = ds.get(kcenon::pacs::core::dicom_tag{0x0018, 0x1063});
680 if (ft_elem != nullptr) {
681 auto ft_result = ft_elem->as_string();
682 if (ft_result.is_ok()) {
683 try {
684 info.frame_time = std::stod(ft_result.value());
685 if (info.frame_time.has_value() && info.frame_time.value() > 0) {
686 info.frame_rate = 1000.0 / info.frame_time.value();
687 }
688 } catch (...) {
689 }
690 }
691 }
692
693 // Rows
694 auto rows_opt = ds.get_numeric<uint16_t>(kcenon::pacs::core::tags::rows);
695 if (rows_opt.has_value()) {
696 info.rows = rows_opt.value();
697 }
698
699 // Columns
700 auto cols_opt = ds.get_numeric<uint16_t>(kcenon::pacs::core::tags::columns);
701 if (cols_opt.has_value()) {
702 info.columns = cols_opt.value();
703 }
704
705 return info;
706}
707
708} // namespace kcenon::pacs::web
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.
constexpr auto is_private() const noexcept -> bool
Check if this is a private tag.
Definition dicom_tag.h:119
constexpr auto group() const noexcept -> uint16_t
Get the group number.
Definition dicom_tag.h:90
constexpr auto element() const noexcept -> uint16_t
Get the element number.
Definition dicom_tag.h:98
sorted_instances_response get_sorted_instances(std::string_view series_uid, sort_order order=sort_order::position, bool ascending=true)
Get sorted instances for a series.
static std::vector< window_level_preset > get_window_level_presets(std::string_view modality)
Get window/level presets for a modality.
std::shared_ptr< storage::index_database > database_
Database for instance lookups.
static std::unordered_set< std::string > get_preset_tags(metadata_preset preset)
Get tags for a specific preset.
metadata_service(std::shared_ptr< storage::index_database > database)
Construct metadata service with database.
std::optional< std::string > get_series_uid(std::string_view sop_instance_uid)
Get series UID for an instance.
std::unordered_map< std::string, std::string > read_dicom_tags(std::string_view file_path, const std::unordered_set< std::string > &requested_tags, bool include_private)
Read DICOM dataset from file.
frame_info get_frame_info(std::string_view sop_instance_uid)
Get frame information for an instance.
voi_lut_info get_voi_lut(std::string_view sop_instance_uid)
Get VOI LUT info from an instance.
navigation_info get_navigation(std::string_view sop_instance_uid)
Get navigation info for an instance.
metadata_response get_metadata(std::string_view sop_instance_uid, const metadata_request &request)
Get selective metadata for an instance.
DICOM Dataset - ordered collection of Data Elements.
DICOM Data Element representation (Tag, VR, Value)
DICOM Part 10 file handling for reading/writing DICOM files.
DICOM Tag representation (Group, Element pairs)
Compile-time constants for commonly used DICOM tags.
PACS index database for metadata storage and retrieval.
Selective DICOM metadata retrieval and series navigation service.
constexpr dicom_tag window_width
Window Width.
constexpr dicom_tag window_center
Window Center.
constexpr dicom_tag rows
Rows.
constexpr dicom_tag rescale_intercept
Rescale Intercept.
constexpr dicom_tag columns
Columns.
constexpr dicom_tag slice_location
Slice Location.
constexpr dicom_tag rescale_slope
Rescale Slope.
constexpr dicom_tag image_position_patient
Image Position (Patient)
constexpr dicom_tag acquisition_time
Acquisition Time.
sort_order
Sort order for series instances.
@ instance_number
Sort by InstanceNumber.
@ position
Sort by ImagePositionPatient/SliceLocation.
@ acquisition_time
Sort by AcquisitionTime.
std::string_view preset_to_string(metadata_preset preset)
Convert preset enum to string.
std::optional< sort_order > sort_order_from_string(std::string_view str)
Parse sort order from string.
std::optional< metadata_preset > preset_from_string(std::string_view str)
Parse preset from string.
metadata_preset
Metadata preset types for common use cases.
@ image_display
Rows, Columns, Bits, PhotometricInterpretation.
@ patient_info
Patient demographics.
@ acquisition
KVP, ExposureTime, SliceThickness.
@ positioning
ImagePosition, ImageOrientation, PixelSpacing.
@ multiframe
NumberOfFrames, FrameTime.
@ window_level
WindowCenter, WindowWidth, Rescale values.
std::string_view sort_order_to_string(sort_order order)
Convert sort order enum to string.
Multi-frame information.
static frame_info ok()
Create a success result.
static frame_info error(std::string message)
Create an error result.
Parameters for selective metadata retrieval.
bool include_private
Include private tags in response.
std::vector< std::string > tags
Specific tags to retrieve (hex format: "00280010")
std::optional< metadata_preset > preset
Preset to apply.
Response for selective metadata retrieval.
static metadata_response error(std::string message)
Create an error result.
static metadata_response ok(std::unordered_map< std::string, std::string > tag_map)
Create a success result.
Navigation info for an instance.
std::string first
First instance UID.
static navigation_info error(std::string message)
Create an error result.
std::string next
Next instance UID (empty if last)
std::string last
Last instance UID.
size_t total
Total instances in series.
size_t index
Current index (0-based)
std::string previous
Previous instance UID (empty if first)
static navigation_info ok()
Create a success result.
Instance info for series navigation.
std::optional< std::vector< double > > image_position_patient
Image Position Patient (if available)
std::optional< double > slice_location
Slice location (if available)
std::string sop_instance_uid
SOP Instance UID.
std::optional< std::string > acquisition_time
Acquisition time (if available)
std::optional< int > instance_number
Instance number (if available)
Response for sorted instances query.
static sorted_instances_response ok(std::vector< sorted_instance > inst, size_t count)
Create a success result.
static sorted_instances_response error(std::string message)
Create an error result.
VOI LUT information from DICOM.
static voi_lut_info error(std::string message)
Create an error result.
static voi_lut_info ok()
Create a success result.