1793 {
1794
1795
1796
1797
1798
1799 CROW_ROUTE(app, "/dicomweb/studies/<string>")
1800 .methods(crow::HTTPMethod::GET)(
1801 [ctx](const crow::request& req, const std::string& study_uid) {
1802 crow::response res;
1803 add_cors_headers(res, *ctx);
1804
1805
1806 if (!check_dicomweb_auth(ctx, req, res,
1807 {"dicomweb.read"})) {
1808 return res;
1809 }
1810
1811 if (!ctx->database) {
1812 res.code = 503;
1813 res.add_header("Content-Type", "application/json");
1814 res.body = make_error_json("DATABASE_UNAVAILABLE",
1815 "Database not configured");
1816 return res;
1817 }
1818
1819
1820 auto accept = req.get_header_value("Accept");
1821 auto accept_infos = dicomweb::parse_accept_header(accept);
1822
1823
1824 if (dicomweb::is_acceptable(accept_infos,
1825 dicomweb::media_type::dicom_json)) {
1826 auto files_result = ctx->database->get_study_files(study_uid);
1827 if (!files_result.is_ok()) {
1828 res.code = 500;
1829 res.add_header("Content-Type", "application/json");
1830 res.body = make_error_json("QUERY_ERROR",
1831 files_result.error().message);
1832 return res;
1833 }
1834 std::string bulk_uri = "/dicomweb/studies/" + study_uid +
1835 "/bulkdata/";
1836 return build_metadata_response(files_result.value(), *ctx, bulk_uri);
1837 }
1838
1839
1840 auto files_result = ctx->database->get_study_files(study_uid);
1841 if (!files_result.is_ok()) {
1842 res.code = 500;
1843 res.add_header("Content-Type", "application/json");
1844 res.body = make_error_json("QUERY_ERROR",
1845 files_result.error().message);
1846 return res;
1847 }
1848 std::string base_uri = "/dicomweb/studies/" + study_uid;
1849 return build_multipart_dicom_response(files_result.value(), *ctx, base_uri);
1850 });
1851
1852
1853 CROW_ROUTE(app, "/dicomweb/studies/<string>/metadata")
1854 .methods(crow::HTTPMethod::GET)(
1855 [ctx](const crow::request& req, const std::string& study_uid) {
1856 crow::response res;
1857 add_cors_headers(res, *ctx);
1858
1859
1860 if (!check_dicomweb_auth(ctx, req, res,
1861 {"dicomweb.read"})) {
1862 return res;
1863 }
1864
1865 if (!ctx->database) {
1866 res.code = 503;
1867 res.add_header("Content-Type", "application/json");
1868 res.body = make_error_json("DATABASE_UNAVAILABLE",
1869 "Database not configured");
1870 return res;
1871 }
1872
1873 auto files_result = ctx->database->get_study_files(study_uid);
1874 if (!files_result.is_ok()) {
1875 res.code = 500;
1876 res.add_header("Content-Type", "application/json");
1877 res.body = make_error_json("QUERY_ERROR",
1878 files_result.error().message);
1879 return res;
1880 }
1881 std::string bulk_uri = "/dicomweb/studies/" + study_uid +
1882 "/bulkdata/";
1883 return build_metadata_response(files_result.value(), *ctx, bulk_uri);
1884 });
1885
1886
1887
1888
1889
1890
1891 CROW_ROUTE(app, "/dicomweb/studies/<string>/series/<string>")
1892 .methods(crow::HTTPMethod::GET)(
1893 [ctx](const crow::request& req,
1894 const std::string& study_uid,
1895 const std::string& series_uid) {
1896 crow::response res;
1897 add_cors_headers(res, *ctx);
1898
1899 if (!ctx->database) {
1900 res.code = 503;
1901 res.add_header("Content-Type", "application/json");
1902 res.body = make_error_json("DATABASE_UNAVAILABLE",
1903 "Database not configured");
1904 return res;
1905 }
1906
1907
1908 auto study = ctx->database->find_study(study_uid);
1909 if (!study) {
1910 res.code = 404;
1911 res.add_header("Content-Type", "application/json");
1912 res.body = make_error_json("NOT_FOUND", "Study not found");
1913 return res;
1914 }
1915
1916
1917 auto accept = req.get_header_value(
"Accept");
1918 auto accept_infos = dicomweb::parse_accept_header(accept);
1919
1920
1921 if (dicomweb::is_acceptable(accept_infos,
1922 dicomweb::media_type::dicom_json)) {
1923 auto files_result = ctx->database->get_series_files(series_uid);
1924 if (!files_result.is_ok()) {
1925 res.code = 500;
1926 res.add_header("Content-Type", "application/json");
1927 res.body = make_error_json("QUERY_ERROR",
1928 files_result.error().message);
1929 return res;
1930 }
1931 std::string bulk_uri = "/dicomweb/studies/" + study_uid +
1932 "/series/" + series_uid + "/bulkdata/";
1933 return build_metadata_response(files_result.value(), *ctx, bulk_uri);
1934 }
1935
1936 auto files_result = ctx->database->get_series_files(series_uid);
1937 if (!files_result.is_ok()) {
1938 res.code = 500;
1939 res.add_header("Content-Type", "application/json");
1940 res.body = make_error_json("QUERY_ERROR",
1941 files_result.error().message);
1942 return res;
1943 }
1944 std::string base_uri = "/dicomweb/studies/" + study_uid +
1945 "/series/" + series_uid;
1946 return build_multipart_dicom_response(files_result.value(), *ctx, base_uri);
1947 });
1948
1949
1950 CROW_ROUTE(app, "/dicomweb/studies/<string>/series/<string>/metadata")
1951 .methods(crow::HTTPMethod::GET)(
1952 [ctx](const crow::request& ,
1953 const std::string& study_uid,
1954 const std::string& series_uid) {
1955 crow::response res;
1956 add_cors_headers(res, *ctx);
1957
1958 if (!ctx->database) {
1959 res.code = 503;
1960 res.add_header("Content-Type", "application/json");
1961 res.body = make_error_json("DATABASE_UNAVAILABLE",
1962 "Database not configured");
1963 return res;
1964 }
1965
1966 auto files_result = ctx->database->get_series_files(series_uid);
1967 if (!files_result.is_ok()) {
1968 res.code = 500;
1969 res.add_header("Content-Type", "application/json");
1970 res.body = make_error_json("QUERY_ERROR",
1971 files_result.error().message);
1972 return res;
1973 }
1974 std::string bulk_uri = "/dicomweb/studies/" + study_uid +
1975 "/series/" + series_uid + "/bulkdata/";
1976 return build_metadata_response(files_result.value(), *ctx, bulk_uri);
1977 });
1978
1979
1980
1981
1982
1983
1984 CROW_ROUTE(app,
1985 "/dicomweb/studies/<string>/series/<string>/instances/<string>")
1986 .methods(crow::HTTPMethod::GET)(
1987 [ctx](const crow::request& req,
1988 const std::string& study_uid,
1989 const std::string& series_uid,
1990 const std::string& sop_uid) {
1991 crow::response res;
1992 add_cors_headers(res, *ctx);
1993
1994 if (!ctx->database) {
1995 res.code = 503;
1996 res.add_header("Content-Type", "application/json");
1997 res.body = make_error_json("DATABASE_UNAVAILABLE",
1998 "Database not configured");
1999 return res;
2000 }
2001
2002 auto file_path_result = ctx->database->get_file_path(sop_uid);
2003 if (!file_path_result.is_ok()) {
2004 res.code = 500;
2005 res.add_header("Content-Type", "application/json");
2006 res.body = make_error_json("QUERY_ERROR",
2007 file_path_result.error().message);
2008 return res;
2009 }
2010 const auto& file_path = file_path_result.value();
2011 if (!file_path) {
2012 res.code = 404;
2013 res.add_header("Content-Type", "application/json");
2014 res.body = make_error_json("NOT_FOUND", "Instance not found");
2015 return res;
2016 }
2017
2018
2019 auto accept = req.get_header_value(
"Accept");
2020 auto accept_infos = dicomweb::parse_accept_header(accept);
2021
2022
2023 if (dicomweb::is_acceptable(accept_infos,
2024 dicomweb::media_type::dicom_json)) {
2025 std::vector<std::string> files = {*file_path};
2026 std::string bulk_uri = "/dicomweb/studies/" + study_uid +
2027 "/series/" + series_uid +
2028 "/instances/" + sop_uid + "/bulkdata/";
2029 return build_metadata_response(files, *ctx, bulk_uri);
2030 }
2031
2032
2033 auto data = read_file_bytes(*file_path);
2034 if (data.empty()) {
2035 res.code = 500;
2036 res.add_header("Content-Type", "application/json");
2037 res.body = make_error_json("READ_ERROR",
2038 "Failed to read DICOM file");
2039 return res;
2040 }
2041
2042 res.code = 200;
2043 res.add_header("Content-Type",
2044 std::string(dicomweb::media_type::dicom));
2045 res.body = std::string(reinterpret_cast<char*>(data.data()),
2046 data.size());
2047 return res;
2048 });
2049
2050
2051 CROW_ROUTE(app,
2052 "/dicomweb/studies/<string>/series/<string>/instances/<string>/metadata")
2053 .methods(crow::HTTPMethod::GET)(
2054 [ctx](const crow::request& ,
2055 const std::string& study_uid,
2056 const std::string& series_uid,
2057 const std::string& sop_uid) {
2058 crow::response res;
2059 add_cors_headers(res, *ctx);
2060
2061 if (!ctx->database) {
2062 res.code = 503;
2063 res.add_header("Content-Type", "application/json");
2064 res.body = make_error_json("DATABASE_UNAVAILABLE",
2065 "Database not configured");
2066 return res;
2067 }
2068
2069 auto file_path_result = ctx->database->get_file_path(sop_uid);
2070 if (!file_path_result.is_ok()) {
2071 res.code = 500;
2072 res.add_header("Content-Type", "application/json");
2073 res.body = make_error_json("QUERY_ERROR",
2074 file_path_result.error().message);
2075 return res;
2076 }
2077 const auto& file_path = file_path_result.value();
2078 if (!file_path) {
2079 res.code = 404;
2080 res.add_header("Content-Type", "application/json");
2081 res.body = make_error_json("NOT_FOUND", "Instance not found");
2082 return res;
2083 }
2084
2085 std::vector<std::string> files = {*file_path};
2086 std::string bulk_uri = "/dicomweb/studies/" + study_uid +
2087 "/series/" + series_uid +
2088 "/instances/" + sop_uid + "/bulkdata/";
2089 return build_metadata_response(files, *ctx, bulk_uri);
2090 });
2091
2092
2093
2094
2095
2096
2097 CROW_ROUTE(app,
2098 "/dicomweb/studies/<string>/series/<string>/instances/<string>/frames/<string>")
2099 .methods(crow::HTTPMethod::GET)(
2100 [ctx](const crow::request& req,
2101 const std::string& study_uid,
2102 const std::string& series_uid,
2103 const std::string& sop_uid,
2104 const std::string& frame_list) {
2105 crow::response res;
2106 add_cors_headers(res, *ctx);
2107
2108 if (!ctx->database) {
2109 res.code = 503;
2110 res.add_header("Content-Type", "application/json");
2111 res.body = make_error_json("DATABASE_UNAVAILABLE",
2112 "Database not configured");
2113 return res;
2114 }
2115
2116 auto file_path_result = ctx->database->get_file_path(sop_uid);
2117 if (!file_path_result.is_ok()) {
2118 res.code = 500;
2119 res.add_header("Content-Type", "application/json");
2120 res.body = make_error_json("QUERY_ERROR",
2121 file_path_result.error().message);
2122 return res;
2123 }
2124 const auto& file_path = file_path_result.value();
2125 if (!file_path) {
2126 res.code = 404;
2127 res.add_header("Content-Type", "application/json");
2128 res.body = make_error_json("NOT_FOUND", "Instance not found");
2129 return res;
2130 }
2131
2132
2133 auto frames = dicomweb::parse_frame_numbers(frame_list);
2134 if (frames.empty()) {
2135 res.code = 400;
2136 res.add_header("Content-Type", "application/json");
2137 res.body = make_error_json("INVALID_FRAME_LIST",
2138 "No valid frame numbers specified");
2139 return res;
2140 }
2141
2142
2143 auto data = read_file_bytes(*file_path);
2144 if (data.empty()) {
2145 res.code = 500;
2146 res.add_header("Content-Type", "application/json");
2147 res.body = make_error_json("READ_ERROR",
2148 "Failed to read DICOM file");
2149 return res;
2150 }
2151
2152 auto dicom_result = core::dicom_file::from_bytes(
2153 std::span<const uint8_t>(data.data(), data.size()));
2154 if (dicom_result.is_err()) {
2155 res.code = 500;
2156 res.add_header("Content-Type", "application/json");
2157 res.body = make_error_json("PARSE_ERROR",
2158 "Failed to parse DICOM file");
2159 return res;
2160 }
2161
2162 const auto& dataset = dicom_result.value().dataset();
2163
2164
2165 auto rows_elem = dataset.get(core::tags::rows);
2166 auto cols_elem = dataset.get(core::tags::columns);
2167 auto bits_alloc_elem = dataset.get(core::tags::bits_allocated);
2168 auto samples_elem = dataset.get(core::tags::samples_per_pixel);
2169
2170 constexpr core::dicom_tag number_of_frames_tag{0x0028, 0x0008};
2171 auto num_frames_elem = dataset.get(number_of_frames_tag);
2172 auto pixel_data_elem = dataset.get(core::tags::pixel_data);
2173
2174 if (!rows_elem || !cols_elem || !pixel_data_elem) {
2175 res.code = 400;
2176 res.add_header("Content-Type", "application/json");
2177 res.body = make_error_json("NOT_IMAGE",
2178 "Instance does not contain image data");
2179 return res;
2180 }
2181
2182 uint16_t
rows = rows_elem->as_numeric<uint16_t>().unwrap_or(0);
2183 uint16_t cols = cols_elem->as_numeric<uint16_t>().unwrap_or(0);
2185 bits_alloc_elem->as_numeric<uint16_t>().unwrap_or(16) : 16;
2187 samples_elem->as_numeric<uint16_t>().unwrap_or(1) : 1;
2188 uint32_t num_frames = 1;
2189 if (num_frames_elem) {
2190 try {
2191 num_frames = std::stoul(num_frames_elem->as_string().unwrap_or("1"));
2192 } catch (...) {}
2193 }
2194
2195
2196 size_t frame_size =
static_cast<size_t>(
rows) * cols *
2197 samples_per_pixel * ((bits_allocated + 7) / 8);
2198
2199 auto pixel_data = pixel_data_elem->raw_data();
2200
2201
2202 auto accept = req.get_header_value(
"Accept");
2203
2204
2205 dicomweb::multipart_builder builder(
2206 dicomweb::media_type::octet_stream);
2207
2208 for (uint32_t frame_num : frames) {
2209 if (frame_num > num_frames) {
2210
2211 continue;
2212 }
2213
2214 auto frame_data = dicomweb::extract_frame(
2215 pixel_data, frame_num, frame_size);
2216
2217 if (!frame_data.empty()) {
2218 std::string
location =
"/dicomweb/studies/" + study_uid +
2219 "/series/" + series_uid +
2220 "/instances/" + sop_uid +
2221 "/frames/" + std::to_string(frame_num);
2222 builder.add_part_with_location(std::move(frame_data), location);
2223 }
2224 }
2225
2226 if (builder.empty()) {
2227 res.code = 404;
2228 res.add_header("Content-Type", "application/json");
2229 res.body = make_error_json("NOT_FOUND",
2230 "No valid frames found");
2231 return res;
2232 }
2233
2234
2235 if (builder.size() == 1) {
2236
2237 auto body = builder.build();
2238 res.code = 200;
2239 res.add_header("Content-Type",
2240 std::string(dicomweb::media_type::octet_stream));
2241
2242 auto frame_data = dicomweb::extract_frame(
2243 pixel_data, frames[0], frame_size);
2244 res.body = std::string(
2245 reinterpret_cast<char*>(frame_data.data()),
2246 frame_data.size());
2247 } else {
2248
2249 res.code = 200;
2250 res.add_header("Content-Type", builder.content_type_header());
2251 res.body = builder.build();
2252 }
2253
2254 return res;
2255 });
2256
2257
2258
2259
2260
2261
2262 CROW_ROUTE(app,
2263 "/dicomweb/studies/<string>/series/<string>/instances/<string>/rendered")
2264 .methods(crow::HTTPMethod::GET)(
2265 [ctx](const crow::request& req,
2266 const std::string& study_uid,
2267 const std::string& series_uid,
2268 const std::string& sop_uid) {
2269 crow::response res;
2270 add_cors_headers(res, *ctx);
2271
2272 if (!ctx->database) {
2273 res.code = 503;
2274 res.add_header("Content-Type", "application/json");
2275 res.body = make_error_json("DATABASE_UNAVAILABLE",
2276 "Database not configured");
2277 return res;
2278 }
2279
2280 auto file_path_result = ctx->database->get_file_path(sop_uid);
2281 if (!file_path_result.is_ok()) {
2282 res.code = 500;
2283 res.add_header("Content-Type", "application/json");
2284 res.body = make_error_json("QUERY_ERROR",
2285 file_path_result.error().message);
2286 return res;
2287 }
2288 const auto& file_path = file_path_result.value();
2289 if (!file_path) {
2290 res.code = 404;
2291 res.add_header("Content-Type", "application/json");
2292 res.body = make_error_json("NOT_FOUND", "Instance not found");
2293 return res;
2294 }
2295
2296
2297 auto accept = req.get_header_value(
"Accept");
2298 auto params = dicomweb::parse_rendered_params(req.raw_url, accept);
2299
2300
2301 auto result = dicomweb::render_dicom_image(*file_path, params);
2302
2303 if (!result.success) {
2304 res.code = 400;
2305 res.add_header("Content-Type", "application/json");
2306 res.body = make_error_json("RENDER_ERROR", result.error_message);
2307 return res;
2308 }
2309
2310 res.code = 200;
2311 res.add_header("Content-Type", result.content_type);
2312 res.body = std::string(
2313 reinterpret_cast<char*>(result.data.data()),
2314 result.data.size());
2315 return res;
2316 });
2317
2318
2319 CROW_ROUTE(app,
2320 "/dicomweb/studies/<string>/series/<string>/instances/<string>/frames/<string>/rendered")
2321 .methods(crow::HTTPMethod::GET)(
2322 [ctx](const crow::request& req,
2323 const std::string& study_uid,
2324 const std::string& series_uid,
2325 const std::string& sop_uid,
2326 const std::string& frame_str) {
2327 crow::response res;
2328 add_cors_headers(res, *ctx);
2329
2330 if (!ctx->database) {
2331 res.code = 503;
2332 res.add_header("Content-Type", "application/json");
2333 res.body = make_error_json("DATABASE_UNAVAILABLE",
2334 "Database not configured");
2335 return res;
2336 }
2337
2338 auto file_path_result = ctx->database->get_file_path(sop_uid);
2339 if (!file_path_result.is_ok()) {
2340 res.code = 500;
2341 res.add_header("Content-Type", "application/json");
2342 res.body = make_error_json("QUERY_ERROR",
2343 file_path_result.error().message);
2344 return res;
2345 }
2346 const auto& file_path = file_path_result.value();
2347 if (!file_path) {
2348 res.code = 404;
2349 res.add_header("Content-Type", "application/json");
2350 res.body = make_error_json("NOT_FOUND", "Instance not found");
2351 return res;
2352 }
2353
2354
2355 uint32_t frame_num = 1;
2356 try {
2357 frame_num = std::stoul(frame_str);
2358 if (frame_num == 0) frame_num = 1;
2359 } catch (...) {
2360 res.code = 400;
2361 res.add_header("Content-Type", "application/json");
2362 res.body = make_error_json("INVALID_FRAME",
2363 "Invalid frame number");
2364 return res;
2365 }
2366
2367
2368 auto accept = req.get_header_value(
"Accept");
2369 auto params = dicomweb::parse_rendered_params(req.raw_url, accept);
2370 params.frame = frame_num;
2371
2372
2373 auto result = dicomweb::render_dicom_image(*file_path, params);
2374
2375 if (!result.success) {
2376 res.code = 400;
2377 res.add_header("Content-Type", "application/json");
2378 res.body = make_error_json("RENDER_ERROR", result.error_message);
2379 return res;
2380 }
2381
2382 res.code = 200;
2383 res.add_header("Content-Type", result.content_type);
2384 res.body = std::string(
2385 reinterpret_cast<char*>(result.data.data()),
2386 result.data.size());
2387 return res;
2388 });
2389
2390
2391
2392
2393
2394
2395 CROW_ROUTE(app, "/dicomweb/studies")
2396 .methods(crow::HTTPMethod::POST)(
2397 [ctx](const crow::request& req) {
2398 crow::response res;
2399 add_cors_headers(res, *ctx);
2400
2401
2402 if (!check_dicomweb_auth(ctx, req, res,
2403 {"dicomweb.write"})) {
2404 return res;
2405 }
2406
2407 if (!ctx->database || !ctx->file_storage) {
2408 res.code = 503;
2409 res.add_header("Content-Type", "application/json");
2410 res.body = make_error_json("SERVICE_UNAVAILABLE",
2411 !ctx->database
2412 ? "Database not configured"
2413 : "File storage not configured");
2414 return res;
2415 }
2416
2417
2418 auto content_type = req.get_header_value("Content-Type");
2419 if (content_type.empty() ||
2420 content_type.find("multipart/related") == std::string::npos) {
2421 res.code = 415;
2422 res.add_header("Content-Type", "application/json");
2423 res.body = make_error_json(
2424 "UNSUPPORTED_MEDIA_TYPE",
2425 "Content-Type must be multipart/related");
2426 return res;
2427 }
2428
2429
2430 auto parse_result = dicomweb::multipart_parser::parse(
2431 content_type, req.body);
2432
2433 if (!parse_result) {
2434 res.code = 400;
2435 res.add_header("Content-Type", "application/json");
2436 res.body = make_error_json(
2437 parse_result.error->code,
2438 parse_result.error->message);
2439 return res;
2440 }
2441
2442 if (parse_result.parts.empty()) {
2443 res.code = 400;
2444 res.add_header("Content-Type", "application/json");
2445 res.body = make_error_json(
2446 "NO_INSTANCES",
2447 "No DICOM instances in request body");
2448 return res;
2449 }
2450
2451
2452 dicomweb::store_response store_response;
2453
2454 for (const auto& part : parse_result.parts) {
2455 dicomweb::store_instance_result result;
2456
2457
2458 if (part.content_type.find("application/dicom") ==
2459 std::string::npos) {
2460 continue;
2461 }
2462
2463
2464 auto dicom_result = core::dicom_file::from_bytes(
2465 std::span<const uint8_t>(part.data.data(), part.data.size()));
2466
2467 if (dicom_result.is_err()) {
2468 result.success = false;
2469 result.error_code = "INVALID_DATA";
2470 result.error_message = "Failed to parse DICOM data";
2471 store_response.failed_instances.push_back(
2472 std::move(result));
2473 continue;
2474 }
2475
2476 const auto& dataset = dicom_result.value().dataset();
2477
2478
2479 auto validation = dicomweb::validate_instance(dataset);
2480 if (!validation) {
2481 result.success = false;
2482 result.error_code = validation.error_code;
2483 result.error_message = validation.error_message;
2484
2485
2486 if (auto elem = dataset.get(core::tags::sop_class_uid)) {
2487 result.sop_class_uid = elem->as_string().unwrap_or("");
2488 }
2489 if (auto elem = dataset.get(core::tags::sop_instance_uid)) {
2490 result.sop_instance_uid = elem->as_string().unwrap_or("");
2491 }
2492
2493 store_response.failed_instances.push_back(
2494 std::move(result));
2495 continue;
2496 }
2497
2498
2499 auto sop_class_elem = dataset.get(core::tags::sop_class_uid);
2500 auto sop_instance_elem = dataset.get(core::tags::sop_instance_uid);
2501 auto study_uid_elem = dataset.get(core::tags::study_instance_uid);
2502 auto series_uid_elem = dataset.get(core::tags::series_instance_uid);
2503
2504 result.sop_class_uid = sop_class_elem->as_string().unwrap_or("");
2505 result.sop_instance_uid = sop_instance_elem->as_string().unwrap_or("");
2506
2507 std::string study_uid = study_uid_elem->as_string().unwrap_or("");
2508 std::string series_uid = series_uid_elem->as_string().unwrap_or("");
2509
2510
2511 auto existing_result = ctx->database->get_file_path(
2512 result.sop_instance_uid);
2513 if (existing_result.is_ok() && existing_result.value()) {
2514 result.success = false;
2515 result.error_code = "DUPLICATE";
2516 result.error_message = "Instance already exists";
2517 store_response.failed_instances.push_back(
2518 std::move(result));
2519 continue;
2520 }
2521
2522
2523 if (!store_instance_to_storage(ctx, dataset, result)) {
2524 store_response.failed_instances.push_back(
2525 std::move(result));
2526 continue;
2527 }
2528
2529 result.success = true;
2530 result.retrieve_url = "/dicomweb/studies/" + study_uid +
2531 "/series/" + series_uid +
2532 "/instances/" + result.sop_instance_uid;
2533 store_response.referenced_instances.push_back(
2534 std::move(result));
2535 }
2536
2537
2538 std::string base_url;
2539
2540 res.add_header("Content-Type",
2541 std::string(dicomweb::media_type::dicom_json));
2542
2543 if (store_response.all_failed()) {
2544 res.code = 409;
2545 } else if (store_response.partial_success()) {
2546 res.code = 202;
2547 } else {
2548 res.code = 200;
2549 }
2550
2551 res.body = dicomweb::build_store_response_json(
2552 store_response, base_url);
2553 return res;
2554 });
2555
2556
2557 CROW_ROUTE(app, "/dicomweb/studies/<string>")
2558 .methods(crow::HTTPMethod::POST)(
2559 [ctx](const crow::request& req, const std::string& target_study_uid) {
2560 crow::response res;
2561 add_cors_headers(res, *ctx);
2562
2563
2564 if (!check_dicomweb_auth(ctx, req, res,
2565 {"dicomweb.write"})) {
2566 return res;
2567 }
2568
2569 if (!ctx->database || !ctx->file_storage) {
2570 res.code = 503;
2571 res.add_header("Content-Type", "application/json");
2572 res.body = make_error_json("SERVICE_UNAVAILABLE",
2573 !ctx->database
2574 ? "Database not configured"
2575 : "File storage not configured");
2576 return res;
2577 }
2578
2579
2580 auto content_type = req.get_header_value("Content-Type");
2581 if (content_type.empty() ||
2582 content_type.find("multipart/related") == std::string::npos) {
2583 res.code = 415;
2584 res.add_header("Content-Type", "application/json");
2585 res.body = make_error_json(
2586 "UNSUPPORTED_MEDIA_TYPE",
2587 "Content-Type must be multipart/related");
2588 return res;
2589 }
2590
2591
2592 auto parse_result = dicomweb::multipart_parser::parse(
2593 content_type, req.body);
2594
2595 if (!parse_result) {
2596 res.code = 400;
2597 res.add_header("Content-Type", "application/json");
2598 res.body = make_error_json(
2599 parse_result.error->code,
2600 parse_result.error->message);
2601 return res;
2602 }
2603
2604 if (parse_result.parts.empty()) {
2605 res.code = 400;
2606 res.add_header("Content-Type", "application/json");
2607 res.body = make_error_json(
2608 "NO_INSTANCES",
2609 "No DICOM instances in request body");
2610 return res;
2611 }
2612
2613
2614 dicomweb::store_response store_response;
2615
2616 for (const auto& part : parse_result.parts) {
2617 dicomweb::store_instance_result result;
2618
2619
2620 if (part.content_type.find("application/dicom") ==
2621 std::string::npos) {
2622 continue;
2623 }
2624
2625
2626 auto dicom_result = core::dicom_file::from_bytes(
2627 std::span<const uint8_t>(part.data.data(), part.data.size()));
2628
2629 if (dicom_result.is_err()) {
2630 result.success = false;
2631 result.error_code = "INVALID_DATA";
2632 result.error_message = "Failed to parse DICOM data";
2633 store_response.failed_instances.push_back(
2634 std::move(result));
2635 continue;
2636 }
2637
2638 const auto& dataset = dicom_result.value().dataset();
2639
2640
2641 auto validation = dicomweb::validate_instance(
2642 dataset, target_study_uid);
2643 if (!validation) {
2644 result.success = false;
2645 result.error_code = validation.error_code;
2646 result.error_message = validation.error_message;
2647
2648 if (auto elem = dataset.get(core::tags::sop_class_uid)) {
2649 result.sop_class_uid = elem->as_string().unwrap_or("");
2650 }
2651 if (auto elem = dataset.get(core::tags::sop_instance_uid)) {
2652 result.sop_instance_uid = elem->as_string().unwrap_or("");
2653 }
2654
2655 store_response.failed_instances.push_back(
2656 std::move(result));
2657 continue;
2658 }
2659
2660
2661 auto sop_class_elem = dataset.get(core::tags::sop_class_uid);
2662 auto sop_instance_elem = dataset.get(core::tags::sop_instance_uid);
2663 auto series_uid_elem = dataset.get(core::tags::series_instance_uid);
2664
2665 result.sop_class_uid = sop_class_elem->as_string().unwrap_or("");
2666 result.sop_instance_uid = sop_instance_elem->as_string().unwrap_or("");
2667 std::string series_uid = series_uid_elem->as_string().unwrap_or("");
2668
2669
2670 auto existing_result = ctx->database->get_file_path(
2671 result.sop_instance_uid);
2672 if (existing_result.is_ok() && existing_result.value()) {
2673 result.success = false;
2674 result.error_code = "DUPLICATE";
2675 result.error_message = "Instance already exists";
2676 store_response.failed_instances.push_back(
2677 std::move(result));
2678 continue;
2679 }
2680
2681
2682 if (!store_instance_to_storage(ctx, dataset, result)) {
2683 store_response.failed_instances.push_back(
2684 std::move(result));
2685 continue;
2686 }
2687
2688 result.success = true;
2689 result.retrieve_url = "/dicomweb/studies/" + target_study_uid +
2690 "/series/" + series_uid +
2691 "/instances/" + result.sop_instance_uid;
2692 store_response.referenced_instances.push_back(
2693 std::move(result));
2694 }
2695
2696
2697 std::string base_url;
2698
2699 res.add_header("Content-Type",
2700 std::string(dicomweb::media_type::dicom_json));
2701
2702 if (store_response.all_failed()) {
2703 res.code = 409;
2704 } else if (store_response.partial_success()) {
2705 res.code = 202;
2706 } else {
2707 res.code = 200;
2708 }
2709
2710 res.body = dicomweb::build_store_response_json(
2711 store_response, base_url);
2712 return res;
2713 });
2714
2715
2716
2717
2718
2719
2720 CROW_ROUTE(app, "/dicomweb/studies")
2721 .methods(crow::HTTPMethod::GET)(
2722 [ctx](const crow::request& req) {
2723 crow::response res;
2724 add_cors_headers(res, *ctx);
2725 res.add_header("Content-Type",
2726 std::string(dicomweb::media_type::dicom_json));
2727
2728
2729 if (!check_dicomweb_auth(ctx, req, res,
2730 {"dicomweb.read", "dicomweb.search"})) {
2731 return res;
2732 }
2733
2734 if (!ctx->database) {
2735 res.code = 503;
2736 res.body = make_error_json("DATABASE_UNAVAILABLE",
2737 "Database not configured");
2738 return res;
2739 }
2740
2741
2742 auto query = dicomweb::parse_study_query_params(req.raw_url);
2743
2744
2745 if (
query.limit == 0) {
2747 }
2748
2749
2750 auto studies_result = ctx->database->search_studies(query);
2751 if (!studies_result.is_ok()) {
2752 res.code = 500;
2753 res.body = make_error_json("QUERY_ERROR",
2754 studies_result.error().message);
2755 return res;
2756 }
2757
2758
2759 std::ostringstream oss;
2760 oss << "[";
2761
2762 bool first = true;
2763 for (const auto& study : studies_result.value()) {
2764 if (!first) oss << ",";
2765 first = false;
2766
2767
2770 if (
auto patient = ctx->database->find_patient_by_pk(
study.patient_pk)) {
2773 }
2774
2775 oss << dicomweb::study_record_to_dicom_json(
2776 study, patient_id, patient_name);
2777 }
2778
2779 oss << "]";
2780
2781 res.code = 200;
2782 res.body = oss.str();
2783 return res;
2784 });
2785
2786
2787 CROW_ROUTE(app, "/dicomweb/series")
2788 .methods(crow::HTTPMethod::GET)(
2789 [ctx](const crow::request& req) {
2790 crow::response res;
2791 add_cors_headers(res, *ctx);
2792 res.add_header("Content-Type",
2793 std::string(dicomweb::media_type::dicom_json));
2794
2795 if (!ctx->database) {
2796 res.code = 503;
2797 res.body = make_error_json("DATABASE_UNAVAILABLE",
2798 "Database not configured");
2799 return res;
2800 }
2801
2802
2803 auto query = dicomweb::parse_series_query_params(req.raw_url);
2804
2805
2806 if (
query.limit == 0) {
2808 }
2809
2810
2811 auto series_list_result = ctx->database->search_series(query);
2812 if (!series_list_result.is_ok()) {
2813 res.code = 500;
2814 res.body = make_error_json("QUERY_ERROR",
2815 series_list_result.error().message);
2816 return res;
2817 }
2818
2819
2820 std::ostringstream oss;
2821 oss << "[";
2822
2823 bool first = true;
2824 for (const auto& series : series_list_result.value()) {
2825 if (!first) oss << ",";
2826 first = false;
2827
2828
2829 std::string study_uid;
2830 if (
auto study = ctx->database->find_study_by_pk(
series.study_pk)) {
2831 study_uid =
study->study_uid;
2832 }
2833
2834 oss << dicomweb::series_record_to_dicom_json(series, study_uid);
2835 }
2836
2837 oss << "]";
2838
2839 res.code = 200;
2840 res.body = oss.str();
2841 return res;
2842 });
2843
2844
2845 CROW_ROUTE(app, "/dicomweb/instances")
2846 .methods(crow::HTTPMethod::GET)(
2847 [ctx](const crow::request& req) {
2848 crow::response res;
2849 add_cors_headers(res, *ctx);
2850 res.add_header("Content-Type",
2851 std::string(dicomweb::media_type::dicom_json));
2852
2853 if (!ctx->database) {
2854 res.code = 503;
2855 res.body = make_error_json("DATABASE_UNAVAILABLE",
2856 "Database not configured");
2857 return res;
2858 }
2859
2860
2861 auto query = dicomweb::parse_instance_query_params(req.raw_url);
2862
2863
2864 if (
query.limit == 0) {
2866 }
2867
2868
2869 auto instances_result = ctx->database->search_instances(query);
2870 if (!instances_result.is_ok()) {
2871 res.code = 500;
2872 res.body = make_error_json("QUERY_ERROR",
2873 instances_result.error().message);
2874 return res;
2875 }
2876
2877
2878 std::ostringstream oss;
2879 oss << "[";
2880
2881 bool first = true;
2882 for (const auto& instance : instances_result.value()) {
2883 if (!first) oss << ",";
2884 first = false;
2885
2886
2887 std::string series_uid;
2888 std::string study_uid;
2889 if (auto series = ctx->database->find_series_by_pk(instance.series_pk)) {
2890 series_uid =
series->series_uid;
2891 if (
auto study = ctx->database->find_study_by_pk(
series->study_pk)) {
2892 study_uid =
study->study_uid;
2893 }
2894 }
2895
2896 oss << dicomweb::instance_record_to_dicom_json(
2897 instance, series_uid, study_uid);
2898 }
2899
2900 oss << "]";
2901
2902 res.code = 200;
2903 res.body = oss.str();
2904 return res;
2905 });
2906
2907
2908 CROW_ROUTE(app, "/dicomweb/studies/<string>/series")
2909 .methods(crow::HTTPMethod::GET)(
2910 [ctx](const crow::request& req, const std::string& study_uid) {
2911 crow::response res;
2912 add_cors_headers(res, *ctx);
2913 res.add_header("Content-Type",
2914 std::string(dicomweb::media_type::dicom_json));
2915
2916
2917 if (!check_dicomweb_auth(ctx, req, res,
2918 {"dicomweb.read", "dicomweb.search"})) {
2919 return res;
2920 }
2921
2922 if (!ctx->database) {
2923 res.code = 503;
2924 res.body = make_error_json("DATABASE_UNAVAILABLE",
2925 "Database not configured");
2926 return res;
2927 }
2928
2929
2930 auto study = ctx->database->find_study(study_uid);
2931 if (!study) {
2932 res.code = 404;
2933 res.body = make_error_json("NOT_FOUND", "Study not found");
2934 return res;
2935 }
2936
2937
2938 auto query = dicomweb::parse_series_query_params(req.raw_url);
2939 query.study_uid = study_uid;
2940
2941
2942 if (
query.limit == 0) {
2944 }
2945
2946
2947 auto series_list_result = ctx->database->search_series(query);
2948 if (!series_list_result.is_ok()) {
2949 res.code = 500;
2950 res.body = make_error_json("QUERY_ERROR",
2951 series_list_result.error().message);
2952 return res;
2953 }
2954
2955
2956 std::ostringstream oss;
2957 oss << "[";
2958
2959 bool first = true;
2960 for (const auto& series : series_list_result.value()) {
2961 if (!first) oss << ",";
2962 first = false;
2963
2964 oss << dicomweb::series_record_to_dicom_json(series, study_uid);
2965 }
2966
2967 oss << "]";
2968
2969 res.code = 200;
2970 res.body = oss.str();
2971 return res;
2972 });
2973
2974
2975 CROW_ROUTE(app, "/dicomweb/studies/<string>/instances")
2976 .methods(crow::HTTPMethod::GET)(
2977 [ctx](const crow::request& req, const std::string& study_uid) {
2978 crow::response res;
2979 add_cors_headers(res, *ctx);
2980 res.add_header("Content-Type",
2981 std::string(dicomweb::media_type::dicom_json));
2982
2983
2984 if (!check_dicomweb_auth(ctx, req, res,
2985 {"dicomweb.read", "dicomweb.search"})) {
2986 return res;
2987 }
2988
2989 if (!ctx->database) {
2990 res.code = 503;
2991 res.body = make_error_json("DATABASE_UNAVAILABLE",
2992 "Database not configured");
2993 return res;
2994 }
2995
2996
2997 auto study = ctx->database->find_study(study_uid);
2998 if (!study) {
2999 res.code = 404;
3000 res.body = make_error_json("NOT_FOUND", "Study not found");
3001 return res;
3002 }
3003
3004
3005 storage::series_query series_query;
3006 series_query.study_uid = study_uid;
3007 auto series_list_result = ctx->database->search_series(series_query);
3008 if (!series_list_result.is_ok()) {
3009 res.code = 500;
3010 res.body = make_error_json("QUERY_ERROR",
3011 series_list_result.error().message);
3012 return res;
3013 }
3014
3015
3016 auto inst_query = dicomweb::parse_instance_query_params(req.raw_url);
3017 if (inst_query.limit == 0) {
3018 inst_query.limit = 100;
3019 }
3020
3021
3022 std::ostringstream oss;
3023 oss << "[";
3024
3025 bool first = true;
3026 size_t count = 0;
3027 size_t skipped = 0;
3028
3029 for (const auto& series : series_list_result.value()) {
3030 if (count >= inst_query.limit) break;
3031
3032
3033 storage::instance_query
query;
3035 if (inst_query.sop_uid.has_value()) {
3036 query.sop_uid = inst_query.sop_uid;
3037 }
3038 if (inst_query.sop_class_uid.has_value()) {
3039 query.sop_class_uid = inst_query.sop_class_uid;
3040 }
3041 if (inst_query.instance_number.has_value()) {
3042 query.instance_number = inst_query.instance_number;
3043 }
3044 query.limit = inst_query.limit - count;
3045
3046 auto instances_result = ctx->database->search_instances(query);
3047 if (!instances_result.is_ok()) {
3048 continue;
3049 }
3050 for (const auto& instance : instances_result.value()) {
3051
3052 if (skipped < inst_query.offset) {
3053 ++skipped;
3054 continue;
3055 }
3056
3057 if (count >= inst_query.limit) break;
3058
3059 if (!first) oss << ",";
3060 first = false;
3061
3062 oss << dicomweb::instance_record_to_dicom_json(
3063 instance,
series.series_uid, study_uid);
3064 ++count;
3065 }
3066 }
3067
3068 oss << "]";
3069
3070 res.code = 200;
3071 res.body = oss.str();
3072 return res;
3073 });
3074
3075
3076 CROW_ROUTE(app, "/dicomweb/studies/<string>/series/<string>/instances")
3077 .methods(crow::HTTPMethod::GET)(
3078 [ctx](const crow::request& req,
3079 const std::string& study_uid,
3080 const std::string& series_uid) {
3081 crow::response res;
3082 add_cors_headers(res, *ctx);
3083 res.add_header("Content-Type",
3084 std::string(dicomweb::media_type::dicom_json));
3085
3086
3087 if (!check_dicomweb_auth(ctx, req, res,
3088 {"dicomweb.read", "dicomweb.search"})) {
3089 return res;
3090 }
3091
3092 if (!ctx->database) {
3093 res.code = 503;
3094 res.body = make_error_json("DATABASE_UNAVAILABLE",
3095 "Database not configured");
3096 return res;
3097 }
3098
3099
3100 auto study = ctx->database->find_study(study_uid);
3101 if (!study) {
3102 res.code = 404;
3103 res.body = make_error_json("NOT_FOUND", "Study not found");
3104 return res;
3105 }
3106
3107
3108 auto series = ctx->database->find_series(series_uid);
3109 if (!series) {
3110 res.code = 404;
3111 res.body = make_error_json("NOT_FOUND", "Series not found");
3112 return res;
3113 }
3114
3115
3116 auto query = dicomweb::parse_instance_query_params(req.raw_url);
3117 query.series_uid = series_uid;
3118
3119
3120 if (
query.limit == 0) {
3122 }
3123
3124
3125 auto instances_result = ctx->database->search_instances(query);
3126 if (!instances_result.is_ok()) {
3127 res.code = 500;
3128 res.body = make_error_json("QUERY_ERROR",
3129 instances_result.error().message);
3130 return res;
3131 }
3132
3133
3134 std::ostringstream oss;
3135 oss << "[";
3136
3137 bool first = true;
3138 for (const auto& instance : instances_result.value()) {
3139 if (!first) oss << ",";
3140 first = false;
3141
3142 oss << dicomweb::instance_record_to_dicom_json(
3143 instance, series_uid, study_uid);
3144 }
3145
3146 oss << "]";
3147
3148 res.code = 200;
3149 res.body = oss.str();
3150 return res;
3151 });
3152
3153
3154
3155
3156
3157 CROW_ROUTE(app, "/dicomweb/<path>")
3158 .methods(crow::HTTPMethod::OPTIONS)(
3159 [ctx](const crow::request& , const std::string& ) {
3160 crow::response res(204);
3161 if (ctx->config) {
3162 res.add_header("Access-Control-Allow-Origin",
3163 ctx->config->cors_allowed_origins);
3164 }
3165 res.add_header("Access-Control-Allow-Methods",
3166 "GET, POST, OPTIONS");
3167 res.add_header("Access-Control-Allow-Headers",
3168 "Content-Type, Accept, Authorization");
3169 res.add_header("Access-Control-Max-Age", "86400");
3170 return res;
3171 });
3172}
@ accept
Clinician accepts AI result as-is.