PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
key_image_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
16// IMPORTANT: Include Crow FIRST before any PACS headers to avoid forward
17// declaration conflicts
18#include "crow.h"
19
20// Workaround for Windows: DELETE is defined as a macro in <winnt.h>
21// which conflicts with crow::HTTPMethod::DELETE
22#ifdef DELETE
23#undef DELETE
24#endif
25
33
34#include <chrono>
35#include <iomanip>
36#include <random>
37#include <sstream>
38
40
41namespace {
42
46void add_cors_headers(crow::response &res, const rest_server_context &ctx) {
47 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
48 res.add_header("Access-Control-Allow-Origin",
49 ctx.config->cors_allowed_origins);
50 }
51}
52
56std::string generate_uuid() {
57 static std::random_device rd;
58 static std::mt19937 gen(rd());
59 static std::uniform_int_distribution<> dis(0, 15);
60 static const char *hex = "0123456789abcdef";
61
62 std::string uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
63 for (char &c : uuid) {
64 if (c == 'x') {
65 c = hex[dis(gen)];
66 } else if (c == 'y') {
67 c = hex[(dis(gen) & 0x3) | 0x8];
68 }
69 }
70 return uuid;
71}
72
76std::string format_timestamp(
77 const std::chrono::system_clock::time_point &tp) {
78 auto time = std::chrono::system_clock::to_time_t(tp);
79 std::ostringstream oss;
80 oss << std::put_time(std::gmtime(&time), "%Y-%m-%dT%H:%M:%SZ");
81 return oss.str();
82}
83
87std::string key_image_to_json(const storage::key_image_record &ki) {
88 std::ostringstream oss;
89 oss << R"({"key_image_id":")" << json_escape(ki.key_image_id)
90 << R"(","study_uid":")" << json_escape(ki.study_uid)
91 << R"(","sop_instance_uid":")" << json_escape(ki.sop_instance_uid)
92 << R"(","frame_number":)";
93 if (ki.frame_number.has_value()) {
94 oss << ki.frame_number.value();
95 } else {
96 oss << "null";
97 }
98 oss << R"(,"user_id":")" << json_escape(ki.user_id)
99 << R"(","reason":")" << json_escape(ki.reason)
100 << R"(","document_title":")" << json_escape(ki.document_title)
101 << R"(","created_at":")" << format_timestamp(ki.created_at) << R"("})";
102 return oss.str();
103}
104
108std::string key_images_to_json(
109 const std::vector<storage::key_image_record> &key_images) {
110 std::ostringstream oss;
111 oss << R"({"data":[)";
112 for (size_t i = 0; i < key_images.size(); ++i) {
113 if (i > 0) {
114 oss << ",";
115 }
116 oss << key_image_to_json(key_images[i]);
117 }
118 oss << "]}";
119 return oss.str();
120}
121
125std::string parse_json_string(const std::string &json, const std::string &key) {
126 std::string search = "\"" + key + "\":\"";
127 auto pos = json.find(search);
128 if (pos == std::string::npos) {
129 return "";
130 }
131 pos += search.length();
132 auto end = json.find("\"", pos);
133 if (end == std::string::npos) {
134 return "";
135 }
136 return json.substr(pos, end - pos);
137}
138
142std::optional<int> parse_json_int(const std::string &json,
143 const std::string &key) {
144 std::string search = "\"" + key + "\":";
145 auto pos = json.find(search);
146 if (pos == std::string::npos) {
147 return std::nullopt;
148 }
149 pos += search.length();
150 while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) {
151 ++pos;
152 }
153 if (pos >= json.size()) {
154 return std::nullopt;
155 }
156 if (json.substr(pos, 4) == "null") {
157 return std::nullopt;
158 }
159 try {
160 return std::stoi(json.substr(pos));
161 } catch (...) {
162 return std::nullopt;
163 }
164}
165
166} // namespace
167
168// Internal implementation function called from rest_server.cpp
169void register_key_image_endpoints_impl(crow::SimpleApp &app,
170 std::shared_ptr<rest_server_context> ctx) {
171 // POST /api/v1/studies/<studyUid>/key-images - Create key image
172 CROW_ROUTE(app, "/api/v1/studies/<string>/key-images")
173 .methods(crow::HTTPMethod::POST)(
174 [ctx](const crow::request &req, const std::string &study_uid) {
175 crow::response res;
176 res.add_header("Content-Type", "application/json");
177 add_cors_headers(res, *ctx);
178
179 if (!ctx->database) {
180 res.code = 503;
181 res.body =
182 make_error_json("DATABASE_UNAVAILABLE", "Database not configured");
183 return res;
184 }
185
186 std::string body = req.body;
187 if (body.empty()) {
188 res.code = 400;
189 res.body = make_error_json("INVALID_REQUEST", "Request body is empty");
190 return res;
191 }
192
194 ki.key_image_id = generate_uuid();
195 ki.study_uid = study_uid;
196 ki.sop_instance_uid = parse_json_string(body, "sop_instance_uid");
197 ki.frame_number = parse_json_int(body, "frame_number");
198 ki.user_id = parse_json_string(body, "user_id");
199 ki.reason = parse_json_string(body, "reason");
200 ki.document_title = parse_json_string(body, "document_title");
201 ki.created_at = std::chrono::system_clock::now();
202
203 if (ki.sop_instance_uid.empty()) {
204 res.code = 400;
205 res.body = make_error_json("MISSING_FIELD", "sop_instance_uid is required");
206 return res;
207 }
208
209#ifdef PACS_WITH_DATABASE_SYSTEM
210 storage::key_image_repository repo(ctx->database->db_adapter());
211#else
212 storage::key_image_repository repo(ctx->database->native_handle());
213#endif
214 auto save_result = repo.save(ki);
215 if (!save_result.is_ok()) {
216 res.code = 500;
217 res.body =
218 make_error_json("SAVE_ERROR", save_result.error().message);
219 return res;
220 }
221
222 res.code = 201;
223 std::ostringstream oss;
224 oss << R"({"key_image_id":")" << json_escape(ki.key_image_id)
225 << R"(","created_at":")" << format_timestamp(ki.created_at)
226 << R"("})";
227 res.body = oss.str();
228 return res;
229 });
230
231 // GET /api/v1/studies/<studyUid>/key-images - List key images for study
232 CROW_ROUTE(app, "/api/v1/studies/<string>/key-images")
233 .methods(crow::HTTPMethod::GET)(
234 [ctx](const crow::request & /*req*/, const std::string &study_uid) {
235 crow::response res;
236 res.add_header("Content-Type", "application/json");
237 add_cors_headers(res, *ctx);
238
239 if (!ctx->database) {
240 res.code = 503;
241 res.body =
242 make_error_json("DATABASE_UNAVAILABLE", "Database not configured");
243 return res;
244 }
245
246#ifdef PACS_WITH_DATABASE_SYSTEM
247 storage::key_image_repository repo(ctx->database->db_adapter());
248 auto key_images_result = repo.find_by_study(study_uid);
249 if (!key_images_result.is_ok()) {
250 res.code = 500;
251 res.body = make_error_json("QUERY_ERROR", key_images_result.error().message);
252 return res;
253 }
254 res.code = 200;
255 res.body = key_images_to_json(key_images_result.value());
256#else
257 storage::key_image_repository repo(ctx->database->native_handle());
258 auto key_images = repo.find_by_study(study_uid);
259 res.code = 200;
260 res.body = key_images_to_json(key_images);
261#endif
262 return res;
263 });
264
265 // DELETE /api/v1/key-images/<keyImageId> - Delete key image
266 CROW_ROUTE(app, "/api/v1/key-images/<string>")
267 .methods(crow::HTTPMethod::DELETE)(
268 [ctx](const crow::request & /*req*/, const std::string &key_image_id) {
269 crow::response res;
270 add_cors_headers(res, *ctx);
271
272 if (!ctx->database) {
273 res.code = 503;
274 res.add_header("Content-Type", "application/json");
275 res.body = make_error_json("DATABASE_UNAVAILABLE",
276 "Database not configured");
277 return res;
278 }
279
280#ifdef PACS_WITH_DATABASE_SYSTEM
281 storage::key_image_repository repo(ctx->database->db_adapter());
282 auto exists_result = repo.exists(key_image_id);
283 if (!exists_result.is_ok()) {
284 res.code = 500;
285 res.add_header("Content-Type", "application/json");
286 res.body = make_error_json("QUERY_ERROR", exists_result.error().message);
287 return res;
288 }
289 if (!exists_result.value()) {
290 res.code = 404;
291 res.add_header("Content-Type", "application/json");
292 res.body = make_error_json("NOT_FOUND", "Key image not found");
293 return res;
294 }
295#else
296 storage::key_image_repository repo(ctx->database->native_handle());
297 if (!repo.exists(key_image_id)) {
298 res.code = 404;
299 res.add_header("Content-Type", "application/json");
300 res.body = make_error_json("NOT_FOUND", "Key image not found");
301 return res;
302 }
303#endif
304
305 auto remove_result = repo.remove(key_image_id);
306 if (!remove_result.is_ok()) {
307 res.code = 500;
308 res.add_header("Content-Type", "application/json");
309 res.body =
310 make_error_json("DELETE_ERROR", remove_result.error().message);
311 return res;
312 }
313
314 res.code = 204;
315 return res;
316 });
317
318 // POST /api/v1/studies/<studyUid>/key-images/export-sr - Export as Key Object Selection SR
319 CROW_ROUTE(app, "/api/v1/studies/<string>/key-images/export-sr")
320 .methods(crow::HTTPMethod::POST)(
321 [ctx](const crow::request & /*req*/, const std::string &study_uid) {
322 crow::response res;
323 add_cors_headers(res, *ctx);
324
325 if (!ctx->database) {
326 res.code = 503;
327 res.add_header("Content-Type", "application/json");
328 res.body = make_error_json("DATABASE_UNAVAILABLE",
329 "Database not configured");
330 return res;
331 }
332
333#ifdef PACS_WITH_DATABASE_SYSTEM
334 storage::key_image_repository repo(ctx->database->db_adapter());
335 auto key_images_result = repo.find_by_study(study_uid);
336 if (!key_images_result.is_ok()) {
337 res.code = 500;
338 res.add_header("Content-Type", "application/json");
339 res.body = make_error_json("QUERY_ERROR", key_images_result.error().message);
340 return res;
341 }
342 const auto& key_images = key_images_result.value();
343#else
344 storage::key_image_repository repo(ctx->database->native_handle());
345 auto key_images = repo.find_by_study(study_uid);
346#endif
347 if (key_images.empty()) {
348 res.code = 404;
349 res.add_header("Content-Type", "application/json");
350 res.body = make_error_json("NO_KEY_IMAGES",
351 "No key images found for study");
352 return res;
353 }
354
355 // Build Key Object Selection Document as JSON
356 // Note: Full DICOM SR encoding requires dcmtk/dicom_dataset integration
357 // This endpoint returns a structured JSON that can be converted to DICOM SR
358 std::ostringstream oss;
359 oss << R"({"document_type":"Key Object Selection",)";
360 oss << R"("study_uid":")" << json_escape(study_uid) << R"(",)";
361 oss << R"("document_title":"Key Images",)";
362 oss << R"("referenced_instances":[)";
363
364 for (size_t i = 0; i < key_images.size(); ++i) {
365 if (i > 0) {
366 oss << ",";
367 }
368 const auto &ki = key_images[i];
369 oss << R"({"sop_instance_uid":")" << json_escape(ki.sop_instance_uid) << R"(",)";
370 oss << R"("frame_number":)";
371 if (ki.frame_number.has_value()) {
372 oss << ki.frame_number.value();
373 } else {
374 oss << "null";
375 }
376 oss << R"(,"reason":")" << json_escape(ki.reason) << R"("})";
377 }
378
379 oss << R"(],"created_at":")" << format_timestamp(std::chrono::system_clock::now()) << R"("})";
380
381 res.code = 200;
382 res.add_header("Content-Type", "application/json");
383 res.body = oss.str();
384 return res;
385 });
386}
387
388} // namespace kcenon::pacs::web::endpoints
Repository for key image persistence (legacy SQLite interface)
auto find_by_study(std::string_view study_uid) const -> std::vector< key_image_record >
auto remove(std::string_view key_image_id) -> VoidResult
auto exists(std::string_view key_image_id) const -> bool
auto save(const key_image_record &record) -> VoidResult
PACS index database for metadata storage and retrieval.
Key image API endpoints for REST server.
Key image record data structures for database operations.
Repository for key image persistence using base_repository pattern.
void register_key_image_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
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::string json_escape(std::string_view s)
Escape a string for JSON.
Definition rest_types.h:101
Configuration for REST API server.
Common types and utilities for REST API.
Key image record from the database.
std::chrono::system_clock::time_point created_at
Record creation timestamp.
std::optional< int > frame_number
Frame number for multi-frame images (1-based)
std::string user_id
User who marked the key image.
std::string study_uid
Study Instance UID - DICOM tag (0020,000D)
std::string sop_instance_uid
SOP Instance UID - DICOM tag (0008,0018)
std::string key_image_id
Unique key image identifier (UUID)
std::string document_title
Document title for Key Object Selection.
std::string reason
Reason for marking as key image.
System API endpoints for REST server.