PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
metadata_endpoints.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
14// IMPORTANT: Include Crow FIRST before any PACS headers to avoid forward
15// declaration conflicts
16#include "crow.h"
17
18// Workaround for Windows: DELETE is defined as a macro in <winnt.h>
19// which conflicts with crow::HTTPMethod::DELETE
20#ifdef DELETE
21#undef DELETE
22#endif
23
30
31#include <memory>
32#include <sstream>
33
35
36namespace {
37
41void add_cors_headers(crow::response& res, const rest_server_context& ctx) {
42 if (ctx.config != nullptr && !ctx.config->cors_allowed_origins.empty()) {
43 res.add_header("Access-Control-Allow-Origin",
44 ctx.config->cors_allowed_origins);
45 }
46}
47
51std::string escape_json(const std::string& str) {
52 std::ostringstream oss;
53 for (char c : str) {
54 switch (c) {
55 case '"':
56 oss << "\\\"";
57 break;
58 case '\\':
59 oss << "\\\\";
60 break;
61 case '\b':
62 oss << "\\b";
63 break;
64 case '\f':
65 oss << "\\f";
66 break;
67 case '\n':
68 oss << "\\n";
69 break;
70 case '\r':
71 oss << "\\r";
72 break;
73 case '\t':
74 oss << "\\t";
75 break;
76 default:
77 oss << c;
78 break;
79 }
80 }
81 return oss.str();
82}
83
87metadata_request parse_metadata_request(const crow::request& req) {
88 metadata_request request;
89
90 // Parse tags parameter (comma-separated)
91 auto tags_param = req.url_params.get("tags");
92 if (tags_param != nullptr) {
93 std::string tags_str = tags_param;
94 std::istringstream iss(tags_str);
95 std::string tag;
96 while (std::getline(iss, tag, ',')) {
97 // Trim whitespace
98 size_t start = tag.find_first_not_of(" \t");
99 size_t end = tag.find_last_not_of(" \t");
100 if (start != std::string::npos) {
101 request.tags.push_back(tag.substr(start, end - start + 1));
102 }
103 }
104 }
105
106 // Parse preset parameter
107 auto preset_param = req.url_params.get("preset");
108 if (preset_param != nullptr) {
109 request.preset = preset_from_string(preset_param);
110 }
111
112 // Parse include_private parameter
113 auto private_param = req.url_params.get("include_private");
114 if (private_param != nullptr) {
115 std::string val = private_param;
116 request.include_private = (val == "true" || val == "1");
117 }
118
119 return request;
120}
121
125std::string metadata_response_to_json(const metadata_response& resp) {
126 std::ostringstream oss;
127 oss << R"({"tags":{)";
128
129 bool first = true;
130 for (const auto& [tag, value] : resp.tags) {
131 if (!first) {
132 oss << ",";
133 }
134 first = false;
135
136 // Check if value looks numeric
137 bool is_numeric = !value.empty();
138 for (char c : value) {
139 if (!std::isdigit(c) && c != '.' && c != '-' && c != '+' &&
140 c != 'e' && c != 'E' && c != '\\') {
141 is_numeric = false;
142 break;
143 }
144 }
145
146 // Multi-valued numeric fields contain backslash separators
147 if (is_numeric && value.find('\\') != std::string::npos) {
148 // Output as array
149 oss << "\"" << tag << "\":[";
150 std::istringstream vals(value);
151 std::string v;
152 bool first_val = true;
153 while (std::getline(vals, v, '\\')) {
154 if (!first_val) {
155 oss << ",";
156 }
157 first_val = false;
158 oss << v;
159 }
160 oss << "]";
161 } else if (is_numeric && value.find('\\') == std::string::npos) {
162 oss << "\"" << tag << "\":" << value;
163 } else {
164 oss << "\"" << tag << "\":\"" << escape_json(value) << "\"";
165 }
166 }
167
168 oss << "}}";
169 return oss.str();
170}
171
175std::string sorted_instances_to_json(const sorted_instances_response& resp) {
176 std::ostringstream oss;
177 oss << R"({"instances":[)";
178
179 bool first = true;
180 for (const auto& inst : resp.instances) {
181 if (!first) {
182 oss << ",";
183 }
184 first = false;
185
186 oss << "{\"sop_instance_uid\":\"" << inst.sop_instance_uid << "\"";
187
188 if (inst.instance_number.has_value()) {
189 oss << ",\"instance_number\":" << inst.instance_number.value();
190 }
191
192 if (inst.slice_location.has_value()) {
193 oss << ",\"slice_location\":" << inst.slice_location.value();
194 }
195
196 oss << "}";
197 }
198
199 oss << "],\"total\":" << resp.total << "}";
200 return oss.str();
201}
202
206std::string navigation_info_to_json(const navigation_info& nav) {
207 std::ostringstream oss;
208 oss << "{";
209
210 if (!nav.previous.empty()) {
211 oss << "\"previous\":\"" << nav.previous << "\",";
212 }
213
214 if (!nav.next.empty()) {
215 oss << "\"next\":\"" << nav.next << "\",";
216 }
217
218 oss << "\"index\":" << nav.index << ",\"total\":" << nav.total
219 << ",\"first\":\"" << nav.first << "\",\"last\":\"" << nav.last << "\"}";
220
221 return oss.str();
222}
223
227std::string presets_to_json(const std::vector<window_level_preset>& presets) {
228 std::ostringstream oss;
229 oss << R"({"presets":[)";
230
231 bool first = true;
232 for (const auto& p : presets) {
233 if (!first) {
234 oss << ",";
235 }
236 first = false;
237
238 oss << "{\"name\":\"" << escape_json(p.name) << "\",\"center\":"
239 << p.center << ",\"width\":" << p.width << "}";
240 }
241
242 oss << "]}";
243 return oss.str();
244}
245
249std::string voi_lut_to_json(const voi_lut_info& info) {
250 std::ostringstream oss;
251 oss << "{\"window_center\":[";
252
253 for (size_t i = 0; i < info.window_center.size(); ++i) {
254 if (i > 0) {
255 oss << ",";
256 }
257 oss << info.window_center[i];
258 }
259
260 oss << "],\"window_width\":[";
261 for (size_t i = 0; i < info.window_width.size(); ++i) {
262 if (i > 0) {
263 oss << ",";
264 }
265 oss << info.window_width[i];
266 }
267
268 oss << "],\"window_explanations\":[";
269 for (size_t i = 0; i < info.window_explanations.size(); ++i) {
270 if (i > 0) {
271 oss << ",";
272 }
273 oss << "\"" << escape_json(info.window_explanations[i]) << "\"";
274 }
275
276 oss << "],\"rescale_slope\":" << info.rescale_slope
277 << ",\"rescale_intercept\":" << info.rescale_intercept << "}";
278
279 return oss.str();
280}
281
285std::string frame_info_to_json(const frame_info& info) {
286 std::ostringstream oss;
287 oss << "{\"total_frames\":" << info.total_frames;
288
289 if (info.frame_time.has_value()) {
290 oss << ",\"frame_time\":" << info.frame_time.value();
291 }
292
293 if (info.frame_rate.has_value()) {
294 oss << ",\"frame_rate\":" << info.frame_rate.value();
295 }
296
297 oss << ",\"rows\":" << info.rows << ",\"columns\":" << info.columns << "}";
298
299 return oss.str();
300}
301
303std::shared_ptr<metadata_service> g_metadata_service;
304
305} // namespace
306
307// Internal implementation function called from rest_server.cpp
308void register_metadata_endpoints_impl(crow::SimpleApp& app,
309 std::shared_ptr<rest_server_context> ctx) {
310 // Initialize metadata service if database is available
311 if (ctx->database != nullptr && g_metadata_service == nullptr) {
312 g_metadata_service = std::make_shared<metadata_service>(ctx->database);
313 }
314
315 // =========================================================================
316 // Selective Metadata
317 // =========================================================================
318
319 // GET /api/v1/instances/{sopInstanceUid}/metadata
320 CROW_ROUTE(app, "/api/v1/instances/<string>/metadata")
321 .methods(crow::HTTPMethod::GET)(
322 [ctx](const crow::request& req, const std::string& sop_uid) {
323 crow::response res;
324 res.add_header("Content-Type", "application/json");
325 add_cors_headers(res, *ctx);
326
327 if (g_metadata_service == nullptr) {
328 res.code = 503;
329 res.body = make_error_json("SERVICE_UNAVAILABLE",
330 "Metadata service not configured");
331 return res;
332 }
333
334 auto request = parse_metadata_request(req);
335 auto result = g_metadata_service->get_metadata(sop_uid, request);
336
337 if (!result.success) {
338 res.code = 404;
339 res.body = make_error_json("NOT_FOUND", result.error_message);
340 return res;
341 }
342
343 res.code = 200;
344 res.body = metadata_response_to_json(result);
345 return res;
346 });
347
348 // =========================================================================
349 // Series Navigation
350 // =========================================================================
351
352 // GET /api/v1/series/{seriesUid}/instances/sorted
353 CROW_ROUTE(app, "/api/v1/series/<string>/instances/sorted")
354 .methods(crow::HTTPMethod::GET)(
355 [ctx](const crow::request& req, const std::string& series_uid) {
356 crow::response res;
357 res.add_header("Content-Type", "application/json");
358 add_cors_headers(res, *ctx);
359
360 if (g_metadata_service == nullptr) {
361 res.code = 503;
362 res.body = make_error_json("SERVICE_UNAVAILABLE",
363 "Metadata service not configured");
364 return res;
365 }
366
367 // Parse sort parameters
369 auto sort_param = req.url_params.get("sort_by");
370 if (sort_param != nullptr) {
371 auto parsed = sort_order_from_string(sort_param);
372 if (parsed.has_value()) {
373 order = parsed.value();
374 }
375 }
376
377 bool ascending = true;
378 auto dir_param = req.url_params.get("direction");
379 if (dir_param != nullptr) {
380 std::string dir = dir_param;
381 ascending = (dir != "desc");
382 }
383
384 auto result = g_metadata_service->get_sorted_instances(
385 series_uid, order, ascending);
386
387 if (!result.success) {
388 res.code = 404;
389 res.body = make_error_json("NOT_FOUND", result.error_message);
390 return res;
391 }
392
393 res.code = 200;
394 res.body = sorted_instances_to_json(result);
395 return res;
396 });
397
398 // GET /api/v1/instances/{sopInstanceUid}/navigation
399 CROW_ROUTE(app, "/api/v1/instances/<string>/navigation")
400 .methods(crow::HTTPMethod::GET)(
401 [ctx](const crow::request& /*req*/, const std::string& sop_uid) {
402 crow::response res;
403 res.add_header("Content-Type", "application/json");
404 add_cors_headers(res, *ctx);
405
406 if (g_metadata_service == nullptr) {
407 res.code = 503;
408 res.body = make_error_json("SERVICE_UNAVAILABLE",
409 "Metadata service not configured");
410 return res;
411 }
412
413 auto result = g_metadata_service->get_navigation(sop_uid);
414
415 if (!result.success) {
416 res.code = 404;
417 res.body = make_error_json("NOT_FOUND", result.error_message);
418 return res;
419 }
420
421 res.code = 200;
422 res.body = navigation_info_to_json(result);
423 return res;
424 });
425
426 // =========================================================================
427 // Window/Level Presets
428 // =========================================================================
429
430 // GET /api/v1/presets/window-level
431 CROW_ROUTE(app, "/api/v1/presets/window-level")
432 .methods(crow::HTTPMethod::GET)([ctx](const crow::request& req) {
433 crow::response res;
434 res.add_header("Content-Type", "application/json");
435 add_cors_headers(res, *ctx);
436
437 std::string modality = "CT"; // Default modality
438 auto modality_param = req.url_params.get("modality");
439 if (modality_param != nullptr) {
440 modality = modality_param;
441 }
442
443 auto presets = metadata_service::get_window_level_presets(modality);
444
445 res.code = 200;
446 res.body = presets_to_json(presets);
447 return res;
448 });
449
450 // GET /api/v1/instances/{sopInstanceUid}/voi-lut
451 CROW_ROUTE(app, "/api/v1/instances/<string>/voi-lut")
452 .methods(crow::HTTPMethod::GET)(
453 [ctx](const crow::request& /*req*/, const std::string& sop_uid) {
454 crow::response res;
455 res.add_header("Content-Type", "application/json");
456 add_cors_headers(res, *ctx);
457
458 if (g_metadata_service == nullptr) {
459 res.code = 503;
460 res.body = make_error_json("SERVICE_UNAVAILABLE",
461 "Metadata service not configured");
462 return res;
463 }
464
465 auto result = g_metadata_service->get_voi_lut(sop_uid);
466
467 if (!result.success) {
468 res.code = 404;
469 res.body = make_error_json("NOT_FOUND", result.error_message);
470 return res;
471 }
472
473 res.code = 200;
474 res.body = voi_lut_to_json(result);
475 return res;
476 });
477
478 // =========================================================================
479 // Multi-frame Support
480 // =========================================================================
481
482 // GET /api/v1/instances/{sopInstanceUid}/frame-info
483 CROW_ROUTE(app, "/api/v1/instances/<string>/frame-info")
484 .methods(crow::HTTPMethod::GET)(
485 [ctx](const crow::request& /*req*/, const std::string& sop_uid) {
486 crow::response res;
487 res.add_header("Content-Type", "application/json");
488 add_cors_headers(res, *ctx);
489
490 if (g_metadata_service == nullptr) {
491 res.code = 503;
492 res.body = make_error_json("SERVICE_UNAVAILABLE",
493 "Metadata service not configured");
494 return res;
495 }
496
497 auto result = g_metadata_service->get_frame_info(sop_uid);
498
499 if (!result.success) {
500 res.code = 404;
501 res.body = make_error_json("NOT_FOUND", result.error_message);
502 return res;
503 }
504
505 res.code = 200;
506 res.body = frame_info_to_json(result);
507 return res;
508 });
509}
510
511} // namespace kcenon::pacs::web::endpoints
static std::vector< window_level_preset > get_window_level_presets(std::string_view modality)
Get window/level presets for a modality.
PACS index database for metadata storage and retrieval.
Metadata REST API endpoints.
Selective DICOM metadata retrieval and series navigation service.
void register_metadata_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
sort_order
Sort order for series instances.
@ position
Sort by ImagePositionPatient/SliceLocation.
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
Definition rest_types.h:79
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.
Configuration for REST API server.
Common types and utilities for REST API.
System API endpoints for REST server.