Mercurial > hg > orthanc
changeset 6078:c314fccfe8b7
added argument "Encapsulate" to "/tools/create-dicom"
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 04 Apr 2025 15:30:00 +0200 (6 weeks ago) |
parents | 836d8b432ae3 |
children | 4a03333b69f6 |
files | NEWS OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h OrthancServer/Resources/Samples/Python/EncapsulateJPEG.py OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp |
diffstat | 5 files changed, 185 insertions(+), 17 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Fri Apr 04 13:00:58 2025 +0200 +++ b/NEWS Fri Apr 04 15:30:00 2025 +0200 @@ -5,9 +5,11 @@ -------- * API version upgraded to 28 -* GET /studies/../archive and sibbling routes now all accept a 'filename' GET argument. -* POST /studies/../archive and sibbling routes now all accept a 'Filename' query argument. -* GET /instances/../file and sibbling ../attachments/../data routes now all accept a 'filename' GET argument. +* POST /tools/create-dicom accepts new argument "Encapsulate" to encapsulate a raw JPEG + image into a DICOM envelope without transcoding, using 1.2.840.10008.1.2.4.50 transfer syntax. +* GET /studies/../archive and sibbling routes now all accept a "filename" GET argument. +* POST /studies/../archive and sibbling routes now all accept a "Filename" query argument. +* GET /instances/../file and sibbling ../attachments/../data routes now all accept a "filename" GET argument. * All routes accepting a "transcode" url argument or a "Transcode" field in the payload now also accepts a "lossy-quality" url argument or a "LossyQuality" field to define the compression quality factor. If not specified, the "DicomLossyTranscodingQuality" configuration is taken into account. @@ -29,16 +31,16 @@ * Enabled support of the 1.2.840.10008.1.2.1.99 transfer syntax (Deflated Explicit VR Little Endian) in static builds + fix length of saved files. https://discourse.orthanc-server.org/t/transcoding-to-deflated-transfer-syntax-fails/5489 -* When anonymizing a resource while forcing some value with the 'Replace' fields, the tag +* When anonymizing a resource while forcing some value with the "Replace" fields, the tag 0012,0063 was cleared out because the DICOM Anonymization profile was not strictly followed. From now on: - - 0012,0063 will contain "Orthanc {version} - {Anonymization profile}" if no 'Replace' is used. - - 0012,0063 will contain "Orthanc {version}" if 'Replace' is used. + - 0012,0063 will contain "Orthanc {version} - {Anonymization profile}" if no "Replace" is used. + - 0012,0063 will contain "Orthanc {version}" if "Replace" is used. (https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=240) * Housekeeper plugin: - When encountering an error, the housekeeper now skips the resource and continues processing. * Orthanc Explorer: - - Allow '-' and '_' in labels. + - Allow "-" and "_" in labels. * Upgraded dependencies for static builds: - lua 5.4.7
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Fri Apr 04 13:00:58 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Fri Apr 04 15:30:00 2025 +0200 @@ -1312,13 +1312,14 @@ } - void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor) + void ParsedDicomFile::ConfigureTagsForUncompressedImage(unsigned int& bytesPerPixel /* out */, + const ImageAccessor& accessor) { if (accessor.GetFormat() != PixelFormat_Grayscale8 && accessor.GetFormat() != PixelFormat_Grayscale16 && accessor.GetFormat() != PixelFormat_SignedGrayscale16 && accessor.GetFormat() != PixelFormat_RGB24 && - accessor.GetFormat() != PixelFormat_RGBA32 && + accessor.GetFormat() != PixelFormat_RGBA32 && accessor.GetFormat() != PixelFormat_RGBA64) { throw OrthancException(ErrorCode_NotImplemented); @@ -1326,7 +1327,7 @@ InvalidateCache(); - if (accessor.GetFormat() == PixelFormat_RGBA32 || + if (accessor.GetFormat() == PixelFormat_RGBA32 || accessor.GetFormat() == PixelFormat_RGBA64) { LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM"; @@ -1351,7 +1352,7 @@ ReplacePlainString(DICOM_TAG_PIXEL_REPRESENTATION, "0"); // Unsigned pixels } - unsigned int bytesPerPixel = 0; + bytesPerPixel = 0; switch (accessor.GetFormat()) { @@ -1379,7 +1380,7 @@ ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0"); // Color channels are interleaved break; - + case PixelFormat_RGBA64: ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB"); ReplacePlainString(DICOM_TAG_SAMPLES_PER_PIXEL, "3"); @@ -1410,6 +1411,12 @@ } assert(bytesPerPixel != 0); + } + + void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor) + { + unsigned int bytesPerPixel = 0; + ConfigureTagsForUncompressedImage(bytesPerPixel, accessor); DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), DICOM_TAG_PIXEL_DATA.GetElement()); @@ -2232,6 +2239,63 @@ } + void ParsedDicomFile::EncapsulatePixelData(const std::string& dataUriScheme) + { + std::string mime, content; + if (!Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Remove(DICOM_TAG_PIXEL_DATA); + + if (mime == MIME_JPEG) + { +#if ORTHANC_ENABLE_JPEG == 1 + JpegReader reader; + reader.ReadFromMemory(content); + unsigned int bytesPerPixel = 0; + + ConfigureTagsForUncompressedImage(bytesPerPixel, reader); + + if (reader.GetFormat() == PixelFormat_RGB24) + { + ReplacePlainString(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "YBR_FULL_422"); + } + + Uint8* raw = const_cast<Uint8*>(reinterpret_cast<const Uint8*>(content.c_str())); + + DcmOffsetList offsetList; + + std::unique_ptr<DcmPixelSequence> pixelSequence(new DcmPixelSequence(DCM_PixelData)); + + DcmPixelItem* offsetTable = new DcmPixelItem(DCM_PixelItemTag); + if (!pixelSequence->insert(offsetTable).good() || + !pixelSequence->storeCompressedFrame(offsetList, raw, content.size(), 0 /* unlimited fragment size */).good() || + !offsetTable->createOffsetTable(offsetList).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::unique_ptr<DcmPixelData> pixelData(new DcmPixelData(DCM_PixelData)); + pixelData->putOriginalRepresentation(EXS_JPEGProcess1, NULL, pixelSequence.release()); + + if (!GetDcmtkObject().getDataset()->insert(pixelData.release(), true, false).good() || + !GetDcmtkObject().getDataset()->chooseRepresentation(EXS_JPEGProcess1, NULL).good()) + { + throw OrthancException(ErrorCode_InternalError); + } +#else + throw OrthancException(ErrorCode_InternalError, "Orthanc was compiled without support for JPEG"); +#endif + } + else + { + throw OrthancException(ErrorCode_NotImplemented, "Cannot encapsulate pixel data from MIME type: " + mime); + } + } + + #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore void ParsedDicomFile::DatasetToJson(Json::Value& target,
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Fri Apr 04 13:00:58 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Fri Apr 04 15:30:00 2025 +0200 @@ -105,6 +105,9 @@ // the top of DCMTK API DcmFileFormat& GetDcmtkObjectConst() const; + void ConfigureTagsForUncompressedImage(unsigned int& bytesPerPixel /* out */, + const ImageAccessor& accessor); + explicit ParsedDicomFile(DcmFileFormat* dicom); // This takes ownership (no clone) #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 @@ -319,5 +322,7 @@ void RemoveFromPixelData(); ValueRepresentation GuessPixelDataValueRepresentation() const; + + void EncapsulatePixelData(const std::string& dataUriScheme); }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/Samples/Python/EncapsulateJPEG.py Fri Apr 04 15:30:00 2025 +0200 @@ -0,0 +1,72 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2023 Osimis S.A., Belgium +# Copyright (C) 2024-2025 Orthanc Team SRL, Belgium +# Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +# +# This sample Python script illustrates how to encapsulate a JPEG +# image into a DICOM enveloppe, without any transcoding. +# + +import base64 +import json +import requests + + +################## +## Parameters ## +################## + +JPEG = '/tmp/sample.jpg' +URL = 'http://localhost:8042/' +USERNAME = 'orthanc' +PASSWORD = 'orthanc' + +TAGS = { + 'PatientID' : 'Test', + 'PatientName' : 'Hello^World', + 'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.7', # Secondary capture + } + + + +######################################## +## Application of the DICOM-ization ## +######################################## + +with open(JPEG, 'rb') as f: + jpeg = f.read() + +b = base64.b64encode(jpeg) +content = 'data:image/jpeg;base64,%s' % b.decode() + +command = { + 'Content' : content, + 'Tags' : TAGS, + 'Encapsulate' : True, +} + +r = requests.post('%s/tools/create-dicom' % URL, json.dumps(command), + auth = requests.auth.HTTPBasicAuth(USERNAME, PASSWORD)) +r.raise_for_status() + +print('URL of the newly created instance: %s/instances/%s/file' % (URL, r.json() ['ID']))
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Fri Apr 04 13:00:58 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Fri Apr 04 15:30:00 2025 +0200 @@ -59,6 +59,7 @@ static const char* const TAGS = "Tags"; static const char* const TRANSCODE = "Transcode"; static const char* const LOSSY_QUALITY = "LossyQuality"; +static const char* const ENCAPSULATE = "Encapsulate"; // New in Orthanc 1.12.7 @@ -1008,20 +1009,40 @@ // Inject the content (either an image, a PDF file, or a STL/OBJ/MTL file) if (request.isMember(CONTENT)) { + bool encapsulate = false; + if (request.isMember(ENCAPSULATE)) + { + encapsulate = request[ENCAPSULATE].asBool(); + } + const Json::Value& content = request[CONTENT]; if (content.type() == Json::stringValue) { - dicom.EmbedContent(request[CONTENT].asString()); - + if (encapsulate) + { + // New in Orthanc 1.12.7 + dicom.EncapsulatePixelData(request[CONTENT].asString()); + } + else + { + dicom.EmbedContent(request[CONTENT].asString()); + } } else if (content.type() == Json::arrayValue) { if (content.size() > 0) { - // Let's create a series instead of a single instance - CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator, force); - return; + if (encapsulate) + { + throw OrthancException(ErrorCode_NotImplemented); + } + else + { + // Let's create a series instead of a single instance + CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator, force); + return; + } } } else @@ -1066,6 +1087,10 @@ "Avoid the consistency checks for the DICOM tags that enforce the DICOM model of the real-world. " "You can notably use this flag if you need to manually set the tags `StudyInstanceUID`, " "`SeriesInstanceUID`, or `SOPInstanceUID`. Be careful with this feature.", false) + .SetRequestField(ENCAPSULATE, RestApiCallDocumentation::Type_Boolean, + "If set to `true`, encapsulate the binary data of `ContentData` as such, using a compressed transfer syntax. " + "Only applicable if `ContentData` contains a grayscale or color JPEG image in 8bpp, " + "in which case the transfer syntax is set to \"1.2.840.10008.1.2.4.50\". (new in Orthanc 1.12.7)", false) .SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Orthanc identifier of the newly created instance") .SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the instance in the REST API"); return;