diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 8501e69..28ae782 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -86,7 +86,6 @@ jobs: brew install \ autoconf \ automake \ - cmake \ emscripten \ libtool \ little-cms2 \ @@ -98,7 +97,7 @@ jobs: run: | mkdir build cd build - cmake -DCMAKE_PREFIX_PATH='/usr/local' .. + cmake -DCMAKE_PREFIX_PATH=$PWD/install -DCMAKE_INSTALL_PREFIX=$PWD/install .. make make install diff --git a/CMakeLists.txt b/CMakeLists.txt index 485c9fe..3e2f6e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16) project (dicomicc) set(DICOMICC_VERSION_MAJOR 0) -set(DICOMICC_VERSION_MINOR 1) +set(DICOMICC_VERSION_MINOR 2) set(DICOMICC_VERSION_PATCH 0) set(DICOMICC_VERSION "${DICOMICC_VERSION_MAJOR}.${DICOMICC_VERSION_MINOR}.${DICOMICC_VERSION_PATCH}") diff --git a/package.json b/package.json index 31f50b0..c9526ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dicomicc", - "version": "0.1.0", + "version": "0.2.0", "description": "WASM bindings and JavaScript API for the dicomicc C library", "main": "dist/dicomiccwasm.js", "publishConfig": { diff --git a/src/dicomicc.c b/src/dicomicc.c index deab91b..90c13f3 100644 --- a/src/dicomicc.c +++ b/src/dicomicc.c @@ -16,23 +16,256 @@ const char *dcm_icc_get_version(void) { return DCMICC_VERSION; } -DmcIccTransform *dcm_icc_transform_create(const char *icc_profile, - uint32_t icc_profile_size, - uint8_t planar_configuration, - uint16_t columns, - uint16_t rows) { +/** + * Create an sRGB ICC profile + */ +cmsHPROFILE create_srgb_profile(void) { + // Use the built-in littleCMS sRGB profile creation function + cmsHPROFILE profile = cmsCreate_sRGBProfile(); + + if (profile == NULL) { + fprintf(stderr, "Error: Failed to create sRGB profile\n"); + return NULL; + } + + return profile; +} + +/** + * Create a Display-P3 ICC profile + * Based on https://www.color.org/chardata/rgb/DisplayP3.xalter + */ +cmsHPROFILE create_display_p3_profile(void) { + // D65 White Point + cmsCIExyY D65 = { 0.3127, 0.3290, 1.0 }; + + // Display-P3 primaries (same as DCI-P3) + cmsCIExyYTRIPLE DisplayP3Primaries = { + {0.6800, 0.3200, 1.0}, // Display P3 Red + {0.2650, 0.6900, 1.0}, // Display P3 Green + {0.1500, 0.0600, 1.0} // Display P3 Blue + }; + + // sRGB transfer function parameters, parametric curve type 4: + // Y = (a * X + b) ^ gamma for X >= d + // Y = c * X for X < d + cmsToneCurve* sRGBTransferFunction[3]; + cmsFloat64Number Parameters[5]; + Parameters[0] = 2.4; // Gamma + Parameters[1] = 1. / 1.055; // a + Parameters[2] = 0.055 / 1.055; // b + Parameters[3] = 1. / 12.92; // c + Parameters[4] = 0.04045; // d + + // Create the tone curve + sRGBTransferFunction[0] = sRGBTransferFunction[1] = sRGBTransferFunction[2] = + cmsBuildParametricToneCurve(NULL, 4, Parameters); + + if (sRGBTransferFunction[0] == NULL) { + fprintf(stderr, "Error: Failed to create sRGB transfer function\n"); + return NULL; + } + + // Combine the white point, primaries, and transfer functions into an RGB profile + cmsHPROFILE profile = cmsCreateRGBProfileTHR(NULL, &D65, &DisplayP3Primaries, sRGBTransferFunction); + + // Free the tone curve (profile now owns a copy) + cmsFreeToneCurve(sRGBTransferFunction[0]); + + if (profile == NULL) { + fprintf(stderr, "Error: Failed to create Display-P3 profile\n"); + return NULL; + } + + // Set the profile description + const wchar_t* description = L"Display-P3"; + cmsMLU* mlu = cmsMLUalloc(NULL, 1); + if (mlu != NULL) { + cmsMLUsetWide(mlu, "en", "US", description); + cmsWriteTag(profile, cmsSigProfileDescriptionTag, mlu); + cmsMLUfree(mlu); + } + + // Set additional profile information + const wchar_t* copyright = L"Public Domain"; + cmsMLU* copyright_mlu = cmsMLUalloc(NULL, 1); + if (copyright_mlu != NULL) { + cmsMLUsetWide(copyright_mlu, "en", "US", copyright); + cmsWriteTag(profile, cmsSigCopyrightTag, copyright_mlu); + cmsMLUfree(copyright_mlu); + } + + return profile; +} + +/** + * Create an Adobe RGB ICC profile + * Based on https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf (incl. Annex C) + */ +cmsHPROFILE create_adobe_rgb_profile(void) { + // D65 White Point + cmsCIExyY D65 = { 0.3127, 0.3290, 1.0 }; + + // AdobeRGB primaries + cmsCIExyYTRIPLE AdobeRGBPrimaries = { + {0.6400, 0.3300, 1.0}, // Adobe RGB Red + {0.2100, 0.7100, 1.0}, // Adobe RGB Green + {0.1500, 0.0600, 1.0} // Adobe RGB Blue + }; + + // Adobe RGB transfer function parameters, parametric curve type 4: + // Y = (a * X + b) ^ gamma for X >= d + // Y = c * X for X < d + cmsToneCurve* AdobeRGBTransferFunction[3]; + cmsFloat64Number Parameters[5]; + Parameters[0] = 2. + 13107. / 65536.; // Gamma + Parameters[1] = 1.; // a + Parameters[2] = 0; // b + Parameters[3] = 1. / 32; // c + Parameters[4] = 0.055680761; // d (= c ^ (1 / (gamma - 1))) + + // Create the tone curve + AdobeRGBTransferFunction[0] = AdobeRGBTransferFunction[1] = AdobeRGBTransferFunction[2] = + cmsBuildParametricToneCurve(NULL, 4, Parameters); + + if (AdobeRGBTransferFunction[0] == NULL) { + fprintf(stderr, "Error: Failed to create Adobe RGB transfer function\n"); + return NULL; + } + + // Combine the white point, primaries, and transfer functions into an RGB profile + cmsHPROFILE profile = cmsCreateRGBProfileTHR(NULL, &D65, &AdobeRGBPrimaries, AdobeRGBTransferFunction); + + // Free the tone curve (profile now owns a copy) + cmsFreeToneCurve(AdobeRGBTransferFunction[0]); + + if (profile == NULL) { + fprintf(stderr, "Error: Failed to create Adobe RGB profile\n"); + return NULL; + } + + // Set the profile description + const wchar_t* description = L"Adobe RGB (1998)"; + cmsMLU* mlu = cmsMLUalloc(NULL, 1); + if (mlu != NULL) { + cmsMLUsetWide(mlu, "en", "US", description); + cmsWriteTag(profile, cmsSigProfileDescriptionTag, mlu); + cmsMLUfree(mlu); + } + + // Set additional profile information + const wchar_t* copyright = L"Public Domain"; + cmsMLU* copyright_mlu = cmsMLUalloc(NULL, 1); + if (copyright_mlu != NULL) { + cmsMLUsetWide(copyright_mlu, "en", "US", copyright); + cmsWriteTag(profile, cmsSigCopyrightTag, copyright_mlu); + cmsMLUfree(copyright_mlu); + } + + return profile; +} + +/* + * Create a ROMM RGB ICC profile + * Based on https://www.color.org/chardata/rgb/ROMMRGB.pdf + */ +cmsHPROFILE create_romm_rgb_profile(void) { + // D50 White Point + cmsCIExyY D50 = { 0.3457, 0.3585, 1.0 }; + + // ROMM RGB primaries + cmsCIExyYTRIPLE RommRGBPrimaries = { + {0.7347, 0.2653, 1.0}, // ROMM RGB Red + {0.1596, 0.8404, 1.0}, // ROMM RGB Green + {0.0366, 0.0001, 1.0} // ROMM RGB Blue + }; + + // ROMM RGB transfer function parameters, parametric curve type 4: + // Y = (a * X + b) ^ gamma for X >= d + // Y = c * X for X < d + cmsToneCurve* RommRGBTransferFunction[3]; + cmsFloat64Number Parameters[5]; + Parameters[0] = 1.8; // Gamma + Parameters[1] = 1.0; // a + Parameters[2] = 0.0; // b + Parameters[3] = 0.0625; // c + Parameters[4] = 0.03125; // d + + // Create the tone curve + RommRGBTransferFunction[0] = RommRGBTransferFunction[1] = RommRGBTransferFunction[2] = + cmsBuildParametricToneCurve(NULL, 4, Parameters); + + if (RommRGBTransferFunction[0] == NULL) { + fprintf(stderr, "Error: Failed to create Romm RGB transfer function\n"); + return NULL; + } + + // Combine the white point, primaries, and transfer functions into an RGB profile + cmsHPROFILE profile = cmsCreateRGBProfileTHR(NULL, &D50, &RommRGBPrimaries, RommRGBTransferFunction); + + // Free the tone curve (profile now owns a copy) + cmsFreeToneCurve(RommRGBTransferFunction[0]); + + if (profile == NULL) { + fprintf(stderr, "Error: Failed to create Romm RGB profile\n"); + return NULL; + } + + // Set the profile description + const wchar_t* description = L"ROMM RGB"; + cmsMLU* mlu = cmsMLUalloc(NULL, 1); + if (mlu != NULL) { + cmsMLUsetWide(mlu, "en", "US", description); + cmsWriteTag(profile, cmsSigProfileDescriptionTag, mlu); + cmsMLUfree(mlu); + } + + // Set additional profile information + const wchar_t* copyright = L"Public Domain"; + cmsMLU* copyright_mlu = cmsMLUalloc(NULL, 1); + if (copyright_mlu != NULL) { + cmsMLUsetWide(copyright_mlu, "en", "US", copyright); + cmsWriteTag(profile, cmsSigCopyrightTag, copyright_mlu); + cmsMLUfree(copyright_mlu); + } + + return profile; +} + +DmcIccTransform *dcm_icc_transform_create_for_output(const char *icc_profile, + uint32_t icc_profile_size, + uint8_t planar_configuration, + uint16_t columns, + uint16_t rows, + DcmIccOutputType output_type) { cmsUInt32Number type; // Input ICC profile: obtained from DICOM data set - const cmsHPROFILE in_handle = cmsOpenProfileFromMem(icc_profile, - icc_profile_size); + const cmsHPROFILE in_handle = cmsOpenProfileFromMem(icc_profile, + icc_profile_size); + cmsHPROFILE out_handle = NULL; if (in_handle == NULL) { return NULL; } - // Output ICC profile: fixed to sRGB - const cmsHPROFILE out_handle = cmsCreate_sRGBProfile(); + switch (output_type) { + case DCM_ICC_OUTPUT_SRGB: + out_handle = create_srgb_profile(); + break; + case DCM_ICC_OUTPUT_DISPLAY_P3: + out_handle = create_display_p3_profile(); + break; + case DCM_ICC_OUTPUT_ADOBE_RGB: + out_handle = create_adobe_rgb_profile(); + break; + case DCM_ICC_OUTPUT_ROMM_RGB: + out_handle = create_romm_rgb_profile(); + break; + default: + cmsCloseProfile(in_handle); + return NULL; + } if (out_handle == NULL) { cmsCloseProfile(in_handle); @@ -72,6 +305,21 @@ DmcIccTransform *dcm_icc_transform_create(const char *icc_profile, return icc_transform; } +// Backward-compatible wrapper function +DmcIccTransform *dcm_icc_transform_create(const char *icc_profile, + uint32_t icc_profile_size, + uint8_t planar_configuration, + uint16_t columns, + uint16_t rows) { + // Call the extended function with DCM_ICC_OUTPUT_SRGB as the default output type + return dcm_icc_transform_create_for_output(icc_profile, + icc_profile_size, + planar_configuration, + columns, + rows, + DCM_ICC_OUTPUT_SRGB); +} + void dcm_icc_transform_apply(const DmcIccTransform *icc_transform, const char *frame, uint32_t frame_size, diff --git a/src/dicomicc.h b/src/dicomicc.h index d2b4d6b..20d223a 100644 --- a/src/dicomicc.h +++ b/src/dicomicc.h @@ -5,8 +5,23 @@ typedef struct _DmcIccTransform DmcIccTransform; +// Enum to specify the desired output ICC profile type +typedef enum { + DCM_ICC_OUTPUT_SRGB = 0, // Standard sRGB profile + DCM_ICC_OUTPUT_DISPLAY_P3 = 1, // Display-P3 profile + DCM_ICC_OUTPUT_ADOBE_RGB = 2, // Adobe RGB (1998) profile + DCM_ICC_OUTPUT_ROMM_RGB = 3 // ROMM RGB profile +} DcmIccOutputType; + extern const char *dcm_icc_get_version(void); +extern DmcIccTransform *dcm_icc_transform_create_for_output(const char *icc_profile, + uint32_t icc_profile_size, + uint8_t planar_configuration, + uint16_t columns, + uint16_t rows, + DcmIccOutputType output_type); + extern DmcIccTransform *dcm_icc_transform_create(const char *icc_profile, uint32_t icc_profile_size, uint8_t planar_configuration, diff --git a/wasm/src/ColorManager.hpp b/wasm/src/ColorManager.hpp index 89e9a52..855fcfd 100644 --- a/wasm/src/ColorManager.hpp +++ b/wasm/src/ColorManager.hpp @@ -21,18 +21,20 @@ class ColorManager { /// Constructor /// ColorManager(FrameInfo frameInfo, - const val &iccProfile) { + const val &iccProfile, + int outputType = DCM_ICC_OUTPUT_SRGB) { this->frameInfo = frameInfo; const std::vector iccProfileVector = convertJSArrayToNumberVector(iccProfile); - this->icc_transform = dcm_icc_transform_create((const char *) iccProfileVector.data(), - (uint32_t) iccProfileVector.size(), - this->frameInfo.planarConfiguration, - this->frameInfo.columns, - this->frameInfo.rows); + this->icc_transform = dcm_icc_transform_create_for_output((const char *) iccProfileVector.data(), + (uint32_t) iccProfileVector.size(), + this->frameInfo.planarConfiguration, + this->frameInfo.columns, + this->frameInfo.rows, + static_cast(outputType)); } /// diff --git a/wasm/src/jslib.cpp b/wasm/src/jslib.cpp index 5c3770f..447fab8 100644 --- a/wasm/src/jslib.cpp +++ b/wasm/src/jslib.cpp @@ -3,8 +3,21 @@ #include #include +extern "C" { + #include +} + using namespace emscripten; +EMSCRIPTEN_BINDINGS(DcmIccOutputType) { + enum_("DcmIccOutputType") + .value("SRGB", DCM_ICC_OUTPUT_SRGB) + .value("DISPLAY_P3", DCM_ICC_OUTPUT_DISPLAY_P3) + .value("ADOBE_RGB", DCM_ICC_OUTPUT_ADOBE_RGB) + .value("ROMM_RGB", DCM_ICC_OUTPUT_ROMM_RGB) + ; +} + EMSCRIPTEN_BINDINGS(FrameInfo) { value_object("FrameInfo") .field("columns", &FrameInfo::columns) @@ -17,7 +30,7 @@ EMSCRIPTEN_BINDINGS(FrameInfo) { EMSCRIPTEN_BINDINGS(ColorManager) { class_("ColorManager") - .constructor() + .constructor() .function("getFrameInfo", &ColorManager::getFrameInfo) .function("transform", &ColorManager::transform) ;