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);
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";
62 std::string uuid =
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
63 for (
char &c : uuid) {
66 }
else if (c ==
'y') {
67 c = hex[(dis(gen) & 0x3) | 0x8];
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");
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();
100 << R"(","document_title":")" << json_escape(ki.document_title)
101 << R"(","created_at":")" << format_timestamp(ki.created_at) << R"("})";
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) {
116 oss << key_image_to_json(key_images[i]);
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) {
132 auto end = json.find(
"\"", pos);
133 if (end == std::string::npos) {
136 return json.substr(pos, end - pos);
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) {
150 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
153 if (pos >= json.size()) {
156 if (json.substr(pos, 4) ==
"null") {
160 return std::stoi(json.substr(pos));
170 std::shared_ptr<rest_server_context> ctx) {
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) {
176 res.add_header(
"Content-Type",
"application/json");
177 add_cors_headers(res, *ctx);
179 if (!ctx->database) {
186 std::string body = req.body;
189 res.body =
make_error_json(
"INVALID_REQUEST",
"Request body is empty");
198 ki.
user_id = parse_json_string(body,
"user_id");
199 ki.
reason = parse_json_string(body,
"reason");
201 ki.
created_at = std::chrono::system_clock::now();
205 res.body =
make_error_json(
"MISSING_FIELD",
"sop_instance_uid is required");
209#ifdef PACS_WITH_DATABASE_SYSTEM
214 auto save_result = repo.
save(ki);
215 if (!save_result.is_ok()) {
223 std::ostringstream oss;
225 << R"(","created_at":")" << format_timestamp(ki.created_at)
227 res.body = oss.str();
232 CROW_ROUTE(app,
"/api/v1/studies/<string>/key-images")
233 .methods(crow::HTTPMethod::GET)(
234 [ctx](
const crow::request & ,
const std::string &study_uid) {
236 res.add_header(
"Content-Type",
"application/json");
237 add_cors_headers(res, *ctx);
239 if (!ctx->database) {
246#ifdef PACS_WITH_DATABASE_SYSTEM
249 if (!key_images_result.is_ok()) {
251 res.body =
make_error_json(
"QUERY_ERROR", key_images_result.error().message);
255 res.body = key_images_to_json(key_images_result.value());
260 res.body = key_images_to_json(key_images);
266 CROW_ROUTE(app,
"/api/v1/key-images/<string>")
267 .methods(crow::HTTPMethod::DELETE)(
268 [ctx](
const crow::request & ,
const std::string &key_image_id) {
270 add_cors_headers(res, *ctx);
272 if (!ctx->database) {
274 res.add_header(
"Content-Type",
"application/json");
276 "Database not configured");
280#ifdef PACS_WITH_DATABASE_SYSTEM
282 auto exists_result = repo.
exists(key_image_id);
283 if (!exists_result.is_ok()) {
285 res.add_header(
"Content-Type",
"application/json");
286 res.body =
make_error_json(
"QUERY_ERROR", exists_result.error().message);
289 if (!exists_result.value()) {
291 res.add_header(
"Content-Type",
"application/json");
297 if (!repo.
exists(key_image_id)) {
299 res.add_header(
"Content-Type",
"application/json");
305 auto remove_result = repo.
remove(key_image_id);
306 if (!remove_result.is_ok()) {
308 res.add_header(
"Content-Type",
"application/json");
319 CROW_ROUTE(app,
"/api/v1/studies/<string>/key-images/export-sr")
320 .methods(crow::HTTPMethod::POST)(
321 [ctx](
const crow::request & ,
const std::string &study_uid) {
323 add_cors_headers(res, *ctx);
325 if (!ctx->database) {
327 res.add_header(
"Content-Type",
"application/json");
329 "Database not configured");
333#ifdef PACS_WITH_DATABASE_SYSTEM
336 if (!key_images_result.is_ok()) {
338 res.add_header(
"Content-Type",
"application/json");
339 res.body =
make_error_json(
"QUERY_ERROR", key_images_result.error().message);
342 const auto& key_images = key_images_result.value();
347 if (key_images.empty()) {
349 res.add_header(
"Content-Type",
"application/json");
351 "No key images found for study");
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":[)";
364 for (
size_t i = 0; i < key_images.size(); ++i) {
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();
376 oss << R
"(,"reason":")" << json_escape(ki.reason) << R"("})";
379 oss << R"(],"created_at":")" << format_timestamp(std::chrono::system_clock::now()) << R"("})";
382 res.add_header("Content-Type",
"application/json");
383 res.body = oss.str();
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.
constexpr std::string_view search
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.
std::string json_escape(std::string_view s)
Escape a string for JSON.
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.