# HG changeset patch # User Sebastien Jodogne # Date 1440076693 -7200 # Node ID d6a93e12b1c16d5a878262b559c06888ee91331d # Parent 89ab71a68fcf79b80a6dec91c847c3b74bd12745 Creation of DICOM files with encapsulated PDF diff -r 89ab71a68fcf -r d6a93e12b1c1 Core/DicomFormat/DicomTag.h --- a/Core/DicomFormat/DicomTag.h Thu Aug 20 11:56:42 2015 +0200 +++ b/Core/DicomFormat/DicomTag.h Thu Aug 20 15:18:13 2015 +0200 @@ -106,11 +106,7 @@ static const DicomTag DICOM_TAG_NUMBER_OF_FRAMES(0x0028, 0x0008); static const DicomTag DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES(0x0018, 0x1090); static const DicomTag DICOM_TAG_IMAGES_IN_ACQUISITION(0x0020, 0x1002); - static const DicomTag DICOM_TAG_PATIENT_NAME(0x0010, 0x0010); - static const DicomTag DICOM_TAG_STUDY_DATE(0x0008, 0x0020); - static const DicomTag DICOM_TAG_SERIES_DATE(0x0008, 0x0021); - static const DicomTag DICOM_TAG_PATIENT_BIRTH_DATE(0x0010, 0x0030); // The following is used for "modify/anonymize" operations static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016); @@ -137,4 +133,18 @@ static const DicomTag DICOM_TAG_PIXEL_REPRESENTATION(0x0028, 0x0103); static const DicomTag DICOM_TAG_PLANAR_CONFIGURATION(0x0028, 0x0006); static const DicomTag DICOM_TAG_PHOTOMETRIC_INTERPRETATION(0x0028, 0x0004); + + // Tags related to date and time + static const DicomTag DICOM_TAG_ACQUISITION_DATE(0x0008, 0x0022); + static const DicomTag DICOM_TAG_ACQUISITION_TIME(0x0008, 0x0032); + static const DicomTag DICOM_TAG_CONTENT_DATE(0x0008, 0x0023); + static const DicomTag DICOM_TAG_CONTENT_TIME(0x0008, 0x0033); + static const DicomTag DICOM_TAG_INSTANCE_CREATION_DATE(0x0008, 0x0012); + static const DicomTag DICOM_TAG_INSTANCE_CREATION_TIME(0x0008, 0x0013); + static const DicomTag DICOM_TAG_PATIENT_BIRTH_DATE(0x0010, 0x0030); + static const DicomTag DICOM_TAG_PATIENT_BIRTH_TIME(0x0010, 0x0032); + static const DicomTag DICOM_TAG_SERIES_DATE(0x0008, 0x0021); + static const DicomTag DICOM_TAG_SERIES_TIME(0x0008, 0x0031); + static const DicomTag DICOM_TAG_STUDY_DATE(0x0008, 0x0020); + static const DicomTag DICOM_TAG_STUDY_TIME(0x0008, 0x0030); } diff -r 89ab71a68fcf -r d6a93e12b1c1 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/Core/Enumerations.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -691,4 +691,61 @@ return ""; } } + + + ResourceType GetChildResourceType(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return ResourceType_Study; + + case ResourceType_Study: + return ResourceType_Series; + + case ResourceType_Series: + return ResourceType_Instance; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + ResourceType GetParentResourceType(ResourceType type) + { + switch (type) + { + case ResourceType_Study: + return ResourceType_Patient; + + case ResourceType_Series: + return ResourceType_Study; + + case ResourceType_Instance: + return ResourceType_Series; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + DicomModule GetModule(ResourceType type) + { + switch (type) + { + case ResourceType_Patient: + return DicomModule_Patient; + + case ResourceType_Study: + return DicomModule_Study; + + case ResourceType_Series: + return DicomModule_Series; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } } diff -r 89ab71a68fcf -r d6a93e12b1c1 Core/Enumerations.h --- a/Core/Enumerations.h Thu Aug 20 11:56:42 2015 +0200 +++ b/Core/Enumerations.h Thu Aug 20 15:18:13 2015 +0200 @@ -380,4 +380,10 @@ const char* GetMimeType(FileContentType type); const char* GetFileExtension(FileContentType type); + + ResourceType GetChildResourceType(ResourceType type); + + ResourceType GetParentResourceType(ResourceType type); + + DicomModule GetModule(ResourceType type); } diff -r 89ab71a68fcf -r d6a93e12b1c1 Core/Toolbox.cpp --- a/Core/Toolbox.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/Core/Toolbox.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -776,6 +776,21 @@ boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); return boost::posix_time::to_iso_string(now); } + + void Toolbox::GetNowDicom(std::string& date, + std::string& time) + { + boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); + tm tm = boost::posix_time::to_tm(now); + + char s[32]; + sprintf(s, "%04d%02d%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); + date.assign(s); + + // TODO milliseconds + sprintf(s, "%02d%02d%02d.%06d", tm.tm_hour, tm.tm_min, tm.tm_sec, 0); + time.assign(s); + } #endif diff -r 89ab71a68fcf -r d6a93e12b1c1 Core/Toolbox.h --- a/Core/Toolbox.h Thu Aug 20 11:56:42 2015 +0200 +++ b/Core/Toolbox.h Thu Aug 20 15:18:13 2015 +0200 @@ -128,6 +128,9 @@ #if BOOST_HAS_DATE_TIME == 1 std::string GetNowIsoString(); + + void GetNowDicom(std::string& date, + std::string& time); #endif // In-place percent-decoding for URL diff -r 89ab71a68fcf -r d6a93e12b1c1 NEWS --- a/NEWS Thu Aug 20 11:56:42 2015 +0200 +++ b/NEWS Thu Aug 20 15:18:13 2015 +0200 @@ -1,6 +1,8 @@ Pending changes in the mainline =============================== + +* Creation of DICOM files with encapsulated PDF through "/tools/create-dicom" * "limit" and "since" arguments while retrieving DICOM resources in the REST API * Support of "deflate" and "gzip" content-types in HTTP requests * Options to validate peers against CA certificates in HTTPS requests diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -428,46 +428,256 @@ } - static void CreateDicom(RestApiPostCall& call) + static bool CreateDicomV1(ParsedDicomFile& dicom, + const Json::Value& request) { // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}' // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}' - Json::Value replacements; - if (call.ParseJsonRequest(replacements) && replacements.isObject()) + assert(request.isObject()); + LOG(WARNING) << "Using a deprecated call to /tools/create-dicom"; + + Json::Value::Members members = request.getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + if (request[name].type() != Json::stringValue) + { + LOG(ERROR) << "Only string values are supported when creating DICOM instances"; + return false; + } + + std::string value = request[name].asString(); + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + if (tag == DICOM_TAG_PIXEL_DATA) + { + dicom.EmbedImage(value); + } + else + { + dicom.Replace(tag, value); + } + } + + return true; + } + + + static bool CreateDicomV2(ParsedDicomFile& dicom, + ServerContext& context, + const Json::Value& request) + { + assert(request.isObject()); + + if (!request.isMember("Tags") || + request["Tags"].type() != Json::objectValue) + { + return false; + } + + ResourceType parentType = ResourceType_Instance; + + if (request.isMember("Parent")) + { + // Locate the parent tags + std::string parent = request["Parent"].asString(); + if (!context.GetIndex().LookupResourceType(parentType, parent)) + { + LOG(ERROR) << "Trying to attach a new DICOM instance to an inexistent resource: " << parent; + return false; + } + + if (parentType == ResourceType_Instance) + { + LOG(ERROR) << "Trying to attach a new DICOM instance to an instance (must be a series, study or patient): " << parent; + return false; + } + + // Select one existing child instance of the parent resource, to + // retrieve all its tags + Json::Value siblingTags; + + { + // Retrieve all the instances of the parent resource + std::list siblingInstances; + context.GetIndex().GetChildInstances(siblingInstances, parent); + + if (siblingInstances.empty()) + { + return false; // Error: No instance (should never happen) + } + + context.ReadJson(siblingTags, siblingInstances.front()); + } + + // Retrieve the tags for all the parent modules + typedef std::set ModuleTags; + ModuleTags moduleTags; + + ResourceType type = parentType; + for (;;) + { + DicomTag::AddTagsForModule(moduleTags, GetModule(type)); + + if (type == ResourceType_Patient) + { + break; // We're done + } + + // Go up + std::string tmp; + if (!context.GetIndex().LookupParent(tmp, parent)) + { + return false; + } + + parent = tmp; + type = GetParentResourceType(type); + } + + for (ModuleTags::const_iterator it = moduleTags.begin(); + it != moduleTags.end(); it++) + { + std::string t = it->Format(); + if (siblingTags.isMember(t)) + { + const Json::Value& tag = siblingTags[t]; + if (tag["Type"] == "Null") + { + dicom.Replace(*it, ""); + } + else if (tag["Type"] == "String") + { + dicom.Replace(*it, tag["Value"].asString()); + } + } + } + } + + + // Inject time-related information + std::string date, time; + Toolbox::GetNowDicom(date, time); + dicom.Replace(DICOM_TAG_ACQUISITION_DATE, date); + dicom.Replace(DICOM_TAG_ACQUISITION_TIME, time); + dicom.Replace(DICOM_TAG_CONTENT_DATE, date); + dicom.Replace(DICOM_TAG_CONTENT_TIME, time); + dicom.Replace(DICOM_TAG_INSTANCE_CREATION_DATE, date); + dicom.Replace(DICOM_TAG_INSTANCE_CREATION_TIME, time); + + if (parentType == ResourceType_Patient || + parentType == ResourceType_Study || + parentType == ResourceType_Instance /* no parent */) + { + dicom.Replace(DICOM_TAG_SERIES_DATE, date); + dicom.Replace(DICOM_TAG_SERIES_TIME, time); + } + + if (parentType == ResourceType_Patient || + parentType == ResourceType_Instance /* no parent */) + { + dicom.Replace(DICOM_TAG_STUDY_DATE, date); + dicom.Replace(DICOM_TAG_STUDY_TIME, time); + } + + + // Inject the user-specified tags + Json::Value::Members members = request["Tags"].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + const std::string& name = members[i]; + if (request["Tags"][name].type() != Json::stringValue) + { + LOG(ERROR) << "Only string values are supported when creating DICOM instances"; + return false; + } + + std::string value = request["Tags"][name].asString(); + + DicomTag tag = FromDcmtkBridge::ParseTag(name); + if (dicom.HasTag(tag)) + { + LOG(ERROR) << "Trying to override a value inherited from a parent module"; + return false; + } + + if (tag == DICOM_TAG_PIXEL_DATA) + { + LOG(ERROR) << "Use \"Content\" to inject an image into a new DICOM instance"; + return false; + } + else + { + dicom.Replace(tag, value); + } + } + + + // Inject the content (either an image, or a PDF file) + if (request.isMember("Content")) + { + if (request["Content"].type() != Json::stringValue) + { + LOG(ERROR) << "The payload of the DICOM instance must be specified according to Data URI scheme"; + return false; + } + + std::string mime, base64; + Toolbox::DecodeDataUriScheme(mime, base64, request["Content"].asString()); + Toolbox::ToLowerCase(mime); + + std::string content; + Toolbox::DecodeBase64(content, base64); + + if (mime == "image/png") + { + dicom.EmbedImage(mime, content); + } + else if (mime == "application/pdf") + { + dicom.EmbedPdf(content); + } + else + { + LOG(ERROR) << "Unsupported MIME type for the content of a new DICOM file"; + return false; + } + } + + + return true; + } + + + static void CreateDicom(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value request; + if (call.ParseJsonRequest(request) && + request.isObject()) { ParsedDicomFile dicom; - Json::Value::Members members = replacements.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) + if (request.isMember("Tags") ? + CreateDicomV2(dicom, context, request) : + CreateDicomV1(dicom, request)) { - const std::string& name = members[i]; - std::string value = replacements[name].asString(); + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(dicom); - DicomTag tag = FromDcmtkBridge::ParseTag(name); - if (tag == DICOM_TAG_PIXEL_DATA) + std::string id; + StoreStatus status = OrthancRestApi::GetContext(call).Store(id, toStore); + + if (status == StoreStatus_Failure) { - dicom.EmbedImage(value); - } - else - { - dicom.Replace(tag, value); + LOG(ERROR) << "Error while storing a manually-created instance"; + return; } + + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, id, status); } - - DicomInstanceToStore toStore; - toStore.SetParsedDicomFile(dicom); - - std::string id; - StoreStatus status = OrthancRestApi::GetContext(call).Store(id, toStore); - - if (status == StoreStatus_Failure) - { - LOG(ERROR) << "Error while storing a manually-created instance"; - return; - } - - OrthancRestApi::GetApi(call).AnswerStoredInstance(call, id, status); } } diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -956,23 +956,7 @@ b.splice(b.begin(), c); } - switch (type) - { - case ResourceType_Patient: - type = ResourceType_Study; - break; - - case ResourceType_Study: - type = ResourceType_Series; - break; - - case ResourceType_Series: - type = ResourceType_Instance; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } + type = GetChildResourceType(type); a.clear(); a.splice(a.begin(), b); @@ -1053,13 +1037,7 @@ } current = parent; - switch (currentType) - { - case ResourceType_Instance: currentType = ResourceType_Series; break; - case ResourceType_Series: currentType = ResourceType_Study; break; - case ResourceType_Study: currentType = ResourceType_Patient; break; - default: throw OrthancException(ErrorCode_InternalError); - } + currentType = GetParentResourceType(currentType); } assert(currentType == end); diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/ParsedDicomFile.cpp --- a/OrthancServer/ParsedDicomFile.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/ParsedDicomFile.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -107,6 +107,7 @@ #include #include #include +#include #include #include @@ -1133,16 +1134,23 @@ void ParsedDicomFile::EmbedImage(const std::string& dataUriScheme) { - std::string mime, content; - Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme); + std::string mime, base64; + Toolbox::DecodeDataUriScheme(mime, base64, dataUriScheme); + + std::string content; + Toolbox::DecodeBase64(content, base64); - std::string decoded; - Toolbox::DecodeBase64(decoded, content); + EmbedImage(mime, content); + } + + void ParsedDicomFile::EmbedImage(const std::string& mime, + const std::string& content) + { if (mime == "image/png") { PngReader reader; - reader.ReadFromMemory(decoded); + reader.ReadFromMemory(content); EmbedImage(reader); } else @@ -1408,4 +1416,59 @@ FromDcmtkBridge::ToJson(target, *pimpl_->file_->getDataset()); } } + + + bool ParsedDicomFile::HasTag(const DicomTag& tag) const + { + DcmTag key(tag.GetGroup(), tag.GetElement()); + return pimpl_->file_->getDataset()->tagExists(key); + } + + + void ParsedDicomFile::EmbedPdf(const std::string& pdf) + { + if (pdf.size() < 5 || // (*) + strncmp("%PDF-", pdf.c_str(), 5) != 0) + { + LOG(ERROR) << "Not a PDF file"; + throw OrthancException(ErrorCode_BadFileFormat); + } + + Replace(DICOM_TAG_SOP_CLASS_UID, UID_EncapsulatedPDFStorage); + Replace(FromDcmtkBridge::Convert(DCM_Modality), "OT"); + Replace(FromDcmtkBridge::Convert(DCM_ConversionType), "WSD"); + Replace(FromDcmtkBridge::Convert(DCM_MIMETypeOfEncapsulatedDocument), "application/pdf"); + Replace(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1"); + + std::auto_ptr element(new DcmPolymorphOBOW(DCM_EncapsulatedDocument)); + + size_t s = pdf.size(); + if (s & 1) + { + // The size of the buffer must be even + s += 1; + } + + Uint8* bytes = NULL; + OFCondition result = element->createUint8Array(s, bytes); + if (!result.good() || bytes == NULL) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + // Blank pad byte (no access violation, as "pdf.size() >= 5" because of (*) ) + bytes[s - 1] = 0; + + memcpy(bytes, pdf.c_str(), pdf.size()); + + DcmPolymorphOBOW* obj = element.release(); + result = pimpl_->file_->getDataset()->insert(obj); + + if (!result.good()) + { + delete obj; + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + } diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/ParsedDicomFile.h --- a/OrthancServer/ParsedDicomFile.h Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/ParsedDicomFile.h Thu Aug 20 15:18:13 2015 +0200 @@ -104,6 +104,9 @@ void EmbedImage(const std::string& dataUriScheme); + void EmbedImage(const std::string& mime, + const std::string& content); + void ExtractImage(ImageBuffer& result, unsigned int frame); @@ -121,6 +124,10 @@ void ToJson(Json::Value& target, bool simplify); + + bool HasTag(const DicomTag& tag) const; + + void EmbedPdf(const std::string& pdf); }; } diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -243,44 +243,6 @@ } - ResourceType GetParentResourceType(ResourceType type) - { - switch (type) - { - case ResourceType_Study: - return ResourceType_Patient; - - case ResourceType_Series: - return ResourceType_Study; - - case ResourceType_Instance: - return ResourceType_Series; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - - ResourceType GetChildResourceType(ResourceType type) - { - switch (type) - { - case ResourceType_Patient: - return ResourceType_Study; - - case ResourceType_Study: - return ResourceType_Series; - - case ResourceType_Series: - return ResourceType_Instance; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - const char* EnumerationToString(ModalityManufacturer manufacturer) { switch (manufacturer) diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/ServerEnumerations.h Thu Aug 20 15:18:13 2015 +0200 @@ -187,8 +187,4 @@ const char* EnumerationToString(TransferSyntax syntax); ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer); - - ResourceType GetParentResourceType(ResourceType type); - - ResourceType GetChildResourceType(ResourceType type); } diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/ServerIndex.cpp Thu Aug 20 15:18:13 2015 +0200 @@ -2097,4 +2097,15 @@ return true; } } + + + bool ServerIndex::LookupResourceType(ResourceType& type, + const std::string& publicId) + { + boost::mutex::scoped_lock lock(mutex_); + + int64_t id; + return db_.LookupResource(id, type, publicId); + } + } diff -r 89ab71a68fcf -r d6a93e12b1c1 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Thu Aug 20 11:56:42 2015 +0200 +++ b/OrthancServer/ServerIndex.h Thu Aug 20 15:18:13 2015 +0200 @@ -264,5 +264,8 @@ bool GetMainDicomTags(DicomMap& result, const std::string& publicId, ResourceType expectedType); + + bool LookupResourceType(ResourceType& type, + const std::string& publicId); }; }