43#ifdef PACS_WITH_DATABASE_SYSTEM
48inline auto make_annotation_repo(storage::index_database* db) {
49 return storage::annotation_repository(db->db_adapter());
55inline std::vector<storage::annotation_record> search_annotations(
56 storage::annotation_repository& repo,
57 const storage::annotation_query& query) {
58 auto result = repo.search(query);
59 return result.is_ok() ? std::move(result.value()) : std::vector<
storage::annotation_record>{};
65inline size_t count_annotations(
66 storage::annotation_repository& repo,
67 const storage::annotation_query& query) {
68 auto result = repo.count_matching(query);
69 return result.is_ok() ? result.value() : 0;
75inline std::optional<storage::annotation_record> find_annotation(
76 storage::annotation_repository& repo,
77 const std::string& annotation_id) {
78 auto result = repo.find_by_id(annotation_id);
79 return result.is_ok() ? std::make_optional(std::move(result.value())) : std::nullopt;
85inline bool annotation_exists(
86 storage::annotation_repository& repo,
87 const std::string& annotation_id) {
88 auto result = repo.exists(annotation_id);
89 return result.is_ok() && result.value();
95inline std::vector<storage::annotation_record> find_by_instance(
96 storage::annotation_repository& repo,
97 const std::string& sop_instance_uid) {
98 auto result = repo.find_by_instance(sop_instance_uid);
99 return result.is_ok() ? std::move(result.value()) : std::vector<
storage::annotation_record>{};
106inline auto make_annotation_repo(storage::index_database* db) {
107 return storage::annotation_repository(db->native_handle());
113inline std::vector<storage::annotation_record> search_annotations(
114 storage::annotation_repository& repo,
115 const storage::annotation_query& query) {
116 return repo.search(query);
122inline size_t count_annotations(
123 storage::annotation_repository& repo,
124 const storage::annotation_query& query) {
125 return repo.count(query);
131inline std::optional<storage::annotation_record> find_annotation(
132 storage::annotation_repository& repo,
133 const std::string& annotation_id) {
134 return repo.find_by_id(annotation_id);
140inline bool annotation_exists(
141 storage::annotation_repository& repo,
142 const std::string& annotation_id) {
143 return repo.exists(annotation_id);
149inline std::vector<storage::annotation_record> find_by_instance(
150 storage::annotation_repository& repo,
151 const std::string& sop_instance_uid) {
152 return repo.find_by_instance(sop_instance_uid);
159void add_cors_headers(crow::response &res,
const rest_server_context &ctx) {
160 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
161 res.add_header(
"Access-Control-Allow-Origin",
162 ctx.config->cors_allowed_origins);
169std::string generate_uuid() {
170 static std::random_device rd;
171 static std::mt19937 gen(rd());
172 static std::uniform_int_distribution<> dis(0, 15);
173 static const char *hex =
"0123456789abcdef";
175 std::string uuid =
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
176 for (
char &c : uuid) {
179 }
else if (c ==
'y') {
180 c = hex[(dis(gen) & 0x3) | 0x8];
189std::string format_timestamp(
190 const std::chrono::system_clock::time_point &tp) {
191 auto time = std::chrono::system_clock::to_time_t(tp);
192 std::ostringstream oss;
193 oss << std::put_time(std::gmtime(&time),
"%Y-%m-%dT%H:%M:%SZ");
200std::string style_to_json(
const storage::annotation_style &
style) {
201 std::ostringstream oss;
202 oss << R
"({"color":")" << json_escape(style.color)
203 << R"(","line_width":)" << style.line_width
204 << R"(,"fill_color":")" << json_escape(style.fill_color)
205 << R"(","fill_opacity":)" << style.fill_opacity
206 << R"(,"font_family":")" << json_escape(style.font_family)
207 << R"(","font_size":)" << style.font_size << "}";
214std::string annotation_to_json(
const storage::annotation_record &ann) {
215 std::ostringstream oss;
216 oss << R
"({"annotation_id":")" << json_escape(ann.annotation_id)
217 << R"(","study_uid":")" << json_escape(ann.study_uid)
218 << R"(","series_uid":")" << json_escape(ann.series_uid)
219 << R"(","sop_instance_uid":")" << json_escape(ann.sop_instance_uid)
220 << R"(","frame_number":)";
221 if (ann.frame_number.has_value()) {
222 oss << ann.frame_number.value();
226 oss << R
"(,"user_id":")" << json_escape(ann.user_id)
227 << R"(","annotation_type":")" << json_escape(to_string(ann.type))
228 << R"(","geometry":)" << ann.geometry_json
229 << R"(,"text":")" << json_escape(ann.text)
230 << R"(","style":)" << style_to_json(ann.style)
231 << R"(,"created_at":")" << format_timestamp(ann.created_at)
232 << R"(","updated_at":")" << format_timestamp(ann.updated_at) << R"("})";
239std::string annotations_to_json(
240 const std::vector<storage::annotation_record> &annotations,
241 size_t total_count) {
242 std::ostringstream oss;
243 oss << R
"({"data":[)";
244 for (
size_t i = 0; i < annotations.size(); ++i) {
248 oss << annotation_to_json(annotations[i]);
250 oss << R
"(],"pagination":{"total":)" << total_count << R"(,"count":)"
251 << annotations.size() << "}}";
258std::pair<size_t, size_t> parse_pagination(
const crow::request &req) {
262 auto limit_param = req.url_params.get(
"limit");
265 limit = std::stoul(limit_param);
274 auto offset_param = req.url_params.get(
"offset");
277 offset = std::stoul(offset_param);
283 return {limit, offset};
289std::string parse_json_string(
const std::string &json,
const std::string &key) {
290 std::string
search =
"\"" + key +
"\":\"";
291 auto pos = json.find(search);
292 if (pos == std::string::npos) {
296 auto end = json.find(
"\"", pos);
297 if (end == std::string::npos) {
300 return json.substr(pos, end - pos);
306std::optional<int> parse_json_int(
const std::string &json,
307 const std::string &key) {
308 std::string
search =
"\"" + key +
"\":";
309 auto pos = json.find(search);
310 if (pos == std::string::npos) {
314 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
317 if (pos >= json.size()) {
320 if (json.substr(pos, 4) ==
"null") {
324 return std::stoi(json.substr(pos));
333std::string parse_json_object(
const std::string &json,
const std::string &key) {
334 std::string
search =
"\"" + key +
"\":";
335 auto pos = json.find(search);
336 if (pos == std::string::npos) {
340 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
343 if (pos >= json.size() || json[pos] !=
'{') {
349 for (; pos < json.size(); ++pos) {
350 if (json[pos] ==
'{') {
352 }
else if (json[pos] ==
'}') {
355 return json.substr(start, pos - start + 1);
365storage::annotation_style parse_style(
const std::string &style_json) {
366 storage::annotation_style
style;
367 auto color = parse_json_string(style_json,
"color");
368 if (!
color.empty()) {
371 auto line_width = parse_json_int(style_json,
"line_width");
375 auto fill_color = parse_json_string(style_json,
"fill_color");
378 std::string
search =
"\"fill_opacity\":";
379 auto pos = style_json.find(search);
380 if (pos != std::string::npos) {
383 style.fill_opacity = std::stof(style_json.substr(pos));
389 auto font_family = parse_json_string(style_json,
"font_family");
393 auto font_size = parse_json_int(style_json,
"font_size");
404 std::shared_ptr<rest_server_context> ctx) {
406 CROW_ROUTE(app,
"/api/v1/annotations")
407 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request &req) {
409 res.add_header(
"Content-Type",
"application/json");
410 add_cors_headers(res, *ctx);
412 if (!ctx->database) {
415 make_error_json(
"DATABASE_UNAVAILABLE",
"Database not configured");
419 std::string body = req.body;
422 res.body = make_error_json(
"INVALID_REQUEST",
"Request body is empty");
428 ann.
study_uid = parse_json_string(body,
"study_uid");
429 ann.
series_uid = parse_json_string(body,
"series_uid");
431 ann.
frame_number = parse_json_int(body,
"frame_number");
432 ann.
user_id = parse_json_string(body,
"user_id");
434 auto type_str = parse_json_string(body,
"annotation_type");
435 auto type_opt = storage::annotation_type_from_string(type_str);
436 if (!type_opt.has_value()) {
438 res.body = make_error_json(
"INVALID_TYPE",
"Invalid annotation type");
441 ann.
type = type_opt.value();
444 ann.
text = parse_json_string(body,
"text");
445 ann.
style = parse_style(parse_json_object(body,
"style"));
446 ann.
created_at = std::chrono::system_clock::now();
451 res.body = make_error_json(
"MISSING_FIELD",
"study_uid is required");
455 auto repo = make_annotation_repo(ctx->database.get());
456 auto save_result = repo.save(ann);
457 if (!save_result.is_ok()) {
460 make_error_json(
"SAVE_ERROR", save_result.error().message);
465 std::ostringstream oss;
466 oss << R
"({"annotation_id":")" << json_escape(ann.annotation_id)
467 << R"(","created_at":")" << format_timestamp(ann.created_at)
469 res.body = oss.str();
474 CROW_ROUTE(app,
"/api/v1/annotations")
475 .methods(crow::HTTPMethod::GET)([ctx](
const crow::request &req) {
477 res.add_header(
"Content-Type",
"application/json");
478 add_cors_headers(res, *ctx);
480 if (!ctx->database) {
483 make_error_json(
"DATABASE_UNAVAILABLE",
"Database not configured");
487 auto [limit, offset] = parse_pagination(req);
491 query.offset = offset;
493 auto study_uid = req.url_params.get(
"study_uid");
495 query.study_uid = study_uid;
497 auto series_uid = req.url_params.get(
"series_uid");
499 query.series_uid = series_uid;
501 auto sop_instance_uid = req.url_params.get(
"sop_instance_uid");
502 if (sop_instance_uid) {
503 query.sop_instance_uid = sop_instance_uid;
505 auto user_id = req.url_params.get(
"user_id");
507 query.user_id = user_id;
510 auto repo = make_annotation_repo(ctx->database.get());
513 count_query.
limit = 0;
515 size_t total_count = count_annotations(repo, count_query);
517 auto annotations = search_annotations(repo, query);
520 res.body = annotations_to_json(annotations, total_count);
525 CROW_ROUTE(app,
"/api/v1/annotations/<string>")
526 .methods(crow::HTTPMethod::GET)(
527 [ctx](
const crow::request & ,
const std::string &annotation_id) {
529 res.add_header(
"Content-Type",
"application/json");
530 add_cors_headers(res, *ctx);
532 if (!ctx->database) {
534 res.body = make_error_json(
"DATABASE_UNAVAILABLE",
535 "Database not configured");
539 auto repo = make_annotation_repo(ctx->database.get());
540 auto ann = find_annotation(repo, annotation_id);
541 if (!ann.has_value()) {
543 res.body = make_error_json(
"NOT_FOUND",
"Annotation not found");
548 res.body = annotation_to_json(ann.value());
553 CROW_ROUTE(app,
"/api/v1/annotations/<string>")
554 .methods(crow::HTTPMethod::PUT)(
555 [ctx](
const crow::request &req,
const std::string &annotation_id) {
557 res.add_header(
"Content-Type",
"application/json");
558 add_cors_headers(res, *ctx);
560 if (!ctx->database) {
562 res.body = make_error_json(
"DATABASE_UNAVAILABLE",
563 "Database not configured");
567 auto repo = make_annotation_repo(ctx->database.get());
568 auto existing = find_annotation(repo, annotation_id);
569 if (!existing.has_value()) {
571 res.body = make_error_json(
"NOT_FOUND",
"Annotation not found");
575 std::string body = req.body;
579 make_error_json(
"INVALID_REQUEST",
"Request body is empty");
584 auto geometry = parse_json_object(body,
"geometry");
585 if (geometry !=
"{}") {
588 auto text = parse_json_string(body,
"text");
589 if (!text.empty() || body.find(
"\"text\":\"\"") != std::string::npos) {
592 auto style_json = parse_json_object(body,
"style");
593 if (style_json !=
"{}") {
594 ann.
style = parse_style(style_json);
596 ann.
updated_at = std::chrono::system_clock::now();
598 auto update_result = repo.update(ann);
599 if (!update_result.is_ok()) {
602 make_error_json(
"UPDATE_ERROR", update_result.error().message);
607 std::ostringstream oss;
608 oss << R
"({"annotation_id":")" << json_escape(ann.annotation_id)
609 << R"(","updated_at":")" << format_timestamp(ann.updated_at)
611 res.body = oss.str();
616 CROW_ROUTE(app,
"/api/v1/annotations/<string>")
617 .methods(crow::HTTPMethod::DELETE)(
618 [ctx](
const crow::request & ,
const std::string &annotation_id) {
620 add_cors_headers(res, *ctx);
622 if (!ctx->database) {
624 res.add_header(
"Content-Type",
"application/json");
625 res.body = make_error_json(
"DATABASE_UNAVAILABLE",
626 "Database not configured");
630 auto repo = make_annotation_repo(ctx->database.get());
631 if (!annotation_exists(repo, annotation_id)) {
633 res.add_header(
"Content-Type",
"application/json");
634 res.body = make_error_json(
"NOT_FOUND",
"Annotation not found");
638 auto remove_result = repo.remove(annotation_id);
639 if (!remove_result.is_ok()) {
641 res.add_header(
"Content-Type",
"application/json");
643 make_error_json(
"DELETE_ERROR", remove_result.error().message);
652 CROW_ROUTE(app,
"/api/v1/instances/<string>/annotations")
653 .methods(crow::HTTPMethod::GET)(
654 [ctx](
const crow::request & ,
655 const std::string &sop_instance_uid) {
657 res.add_header(
"Content-Type",
"application/json");
658 add_cors_headers(res, *ctx);
660 if (!ctx->database) {
662 res.body = make_error_json(
"DATABASE_UNAVAILABLE",
663 "Database not configured");
667 auto repo = make_annotation_repo(ctx->database.get());
668 auto annotations = find_by_instance(repo, sop_instance_uid);
670 std::ostringstream oss;
671 oss << R
"({"data":[)";
672 for (
size_t i = 0; i < annotations.size(); ++i) {
676 oss << annotation_to_json(annotations[i]);
681 res.body = oss.str();
Annotation API endpoints for REST server.
Annotation record data structures for database operations.
Repository for annotation persistence using base_repository pattern.
PACS index database for metadata storage and retrieval.
@ storage
Storage Service Class.
constexpr std::string_view search
void register_annotation_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
Configuration for REST API server.
Common types and utilities for REST API.
size_t limit
Maximum number of results to return (0 = unlimited)
size_t offset
Offset for pagination.
Annotation record from the database.
std::string annotation_id
Unique annotation identifier (UUID)
annotation_type type
Type of annotation.
std::optional< int > frame_number
Frame number for multi-frame images (1-based)
std::string text
Text content for text annotations or labels.
std::string series_uid
Series Instance UID - DICOM tag (0020,000E)
std::string study_uid
Study Instance UID - DICOM tag (0020,000D)
annotation_style style
Visual style of the annotation.
std::chrono::system_clock::time_point created_at
Record creation timestamp.
std::chrono::system_clock::time_point updated_at
Record last update timestamp.
std::string sop_instance_uid
SOP Instance UID - DICOM tag (0008,0018)
std::string geometry_json
Geometry data as JSON string (type-specific coordinates)
std::string user_id
User who created the annotation.
System API endpoints for REST server.